feat: wi-fi - expand support and fix issues, add GUI components

This commit is contained in:
2026-08-07 10:36:53 +02:00
parent bbe1df62fd
commit 9eb21eb3e3
67 changed files with 4573 additions and 245 deletions
+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once
#define MONTAUK_BUILD_NUMBER 69
#define MONTAUK_BUILD_NUMBER 84
+32
View File
@@ -160,6 +160,38 @@ namespace montauk::abi {
}
}
// List the registered link-layer interfaces. The panel needs this to keep
// wired and wireless status apart: the IP configuration is global, so the
// only way to say which interface owns it is `active`.
static int Sys_NetIfs(NetIfInfo* out, int maxCount) {
if (out == nullptr || maxCount <= 0) return -1;
const auto* active = ::Net::NetIf::Active();
int count = ::Net::NetIf::Count();
int n = 0;
for (int i = 0; i < count && n < maxCount; i++) {
const auto* iface = ::Net::NetIf::At(i);
if (iface == nullptr) continue;
NetIfInfo& info = out[n];
for (uint64_t k = 0; k < sizeof(info.name); k++) info.name[k] = '\0';
for (uint64_t k = 0; k + 1 < sizeof(info.name) && iface->Name && iface->Name[k]; k++)
info.name[k] = iface->Name[k];
const uint8_t* mac = iface->GetMac ? iface->GetMac() : nullptr;
for (int k = 0; k < 6; k++) info.mac[k] = mac ? mac[k] : 0;
info.kind = iface->Type == ::Net::NetIf::Kind::Wireless
? NETIF_KIND_WIRELESS : NETIF_KIND_ETHERNET;
info.linkUp = (iface->IsLinkUp && iface->IsLinkUp()) ? 1 : 0;
info.active = iface == active ? 1 : 0;
info._pad[0] = info._pad[1] = info._pad[2] = 0;
n++;
}
return n;
}
static int Sys_SetNetCfg(const NetCfg* in) {
if (in == nullptr) return -1;
Net::SetIpAddress(in->ipAddress);
+14
View File
@@ -457,6 +457,20 @@ namespace montauk::abi {
return Sys_WifiConnect((const char*)frame->arg1, (const char*)frame->arg2);
case SYS_WIFI_DISCONNECT:
return Sys_WifiDisconnect();
case SYS_WIFI_SCAN_START:
return Sys_WifiScanStart((uint32_t)frame->arg1);
case SYS_WIFI_RESULTS:
if ((int64_t)frame->arg2 < 0) return -1;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(WifiNetwork), true)) return -1;
return Sys_WifiResults((WifiNetwork*)frame->arg1, (int)frame->arg2);
case SYS_WIFI_CONNECT_ASYNC:
if (!UserMemory::String(frame->arg1, 64)) return -1;
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, 128)) return -1;
return Sys_WifiConnectAsync((const char*)frame->arg1, (const char*)frame->arg2);
case SYS_NETIFS:
if ((int64_t)frame->arg2 < 0) return -1;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(NetIfInfo), true)) return -1;
return Sys_NetIfs((NetIfInfo*)frame->arg1, (int)frame->arg2);
case SYS_SUSPEND:
return Sys_Suspend();
case SYS_SETTZ:
+24
View File
@@ -302,6 +302,10 @@ namespace montauk::abi {
static constexpr uint64_t SYS_WIFI_INFO = 159; // (WifiInfo*) -> 0, -1 if absent
static constexpr uint64_t SYS_WIFI_CONNECT = 160; // (ssid, password) -> 0, <0 on error
static constexpr uint64_t SYS_WIFI_DISCONNECT = 161; // () -> 0
static constexpr uint64_t SYS_WIFI_SCAN_START = 162; // (timeoutMs) -> 0 started, 1 busy, -1 no adapter
static constexpr uint64_t SYS_WIFI_RESULTS = 163; // (WifiNetwork*, maxCount) -> count, no radio work
static constexpr uint64_t SYS_WIFI_CONNECT_ASYNC = 164; // (ssid, password) -> 0 accepted, <0 on error
static constexpr uint64_t SYS_NETIFS = 165; // (NetIfInfo*, maxCount) -> count
// Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM).
static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz
@@ -708,6 +712,26 @@ namespace montauk::abi {
uint8_t bssid[6];
uint8_t connected; // 1 once the link can carry IP traffic
uint8_t channel;
int32_t lastError; // WIFI_ERR_* from the last async join, 0 = none
uint32_t scanGeneration; // bumped every time a scan finishes
uint8_t joining; // 1 while an async join is in flight
uint8_t _pad[3];
};
// Link-layer interface kinds reported in NetIfInfo.kind.
static constexpr uint8_t NETIF_KIND_ETHERNET = 0;
static constexpr uint8_t NETIF_KIND_WIRELESS = 1;
// One registered link-layer interface (returned by SYS_NETIFS). The IP
// configuration is global to the stack, so it belongs to whichever
// interface reports active = 1.
struct NetIfInfo {
char name[16]; // "eth0", "wlan0"
uint8_t mac[6];
uint8_t kind; // NETIF_KIND_*
uint8_t linkUp;
uint8_t active; // 1 if this is the interface carrying traffic
uint8_t _pad[3];
};
struct ThermalInfo {
+15
View File
@@ -23,11 +23,26 @@ namespace montauk::abi {
return (int64_t)Drivers::Net::Wifi::GetInfo(buf);
}
static int64_t Sys_WifiScanStart(uint32_t timeoutMs) {
return (int64_t)Drivers::Net::Wifi::StartScan(timeoutMs);
}
static int64_t Sys_WifiResults(WifiNetwork* buf, int maxCount) {
if (!buf || maxCount <= 0) return -1;
if (maxCount > 64) maxCount = 64;
return (int64_t)Drivers::Net::Wifi::GetResults(buf, maxCount);
}
static int64_t Sys_WifiConnect(const char* ssid, const char* password) {
if (!ssid) return -1;
return (int64_t)Drivers::Net::Wifi::Connect(ssid, password);
}
static int64_t Sys_WifiConnectAsync(const char* ssid, const char* password) {
if (!ssid) return -1;
return (int64_t)Drivers::Net::Wifi::ConnectAsync(ssid, password);
}
static int64_t Sys_WifiDisconnect() {
return (int64_t)Drivers::Net::Wifi::Disconnect();
}
+23
View File
@@ -302,6 +302,21 @@ namespace Drivers::Net::Wifi {
bool IwxSendCmdStatus(uint32_t id, const void* data, uint32_t len,
uint32_t* statusOut);
// Bracket a pass of the idle-loop service work. The wait inside
// IwxSendCmd is a busy spin on a core the scheduler has been told not to
// touch, so a pass is given one command's worth of waiting in total and
// stops sending once that is gone; the rest is picked up next pass. The
// bring-up path does not bracket itself and keeps the full per-command
// budget. See the comment above IwxCmdWaitBudgetMs().
// Returns false if another core already owns this pass; only the owner
// may end it.
bool IwxBeginServicePass();
void IwxEndServicePass();
// Forget the accumulated command-failure score and the one-shot error
// dump latch. Called after the adapter has been brought back up.
void IwxResetCmdHealth();
// Poll interrupt causes + drain the RX/notification ring. Safe to call
// from any process/idle context; self-guarded against reentry.
void IwxProcessEvents();
@@ -387,6 +402,14 @@ namespace Drivers::Net::Wifi {
const uint8_t* rsnIe, uint32_t rsnIeLen,
uint16_t beaconInterval, uint8_t dtimPeriod);
void IwxConnectAbort();
// Drop all connection state without talking to the firmware. For the
// recovery path, where the adapter is being reset out from under the state
// machine and IwxConnectAbort()'s teardown commands would only burn
// timeouts against a device that is about to be reinitialised anyway.
void IwxConnectReset();
// Per-frame transmit outcome, reported by the TX completion path. Feeds
// the link supervision described in IwxConnect.cpp.
void IwxConnectNoteTx(bool acked);
// Why the last IwxConnectStart() refused: false means the radio or the
// firmware failed, true means the network's security is unsupported. The
// two need very different advice, so they must not be conflated.
@@ -75,6 +75,28 @@ namespace Drivers::Net::Wifi {
static volatile bool g_postAssocPending = false;
static volatile bool g_teardownPending = false;
static volatile bool g_sendAssocPending = false;
static volatile bool g_linkLostPending = false;
// Link supervision.
//
// IwxLinkUp() is nothing but "the state machine reached Connected", and
// until now nothing moved it back 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 selecting wlan0, every
// packet went into the void, and the desktop showed a healthy connection
// while nothing resolved and nothing connected.
//
// Beacon loss is not observable from here: once associated, MacConfigCmd
// stops asking for beacons and the firmware tracks them itself. What is
// observable is that our own frames stop being acknowledged -- IwxTxComplete
// gets a per-frame status. A long enough run of failures with nothing
// heard from the BSS in between means the access point is gone. The
// threshold is in frames rather than time because it must not fire on an
// idle link that simply has nothing to send.
static volatile uint32_t g_txFailStreak = 0;
static constexpr uint32_t TX_FAIL_STREAK_LIMIT = 16;
// Timers for retransmission and give-up.
static uint64_t g_stateEnteredMs = 0;
@@ -585,6 +607,10 @@ namespace Drivers::Net::Wifi {
g_iwx.RxDataPackets++;
// Anything at all arriving from the BSS proves the access point is
// still there, so the transmit-failure streak starts over.
g_txFailStreak = 0;
if (etherType == ETHERTYPE_EAPOL) {
KernelLogStream(INFO, "WiFi") << "EAPOL frame received ("
<< (uint64_t)payloadLen << " bytes)";
@@ -810,6 +836,46 @@ namespace Drivers::Net::Wifi {
return true;
}
// Called from the TX completion path (inside the RX pump), so it may only
// set a flag; ServiceLocked() does the actual teardown, where sending the
// firmware commands it needs is allowed.
void IwxConnectNoteTx(bool acked) {
if (acked) { g_txFailStreak = 0; return; }
if (g_state != ConnState::Connected) return;
if (++g_txFailStreak >= TX_FAIL_STREAK_LIMIT) {
g_txFailStreak = 0;
g_linkLostPending = true;
}
}
// Drop everything without touching the firmware. Used by the recovery
// path, which is about to reinitialise the adapter: the teardown commands
// IwxConnectAbort() would send have nothing to talk to and would only burn
// a timeout each.
void IwxConnectReset() {
WpaReset();
g_keysInstalled = false;
g_ptkKey = {};
g_gtkKey = {};
g_phyActive = false;
g_macActive = false;
g_macAssoc = false;
g_bindingActive = false;
g_linkActive = false;
g_staActive = false;
g_aid = 0;
g_eapolHead = 0;
g_eapolTail = 0;
g_txFailStreak = 0;
g_postAssocPending = false;
g_teardownPending = false;
g_sendAssocPending = false;
g_linkLostPending = false;
memset(&g_wpaCfg, 0, sizeof(g_wpaCfg));
g_secured = false;
g_state = ConnState::Idle;
}
void IwxConnectAbort() {
if (g_state == ConnState::Idle) return;
// Tell the AP we are leaving, but only while the station context (and
@@ -884,6 +950,22 @@ namespace Drivers::Net::Wifi {
return;
}
// The access point stopped acknowledging anything. Bring the link down
// rather than leaving the stack transmitting into a hole: NetIf can
// then fall back to a wired interface, and the desktop reports the
// truth instead of a connection that only exists on paper.
if (g_linkLostPending) {
g_linkLostPending = false;
if (g_state != ConnState::Idle) {
KernelLogStream(WARNING, "WiFi")
<< "\"" << g_ssid << "\" stopped acknowledging frames; "
<< "dropping the link";
TearDown();
g_state = ConnState::Idle;
}
return;
}
if (g_state == ConnState::Idle || g_state == ConnState::Failed) return;
if (g_sendAssocPending) {
+102 -14
View File
@@ -869,11 +869,84 @@ namespace Drivers::Net::Wifi {
}
void IwxDumpFwError();
static uint32_t g_cmdTimeouts = 0; // consecutive unanswered commands
// Counting *consecutive* unanswered commands is not enough. An adapter
// that answers some commands and drops others resets a consecutive counter
// on every success and so never reaches the cutoff, while each drop still
// costs a full timeout of busy-waiting -- and that is exactly the state a
// marginal access point leaves the firmware in. The result was a machine
// that stalled a second at a time, indefinitely, with the driver never
// concluding anything was wrong. Count in a leaky bucket instead: a
// failure adds one, a success drains one, so a firmware failing even a
// fraction of its commands still trips the cutoff in bounded time.
static uint32_t g_cmdFailScore = 0;
static bool g_fwErrorDumped = false;
static constexpr uint32_t CMD_FAIL_SCORE_MAX = 6;
void IwxResetCmdHealth() {
g_cmdFailScore = 0;
g_fwErrorDumped = false;
}
// How long one command may wait, and whether it may be sent at all.
//
// The bring-up path reserves its CPU deliberately and has nothing to
// starve, so it keeps the full second iwlwifi allows. The idle service
// pass is a different animal: ServiceDeferredWork() sets
// reservedForKernelWork, which makes that core ineligible to run any
// process and immune to the reschedule IPI (Scheduler.cpp), and on the BSP
// it also defers RunBspMaintenance() -- the thing that wakes sleeping
// processes. The wait below is a busy spin, not a sleep. So a pass that
// issues eight commands (TearDown does exactly that) against an
// unresponsive firmware pins a core for eight seconds, which is what makes
// the mouse crawl when an access point goes bad.
//
// A pass therefore gets one command's worth of waiting in total.
static constexpr uint32_t IWX_CMD_TIMEOUT_MS = 1000;
static constexpr uint32_t IWX_PASS_BUDGET_MS = 1000;
// Below this there is no point starting a command: it would be abandoned
// almost immediately, and an abandoned command still holds its ring slot
// and may be answered later, which the *next* command would misread as its
// own completion.
static constexpr uint32_t IWX_CMD_MIN_WAIT_MS = 50;
static uint64_t g_passDeadline = 0; // 0 = not inside a pass
static volatile bool g_passOwned = false;
// Every idling core runs ServiceEvents(), so the bracket needs an owner:
// otherwise the second core to arrive would clear the first core's deadline
// on its way out and hand it back the unbounded wait this exists to
// prevent. A core that does not win still runs under the winner's
// deadline, which is strictly tighter than none.
bool IwxBeginServicePass() {
if (__atomic_test_and_set(&g_passOwned, __ATOMIC_ACQUIRE)) return false;
g_passDeadline = Timekeeping::GetMilliseconds() + IWX_PASS_BUDGET_MS;
return true;
}
void IwxEndServicePass() {
g_passDeadline = 0;
__atomic_clear(&g_passOwned, __ATOMIC_RELEASE);
}
// Remaining wait allowance, clamped to the per-command timeout.
static uint32_t IwxCmdWaitBudgetMs() {
if (!g_passDeadline) return IWX_CMD_TIMEOUT_MS;
uint64_t now = Timekeeping::GetMilliseconds();
if (now >= g_passDeadline) return 0;
uint64_t left = g_passDeadline - now;
return left > IWX_CMD_TIMEOUT_MS ? IWX_CMD_TIMEOUT_MS : (uint32_t)left;
}
bool IwxSendCmd(IwxHostCmd& hcmd) {
if (g_iwx.State == IwxFwState::Error) return false;
// Out of budget for this pass. Fail quietly -- the firmware has done
// nothing wrong, so this must not count against it -- and let the
// caller's state machine retry on the next pass.
uint32_t waitMs = IwxCmdWaitBudgetMs();
if (waitMs < IWX_CMD_MIN_WAIT_MS) return false;
IwxTxRing& ring = g_iwx.CmdQ;
g_iwx.CmdLock.Acquire();
@@ -961,12 +1034,16 @@ namespace Drivers::Net::Wifi {
constexpr uint32_t MAX_SPINS = 20000; // ~2 s at 100 us
bool ok = false;
bool died = false;
// Re-read the allowance: acquiring CmdLock above can itself have waited
// out another core's command, and the budget is for the pass, not for
// each caller's view of it when it arrived.
waitMs = IwxCmdWaitBudgetMs();
uint64_t start = Timekeeping::GetMilliseconds();
for (uint32_t spins = 0; spins < MAX_SPINS; spins++) {
IwxProcessEvents();
if (g_iwx.CmdDone) { ok = true; break; }
if (g_iwx.State == IwxFwState::Error) { died = true; break; }
if (Timekeeping::GetMilliseconds() - start >= 1000) break;
if (Timekeeping::GetMilliseconds() - start >= waitMs) break;
IwxDelayUs(100);
}
@@ -979,20 +1056,26 @@ namespace Drivers::Net::Wifi {
if (!died) {
// Dump on the first silence: the firmware's error table names
// the command that asserted, and it is overwritten as later
// commands go unanswered.
if (++g_cmdTimeouts == 1) IwxDumpFwError();
// Repeated silence means it is wedged and every later command
// would burn the same timeout, so stop trying. A single late
// response is not worth disabling the adapter over.
if (g_cmdTimeouts >= 3) {
// commands go unanswered. Latched separately from the score,
// which now goes up and down and would otherwise re-dump every
// time it passed through one.
g_cmdFailScore++;
if (!g_fwErrorDumped) {
g_fwErrorDumped = true;
IwxDumpFwError();
}
// Enough net silence means it is wedged and every later command
// would burn the same timeout, so stop trying and let
// ServiceRecovery() put the adapter back together.
if (g_cmdFailScore >= CMD_FAIL_SCORE_MAX) {
KernelLogStream(ERROR, "WiFi")
<< "Firmware stopped responding to host commands";
g_iwx.FwErrors++;
g_iwx.State = IwxFwState::Error;
}
}
} else {
g_cmdTimeouts = 0;
} else if (g_cmdFailScore) {
g_cmdFailScore--;
}
g_iwx.CmdWantResp = false;
@@ -1272,10 +1355,15 @@ namespace Drivers::Net::Wifi {
if (ring->Queued > 0) ring->Queued--;
g_iwx.TxLock.Release();
if (status == IWX_TX_STATUS_SUCCESS || status == IWX_TX_STATUS_DIRECT_DONE)
g_iwx.TxPackets++;
else
g_iwx.TxFailures++;
bool acked = status == IWX_TX_STATUS_SUCCESS
|| status == IWX_TX_STATUS_DIRECT_DONE;
if (acked) g_iwx.TxPackets++;
else g_iwx.TxFailures++;
// Feeds the link supervision in IwxConnect.cpp: an access point that
// vanishes without deauthenticating is only visible as our frames
// going unacknowledged.
IwxConnectNoteTx(acked);
}
// =========================================================================
+282 -20
View File
@@ -61,6 +61,22 @@ namespace Drivers::Net::Wifi {
static int g_resultCount = 0;
static kcp::Spinlock g_resultLock;
// =========================================================================
// Non-blocking scan / join state
//
// The GUI cannot afford the blocking Scan() and Connect() below: they take
// seconds, and the compositor calling them would freeze the whole desktop.
// The async entry points start the same work and return immediately; the
// deadlines and the final connect result are looked after by ServiceAsync()
// from the idle loop, and the caller polls GetInfo().
// =========================================================================
static uint64_t g_scanDeadline = 0; // 0 = no async scan outstanding
static uint32_t g_scanGeneration = 0; // bumped when a scan finishes
static bool g_asyncConnect = false;
static uint64_t g_asyncConnectDeadline = 0;
static int g_lastError = 0; // result of the last async join
static void ClearResults() {
g_resultLock.Acquire();
for (int i = 0; i < MAX_SCAN_RESULTS; i++) g_results[i].Used = false;
@@ -305,6 +321,102 @@ namespace Drivers::Net::Wifi {
<< g_iwx.Fw.Version << ")";
}
// =========================================================================
// Recovery from a wedged adapter
//
// IwxFwState::Error used to be a one-way door. Nothing anywhere cleared
// it, so a single firmware assert -- which a flaky access point provokes
// readily enough -- left StartJoin() returning WIFI_ERR_NO_ADAPTER ("no
// adapter is ready") for the rest of the uptime, and the only way back was
// a reboot.
//
// The firmware image is still parsed and resident (IwxReadFirmware() is
// idempotent and keeps g_iwx.Fw.Raw), so the cure is simply to stop the
// device and run the same bring-up again. It is not free -- the handshakes
// inside take a couple of seconds with this core reserved -- so it is
// rate-limited and capped. If the adapter will not come back after a few
// tries it is genuinely broken and repeating the reset would be its own
// kind of stall.
// =========================================================================
static uint32_t g_recoveryAttempts = 0;
static uint64_t g_lastRecoveryMs = 0;
static bool g_recoveryGaveUp = false;
static constexpr uint32_t MAX_RECOVERY_ATTEMPTS = 3;
static constexpr uint64_t RECOVERY_BACKOFF_MS = 5000;
static void ServiceRecovery() {
if (g_iwx.State != IwxFwState::Error || g_recoveryGaveUp) return;
// An adapter that never finished its first bring-up is ServiceDeferredInit's
// problem, not this one.
if (!g_initialized) return;
// Every idling core reaches here, and two of them resetting the device
// at once would be considerably worse than the fault being recovered
// from. Same test-and-set gate ServiceAsync() and IwxConnectService()
// use; the loser has nothing to do.
static volatile bool inRecovery = false;
if (__atomic_test_and_set(&inRecovery, __ATOMIC_ACQUIRE)) return;
struct Guard {
volatile bool* flag;
~Guard() { __atomic_clear(flag, __ATOMIC_RELEASE); }
} guard{&inRecovery};
// Re-check under the gate: the winner of the race may have just
// finished a reset that fixed things.
if (g_iwx.State != IwxFwState::Error) return;
uint64_t now = Timekeeping::GetMilliseconds();
if (g_lastRecoveryMs && now - g_lastRecoveryMs < RECOVERY_BACKOFF_MS) return;
g_lastRecoveryMs = now;
if (++g_recoveryAttempts > MAX_RECOVERY_ATTEMPTS) {
g_recoveryGaveUp = true;
KernelLogStream(ERROR, "WiFi")
<< "Adapter did not come back after " << (uint64_t)MAX_RECOVERY_ATTEMPTS
<< " resets; leaving it down";
return;
}
KernelLogStream(WARNING, "WiFi") << "Resetting the adapter after a firmware error"
<< " (attempt " << (uint64_t)g_recoveryAttempts << " of "
<< (uint64_t)MAX_RECOVERY_ATTEMPTS << ")";
// Nothing may be mid-command while the device is torn down. Taking
// CmdLock is enough: it is what serializes every sender, and the RX
// pump has its own reentrancy guard.
g_iwx.CmdLock.Acquire();
g_initialized = false;
g_asyncConnect = false;
g_scanDeadline = 0;
g_lastError = WIFI_ERR_NO_ADAPTER;
g_iwx.ScanActive = false;
// Not IwxConnectAbort(): its teardown commands would be sent to a
// device that has already stopped answering, costing a timeout each for
// contexts that the reset below discards anyway.
IwxConnectReset();
ClearResults();
IwxStopDevice();
IwxResetCmdHealth();
// Detected, not Absent: the PCI device is still claimed and mapped, and
// IsPresent() keys off Absent -- the Wi-Fi icon must not blink out of
// the panel every time the adapter is reset.
g_iwx.State = IwxFwState::Detected;
g_iwx.CmdLock.Release();
CompleteInit();
if (g_initialized) {
KernelLogStream(OK, "WiFi") << "Adapter recovered";
g_recoveryAttempts = 0;
g_lastError = 0;
}
}
void ServiceDeferredInit() {
if (!g_initPending.load(std::memory_order_relaxed) || g_initialized) return;
if (!Fs::Vfs::IsDriveRegistered(0)) return; // ramdisk not mounted yet
@@ -325,12 +437,36 @@ namespace Drivers::Net::Wifi {
if (cpu) cpu->reservedForKernelWork = wasReserved;
}
static void ServiceAsync();
static void ServiceRecovery();
void ServiceEvents() {
if (!g_iwx.Mmio) return;
if (g_iwx.WorkPending) IwxProcessEvents();
// Everything below can send firmware commands, and each one waits by
// busy-spinning. This runs from ServiceDeferredWork(), which has set
// reservedForKernelWork on this core -- so the scheduler will not run a
// process here and, on the BSP, RunBspMaintenance() is not reached
// until we return. The pass budget caps the whole group at roughly one
// command's wait; whatever does not fit is retried next time round the
// idle loop. Without it a teardown against a wedged adapter held a
// core for the better part of ten seconds, which is what the stalled
// cursor and the stuttering desktop actually were.
bool ownsPass = IwxBeginServicePass();
// Firmware commands the RX path deferred (it runs under the event
// pump's reentrancy guard and cannot wait for a completion itself).
IwxConnectService();
// Deadlines for the non-blocking scan/join the GUI drives. Runs after
// the pump returns, never inside it, because both paths send commands.
ServiceAsync();
if (ownsPass) IwxEndServicePass();
// Deliberately outside the budget: a reset is a bring-up, not a pass of
// routine servicing, and it has its own wall-clock handshakes to run.
ServiceRecovery();
}
bool IsInitialized() { return g_initialized; }
@@ -340,6 +476,35 @@ namespace Drivers::Net::Wifi {
// Public operations
// =========================================================================
// Copy the current scan table out. No radio work: whatever the last scan
// (blocking or not) left behind is what the caller sees.
static int CopyResults(WifiNetwork* out, int maxCount) {
g_resultLock.Acquire();
int n = 0;
for (int i = 0; i < MAX_SCAN_RESULTS && n < maxCount; i++) {
if (!g_results[i].Used) continue;
const ScanEntry& e = g_results[i];
WifiNetwork& w = out[n];
memset(&w, 0, sizeof(w));
for (int k = 0; k < 32 && e.Ssid[k]; k++) w.ssid[k] = e.Ssid[k];
memcpy(w.bssid, e.Bssid, 6);
w.channel = e.Channel;
w.rssi = e.Rssi;
w.band = e.Band;
w.security = e.Security;
w.beaconInterval = e.BeaconInterval;
n++;
}
g_resultLock.Release();
return n;
}
int GetResults(WifiNetwork* out, int maxCount) {
if (!out || maxCount <= 0) return -1;
if (!g_initialized) return -1;
return CopyResults(out, maxCount);
}
int Scan(WifiNetwork* out, int maxCount, uint32_t timeoutMs) {
if (!out || maxCount <= 0) return -1;
if (!g_initialized) return -1;
@@ -365,24 +530,23 @@ namespace Drivers::Net::Wifi {
while (Timekeeping::GetMilliseconds() - t0 < 200) IwxProcessEvents();
}
g_resultLock.Acquire();
int n = 0;
for (int i = 0; i < MAX_SCAN_RESULTS && n < maxCount; i++) {
if (!g_results[i].Used) continue;
const ScanEntry& e = g_results[i];
WifiNetwork& w = out[n];
memset(&w, 0, sizeof(w));
for (int k = 0; k < 32 && e.Ssid[k]; k++) w.ssid[k] = e.Ssid[k];
memcpy(w.bssid, e.Bssid, 6);
w.channel = e.Channel;
w.rssi = e.Rssi;
w.band = e.Band;
w.security = e.Security;
w.beaconInterval = e.BeaconInterval;
n++;
}
g_resultLock.Release();
return n;
g_scanGeneration++;
return CopyResults(out, maxCount);
}
int StartScan(uint32_t timeoutMs) {
if (!g_initialized) return -1;
if (g_iwx.State != IwxFwState::Running) return -1;
if (g_iwx.ScanActive) return 1; // already sweeping
if (timeoutMs < 1000) timeoutMs = 1000;
if (timeoutMs > 20000) timeoutMs = 20000;
ClearResults();
if (!IwxStartScan(nullptr)) return -1;
g_scanDeadline = Timekeeping::GetMilliseconds() + timeoutMs;
return 0;
}
int GetInfo(WifiInfo* out) {
@@ -403,6 +567,9 @@ namespace Drivers::Net::Wifi {
out->fwErrors = (uint32_t)g_iwx.FwErrors;
out->connState = (uint32_t)IwxConnectState();
out->connected = IwxLinkUp() ? 1 : 0;
out->lastError = (int32_t)g_lastError;
out->scanGeneration = g_scanGeneration;
out->joining = g_asyncConnect ? 1 : 0;
if (IwxConnectState() != (int)IwxConnStateId::Idle) {
const char* ssid = IwxConnectSsid();
@@ -455,7 +622,12 @@ namespace Drivers::Net::Wifi {
return WIFI_ERR_TIMEOUT;
}
int Connect(const char* ssid, const char* password) {
// Everything a join needs before the exchange with the AP starts: find the
// BSS, check the ciphers are ones the supplicant implements, and hand the
// firmware its contexts. Returns 0 once the state machine is running, or a
// WIFI_ERR_* value. `rescan` controls whether an SSID missing from the
// scan table is worth a (blocking) sweep to look for it.
static int StartJoin(const char* ssid, const char* password, bool rescan) {
if (!g_initialized || !ssid) return WIFI_ERR_NO_ADAPTER;
if (g_iwx.State != IwxFwState::Running) return WIFI_ERR_NO_ADAPTER;
@@ -476,7 +648,8 @@ namespace Drivers::Net::Wifi {
// joined through the best AP rather than whichever answered first.
int8_t bestRssi = -128;
for (int attempt = 0; attempt < 2 && !found; attempt++) {
int attempts = rescan ? 2 : 1;
for (int attempt = 0; attempt < attempts && !found; attempt++) {
if (attempt == 1) {
// Nothing matched: the caller may never have scanned, or the
// results may predate this network appearing.
@@ -547,15 +720,104 @@ namespace Drivers::Net::Wifi {
return security_ ? WIFI_ERR_UNSUPPORTED : WIFI_ERR_FAILED;
}
return 0;
}
int Connect(const char* ssid, const char* password) {
int rc = StartJoin(ssid, password, true);
if (rc != 0) return rc;
return WaitForConnection(15000);
}
int ConnectAsync(const char* ssid, const char* password) {
// A join already in flight owns the firmware contexts; tear it down
// rather than stacking a second one on top.
if (g_asyncConnect || (IwxConnectState() != (int)IwxConnStateId::Idle))
IwxConnectAbort();
g_asyncConnect = false;
g_lastError = 0;
// No blocking rescan here: the caller has a scan table on screen, and
// the point of this entry point is that it returns immediately.
int rc = StartJoin(ssid, password, false);
if (rc != 0) {
g_lastError = rc;
return rc;
}
g_asyncConnect = true;
g_asyncConnectDeadline = Timekeeping::GetMilliseconds() + 20000;
return 0;
}
int Disconnect() {
if (!g_initialized) return -1;
g_asyncConnect = false;
g_lastError = 0;
IwxConnectAbort();
return 0;
}
// Deadlines and completion for the non-blocking entry points. Called from
// ServiceEvents() after the RX pump has returned, so sending commands (the
// scan abort, the connect teardown) is safe here.
//
// Every idling core calls this, so it takes the same test-and-set gate
// IwxConnectService() uses: two cores both deciding a join has failed would
// tear the contexts down twice. A core that loses the race has nothing to
// do - the winner is already doing it.
static void ServiceAsync() {
static volatile bool inService = false;
if (__atomic_test_and_set(&inService, __ATOMIC_ACQUIRE)) return;
struct Guard {
volatile bool* flag;
~Guard() { __atomic_clear(flag, __ATOMIC_RELEASE); }
} guard{&inService};
uint64_t now = Timekeeping::GetMilliseconds();
if (g_scanDeadline) {
if (!g_iwx.ScanActive) {
g_scanDeadline = 0;
g_scanGeneration++;
} else if (now >= g_scanDeadline) {
IwxAbortScan();
g_scanDeadline = 0;
g_scanGeneration++;
}
}
if (!g_asyncConnect) return;
auto state = (IwxConnStateId)IwxConnectState();
if (state == IwxConnStateId::Connected) {
g_asyncConnect = false;
g_lastError = 0;
return;
}
if (state == IwxConnStateId::Failed) {
// Same reading as the blocking path: a handshake that exchanged
// EAPOL frames and then failed is almost always a wrong passphrase.
g_lastError = WpaGetState() == WpaState::Failed
? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
g_asyncConnect = false;
IwxConnectAbort();
return;
}
if (state == IwxConnStateId::Idle || g_iwx.State == IwxFwState::Error) {
g_lastError = WIFI_ERR_FAILED;
g_asyncConnect = false;
if (state != IwxConnStateId::Idle) IwxConnectAbort();
return;
}
if (now >= g_asyncConnectDeadline) {
g_lastError = WIFI_ERR_TIMEOUT;
g_asyncConnect = false;
IwxConnectAbort();
}
}
// =========================================================================
// Network interface
// =========================================================================
+14
View File
@@ -29,6 +29,14 @@ namespace Drivers::Net::Wifi {
// Returns the number of entries written, or -1 on error.
int Scan(montauk::abi::WifiNetwork* out, int maxCount, uint32_t timeoutMs);
// Start a scan and return at once: 0 started, 1 one was already running,
// -1 no adapter. GetInfo().scanning falls back to 0 when it finishes and
// GetInfo().scanGeneration moves on; the results are read with GetResults.
int StartScan(uint32_t timeoutMs);
// Copy out the current scan table without touching the radio.
int GetResults(montauk::abi::WifiNetwork* out, int maxCount);
// Fill in adapter/firmware status.
int GetInfo(montauk::abi::WifiInfo* out);
@@ -36,6 +44,12 @@ namespace Drivers::Net::Wifi {
// or the attempt fails. Returns 0 on success, or a negative WIFI_ERR_*
// value describing why it could not connect.
int Connect(const char* ssid, const char* password);
// Start a join and return at once: 0 accepted, or a WIFI_ERR_* the attempt
// failed on before any frame went out. Progress shows up in
// GetInfo().connState / .joining, and the outcome in .lastError.
int ConnectAsync(const char* ssid, const char* password);
int Disconnect();
// -------------------------------------------------------------------------