Implementing WiFi Connectivity in Zig with ESP-IDF: A Complete Guide

You can implement WiFi station connectivity in Zig by initializing the NVS flash, creating a FreeRTOS event group, configuring the WiFi driver with esp_wifi_init(), registering event handlers for WIFI_EVENT and IP_EVENT, and blocking on xEventGroupWaitBits() until the connection succeeds or fails.

The kassane/zig-esp-idf-sample repository demonstrates how to leverage Zig’s compile-time features and error-handling ergonomics while using the ESP-IDF’s native WiFi driver. This guide walks through the architecture, key source files, and a complete implementation for connecting an ESP32-class chip to a WiFi network using pure Zig.

Architecture Overview

The WiFi station implementation follows a layered architecture that separates system initialization, driver configuration, and event-driven connection management. The following table maps each layer to its implementation in the repository:

Layer Description Key Source
System setup Initializes NVS (required for WiFi calibration), creates the default network interface and the default event loop. main/examples/wifi-station.zig:41-45
WiFi driver init Calls esp_wifi_init() with a default configuration generated by idf.wifi.init_config_default(). main/examples/wifi-station.zig:46-48
Event handling Registers two event-handler instances – one for WiFi events (WIFI_EVENT) and one for IP events (IP_EVENT_STA_GOT_IP). The handlers update a FreeRTOS event-group that the main task waits on. main/examples/wifi-station.zig:50-56
Configuration Builds a wifi_config_t structure with SSID, password and optional WPA-3 settings, then applies it with esp_wifi_set_mode() and esp_wifi_set_config(). main/examples/wifi-station.zig:57-72
Connection flow The onWifiEvent handler launches esp_wifi_connect() when the station starts, retries on disconnect, and signals success/failure via the event-group. The main task blocks on xEventGroupWaitBits() until it receives either the connected or failed bit. main/examples/wifi-station.zig:99-118
IP acquisition When the IP_EVENT_STA_GOT_IP event arrives, the IP address is extracted from ip_event_got_ip_t and logged; the connected bit is set. main/examples/wifi-station.zig:120-136

Key Implementation Files and API Wrappers

The repository provides idiomatic Zig wrappers around the ESP-IDF C API, located in main/imports/wifi.zig. These wrappers expose Zig-friendly error handling (!void) and map ESP-IDF enums to Zig enums.

For example, the setMode() function wraps esp_wifi_set_mode():

pub fn setMode(mode: wifi_mode_t) !void {
    return try errors.espCheckError(sys.esp_wifi_set_mode(@intFromEnum(mode)));
}

See main/imports/wifi.zig:62-64.

For applications using the remote driver model (where the WiFi stack runs on a different CPU core), the repository also provides main/imports/wifi_remote.zig.

Step-by-Step WiFi Station Implementation

Initializing NVS and Network Stack

Before initializing WiFi, you must initialize the NVS flash and create the default event loop. This is required for WiFi calibration and event handling.

const idf = @import("esp_idf");
const sys = idf.sys;

// Initialize NVS (erase if no free pages or new version)
try idf.nvs.flashInitOrErase();

// Create default event loop
try idf.event.loopCreateDefault();

// Initialize TCP/IP stack
try idf.err.espCheckError(sys.esp_netif_init());

Configuring the WiFi Driver

Create the default WiFi station interface and initialize the driver with default configuration:

// Create default WiFi station interface
_ = sys.esp_netif_create_default_wifi_sta();

// Initialize WiFi with default config
var cfg = idf.wifi.init_config_default();
try idf.err.espCheckError(sys.esp_wifi_init(&cfg));

Setting Up Event Handlers

Register callbacks for WiFi events (connection status) and IP events (address assignment):

// Register WiFi event handler
const wifi_inst = try idf.event.handlerInstanceRegister(
    sys.WIFI_EVENT, 
    idf.event.ANY_ID, 
    &onWifiEvent, 
    null
);

// Register IP event handler
const ip_inst = try idf.event.handlerInstanceRegister(
    sys.IP_EVENT, 
    sys.IP_EVENT_STA_GOT_IP, 
    &onIpEvent, 
    null
);

Connecting and Waiting for IP

Configure the station with your SSID and password, then start the connection process:

// Build configuration
var wifi_cfg = idf.wifi.wifiConfig{
    .sta = .{
        .ssid = std.mem.zeroes([32]u8),
        .password = std.mem.zeroes([64]u8),
        .threshold = .{ .authmode = sys.WIFI_AUTH_WPA2_PSK },
    },
};

// Copy credentials (helper function needed)
copyZ(&wifi_cfg.sta.ssid, "YOUR_SSID");
copyZ(&wifi_cfg.sta.password, "YOUR_PASSWORD");

