feat: wi-fi - expand support and fix issues, add GUI components
This commit is contained in:
+161
@@ -12,8 +12,12 @@ wifi scan list nearby networks
|
||||
wifi connect <ssid> <passphrase> join one
|
||||
dhcp pick up an address
|
||||
wifi status what you are connected to
|
||||
wifi saved / wifi forget <ssid> networks remembered for next time
|
||||
```
|
||||
|
||||
There is a graphical path too: a Wi-Fi entry in the desktop panel and a
|
||||
Wi-Fi tab in the Network app. See "The desktop side" below.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
@@ -147,6 +151,163 @@ removes the MAC and drops the PHY context - the order
|
||||
`iwl_mvm_mld_vif_cfg_changed_station` and the paths below it use on the way
|
||||
down. `tests/wifi/ap_mlme.py` pins that order.
|
||||
|
||||
### A bad access point must not become a bad computer
|
||||
|
||||
Three separate defects turned "the Wi-Fi connection went wrong" into "the whole
|
||||
machine went wrong". They are worth keeping straight because they have nothing
|
||||
to do with each other beyond sharing a trigger.
|
||||
|
||||
**The command wait is a busy spin on a core the scheduler has been told not to
|
||||
touch.** `ServiceEvents()` runs from `ApicTimer::ServiceDeferredWork()`, which
|
||||
sets `cpu->reservedForKernelWork` so a bottom half holding a process-context
|
||||
mutex cannot be preempted into the process that would wait on it. The
|
||||
scheduler honours that by refusing to place any process on that core and by
|
||||
skipping it for the reschedule IPI, and on the BSP `RunBspMaintenance()` - the
|
||||
thing that wakes sleeping processes - is not reached until the pass returns.
|
||||
`IwxSendCmd` then waits for the firmware by spinning on `IwxDelayUs`, up to a
|
||||
second. One pass of `TearDown()` is eight commands. Against an adapter that has
|
||||
stopped answering, that pinned a core for the better part of ten seconds. The
|
||||
symptom is not a Wi-Fi symptom at all: the cursor crawls, windows stop
|
||||
repainting, everything stutters.
|
||||
|
||||
So a service pass now carries a budget - `IwxBeginServicePass()` /
|
||||
`IwxEndServicePass()` - of roughly one command's worth of waiting in total.
|
||||
Commands that do not fit are not sent at all (an abandoned command still holds
|
||||
its ring slot, and a late answer would be misread as the *next* command's
|
||||
completion) and are retried on the next trip round the idle loop. The bring-up
|
||||
path does not bracket itself: it reserves its CPU deliberately and has nothing
|
||||
to starve, so it keeps the full per-command timeout.
|
||||
|
||||
**Counting consecutive failures never fires on the failure that matters.** The
|
||||
give-up rule was three unanswered commands in a row, with any success resetting
|
||||
the count. An adapter that answers some commands and drops others - which is
|
||||
exactly what a marginal link leaves the firmware doing - therefore never
|
||||
reached the cutoff, while every drop still cost a full timeout. The stall was
|
||||
not a one-off; it repeated indefinitely, and the driver never concluded
|
||||
anything was wrong. It is a leaky bucket now: a failure adds one, a success
|
||||
drains one, so a firmware failing even a fraction of its commands trips the
|
||||
cutoff in bounded time.
|
||||
|
||||
**`IwxFwState::Error` was a one-way door.** Nothing anywhere cleared it. A
|
||||
single firmware assert left `StartJoin()` returning `WIFI_ERR_NO_ADAPTER` -
|
||||
which the user sees as *no adapter is ready* - for the rest of the uptime, and
|
||||
the only way back was a reboot. Disconnecting and reconnecting could not help:
|
||||
there was nothing to reconnect with. Since `IwxReadFirmware()` is idempotent
|
||||
and the parsed image stays resident, the cure is to stop the device and run the
|
||||
same bring-up again, which `ServiceRecovery()` now does. It is rate-limited to
|
||||
one attempt per five seconds and capped at three, because the reset has its own
|
||||
multi-second handshakes and an adapter that will not come back after three
|
||||
tries is genuinely broken - repeating it forever would be its own kind of
|
||||
stall.
|
||||
|
||||
### The link is up only while the access point says so
|
||||
|
||||
`IwxLinkUp()` was nothing but `g_state == Connected`, and nothing moved it off
|
||||
that state unless the access point was polite enough to send a
|
||||
deauthentication frame. An access point that simply stops being there - a phone
|
||||
hotspot that sleeps, wanders off channel, or drops the station without saying
|
||||
so - left the link reported as up indefinitely. `NetIf::Active()` went on
|
||||
choosing `wlan0`, every packet went into the void, and the desktop showed a
|
||||
healthy connection while nothing resolved and nothing connected.
|
||||
|
||||
Beacon loss cannot be used to notice this: once associated, `MacConfigCmd`
|
||||
stops asking for beacons and the firmware tracks them itself. What is
|
||||
observable is that our own frames stop being acknowledged, since `IwxTxComplete`
|
||||
gets a status per frame. A long enough run of failures - with any
|
||||
acknowledgement, or any frame received from the BSS, restarting the count -
|
||||
means the access point is gone, and the link comes down so the stack can fall
|
||||
back to a cable and the panel can report the truth.
|
||||
|
||||
The threshold is counted in frames rather than seconds on purpose: a link that
|
||||
is merely idle has nothing to send and must not be torn down for it. And
|
||||
`IwxTxComplete` runs inside the RX pump, so it only sets a flag; the teardown
|
||||
happens in `IwxConnectService()`, where sending the commands it needs is
|
||||
allowed. `tests/wifi/ap_mlme.py` pins both halves of that.
|
||||
|
||||
## The desktop side
|
||||
|
||||
### Nothing in the GUI may block
|
||||
|
||||
`SYS_WIFI_SCAN` sweeps for up to twenty seconds and `SYS_WIFI_CONNECT` waits
|
||||
out a whole handshake. Either one called from `desktop.elf` would freeze the
|
||||
compositor - and with it the mouse, the panel and every window - for seconds at
|
||||
a time. So the same work has a second, non-blocking entry point:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `SYS_WIFI_SCAN_START` | starts a sweep and returns immediately |
|
||||
| `SYS_WIFI_RESULTS` | copies the scan table out without touching the radio |
|
||||
| `SYS_WIFI_CONNECT_ASYNC` | starts a join and returns immediately |
|
||||
| `SYS_NETIFS` | lists the registered interfaces (see below) |
|
||||
|
||||
`WifiInfo` grew the fields that make polling enough to follow along:
|
||||
`scanning` and `scanGeneration` for the sweep, `joining` and `connState` for
|
||||
the handshake, and `lastError` for how the last join ended. The GUI reads
|
||||
`SYS_WIFI_INFO` every 400 ms while something is in flight and every three
|
||||
seconds otherwise.
|
||||
|
||||
The deadlines belong to the kernel, not the caller: `ServiceAsync()` runs from
|
||||
`ServiceEvents()` - after the RX pump has returned, so it is allowed to send
|
||||
commands - and aborts a scan that overruns, tears down a join that stalls, and
|
||||
records `lastError` when one fails. A failed join is unwound the moment it
|
||||
fails, so `connState` is back to idle by the time anyone looks; `lastError` is
|
||||
what survives to be reported.
|
||||
|
||||
### Wired and wireless are separate on the panel
|
||||
|
||||
The IP configuration is global to the stack, so "which interface does this
|
||||
address belong to?" is not a question `SYS_GETNETCFG` can answer. `SYS_NETIFS`
|
||||
reports each registered interface with its name, MAC, link state and an
|
||||
`active` flag - the one `NetIf::Active()` currently sends through. The Ethernet
|
||||
popup shows the address only while the wired interface is the active one, and
|
||||
says "Not in use" when the cable is up but Wi-Fi is carrying the traffic; the
|
||||
Wi-Fi popup does the mirror image. Each icon appears only when its hardware
|
||||
does: no wired interface, no Ethernet icon; no adapter, no Wi-Fi icon.
|
||||
|
||||
The Wi-Fi icon stays white whatever the radio is doing. State belongs in the
|
||||
popup, and an icon that changes colour next to the clock is just noise.
|
||||
|
||||
Picking a network that needs a passphrase opens a real window - created with
|
||||
`desktop_create_window()` and the four callbacks, exactly like the reboot and
|
||||
shutdown dialogs in `dialogs.cpp` - rather than something painted into the
|
||||
panel overlay. It therefore has a title bar, can be dragged and closed, appears
|
||||
in the window list, and gets its text field, checkboxes and buttons from the
|
||||
same `mtk` widgets the settings apps use.
|
||||
|
||||
### What runs at startup
|
||||
|
||||
`desktop.elf` keeps looking for an adapter until one appears (firmware loads
|
||||
well after login), starts one scan as soon as the firmware reports ready, and
|
||||
when the results land joins the strongest saved network that is in range. Once
|
||||
the link is up, and only if no address is configured, it spawns `dhcp.elf`.
|
||||
|
||||
### Saved networks
|
||||
|
||||
`0:/config/wifi.toml` holds them:
|
||||
|
||||
```toml
|
||||
[wifi]
|
||||
autoconnect = true
|
||||
|
||||
[network.0]
|
||||
ssid = "Home"
|
||||
psk = "passphrase"
|
||||
```
|
||||
|
||||
**No default copy of this file ships in the image.** It is created on the first
|
||||
save, the way `bluetooth.toml`, `display.toml` and `session.toml` are. A shipped
|
||||
default looks harmless - it only documents the schema - but it is laid down
|
||||
again by anything that refreshes the system files, and it takes the user's saved
|
||||
networks with it when it lands. That is exactly what happened the first time
|
||||
this was written, and it is why the schema is documented here instead.
|
||||
|
||||
`programs/include/montauk/wifi.h` is the one implementation of reading,
|
||||
writing and searching that file, shared by the panel, the Network app and the
|
||||
`wifi` command, so all three agree on the schema. The passphrase is stored as
|
||||
typed because that is what the join needs - the kernel derives the PMK from it,
|
||||
or takes a 64-character hex string as a raw PSK. There is no key store to hide
|
||||
it in: anyone who can read `0:/config` can read the passphrases.
|
||||
|
||||
## The interface registry
|
||||
|
||||
`Net::NetIf` replaced the Ethernet layer's direct calls into the E1000
|
||||
|
||||
Reference in New Issue
Block a user