Sending buffers to C++ functions and retrieving data

Using the embedded swift example here as my starting point and incorporating the code from the esp-idf installation examples/wifi/scan/, I can successfully scan the APs in my local environment. My problem is accessing the results! Here is my code:

@_cdecl("app_main")
func app_main() {
    print("Hello from Swift on ESP32-C6!")
    var ret = nvs_flash_init()
    if ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND {
        ESP_ERROR_CHECK(nvs_flash_erase())
        ret = nvs_flash_init()
    }
    ESP_ERROR_CHECK(ret)

    ESP_ERROR_CHECK(esp_netif_init())

    var cfg = get_config()
    ESP_ERROR_CHECK(esp_wifi_init(&cfg))
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA))
    ESP_ERROR_CHECK(esp_wifi_start())

    var scanListSize: UInt16 = 32
    var ap_count: UInt16 = 0
    var ap_info = UnsafeMutablePointer<wifi_ap_record_t>.allocate(capacity: Int(scanListSize))

    ESP_ERROR_CHECK(esp_netif_init())
    ESP_ERROR_CHECK(esp_event_loop_create_default())
    let _ = esp_netif_create_default_wifi_sta()
    // assert(sta_netif)

    while true {
        scanListSize = 32
        esp_wifi_scan_start(nil, true)

        print("Max AP scanListSize ap_info can hold = \(scanListSize)")
        ESP_ERROR_CHECK(esp_wifi_scan_get_ap_num(&ap_count))
        ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&scanListSize, ap_info))
        print("Total APs scanned = \(ap_count), actual AP scanListSize ap_info holds = \(scanListSize)" )
        for i in 0..<Int(scanListSize) {
            let pointer = I * 47
            let record: wifi_ap_record_t = ap_info[pointer]
            print("\(record.ssid.0),\(record.ssid.1),\(record.ssid.2),\(record.ssid.3),\(record.ssid.4),")
        }
    }
}

// The ESP_ERROR_CHECK macro is unavailable
func ESP_ERROR_CHECK(_ errno: esp_err_t, file: String = #file, line: Int = #line) {
    if errno == ESP_OK { return }
    print("\(errno) at line \(line) in \(file)")
}

The structure of wifi_ap_record_t is:

typedef struct {
    uint8_t bssid[6];                     /**< MAC address of AP */
    uint8_t ssid[32];                     /**< SSID of AP */
    uint8_t primary;                      /**< channel of AP */
    wifi_second_chan_t second;            /**< second channel of AP */
    int8_t  rssi;                         /**< signal strength of AP */
    wifi_auth_mode_t authmode;            /**< authmode of AP */
    uint32_t low_rate_enable:1;           /**< bit: 0 flag to identify if low rate is enabled or not */
    uint32_t reserved:31;                 /**< bit: 1..31 reserved */
} wifi_ap_record_t;

The code above returns a reasonable value for the number of APs discovered and prints out the first five values of the SSID field. It runs for extended periods without crashing, but the second and subsequent runs don't appear to return as good data as the first.

The SSID field is a 32-element tuple of UInt8s. In other situations (e.g. decoding similar data from libgpiod on a raspberry pi, I use Mirror to convert tuples to arrays (and onward to String if required). However, Mirror is unavailable in embedded swift. Trying to go via Data(bytes:) also fails because Data isn't available either - although it doesn't explicitly say this like it does with Mirror, it just says it cannot find it in scope. Accessing tuple elements has to be literal, i.e. you can't do record.ssid.n, so I next switched to trying to convert individual bytes to Character using let ch0 = record.ssid.0 as? Character but this gives nil when appended to a String and cannot print value in embedded Swift if printed directly.

I've got two questions:

  1. Is the way I am allocating the buffer the best or even the correct way?
  2. Is there an easy way to get the data from the SSID field into, say, a String?

Hi Nick,

  1. Yes, that looks OK to me
  2. print(String(decoding: record.ssid, as: UTF8.self)) should work

Hi Eric

Thank you for your reply. I'm glad about the buffer! I hadn't tried your suggestion before, but sadly it gives a compiler error:

error: type '(United, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)' cannot conform to 'Collection'

Interesting, before posting, I tested this small code on a nRF52840dk and it worked fine

    var s : [UInt8] = [97, 98, 99, 100]
    print(String(decoding: s, as: UTF8.self))

I now tested something closer to your use case and indeed I have the same compilation error. The issue is that ssid is exposed as a tuple, not an array.
A quick search to perform a conversion lead me to convert tuple to swift array | Apple Developer Forums

And so my code looks like this
In BridgingHeader.h:

typedef struct {
    uint8_t ssid[4];
} mock_record_t;

Swift code:

    let record = UnsafeMutablePointer<mock_record_t>.allocate(capacity:1)
    record.pointee.ssid = (100, 99, 98, 97)

    let ssid_a = withUnsafeBytes(of: record.pointee.ssid) { buf in
      [UInt8](buf)
    }

    print(String(decoding: ssid_a, as: UTF8.self))

Not super elegant but worked for me.

Hi Eric

Thank you again for your help. This works perfectly! Just for anyone else reading this far, to make it work with an array of records you just have to offset the pointer by the index of the record you want:

for i in 0..<Int(ap_count) {
    let ap_info = UnsafeMutablePointer<wifi_ap_record_t>(ap_info_buffer + i)
    let ssid_a = withUnsafeBytes(of: ap_info.pointee.ssid) { buf in
        [UInt8](buf)
    }
    print("SSID: \(String(decoding: ssid_a, as: UTF8.self))")
}