Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 69 additions & 6 deletions components/ml_dev_tether/src/ml_dev_tether.c
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,63 @@ static esp_err_t on_usb_rx(void * buffer, uint16_t len, void * ctx)
return r;
}

/* === USB bus state -> netif link state ============================ */

/* The tether netif was brought up with a synthetic "link connected" and
* nothing ever took it down again: a host that de-enumerated us, suspended
* the bus, or simply went away left the netif up with its lease, so the net
* supervisor kept ranking a dead tether as the best uplink and never failed
* over (bench, 2026-09-18: host port de-authorised, device kept 10.42.0.x,
* no WiFi for minutes). Mirror the bus state onto the netif: detached or
* suspended -> link down (lease dropped, netif_usable() false, supervisor
* fails over); attached or resumed -> link up (DHCP restarts).
* Runs on the TinyUSB task; the same lock on_usb_rx() uses keeps it from
* racing ml_dev_tether_stop()'s esp_netif_destroy(). */
static void on_usb_event(tinyusb_event_t * event, void * arg)
{
(void)arg;
if (event == NULL) {
return;
}
bool link_up;
const char * what;
switch (event->id) {
case TINYUSB_EVENT_ATTACHED:
link_up = true;
what = "attached";
break;
case TINYUSB_EVENT_DETACHED:
link_up = false;
what = "detached";
break;
#ifdef CONFIG_TINYUSB_SUSPEND_CALLBACK
case TINYUSB_EVENT_SUSPENDED:
link_up = false;
what = "suspended";
break;
#endif
#ifdef CONFIG_TINYUSB_RESUME_CALLBACK
case TINYUSB_EVENT_RESUMED:
link_up = true;
what = "resumed";
break;
#endif
default:
return;
}
if (s_rx_lock) xSemaphoreTake(s_rx_lock, portMAX_DELAY);
esp_netif_t * n = s_netif;
if (n) {
ESP_LOGW(TAG, "USB %s -> tether link %s", what, link_up ? "up" : "down");
if (link_up) {
esp_netif_action_connected(n, 0, 0, 0);
} else {
esp_netif_action_disconnected(n, 0, 0, 0);
}
}
if (s_rx_lock) xSemaphoreGive(s_rx_lock);
}

/* === DHCP lease signal ============================================ */

static void on_got_ip(void * arg, esp_event_base_t base, int32_t id, void * data)
Expand Down Expand Up @@ -163,7 +220,7 @@ esp_err_t ml_dev_tether_try_start(uint32_t timeout_ms)