// Apply configuration
try idf.wifi.setMode(.WIFI_MODE_STA);
try idf.wifi.setConfig(.WIFI_IF_STA, &wifi_cfg);
try idf.wifi.start();

Complete Minimal Example

Here is a complete, copy-pasteable example that demonstrates the full WiFi station implementation in Zig:

const std = @import("std");
const idf = @import("esp_idf");
const sys = idf.sys;
const log = std.log.scoped(.wifi);

export fn app_main() callconv(.c) void {
    // 1️⃣ NVS (required)
    idf.nvs.flashInitOrErase() catch |e| {
        log.err("NVS init failed: {}", .{@errorName(e)});
        return;
    };

    // 2️⃣ Event group for synchronization
    var ev_group = sys.xEventGroupCreate() orelse return error.EventGroupCreateFailed;
    defer sys.vEventGroupDelete(ev_group);

    // 3️⃣ Init TCP/IP stack & default event loop
    try idf.err.espCheckError(sys.esp_netif_init());
    try idf.event.loopCreateDefault();

    // 4️⃣ Default WiFi interface
    _ = sys.esp_netif_create_default_wifi_sta();

    // 5️⃣ Initialise WiFi driver
    var cfg = idf.wifi.init_config_default();
    try idf.err.espCheckError(sys.esp_wifi_init(&cfg));

    // 6️⃣ Register events (WiFi & IP)
    const wifi_inst = try idf.event.handlerInstanceRegister(
        sys.WIFI_EVENT, idf.event.ANY_ID, &onWifiEvent, null);
    defer idf.event.handlerInstanceUnregister(sys.WIFI_EVENT, idf.event.ANY_ID, wifi_inst) catch {};

    const ip_inst = try idf.event.handlerInstanceRegister(
        sys.IP_EVENT, sys.IP_EVENT_STA_GOT_IP, &onIpEvent, null);
    defer idf.event.handlerInstanceUnregister(sys.IP_EVENT, sys.IP_EVENT_STA_GOT_IP, ip_inst) catch {};

    // 7️⃣ Build station config
    var wifi_cfg = idf.wifi.wifiConfig{
        .sta = .{
            .ssid = std.mem.zeroes([32]u8),
            .password = std.mem.zeroes([64]u8),
            .threshold = .{ .authmode = sys.WIFI_AUTH_WPA2_PSK },
        },
    };
    copyZ(&wifi_cfg.sta.ssid, sys.CONFIG_ESP_WIFI_SSID);
    copyZ(&wifi_cfg.sta.password, sys.CONFIG_ESP_WIFI_PASSWORD);

    // 8️⃣ Apply config and start
    try idf.wifi.setMode(.WIFI_MODE_STA);
    try idf.wifi.setConfig(.WIFI_IF_STA, &wifi_cfg);
    try idf.wifi.start();

    // 9️⃣ Wait for connection (or failure)
    const bits = sys.xEventGroupWaitBits(
        ev_group,
        sys.WIFI_CONNECTED_BIT | sys.WIFI_FAIL_BIT,
        0,
        0,
        sys.portMAX_DELAY,
    );
    if ((bits & sys.WIFI_CONNECTED_BIT) != 0) {
        log.info("✅ Connected to SSID: {}", .{sys.CONFIG_ESP_WIFI_SSID});
    } else {
        log.err("❌ Connection failed after retries");
    }
}

// Helper to copy a C-string into a fixed-size Zig buffer (null-terminated)
fn copyZ(dst: []u8, src: []const u8) void {
    const n = @min(dst.len - 1, src.len);
    @memcpy(dst[0..n], src[0..n]);
    dst[n] = 0;
}

// ---------------------------------------------------------------------
// Event callbacks (kept tiny – real logic lives in the main task)
// ---------------------------------------------------------------------
export fn onWifiEvent(_: ?*anyopaque, _: sys.esp_event_base_t, id: i32, _: ?*anyopaque) callconv(.c) void {
    switch (id) {
        sys.WIFI_EVENT_STA_START => idf.wifi.connect() catch |e| log.err("connect error: {}", .{@errorName(e)}),
        sys.WIFI_EVENT_STA_DISCONNECTED => idf.wifi.connect() catch {},
        else => {},
    }
}

export fn onIpEvent(_: ?*anyopaque, _: sys.esp_event_base_t, id: i32, data: ?*anyopaque) callconv(.c) void {
    if (id == sys.IP_EVENT_STA_GOT_IP) {
        const ev = @as(*sys.ip_event_got_ip_t, @ptrCast(@alignCast(data)));
        const ip = ev.ip_info.ip.addr;
        log.info("Got IP: {}.{}.{}.{}", .{
            @as(u8, @truncate(ip)),
            @as(u8, @truncate(ip >> 8)),
            @as(u8, @truncate(ip >> 16)),
            @as(u8, @truncate(ip >> 24)),
        });
        _ = sys.xEventGroupSetBits(g_event_group, sys.WIFI_CONNECTED_BIT);
    }
}

