Can I add Wifi to the ESP32C6 project with Swift?

This article is part of a series.

I knew this was coming. Having to create a separate component. I tried to get Wifi working in the demo project without creating a bridge library but I couldn’t quite get the mismatch between what C elements Swift will compile inline and the C elements the SDK uses. (looking at you function style macros)

I am going to put out there right now that there could be a better version of this code. I am not using all the C import features in Swift to minimize what’s in the bridging library. Nor am I using the C decorators that make things safer from WWDC25. APINotes make no appearance at all. Those are all niceties for a future version. This is all about Wifi Hello World.

Ultimately my solution is still a whole lot of C wrapped in a thin Swift shell, but it’s somewhere to start. It’s also just a concrete type. No “WifiProvider” protocol for now.

Useful References:

How to write a Test Component

So there’s a whole page on how to write a test component, but I felt it left out some key details (or at least phrased them in away I didn’t grok) that I filled in just by looking at someone elses demo.

Here is how I got my test component working with the Swift base. I did not have to touch the CMakeLists.txt to add the library, although I could have. I relied on the esp-idf searching that in-project components folder for free. Didn’t even have to add the dependency to /main/idf_component.yml.

cd $CURRENT_PROJECT_DIR
mkdir -p components
cd components
idf.py create-component test_cmp

Then I updated the files to the following:

/components/test_cmp/test_cmp.c

#include <stdio.h>
#include "test_cmp.h"


// One of my go to canary functions.
int test_cmp_return_twelve(void)
{
    return 12;
}

/components/test_cmp/include/test_cmp.h

int test_cmp__return_twelve(void);

/components/test_cmp/CMakeLists.txt

left unchanged.

idf_component_register(SRCS "test_cmp.c"
                    INCLUDE_DIRS "include")

/main/BridgingHeader.h

//existing items....

#include "test_cmp.h"

/main/Main.swift

@_cdecl("app_main")
func main() {
  //before...

  print(test_cmp_return_twelve())

  //..etc
  while true {
    //etc.
  }
}

Wifi Component In the Project

So I started with a simple version, but it was having trouble. What didn’t occur to me until much later was that the symptoms I was seeing lined up with the code reacting poorly with the USB startup processes. The esp-idf driven code (sdk and monitor) handles that all so automagically. (print just works! With no Serial.begin or anything!), it just wasn’t on my radar to factor that in until todbot mentioned it.

I made two other working but more complicated versions to try to troubleshoot that can be seen in the git history:

Those both work, but since this is all for demo code more than a targeting having a robust WiFi library, I really wanted to make the simple iteration happen.

The code current to this post works by adding delays in the Main.swift, it does not recover from a disconnect. It does not retry. It’s not GOOD wifi code, but only essential. (minus a deinit).

@_cdecl("app_main")
func main() {

  guard var led = DigitalIndicator(15) else {
    fatalError("Difficulty setting up pin.")
  }

  guard let button = MomentaryInput(9) else {
    fatalError("Difficulty setting up button.")
  }

  //Waiting for USB...
  delay(2000);

  print("Hello from Swift on ESP32-C6!")
  print(wifi_bridge_return_twelve())

  let wifi = WiFiStation()
  wifi.connect(ssid: "somenetwork", password: "somepassword")

    //Waiting for wifi to connect...
  delay(2000);

  while true {
    if button.isActive {
      led.blink(millis: 500)
    } else {
      led.blink(millis: 2000)
    }
  }
}

It’s an old solution. But it checks out.

Wifi.swift

Getting a cleaner version of the string passing from Swift into C might be more possible on a machine running macOS 26? The C-Interop safety features with spans weren’t quite working correctly. (Could decorate the C, wasn’t being acknowledge by the Swift.)

final class WiFiStation {

    init() {
        checkWithFatal(wifi_bridge_initialize_nvs())
        checkWithFatal(wifi_bridge_initialize_netif())
        checkWithFatal(wifi_bridge_wifi_init_default_config())
    }

    func connect(ssid: String, password: String) {
        //     currentSSID = ssid
        //     currentPassword = password
        let local_ssid = ssid.utf8CString
        let local_pass = password.utf8CString

        //TODO: test with span on beta? 
        // Span was not rendering with 6.2 Sept 6 snapshot and lifetime. 
        local_pass.withContiguousStorageIfAvailable { pass_buffer in
            local_ssid.withContiguousStorageIfAvailable { ssid_buffer in
                checkWithFatal(
                    wifi_bridge_wifi_set_config_and_connect(
                        ssid_buffer.baseAddress, pass_buffer.baseAddress))
            }
        }
        // Compiles, runs, but spits out garbage.
        // checkWithFatal(wifi_bridge_wifi_set_config_and_connect(&local_ssid, &local_pass))
    }
}

