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()andesp_event_loop_create_default(), then create the default WiFi station interface. - Configure the driver using
idf.wifi.init_config_default()andesp_wifi_init(), then set the mode toWIFI_MODE_STA. - Register event handlers for
WIFI_EVENT(to triggeresp_wifi_connect()) andIP_EVENT_STA_GOT_IP(to signal successful IP acquisition). - Apply configuration by filling the
wifi_config_tstruct with SSID and password, then callingesp_wifi_set_config(). - Start and wait by calling
esp_wifi_start()and blocking onxEventGroupWaitBits()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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →