Skip to content

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

  1. setup() calls startConnect() if enabled and ssid is non-empty.
  2. loop() polls wifi_sta_is_connected() each tick while connecting_ is true.
  3. If connected: writes IP to ip_address_, sets status_ to connected, clears connecting_.
  4. If not connected after 10 seconds (CONNECT_TIMEOUT_MS): sets status_ to failed, clears connecting_.
  5. While enabled, not connecting_, and not connected: retries startConnect() 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

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.