All symbols (idf.wifi.*, sys.WIFI_*, idf.event.*) are defined in the repository – see the files listed below.

Key Source Files in zig-esp-idf-sample

Understanding the repository structure helps you navigate the Zig wrappers and ESP-IDF bindings:

File Role Link
main/examples/wifi-station.zig Full example that ties together NVS, event loop, WiFi init, config, and connection handling. wifi-station.zig
main/imports/wifi.zig Zig wrapper around the core ESP-IDF WiFi C API (esp_wifi_*). Provides idiomatic Zig error handling and enum conversions. wifi.zig
main/imports/wifi_remote.zig Wrapper for the remote WiFi driver (useful when the WiFi stack runs on a different CPU core). wifi_remote.zig
main/patches/wifi/wifi_sta_config_t.zig & wifi_ap_config_t.zig Zig representations of the wifi_sta_config_t and wifi_ap_config_t structs used when building the config object. wifi_sta_config_t.zig • wifi_ap_config_t.zig
main/app.zig Demonstrates the general Zig-ESP-IDF project skeleton, including allocator setup and FreeRTOS task creation (useful for extending the WiFi example). app.zig

Using the Remote WiFi Driver

If your application runs on a separate core or uses ESP-IDF’s remote driver model, you can replace the local API with the remote version found in main/imports/wifi_remote.zig:

const wifi = @import("wifi_remote");

// Initialise remote driver
try wifi.init(&idf.wifi.init_config_default());

// Set station mode & start
try wifi.setMode(.WIFI_MODE_STA);
try wifi.start();

This wrapper maintains the same Zig-idiomatic error handling while targeting the remote driver interface.

Summary

Implementing WiFi connectivity in Zig with ESP-IDF requires orchestrating several initialization steps before the station can connect:

  • Initialize NVS flash using idf.nvs.flashInitOrErase() to provide the calibration storage required by the WiFi driver.
  • Create synchronization primitives such as a FreeRTOS event group to block the main task until connection success or failure.
  • Initialize the network stack with esp_netif_init() and esp_event_loop_create_default(), then create the default WiFi station interface.
  • Configure the driver using idf.wifi.init_config_default() and esp_wifi_init(), then set the mode to WIFI_MODE_STA.
  • Register event handlers for WIFI_EVENT (to trigger esp_wifi_connect()) and IP_EVENT_STA_GOT_IP (to signal successful IP acquisition).
  • Apply configuration by filling the wifi_config_t struct with SSID and password, then calling esp_wifi_set_config().
  • Start and wait by calling esp_wifi_start() and blocking on xEventGroupWaitBits() for the connection bits.

Frequently Asked Questions

How does Zig error handling work with ESP-IDF WiFi functions?

The main/imports/wifi.zig file wraps raw ESP-IDF C functions with Zig error handling. For example, setMode() converts the ESP-IDF esp_err_t return code into a Zig error union (!void) using errors.espCheckError(). This allows you to use try and catch syntax instead of manually checking error codes, making the code more idiomatic and safer.

What is the purpose of the NVS initialization before WiFi setup?

The ESP-IDF WiFi driver requires Non-Volatile Storage (NVS) to store calibration data, PHY settings, and other persistent information. According to the implementation in main/examples/wifi-station.zig, you must call idf.nvs.flashInitOrErase() before any WiFi operations. Without this initialization, esp_wifi_init() will return an error, and the driver will fail to start.

How does the connection retry mechanism work in the Zig implementation?

The sample implements a retry counter (g_retry_count) capped at MAX_RETRY_ATTEMPTS (typically 5). When a WIFI_EVENT_STA_DISCONNECTED event occurs, the onWifiEvent callback in main/examples/wifi-station.zig increments the counter and calls esp_wifi_connect() again. If the counter exceeds the maximum, it sets the WIFI_FAIL_BIT in the event group, causing the main task to exit with an error rather than retrying indefinitely.

Can I use WPA3 security with this Zig WiFi implementation?

Yes. The wifi_config_t struct in main/patches/wifi/wifi_sta_config_t.zig includes the sae_pwe_h2e field for WPA3 Simultaneous Authentication of Equals (SAE) configuration. You can set this to WPA3_SAE_PWE_BOTH when building the configuration struct, allowing the station to connect to WPA3-enabled access points while maintaining backward compatibility with WPA2 networks.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →