diff options
Diffstat (limited to 'src/net/hosted/wifi_shim.c')
| -rw-r--r-- | src/net/hosted/wifi_shim.c | 200 |
1 files changed, 200 insertions, 0 deletions
diff --git a/src/net/hosted/wifi_shim.c b/src/net/hosted/wifi_shim.c new file mode 100644 index 0000000..67bdcc6 --- /dev/null +++ b/src/net/hosted/wifi_shim.c @@ -0,0 +1,200 @@ +/* + * A narrow C surface over ESP-Hosted's Wi-Fi RPC, so Zig never transcribes an IDF struct. + * + * `rpc_wifi_init` takes a `wifi_init_config_t`, `rpc_wifi_set_config` takes a `wifi_config_t`, and + * scanning hands back `wifi_ap_record_t`. Those are large, versioned structs full of bitfields, and + * IDF builds them with macros - WIFI_INIT_CONFIG_DEFAULT() alone sets over twenty fields + * (esp_wifi.h:316). Writing Zig `extern struct`s to match would be a transcription that compiles + * happily and goes wrong on the next IDF release, exactly the mistake that + * `esp_hosted_sdio_get_config` taught this project once already. + * + * So the structs stay on the C side, built by IDF's own macros, and Zig gets plain scalars and byte + * buffers. Everything here is a thin forwarder; the interesting code is all in ESP-Hosted's RPC + * layer, which this does not duplicate. + */ + +#include <string.h> + +#include "esp_wifi_types.h" +#include "esp_wifi.h" +#include "rpc_wrap.h" + +/* ESP-Hosted's RPC entry points (host/drivers/rpc/wrap/rpc_wrap.c). Declared here rather than + * relying on the header, so a signature change is a compile error in this file. */ +int rpc_wifi_init(const wifi_init_config_t *arg); +int rpc_wifi_set_mode(wifi_mode_t mode); +int rpc_wifi_set_config(wifi_interface_t interface, wifi_config_t *conf); +int rpc_wifi_connect(void); +int rpc_wifi_scan_start(const wifi_scan_config_t *config, bool block); +int rpc_wifi_scan_get_ap_num(uint16_t *number); +int rpc_wifi_scan_get_ap_records(uint16_t *number, wifi_ap_record_t *ap_records); +int rpc_wifi_start(void); +int rpc_wifi_get_mac(wifi_interface_t mode, uint8_t mac[6]); +int rpc_wifi_set_ps(wifi_ps_type_t type); + +/* Initialise the coprocessor's Wi-Fi and put it in station mode, started. + * + * The order is IDF's own and is not negotiable: init, set_mode, start. `esp_wifi_start` is what + * actually brings the radio up on the C6; a config set before it is accepted and a connect before + * it is not. */ +int hosted_wifi_sta_start(void) +{ + /* IDF's WIFI_INIT_CONFIG_DEFAULT() is deliberately NOT used, and this is not a shortcut. + * + * That macro's first two fields are `.osi_funcs = &g_wifi_osi_funcs` and + * `.wpa_crypto_funcs = g_wifi_default_wpa_crypto_funcs` (esp_wifi.h:317-318) - the local Wi-Fi + * driver's OS adapter and crypto tables. This chip has no Wi-Fi driver: the C6 does, and it uses + * its own. Referencing them here pulls in symbols that cannot exist in this image, which is + * exactly the link error that led to this comment. + * + * They are also provably unnecessary. rpc_req.c:182-215 packs the request field by field, and + * every field it packs is a scalar; neither function pointer is ever serialised. So the struct + * only has to carry the scalars, and those come from the same Kconfig-derived macros the real + * default uses - via the checked-in sdkconfig, so they are this project's configuration and not + * a second set of numbers. + * + * `magic` is load-bearing: the coprocessor validates it (esp_wifi.h's own note says it must + * always be WIFI_INIT_CONFIG_MAGIC), so a zeroed struct is rejected. */ + wifi_init_config_t cfg = { 0 }; + cfg.static_rx_buf_num = CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM; + cfg.dynamic_rx_buf_num = CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM; + cfg.tx_buf_type = CONFIG_ESP_WIFI_TX_BUFFER_TYPE; + cfg.static_tx_buf_num = WIFI_STATIC_TX_BUFFER_NUM; + cfg.dynamic_tx_buf_num = WIFI_DYNAMIC_TX_BUFFER_NUM; + cfg.rx_mgmt_buf_type = CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUF; + cfg.rx_mgmt_buf_num = WIFI_RX_MGMT_BUF_NUM_DEF; + cfg.cache_tx_buf_num = WIFI_CACHE_TX_BUFFER_NUM; + cfg.csi_enable = WIFI_CSI_ENABLED; + cfg.ampdu_rx_enable = WIFI_AMPDU_RX_ENABLED; + cfg.ampdu_tx_enable = WIFI_AMPDU_TX_ENABLED; + cfg.amsdu_tx_enable = WIFI_AMSDU_TX_ENABLED; + cfg.nvs_enable = WIFI_NVS_ENABLED; + cfg.nano_enable = WIFI_NANO_FORMAT_ENABLED; + cfg.rx_ba_win = WIFI_DEFAULT_RX_BA_WIN; + cfg.wifi_task_core_id = WIFI_TASK_CORE_ID; + cfg.beacon_max_len = WIFI_SOFTAP_BEACON_MAX_LEN; + cfg.mgmt_sbuf_num = WIFI_MGMT_SBUF_NUM; + cfg.feature_caps = WIFI_FEATURE_CAPS; + cfg.sta_disconnected_pm = WIFI_STA_DISCONNECTED_PM_ENABLED; + cfg.espnow_max_encrypt_num = CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM; + cfg.tx_hetb_queue_num = WIFI_TX_HETB_QUEUE_NUM; + cfg.dump_hesigb_enable = WIFI_DUMP_HESIGB_ENABLED; + cfg.magic = WIFI_INIT_CONFIG_MAGIC; + + int err = rpc_wifi_init(&cfg); + if (err) { + return err; + } + err = rpc_wifi_set_mode(WIFI_MODE_STA); + if (err) { + return err; + } + err = rpc_wifi_start(); + if (err) { + return err; + } + + /* Power save OFF, and this is not a performance tweak - it decides whether the board is + * reachable at all. + * + * ESP-IDF's default is WIFI_PS_MIN_MODEM (esp_wifi_types_generic.h:376): the station sleeps and + * only wakes for a beacon every DTIM period. A sleeping station misses frames the AP does not + * buffer for it, and broadcast ARP is exactly that. The observed symptom on this board was + * precise and misleading: DHCP completed - because the host speaks first and the reply arrives + * inside the wake window - the board took a real lease, and then it answered no ARP and no ping, + * with the frame counter advancing about once per ten seconds. It looked like a broken receive + * path rather than a radio that was asleep. + * + * A device that exists to answer requests cannot sleep between them. WIFI_PS_NONE. */ + return rpc_wifi_set_ps(WIFI_PS_NONE); +} + +/* The station's MAC. Needed by the IP stack: ARP and Ethernet framing are built around it, and it + * belongs to the C6's radio, not to this chip. */ +int hosted_wifi_get_mac(uint8_t out[6]) +{ + return rpc_wifi_get_mac(WIFI_IF_STA, out); +} + +/* Scan every channel and report how many networks were seen. + * + * Blocking: the RPC layer waits for the coprocessor to finish, which takes a couple of seconds + * across all channels. A scan needs no credentials, which makes it the cheapest end-to-end proof + * that the RPC path and the radio both work. */ +int hosted_wifi_scan(uint16_t *found) +{ + wifi_scan_config_t scan = { 0 }; + scan.show_hidden = true; + int err = rpc_wifi_scan_start(&scan, true); + if (err) { + return err; + } + return rpc_wifi_scan_get_ap_num(found); +} + +/* One scan result, flattened to scalars. + * + * `ssid_out` must have room for 33 bytes; the SSID is copied NUL-terminated. Returns the number of + * records actually written into the caller's view, which is `min(*count, what the slave has)`. + */ +int hosted_wifi_scan_record(uint16_t index, char *ssid_out, int8_t *rssi_out, + uint8_t *channel_out, uint8_t *authmode_out) +{ + /* One record at a time, into a local, so the caller never sees a wifi_ap_record_t. Asking the + * slave for a single record by index is not part of the RPC, so this fetches the run up to + * `index` and keeps the last - fine for the small numbers a diagnostic prints, and stated here + * rather than hidden because it is O(n^2) if someone loops it over hundreds of networks. */ + static wifi_ap_record_t records[16]; + uint16_t want = index + 1; + if (want > 16) { + return -1; + } + int err = rpc_wifi_scan_get_ap_records(&want, records); + if (err) { + return err; + } + if (index >= want) { + return -1; + } + + const wifi_ap_record_t *r = &records[index]; + size_t n = strnlen((const char *)r->ssid, 32); + memcpy(ssid_out, r->ssid, n); + ssid_out[n] = 0; + *rssi_out = r->rssi; + *channel_out = r->primary; + *authmode_out = (uint8_t)r->authmode; + return 0; +} + +/* Join a network. + * + * `ssid` and `psk` are NUL-terminated. The PSK is copied into the request and never stored here; + * it arrives from a build option so it is not in the source, and this function keeps no copy after + * the RPC returns. + * + * `threshold.authmode` is deliberately left at 0 (WIFI_AUTH_OPEN) rather than forced to WPA2: it is + * a *minimum* acceptable security level, and pinning it too high refuses networks that would + * otherwise work while pinning it low refuses nothing. The AP's actual authmode is what gets used. + */ +int hosted_wifi_connect(const char *ssid, const char *psk) +{ + wifi_config_t conf = { 0 }; + + size_t ssid_len = strnlen(ssid, sizeof(conf.sta.ssid) - 1); + memcpy(conf.sta.ssid, ssid, ssid_len); + + size_t psk_len = strnlen(psk, sizeof(conf.sta.password) - 1); + memcpy(conf.sta.password, psk, psk_len); + + /* Scan all channels and pick the strongest match rather than the first: this network has both a + * 2.4 GHz and a 5 GHz radio on the same SSID family, and the C6 is 2.4 GHz only. */ + conf.sta.scan_method = WIFI_ALL_CHANNEL_SCAN; + conf.sta.sort_method = WIFI_CONNECT_AP_BY_SIGNAL; + + int err = rpc_wifi_set_config(WIFI_IF_STA, &conf); + if (err) { + return err; + } + return rpc_wifi_connect(); +} |
