feat: wi-fi - expand support and fix issues, add GUI components
This commit is contained in:
@@ -12,4 +12,4 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define MONTAUK_BUILD_NUMBER 69
|
||||
#define MONTAUK_BUILD_NUMBER 84
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
||||
@@ -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
|
||||
// =========================================================================
|
||||
|
||||
@@ -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();
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user