Developer Guide — Network¶
This document covers the four network modules (NetworkModule, WifiStaModule, WifiApModule, EthernetModule), how they are wired together, the management policy that governs when interfaces come up or go down, and the platform-abstraction functions that drive them.
Module map¶
NetworkModule (parent)
├── WifiStaModule input key "sta"
├── WifiApModule input key "ap"
└── EthernetModule input key "eth"
NetworkModule is the parent in the module tree. It holds the device identity (name, MAC) and runs the management policy in its own loop(). The three child modules own their respective hardware interfaces. NetworkModule receives pointers to them via setInput() and calls setControl("enabled", …) on them to gate the interfaces.
NetworkModule¶
Source: src/modules/system/Network.h
Identity¶
| Control | Type | Persisted | Description |
|---|---|---|---|
device_name |
text | yes | Human-readable name shown in the UI top bar and used as the AP SSID. Auto-generated as MM-XXXX from the last 4 hex digits of the MAC address on first boot. |
mac_address |
display | no | Full MAC in AA:BB:CC:DD:EE:FF format, read from ESP32 eFuse at setup. |
The auto-name logic only fires when device_name is empty or still set to the sentinel values MM0000 or MM-0000 from old firmware. Once the user renames the device, the custom name is preserved across reflash (it is stored in state/network1.json).
WiFi mode initialisation¶
setup() calls WiFi.mode(WIFI_STA), not WIFI_AP_STA.
The AP netif is only allocated when WifiApModule actually calls wifi_ap_start(), which switches the mode to WIFI_AP_STA immediately before softAP(). Starting in WIFI_STA prevents a ~29 KB AP netif from being allocated at boot and then freed when STA connects — that deallocation fragments the heap and has historically corrupted the lwIP free list.
Management tick¶
loop() runs the management logic every 10 seconds (CHECK_INTERVAL_MS = 10 000 ms). See Management policy for full details.
Wiring¶
NetworkModule needs setInput("sta", …), setInput("ap", …), and optionally setInput("eth", …) to wire the child modules for management. Without the inputs, the management tick is a no-op (no pointers to act on). The child modules still function independently; they just are not managed.
WifiStaModule¶
Source: src/modules/system/WifiSta.h
Controls¶
| Control | Type | Persisted | Description |
|---|---|---|---|
ssid |
text | yes | Network name. Empty means no credentials — connection is not attempted. |
password |
password | yes | WPA2 passphrase. Empty means open network. |
status |
display | no | Current state string (see table below). |
ip_address |
display | no | Assigned IP once connected; empty otherwise. |
Status values¶
| Value | Meaning |
|---|---|
no credentials |
ssid is empty |
no WiFi |
platform has no WiFi hardware |
connecting… |
association in progress |
connected |
link up, IP assigned |
failed |
connection attempt timed out |
disabled |
enabled set to false by NetworkModule |
Connection lifecycle¶
setup()callsstartConnect()if enabled andssidis non-empty.loop()pollswifi_sta_is_connected()each tick whileconnecting_is true.- If connected: writes IP to
ip_address_, setsstatus_toconnected, clearsconnecting_. - If not connected after 10 seconds (
CONNECT_TIMEOUT_MS): setsstatus_tofailed, clearsconnecting_. - While enabled, not
connecting_, and not connected: retriesstartConnect()every 30 seconds (RETRY_INTERVAL_MS). This covers router reboots and brief signal loss without any external intervention.
Runtime updates¶
| Key | Effect |
|---|---|
ssid or password |
Sets status to connecting…, schedules a reconnect on the next loop() tick via pendingConnect_ flag (avoids connecting from the control callback). |
enabled |
true: calls startConnect(). false: calls wifi_sta_disconnect(), clears IP and status. NetworkModule uses this to gate STA alongside Ethernet. |
WifiApModule¶
Source: src/modules/system/WifiAp.h
Controls¶
| Control | Type | Persisted | Description |
|---|---|---|---|
enabled |
(inherited) | yes | Whether the AP should be running. Managed at runtime by NetworkModule. |
status |
display | no | Current AP state string. |
ap_password |
password | yes | Only present if the firmware is built with -DWIFIAP_PASSWORD. Off by default — the AP is open. |
Status values¶
| Value | Meaning |
|---|---|
starting |
initial value before startAp() runs |
active |
softAP() succeeded |
failed |
softAP() returned false |
disabled |
enabled set to false |
no WiFi |
platform has no WiFi hardware |
Fixed network parameters¶
| Parameter | Value |
|---|---|
| IP address | 4.3.2.1 |
| Gateway | 4.3.2.1 |
| Subnet | 255.255.255.0 |
| SSID | device_name from the parent NetworkModule (e.g. MM-C1BC) |
| Security | Open (no password) unless WIFIAP_PASSWORD is defined |
The IP 4.3.2.1 is intentional: it does not conflict with any common home router subnet and is easy to remember. The browser entry point on first boot is http://4.3.2.1.
Startup and runtime behaviour¶
setup() calls startAp() only if isEnabled() is true (loaded from state/ap1.json). On a fresh device with no saved state, enabled defaults to true, so the AP comes up automatically.
startAp() calls WiFi.mode(WIFI_AP_STA) before softAP() so the mode switch happens at a controlled time (during setup, before the HTTP/WS servers start accepting connections).
onUpdate("enabled") calls either startAp() or wifi_ap_stop() at runtime. wifi_ap_stop() checks WiFi.softAPIP() first and returns immediately if the AP is not actually running, so calling it on a device that booted in STA-only mode (AP was disabled in saved state) is a true no-op.
teardown() calls wifi_ap_stop() to cleanly disconnect any connected stations before the module is removed.
EthernetModule¶
Source: src/modules/system/Ethernet.h
Hardware variants¶
Selected at compile time via build flags in platformio.ini:
| Flag | Hardware | Interface | Target board |
|---|---|---|---|
-DPMM_ETH_LAN8720 |
LAN8720 PHY | RMII | esp32dev |
-DPMM_ETH_W5500 |
W5500 | SPI | esp32s3_n16r8 |
Additional flags for LAN8720 pin assignment: ETH_RMII_PHY_ADDR, ETH_RMII_MDC, ETH_RMII_MDIO, ETH_CLK_MODE.
Additional flags for W5500: ETH_SPI_HOST, ETH_SPI_FREQ, ETH_SPI_MOSI, ETH_SPI_MISO, ETH_SPI_SCK, ETH_SPI_CS, ETH_SPI_IRQ.
If neither flag is defined (PC build), all PAL Ethernet functions are no-ops and has_ethernet() returns false.
Controls¶
| Control | Type | Persisted | Description |
|---|---|---|---|
status |
display | no | initialising, disconnected, connected, init_failed, or unsupported |
ip_address |
display | no | Assigned IP when connected; empty otherwise |
mode |
select | yes | dhcp or static |
static_ip |
text | yes | Only used when mode is static |
static_gateway |
text | yes | Defaults to <ip>.1 if empty |
static_subnet |
text | yes | Defaults to 255.255.255.0 |
Link polling¶
loop() polls eth_is_connected() every 1 second. On a state change it updates status_ and ip_address_. No retry logic is needed — the Ethernet driver handles reconnect at the hardware level.
isConnected() is the method NetworkModule calls. It returns the cached ethUp_ flag (not a live hardware poll) so the management tick is not blocked by a slow bus read.
IP configuration¶
onUpdate("mode" | "static_ip" | "static_gateway" | "static_subnet") calls applyIpConfig_() immediately if Ethernet is initialised. Changes take effect on the live interface without a restart.
Management policy¶
NetworkModule's manageWifi_(now) runs every 10 seconds and implements the following priority-ordered policy:
Priority 1: Ethernet
Ethernet connected?
YES → disable AP, disable STA, clear grace timer → done
NO → (if Ethernet JUST dropped: re-enable STA)
Priority 2: WiFi STA
STA connected?
YES → disable AP (only if it is currently enabled), clear grace timer → done
NO →
STA just dropped (staWasConnected_ == true && staLostMs_ == 0)?
→ record staLostMs_ = now (start grace countdown)
Grace period expired (staLostMs_ != 0 && now - staLostMs_ >= 30 000 ms)?
→ enable AP (recovery mode), clear staLostMs_
Within grace period?
→ do nothing, wait for STA to recover on its own
Timers¶
| Constant | Value | Purpose |
|---|---|---|
CHECK_INTERVAL_MS |
10 000 ms | How often manageWifi_() runs |
STA_GRACE_MS |
30 000 ms | How long to wait after STA drops before opening the recovery AP |
CONNECT_TIMEOUT_MS (WifiSta) |
10 000 ms | Per-attempt association timeout |
RETRY_INTERVAL_MS (WifiSta) |
30 000 ms | How often WifiSta retries while disconnected |
State diagram¶
┌──────────────────────────────────┐
│ BOOT │
│ AP: enabled (first boot) │
│ STA: connecting... │
└────────────┬─────────────────────┘
│
┌──────────────────▼──────────────────┐
│ STA connected │
│ AP: disabled (after ≤10 s tick) │◄──────────────────┐
└──────────────────┬──────────────────┘ │
│ STA drops │ STA reconnects
┌──────────────────▼──────────────────┐ │
│ GRACE PERIOD (up to 30 s) │ │
│ AP: still disabled │───────────────────┘
│ STA: retrying every 30 s │
└──────────────────┬──────────────────┘
│ grace expires
┌──────────────────▼──────────────────┐
│ RECOVERY AP │
│ AP: enabled (SSID = MM-XXXX) │
└──────────────────┬──────────────────┘
│ user reconfigures STA via AP
│ STA connects
┌──────────────────▼──────────────────┐
│ STA connected │
│ AP: disabled (after ≤10 s tick) │
└─────────────────────────────────────┘
Ethernet connected at any point:
→ AP disabled, STA disabled
→ STA re-enabled when Ethernet drops
Saved state and boot behaviour¶
The enabled flag of WifiApModule is persisted to state/ap1.json. Once a device has connected via STA and the AP has been auto-disabled, subsequent boots start with enabled: false. NetworkModule sets WiFi.mode(WIFI_STA) at boot, so the AP netif is never allocated in the first place — the management tick's wifi_ap_stop() call is a guarded no-op (checked via ap_->isEnabled() and WiFi.softAPIP()).
PAL functions¶
All platform-specific WiFi and Ethernet calls route through pal:: functions in src/pal/Pal.h. Modules have no direct #ifdef ARDUINO calls.
WiFi AP¶
| Function | Behaviour |
|---|---|
wifi_ap_start(ssid, password) |
Switches to WIFI_AP_STA, configures AP at 4.3.2.1/24, calls softAP(). Returns true on success. |
wifi_ap_stop() |
Checks softAPIP() == 0.0.0.0 first; returns immediately if AP is not running. Otherwise calls softAPdisconnect(false) (keeps WiFi driver alive). |
wifi_ap_default_ssid(buf, len) |
Writes MM-XXXX from the last 4 MAC bytes into buf. Used by WifiApModule when the NetworkModule parent is not wired. |
WiFi STA¶
| Function | Behaviour |
|---|---|
wifi_sta_connect(ssid, password) |
Calls WiFi.begin(ssid, password). Non-blocking; connection state is polled separately. |
wifi_sta_disconnect() |
Calls WiFi.disconnect(true). |
wifi_sta_is_connected() |
Returns WiFi.status() == WL_CONNECTED. |
wifi_sta_local_ip(buf, len) |
Writes the assigned IP string into buf. |
wifi_set_tx_power() |
Sets TX power to 8.5 dBm when WIFI_LOLIN_FIX is defined (required for some Lolin boards to avoid brownout on transmission). |
wifi_tx_power_dbm() |
Returns current TX power as a float for the health report. |
Ethernet¶
| Function | Behaviour |
|---|---|
has_ethernet() |
Compile-time constant; true when PMM_ETH_LAN8720 or PMM_ETH_W5500 is defined. |
eth_init() |
Initialises the PHY/SPI Ethernet hardware. Returns false on failure. |
eth_is_connected() |
Returns true when link is up and an IP is assigned. |
eth_local_ip(buf, len) |
Writes the current Ethernet IP into buf. |
eth_set_dhcp() |
Switches to DHCP client mode. |
eth_set_static_ip(ip, gateway, subnet) |
Applies a static IP immediately. Null gateway uses ip/.1 as default. |
Wiring in modulemanager.json¶
A typical full-stack network setup:
{ "id": "network1", "type": "NetworkModule",
"inputs": { "sta": "sta1", "ap": "ap1", "eth": "eth1" } },
{ "id": "sta1", "type": "WifiStaModule", "parent_id": "network1" },
{ "id": "ap1", "type": "WifiApModule", "parent_id": "network1",
"inputs": { "network": "network1" } },
{ "id": "eth1", "type": "EthernetModule", "parent_id": "network1" }
WifiApModule takes a "network" input to read deviceName() for the AP SSID. Without it, the SSID falls back to the MAC-derived default via wifi_ap_default_ssid().
EthernetModule only needs to be present if the hardware supports it. If eth1 is wired but the module fails eth_init() (hardware not present or misconfigured), NetworkModule's eth_ pointer is set but isConnected() always returns false, so Ethernet management is effectively disabled without breaking anything.
PC build¶
On PC (no ARDUINO define), all WiFi and Ethernet PAL functions are no-ops:
- has_wifi() returns false
- has_ethernet() returns false
- NetworkModule::loop() is compiled out via #ifdef ARDUINO
- WifiStaModule and WifiApModule set status_ = "no WiFi" in startConnect()/startAp()
- EthernetModule sets status_ = "unsupported" in setup()
The modules are still instantiated and their controls appear in the UI, which is useful for testing the module wiring and persistence logic without hardware.