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

> Implement WiFi station connectivity in Zig with ESP-IDF. This guide details initializing NVS, setting up FreeRTOS events, configuring the WiFi driver, and handling connection events for success or failure.

- Repository: [Matheus C. França/zig-esp-idf-sample](https://github.com/kassane/zig-esp-idf-sample)
- Tags: how-to-guide
- Published: 2026-03-05

---

**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`](https://github.com/kassane/zig-esp-idf-sample/blob/main/main/examples/wifi-station.zig#L41-L45) |
| **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`](https://github.com/kassane/zig-esp-idf-sample/blob/main/main/examples/wifi-station.zig#L46-L48) |
| **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`](https://github.com/kassane/zig-esp-idf-sample/blob/main/main/examples/wifi-station.zig#L50-L56) |
| **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`](https://github.com/kassane/zig-esp-idf-sample/blob/main/main/examples/wifi-station.zig#L57-L72) |
| **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`](https://github.com/kassane/zig-esp-idf-sample/blob/main/main/examples/wifi-station.zig#L99-L118) |
| **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`](https://github.com/kassane/zig-esp-idf-sample/blob/main/main/examples/wifi-station.zig#L120-L136) |

## 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()`:

```zig
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`](https://github.com/kassane/zig-esp-idf-sample/blob/main/main/imports/wifi.zig#L62-L64).

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.

```zig
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:

```zig
// 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):

```zig
// 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:

```zig
// 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:

```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](https://github.com/kassane/zig-esp-idf-sample/blob/main/main/examples/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](https://github.com/kassane/zig-esp-idf-sample/blob/main/main/imports/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](https://github.com/kassane/zig-esp-idf-sample/blob/main/main/imports/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](https://github.com/kassane/zig-esp-idf-sample/blob/main/main/patches/wifi/wifi_sta_config_t.zig) • [wifi_ap_config_t.zig](https://github.com/kassane/zig-esp-idf-sample/blob/main/main/patches/wifi/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](https://github.com/kassane/zig-esp-idf-sample/blob/main/main/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`:

```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.