/* --- TinyUSB up --- */
if (!s_tusb_installed) {
const tinyusb_config_t tusb_cfg = TINYUSB_DEFAULT_CONFIG();
const tinyusb_config_t tusb_cfg = TINYUSB_CONFIG_EVENT(on_usb_event);
err = tinyusb_driver_install(&tusb_cfg);
if (err != ESP_OK) {
ESP_LOGE(TAG, "tinyusb_driver_install: %s", esp_err_to_name(err));
Expand Down Expand Up @@ -291,11 +348,17 @@ esp_err_t ml_dev_tether_try_start(uint32_t timeout_ms)
}
ml_usb_tx_set_enabled(1);

/* Use the SAME MAC for the lwIP netif as the NCM endpoint. The IDF
* sta2eth example uses different MACs because it bridges to a separate
* DHCP server; in our DHCP-CLIENT topology there's a single logical
* endpoint and ARP/DHCP must resolve to the same MAC the host sees. */
esp_netif_set_mac(s_netif, net_cfg.mac_addr);
/* The MAC handed to tinyusb_net_init() is the one the HOST adopts for its
* side of the link (CDC-NCM iMACAddress); the lwIP netif is the other end
* and needs its own address, like any two NICs on a cable. Sharing one
* address (as before) had the host learn the device's IP at the host's
* own MAC — it happened to work on Linux, but is a spec violation and
* indistinguishable frames in any capture. Flip one more bit: still
* locally administered, still derived from the unit's WiFi MAC. */
uint8_t dev_mac[6];
memcpy(dev_mac, net_cfg.mac_addr, sizeof(dev_mac));
dev_mac[5] ^= 0x02;
esp_netif_set_mac(s_netif, dev_mac);

/* Bring up the netif and also signal "link connected" — without this,
* the Ethernet-class netif stays in admin-up/link-down state and the
Expand Down
59 changes: 53 additions & 6 deletions firmware/components/dcs_support/src/dcs_net_supervisor.c
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#include "esp_timer.h"
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "ml_app.h"

Expand All @@ -45,6 +46,53 @@ static const char * TAG = "dcs_netsup";
* poll period. Only DOWN transitions kick — UP/promote stays on the periodic
* loop for natural anti-flap debounce (asymmetric by design). */
static TaskHandle_t s_sup_task = NULL;

/* dcs_wifi_set_enabled() reaches flash: the wifi_list NVS blob, and
* esp_wifi_init/start's PHY-calibration NVS access. Flash access asserts when
* the caller's stack is in PSRAM (cache_utils.c:127,
* esp_task_stack_is_sane_cache_disabled) — and the supervisor's is. So the
* toggle runs on a short-lived INTERNAL-stack worker and the supervisor only
* waits on a dedicated semaphore (its task notification is the link-down
* wake-up and must not be consumed here). Bench 2026-09-19 (cad4): tether
* link-down -> "bringing up WiFi" -> assert abort in net_sup, boot loop. */
typedef struct
{
bool on;
esp_err_t result;
} wifi_toggle_req_t;

static SemaphoreHandle_t s_wifi_toggle_done = NULL;
static wifi_toggle_req_t s_wifi_toggle_req; /* one caller (this task), one toggle at a time */

static void wifi_toggle_task(void * arg)
{
wifi_toggle_req_t * req = (wifi_toggle_req_t *)arg;
req->result = dcs_wifi_set_enabled(req->on);
(void)xSemaphoreGive(s_wifi_toggle_done);
vTaskDelete(NULL);
}

static esp_err_t wifi_set_enabled_on_internal_stack(bool on)
{
if (s_wifi_toggle_done == NULL) {
s_wifi_toggle_done = xSemaphoreCreateBinary();
if (s_wifi_toggle_done == NULL) {
return ESP_ERR_NO_MEM;
}
}
s_wifi_toggle_req.on = on;
s_wifi_toggle_req.result = ESP_FAIL;
/* Internal stack by construction (plain xTaskCreate): 4 KB, transient. */
if (
xTaskCreatePinnedToCore(wifi_toggle_task, "wifi_tog", 4096, &s_wifi_toggle_req, 4, NULL, tskNO_AFFINITY) != pdPASS)
{
ESP_LOGW(TAG, "WiFi toggle worker: no internal RAM for its stack — retry next tick");
return ESP_ERR_NO_MEM;
}
(void)xSemaphoreTake(s_wifi_toggle_done, portMAX_DELAY); /* worker always gives before exiting */
return s_wifi_toggle_req.result;
}

static atomic_uint s_link_down_kicks = 0;

#define SUPERVISOR_PERIOD_MS 1000
Expand Down Expand Up @@ -209,12 +257,12 @@ static void supervisor_task(void * arg)
if (dcs_boot_alt_network_won()) {
if ((!high_ok) && (high_bad_s >= WIFI_FAILOVER_AFTER_S) && (!dcs_wifi_is_enabled())) {
ESP_LOGW(TAG, "no Eth/USB for %us — bringing up WiFi", (unsigned)high_bad_s);
if (dcs_wifi_set_enabled(true) == ESP_OK) {
if (wifi_set_enabled_on_internal_stack(true) == ESP_OK) {
wifi_auto = true;
}
} else if (high_ok && (high_ok_s >= FAILOVER_DROP_HOLD_S) && wifi_auto && dcs_wifi_is_enabled()) {
ESP_LOGW(TAG, "Eth/USB stable %us — dropping WiFi", (unsigned)high_ok_s);
if (dcs_wifi_set_enabled(false) == ESP_OK) {
if (wifi_set_enabled_on_internal_stack(false) == ESP_OK) {
wifi_auto = false;
}
} else {
Expand All @@ -239,10 +287,9 @@ static void supervisor_task(void * arg)
void dcs_net_supervisor_start(void)
{
atomic_store(&g_dcs_active_iface, (int)DCS_IFACE_NONE);
/* 4608 B: the task now calls dcs_wifi_set_enabled() (esp_wifi_init/deinit,
* esp_netif create/destroy) on auto-failover, which needs more stack than
* the bare route-arbitration loop. */
/* PSRAM stack: route supervisor is non-safety, does no flash/NVS. */
/* PSRAM stack: route supervisor is non-safety and does no flash/NVS itself —
* the WiFi failover toggle, which does, runs on an internal-stack worker
* (wifi_set_enabled_on_internal_stack). */
s_sup_task = dcs_task_spawn_psram(supervisor_task, "net_sup", 4608, NULL, 4, tskNO_AFFINITY);

/* Event-driven demote: wake the supervisor the instant a link drops so it
Expand Down
4 changes: 4 additions & 0 deletions firmware/sdkconfig.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ CONFIG_TINYUSB_CDC_COUNT=1
CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG=n
CONFIG_TINYUSB_CDC_RX_BUFSIZE=512
CONFIG_TINYUSB_CDC_TX_BUFSIZE=512
# Bus suspend/resume events: the tether mirrors them onto its netif link
# state so the net supervisor fails over when the host goes away.
CONFIG_TINYUSB_SUSPEND_CALLBACK=y
CONFIG_TINYUSB_RESUME_CALLBACK=y
# USB descriptors
CONFIG_TINYUSB_DESC_MANUFACTURER_STRING="Polymath"
CONFIG_TINYUSB_DESC_PRODUCT_STRING="MicroLink dual_core_safety"
Expand Down
4 changes: 4 additions & 0 deletions machn/sdkconfig.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ CONFIG_TINYUSB_CDC_COUNT=1
CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG=n
CONFIG_TINYUSB_CDC_RX_BUFSIZE=512
CONFIG_TINYUSB_CDC_TX_BUFSIZE=512
# Bus suspend/resume events: the tether mirrors them onto its netif link
# state so the net supervisor fails over when the host goes away.
CONFIG_TINYUSB_SUSPEND_CALLBACK=y
CONFIG_TINYUSB_RESUME_CALLBACK=y
# USB descriptors
CONFIG_TINYUSB_DESC_MANUFACTURER_STRING="Polymath"
CONFIG_TINYUSB_DESC_PRODUCT_STRING="MicroLink dual_core_safety"
Expand Down
Loading