ErrorHandler.swift

typealias SDKError = esp_err_t

func checkWithFatal(_ error:SDKError, message:String) {
    guard error == ESP_OK else {
        fatalError(message)
    }
}

func checkWithFatal(_ error:SDKError) {
    guard error == ESP_OK else {
        fatalError(String(cString:esp_err_to_name(error)))
    }
}

func checkWithThrow(_ error:SDKError, throws swiftError: some Error) throws {
    guard error == ESP_OK else {
        throw(swiftError)
    }
}

func checkWithThrow(_ error:SDKError) throws {
    guard error == ESP_OK else {
        throw(SDKErrorWrapper.passMessage(String(cString:esp_err_to_name(error))))
    }
}

enum SDKErrorWrapper:Error {
    case passMessage(String)
}

Component Internals

This is what the project layout looks like with the component now.

|- components
|  |- wifi_bridge
|  |  |- include
|  |  |  |- wifi_bridge.h
|  |  |- CMakeLists.txt
|  |  |- common_config.txt (vestigial from previous version)
|  |  |- idf_component.yml (needs to be updated)
|  |  |- wifi_bridge.c 
|- main
|  |- Bool_Int.swift
|  |- BridgingHeader.swift
|  |- CMakeLists.txt
|  |- Delay.swift
|  |- DigitalIndicator.swift
|  |- ErrorHandler.swift
|  |- GPIOPin.swift
|  |- Main.swift
|  |- MomentaryInput.swift
|  |- Wifi.swift
|  |- idf_component.yml
|
|- CMakeLists.txt
|- .gitignore

wifi_bridge.h

#include <string.h>
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"

int wifi_bridge_return_twelve(void);

void test_string_passing(const char *bridge_first, const char *bridge_last);

esp_err_t wifi_bridge_initialize_nvs(void);

esp_err_t wifi_bridge_initialize_netif(void);

esp_err_t wifi_bridge_wifi_init_default_config(void);

esp_err_t wifi_bridge_wifi_set_config_and_connect(const char *wifi_ssid, const char *wifi_pass);

wifi_bridge.c

#include "wifi_bridge.h"


int wifi_bridge_return_twelve(void)
{
    return 12;
}

esp_err_t wifi_bridge_initialize_nvs(void) {
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        esp_err_t flash_ret = nvs_flash_erase();
        if (flash_ret == ESP_OK) {
             ret = nvs_flash_init();
        } else {
            ret = flash_ret;
        }
    }
    return ret;
}

//TODO: what if needed that pointer later? 
esp_err_t wifi_bridge_initialize_netif(void) {
    esp_err_t ret = esp_netif_init();
    if (ret == ESP_OK) {
        ret = esp_event_loop_create_default();
    }
    esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
    assert(sta_netif);
    return ret ;
}

esp_err_t wifi_bridge_wifi_init_default_config(void) {
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    return esp_wifi_init(&cfg);
}

// #define TMP_SSID "example"
// #define TMP_PASS "example"


//strings don't actually work
esp_err_t wifi_bridge_wifi_set_config_and_connect(const char *wifi_ssid, const char *wifi_pass) {
    //uint8_t ssid[32]
    //uint8_t password[64]

    // wifi_config_t wifi_config = {
    //      .sta = { 
    //          .ssid = TMP_SSID, 
    //          .password = TMP_PASS, 
    //      }, 
    //  }; 

    printf("ssid: %s, pass: %s\n", wifi_ssid, wifi_pass);

    wifi_config_t wifi_config = {0}; // empty config
    // //count is the maximum 
    strncpy((char *)wifi_config.sta.ssid, wifi_ssid, sizeof(wifi_config.sta.ssid));
    strncpy((char *)wifi_config.sta.password, wifi_pass, sizeof(wifi_config.sta.password));

    esp_err_t ret = esp_wifi_set_mode(WIFI_MODE_STA);
    if (ret == ESP_OK) {
        ret = esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
    } else {
        return ret;
    }

    if (ret == ESP_OK) {
        ret = esp_wifi_start();
    } else {
        return ret;
    }

    if (ret == ESP_OK) {
        ret = esp_wifi_connect();
    } else {
        return ret;
    }

    return ret;
}

CMakeLists.txt

idf_component_register(SRCS "wifi_bridge.c"
                    PRIV_REQUIRES esp_wifi nvs_flash esp_netif
                    INCLUDE_DIRS "include")

Summary

Went through some dead end investigations that I think will have served me well in the future. An HTTP get request is next!

This article is part of a series.