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
+161
View File
@@ -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
+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();
// -------------------------------------------------------------------------
+24
View File
@@ -224,6 +224,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
@@ -633,6 +637,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 {
+35
View File
@@ -180,6 +180,41 @@ struct DesktopState {
uint64_t net_cfg_last_poll;
Rect net_icon_rect;
// Registered link-layer interfaces. The IP configuration is global to the
// stack, so the wired and wireless popups use `active` to decide which of
// them owns the address currently on screen.
static constexpr int MAX_NETIFS = 4;
montauk::abi::NetIfInfo netifs[MAX_NETIFS];
int netif_count;
bool eth_present;
// ---- Wi-Fi -------------------------------------------------------------
// The panel entry exists only when a supported adapter is present.
static constexpr int MAX_WIFI_NETWORKS = 32;
static constexpr int WIFI_SSID_CAP = 36;
static constexpr int WIFI_PSK_CAP = 72;
SvgIcon icon_wifi;
bool wifi_present;
bool wifi_popup_open;
Rect wifi_icon_rect;
montauk::abi::WifiInfo wifi_info;
montauk::abi::WifiNetwork wifi_networks[MAX_WIFI_NETWORKS];
int wifi_network_count;
uint64_t wifi_last_poll;
uint32_t wifi_scan_generation;
bool wifi_scanning;
int wifi_scroll; // first visible row of the list
bool wifi_boot_scan_started; // the automatic scan at startup
bool wifi_autoconnect_done; // saved-network join already tried
bool wifi_joining; // a join we started is in flight
bool wifi_dhcp_pending; // ask for a lease once the link is up
bool wifi_dhcp_waiting; // dhcp.elf running, no address yet
char wifi_joining_ssid[WIFI_SSID_CAP];
char wifi_status[96]; // last result line in the popup
uint64_t wifi_status_time;
bool vol_popup_open;
Rect vol_icon_rect;
int vol_level; // 0-100
+4
View File
@@ -185,6 +185,10 @@ extern "C" {
#define MTK_SYS_WIFI_INFO 159
#define MTK_SYS_WIFI_CONNECT 160
#define MTK_SYS_WIFI_DISCONNECT 161
#define MTK_SYS_WIFI_SCAN_START 162
#define MTK_SYS_WIFI_RESULTS 163
#define MTK_SYS_WIFI_CONNECT_ASYNC 164
#define MTK_SYS_NETIFS 165
/* @SYSCALLS-END */
#define MTK_SOCK_TCP 1
+29 -2
View File
@@ -607,8 +607,8 @@ namespace montauk {
inline int wifi_info(montauk::abi::WifiInfo* out) {
return (int)syscall1(montauk::abi::SYS_WIFI_INFO, (uint64_t)out);
}
// Association is groundwork only: open networks bring the firmware
// contexts up, encrypted ones are rejected with -2.
// Join a network. Blocks until the link is up or the attempt fails, and
// returns 0 or a WIFI_ERR_* code.
inline int wifi_connect(const char* ssid, const char* password) {
return (int)syscall2(montauk::abi::SYS_WIFI_CONNECT, (uint64_t)ssid,
(uint64_t)password);
@@ -617,6 +617,33 @@ namespace montauk {
return (int)syscall0(montauk::abi::SYS_WIFI_DISCONNECT);
}
// Non-blocking pair for GUI code, which cannot stall for the seconds a
// sweep or a handshake takes. scan_start() kicks off a sweep (0 started,
// 1 one was already running, -1 no adapter); wifi_info().scanning drops
// back to 0 and .scanGeneration moves on when it finishes, and
// wifi_results() copies out the table without touching the radio.
inline int wifi_scan_start(uint32_t timeoutMs) {
return (int)syscall1(montauk::abi::SYS_WIFI_SCAN_START, (uint64_t)timeoutMs);
}
inline int wifi_results(montauk::abi::WifiNetwork* buf, int maxCount) {
return (int)syscall2(montauk::abi::SYS_WIFI_RESULTS, (uint64_t)buf,
(uint64_t)maxCount);
}
// Returns 0 once the join is under way, or a WIFI_ERR_* it failed on
// before any frame went out. Watch wifi_info().joining for progress and
// .lastError for the outcome.
inline int wifi_connect_async(const char* ssid, const char* password) {
return (int)syscall2(montauk::abi::SYS_WIFI_CONNECT_ASYNC, (uint64_t)ssid,
(uint64_t)password);
}
// List the registered link-layer interfaces. The IP configuration is
// global to the stack; it belongs to whichever entry has active = 1.
inline int net_interfaces(montauk::abi::NetIfInfo* buf, int maxCount) {
return (int)syscall2(montauk::abi::SYS_NETIFS, (uint64_t)buf,
(uint64_t)maxCount);
}
// Software-defined radio (Rx). Receivers are identified by index [0, count);
// open() returns a handle used by the rest of the calls. Samples are read
// as interleaved 8-bit unsigned I/Q (CU8) from the device's ring buffer.
+261
View File
@@ -0,0 +1,261 @@
/*
* wifi.h
* Saved Wi-Fi networks (0:/config/wifi.toml) and small formatting helpers
* shared by the desktop panel, the Network app and the wifi command.
*
* The file looks like this:
*
* [wifi]
* autoconnect = true
*
* [network.0]
* ssid = "Home"
* psk = "passphrase"
*
* 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). Anyone who can read 0:/config can read the keys.
*
* No default copy of this file ships in the image, deliberately. Every other
* config an app writes (bluetooth.toml, display.toml, session.toml) is
* created on demand for the same reason: a shipped copy is laid down again
* by anything that refreshes the system files, and it would overwrite the
* networks the user had saved.
*
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <montauk/config.h>
#include <montauk/string.h>
#include <montauk/syscall.h>
namespace montauk {
namespace wifi {
// The scan table the kernel keeps is 64 entries; saving that many networks
// is already far more than a laptop accumulates.
static constexpr int MAX_SAVED = 32;
static constexpr int SSID_CAP = 36;
static constexpr int PSK_CAP = 72; // 64-character hex PSK plus NUL
struct SavedNetwork {
char ssid[SSID_CAP];
char psk[PSK_CAP];
};
struct SavedList {
SavedNetwork items[MAX_SAVED];
int count;
bool autoconnect;
};
// ---- helpers -----------------------------------------------------------
inline void copy_str(char* dst, int cap, const char* src) {
int i = 0;
for (; src && src[i] && i < cap - 1; i++) dst[i] = src[i];
dst[i] = '\0';
}
// "network.<index>.<field>"
inline void network_key(char* out, int cap, int index, const char* field) {
char idx[8];
int n = 0;
if (index == 0) {
idx[n++] = '0';
} else {
char tmp[8];
int t = 0;
for (int v = index; v > 0 && t < (int)sizeof(tmp); v /= 10)
tmp[t++] = (char)('0' + (v % 10));
while (t > 0) idx[n++] = tmp[--t];
}
idx[n] = '\0';
int p = 0;
const char* prefix = "network.";
while (*prefix && p < cap - 1) out[p++] = *prefix++;
for (int i = 0; i < n && p < cap - 1; i++) out[p++] = idx[i];
if (p < cap - 1) out[p++] = '.';
while (field && *field && p < cap - 1) out[p++] = *field++;
out[p] = '\0';
}
// ---- load / store ------------------------------------------------------
inline void saved_load(SavedList* out) {
if (!out) return;
out->count = 0;
out->autoconnect = true;
auto doc = montauk::config::load("wifi");
out->autoconnect = doc.get_bool("wifi.autoconnect", true);
for (int i = 0; i < MAX_SAVED; i++) {
char key[64];
network_key(key, sizeof(key), i, "ssid");
const char* ssid = doc.get_string(key, nullptr);
if (!ssid || !ssid[0]) break; // entries are written contiguously
network_key(key, sizeof(key), i, "psk");
const char* psk = doc.get_string(key, "");
SavedNetwork& n = out->items[out->count++];
copy_str(n.ssid, SSID_CAP, ssid);
copy_str(n.psk, PSK_CAP, psk);
}
doc.destroy();
}
// Rewrite the file from the list. Returns 0 on success.
inline int saved_store(const SavedList* list) {
if (!list) return -1;
montauk::toml::Doc doc;
doc.init();
montauk::config::set_bool(&doc, "wifi.autoconnect", list->autoconnect);
for (int i = 0; i < list->count && i < MAX_SAVED; i++) {
char key[64];
network_key(key, sizeof(key), i, "ssid");
montauk::config::set_string(&doc, key, list->items[i].ssid);
network_key(key, sizeof(key), i, "psk");
montauk::config::set_string(&doc, key, list->items[i].psk);
}
int rc = montauk::config::save("wifi", &doc);
doc.destroy();
return rc;
}
inline int saved_index_of(const SavedList* list, const char* ssid) {
if (!list || !ssid) return -1;
for (int i = 0; i < list->count; i++) {
if (montauk::streq(list->items[i].ssid, ssid)) return i;
}
return -1;
}
inline const SavedNetwork* saved_find(const SavedList* list, const char* ssid) {
int idx = saved_index_of(list, ssid);
return idx < 0 ? nullptr : &list->items[idx];
}
// Add or update an entry in memory. Returns false when the list is full.
inline bool saved_set(SavedList* list, const char* ssid, const char* psk) {
if (!list || !ssid || !ssid[0]) return false;
int idx = saved_index_of(list, ssid);
if (idx < 0) {
if (list->count >= MAX_SAVED) return false;
idx = list->count++;
copy_str(list->items[idx].ssid, SSID_CAP, ssid);
}
copy_str(list->items[idx].psk, PSK_CAP, psk ? psk : "");
return true;
}
inline bool saved_remove(SavedList* list, const char* ssid) {
int idx = saved_index_of(list, ssid);
if (idx < 0) return false;
for (int i = idx; i < list->count - 1; i++) list->items[i] = list->items[i + 1];
list->count--;
return true;
}
// Convenience wrappers that touch the file directly.
inline bool remember(const char* ssid, const char* psk) {
SavedList list;
saved_load(&list);
if (!saved_set(&list, ssid, psk)) return false;
return saved_store(&list) == 0;
}
inline bool forget(const char* ssid) {
SavedList list;
saved_load(&list);
if (!saved_remove(&list, ssid)) return false;
return saved_store(&list) == 0;
}
// Copy the saved passphrase for `ssid` into out. False when not saved.
inline bool lookup(const char* ssid, char* out, int cap) {
SavedList list;
saved_load(&list);
const SavedNetwork* n = saved_find(&list, ssid);
if (!n) return false;
copy_str(out, cap, n->psk);
return true;
}
// ---- presentation ------------------------------------------------------
inline const char* security_name(uint8_t security) {
switch (security) {
case montauk::abi::WIFI_SEC_OPEN: return "Open";
case montauk::abi::WIFI_SEC_WEP: return "WEP";
case montauk::abi::WIFI_SEC_WPA: return "WPA";
case montauk::abi::WIFI_SEC_WPA2: return "WPA2";
case montauk::abi::WIFI_SEC_WPA3: return "WPA3";
default: return "Unknown";
}
}
inline bool needs_key(uint8_t security) {
return security != montauk::abi::WIFI_SEC_OPEN;
}
// 0-4 bars from an RSSI in dBm.
inline int signal_bars(int8_t rssi) {
if (rssi >= -55) return 4;
if (rssi >= -67) return 3;
if (rssi >= -75) return 2;
if (rssi >= -85) return 1;
return 0;
}
inline const char* state_name(uint8_t state) {
switch (state) {
case montauk::abi::WIFI_STATE_ABSENT: return "No adapter";
case montauk::abi::WIFI_STATE_DETECTED: return "Loading firmware";
case montauk::abi::WIFI_STATE_BOOTING: return "Starting";
case montauk::abi::WIFI_STATE_RUNNING: return "Ready";
case montauk::abi::WIFI_STATE_ERROR: return "Adapter error";
case montauk::abi::WIFI_STATE_RFKILL: return "Radio off";
default: return "Unknown";
}
}
// What the join is doing right now, for a progress line.
inline const char* conn_state_name(uint32_t connState) {
switch (connState) {
case montauk::abi::WIFI_CONN_IDLE: return "Not connected";
case montauk::abi::WIFI_CONN_CONTEXTS_UP: return "Preparing radio...";
case montauk::abi::WIFI_CONN_AUTHENTICATING: return "Authenticating...";
case montauk::abi::WIFI_CONN_AUTHENTICATED: return "Authenticated";
case montauk::abi::WIFI_CONN_ASSOCIATING: return "Associating...";
case montauk::abi::WIFI_CONN_ASSOCIATED: return "Associated";
case montauk::abi::WIFI_CONN_HANDSHAKING: return "Exchanging keys...";
case montauk::abi::WIFI_CONN_CONNECTED: return "Connected";
case montauk::abi::WIFI_CONN_FAILED: return "Connection failed";
default: return "";
}
}
inline const char* error_message(int err) {
switch (err) {
case 0: return "";
case montauk::abi::WIFI_ERR_NO_ADAPTER: return "No Wi-Fi adapter is ready";
case montauk::abi::WIFI_ERR_NOT_FOUND: return "That network is out of range";
case montauk::abi::WIFI_ERR_NEED_KEY: return "This network needs a password";
case montauk::abi::WIFI_ERR_UNSUPPORTED: return "This security type is not supported";
case montauk::abi::WIFI_ERR_AUTH: return "Wrong password";
case montauk::abi::WIFI_ERR_TIMEOUT: return "The network did not respond";
default: return "Could not join the network";
}
}
} // namespace wifi
} // namespace montauk
+34 -5
View File
@@ -7,7 +7,10 @@
wifi info
wifi debug
wifi connect <ssid> [passphrase]
wifi status
wifi disconnect
wifi saved
wifi forget <ssid>
.SH DESCRIPTION
Drives the Intel Wi-Fi adapter. With no arguments, runs a five
@@ -33,13 +36,24 @@
security tallies. Use this when a scan finds nothing.
connect <ssid> [passphrase]
Brings the firmware contexts (PHY, MAC, binding, station) up
for a network from the last scan. The 802.11 authentication
and association exchange is not implemented, so this does not
produce a usable link; encrypted networks are refused outright.
Joins a network from the last scan: open, or WPA2/WPA3-PSK
with CCMP or GCMP. Blocks until the link is up or the attempt
fails, and says which it was. A passphrase given here is
remembered in 0:/config/wifi.toml; given only an SSID, the
remembered one is used. Run dhcp(1) afterwards for an address.
status
The network currently joined, with signal and cipher.
disconnect
Tears those contexts back down.
Leaves the network and tears the firmware contexts down.
saved
Lists the networks in 0:/config/wifi.toml and whether the
desktop rejoins them by itself at startup.
forget <ssid>
Removes a network from 0:/config/wifi.toml.
.SH OUTPUT
SSID SIGNAL RSSI CH BAND SECURITY
@@ -59,6 +73,21 @@
yet)". The PNVM file carries regulatory data; without it the
firmware falls back to conservative built-in limits.
.SH SAVED NETWORKS
0:/config/wifi.toml holds the networks the system may rejoin
without being asked, as [network.<n>] tables of ssid and psk, and
a wifi.autoconnect flag. The desktop writes it when "Remember this
network" is ticked, and reads it after the automatic scan it runs
at startup. Passphrases are stored as typed; anyone who can read
0:/config can read them.
.SH GRAPHICAL USE
The desktop panel carries a Wi-Fi icon whenever a supported
adapter is present, separate from the Ethernet one. Its menu lists
the networks in range, asks for a passphrase when one is needed,
and shows the address once the link is up. The Network app has the
same list under its Wi-Fi tab, along with Forget.
.SH DIAGNOSTICS
no supported Wi-Fi adapter found
No matching device, or its RF type is not GF.
+1 -1
View File
@@ -56,7 +56,7 @@ LDFLAGS := \
# ---- C++ source files ----
CORE_SRCS := main.cpp window.cpp panel.cpp compose.cpp input.cpp dialogs.cpp launcher.cpp desktop_builtin.cpp desktop_catalog.cpp font_data.cpp stb_truetype_impl.cpp
CORE_SRCS := main.cpp window.cpp panel.cpp wifi.cpp compose.cpp input.cpp dialogs.cpp launcher.cpp desktop_builtin.cpp desktop_catalog.cpp font_data.cpp stb_truetype_impl.cpp
APP_SRCS := $(sort $(shell find apps -name '*.cpp' -print))
SRCS := $(CORE_SRCS) $(APP_SRCS)
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
+5
View File
@@ -263,6 +263,11 @@ void gui::desktop_compose(DesktopState* ds) {
desktop_draw_net_popup(ds);
}
// Draw Wi-Fi popup if open
if (ds->wifi_popup_open) {
desktop_draw_wifi_popup(ds);
}
// Draw volume popup if open
if (ds->vol_popup_open) {
desktop_draw_vol_popup(ds);
+1
View File
@@ -66,6 +66,7 @@ void desktop_lock_screen(DesktopState* ds) {
desktop_close_launcher(ds);
ds->ctx_menu_open = false;
ds->net_popup_open = false;
ds->wifi_popup_open = false;
ds->vol_popup_open = false;
// Cache display name for lock screen rendering
+10
View File
@@ -158,12 +158,22 @@ gui::CursorStyle cursor_for_edge(gui::ResizeEdge edge);
void desktop_draw_app_menu(gui::DesktopState* ds);
void desktop_draw_net_popup(gui::DesktopState* ds);
void desktop_draw_vol_popup(gui::DesktopState* ds);
const montauk::abi::NetIfInfo* desktop_eth_iface(const gui::DesktopState* ds);
bool desktop_eth_online(const gui::DesktopState* ds);
// wifi.cpp
void desktop_wifi_init(gui::DesktopState* ds);
bool desktop_wifi_poll(gui::DesktopState* ds, uint64_t now);
void desktop_draw_wifi_popup(gui::DesktopState* ds);
bool desktop_wifi_handle_mouse(gui::DesktopState* ds, int mx, int my,
bool left_pressed, int scroll);
// compose.cpp
void desktop_draw_lock_screen(gui::DesktopState* ds);
void desktop_mark_background_dirty(gui::DesktopState* ds);
// main.cpp
void desktop_refresh_netifs(gui::DesktopState* ds);
void desktop_scan_apps(gui::DesktopState* ds);
void desktop_build_menu(gui::DesktopState* ds);
void desktop_open_launcher(gui::DesktopState* ds);
+23
View File
@@ -166,6 +166,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->ctx_menu_open = false;
ds->vol_popup_open = false;
ds->net_popup_open = false;
ds->wifi_popup_open = false;
return;
}
@@ -507,6 +508,13 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
}
}
// Wi-Fi popup. The passphrase dialog is an ordinary window and needs
// nothing here.
if (ds->wifi_popup_open) {
if (desktop_wifi_handle_mouse(ds, mx, my, left_pressed, ev.scroll))
return;
}
// Handle net popup clicks
if (ds->net_popup_open && left_pressed) {
int popup_w = 220;
@@ -522,6 +530,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
return;
} else if (!ds->net_icon_rect.contains(mx, my)) {
ds->net_popup_open = false;
ds->wifi_popup_open = false;
}
}
@@ -531,6 +540,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
if (mx < 36) {
ds->app_menu_open = !ds->app_menu_open;
ds->net_popup_open = false;
ds->wifi_popup_open = false;
ds->vol_popup_open = false;
ds->ctx_menu_open = false;
return;
@@ -542,6 +552,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->vol_dragging = false;
ds->app_menu_open = false;
ds->net_popup_open = false;
ds->wifi_popup_open = false;
ds->ctx_menu_open = false;
return;
}
@@ -551,6 +562,17 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->net_popup_open = !ds->net_popup_open;
ds->app_menu_open = false;
ds->vol_popup_open = false;
ds->wifi_popup_open = false;
ds->ctx_menu_open = false;
return;
}
// Wi-Fi icon
if (ds->wifi_icon_rect.w > 0 && ds->wifi_icon_rect.contains(mx, my)) {
ds->wifi_popup_open = !ds->wifi_popup_open;
ds->app_menu_open = false;
ds->vol_popup_open = false;
ds->net_popup_open = false;
ds->ctx_menu_open = false;
return;
}
@@ -775,6 +797,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->ctx_menu_y = my;
ds->app_menu_open = false;
ds->net_popup_open = false;
ds->wifi_popup_open = false;
ds->vol_popup_open = false;
}
}
+1
View File
@@ -406,6 +406,7 @@ void desktop_open_launcher(DesktopState* ds) {
ds->app_menu_open = false;
ds->ctx_menu_open = false;
ds->net_popup_open = false;
ds->wifi_popup_open = false;
ds->vol_popup_open = false;
ds->vol_dragging = false;
desktop_update_launcher_results(ds);
+39
View File
@@ -276,6 +276,7 @@ void gui::desktop_init(DesktopState* ds) {
ds->icon_folder = svg_load("0:/icons/folder.svg", 16, 16, defColor);
ds->icon_file = svg_load("0:/icons/text-x-generic.svg", 16, 16, defColor);
ds->icon_network = svg_load("0:/icons/network-wired-symbolic.svg", 16, 16, colors::PANEL_TEXT);
ds->icon_wifi = svg_load("0:/icons/network-wireless-symbolic.svg", 16, 16, colors::PANEL_TEXT);
ds->icon_go_up = svg_load("0:/icons/go-up-symbolic.svg", 16, 16, defColor);
ds->icon_go_back = svg_load("0:/icons/go-previous-symbolic.svg", 16, 16, defColor);
ds->icon_go_forward = svg_load("0:/icons/go-next-symbolic.svg", 16, 16, defColor);
@@ -392,6 +393,9 @@ void gui::desktop_init(DesktopState* ds) {
ds->net_cfg_last_poll = montauk::get_milliseconds();
ds->net_icon_rect = {0, 0, 0, 0};
desktop_refresh_netifs(ds);
desktop_wifi_init(ds);
ds->vol_popup_open = false;
ds->vol_icon_rect = {0, 0, 0, 0};
int vol = montauk::audio_get_master_volume();
@@ -465,6 +469,30 @@ static bool desktop_netcfg_equal(const montauk::abi::NetCfg& a, const montauk::a
return true;
}
// Re-read the interface registry. Cheap, and it is the only way to tell the
// wired and wireless halves of the panel apart.
void desktop_refresh_netifs(DesktopState* ds) {
int n = montauk::net_interfaces(ds->netifs, DesktopState::MAX_NETIFS);
ds->netif_count = n > 0 ? n : 0;
ds->eth_present = false;
for (int i = 0; i < ds->netif_count; i++) {
if (ds->netifs[i].kind == montauk::abi::NETIF_KIND_ETHERNET)
ds->eth_present = true;
}
}
static bool desktop_netifs_equal(const montauk::abi::NetIfInfo* a, int an,
const montauk::abi::NetIfInfo* b, int bn) {
if (an != bn) return false;
for (int i = 0; i < an; i++) {
if (a[i].linkUp != b[i].linkUp || a[i].active != b[i].active ||
a[i].kind != b[i].kind) {
return false;
}
}
return true;
}
static bool desktop_refresh_panel_state(DesktopState* ds, uint64_t now) {
if (ds->screen_locked) return false;
@@ -476,9 +504,20 @@ static bool desktop_refresh_panel_state(DesktopState* ds, uint64_t now) {
ds->cached_net_cfg = next;
changed = true;
}
montauk::abi::NetIfInfo ifaces[DesktopState::MAX_NETIFS];
int count = montauk::net_interfaces(ifaces, DesktopState::MAX_NETIFS);
if (count < 0) count = 0;
if (!desktop_netifs_equal(ifaces, count, ds->netifs, ds->netif_count)) {
desktop_refresh_netifs(ds);
changed = true;
}
ds->net_cfg_last_poll = now;
}
if (desktop_wifi_poll(ds, now)) changed = true;
// Event-driven sync: the kernel mixer bumps a serial on every state
// change. Reading it is one cheap syscall; refresh only when the serial
// moved since we last looked. The audio app (and any other client) will
+89 -33
View File
@@ -141,25 +141,48 @@ void gui::desktop_draw_panel(DesktopState* ds) {
}
}
// Network icon (to the left of the volume icon)
int net_icon_x = vol_icon_x - 16 - 10;
int net_icon_y = (PANEL_HEIGHT - 16) / 2;
ds->net_icon_rect = {net_icon_x, net_icon_y, 16, 16};
// Status icons fill in leftwards from the volume icon. Ethernet only
// appears when a wired interface is registered, Wi-Fi only when a
// supported adapter is present, so a machine without either shows neither.
int icon_y = (PANEL_HEIGHT - 16) / 2;
int next_icon_x = vol_icon_x - 16 - 10;
if (ds->icon_network.pixels) {
if (ds->cached_net_cfg.ipAddress == 0) {
uint32_t* src = ds->icon_network.pixels;
int npx = 16 * 16;
uint32_t tinted[256];
for (int p = 0; p < npx; p++) {
uint32_t px = src[p];
uint8_t a = (px >> 24) & 0xFF;
tinted[p] = ((uint32_t)a << 24) | 0x004444CC;
if (ds->eth_present) {
ds->net_icon_rect = {next_icon_x, icon_y, 16, 16};
next_icon_x -= 16 + 10;
if (ds->icon_network.pixels) {
// Tinted while the cable is doing nothing for us: no link, or a
// link that is not the one carrying traffic.
if (!desktop_eth_online(ds)) {
uint32_t* src = ds->icon_network.pixels;
uint32_t tinted[256];
for (int p = 0; p < 16 * 16; p++) {
uint8_t a = (src[p] >> 24) & 0xFF;
tinted[p] = ((uint32_t)a << 24) | 0x004444CC;
}
fb.blit_alpha(ds->net_icon_rect.x, icon_y, 16, 16, tinted);
} else {
fb.blit_alpha(ds->net_icon_rect.x, icon_y, ds->icon_network.width,
ds->icon_network.height, ds->icon_network.pixels);
}
fb.blit_alpha(net_icon_x, net_icon_y, 16, 16, tinted);
} else {
fb.blit_alpha(net_icon_x, net_icon_y, ds->icon_network.width, ds->icon_network.height, ds->icon_network.pixels);
}
} else {
ds->net_icon_rect = {0, 0, 0, 0};
}
// Wi-Fi stays white whatever the radio is doing: the popup carries the
// state, and a colour-shifting icon next to the clock is just noise.
if (ds->wifi_present) {
ds->wifi_icon_rect = {next_icon_x, icon_y, 16, 16};
next_icon_x -= 16 + 10;
if (ds->icon_wifi.pixels) {
fb.blit_alpha(ds->wifi_icon_rect.x, icon_y, ds->icon_wifi.width,
ds->icon_wifi.height, ds->icon_wifi.pixels);
}
} else {
ds->wifi_icon_rect = {0, 0, 0, 0};
}
}
@@ -265,16 +288,38 @@ void desktop_draw_app_menu(DesktopState* ds) {
// Network Popup
// ============================================================================
// The wired interface, if one is registered.
const montauk::abi::NetIfInfo* desktop_eth_iface(const DesktopState* ds) {
for (int i = 0; i < ds->netif_count; i++) {
if (ds->netifs[i].kind == montauk::abi::NETIF_KIND_ETHERNET)
return &ds->netifs[i];
}
return nullptr;
}
// "Online" for the panel icon means the cable is both up and the interface the
// stack is actually sending through, with an address to send from.
bool desktop_eth_online(const DesktopState* ds) {
const montauk::abi::NetIfInfo* eth = desktop_eth_iface(ds);
return eth && eth->linkUp && eth->active && ds->cached_net_cfg.ipAddress != 0;
}
void desktop_draw_net_popup(DesktopState* ds) {
Framebuffer& fb = ds->fb;
montauk::abi::NetCfg& nc = ds->cached_net_cfg;
bool connected = nc.ipAddress != 0;
const montauk::abi::NetIfInfo* eth = desktop_eth_iface(ds);
bool link_up = eth && eth->linkUp;
// The IP configuration is global to the stack, so it is only this
// interface's address while this interface is the active one.
bool owns_address = eth && eth->active && nc.ipAddress != 0;
bool connected = link_up && owns_address;
int popup_w = 220;
int fh = system_font_height();
int row_h = fh + 8;
int header_h = row_h + 8; // title + status row + padding
int body_rows = 5; // IP, Subnet, Gateway, DNS, MAC
int body_rows = 6; // Interface, IP, Subnet, Gateway, DNS, MAC
int popup_h = header_h + row_h * body_rows + 12;
int popup_x = ds->net_icon_rect.x + ds->net_icon_rect.w - popup_w;
int popup_y = PANEL_HEIGHT + 2;
@@ -290,11 +335,14 @@ void desktop_draw_net_popup(DesktopState* ds) {
// Header: "Ethernet" + status dot
draw_text(fb, lx, ty, "Ethernet", colors::TEXT_COLOR);
// Status dot + label (right-aligned in header)
// Status dot + label (right-aligned in header). A cable that is plugged
// in but idle (Wi-Fi is carrying the traffic) is not the same as unplugged.
Color dot_color = connected
? Color::from_rgb(0x4C, 0xAF, 0x50) // green
: Color::from_rgb(0xCC, 0x33, 0x33); // red
const char* status_str = connected ? "Connected" : "Disconnected";
: (link_up ? Color::from_rgb(0xD0, 0x9A, 0x2E) // amber
: Color::from_rgb(0xCC, 0x33, 0x33)); // red
const char* status_str = connected ? "Connected"
: (link_up ? "Link up" : "Disconnected");
int sw = text_width(status_str);
int dot_r = 4;
int status_x = popup_x + popup_w - 14 - sw;
@@ -318,23 +366,31 @@ void desktop_draw_net_popup(DesktopState* ds) {
int val_x = popup_x + 76; // fixed column for values
struct NetRow { const char* label; char value[24]; };
NetRow rows[5];
NetRow rows[6];
rows[0].label = "IP";
if (connected) format_ip(rows[0].value, nc.ipAddress);
else montauk::strcpy(rows[0].value, "0.0.0.0");
rows[0].label = "Interface";
montauk::strncpy(rows[0].value, eth ? eth->name : "None", sizeof(rows[0].value));
rows[1].label = "Subnet";
format_ip(rows[1].value, nc.subnetMask);
// Addresses are shown only when this interface owns them; otherwise the
// wireless popup is where they belong.
rows[1].label = "IP";
if (owns_address) format_ip(rows[1].value, nc.ipAddress);
else montauk::strcpy(rows[1].value, link_up ? "Not in use" : "0.0.0.0");
rows[2].label = "Gateway";
format_ip(rows[2].value, nc.gateway);
rows[2].label = "Subnet";
if (owns_address) format_ip(rows[2].value, nc.subnetMask);
else montauk::strcpy(rows[2].value, "-");
rows[3].label = "DNS";
format_ip(rows[3].value, nc.dnsServer);
rows[3].label = "Gateway";
if (owns_address) format_ip(rows[3].value, nc.gateway);
else montauk::strcpy(rows[3].value, "-");
rows[4].label = "MAC";
format_mac(rows[4].value, nc.macAddress);
rows[4].label = "DNS";
if (owns_address) format_ip(rows[4].value, nc.dnsServer);
else montauk::strcpy(rows[4].value, "-");
rows[5].label = "MAC";
format_mac(rows[5].value, eth ? eth->mac : nc.macAddress);
for (int i = 0; i < body_rows; i++) {
draw_text(fb, lx, ty, rows[i].label, dim);
+867
View File
@@ -0,0 +1,867 @@
/*
* wifi.cpp
* Wi-Fi panel entry: the network list popup, the passphrase dialog window,
* and the automatic scan and saved-network join that run at startup.
*
* Everything here goes through the non-blocking Wi-Fi syscalls
* (wifi_scan_start / wifi_results / wifi_connect_async). The blocking pair
* takes seconds, and the compositor cannot stand still for that long.
*
* Copyright (c) 2026 Daniel Hammer
*/
#include "desktop_internal.hpp"
#include <montauk/wifi.h>
#include <gui/mtk.hpp>
using namespace gui;
// ============================================================================
// Geometry
// ============================================================================
static constexpr int WIFI_POPUP_W = 264;
static constexpr int WIFI_ROW_H = 30;
static constexpr int WIFI_VISIBLE = 6; // network rows on screen at once
static constexpr int WIFI_BTN_H = 26;
static int wifi_popup_x(const DesktopState* ds) {
int x = ds->wifi_icon_rect.x + ds->wifi_icon_rect.w - WIFI_POPUP_W;
return x < 4 ? 4 : x;
}
static int wifi_visible_rows(const DesktopState* ds) {
int rows = ds->wifi_network_count;
if (rows > WIFI_VISIBLE) rows = WIFI_VISIBLE;
if (rows < 1) rows = 1; // the "no networks" line
return rows;
}
static int wifi_popup_h(const DesktopState* ds) {
int fh = system_font_height();
int header = fh + 10; // title + status
int detail = ds->wifi_info.connected ? (fh + 6) * 3 + 8 : 0;
int list = wifi_visible_rows(ds) * WIFI_ROW_H;
int footer = WIFI_BTN_H + 14 + fh + 6;
return header + 10 + detail + list + 10 + footer;
}
static Rect wifi_popup_rect(const DesktopState* ds) {
return {wifi_popup_x(ds), PANEL_HEIGHT + 2, WIFI_POPUP_W, wifi_popup_h(ds)};
}
// y of the first network row
static int wifi_list_y(const DesktopState* ds) {
int fh = system_font_height();
int y = PANEL_HEIGHT + 2 + 10 + fh + 10;
if (ds->wifi_info.connected) y += (fh + 6) * 3 + 8;
return y;
}
static Rect wifi_scan_button(const DesktopState* ds) {
Rect popup = wifi_popup_rect(ds);
int fh = system_font_height();
int y = popup.y + popup.h - 10 - fh - 6 - WIFI_BTN_H;
return {popup.x + 12, y, 76, WIFI_BTN_H};
}
static Rect wifi_action_button(const DesktopState* ds) {
Rect scan = wifi_scan_button(ds);
Rect popup = wifi_popup_rect(ds);
int w = 96;
return {popup.x + popup.w - 12 - w, scan.y, w, WIFI_BTN_H};
}
// ============================================================================
// State helpers
// ============================================================================
// The saved-network list lives here rather than on the stack: it is several
// kilobytes, and the compositor's stack is 32 KiB shared with the TrueType
// rasteriser. One copy also saves re-parsing the file on every click.
static montauk::wifi::SavedList g_saved;
static void wifi_reload_saved() {
montauk::wifi::saved_load(&g_saved);
}
static const char* wifi_saved_psk(const char* ssid) {
const montauk::wifi::SavedNetwork* entry =
montauk::wifi::saved_find(&g_saved, ssid);
return (entry && entry->psk[0]) ? entry->psk : nullptr;
}
static void wifi_set_status(DesktopState* ds, const char* msg) {
montauk::strncpy(ds->wifi_status, msg ? msg : "", sizeof(ds->wifi_status));
ds->wifi_status_time = montauk::get_milliseconds();
}
// Sorted strongest-first so the list reads the way a user expects.
static void wifi_sort_results(DesktopState* ds) {
for (int i = 1; i < ds->wifi_network_count; i++) {
montauk::abi::WifiNetwork key = ds->wifi_networks[i];
int j = i - 1;
while (j >= 0 && ds->wifi_networks[j].rssi < key.rssi) {
ds->wifi_networks[j + 1] = ds->wifi_networks[j];
j--;
}
ds->wifi_networks[j + 1] = key;
}
}
static void wifi_refresh_results(DesktopState* ds) {
montauk::abi::WifiNetwork raw[DesktopState::MAX_WIFI_NETWORKS];
int n = montauk::wifi_results(raw, DesktopState::MAX_WIFI_NETWORKS);
if (n < 0) n = 0;
// One row per network rather than one per access point: a house with a
// repeater answers on several BSSIDs under the same name, and the strongest
// is the one worth joining. Hidden networks have no name to join by, so
// they are left out of the list entirely.
ds->wifi_network_count = 0;
for (int i = 0; i < n; i++) {
if (!raw[i].ssid[0]) continue;
int existing = -1;
for (int k = 0; k < ds->wifi_network_count; k++) {
if (montauk::streq(ds->wifi_networks[k].ssid, raw[i].ssid)) {
existing = k;
break;
}
}
if (existing >= 0) {
if (raw[i].rssi > ds->wifi_networks[existing].rssi)
ds->wifi_networks[existing] = raw[i];
continue;
}
ds->wifi_networks[ds->wifi_network_count++] = raw[i];
}
wifi_sort_results(ds);
if (ds->wifi_scroll > ds->wifi_network_count - 1) ds->wifi_scroll = 0;
}
static void wifi_start_scan(DesktopState* ds) {
int rc = montauk::wifi_scan_start(6000);
if (rc < 0) {
wifi_set_status(ds, "The adapter is not ready yet");
return;
}
ds->wifi_scanning = true;
// No status line here: the button reads "Scanning" and the list says so
// too, and three copies of the same word is not progress reporting.
ds->wifi_status[0] = '\0';
}
static void wifi_begin_join(DesktopState* ds, const char* ssid, const char* psk) {
int rc = montauk::wifi_connect_async(ssid, psk);
if (rc < 0) {
wifi_set_status(ds, montauk::wifi::error_message(rc));
ds->wifi_joining = false;
return;
}
ds->wifi_joining = true;
ds->wifi_dhcp_pending = true;
montauk::strncpy(ds->wifi_joining_ssid, ssid, sizeof(ds->wifi_joining_ssid));
char msg[96];
snprintf(msg, sizeof(msg), "Connecting to %s...", ssid);
wifi_set_status(ds, msg);
}
// Defined below: the passphrase dialog is a real desktop window, created the
// same way the reboot and shutdown dialogs are.
static void wifi_open_password_dialog(DesktopState* ds,
const montauk::abi::WifiNetwork& net);
// Join the network under the cursor: straight away when it is open or its
// passphrase is already saved, otherwise ask for the key.
static void wifi_select_network(DesktopState* ds, int index) {
if (index < 0 || index >= ds->wifi_network_count) return;
const montauk::abi::WifiNetwork& net = ds->wifi_networks[index];
if (!montauk::wifi::needs_key(net.security)) {
wifi_begin_join(ds, net.ssid, "");
return;
}
const char* saved = wifi_saved_psk(net.ssid);
if (saved) {
wifi_begin_join(ds, net.ssid, saved);
return;
}
wifi_open_password_dialog(ds, net);
}
// ============================================================================
// Startup and polling
// ============================================================================
void desktop_wifi_init(DesktopState* ds) {
ds->wifi_popup_open = false;
ds->wifi_icon_rect = {0, 0, 0, 0};
ds->wifi_network_count = 0;
ds->wifi_scroll = 0;
ds->wifi_scanning = false;
ds->wifi_boot_scan_started = false;
ds->wifi_autoconnect_done = false;
ds->wifi_joining = false;
ds->wifi_dhcp_pending = false;
ds->wifi_dhcp_waiting = false;
ds->wifi_joining_ssid[0] = '\0';
ds->wifi_status[0] = '\0';
ds->wifi_status_time = 0;
ds->wifi_scan_generation = 0;
ds->wifi_last_poll = 0;
wifi_reload_saved();
montauk::memset(&ds->wifi_info, 0, sizeof(ds->wifi_info));
ds->wifi_present = montauk::wifi_info(&ds->wifi_info) == 0 && ds->wifi_info.present;
}
// Try the strongest saved network that is actually in range.
static void wifi_try_autoconnect(DesktopState* ds) {
if (!g_saved.autoconnect || g_saved.count == 0) {
ds->wifi_autoconnect_done = true;
return;
}
for (int i = 0; i < ds->wifi_network_count; i++) { // strongest first
const montauk::abi::WifiNetwork& net = ds->wifi_networks[i];
const montauk::wifi::SavedNetwork* entry =
montauk::wifi::saved_find(&g_saved, net.ssid);
if (!entry) continue;
wifi_begin_join(ds, entry->ssid, entry->psk);
break;
}
ds->wifi_autoconnect_done = true;
}
// Called from the panel refresh. Returns true when something on screen moved.
bool desktop_wifi_poll(DesktopState* ds, uint64_t now) {
// The Network app writes the same file, so pick up its edits whenever the
// popup is opened rather than trusting the copy read at startup.
static bool popup_was_open = false;
if (ds->wifi_popup_open && !popup_was_open) wifi_reload_saved();
popup_was_open = ds->wifi_popup_open;
// The adapter finishes its firmware load well after login, so keep looking
// for it until it shows up rather than deciding once at startup.
if (!ds->wifi_present) {
if (now - ds->wifi_last_poll < 3000) return false;
ds->wifi_last_poll = now;
montauk::abi::WifiInfo info;
if (montauk::wifi_info(&info) != 0 || !info.present) return false;
ds->wifi_info = info;
ds->wifi_present = true;
return true; // the icon appears now
}
// Poll faster while something is in flight so progress text keeps up.
bool busy = ds->wifi_scanning || ds->wifi_joining || ds->wifi_popup_open;
if (now - ds->wifi_last_poll < (busy ? 400u : 3000u)) return false;
ds->wifi_last_poll = now;
montauk::abi::WifiInfo info;
if (montauk::wifi_info(&info) != 0) return false;
bool changed = info.connected != ds->wifi_info.connected
|| info.connState != ds->wifi_info.connState
|| info.scanning != ds->wifi_info.scanning
|| info.scanGeneration != ds->wifi_info.scanGeneration
|| info.state != ds->wifi_info.state;
ds->wifi_info = info;
// The first scan runs by itself once the firmware is up, so the list is
// already populated the first time the user opens the popup.
if (!ds->wifi_boot_scan_started && info.state == montauk::abi::WIFI_STATE_RUNNING) {
ds->wifi_boot_scan_started = true;
wifi_start_scan(ds);
return true;
}
if (info.scanGeneration != ds->wifi_scan_generation) {
ds->wifi_scan_generation = info.scanGeneration;
ds->wifi_scanning = false;
wifi_refresh_results(ds);
changed = true;
char msg[64];
if (ds->wifi_network_count > 0) {
snprintf(msg, sizeof(msg), "Found %d network%s",
ds->wifi_network_count, ds->wifi_network_count == 1 ? "" : "s");
} else {
snprintf(msg, sizeof(msg), "No networks found");
}
wifi_set_status(ds, msg);
// Rejoin whatever was saved as soon as the first sweep lands.
if (!ds->wifi_autoconnect_done && !info.connected && !ds->wifi_joining) {
wifi_reload_saved();
wifi_try_autoconnect(ds);
}
} else if (ds->wifi_scanning && !info.scanning) {
ds->wifi_scanning = false;
changed = true;
}
if (ds->wifi_joining) {
if (info.connected) {
ds->wifi_joining = false;
char msg[96];
snprintf(msg, sizeof(msg), "Connected to %s", info.ssid);
wifi_set_status(ds, msg);
changed = true;
} else if (info.lastError != 0 && !info.joining) {
ds->wifi_joining = false;
ds->wifi_dhcp_pending = false;
ds->wifi_dhcp_waiting = false;
wifi_set_status(ds, montauk::wifi::error_message(info.lastError));
changed = true;
}
}
// A wireless link with no address of its own needs a lease; the wired
// interface, if there is one, keeps whatever it already had.
if (ds->wifi_dhcp_pending && info.connected && ds->cached_net_cfg.ipAddress == 0) {
ds->wifi_dhcp_pending = false;
ds->wifi_dhcp_waiting = true;
montauk::spawn("0:/os/dhcp.elf");
wifi_set_status(ds, "Requesting an address...");
changed = true;
} else if (ds->wifi_dhcp_pending && info.connected) {
ds->wifi_dhcp_pending = false;
}
// "Requesting an address..." is only true until the lease lands. Leaving it
// up afterwards contradicts the address row further up the popup, so it is
// replaced the moment there is an address (or the link goes away).
if (ds->wifi_dhcp_waiting && (ds->cached_net_cfg.ipAddress != 0 || !info.connected)) {
bool leased = ds->cached_net_cfg.ipAddress != 0;
ds->wifi_dhcp_waiting = false;
if (leased) {
char msg[96];
snprintf(msg, sizeof(msg), "Connected to %s", info.ssid);
wifi_set_status(ds, msg);
} else {
ds->wifi_status[0] = '\0';
}
changed = true;
}
return changed;
}
// ============================================================================
// Drawing
// ============================================================================
static void draw_signal_bars(Framebuffer& fb, int x, int y, int8_t rssi, Color on, Color off) {
int bars = montauk::wifi::signal_bars(rssi);
for (int i = 0; i < 4; i++) {
int h = 3 + i * 3;
fb.fill_rect(x + i * 4, y + 12 - h, 3, h, i < bars ? on : off);
}
}
// A small padlock, so encrypted networks read at a glance.
static void draw_lock(Framebuffer& fb, int x, int y, Color c) {
fb.fill_rect(x + 1, y + 5, 8, 6, c);
fb.fill_rect(x + 2, y + 2, 1, 3, c);
fb.fill_rect(x + 7, y + 2, 1, 3, c);
fb.fill_rect(x + 3, y + 1, 4, 1, c);
}
static void draw_kv_row(Framebuffer& fb, int lx, int vx, int y,
const char* label, const char* value, Color dim, Color text) {
draw_text(fb, lx, y, label, dim);
draw_text(fb, vx, y, value, text);
}
void desktop_draw_wifi_popup(DesktopState* ds) {
Framebuffer& fb = ds->fb;
Rect popup = wifi_popup_rect(ds);
int fh = system_font_height();
Color dim = Color::from_rgb(0x66, 0x66, 0x66);
Color faint = Color::from_rgb(0xCC, 0xCC, 0xCC);
draw_shadow(fb, popup.x, popup.y, popup.w, popup.h, 4, colors::SHADOW);
fill_rounded_rect(fb, popup.x, popup.y, popup.w, popup.h, 8, colors::MENU_BG);
draw_rect(fb, popup.x, popup.y, popup.w, popup.h, colors::BORDER);
int lx = popup.x + 14;
int ty = popup.y + 10;
// Header: "Wi-Fi" + connection state
draw_text(fb, lx, ty, "Wi-Fi", colors::TEXT_COLOR);
const char* status_str;
if (ds->wifi_info.connected) status_str = "Connected";
else if (ds->wifi_joining) status_str = "Connecting";
else if (ds->wifi_info.state == montauk::abi::WIFI_STATE_RFKILL) status_str = "Radio off";
else status_str = "Not connected";
Color dot_color = ds->wifi_info.connected
? Color::from_rgb(0x4C, 0xAF, 0x50)
: (ds->wifi_joining ? Color::from_rgb(0xD0, 0x9A, 0x2E)
: Color::from_rgb(0xCC, 0x33, 0x33));
int sw = text_width(status_str);
int status_x = popup.x + popup.w - 14 - sw;
fill_circle(fb, status_x - 9, ty + fh / 2, 4, dot_color);
draw_text(fb, status_x, ty, status_str, dim);
ty += fh + 10;
// What this interface has, when it has it. The address belongs to the
// wireless interface only while that is the one carrying traffic.
if (ds->wifi_info.connected) {
int vx = popup.x + 76;
char value[40];
montauk::strncpy(value, ds->wifi_info.ssid[0] ? ds->wifi_info.ssid : "-", sizeof(value));
draw_kv_row(fb, lx, vx, ty, "Network", value, dim, colors::TEXT_COLOR);
ty += fh + 6;
bool wireless_active = false;
for (int i = 0; i < ds->netif_count; i++) {
if (ds->netifs[i].kind == montauk::abi::NETIF_KIND_WIRELESS && ds->netifs[i].active)
wireless_active = true;
}
if (wireless_active && ds->cached_net_cfg.ipAddress != 0)
format_ip(value, ds->cached_net_cfg.ipAddress);
else
montauk::strcpy(value, "No address");
draw_kv_row(fb, lx, vx, ty, "IP", value, dim, colors::TEXT_COLOR);
ty += fh + 6;
snprintf(value, sizeof(value), "Channel %d", (int)ds->wifi_info.channel);
draw_kv_row(fb, lx, vx, ty, "Radio", value, dim, colors::TEXT_COLOR);
ty += fh + 6 + 8;
}
// Network list
int list_y = wifi_list_y(ds);
int mx = ds->mouse.x, my = ds->mouse.y;
if (ds->wifi_network_count == 0) {
const char* empty = ds->wifi_scanning ? "Looking for networks..."
: "No networks found";
draw_text(fb, lx, list_y + (WIFI_ROW_H - fh) / 2, empty, dim);
}
int rows = wifi_visible_rows(ds);
for (int r = 0; r < rows && (ds->wifi_scroll + r) < ds->wifi_network_count; r++) {
int idx = ds->wifi_scroll + r;
const montauk::abi::WifiNetwork& net = ds->wifi_networks[idx];
Rect row = {popup.x + 6, list_y + r * WIFI_ROW_H, popup.w - 12, WIFI_ROW_H};
bool current = ds->wifi_info.connected
&& montauk::streq(net.ssid, ds->wifi_info.ssid);
if (row.contains(mx, my))
fill_rounded_rect(fb, row.x, row.y, row.w, row.h, 4,
gui::mtk::accent_hover_tint(ds->settings.accent_color));
else if (current)
fill_rounded_rect(fb, row.x, row.y, row.w, row.h, 4,
Color::from_rgb(0xEC, 0xF2, 0xFB));
draw_signal_bars(fb, row.x + 8, row.y + (WIFI_ROW_H - 12) / 2, net.rssi,
current ? ds->settings.accent_color : Color::from_rgb(0x55, 0x55, 0x55),
faint);
int text_x = row.x + 32;
int text_y = row.y + (WIFI_ROW_H - fh) / 2;
int text_max = row.w - 32 - 26;
char label[40];
montauk::strncpy(label, net.ssid[0] ? net.ssid : "(hidden)", sizeof(label));
while (label[0] && text_width(label) > text_max) {
int n = montauk::slen(label);
label[n - 1] = '\0';
}
draw_text(fb, text_x, text_y, label, colors::TEXT_COLOR);
if (montauk::wifi::needs_key(net.security))
draw_lock(fb, row.x + row.w - 20, row.y + (WIFI_ROW_H - 12) / 2, dim);
}
// A slim scrollbar rather than a line of text: the list scrolls with the
// wheel, and there is no room under it for a hint.
if (ds->wifi_network_count > WIFI_VISIBLE) {
int track_x = popup.x + popup.w - 8;
int track_y = list_y + 2;
int track_h = rows * WIFI_ROW_H - 4;
fb.fill_rect(track_x, track_y, 3, track_h, faint);
int thumb_h = track_h * WIFI_VISIBLE / ds->wifi_network_count;
if (thumb_h < 12) thumb_h = 12;
int span = ds->wifi_network_count - WIFI_VISIBLE;
int thumb_y = track_y + (span > 0 ? (track_h - thumb_h) * ds->wifi_scroll / span : 0);
fb.fill_rect(track_x, thumb_y, 3, thumb_h, Color::from_rgb(0x99, 0x99, 0x99));
}
// Footer: scan / disconnect and the last status line
Rect scan = wifi_scan_button(ds);
Rect action = wifi_action_button(ds);
auto draw_btn = [&](const Rect& r, const char* label, bool primary) {
Color bg = primary ? ds->settings.accent_color : Color::from_rgb(0xE0, 0xE0, 0xE0);
if (r.contains(mx, my))
bg = primary ? gui::mtk::darken(ds->settings.accent_color, 32)
: Color::from_rgb(0xD0, 0xD0, 0xD0);
fill_rounded_rect(fb, r.x, r.y, r.w, r.h, 6, bg);
int tw = text_width(label);
draw_text(fb, r.x + (r.w - tw) / 2, r.y + (r.h - fh) / 2, label,
primary ? colors::WHITE : colors::TEXT_COLOR);
};
draw_btn(scan, ds->wifi_scanning ? "Scanning" : "Scan", false);
draw_btn(action, ds->wifi_info.connected ? "Disconnect" : "Rescan",
ds->wifi_info.connected);
// A result worth reading now, not one left over from ten minutes ago.
bool status_fresh = ds->wifi_status[0]
&& montauk::get_milliseconds() - ds->wifi_status_time < 20000;
const char* line = status_fresh ? ds->wifi_status : "";
if (ds->wifi_joining && ds->wifi_info.connState != montauk::abi::WIFI_CONN_IDLE)
line = montauk::wifi::conn_state_name(ds->wifi_info.connState);
if (line && line[0]) {
char fitted[64];
montauk::strncpy(fitted, line, sizeof(fitted));
while (fitted[0] && text_width(fitted) > popup.w - 28) {
int n = montauk::slen(fitted);
fitted[n - 1] = '\0';
}
draw_text(fb, lx, scan.y + scan.h + 8, fitted, dim);
}
}
// ============================================================================
// Input
// ============================================================================
// Returns true when the click was consumed.
bool desktop_wifi_handle_mouse(DesktopState* ds, int mx, int my,
bool left_pressed, int scroll) {
if (!ds->wifi_popup_open) return false;
Rect popup = wifi_popup_rect(ds);
if (scroll != 0 && popup.contains(mx, my)) {
int max_scroll = ds->wifi_network_count - WIFI_VISIBLE;
if (max_scroll < 0) max_scroll = 0;
ds->wifi_scroll -= scroll;
if (ds->wifi_scroll < 0) ds->wifi_scroll = 0;
if (ds->wifi_scroll > max_scroll) ds->wifi_scroll = max_scroll;
return true;
}
if (!left_pressed) return false;
if (!popup.contains(mx, my)) {
if (!ds->wifi_icon_rect.contains(mx, my)) ds->wifi_popup_open = false;
return false;
}
if (wifi_scan_button(ds).contains(mx, my)) {
if (!ds->wifi_scanning) wifi_start_scan(ds);
return true;
}
if (wifi_action_button(ds).contains(mx, my)) {
if (ds->wifi_info.connected) {
montauk::wifi_disconnect();
ds->wifi_joining = false;
ds->wifi_dhcp_pending = false;
ds->wifi_dhcp_waiting = false;
wifi_set_status(ds, "Disconnected");
} else if (!ds->wifi_scanning) {
wifi_start_scan(ds);
}
return true;
}
int list_y = wifi_list_y(ds);
if (my >= list_y && my < list_y + wifi_visible_rows(ds) * WIFI_ROW_H) {
int row = (my - list_y) / WIFI_ROW_H;
wifi_select_network(ds, ds->wifi_scroll + row);
return true;
}
return true; // a click inside the popup never falls through
}
// ============================================================================
// Passphrase dialog
//
// A real desktop window, created and driven exactly like the reboot and
// shutdown dialogs in dialogs.cpp: desktop_create_window() plus the four
// callbacks. It gets a title bar, dragging, a close button and a place in the
// window list for free, and the compositor draws it in the window layer rather
// than in the panel overlay.
// ============================================================================
static constexpr int PWD_W = 400;
static constexpr int PWD_PAD = 20;
static constexpr int PWD_FIELD_H = 32;
static constexpr int PWD_BTN_W = 96;
static constexpr int PWD_BTN_H = 32;
// The vertical rhythm, in one place, so the window height computed at open
// time and the rects drawn into it cannot drift apart.
static constexpr int PWD_TOP = 18; // content top to the heading
static constexpr int PWD_HEAD_GAP = 6; // heading to the network line
static constexpr int PWD_FIELD_GAP = 22; // network line to the field label
static constexpr int PWD_CHECK_GAP = 20; // field to the first checkbox
static constexpr int PWD_ROW_GAP = 6; // between the checkboxes
static constexpr int PWD_BTN_GAP = 30; // last checkbox to the buttons
struct WifiPasswordDialog {
DesktopState* ds;
char ssid[DesktopState::WIFI_SSID_CAP];
char password[DesktopState::WIFI_PSK_CAP];
uint8_t security;
bool reveal;
bool remember;
mtk::TextInputState input;
};
static mtk::Theme wifi_dialog_theme(const DesktopState* ds) {
return mtk::make_theme(ds->settings.accent_color);
}
// Content-relative layout. Everything above the buttons is measured down from
// the top and everything below them up from the bottom, so the two meet in the
// middle wherever the window is sized.
static int pwd_row_h() {
return system_font_height() + 10; // a checkbox row
}
static int pwd_label_y() {
int fh = system_font_height();
return PWD_TOP + fh + PWD_HEAD_GAP + fh + PWD_FIELD_GAP;
}
static Rect pwd_field_rect(int cw, const mtk::Theme& theme) {
return mtk::labeled_text_input_rect(PWD_PAD, pwd_label_y(), cw - PWD_PAD * 2,
theme, PWD_FIELD_H);
}
static Rect pwd_show_rect(int cw, const mtk::Theme& theme) {
Rect field = pwd_field_rect(cw, theme);
return {PWD_PAD, field.y + field.h + PWD_CHECK_GAP, 190, pwd_row_h()};
}
static Rect pwd_remember_rect(int cw, const mtk::Theme& theme) {
Rect show = pwd_show_rect(cw, theme);
return {show.x, show.y + show.h + PWD_ROW_GAP, 240, show.h};
}
// The height the content needs for all of that plus the button row.
static int pwd_content_h(const mtk::Theme& theme) {
Rect remember = pwd_remember_rect(PWD_W, theme);
return remember.y + remember.h + PWD_BTN_GAP + PWD_BTN_H + PWD_PAD;
}
static Rect pwd_join_rect(const Canvas& c) {
return {c.w - PWD_PAD - PWD_BTN_W, c.h - PWD_PAD - PWD_BTN_H,
PWD_BTN_W, PWD_BTN_H};
}
static Rect pwd_cancel_rect(const Canvas& c) {
Rect join = pwd_join_rect(c);
return {join.x - 12 - PWD_BTN_W, join.y, PWD_BTN_W, PWD_BTN_H};
}
static void wifi_dialog_close(WifiPasswordDialog* pd) {
if (!pd) return;
for (int i = 0; i < pd->ds->window_count; i++) {
if (pd->ds->windows[i].app_data == pd) {
desktop_close_window(pd->ds, i);
return;
}
}
}
static void wifi_dialog_submit(WifiPasswordDialog* pd) {
if (!pd->password[0]) return;
// Join either way; a passphrase that cannot be written down still works
// for this session.
bool save_failed = false;
if (pd->remember) {
montauk::wifi::saved_set(&g_saved, pd->ssid, pd->password);
save_failed = montauk::wifi::saved_store(&g_saved) != 0;
}
wifi_begin_join(pd->ds, pd->ssid, pd->password);
if (save_failed)
wifi_set_status(pd->ds, "Connected, but the password could not be saved");
wifi_dialog_close(pd);
}
static void wifi_dialog_on_draw(Window* win, Framebuffer& fb) {
(void)fb;
auto* pd = (WifiPasswordDialog*)win->app_data;
if (!pd) return;
Canvas c(win);
mtk::Theme theme = wifi_dialog_theme(pd->ds);
c.fill(theme.window_bg);
int fh = system_font_height();
int mx = pd->ds->mouse.x - win->content_rect().x;
int my = pd->ds->mouse.y - win->content_rect().y;
char line[96];
snprintf(line, sizeof(line), "Enter the password for %s", pd->ssid);
while (line[0] && text_width(line) > c.w - PWD_PAD * 2) {
int n = montauk::slen(line);
line[n - 1] = '\0';
}
c.text(PWD_PAD, PWD_TOP, line, theme.text);
char sub[64];
snprintf(sub, sizeof(sub), "%s network",
montauk::wifi::security_name(pd->security));
c.text(PWD_PAD, PWD_TOP + fh + PWD_HEAD_GAP, sub, theme.text_subtle);
Rect field = pwd_field_rect(c.w, theme);
mtk::draw_labeled_text_field(c, PWD_PAD, field.y - fh - theme.gap_xs,
c.w - PWD_PAD * 2, "Password", pd->password,
pd->input.cursor, true, !pd->reveal, theme,
PWD_FIELD_H, pd->input.selection_anchor);
Rect show = pwd_show_rect(c.w, theme);
mtk::draw_checkbox(c, show, "Show password",
mtk::check_state(pd->reveal), theme, true,
show.contains(mx, my));
Rect remember = pwd_remember_rect(c.w, theme);
mtk::draw_checkbox(c, remember, "Remember this network",
mtk::check_state(pd->remember), theme, true,
remember.contains(mx, my));
Rect cancel = pwd_cancel_rect(c);
Rect join = pwd_join_rect(c);
mtk::draw_button(c, cancel, "Cancel", mtk::BUTTON_SECONDARY,
mtk::widget_state(false, cancel.contains(mx, my), true), theme);
mtk::draw_button(c, join, "Join", mtk::BUTTON_PRIMARY,
mtk::widget_state(false, join.contains(mx, my),
pd->password[0] != '\0'), theme);
}
static void wifi_dialog_on_mouse(Window* win, MouseEvent& ev) {
auto* pd = (WifiPasswordDialog*)win->app_data;
if (!pd) return;
Canvas c(win);
mtk::Theme theme = wifi_dialog_theme(pd->ds);
Rect cr = win->content_rect();
int mx = ev.x - cr.x;
int my = ev.y - cr.y;
// The field owns the pointer while a drag or its context menu is running.
Rect field = pwd_field_rect(c.w, theme);
if (pd->input.context.open || pd->input.dragging || field.contains(mx, my)) {
mtk::text_input_handle_mouse(pd->input, field, pd->password,
(int)sizeof(pd->password), mx, my,
ev.buttons, ev.prev_buttons, c.w, c.h,
true, !pd->reveal, nullptr);
return;
}
if (!ev.left_pressed()) return;
if (pwd_show_rect(c.w, theme).contains(mx, my)) {
pd->reveal = !pd->reveal;
return;
}
if (pwd_remember_rect(c.w, theme).contains(mx, my)) {
pd->remember = !pd->remember;
return;
}
if (pwd_cancel_rect(c).contains(mx, my)) {
wifi_dialog_close(pd);
return;
}
if (pwd_join_rect(c).contains(mx, my)) {
wifi_dialog_submit(pd);
return;
}
}
static void wifi_dialog_on_key(Window* win, const montauk::abi::KeyEvent& key) {
auto* pd = (WifiPasswordDialog*)win->app_data;
if (!pd || !key.pressed) return;
if (key.scancode == 0x01) { // Escape
wifi_dialog_close(pd);
return;
}
if (key.ascii == '\n' || key.ascii == '\r') {
wifi_dialog_submit(pd);
return;
}
mtk::text_input_key(pd->input, pd->password, (int)sizeof(pd->password),
key, nullptr);
}
static void wifi_dialog_on_close(Window* win) {
if (!win->app_data) return;
// Do not leave the passphrase behind in freed memory.
auto* pd = (WifiPasswordDialog*)win->app_data;
montauk::memset(pd, 0, sizeof(*pd));
montauk::mfree(win->app_data);
win->app_data = nullptr;
}
static void wifi_open_password_dialog(DesktopState* ds,
const montauk::abi::WifiNetwork& net) {
// One dialog at a time: asking twice for the same key helps nobody.
for (int i = 0; i < ds->window_count; i++) {
if (ds->windows[i].on_close == wifi_dialog_on_close
&& ds->windows[i].state != WIN_CLOSED) {
desktop_raise_window(ds, i);
return;
}
}
char title[MAX_TITLE_LEN];
snprintf(title, sizeof(title), "Join %s", net.ssid);
// Height comes from the layout above rather than a constant: at the shipped
// UI size a fixed 250px window put the checkboxes under the buttons.
int pwd_h = pwd_content_h(wifi_dialog_theme(ds))
+ TITLEBAR_HEIGHT + BORDER_WIDTH;
int wx = (ds->screen_w - PWD_W) / 2;
int wy = (ds->screen_h - pwd_h) / 2;
if (wy < PANEL_HEIGHT + 8) wy = PANEL_HEIGHT + 8;
int idx = desktop_create_window(ds, title, wx, wy, PWD_W, pwd_h);
if (idx < 0) return;
auto* pd = (WifiPasswordDialog*)montauk::malloc(sizeof(WifiPasswordDialog));
if (!pd) {
desktop_close_window(ds, idx);
return;
}
montauk::memset(pd, 0, sizeof(*pd));
pd->ds = ds;
montauk::strncpy(pd->ssid, net.ssid, sizeof(pd->ssid));
pd->security = net.security;
pd->remember = true;
mtk::text_input_reset(pd->input, 0);
Window* win = &ds->windows[idx];
win->app_data = pd;
win->on_draw = wifi_dialog_on_draw;
win->on_mouse = wifi_dialog_on_mouse;
win->on_key = wifi_dialog_on_key;
win->on_close = wifi_dialog_on_close;
// The popup has done its job; the dialog is where the interaction is now.
ds->wifi_popup_open = false;
}
+625 -6
View File
@@ -6,6 +6,7 @@
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <montauk/wifi.h>
#include <gui/mtk.hpp>
#include <gui/mtk/settings.hpp>
#include <gui/standalone.hpp>
@@ -17,8 +18,8 @@ extern "C" {
using namespace gui;
static constexpr int WIN_W = 560;
static constexpr int WIN_H = 420;
static constexpr int WIN_W = 620;
static constexpr int WIN_H = 480;
static constexpr int TAB_H = 36;
static constexpr int FOOTER_H = 44;
static constexpr int PAD = 16;
@@ -31,8 +32,9 @@ static constexpr int FIELD_H = 32;
enum Tab {
TAB_STATUS = 0,
TAB_CONFIG = 1,
TAB_COUNT = 2,
TAB_WIFI = 1,
TAB_CONFIG = 2,
TAB_COUNT = 3,
};
struct Field {
@@ -43,6 +45,7 @@ struct Field {
static const char* const kTabLabels[TAB_COUNT] = {
"Status",
"Wi-Fi",
"Configure",
};
@@ -59,6 +62,26 @@ static Field g_fields[FIELD_COUNT] = {
{"Gateway", {}, {}},
{"DNS Server", {}, {}},
};
// ---- Wi-Fi tab state -------------------------------------------------------
static constexpr int WIFI_MAX_NETWORKS = 32;
static constexpr int WIFI_ROW_H = 34;
static montauk::abi::WifiInfo g_wifi = {};
static montauk::abi::WifiNetwork g_wifi_nets[WIFI_MAX_NETWORKS];
static int g_wifi_count = 0;
static int g_wifi_selected = -1;
static int g_wifi_scroll = 0; // first visible row
static uint32_t g_wifi_generation = 0;
static bool g_wifi_scanning = false;
static bool g_wifi_joining = false;
static char g_wifi_psk[montauk::wifi::PSK_CAP] = {};
static mtk::TextInputState g_wifi_psk_input = {};
static bool g_wifi_psk_focus = false;
static bool g_wifi_remember = true;
static montauk::wifi::SavedList g_wifi_saved = {};
static bool g_dirty = false;
static char g_status[128] = {};
static uint64_t g_status_time = 0;
@@ -246,12 +269,15 @@ static void copy_cfg_to_fields() {
g_dirty = false;
}
static void wifi_refresh();
static void refresh_state(bool update_fields) {
montauk::get_netcfg(&g_cfg);
if (montauk::net_status(&g_net) < 0) {
montauk::memset(&g_net, 0, sizeof(g_net));
snprintf(g_net.driver, sizeof(g_net.driver), "Unavailable");
}
wifi_refresh();
if (update_fields) copy_cfg_to_fields();
g_last_refresh = montauk::get_milliseconds();
}
@@ -387,6 +413,434 @@ static void draw_status_tab(Canvas& c, const mtk::Theme& theme) {
draw_kv(c, &y, "TX Packets", tx, theme);
}
// ============================================================================
// Wi-Fi tab
//
// Laid out the way the Display app lays out its mode list: borderless rows on
// the window background, a radio for the selection, and state as right-aligned
// text rather than a pill. The block under the list keeps a fixed height so the
// row count does not change as the selection moves between open and encrypted
// networks.
// ============================================================================
static int wifi_status_y() {
return TAB_H + 20;
}
static int wifi_separator_y() {
return wifi_status_y() + system_font_height() * 2 + 26;
}
static int wifi_list_header_y() {
return wifi_separator_y() + 16;
}
static int wifi_list_y() {
return wifi_list_header_y() + system_font_height() + 10;
}
// The passphrase field, the checkbox under it, and the padding around them.
static int wifi_action_h() {
return mtk::labeled_text_height(app_theme(), FIELD_H) + 10
+ system_font_height() + 6;
}
static Rect wifi_action_rect() {
int h = wifi_action_h();
return {PAD, footer_rect().y - 14 - h, g_win.width - PAD * 2, h};
}
static Rect wifi_psk_input_rect() {
Rect action = wifi_action_rect();
return mtk::labeled_text_input_rect(action.x, action.y, action.w,
app_theme(), FIELD_H);
}
static Rect wifi_remember_rect() {
Rect action = wifi_action_rect();
int block = mtk::labeled_text_height(app_theme(), FIELD_H);
return {action.x, action.y + block + 10, 240, system_font_height() + 6};
}
static int wifi_visible_rows() {
int available = wifi_action_rect().y - 14 - wifi_list_y();
int rows = available / WIFI_ROW_H;
if (rows < 1) rows = 1;
if (rows > WIFI_MAX_NETWORKS) rows = WIFI_MAX_NETWORKS;
return rows;
}
static Rect wifi_row_rect(int visibleIndex) {
return {PAD, wifi_list_y() + visibleIndex * WIFI_ROW_H,
g_win.width - PAD * 2, WIFI_ROW_H};
}
static void wifi_meta_text(char* out, size_t cap,
const montauk::abi::WifiNetwork& net) {
snprintf(out, cap, "%s - ch %d - %d dBm",
montauk::wifi::security_name(net.security),
(int)net.channel, (int)net.rssi);
}
static void wifi_clamp_scroll() {
int max_scroll = g_wifi_count - wifi_visible_rows();
if (max_scroll < 0) max_scroll = 0;
if (g_wifi_scroll > max_scroll) g_wifi_scroll = max_scroll;
if (g_wifi_scroll < 0) g_wifi_scroll = 0;
}
static Rect wifi_forget_button() {
Rect refresh = status_refresh_button();
return {refresh.x - (GAP + BUTTON_W) * 2, refresh.y, BUTTON_W, BUTTON_H};
}
static Rect wifi_scan_button() {
Rect refresh = status_refresh_button();
return {refresh.x - GAP - BUTTON_W, refresh.y, BUTTON_W, BUTTON_H};
}
static Rect wifi_connect_button() {
return status_refresh_button();
}
static bool wifi_selection_valid() {
return g_wifi_selected >= 0 && g_wifi_selected < g_wifi_count;
}
static bool wifi_selection_is_current() {
return wifi_selection_valid() && g_wifi.connected
&& montauk::streq(g_wifi_nets[g_wifi_selected].ssid, g_wifi.ssid);
}
// True when joining the selection would need a passphrase typed in: encrypted,
// and nothing remembered for it.
static bool wifi_selection_needs_key() {
if (!wifi_selection_valid()) return false;
const montauk::abi::WifiNetwork& net = g_wifi_nets[g_wifi_selected];
return montauk::wifi::needs_key(net.security);
}
static void wifi_sort() {
for (int i = 1; i < g_wifi_count; i++) {
montauk::abi::WifiNetwork key = g_wifi_nets[i];
int j = i - 1;
while (j >= 0 && g_wifi_nets[j].rssi < key.rssi) {
g_wifi_nets[j + 1] = g_wifi_nets[j];
j--;
}
g_wifi_nets[j + 1] = key;
}
}
static void wifi_load_results() {
char previous[montauk::wifi::SSID_CAP] = {};
if (wifi_selection_valid())
montauk::strncpy(previous, g_wifi_nets[g_wifi_selected].ssid, sizeof(previous));
montauk::abi::WifiNetwork raw[WIFI_MAX_NETWORKS];
int n = montauk::wifi_results(raw, WIFI_MAX_NETWORKS);
if (n < 0) n = 0;
// One row per network, not one per access point: a house with a repeater
// answers on several BSSIDs and the strongest is the one worth joining.
// Hidden networks have no SSID to join by, so they are left out entirely.
g_wifi_count = 0;
for (int i = 0; i < n; i++) {
if (!raw[i].ssid[0]) continue;
int existing = -1;
for (int k = 0; k < g_wifi_count; k++) {
if (montauk::streq(g_wifi_nets[k].ssid, raw[i].ssid)) { existing = k; break; }
}
if (existing >= 0) {
if (raw[i].rssi > g_wifi_nets[existing].rssi)
g_wifi_nets[existing] = raw[i];
continue;
}
g_wifi_nets[g_wifi_count++] = raw[i];
}
wifi_sort();
// Keep the selection on the network it was on, wherever it moved to.
g_wifi_selected = -1;
if (previous[0]) {
for (int i = 0; i < g_wifi_count; i++) {
if (montauk::streq(g_wifi_nets[i].ssid, previous)) { g_wifi_selected = i; break; }
}
}
wifi_clamp_scroll();
}
// Keeps the adapter snapshot, the scan table and the saved list in step.
static void wifi_refresh() {
if (montauk::wifi_info(&g_wifi) != 0) {
montauk::memset(&g_wifi, 0, sizeof(g_wifi));
return;
}
if (g_wifi.scanGeneration != g_wifi_generation) {
g_wifi_generation = g_wifi.scanGeneration;
g_wifi_scanning = false;
wifi_load_results();
char msg[64];
snprintf(msg, sizeof(msg), "Found %d network%s", g_wifi_count,
g_wifi_count == 1 ? "" : "s");
set_status(msg);
} else if (g_wifi_scanning && !g_wifi.scanning) {
g_wifi_scanning = false;
}
if (g_wifi_joining) {
if (g_wifi.connected) {
g_wifi_joining = false;
char msg[96];
snprintf(msg, sizeof(msg), "Connected to %s", g_wifi.ssid);
set_status(msg);
} else if (g_wifi.lastError != 0 && !g_wifi.joining) {
g_wifi_joining = false;
set_status(montauk::wifi::error_message(g_wifi.lastError));
}
}
}
static void wifi_start_scan() {
int rc = montauk::wifi_scan_start(6000);
if (rc < 0) {
set_status("The Wi-Fi adapter is not ready");
return;
}
g_wifi_scanning = true;
set_status("Scanning for networks...");
}
static void wifi_join_selected() {
if (!wifi_selection_valid()) {
set_status("Select a network first");
return;
}
const montauk::abi::WifiNetwork& net = g_wifi_nets[g_wifi_selected];
const char* psk = "";
if (montauk::wifi::needs_key(net.security)) {
if (g_wifi_psk[0]) {
psk = g_wifi_psk;
} else {
const montauk::wifi::SavedNetwork* saved =
montauk::wifi::saved_find(&g_wifi_saved, net.ssid);
if (saved && saved->psk[0]) {
psk = saved->psk;
} else {
g_wifi_psk_focus = true;
set_status("This network needs a password");
return;
}
}
}
bool save_failed = false;
if (montauk::wifi::needs_key(net.security) && g_wifi_remember && psk[0]) {
montauk::wifi::saved_set(&g_wifi_saved, net.ssid, psk);
save_failed = montauk::wifi::saved_store(&g_wifi_saved) != 0;
}
int rc = montauk::wifi_connect_async(net.ssid, psk);
if (rc < 0) {
set_status(montauk::wifi::error_message(rc));
return;
}
g_wifi_joining = true;
char msg[96];
if (save_failed) {
snprintf(msg, sizeof(msg),
"Connecting to %s, but the password could not be saved", net.ssid);
} else {
snprintf(msg, sizeof(msg), "Connecting to %s...", net.ssid);
}
set_status(msg);
}
static void wifi_forget_selected() {
if (!wifi_selection_valid()) return;
const char* ssid = g_wifi_nets[g_wifi_selected].ssid;
if (!montauk::wifi::saved_remove(&g_wifi_saved, ssid)) {
set_status("That network was not saved");
return;
}
bool save_failed = montauk::wifi::saved_store(&g_wifi_saved) != 0;
g_wifi_psk[0] = '\0';
mtk::text_input_reset(g_wifi_psk_input, 0);
char msg[96];
if (save_failed) {
snprintf(msg, sizeof(msg), "Could not update the saved network list");
} else {
snprintf(msg, sizeof(msg), "Forgot %s", ssid);
}
set_status(msg);
}
static void draw_signal(Canvas& c, int x, int y, int8_t rssi, Color on, Color off) {
int bars = montauk::wifi::signal_bars(rssi);
for (int i = 0; i < 4; i++) {
int h = 3 + i * 3;
c.fill_rect(x + i * 4, y + 12 - h, 3, h, i < bars ? on : off);
}
}
static void draw_wifi_tab(Canvas& c, const mtk::Theme& theme) {
int fh = system_font_height();
int y = wifi_status_y();
bool present = g_wifi.present != 0;
bool ready = present && g_wifi.state == montauk::abi::WIFI_STATE_RUNNING;
Color dot = !present ? theme.danger
: (g_wifi.connected ? Color::from_rgb(0x27, 0xA0, 0x58)
: Color::from_rgb(0xD0, 0x9A, 0x2E));
fill_circle(c, PAD + 6, y + fh / 2, 6, dot);
char headline[128];
if (!present) {
snprintf(headline, sizeof(headline), "No Wi-Fi adapter");
} else if (g_wifi.connected) {
snprintf(headline, sizeof(headline), "Connected to %s", g_wifi.ssid);
} else if (g_wifi_joining) {
snprintf(headline, sizeof(headline), "%s",
montauk::wifi::conn_state_name(g_wifi.connState));
} else {
snprintf(headline, sizeof(headline), "%s",
montauk::wifi::state_name(g_wifi.state));
}
draw_text_fit(c, PAD + 22, y, headline, g_win.width - PAD * 2 - 22,
present ? theme.text : theme.danger);
char sub[160];
if (!present) {
snprintf(sub, sizeof(sub),
"Nothing to configure until a supported adapter is fitted.");
} else if (g_wifi.connected) {
snprintf(sub, sizeof(sub), "Channel %d - firmware %s",
(int)g_wifi.channel,
g_wifi.fwVersion[0] ? g_wifi.fwVersion : "unknown");
} else {
snprintf(sub, sizeof(sub), "Firmware %s",
g_wifi.fwVersion[0] ? g_wifi.fwVersion : "not loaded");
}
draw_text_fit(c, PAD + 22, y + fh + 3, sub, g_win.width - PAD * 2 - 22,
theme.text_subtle);
mtk::draw_separator(c, PAD, wifi_separator_y(), g_win.width - PAD * 2, theme);
// List header, with the page indicator where the Display app puts its hint.
int header_y = wifi_list_header_y();
c.text(PAD, header_y, "NETWORKS IN RANGE", theme.text_muted);
int rows = wifi_visible_rows();
char hint[48];
hint[0] = '\0';
if (g_wifi_scanning) {
snprintf(hint, sizeof(hint), "Scanning...");
} else if (g_wifi_count > rows) {
int last = g_wifi_scroll + rows;
if (last > g_wifi_count) last = g_wifi_count;
snprintf(hint, sizeof(hint), "%d-%d of %d (scroll)",
g_wifi_scroll + 1, last, g_wifi_count);
}
if (hint[0])
c.text(g_win.width - PAD - text_width(hint), header_y, hint, theme.text_muted);
if (g_wifi_count == 0) {
const char* empty = !ready ? "The adapter is not ready yet."
: (g_wifi_scanning
? "Looking for networks..."
: "No networks found. Press Scan to look again.");
c.text(PAD + 4, wifi_list_y() + 9, empty, theme.text_subtle);
}
// Fixed right-hand column for the row state, so the signal/channel detail
// beside it can never be drawn over the word "Connected".
static constexpr int STATE_W = 84;
// The detail column starts at one x for every row, sized from the widest
// line in the whole list: right-aligning each row instead would make short
// strings ("ch 1") look indented, and sizing it from the visible rows only
// would make the column jump about while scrolling.
int meta_w = 0;
for (int index = 0; index < g_wifi_count; index++) {
char meta[48];
wifi_meta_text(meta, sizeof(meta), g_wifi_nets[index]);
int w = text_width(meta);
if (w > meta_w) meta_w = w;
}
Rect list_row = wifi_row_rect(0);
int meta_x = list_row.x + list_row.w - STATE_W - 12 - meta_w;
for (int row = 0; row < rows; row++) {
int index = g_wifi_scroll + row;
if (index >= g_wifi_count) break;
const montauk::abi::WifiNetwork& net = g_wifi_nets[index];
Rect option = wifi_row_rect(row);
bool selected = index == g_wifi_selected;
bool current = g_wifi.connected && montauk::streq(net.ssid, g_wifi.ssid);
if (selected)
c.fill_rounded_rect(option.x, option.y + 2, option.w, option.h - 4,
theme.radius_md, theme.accent_soft);
else if (option.contains(g_mouse_x, g_mouse_y))
c.fill_rounded_rect(option.x, option.y + 2, option.w, option.h - 4,
theme.radius_md, theme.surface_hover);
Rect radio = {option.x + 4, option.y, 18, option.h};
mtk::draw_radio(c, radio, "", selected, theme);
int text_y = option.y + (option.h - fh) / 2;
draw_signal(c, option.x + 34, option.y + (option.h - 12) / 2, net.rssi,
current ? theme.accent : theme.text_muted, theme.border);
const char* state = current ? "Connected"
: (montauk::wifi::saved_find(&g_wifi_saved, net.ssid)
? "Saved" : nullptr);
if (state)
c.text(option.x + option.w - STATE_W, text_y, state,
current ? theme.accent : theme.text_muted);
char meta[48];
wifi_meta_text(meta, sizeof(meta), net);
c.text(meta_x, text_y, meta, theme.text_subtle);
int ssid_x = option.x + 60;
draw_text_fit(c, ssid_x, text_y, net.ssid, meta_x - 12 - ssid_x, theme.text);
}
// The block below the list keeps its height whatever is in it: a passphrase
// field when one is wanted, and a line explaining why not when it is not.
Rect action = wifi_action_rect();
if (wifi_selection_needs_key()) {
const montauk::wifi::SavedNetwork* saved =
montauk::wifi::saved_find(&g_wifi_saved,
g_wifi_nets[g_wifi_selected].ssid);
mtk::draw_labeled_text_field(c, action.x, action.y, action.w,
saved ? "Password (saved)" : "Password",
g_wifi_psk, g_wifi_psk_input.cursor,
g_wifi_psk_focus, true, theme, FIELD_H,
g_wifi_psk_input.selection_anchor);
Rect remember = wifi_remember_rect();
mtk::draw_checkbox(c, remember, "Remember this network",
mtk::check_state(g_wifi_remember), theme, true,
remember.contains(g_mouse_x, g_mouse_y));
} else {
const char* line;
if (!wifi_selection_valid())
line = "";
else
line = "Open network - no password needed.";
if (line[0]) c.text(action.x, action.y, line, theme.text_subtle);
}
}
static void draw_config_tab(Canvas& c, const mtk::Theme& theme) {
int y = config_header_y();
c.text(PAD, y, "IPv4 Configuration", theme.text);
@@ -418,10 +872,33 @@ static void draw_footer(Canvas& c, const mtk::Theme& theme) {
const char* msg = status_visible()
? g_status
: (g_dirty ? "Unsaved static IPv4 changes" : "Network settings ready");
int text_right = (g_tab == TAB_STATUS ? status_clear_button().x : config_dhcp_button().x) - GAP;
int text_right = PAD;
if (g_tab == TAB_STATUS) text_right = status_clear_button().x - GAP;
else if (g_tab == TAB_WIFI) text_right = wifi_forget_button().x - GAP;
else text_right = config_dhcp_button().x - GAP;
draw_text_fit(c, PAD, foot.y + (FOOTER_H - system_font_height()) / 2,
msg, text_right - PAD, g_dirty ? theme.text : theme.text_subtle);
if (g_tab == TAB_WIFI) {
Rect forget = wifi_forget_button();
Rect scan = wifi_scan_button();
Rect connect = wifi_connect_button();
bool saved = wifi_selection_valid()
&& montauk::wifi::saved_find(&g_wifi_saved,
g_wifi_nets[g_wifi_selected].ssid) != nullptr;
bool connected = wifi_selection_is_current() || (g_wifi.connected && !wifi_selection_valid());
mtk::draw_button(c, forget, "Forget", mtk::BUTTON_SECONDARY,
button_state(forget, saved), theme);
mtk::draw_button(c, scan, g_wifi_scanning ? "Scanning" : "Scan",
mtk::BUTTON_SECONDARY,
button_state(scan, !g_wifi_scanning && g_wifi.present), theme);
mtk::draw_button(c, connect, connected ? "Disconnect" : "Connect",
mtk::BUTTON_PRIMARY,
button_state(connect, g_wifi.present != 0), theme);
return;
}
if (g_tab == TAB_STATUS) {
Rect clear = status_clear_button();
Rect dhcp = status_dhcp_button();
@@ -455,12 +932,20 @@ static void render() {
if (g_tab == TAB_STATUS) {
draw_status_tab(c, theme);
} else if (g_tab == TAB_WIFI) {
draw_wifi_tab(c, theme);
} else {
draw_config_tab(c, theme);
}
draw_footer(c, theme);
if (g_tab == TAB_CONFIG)
draw_config_context_menus(c, theme);
if (g_tab == TAB_WIFI) {
mtk::draw_text_input_context_menu(
c, g_wifi_psk_input, theme,
mtk::text_input_has_selection(g_wifi_psk_input, g_wifi_psk,
(int)sizeof(g_wifi_psk)));
}
host.present();
}
@@ -523,6 +1008,19 @@ static void set_tab(Tab tab) {
return;
}
g_tab = tab;
if (tab == TAB_WIFI) {
g_focus_field = -1;
g_wifi_psk_focus = false;
montauk::wifi::saved_load(&g_wifi_saved);
wifi_refresh();
wifi_load_results();
// Nothing on screen yet and an adapter that can look: go and find out.
if (g_wifi_count == 0 && !g_wifi_scanning
&& g_wifi.state == montauk::abi::WIFI_STATE_RUNNING) {
wifi_start_scan();
}
return;
}
if (tab == TAB_CONFIG) {
focus_field(0);
} else {
@@ -546,6 +1044,80 @@ static bool handle_mouse(int mx, int my, uint8_t buttons, uint8_t prev_buttons)
}
}
if (g_tab == TAB_WIFI) {
// The passphrase field owns the click while a drag or its context menu
// is in progress, the same rule the IPv4 fields follow.
if (g_wifi_psk_input.context.open || g_wifi_psk_input.dragging) {
int result = mtk::text_input_handle_mouse(
g_wifi_psk_input, wifi_psk_input_rect(), g_wifi_psk,
(int)sizeof(g_wifi_psk), mx, my, buttons, prev_buttons,
g_win.width, g_win.height, g_wifi_psk_focus, true, nullptr);
return result != mtk::TEXT_INPUT_NONE;
}
if (!left_pressed && !right_pressed) return false;
if (wifi_selection_needs_key() && wifi_psk_input_rect().contains(mx, my)) {
g_wifi_psk_focus = true;
mtk::text_input_handle_mouse(g_wifi_psk_input, wifi_psk_input_rect(),
g_wifi_psk, (int)sizeof(g_wifi_psk),
mx, my, buttons, prev_buttons,
g_win.width, g_win.height, true, true, nullptr);
return true;
}
if (!left_pressed) return false;
int rows = wifi_visible_rows();
for (int row = 0; row < rows; row++) {
int idx = g_wifi_scroll + row;
if (idx >= g_wifi_count) break;
if (!wifi_row_rect(row).contains(mx, my)) continue;
if (idx != g_wifi_selected) {
g_wifi_selected = idx;
// A different network means a different key; do not carry the
// previous one over, but do offer the remembered one.
g_wifi_psk[0] = '\0';
mtk::text_input_reset(g_wifi_psk_input, 0);
const montauk::wifi::SavedNetwork* saved =
montauk::wifi::saved_find(&g_wifi_saved, g_wifi_nets[idx].ssid);
if (saved) {
montauk::strncpy(g_wifi_psk, saved->psk, sizeof(g_wifi_psk));
mtk::text_input_reset(g_wifi_psk_input, str_len(g_wifi_psk));
}
}
g_wifi_psk_focus = false;
return true;
}
if (wifi_selection_needs_key() && wifi_remember_rect().contains(mx, my)) {
g_wifi_remember = !g_wifi_remember;
return true;
}
if (wifi_scan_button().contains(mx, my)) {
if (!g_wifi_scanning) wifi_start_scan();
return true;
}
if (wifi_forget_button().contains(mx, my)) {
wifi_forget_selected();
return true;
}
if (wifi_connect_button().contains(mx, my)) {
if (wifi_selection_is_current() || (g_wifi.connected && !wifi_selection_valid())) {
montauk::wifi_disconnect();
g_wifi_joining = false;
set_status("Disconnected");
} else {
wifi_join_selected();
}
return true;
}
g_wifi_psk_focus = false;
return true;
}
if (g_tab == TAB_STATUS) {
if (!left_pressed) return false;
if (status_refresh_button().contains(mx, my)) {
@@ -631,6 +1203,42 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
return true;
}
if (g_tab == TAB_WIFI) {
if (key.ascii == '\t') {
set_tab(TAB_CONFIG);
return true;
}
if (key.ascii == '\n' || key.ascii == '\r') {
wifi_join_selected();
return true;
}
if (key.scancode == 0x48 || key.scancode == 0x50) { // up / down
int step = key.scancode == 0x48 ? -1 : 1;
int next = g_wifi_selected < 0 ? 0 : g_wifi_selected + step;
if (next < 0) next = 0;
if (next >= g_wifi_count) next = g_wifi_count - 1;
g_wifi_selected = next;
if (g_wifi_selected >= 0) {
if (g_wifi_selected < g_wifi_scroll)
g_wifi_scroll = g_wifi_selected;
if (g_wifi_selected >= g_wifi_scroll + wifi_visible_rows())
g_wifi_scroll = g_wifi_selected - wifi_visible_rows() + 1;
}
return true;
}
if (g_wifi_psk_focus && wifi_selection_needs_key()) {
int result = mtk::text_input_key(g_wifi_psk_input, g_wifi_psk,
(int)sizeof(g_wifi_psk), key, nullptr);
return (result & mtk::TEXT_INPUT_CONSUMED) != 0;
}
if (key.ascii == 's' || key.ascii == 'S') {
if (!g_wifi_scanning) wifi_start_scan();
return true;
}
return false;
}
if (key.ascii == '\t') {
if (g_tab != TAB_CONFIG) {
set_tab(TAB_CONFIG);
@@ -690,7 +1298,9 @@ extern "C" void _start() {
montauk::exit(1);
}
montauk::wifi::saved_load(&g_wifi_saved);
refresh_state(true);
wifi_load_results();
render();
while (g_win.id >= 0 && !g_win.closed) {
@@ -701,7 +1311,10 @@ extern "C" void _start() {
if (r < 0) break;
if (r == 0) {
uint64_t now = montauk::get_milliseconds();
if (now - g_last_refresh >= 3000) {
// The Wi-Fi tab has work in flight worth watching closely; the rest
// of the app is happy with the slower cadence.
bool wifi_busy = g_tab == TAB_WIFI && (g_wifi_scanning || g_wifi_joining);
if (now - g_last_refresh >= (wifi_busy ? 400u : 3000u)) {
refresh_state(!g_dirty);
redraw = true;
}
@@ -718,6 +1331,12 @@ extern "C" void _start() {
g_mouse_x = ev.mouse.x;
g_mouse_y = ev.mouse.y;
redraw = true;
if (ev.mouse.scroll != 0 && g_tab == TAB_WIFI
&& ev.mouse.y >= wifi_list_y()
&& ev.mouse.y < wifi_action_rect().y) {
g_wifi_scroll += ev.mouse.scroll > 0 ? -1 : 1;
wifi_clamp_scroll();
}
if (handle_mouse(ev.mouse.x, ev.mouse.y,
ev.mouse.buttons, ev.mouse.prev_buttons)) {
redraw = true;
+63 -3
View File
@@ -17,6 +17,7 @@
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <montauk/wifi.h>
using namespace montauk;
@@ -363,14 +364,30 @@ static int cmd_debug() {
return 0;
}
static int cmd_connect(const char* ssid, const char* password) {
static int cmd_connect(const char* ssid, const char* password, bool remember) {
abi::WifiInfo info;
if (!require_adapter(info)) return 1;
// No passphrase on the command line: fall back to the one the desktop or a
// previous run saved, so "wifi connect Home" works on its own.
char saved[montauk::wifi::PSK_CAP];
if ((!password || !password[0])
&& montauk::wifi::lookup(ssid, saved, sizeof(saved)) && saved[0]) {
print("Using the saved passphrase for \""); print(ssid); print("\".\n");
password = saved;
remember = false; // already stored
}
print("Connecting to \""); print(ssid); print("\"...\n");
int rc = wifi_connect(ssid, password);
if (rc == 0) {
if (remember && password && password[0]) {
if (montauk::wifi::remember(ssid, password))
print("Saved to 0:/config/wifi.toml for next time.\n");
else
print("wifi: could not write 0:/config/wifi.toml.\n");
}
print("Connected.\n");
print("Run \"dhcp\" to get an address, then the network is usable\n");
print("just like a wired connection.\n");
@@ -410,6 +427,36 @@ static int cmd_connect(const char* ssid, const char* password) {
return 1;
}
static int cmd_saved() {
montauk::wifi::SavedList list;
montauk::wifi::saved_load(&list);
if (list.count == 0) {
print("No saved networks.\n");
return 0;
}
print("Saved networks (0:/config/wifi.toml):\n");
for (int i = 0; i < list.count; i++) {
print(" ");
print(list.items[i].ssid);
print("\n");
}
print("\nAutomatic reconnect is ");
print(list.autoconnect ? "on" : "off");
print(".\n");
return 0;
}
static int cmd_forget(const char* ssid) {
if (montauk::wifi::forget(ssid)) {
print("Forgot \""); print(ssid); print("\".\n");
return 0;
}
print("wifi: \""); print(ssid); print("\" was not saved.\n");
return 1;
}
static void usage() {
print("usage: wifi [command]\n\n");
print(" scan [seconds] scan and list nearby networks (default 5)\n");
@@ -417,7 +464,11 @@ static void usage() {
print(" debug diagnostics plus per-BSS scan detail\n");
print(" connect <ssid> [key] join a network (open or WPA2/WPA3-PSK)\n");
print(" status show the network currently joined\n");
print(" disconnect leave the current network\n\n");
print(" disconnect leave the current network\n");
print(" saved list remembered networks\n");
print(" forget <ssid> remove a remembered network\n\n");
print("A passphrase given to connect is remembered, and one that was\n");
print("remembered is used when connect is given only an SSID.\n\n");
print("With no command, runs a 5 second scan.\n");
}
@@ -454,7 +505,16 @@ extern "C" void _start() {
while (p[n] && n < (int)sizeof(pass) - 1) { pass[n] = p[n]; n++; }
while (n > 0 && pass[n - 1] == ' ') n--;
pass[n] = '\0';
exit(cmd_connect(ssid, pass));
exit(cmd_connect(ssid, pass, true));
} else if (streq(cmd, "saved")) {
exit(cmd_saved());
} else if (streq(cmd, "forget")) {
char ssid[40];
if (!next_token(&rest, ssid, sizeof(ssid))) {
print("wifi: forget needs an SSID\n");
exit(1);
}
exit(cmd_forget(ssid));
} else if (streq(cmd, "disconnect")) {
wifi_disconnect();
print("Disconnected.\n");
+2
View File
@@ -47,6 +47,7 @@ ICONS=(
"mimetypes/symbolic/application-x-executable-symbolic.svg"
"devices/symbolic/computer-symbolic.svg"
"devices/symbolic/network-wired-symbolic.svg"
"devices/symbolic/network-wireless-symbolic.svg"
"apps/symbolic/web-browser-symbolic.svg"
# Scalable (colorful) icons for app menu
"apps/scalable/utilities-terminal.svg"
@@ -60,6 +61,7 @@ ICONS=(
"places/scalable/user-home.svg"
"devices/scalable/computer.svg"
"devices/scalable/network-wired.svg"
"devices/scalable/network-wireless.svg"
"devices/scalable/printer.svg"
"devices/symbolic/printer-symbolic.svg"
"mimetypes/scalable/text-x-generic.svg"
+150 -2
View File
@@ -182,6 +182,13 @@ namespace montauk::abi {
// Paginated directory read (path, names, max, startIndex)
static constexpr uint64_t SYS_READDIR_AT = 136;
// Set adapter BD_ADDR (6-byte buffer, addr[0] = LSB)
static constexpr uint64_t SYS_BTSETADDR = 137;
// List bonded (paired) devices / forget a bond
static constexpr uint64_t SYS_BTBONDS = 138;
static constexpr uint64_t SYS_BTFORGET = 139;
/* Sdr.hpp -- software-defined radio receive API */
static constexpr uint64_t SYS_SDR_COUNT = 140; // number of receivers
static constexpr uint64_t SYS_SDR_INFO = 141; // (index, SdrDeviceInfo*)
@@ -192,13 +199,36 @@ namespace montauk::abi {
static constexpr uint64_t SYS_SDR_READ = 146; // (handle, buf, len) -> bytes
static constexpr uint64_t SYS_SDR_SETPARAM = 147; // (handle, param, value)
static constexpr uint64_t SYS_SDR_GETPARAM = 148; // (handle, param) -> value
// CPU power/thermal status
static constexpr uint64_t SYS_POWERINFO = 149; // (PowerInfo*) -> 0, -1 unsupported
// Framebuffer page flip (double-buffered scanout)
static constexpr uint64_t SYS_FBFLIP = 150;
// Absolute path of the running executable (for argv[0]).
static constexpr uint64_t SYS_GETEXECPATH = 151; // (index, flags) -> new front index; index=-1 queries support (1/0); flags bit0 = wait vsync
// Path metadata (size, timestamps, mode). (const char* path, FileStat* out) -> 0, -1 on error/unsupported.
static constexpr uint64_t SYS_STAT = 152;
static constexpr uint64_t SYS_SETUNIXTIME = 153;
// Display/modesetting control
static constexpr uint64_t SYS_DISPLAYINFO = 154;
static constexpr uint64_t SYS_DISPLAYMODES = 155;
static constexpr uint64_t SYS_DISPLAYSETMODE = 156;
static constexpr uint64_t SYS_DISPLAYBRIGHTNESS = 157;
// Wi-Fi adapter control
static constexpr uint64_t SYS_WIFI_SCAN = 158; // (WifiNetwork*, maxCount, timeoutMs) -> count
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
static constexpr int SDR_PARAM_SAMPLE_RATE = 1; // sample rate, Hz
@@ -238,7 +268,7 @@ namespace montauk::abi {
static constexpr int AUDIO_CTL_PAUSE = 3;
static constexpr int AUDIO_CTL_GET_OUTPUT = 4; // 0=HDA, 1=Bluetooth
static constexpr int AUDIO_CTL_SET_OUTPUT = 5; // Switch audio output
static constexpr int AUDIO_CTL_BT_STATUS = 6; // Get Bluetooth status
static constexpr int AUDIO_CTL_BT_STATUS = 6; // 0=unavailable, 1=setup, 2=ready
static constexpr int AUDIO_CTL_SET_MASTER_VOLUME = 7; // 0-100
static constexpr int AUDIO_CTL_GET_MASTER_VOLUME = 8;
static constexpr int AUDIO_CTL_SET_MUTE = 9; // 0/1, per-stream
@@ -279,6 +309,17 @@ namespace montauk::abi {
uint8_t Second;
};
// Path metadata returned by SYS_STAT. Timestamps are UTC unix seconds;
// a filesystem that does not record a given time reports it as 0.
struct FileStat {
uint64_t size; // file size in bytes
int64_t mtime; // last data modification time
int64_t ctime; // last inode (metadata) change time
int64_t atime; // last access time
uint32_t mode; // ext2/POSIX mode bits (type + permissions)
uint32_t isDir; // 1 if the entry is a directory, else 0
};
struct FbInfo {
uint64_t width;
uint64_t height;
@@ -321,7 +362,7 @@ namespace montauk::abi {
uint32_t capabilities;
uint32_t modeCount;
int32_t currentMode;
int32_t brightness;
int32_t brightness; // 0..100, or -1 when unavailable
uint16_t deviceId;
uint8_t generation;
uint8_t connectorType;
@@ -509,6 +550,12 @@ namespace montauk::abi {
char name[64];
};
// Bluetooth bonded (paired) device (returned by SYS_BTBONDS)
struct BtBondInfo {
uint8_t bdAddr[6];
uint8_t _pad[2];
};
// Software-defined radio receiver description (returned by SYS_SDR_INFO).
struct SdrDeviceInfo {
char name[64]; // e.g. "Realtek RTL2832U"
@@ -527,12 +574,113 @@ namespace montauk::abi {
uint32_t _pad2;
};
// Wi-Fi security suites reported in WifiNetwork.security.
static constexpr uint8_t WIFI_SEC_OPEN = 0;
static constexpr uint8_t WIFI_SEC_WEP = 1;
static constexpr uint8_t WIFI_SEC_WPA = 2;
static constexpr uint8_t WIFI_SEC_WPA2 = 3;
static constexpr uint8_t WIFI_SEC_WPA3 = 4;
// Adapter states reported in WifiInfo.state.
static constexpr uint8_t WIFI_STATE_ABSENT = 0; // no device
static constexpr uint8_t WIFI_STATE_DETECTED = 1; // waiting for firmware load
static constexpr uint8_t WIFI_STATE_BOOTING = 2;
static constexpr uint8_t WIFI_STATE_RUNNING = 3;
static constexpr uint8_t WIFI_STATE_ERROR = 4;
static constexpr uint8_t WIFI_STATE_RFKILL = 5; // radio disabled in hardware
// One scanned network (returned by SYS_WIFI_SCAN).
struct WifiNetwork {
char ssid[36]; // NUL-terminated; empty for hidden networks
uint8_t bssid[6];
uint8_t channel;
int8_t rssi; // dBm
uint8_t band; // 0 = 2.4 GHz, 1 = 5 GHz
uint8_t security; // WIFI_SEC_*
uint16_t beaconInterval; // TU
};
// Association progress reported in WifiInfo.connState.
static constexpr uint32_t WIFI_CONN_IDLE = 0;
static constexpr uint32_t WIFI_CONN_CONTEXTS_UP = 1;
static constexpr uint32_t WIFI_CONN_AUTHENTICATING = 2;
static constexpr uint32_t WIFI_CONN_AUTHENTICATED = 3;
static constexpr uint32_t WIFI_CONN_ASSOCIATING = 4;
static constexpr uint32_t WIFI_CONN_ASSOCIATED = 5;
static constexpr uint32_t WIFI_CONN_HANDSHAKING = 6;
static constexpr uint32_t WIFI_CONN_CONNECTED = 7;
static constexpr uint32_t WIFI_CONN_FAILED = 8;
// Negative results from SYS_WIFI_CONNECT.
static constexpr int WIFI_ERR_NO_ADAPTER = -1; // no adapter, or not ready
static constexpr int WIFI_ERR_NOT_FOUND = -2; // SSID absent from the scan
static constexpr int WIFI_ERR_NEED_KEY = -3; // encrypted, no passphrase
static constexpr int WIFI_ERR_UNSUPPORTED = -4; // WPA3-SAE, WEP, enterprise
static constexpr int WIFI_ERR_AUTH = -5; // key exchange rejected
static constexpr int WIFI_ERR_TIMEOUT = -6; // AP never answered
static constexpr int WIFI_ERR_FAILED = -7; // anything else
// Adapter status (returned by SYS_WIFI_INFO).
struct WifiInfo {
uint8_t mac[6];
uint8_t present; // 1 if a supported device was found
uint8_t state; // WIFI_STATE_*
uint8_t scanning;
uint8_t bands; // bit0 = 2.4 GHz, bit1 = 5 GHz
uint16_t channels; // usable channels after regulatory filtering
char fwVersion[32];
uint64_t rxPackets;
uint32_t fwErrors;
uint32_t connState; // WIFI_CONN_*
uint64_t txPackets;
char ssid[36]; // network joined, empty when disconnected
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 {
char name[32]; // short zone name (e.g. "THRM", "TZ00")
int32_t temperature; // tenths of degrees Celsius, or -1 if unavailable
uint32_t _pad;
};
// CPU power/thermal snapshot (returned by SYS_POWERINFO)
struct PowerInfo {
uint8_t hwpActive; // hardware P-state scaling enabled
uint8_t throttling; // thermal governor currently limiting frequency
uint8_t tempC; // package temperature, degrees C (0 = unknown)
uint8_t tjMaxC; // hardware throttle temperature
uint8_t highestPerf; // HWP performance range (ratio units)
uint8_t lowestPerf;
uint8_t curMaxPerf; // thermal governor's current ceiling
uint8_t epp; // energy/perf preference (0=perf, 255=power)
uint32_t baseMHz; // nominal base frequency (0 = unknown)
uint32_t maxMHz; // max turbo frequency
uint32_t effMHz; // measured average active frequency
uint32_t apIdleHint; // MWAIT hint used for AP deep idle
};
struct ProcInfo {
int32_t pid;
int32_t parentPid;
+39 -5
View File
@@ -161,24 +161,58 @@ struct Canvas {
// ---- Text ----
void text_bitmap(int x, int y, const char* str, Color c, int scale = 1) {
if (!str || !font_data || scale <= 0) return;
uint32_t pixel = c.to_pixel();
int cx = x;
for (int i = 0; str[i]; i++, cx += FONT_WIDTH * scale) {
const uint8_t* glyph =
&font_data[(unsigned char)str[i] * FONT_HEIGHT];
for (int row = 0; row < FONT_HEIGHT; row++) {
uint8_t bits = glyph[row];
if (!bits) continue;
for (int col = 0; col < FONT_WIDTH; col++) {
if (!(bits & (0x80 >> col))) continue;
int px = cx + col * scale;
int py = y + row * scale;
for (int sy = 0; sy < scale; sy++) {
int dy = py + sy;
if (dy < 0 || dy >= h) continue;
for (int sx = 0; sx < scale; sx++) {
int dx = px + sx;
if (dx >= 0 && dx < w) pixels[dy * w + dx] = pixel;
}
}
}
}
}
}
void text(int x, int y, const char* str, Color c) {
if (fonts::system_font && fonts::system_font->valid) {
fonts::system_font->draw_to_buffer(pixels, w, h, x, y, str, c, fonts::UI_SIZE);
if (fonts::system_font->draw_to_buffer(
pixels, w, h, x, y, str, c, fonts::UI_SIZE))
return;
}
text_bitmap(x, y, str, c);
}
void text_2x(int x, int y, const char* str, Color c) {
if (fonts::system_font && fonts::system_font->valid) {
fonts::system_font->draw_to_buffer(pixels, w, h, x, y, str, c, fonts::LARGE_SIZE);
if (fonts::system_font->draw_to_buffer(
pixels, w, h, x, y, str, c, fonts::LARGE_SIZE))
return;
}
text_bitmap(x, y, str, c, 2);
}
void text_mono(int x, int y, const char* str, Color c) {
if (fonts::mono && fonts::mono->valid) {
fonts::mono->draw_to_buffer(pixels, w, h, x, y, str, c, fonts::TERM_SIZE);
return;
if (fonts::mono->draw_to_buffer(
pixels, w, h, x, y, str, c, fonts::TERM_SIZE))
return;
}
text(x, y, str, c);
text_bitmap(x, y, str, c);
}
// ---- Icons ----
+47 -6
View File
@@ -15,9 +15,8 @@
namespace gui {
static constexpr int MAX_WINDOWS = 8;
static constexpr int MAX_WINDOWS = 32;
static constexpr int PANEL_HEIGHT = 32;
static constexpr int MAX_LAUNCHER_ITEMS = 64;
enum DesktopItemSection : uint8_t {
DESKTOP_ITEM_SECTION_HIDDEN = 0,
@@ -111,9 +110,10 @@ struct DesktopState {
bool launcher_open;
char launcher_query[64];
int launcher_query_len;
LauncherItem launcher_items[MAX_LAUNCHER_ITEMS];
LauncherItem* launcher_items; // dynamically grown
int launcher_item_count;
int launcher_results[MAX_LAUNCHER_ITEMS];
int launcher_item_capacity;
int* launcher_results; // indices into launcher_items, same capacity
int launcher_result_count;
int launcher_selected;
int launcher_scroll;
@@ -122,7 +122,6 @@ struct DesktopState {
SvgIcon icon_terminal;
SvgIcon icon_filemanager;
SvgIcon icon_sysinfo;
SvgIcon icon_appmenu;
SvgIcon icon_folder;
SvgIcon icon_file;
@@ -181,6 +180,48 @@ struct DesktopState {
uint64_t net_cfg_last_poll;
Rect net_icon_rect;
// Registered link-layer interfaces. The IP configuration is global to the
// stack, so the wired and wireless popups use `active` to decide which of
// them owns the address currently on screen.
static constexpr int MAX_NETIFS = 4;
montauk::abi::NetIfInfo netifs[MAX_NETIFS];
int netif_count;
bool eth_present;
// ---- Wi-Fi -------------------------------------------------------------
// The panel entry exists only when a supported adapter is present.
static constexpr int MAX_WIFI_NETWORKS = 32;
static constexpr int WIFI_SSID_CAP = 36;
static constexpr int WIFI_PSK_CAP = 72;
SvgIcon icon_wifi;
bool wifi_present;
bool wifi_popup_open;
Rect wifi_icon_rect;
montauk::abi::WifiInfo wifi_info;
montauk::abi::WifiNetwork wifi_networks[MAX_WIFI_NETWORKS];
int wifi_network_count;
uint64_t wifi_last_poll;
uint32_t wifi_scan_generation;
bool wifi_scanning;
int wifi_scroll; // first visible row of the list
bool wifi_boot_scan_started; // the automatic scan at startup
bool wifi_autoconnect_done; // saved-network join already tried
bool wifi_joining; // a join we started is in flight
bool wifi_dhcp_pending; // ask for a lease once the link is up
char wifi_joining_ssid[WIFI_SSID_CAP];
char wifi_status[96]; // last result line in the popup
uint64_t wifi_status_time;
// Passphrase prompt, shown when a selected network needs a key.
bool wifi_prompt_open;
char wifi_prompt_ssid[WIFI_SSID_CAP];
char wifi_prompt_password[WIFI_PSK_CAP];
int wifi_prompt_len;
bool wifi_prompt_reveal;
bool wifi_prompt_remember;
uint8_t wifi_prompt_security;
bool vol_popup_open;
Rect vol_icon_rect;
int vol_level; // 0-100
@@ -207,7 +248,7 @@ struct DesktopState {
// IDs of external windows we've sent a close event to but that haven't
// been destroyed yet by their owning process. Prevents the poll loop
// from re-creating them at the default position (visible flicker).
static constexpr int MAX_CLOSING = 8;
static constexpr int MAX_CLOSING = MAX_WINDOWS;
int closing_ext_ids[MAX_CLOSING];
int closing_ext_count;
+4 -2
View File
@@ -14,8 +14,10 @@ namespace gui {
static constexpr int FONT_WIDTH = 8;
static constexpr int FONT_HEIGHT = 16;
// Defined in font_data.cpp
extern const uint8_t font_data[256 * 16];
// Defined by programs that ship the built-in VGA fallback. Keep the symbol
// weak so Canvas remains usable by small apps that intentionally rely only on
// TrueType fonts, while login/desktop can recover when TrueType setup fails.
extern const uint8_t font_data[256 * 16] __attribute__((weak));
// Dynamic font height: TTF line height or 16 (bitmap fallback)
inline int system_font_height() {
+235 -4
View File
@@ -678,6 +678,78 @@ inline bool same_color(Color a, Color b) {
return a.r == b.r && a.g == b.g && a.b == b.b && a.a == b.a;
}
// ============================================================================
// Anti-aliased rounded-rect fill
// ============================================================================
// Coverage-based pixel blend; `cov` is 0..255 and the destination is treated
// as opaque.
inline void aa_blend_px(Canvas& c, int x, int y, Color color, int cov) {
if (x < 0 || x >= c.w || y < 0 || y >= c.h || cov <= 0) return;
if (cov > 255) cov = 255;
uint32_t dst = c.pixels[y * c.w + x];
uint32_t dr = (dst >> 16) & 0xFF, dg = (dst >> 8) & 0xFF, db = dst & 0xFF;
uint32_t a = (uint32_t)cov, ia = 255 - a;
uint32_t nr = (color.r * a + dr * ia + 127) / 255;
uint32_t ng = (color.g * a + dg * ia + 127) / 255;
uint32_t nb = (color.b * a + db * ia + 127) / 255;
c.pixels[y * c.w + x] = 0xFF000000u | (nr << 16) | (ng << 8) | nb;
}
// Rounded-rect fill with 4x4-supersampled corners. The straight interior is
// filled solid (fast); only the four corner arcs pay for anti-aliasing, so
// this stays cheap even for large frames.
inline void fill_round_rect_aa(Canvas& c, const Rect& box, int radius, Color color) {
if (box.w <= 0 || box.h <= 0) return;
int r = radius;
if (r < 0) r = 0;
if (r > box.w / 2) r = box.w / 2;
if (r > box.h / 2) r = box.h / 2;
if (r == 0) {
c.fill_rect(box.x, box.y, box.w, box.h, color);
return;
}
// Solid interior: center band + top/bottom edge bands between the corners.
c.fill_rect(box.x, box.y + r, box.w, box.h - 2 * r, color);
c.fill_rect(box.x + r, box.y, box.w - 2 * r, r, color);
c.fill_rect(box.x + r, box.y + box.h - r, box.w - 2 * r, r, color);
const int S = 4;
double r2 = (double)r * r;
// (cell origin x, cell origin y, arc center x, arc center y)
int corners[4][4] = {
{box.x, box.y, box.x + r, box.y + r},
{box.x + box.w - r, box.y, box.x + box.w - r, box.y + r},
{box.x, box.y + box.h - r, box.x + r, box.y + box.h - r},
{box.x + box.w - r, box.y + box.h - r, box.x + box.w - r, box.y + box.h - r},
};
for (auto& cn : corners) {
// Push the arc center a half pixel away from this corner (toward the
// box interior). Pixel-center sampling otherwise sits half a pixel
// closer to the center than the legacy pixel-corner test did, which
// filled the arcs fuller and made small radii read as square. Left
// corners share x == box.x, top corners share y == box.y.
double cx = cn[2] + (cn[0] == box.x ? 0.5 : -0.5);
double cy = cn[3] + (cn[1] == box.y ? 0.5 : -0.5);
for (int yy = 0; yy < r; yy++) {
int py = cn[1] + yy;
for (int xx = 0; xx < r; xx++) {
int px = cn[0] + xx;
int hits = 0;
for (int sy = 0; sy < S; sy++)
for (int sx = 0; sx < S; sx++) {
double dx = (px + (sx + 0.5) / S) - cx;
double dy = (py + (sy + 0.5) / S) - cy;
if (dx * dx + dy * dy <= r2) hits++;
}
if (hits) aa_blend_px(c, px, py, color, hits * 255 / (S * S));
}
}
}
}
inline void draw_rounded_frame(Canvas& c,
const Rect& bounds,
int radius,
@@ -686,16 +758,16 @@ inline void draw_rounded_frame(Canvas& c,
int border_w = 1) {
if (bounds.empty()) return;
if (border_w <= 0 || same_color(fill, border)) {
c.fill_rounded_rect(bounds.x, bounds.y, bounds.w, bounds.h, gui_max(radius, 0), fill);
fill_round_rect_aa(c, bounds, gui_max(radius, 0), fill);
return;
}
c.fill_rounded_rect(bounds.x, bounds.y, bounds.w, bounds.h, gui_max(radius, 0), border);
fill_round_rect_aa(c, bounds, gui_max(radius, 0), border);
int inner_w = bounds.w - border_w * 2;
int inner_h = bounds.h - border_w * 2;
if (inner_w <= 0 || inner_h <= 0) return;
c.fill_rounded_rect(bounds.x + border_w, bounds.y + border_w,
inner_w, inner_h, gui_max(radius - border_w, 0), fill);
Rect inner = {bounds.x + border_w, bounds.y + border_w, inner_w, inner_h};
fill_round_rect_aa(c, inner, gui_max(radius - border_w, 0), fill);
}
inline ButtonColors resolve_button_colors(ButtonVariant variant,
@@ -942,6 +1014,165 @@ inline void draw_radio(Canvas& c,
label, theme.text);
}
// ============================================================================
// Checkboxes and disclosure arrows
// ============================================================================
enum CheckState : uint8_t {
CHECK_OFF = 0,
CHECK_ON,
CHECK_MIXED,
};
inline CheckState check_state(bool checked) {
return checked ? CHECK_ON : CHECK_OFF;
}
inline Rect checkbox_indicator_rect(const Rect& option, int size = 16) {
return {option.x, option.y + (option.h - size) / 2, size, size};
}
// Squared distance from a point to a line segment (no sqrt; used for the
// thickness test of the checkmark strokes).
inline double checkbox_seg_dist2(double px, double py,
double ax, double ay, double bx, double by) {
double dx = bx - ax, dy = by - ay;
double len2 = dx * dx + dy * dy;
double t = len2 > 0 ? ((px - ax) * dx + (py - ay) * dy) / len2 : 0.0;
if (t < 0) t = 0; else if (t > 1) t = 1;
double ex = px - (ax + t * dx), ey = py - (ay + t * dy);
return ex * ex + ey * ey;
}
// Anti-aliased two-stroke checkmark via 4x4 supersampling of a fixed-width
// polyline.
inline void draw_check_mark(Canvas& c, const Rect& box, Color color) {
double ax = box.x + box.w * 0.24, ay = box.y + box.h * 0.50;
double bx = box.x + box.w * 0.42, by = box.y + box.h * 0.68;
double cx = box.x + box.w * 0.74, cy = box.y + box.h * 0.26;
double hw = box.w * 0.085 + 0.35; // half stroke width
double hw2 = hw * hw;
const int S = 4;
for (int py = box.y - 1; py <= box.y + box.h + 1; py++) {
if (py < 0 || py >= c.h) continue;
for (int px = box.x - 1; px <= box.x + box.w + 1; px++) {
if (px < 0 || px >= c.w) continue;
int hits = 0;
for (int sy = 0; sy < S; sy++)
for (int sx = 0; sx < S; sx++) {
double fx = px + (sx + 0.5) / S;
double fy = py + (sy + 0.5) / S;
double d1 = checkbox_seg_dist2(fx, fy, ax, ay, bx, by);
double d2 = checkbox_seg_dist2(fx, fy, bx, by, cx, cy);
if ((d1 < d2 ? d1 : d2) <= hw2) hits++;
}
if (hits) aa_blend_px(c, px, py, color, hits * 255 / (S * S));
}
}
}
inline void draw_checkbox(Canvas& c,
const Rect& option,
const char* label,
CheckState state,
const Theme& theme,
bool enabled = true,
bool hovered = false) {
Rect box = checkbox_indicator_rect(option);
int radius = gui_max(box.w / 5, 2);
Color fill, border, mark;
if (!enabled) {
fill = theme.disabled_bg;
border = theme.disabled_bg;
mark = theme.disabled_fg;
} else if (state == CHECK_OFF) {
fill = colors::WHITE;
border = hovered ? theme.accent : theme.border;
mark = theme.accent_fg;
} else {
fill = hovered ? theme.accent_hover : theme.accent;
border = fill;
mark = theme.accent_fg;
}
if (state == CHECK_OFF) {
// Colored ring with a white interior.
fill_round_rect_aa(c, box, radius, border);
Rect inner = {box.x + 1, box.y + 1, box.w - 2, box.h - 2};
fill_round_rect_aa(c, inner, gui_max(radius - 1, 1), fill);
} else {
fill_round_rect_aa(c, box, radius, fill);
}
if (state == CHECK_ON) {
draw_check_mark(c, box, mark);
} else if (state == CHECK_MIXED) {
Rect dash = {box.x + 4, box.y + box.h / 2 - 1, box.w - 8, 2};
fill_round_rect_aa(c, dash, 1, mark);
}
if (label && label[0]) {
int fh = system_font_height();
c.text(box.x + box.w + theme.gap_sm,
option.y + (option.h - fh) / 2,
label, enabled ? theme.text : theme.text_muted);
}
}
inline Rect disclosure_rect(const Rect& option, int size = 16) {
return {option.x, option.y + (option.h - size) / 2, size, size};
}
inline void draw_disclosure(Canvas& c,
const Rect& box,
bool expanded,
const Theme& theme,
bool hovered = false) {
Color color = hovered ? theme.text : theme.text_subtle;
double cx = box.x + box.w / 2.0;
double cy = box.y + box.h / 2.0;
double s = box.w * 0.26;
if (s < 3) s = 3;
// Triangle vertices (equilateral-ish), rotated per state.
double vx[3], vy[3];
if (expanded) { // pointing down
vx[0] = cx - s; vy[0] = cy - s * 0.6;
vx[1] = cx + s; vy[1] = cy - s * 0.6;
vx[2] = cx; vy[2] = cy + s * 0.75;
} else { // pointing right
vx[0] = cx - s * 0.6; vy[0] = cy - s;
vx[1] = cx - s * 0.6; vy[1] = cy + s;
vx[2] = cx + s * 0.75; vy[2] = cy;
}
// Anti-aliased fill via 4x4 supersampling of the triangle's half-plane test.
auto edge = [](double ax, double ay, double bx, double by, double px, double py) {
return (px - ax) * (by - ay) - (py - ay) * (bx - ax);
};
const int S = 4;
for (int py = box.y; py < box.y + box.h; py++) {
if (py < 0 || py >= c.h) continue;
for (int px = box.x; px < box.x + box.w; px++) {
if (px < 0 || px >= c.w) continue;
int hits = 0;
for (int sy = 0; sy < S; sy++)
for (int sx = 0; sx < S; sx++) {
double fx = px + (sx + 0.5) / S;
double fy = py + (sy + 0.5) / S;
double e0 = edge(vx[0], vy[0], vx[1], vy[1], fx, fy);
double e1 = edge(vx[1], vy[1], vx[2], vy[2], fx, fy);
double e2 = edge(vx[2], vy[2], vx[0], vy[0], fx, fy);
bool inside = (e0 >= 0 && e1 >= 0 && e2 >= 0) ||
(e0 <= 0 && e1 <= 0 && e2 <= 0);
if (inside) hits++;
}
if (hits) aa_blend_px(c, px, py, color, hits * 255 / (S * S));
}
}
}
inline Rect swatch_rect(int row_x, int row_y, int index, int size = 24, int gap = 6) {
return {row_x + index * (size + gap), row_y, size, size};
}
+12 -2
View File
@@ -795,11 +795,21 @@ static inline void terminal_resize(TerminalState* t, int new_cols, int new_rows)
}
}
int new_scrollback = keep - new_rows;
// Anchor the new visible region to the cursor's line. Treating the bottom
// new_rows of kept content as the screen (the naive keep - new_rows) only
// works when the screen is full: with a partly filled screen -- e.g. a
// shell prompt a few lines down with blank rows below it -- it would push
// the real text up into scrollback and drop the view onto the blank region,
// so a zoom appears to scroll down and strands the cursor in empty space.
// Instead pin the cursor to the bottom row when there is enough history
// above it, otherwise keep the content anchored at the top.
int abs_cursor_y = t->scrollback_lines + t->cursor_y - discard;
if (abs_cursor_y < 0) abs_cursor_y = 0;
int new_scrollback = abs_cursor_y - (new_rows - 1);
if (new_scrollback < 0) new_scrollback = 0;
if (new_scrollback > t->max_scrollback) new_scrollback = t->max_scrollback;
// Adjust cursor
int abs_cursor_y = t->scrollback_lines + t->cursor_y - discard;
int new_cursor_y = abs_cursor_y - new_scrollback;
if (new_cursor_y < 0) new_cursor_y = 0;
if (new_cursor_y >= new_rows) new_cursor_y = new_rows - 1;
+15 -4
View File
@@ -121,10 +121,18 @@ struct TrueTypeFont {
return false;
}
montauk::read(fd, data, 0, size);
int read_result = montauk::read(fd, data, 0, size);
montauk::close(fd);
if (read_result < 0 || (uint64_t)read_result != size) {
montauk::free(data);
data = nullptr;
return false;
}
if (!stbtt_InitFont(&info, data, stbtt_GetFontOffsetForIndex(data, 0))) {
// stbtt_GetFontOffsetForIndex returns -1 for non-font data; passing
// that into stbtt_InitFont makes it dereference data + (uint32)-1.
int off = stbtt_GetFontOffsetForIndex(data, 0);
if (off < 0 || !stbtt_InitFont(&info, data, off)) {
montauk::free(data);
data = nullptr;
return false;
@@ -308,13 +316,14 @@ struct TrueTypeFont {
draw(fb, x, y, text, fg, pixel_size);
}
void draw_to_buffer(uint32_t* pixels, int buf_w, int buf_h,
bool draw_to_buffer(uint32_t* pixels, int buf_w, int buf_h,
int x, int y, const char* text,
Color color, int pixel_size) {
if (!valid) return;
if (!valid) return false;
GlyphCache* gc = get_cache(pixel_size);
int cx = x;
int baseline = y + gc->ascent;
bool drew_pixel = false;
for (int i = 0; text[i]; i++) {
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
@@ -331,6 +340,7 @@ struct TrueTypeFont {
if (dx < 0 || dx >= buf_w) continue;
uint8_t alpha = g->bitmap[row * g->width + col];
if (alpha == 0) continue;
drew_pixel = true;
if (alpha == 255) {
pixels[dy * buf_w + dx] =
@@ -353,6 +363,7 @@ struct TrueTypeFont {
}
cx += g->advance;
}
return drew_pixel;
}
// Draw text to buffer with clip rectangle (pixels outside clip_x..clip_x+clip_w are not drawn)
+3
View File
@@ -16,6 +16,9 @@ int islower(int c);
int isprint(int c);
int ispunct(int c);
int isxdigit(int c);
#define isascii(c) (((c) & ~0x7F) == 0)
#define toascii(c) ((c) & 0x7F)
int iscntrl(int c);
int isgraph(int c);
int toupper(int c);
+5
View File
@@ -9,8 +9,13 @@ extern "C" {
#define NAME_MAX 255
#define DT_UNKNOWN 0
#define DT_FIFO 1
#define DT_CHR 2
#define DT_DIR 4
#define DT_BLK 6
#define DT_REG 8
#define DT_LNK 10
#define DT_SOCK 12
struct dirent {
unsigned char d_type;
+82 -12
View File
@@ -9,18 +9,88 @@ extern "C" {
extern int errno;
#define ENOENT 2
#define EIO 5
#define ENOMEM 12
#define EACCES 13
#define EINVAL 22
#define ERANGE 34
#define ENOSYS 38
#define EISDIR 21
#define ENOTDIR 20
#define EEXIST 17
#define EBADF 9
#define EPERM 1
/* Linux errno numbering. */
#define EPERM 1
#define ENOENT 2
#define ESRCH 3
#define EINTR 4
#define EIO 5
#define ENXIO 6
#define E2BIG 7
#define ENOEXEC 8
#define EBADF 9
#define ECHILD 10
#define EAGAIN 11
#define ENOMEM 12
#define EACCES 13
#define EFAULT 14
#define EBUSY 16
#define EEXIST 17
#define EXDEV 18
#define ENODEV 19
#define ENOTDIR 20
#define EISDIR 21
#define EINVAL 22
#define ENFILE 23
#define EMFILE 24
#define ENOTTY 25
#define EFBIG 27
#define ENOSPC 28
#define ESPIPE 29
#define EROFS 30
#define EMLINK 31
#define EPIPE 32
#define EDOM 33
#define ERANGE 34
#define EDEADLK 35
#define ENAMETOOLONG 36
#define ENOLCK 37
#define ENOSYS 38
#define ENOTEMPTY 39
#define ELOOP 40
#define EWOULDBLOCK EAGAIN
#define ETXTBSY 26
#define ENOMSG 42
#define EIDRM 43
#define ENOSTR 60
#define ENODATA 61
#define ETIME 62
#define ENOSR 63
#define ENOLINK 67
#define EPROTO 71
#define EMULTIHOP 72
#define EBADMSG 74
#define EOVERFLOW 75
#define EILSEQ 84
#define ENOTSOCK 88
#define EDESTADDRREQ 89
#define EMSGSIZE 90
#define EPROTOTYPE 91
#define ENOPROTOOPT 92
#define EPROTONOSUPPORT 93
#define EOPNOTSUPP 95 /* == ENOTSUP, Linux numbering */
#define EAFNOSUPPORT 97
#define EADDRINUSE 98
#define EADDRNOTAVAIL 99
#define ENETDOWN 100
#define ENETUNREACH 101
#define ENETRESET 102
#define ECONNABORTED 103
#define ECONNRESET 104
#define ENOBUFS 105
#define EISCONN 106
#define ENOTCONN 107
#define ETIMEDOUT 110
#define ECONNREFUSED 111
#define EHOSTUNREACH 113
#define EALREADY 114
#define EINPROGRESS 115
#define ESTALE 116
#define EDQUOT 122
#define ECANCELED 125
#define EOWNERDEAD 130
#define ENOTRECOVERABLE 131
#define ENOTSUP 95
#ifdef __cplusplus
}
+19 -5
View File
@@ -7,13 +7,27 @@
extern "C" {
#endif
#define O_RDONLY 0
#define O_WRONLY 1
#define O_RDWR 2
#define O_CREAT 0x40
#define O_TRUNC 0x200
#define O_RDONLY 0
#define O_WRONLY 1
#define O_RDWR 2
#define O_ACCMODE 3
#define O_CREAT 0x40
#define O_EXCL 0x80
#define O_TRUNC 0x200
#define O_APPEND 0x400
#define O_NONBLOCK 0x800
#define O_CLOEXEC 0x80000
#define F_DUPFD 0
#define F_GETFD 1
#define F_SETFD 2
#define F_GETFL 3
#define F_SETFL 4
#define FD_CLOEXEC 1
int open(const char *path, int flags, ...);
int fcntl(int fd, int cmd, ...);
#ifdef __cplusplus
}
+33
View File
@@ -23,5 +23,38 @@
#define PRIX16 "X"
#define PRIX32 "X"
#define PRIX64 "lX"
#define PRIo8 "o"
#define PRIo16 "o"
#define PRIo32 "o"
#define PRIo64 "lo"
#define PRIdPTR "ld"
#define PRIiPTR "li"
#define PRIuPTR "lu"
#define PRIxPTR "lx"
#define PRIXPTR "lX"
#define PRIdMAX "ld"
#define PRIuMAX "lu"
#define PRIxMAX "lx"
#define SCNd8 "d"
#define SCNd16 "d"
#define SCNd32 "d"
#define SCNd64 "ld"
#define SCNi8 "i"
#define SCNi16 "i"
#define SCNi32 "i"
#define SCNi64 "li"
#define SCNu8 "u"
#define SCNu16 "u"
#define SCNu32 "u"
#define SCNu64 "lu"
#define SCNx8 "x"
#define SCNx16 "x"
#define SCNx32 "x"
#define SCNx64 "lx"
#define SCNdPTR "ld"
#define SCNuPTR "lu"
#define SCNxPTR "lx"
#endif /* _LIBC_INTTYPES_H */
+39
View File
@@ -11,8 +11,47 @@ extern "C" {
#define INFINITY __builtin_inff()
#define NAN __builtin_nanf("")
/* C99 floating-point classification. */
#define FP_NAN 0
#define FP_INFINITE 1
#define FP_ZERO 2
#define FP_SUBNORMAL 3
#define FP_NORMAL 4
#define fpclassify(x) \
__builtin_fpclassify(FP_NAN, FP_INFINITE, FP_NORMAL, FP_SUBNORMAL, \
FP_ZERO, x)
#define isnan(x) __builtin_isnan(x)
#define isinf(x) __builtin_isinf(x)
#define isfinite(x) __builtin_isfinite(x)
#define isnormal(x) __builtin_isnormal(x)
#define signbit(x) __builtin_signbit(x)
double fabs(double x);
double frexp(double x, int *exp);
/* C89 modf and the C99 float variants assumed by hosted libstdc++
(--with-newlib crossconfig). The float forms wrap the double
implementations. */
double modf(double x, double *iptr);
float modff(float x, float *iptr);
double hypot(double x, double y);
float hypotf(float x, float y);
float acosf(float x);
float asinf(float x);
float atanf(float x);
float atan2f(float y, float x);
float coshf(float x);
float expf(float x);
float fmodf(float x, float y);
float frexpf(float x, int *exp);
float ldexpf(float x, int exp);
float logf(float x);
float log10f(float x);
float powf(float x, float y);
float sinhf(float x);
float tanf(float x);
float tanhf(float x);
double ldexp(double x, int n);
long double ldexpl(long double x, int n);
double floor(double x);
+9
View File
@@ -0,0 +1,9 @@
#ifndef _LIBC_MEMORY_H
#define _LIBC_MEMORY_H
#pragma once
/* Traditional alias for string.h. */
#include <string.h>
#endif /* _LIBC_MEMORY_H */
+175 -100
View File
@@ -19,106 +19,177 @@
extern "C" {
#endif
/* ====================================================================
Syscall numbers
==================================================================== */
#define MTK_SYS_EXIT 0
#define MTK_SYS_YIELD 1
#define MTK_SYS_SLEEP_MS 2
#define MTK_SYS_GETPID 3
#define MTK_SYS_PRINT 4
#define MTK_SYS_PUTCHAR 5
#define MTK_SYS_OPEN 6
#define MTK_SYS_READ 7
#define MTK_SYS_GETSIZE 8
#define MTK_SYS_CLOSE 9
#define MTK_SYS_READDIR 10
#define MTK_SYS_ALLOC 11
#define MTK_SYS_FREE 12
#define MTK_SYS_GETTICKS 13
#define MTK_SYS_GETMILLISECONDS 14
#define MTK_SYS_GETINFO 15
#define MTK_SYS_ISKEYAVAILABLE 16
#define MTK_SYS_GETKEY 17
#define MTK_SYS_GETCHAR 18
#define MTK_SYS_PING 19
#define MTK_SYS_SPAWN 20
#define MTK_SYS_WAITPID 23
#define MTK_SYS_TERMSIZE 24
#define MTK_SYS_GETARGS 25
#define MTK_SYS_RESET 26
#define MTK_SYS_SHUTDOWN 27
#define MTK_SYS_SETUNIXTIME 153
#define MTK_SYS_GETTIME 28
#define MTK_SYS_SOCKET 29
#define MTK_SYS_CONNECT 30
#define MTK_SYS_BIND 31
#define MTK_SYS_LISTEN 32
#define MTK_SYS_ACCEPT 33
#define MTK_SYS_SEND 34
#define MTK_SYS_RECV 35
#define MTK_SYS_CLOSESOCK 36
#define MTK_SYS_GETNETCFG 37
#define MTK_SYS_SETNETCFG 38
#define MTK_SYS_SENDTO 39
#define MTK_SYS_RECVFROM 40
#define MTK_SYS_FWRITE 41
#define MTK_SYS_FCREATE 42
#define MTK_SYS_TERMSCALE 43
#define MTK_SYS_RESOLVE 44
#define MTK_SYS_GETRANDOM 45
#define MTK_SYS_KLOG 46
#define MTK_SYS_MOUSESTATE 47
#define MTK_SYS_SETMOUSEBOUNDS 48
#define MTK_SYS_SPAWN_REDIR 49
#define MTK_SYS_CHILDIO_READ 50
#define MTK_SYS_CHILDIO_WRITE 51
#define MTK_SYS_WINCREATE 54
#define MTK_SYS_WINDESTROY 55
#define MTK_SYS_WINPRESENT 56
#define MTK_SYS_WINPOLL 57
#define MTK_SYS_WINENUM 58
#define MTK_SYS_WINMAP 59
#define MTK_SYS_WINUNMAP 97
#define MTK_SYS_WINSENDEVENT 60
#define MTK_SYS_PROCLIST 61
#define MTK_SYS_KILL 62
#define MTK_SYS_WINRESIZE 64
#define MTK_SYS_WINSETSCALE 65
#define MTK_SYS_WINGETSCALE 66
#define MTK_SYS_MEMSTATS 67
#define MTK_SYS_WINSETCURSOR 68
#define MTK_SYS_WINSETFLAGS 126
#define MTK_SYS_FDELETE 77
#define MTK_SYS_FMKDIR 78
#define MTK_SYS_FRENAME 94
#define MTK_SYS_DRIVELIST 79
#define MTK_SYS_DRIVELABEL 124
#define MTK_SYS_AUDIOOPEN 80
#define MTK_SYS_AUDIOCLOSE 81
#define MTK_SYS_AUDIOWRITE 82
#define MTK_SYS_AUDIOCTL 83
#define MTK_SYS_SETTZ 90
#define MTK_SYS_GETTZ 91
#define MTK_SYS_GETCWD 95
#define MTK_SYS_CHDIR 96
#define MTK_SYS_DUPHANDLE 98
#define MTK_SYS_WAIT_HANDLE 99
#define MTK_SYS_STREAM_CREATE 100
#define MTK_SYS_STREAM_READ 101
#define MTK_SYS_STREAM_WRITE 102
#define MTK_SYS_MAILBOX_CREATE 103
#define MTK_SYS_MAILBOX_SEND 104
#define MTK_SYS_MAILBOX_RECV 105
#define MTK_SYS_WAITSET_CREATE 106
#define MTK_SYS_WAITSET_ADD 107
#define MTK_SYS_WAITSET_REMOVE 108
#define MTK_SYS_WAITSET_WAIT 109
#define MTK_SYS_PROC_OPEN 110
#define MTK_SYS_SURFACE_CREATE 111
#define MTK_SYS_SURFACE_MAP 112
#define MTK_SYS_SURFACE_RESIZE 113
/* @SYSCALLS-BEGIN
Generated from Api/Syscall.hpp by scripts/montauk-syscalls.py.
Do not edit by hand; run 'make gen-syscalls' to refresh.
Wrapper functions below are hand-written. */
#define MTK_SYS_EXIT 0
#define MTK_SYS_YIELD 1
#define MTK_SYS_SLEEP_MS 2
#define MTK_SYS_GETPID 3
#define MTK_SYS_PRINT 4
#define MTK_SYS_PUTCHAR 5
#define MTK_SYS_OPEN 6
#define MTK_SYS_READ 7
#define MTK_SYS_GETSIZE 8
#define MTK_SYS_CLOSE 9
#define MTK_SYS_READDIR 10
#define MTK_SYS_ALLOC 11
#define MTK_SYS_FREE 12
#define MTK_SYS_GETTICKS 13
#define MTK_SYS_GETMILLISECONDS 14
#define MTK_SYS_GETINFO 15
#define MTK_SYS_ISKEYAVAILABLE 16
#define MTK_SYS_GETKEY 17
#define MTK_SYS_GETCHAR 18
#define MTK_SYS_PING 19
#define MTK_SYS_SPAWN 20
#define MTK_SYS_FBINFO 21
#define MTK_SYS_FBMAP 22
#define MTK_SYS_WAITPID 23
#define MTK_SYS_TERMSIZE 24
#define MTK_SYS_GETARGS 25
#define MTK_SYS_RESET 26
#define MTK_SYS_SHUTDOWN 27
#define MTK_SYS_GETTIME 28
#define MTK_SYS_SOCKET 29
#define MTK_SYS_CONNECT 30
#define MTK_SYS_BIND 31
#define MTK_SYS_LISTEN 32
#define MTK_SYS_ACCEPT 33
#define MTK_SYS_SEND 34
#define MTK_SYS_RECV 35
#define MTK_SYS_CLOSESOCK 36
#define MTK_SYS_GETNETCFG 37
#define MTK_SYS_SETNETCFG 38
#define MTK_SYS_SENDTO 39
#define MTK_SYS_RECVFROM 40
#define MTK_SYS_FWRITE 41
#define MTK_SYS_FCREATE 42
#define MTK_SYS_TERMSCALE 43
#define MTK_SYS_RESOLVE 44
#define MTK_SYS_GETRANDOM 45
#define MTK_SYS_KLOG 46
#define MTK_SYS_MOUSESTATE 47
#define MTK_SYS_SETMOUSEBOUNDS 48
#define MTK_SYS_SPAWN_REDIR 49
#define MTK_SYS_CHILDIO_READ 50
#define MTK_SYS_CHILDIO_WRITE 51
#define MTK_SYS_CHILDIO_WRITEKEY 52
#define MTK_SYS_CHILDIO_SETTERMSZ 53
#define MTK_SYS_WINCREATE 54
#define MTK_SYS_WINDESTROY 55
#define MTK_SYS_WINPRESENT 56
#define MTK_SYS_WINPOLL 57
#define MTK_SYS_WINENUM 58
#define MTK_SYS_WINMAP 59
#define MTK_SYS_WINSENDEVENT 60
#define MTK_SYS_PROCLIST 61
#define MTK_SYS_KILL 62
#define MTK_SYS_DEVLIST 63
#define MTK_SYS_WINRESIZE 64
#define MTK_SYS_WINSETSCALE 65
#define MTK_SYS_WINGETSCALE 66
#define MTK_SYS_MEMSTATS 67
#define MTK_SYS_WINSETCURSOR 68
#define MTK_SYS_DISKINFO 69
#define MTK_SYS_PARTLIST 70
#define MTK_SYS_DISKREAD 71
#define MTK_SYS_DISKWRITE 72
#define MTK_SYS_GPTINIT 73
#define MTK_SYS_GPTADD 74
#define MTK_SYS_FSMOUNT 75
#define MTK_SYS_FSFORMAT 76
#define MTK_SYS_FDELETE 77
#define MTK_SYS_FMKDIR 78
#define MTK_SYS_DRIVELIST 79
#define MTK_SYS_AUDIOOPEN 80
#define MTK_SYS_AUDIOCLOSE 81
#define MTK_SYS_AUDIOWRITE 82
#define MTK_SYS_AUDIOCTL 83
#define MTK_SYS_BTSCAN 84
#define MTK_SYS_BTCONNECT 85
#define MTK_SYS_BTDISCONNECT 86
#define MTK_SYS_BTLIST 87
#define MTK_SYS_BTINFO 88
#define MTK_SYS_SUSPEND 89
#define MTK_SYS_SETTZ 90
#define MTK_SYS_GETTZ 91
#define MTK_SYS_SETUSER 92
#define MTK_SYS_GETUSER 93
#define MTK_SYS_FRENAME 94
#define MTK_SYS_GETCWD 95
#define MTK_SYS_CHDIR 96
#define MTK_SYS_WINUNMAP 97
#define MTK_SYS_DUPHANDLE 98
#define MTK_SYS_WAIT_HANDLE 99
#define MTK_SYS_STREAM_CREATE 100
#define MTK_SYS_STREAM_READ 101
#define MTK_SYS_STREAM_WRITE 102
#define MTK_SYS_MAILBOX_CREATE 103
#define MTK_SYS_MAILBOX_SEND 104
#define MTK_SYS_MAILBOX_RECV 105
#define MTK_SYS_WAITSET_CREATE 106
#define MTK_SYS_WAITSET_ADD 107
#define MTK_SYS_WAITSET_REMOVE 108
#define MTK_SYS_WAITSET_WAIT 109
#define MTK_SYS_PROC_OPEN 110
#define MTK_SYS_SURFACE_CREATE 111
#define MTK_SYS_SURFACE_MAP 112
#define MTK_SYS_SURFACE_RESIZE 113
#define MTK_SYS_LOAD_LIB 114
#define MTK_SYS_UNLOAD_LIB 115
#define MTK_SYS_DLSYM 116
#define MTK_SYS_GETLIBBASE 117
#define MTK_SYS_CRASH_REPORT 118
#define MTK_SYS_CLIPBOARD_SET_TEXT 119
#define MTK_SYS_CLIPBOARD_GET_INFO 120
#define MTK_SYS_CLIPBOARD_GET_TEXT 121
#define MTK_SYS_CLIPBOARD_CLEAR 122
#define MTK_SYS_INPUT_WAIT 123
#define MTK_SYS_DRIVELABEL 124
#define MTK_SYS_NETSTATUS 125
#define MTK_SYS_WINSETFLAGS 126
#define MTK_SYS_DRIVEKIND 127
#define MTK_SYS_AUDIOLIST 128
#define MTK_SYS_AUDIOWAIT 129
#define MTK_SYS_THREAD_SPAWN 130
#define MTK_SYS_THREAD_EXIT 131
#define MTK_SYS_THREAD_JOIN 132
#define MTK_SYS_THREAD_SELF 133
#define MTK_SYS_FS_SYNC 134
#define MTK_SYS_POWER_REQUEST 135
#define MTK_SYS_READDIR_AT 136
#define MTK_SYS_BTSETADDR 137
#define MTK_SYS_BTBONDS 138
#define MTK_SYS_BTFORGET 139
#define MTK_SYS_SDR_COUNT 140
#define MTK_SYS_SDR_INFO 141
#define MTK_SYS_SDR_OPEN 142
#define MTK_SYS_SDR_CLOSE 143
#define MTK_SYS_SDR_START 144
#define MTK_SYS_SDR_STOP 145
#define MTK_SYS_SDR_READ 146
#define MTK_SYS_SDR_SETPARAM 147
#define MTK_SYS_SDR_GETPARAM 148
#define MTK_SYS_POWERINFO 149
#define MTK_SYS_FBFLIP 150
#define MTK_SYS_GETEXECPATH 151
#define MTK_SYS_STAT 152
#define MTK_SYS_SETUNIXTIME 153
#define MTK_SYS_DISPLAYINFO 154
#define MTK_SYS_DISPLAYMODES 155
#define MTK_SYS_DISPLAYSETMODE 156
#define MTK_SYS_DISPLAYBRIGHTNESS 157
#define MTK_SYS_WIFI_SCAN 158
#define MTK_SYS_WIFI_INFO 159
#define MTK_SYS_WIFI_CONNECT 160
#define MTK_SYS_WIFI_DISCONNECT 161
#define MTK_SYS_WIFI_SCAN_START 162
#define MTK_SYS_WIFI_RESULTS 163
#define MTK_SYS_WIFI_CONNECT_ASYNC 164
#define MTK_SYS_NETIFS 165
/* @SYSCALLS-END */
#define MTK_SOCK_TCP 1
#define MTK_SOCK_UDP 2
@@ -354,6 +425,10 @@ static inline int mtk_getargs(char *buf, unsigned long max_len) {
return (int)_mtk_syscall2(MTK_SYS_GETARGS, (long)buf, (long)max_len);
}
static inline int mtk_getexecpath(char *buf, unsigned long maxLen) {
return (int)mtk_syscall2(MTK_SYS_GETEXECPATH, (long)buf, (long)maxLen);
}
static inline int mtk_chdir(const char *path) {
return (int)_mtk_syscall1(MTK_SYS_CHDIR, (long)path);
}
+23 -1
View File
@@ -3,6 +3,8 @@
#pragma once
#include <sys/types.h>
#ifdef __cplusplus
extern "C" {
#endif
@@ -10,7 +12,26 @@ extern "C" {
typedef int sig_atomic_t;
typedef void (*sighandler_t)(int);
#define SIGINT 2
/* Linux signal numbering. Only SIGINT is ever delivered today; the
rest exist so hosted code can name them. */
#define SIGHUP 1
#define SIGINT 2
#define SIGQUIT 3
#define SIGILL 4
#define SIGTRAP 5
#define SIGABRT 6
#define SIGBUS 7
#define SIGFPE 8
#define SIGKILL 9
#define SIGUSR1 10
#define SIGSEGV 11
#define SIGUSR2 12
#define SIGPIPE 13
#define SIGALRM 14
#define SIGTERM 15
#define SIGCHLD 17
#define SIGCONT 18
#define SIGSTOP 19
#define SIG_DFL ((sighandler_t)0)
#define SIG_IGN ((sighandler_t)1)
@@ -18,6 +39,7 @@ typedef void (*sighandler_t)(int);
sighandler_t signal(int sig, sighandler_t handler);
int raise(int sig);
int kill(pid_t pid, int sig);
#ifdef __cplusplus
}
+60
View File
@@ -0,0 +1,60 @@
#ifndef _LIBC_SPAWN_H
#define _LIBC_SPAWN_H
#pragma once
#include <sys/types.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* posix_spawn for MontaukOS, layered on SYS_SPAWN.
*
* Limitations (kernel spawn model):
* - argv is joined into a single args string; arguments containing
* spaces are rejected with EINVAL (no quoting in the kernel).
* - envp is ignored (no environment transfer on spawn).
* - file actions must be empty: stdio redirection needs kernel
* support that does not exist yet, so any recorded action makes
* posix_spawn fail with ENOTSUP rather than misbehave silently.
*/
typedef struct {
short flags;
} posix_spawnattr_t;
typedef struct {
int action_count;
} posix_spawn_file_actions_t;
int posix_spawnattr_init(posix_spawnattr_t *attr);
int posix_spawnattr_destroy(posix_spawnattr_t *attr);
int posix_spawnattr_setflags(posix_spawnattr_t *attr, short flags);
int posix_spawnattr_getflags(const posix_spawnattr_t *attr, short *flags);
int posix_spawn_file_actions_init(posix_spawn_file_actions_t *actions);
int posix_spawn_file_actions_destroy(posix_spawn_file_actions_t *actions);
int posix_spawn_file_actions_adddup2(posix_spawn_file_actions_t *actions,
int fd, int newfd);
int posix_spawn_file_actions_addclose(posix_spawn_file_actions_t *actions,
int fd);
int posix_spawn_file_actions_addopen(posix_spawn_file_actions_t *actions,
int fd, const char *path, int oflag,
mode_t mode);
int posix_spawn(pid_t *pid, const char *path,
const posix_spawn_file_actions_t *actions,
const posix_spawnattr_t *attr,
char *const argv[], char *const envp[]);
int posix_spawnp(pid_t *pid, const char *file,
const posix_spawn_file_actions_t *actions,
const posix_spawnattr_t *attr,
char *const argv[], char *const envp[]);
#ifdef __cplusplus
}
#endif
#endif /* _LIBC_SPAWN_H */
+21
View File
@@ -1,6 +1,12 @@
#ifndef _LIBC_STDIO_H
#define _LIBC_STDIO_H
/* Conventional guard marker: packages like GMP sniff the libc's stdio
include-guard name to detect that FILE is available. */
#ifndef _STDIO_H
#define _STDIO_H 1
#endif
#pragma once
#include <stdarg.h>
@@ -36,6 +42,21 @@ extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
int putc(int c, FILE *stream);
void rewind(FILE *stream);
int getchar(void);
typedef long fpos_t;
int fgetpos(FILE *stream, fpos_t *pos);
int fsetpos(FILE *stream, const fpos_t *pos);
void setbuf(FILE *stream, char *buf);
int fscanf(FILE *stream, const char *fmt, ...);
int scanf(const char *fmt, ...);
int vfscanf(FILE *stream, const char *fmt, va_list ap);
int fileno(FILE *stream);
FILE *fdopen(int fd, const char *mode);
int printf(const char *fmt, ...);
int fprintf(FILE *stream, const char *fmt, ...);
int sprintf(char *str, const char *fmt, ...);
+15 -1
View File
@@ -49,10 +49,24 @@ int unsetenv(const char *name);
int putenv(char *string);
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
int (*compar)(const void *, const void *));
void *bsearch(const void *key, const void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
long strtol(const char *nptr, char **endptr, int base);
unsigned long strtoul(const char *nptr, char **endptr, int base);
long long strtoll(const char *nptr, char **endptr, int base);
unsigned long long strtoull(const char *nptr, char **endptr, int base);
long long atoll(const char *nptr);
int mkstemp(char *template_);
char *mktemp(char *template_);
char *realpath(const char *path, char *resolved);
size_t mbstowcs(wchar_t *dst, const char *src, size_t n);
#define MB_CUR_MAX 1
int mblen(const char *s, size_t n);
int mbtowc(wchar_t *pwc, const char *s, size_t n);
int wctomb(char *s, wchar_t wc);
double strtod(const char *nptr, char **endptr);
float strtof(const char *nptr, char **endptr);
long double strtold(const char *nptr, char **endptr);
+3
View File
@@ -31,6 +31,9 @@ char *strpbrk(const char *s, const char *accept);
int strcasecmp(const char *s1, const char *s2);
int strncasecmp(const char *s1, const char *s2, size_t n);
int strcoll(const char *s1, const char *s2);
size_t strxfrm(char *dest, const char *src, size_t n);
char *strtok(char *str, const char *delim);
char *strtok_r(char *str, const char *delim, char **saveptr);
char *strstr(const char *haystack, const char *needle);
const char *strerror(int errnum);
+41
View File
@@ -0,0 +1,41 @@
#ifndef _LIBC_SYS_MMAN_H
#define _LIBC_SYS_MMAN_H
#pragma once
#include <sys/types.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* Anonymous-memory mmap over SYS_ALLOC. SYS_ALLOC returns
* page-aligned process memory, which is exactly what callers like
* GCC's page allocator need. File-backed mappings are not
* supported and fail with ENODEV.
*/
#define PROT_NONE 0
#define PROT_READ 1
#define PROT_WRITE 2
#define PROT_EXEC 4
#define MAP_SHARED 0x01
#define MAP_PRIVATE 0x02
#define MAP_FIXED 0x10
#define MAP_ANONYMOUS 0x20
#define MAP_ANON MAP_ANONYMOUS
#define MAP_FAILED ((void *)-1)
void *mmap(void *addr, size_t length, int prot, int flags, int fd,
long offset);
int munmap(void *addr, size_t length);
int mprotect(void *addr, size_t length, int prot);
#ifdef __cplusplus
}
#endif
#endif /* _LIBC_SYS_MMAN_H */
+23
View File
@@ -0,0 +1,23 @@
#ifndef _LIBC_SYS_PARAM_H
#define _LIBC_SYS_PARAM_H
#pragma once
#include <limits.h>
#include <sys/types.h>
#ifndef MAXPATHLEN
#define MAXPATHLEN 4096
#endif
#ifndef MIN
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
#endif
#ifndef MAX
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
#endif
#define howmany(x, y) (((x) + ((y) - 1)) / (y))
#define roundup(x, y) ((((x) + ((y) - 1)) / (y)) * (y))
#endif /* _LIBC_SYS_PARAM_H */
+44 -8
View File
@@ -12,19 +12,55 @@ extern "C" {
#define S_IFMT 0170000
#define S_IFDIR 0040000
#define S_IFREG 0100000
#define S_IFCHR 0020000
#define S_IFLNK 0120000
#define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR)
#define S_ISREG(mode) (((mode) & S_IFMT) == S_IFREG)
#define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR)
#define S_ISREG(mode) (((mode) & S_IFMT) == S_IFREG)
#define S_ISCHR(mode) (((mode) & S_IFMT) == S_IFCHR)
#define S_ISLNK(mode) (((mode) & S_IFMT) == S_IFLNK)
#define S_ISFIFO(mode) (0)
#define S_ISSOCK(mode) (0)
#define S_ISBLK(mode) (0)
/* stat() reports real permission bits: ext2 stores them natively, the
ramdisk takes them from the USTAR header, and FAT32 synthesizes them from
its read-only attribute. Changing them (chmod) is still not supported. */
#define S_IRWXU 0700
#define S_IRUSR 0400
#define S_IWUSR 0200
#define S_IXUSR 0100
#define S_IRWXG 0070
#define S_IRGRP 0040
#define S_IWGRP 0020
#define S_IXGRP 0010
#define S_IRWXO 0007
#define S_IROTH 0004
#define S_IWOTH 0002
#define S_IXOTH 0001
struct stat {
mode_t st_mode;
unsigned long st_size;
time_t st_mtime;
dev_t st_dev;
ino_t st_ino;
mode_t st_mode;
nlink_t st_nlink;
uid_t st_uid;
gid_t st_gid;
off_t st_size;
blksize_t st_blksize;
blkcnt_t st_blocks;
time_t st_atime;
time_t st_mtime;
time_t st_ctime;
};
int mkdir(const char *path, unsigned int mode);
int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
int mkdir(const char *path, unsigned int mode);
int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);
int chmod(const char *path, mode_t mode);
int fchmod(int fd, mode_t mode);
mode_t umask(mode_t mask);
#ifdef __cplusplus
}
+13
View File
@@ -3,6 +3,12 @@
#pragma once
/* Fixed-width and pointer-sized integer types. Freestanding builds get
this from the kernel freestanding headers; the cross toolchain from
the GCC-provided stdint.h. Hosted code (e.g. libgcov) expects
intptr_t to be visible via the stdio/stdlib include chain. */
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
@@ -15,6 +21,13 @@ typedef unsigned int mode_t;
typedef unsigned int uid_t;
typedef unsigned int gid_t;
typedef long time_t;
typedef unsigned long ino_t;
typedef unsigned long dev_t;
typedef unsigned long nlink_t;
typedef long blkcnt_t;
typedef long blksize_t;
typedef long suseconds_t;
typedef int clockid_t;
#ifdef __cplusplus
}
+31
View File
@@ -0,0 +1,31 @@
#ifndef _LIBC_SYS_WAIT_H
#define _LIBC_SYS_WAIT_H
#pragma once
#include <sys/types.h>
#ifdef __cplusplus
extern "C" {
#endif
/* POSIX status decoding. waitpid() encodes a normal exit as code<<8
and a killed/crashed child as the signal number in the low bits. */
#define WIFEXITED(s) (((s) & 0x7F) == 0)
#define WEXITSTATUS(s) (((s) >> 8) & 0xFF)
#define WIFSIGNALED(s) (((s) & 0x7F) != 0)
#define WTERMSIG(s) ((s) & 0x7F)
#define WIFSTOPPED(s) (0)
#define WSTOPSIG(s) (0)
#define WNOHANG 1
#define WUNTRACED 2
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
#ifdef __cplusplus
}
#endif
#endif /* _LIBC_SYS_WAIT_H */
+3
View File
@@ -34,6 +34,9 @@ struct tm *localtime(const time_t *timer);
time_t mktime(struct tm *tm);
size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);
char *asctime(const struct tm *tm);
char *ctime(const time_t *timep);
#ifdef __cplusplus
}
#endif
+35
View File
@@ -4,11 +4,16 @@
#pragma once
#include <stddef.h>
#include <sys/types.h>
#ifdef __cplusplus
extern "C" {
#endif
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
#define F_OK 0
#define X_OK 1
#define W_OK 2
@@ -22,6 +27,36 @@ int chdir(const char *path);
char *getcwd(char *buf, size_t size);
int access(const char *path, int mode);
int isatty(int fd);
int unlink(const char *path);
int rmdir(const char *path);
int dup(int fd);
int dup2(int oldfd, int newfd);
pid_t getpid(void);
void _exit(int status) __attribute__((noreturn));
pid_t fork(void);
int execv(const char *path, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
int execvp(const char *file, char *const argv[]);
int pipe(int fds[2]);
long pathconf(const char *path, int name);
int getpagesize(void);
long sysconf(int name);
/* sysconf() names (Linux numbering). */
#define _SC_OPEN_MAX 4
#define _SC_PAGESIZE 30
#define _SC_PAGE_SIZE 30
#define _SC_NPROCESSORS_ONLN 84
/* pathconf() names (Linux numbering). */
#define _PC_LINK_MAX 0
#define _PC_MAX_CANON 1
#define _PC_MAX_INPUT 2
#define _PC_NAME_MAX 3
#define _PC_PATH_MAX 4
#define _PC_PIPE_BUF 5
extern char **environ;
unsigned int sleep(unsigned int seconds);
+25
View File
@@ -0,0 +1,25 @@
#ifndef _LIBC_UTIME_H
#define _LIBC_UTIME_H
#pragma once
#include <sys/types.h>
#ifdef __cplusplus
extern "C" {
#endif
struct utimbuf {
time_t actime;
time_t modtime;
};
/* Accepted for POSIX compatibility; the Montauk VFS does not support
setting file times. */
int utime(const char *path, const struct utimbuf *times);
#ifdef __cplusplus
}
#endif
#endif /* _LIBC_UTIME_H */
+29
View File
@@ -0,0 +1,29 @@
#ifndef _LIBC_WCHAR_H
#define _LIBC_WCHAR_H
#pragma once
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef unsigned int wint_t;
/* Opaque shift state for the (byte-oriented) C locale. */
typedef struct {
unsigned long __opaque;
} mbstate_t;
#define WEOF ((wint_t)-1)
/* Byte-oriented C locale only: one byte, one character. */
size_t mbstowcs(wchar_t *dst, const char *src, size_t n);
int mblen(const char *s, size_t n);
#ifdef __cplusplus
}
#endif
#endif /* _LIBC_WCHAR_H */
+20 -5
View File
@@ -123,13 +123,28 @@ namespace heap_detail {
return nullptr;
}
static inline bool grow(uint64_t bytes) {
uint64_t pages = (bytes + 0xFFF) / 0x1000;
if (pages < 4) pages = 4;
// Next slab size for heap growth. The kernel tracks a finite number
// of SYS_ALLOC records per process (MaxHeapAllocs), so growing once
// per large allocation exhausts them under allocation-heavy loads
// (the native ld ran out mid-link). Doubling slabs keep the syscall
// count logarithmic in total heap size.
inline uint64_t g_grow_slab = 16 * 0x1000;
void* mem = montauk::alloc(pages * 0x1000);
static inline bool grow(uint64_t bytes) {
uint64_t want = (bytes + 0xFFF) & ~0xFFFULL;
if (want < 0x4000) want = 0x4000;
uint64_t slab = (want > g_grow_slab) ? want : g_grow_slab;
if (g_grow_slab < 4 * 1024 * 1024) g_grow_slab *= 2;
void* mem = montauk::alloc(slab);
if (mem == nullptr && slab > want) {
// Big slab refused (low memory): retry with the exact need.
slab = want;
mem = montauk::alloc(slab);
}
if (mem == nullptr) return false;
insert_overflow(mem, pages * 0x1000);
insert_overflow(mem, slab);
return true;
}
+135 -3
View File
@@ -165,6 +165,11 @@ namespace montauk {
inline int frename(const char* oldPath, const char* newPath) {
return (int)syscall2(montauk::abi::SYS_FRENAME, (uint64_t)oldPath, (uint64_t)newPath);
}
// Fill *out with metadata (size, timestamps, mode) for the path.
// Returns 0 on success, -1 on error or if the filesystem lacks stat support.
inline int stat(const char* path, montauk::abi::FileStat* out) {
return (int)syscall2(montauk::abi::SYS_STAT, (uint64_t)path, (uint64_t)out);
}
inline int drivelist(int* outDrives, int max) {
return (int)syscall2(montauk::abi::SYS_DRIVELIST, (uint64_t)outDrives, (uint64_t)max);
}
@@ -321,16 +326,24 @@ namespace montauk {
}
// Process management
inline void waitpid(int pid) { syscall1(montauk::abi::SYS_WAITPID, (uint64_t)pid); }
// Blocks until pid exits. Returns 0..255 for a normal exit,
// 256+signal when the process was killed or crashed.
inline int waitpid(int pid) { return (int)syscall1(montauk::abi::SYS_WAITPID, (uint64_t)pid); }
// Framebuffer
inline void fb_info(montauk::abi::FbInfo* info) { syscall1(montauk::abi::SYS_FBINFO, (uint64_t)info); }
inline void* fb_map() { return (void*)syscall0(montauk::abi::SYS_FBMAP); }
// Page flip between two scanout buffers. index -1 queries support;
// index -2 acquires ownership and returns the live front buffer.
// Page flip between the two scanout buffers. fb_map() maps buffer 1
// directly after buffer 0 (at +page_align(height*pitch)). index selects
// the buffer to show; the hardware latches it at vblank (tear-free).
// flags bit0 = block until the flip has been latched (vsync).
// fb_flip(-1, 0) returns 1 when page flipping is available, 0 when not.
// fb_flip(-2, 0) acquires ownership and returns the live buffer (0 or 1).
inline int64_t fb_flip(int64_t index, uint64_t flags) {
return syscall2(montauk::abi::SYS_FBFLIP, (uint64_t)index, flags);
}
inline int display_info(montauk::abi::DisplayInfo* out) {
return (int)syscall1(montauk::abi::SYS_DISPLAYINFO, (uint64_t)out);
}
@@ -562,6 +575,20 @@ namespace montauk {
inline int bt_disconnect(const uint8_t* bdAddr) {
return (int)syscall1(montauk::abi::SYS_BTDISCONNECT, (uint64_t)bdAddr);
}
// Change the adapter's BD_ADDR. bdAddr is a 6-byte buffer with bdAddr[0] as
// the least-significant octet (same order as BtAdapterInfo::bdAddr). Returns
// 0 on success, negative on failure. Persist separately to bluetooth.toml.
inline int bt_set_addr(const uint8_t* bdAddr) {
return (int)syscall1(montauk::abi::SYS_BTSETADDR, (uint64_t)bdAddr);
}
// List bonded (paired) devices. Returns count written to buf (may be 0).
inline int bt_bonds(montauk::abi::BtBondInfo* buf, int maxCount) {
return (int)syscall2(montauk::abi::SYS_BTBONDS, (uint64_t)buf, (uint64_t)maxCount);
}
// Forget a paired device (removes the bond; it must re-pair next time).
inline int bt_forget(const uint8_t* bdAddr) {
return (int)syscall1(montauk::abi::SYS_BTFORGET, (uint64_t)bdAddr);
}
inline int bt_list(montauk::abi::BtDevInfo* buf, int maxCount) {
return (int)syscall2(montauk::abi::SYS_BTLIST, (uint64_t)buf, (uint64_t)maxCount);
}
@@ -569,6 +596,111 @@ namespace montauk {
return (int)syscall1(montauk::abi::SYS_BTINFO, (uint64_t)buf);
}
// Wi-Fi. scan() runs a full channel sweep and blocks until it finishes or
// timeoutMs elapses, then fills buf with the networks seen; it returns the
// number of entries written, or -1 when no adapter is ready.
inline int wifi_scan(montauk::abi::WifiNetwork* buf, int maxCount,
uint32_t timeoutMs) {
return (int)syscall3(montauk::abi::SYS_WIFI_SCAN, (uint64_t)buf,
(uint64_t)maxCount, (uint64_t)timeoutMs);
}
inline int wifi_info(montauk::abi::WifiInfo* out) {
return (int)syscall1(montauk::abi::SYS_WIFI_INFO, (uint64_t)out);
}
// Join a network. Blocks until the link is up or the attempt fails, and
// returns 0 or a WIFI_ERR_* code.
inline int wifi_connect(const char* ssid, const char* password) {
return (int)syscall2(montauk::abi::SYS_WIFI_CONNECT, (uint64_t)ssid,
(uint64_t)password);
}
inline int wifi_disconnect() {
return (int)syscall0(montauk::abi::SYS_WIFI_DISCONNECT);
}
// Non-blocking pair for GUI code, which cannot stall for the seconds a
// sweep or a handshake takes. scan_start() kicks off a sweep (0 started,
// 1 one was already running, -1 no adapter); wifi_info().scanning drops
// back to 0 and .scanGeneration moves on when it finishes, and
// wifi_results() copies out the table without touching the radio.
inline int wifi_scan_start(uint32_t timeoutMs) {
return (int)syscall1(montauk::abi::SYS_WIFI_SCAN_START, (uint64_t)timeoutMs);
}
inline int wifi_results(montauk::abi::WifiNetwork* buf, int maxCount) {
return (int)syscall2(montauk::abi::SYS_WIFI_RESULTS, (uint64_t)buf,
(uint64_t)maxCount);
}
// Returns 0 once the join is under way, or a WIFI_ERR_* it failed on
// before any frame went out. Watch wifi_info().joining for progress and
// .lastError for the outcome.
inline int wifi_connect_async(const char* ssid, const char* password) {
return (int)syscall2(montauk::abi::SYS_WIFI_CONNECT_ASYNC, (uint64_t)ssid,
(uint64_t)password);
}
// List the registered link-layer interfaces. The IP configuration is
// global to the stack; it belongs to whichever entry has active = 1.
inline int net_interfaces(montauk::abi::NetIfInfo* buf, int maxCount) {
return (int)syscall2(montauk::abi::SYS_NETIFS, (uint64_t)buf,
(uint64_t)maxCount);
}
// Software-defined radio (Rx). Receivers are identified by index [0, count);
// open() returns a handle used by the rest of the calls. Samples are read
// as interleaved 8-bit unsigned I/Q (CU8) from the device's ring buffer.
inline int sdr_count() {
return (int)syscall0(montauk::abi::SYS_SDR_COUNT);
}
inline int sdr_info(int index, montauk::abi::SdrDeviceInfo* out) {
return (int)syscall2(montauk::abi::SYS_SDR_INFO, (uint64_t)index, (uint64_t)out);
}
inline int sdr_open(int index) {
return (int)syscall1(montauk::abi::SYS_SDR_OPEN, (uint64_t)index);
}
inline int sdr_close(int handle) {
return (int)syscall1(montauk::abi::SYS_SDR_CLOSE, (uint64_t)handle);
}
inline int sdr_start(int handle) {
return (int)syscall1(montauk::abi::SYS_SDR_START, (uint64_t)handle);
}
inline int sdr_stop(int handle) {
return (int)syscall1(montauk::abi::SYS_SDR_STOP, (uint64_t)handle);
}
// Non-blocking: copies up to len bytes of queued I/Q, returns bytes copied.
inline int sdr_read(int handle, void* buf, uint32_t len) {
return (int)syscall3(montauk::abi::SYS_SDR_READ, (uint64_t)handle, (uint64_t)buf, (uint64_t)len);
}
inline int sdr_set_param(int handle, int param, uint64_t value) {
return (int)syscall3(montauk::abi::SYS_SDR_SETPARAM, (uint64_t)handle, (uint64_t)param, value);
}
inline int64_t sdr_get_param(int handle, int param) {
return syscall2(montauk::abi::SYS_SDR_GETPARAM, (uint64_t)handle, (uint64_t)param);
}
// Convenience wrappers over sdr_set_param / sdr_get_param.
inline int sdr_set_freq(int handle, uint64_t hz) {
return sdr_set_param(handle, montauk::abi::SDR_PARAM_FREQ, hz);
}
inline uint64_t sdr_get_freq(int handle) {
return (uint64_t)sdr_get_param(handle, montauk::abi::SDR_PARAM_FREQ);
}
inline int sdr_set_sample_rate(int handle, uint32_t hz) {
return sdr_set_param(handle, montauk::abi::SDR_PARAM_SAMPLE_RATE, hz);
}
inline uint32_t sdr_get_sample_rate(int handle) {
return (uint32_t)sdr_get_param(handle, montauk::abi::SDR_PARAM_SAMPLE_RATE);
}
inline int sdr_set_gain_mode(int handle, int manual) {
return sdr_set_param(handle, montauk::abi::SDR_PARAM_GAIN_MODE, (uint64_t)manual);
}
inline int sdr_set_gain(int handle, int tenthsDb) {
return sdr_set_param(handle, montauk::abi::SDR_PARAM_GAIN, (uint64_t)(int64_t)tenthsDb);
}
inline int sdr_set_freq_correction(int handle, int ppm) {
return sdr_set_param(handle, montauk::abi::SDR_PARAM_FREQ_CORR, (uint64_t)(int64_t)ppm);
}
inline int sdr_set_agc(int handle, int on) {
return sdr_set_param(handle, montauk::abi::SDR_PARAM_AGC, (uint64_t)on);
}
// Kernel introspection
inline void memstats(montauk::abi::MemStats* out) { syscall1(montauk::abi::SYS_MEMSTATS, (uint64_t)out); }
+255
View File
@@ -0,0 +1,255 @@
/*
* wifi.h
* Saved Wi-Fi networks (0:/config/wifi.toml) and small formatting helpers
* shared by the desktop panel, the Network app and the wifi command.
*
* The file looks like this:
*
* [wifi]
* autoconnect = true
*
* [network.0]
* ssid = "Home"
* psk = "passphrase"
*
* 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). Anyone who can read 0:/config can read the keys.
*
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <montauk/config.h>
#include <montauk/string.h>
#include <montauk/syscall.h>
namespace montauk {
namespace wifi {
// The scan table the kernel keeps is 64 entries; saving that many networks
// is already far more than a laptop accumulates.
static constexpr int MAX_SAVED = 32;
static constexpr int SSID_CAP = 36;
static constexpr int PSK_CAP = 72; // 64-character hex PSK plus NUL
struct SavedNetwork {
char ssid[SSID_CAP];
char psk[PSK_CAP];
};
struct SavedList {
SavedNetwork items[MAX_SAVED];
int count;
bool autoconnect;
};
// ---- helpers -----------------------------------------------------------
inline void copy_str(char* dst, int cap, const char* src) {
int i = 0;
for (; src && src[i] && i < cap - 1; i++) dst[i] = src[i];
dst[i] = '\0';
}
// "network.<index>.<field>"
inline void network_key(char* out, int cap, int index, const char* field) {
char idx[8];
int n = 0;
if (index == 0) {
idx[n++] = '0';
} else {
char tmp[8];
int t = 0;
for (int v = index; v > 0 && t < (int)sizeof(tmp); v /= 10)
tmp[t++] = (char)('0' + (v % 10));
while (t > 0) idx[n++] = tmp[--t];
}
idx[n] = '\0';
int p = 0;
const char* prefix = "network.";
while (*prefix && p < cap - 1) out[p++] = *prefix++;
for (int i = 0; i < n && p < cap - 1; i++) out[p++] = idx[i];
if (p < cap - 1) out[p++] = '.';
while (field && *field && p < cap - 1) out[p++] = *field++;
out[p] = '\0';
}
// ---- load / store ------------------------------------------------------
inline void saved_load(SavedList* out) {
if (!out) return;
out->count = 0;
out->autoconnect = true;
auto doc = montauk::config::load("wifi");
out->autoconnect = doc.get_bool("wifi.autoconnect", true);
for (int i = 0; i < MAX_SAVED; i++) {
char key[64];
network_key(key, sizeof(key), i, "ssid");
const char* ssid = doc.get_string(key, nullptr);
if (!ssid || !ssid[0]) break; // entries are written contiguously
network_key(key, sizeof(key), i, "psk");
const char* psk = doc.get_string(key, "");
SavedNetwork& n = out->items[out->count++];
copy_str(n.ssid, SSID_CAP, ssid);
copy_str(n.psk, PSK_CAP, psk);
}
doc.destroy();
}
// Rewrite the file from the list. Returns 0 on success.
inline int saved_store(const SavedList* list) {
if (!list) return -1;
montauk::toml::Doc doc;
doc.init();
montauk::config::set_bool(&doc, "wifi.autoconnect", list->autoconnect);
for (int i = 0; i < list->count && i < MAX_SAVED; i++) {
char key[64];
network_key(key, sizeof(key), i, "ssid");
montauk::config::set_string(&doc, key, list->items[i].ssid);
network_key(key, sizeof(key), i, "psk");
montauk::config::set_string(&doc, key, list->items[i].psk);
}
int rc = montauk::config::save("wifi", &doc);
doc.destroy();
return rc;
}
inline int saved_index_of(const SavedList* list, const char* ssid) {
if (!list || !ssid) return -1;
for (int i = 0; i < list->count; i++) {
if (montauk::streq(list->items[i].ssid, ssid)) return i;
}
return -1;
}
inline const SavedNetwork* saved_find(const SavedList* list, const char* ssid) {
int idx = saved_index_of(list, ssid);
return idx < 0 ? nullptr : &list->items[idx];
}
// Add or update an entry in memory. Returns false when the list is full.
inline bool saved_set(SavedList* list, const char* ssid, const char* psk) {
if (!list || !ssid || !ssid[0]) return false;
int idx = saved_index_of(list, ssid);
if (idx < 0) {
if (list->count >= MAX_SAVED) return false;
idx = list->count++;
copy_str(list->items[idx].ssid, SSID_CAP, ssid);
}
copy_str(list->items[idx].psk, PSK_CAP, psk ? psk : "");
return true;
}
inline bool saved_remove(SavedList* list, const char* ssid) {
int idx = saved_index_of(list, ssid);
if (idx < 0) return false;
for (int i = idx; i < list->count - 1; i++) list->items[i] = list->items[i + 1];
list->count--;
return true;
}
// Convenience wrappers that touch the file directly.
inline bool remember(const char* ssid, const char* psk) {
SavedList list;
saved_load(&list);
if (!saved_set(&list, ssid, psk)) return false;
return saved_store(&list) == 0;
}
inline bool forget(const char* ssid) {
SavedList list;
saved_load(&list);
if (!saved_remove(&list, ssid)) return false;
return saved_store(&list) == 0;
}
// Copy the saved passphrase for `ssid` into out. False when not saved.
inline bool lookup(const char* ssid, char* out, int cap) {
SavedList list;
saved_load(&list);
const SavedNetwork* n = saved_find(&list, ssid);
if (!n) return false;
copy_str(out, cap, n->psk);
return true;
}
// ---- presentation ------------------------------------------------------
inline const char* security_name(uint8_t security) {
switch (security) {
case montauk::abi::WIFI_SEC_OPEN: return "Open";
case montauk::abi::WIFI_SEC_WEP: return "WEP";
case montauk::abi::WIFI_SEC_WPA: return "WPA";
case montauk::abi::WIFI_SEC_WPA2: return "WPA2";
case montauk::abi::WIFI_SEC_WPA3: return "WPA3";
default: return "Unknown";
}
}
inline bool needs_key(uint8_t security) {
return security != montauk::abi::WIFI_SEC_OPEN;
}
// 0-4 bars from an RSSI in dBm.
inline int signal_bars(int8_t rssi) {
if (rssi >= -55) return 4;
if (rssi >= -67) return 3;
if (rssi >= -75) return 2;
if (rssi >= -85) return 1;
return 0;
}
inline const char* state_name(uint8_t state) {
switch (state) {
case montauk::abi::WIFI_STATE_ABSENT: return "No adapter";
case montauk::abi::WIFI_STATE_DETECTED: return "Loading firmware";
case montauk::abi::WIFI_STATE_BOOTING: return "Starting";
case montauk::abi::WIFI_STATE_RUNNING: return "Ready";
case montauk::abi::WIFI_STATE_ERROR: return "Adapter error";
case montauk::abi::WIFI_STATE_RFKILL: return "Radio off";
default: return "Unknown";
}
}
// What the join is doing right now, for a progress line.
inline const char* conn_state_name(uint32_t connState) {
switch (connState) {
case montauk::abi::WIFI_CONN_IDLE: return "Not connected";
case montauk::abi::WIFI_CONN_CONTEXTS_UP: return "Preparing radio...";
case montauk::abi::WIFI_CONN_AUTHENTICATING: return "Authenticating...";
case montauk::abi::WIFI_CONN_AUTHENTICATED: return "Authenticated";
case montauk::abi::WIFI_CONN_ASSOCIATING: return "Associating...";
case montauk::abi::WIFI_CONN_ASSOCIATED: return "Associated";
case montauk::abi::WIFI_CONN_HANDSHAKING: return "Exchanging keys...";
case montauk::abi::WIFI_CONN_CONNECTED: return "Connected";
case montauk::abi::WIFI_CONN_FAILED: return "Connection failed";
default: return "";
}
}
inline const char* error_message(int err) {
switch (err) {
case 0: return "";
case montauk::abi::WIFI_ERR_NO_ADAPTER: return "No Wi-Fi adapter is ready";
case montauk::abi::WIFI_ERR_NOT_FOUND: return "That network is out of range";
case montauk::abi::WIFI_ERR_NEED_KEY: return "This network needs a password";
case montauk::abi::WIFI_ERR_UNSUPPORTED: return "This security type is not supported";
case montauk::abi::WIFI_ERR_AUTH: return "Wrong password";
case montauk::abi::WIFI_ERR_TIMEOUT: return "The network did not respond";
default: return "Could not join the network";
}
}
} // namespace wifi
} // namespace montauk
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+58
View File
@@ -633,6 +633,64 @@ check(link_rm is not None and mac_rm is not None and link_rm < mac_rm,
"the link is removed before the MAC that owns it")
d7.p.stdin.close()
# =============================================================================
# An access point that stops acknowledging brings the link down.
#
# IwxLinkUp() used to be nothing but "the state machine reached Connected", and
# only an explicit deauthentication frame moved it off that state. A hotspot
# that simply went away -- slept, changed channel, dropped the station without
# saying so -- therefore left the link reported as up forever: NetIf kept
# choosing wlan0, every packet vanished, and the desktop showed a healthy
# connection while nothing worked. Beacons cannot be used to notice this
# (MacConfigCmd stops asking for them once associated), so the driver watches
# its own frames going unacknowledged instead.
# =============================================================================
print("\n=== a silent access point takes the link down ===")
d8 = Driver()
d8.cmd("MAC " + STA.hex())
d8.cmd(f"CONNECT {AP.hex()} {CHANNEL} 0 OpenNet - -")
d8.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('<HHH', 0, 2, 0)).hex())
d8.cmd("SERVICE")
d8.cmd("RXMGMT " + mgmt(0x10, STA, AP, AP,
struct.pack('<HHH', 0x0421, 0, 7 | 0xc000)).hex())
ev = d8.cmd("SERVICE")
check(link_of(ev['result']) == 1, "the open network is associated and the link is up")
# Well short of the threshold: a few unacknowledged frames are ordinary.
ev = d8.cmd("TXSTATUS 0 15")
check(link_of(ev['result']) == 1,
"a handful of unacknowledged frames does not drop the link")
ev = d8.cmd("SERVICE")
check(link_of(ev['result']) == 1, "and the service pass leaves it alone")
# One acknowledgement means the access point is still there; the count restarts.
d8.cmd("TXSTATUS 1 1")
ev = d8.cmd("TXSTATUS 0 15")
check(link_of(ev['result']) == 1,
"an acknowledgement in between restarts the count")
# Anything received from the BSS is equally good proof, and also restarts it.
d8.cmd("RXDATA " + data_from_ds(STA, AP, AP, 0x0800, b'\x45' * 20).hex())
ev = d8.cmd("TXSTATUS 0 15")
check(link_of(ev['result']) == 1,
"a frame received from the BSS restarts the count too")
# Now let it run past the threshold with nothing coming back.
ev = d8.cmd("TXSTATUS 0 16")
check(link_of(ev['result']) == 1,
"the transmit path itself does not tear anything down")
check(not ev['CMD'],
"no firmware command is sent from the completion path")
ev = d8.cmd("SERVICE")
check(state_of(ev['result']) == 0 and link_of(ev['result']) == 0,
"the next service pass drops the link")
check(any(c == MAC_CONFIG and struct.unpack_from('<I', p, 4)[0] == 3
for c, p in ev['CMD']),
"and unwinds the firmware contexts")
d8.p.stdin.close()
print()
if fails:
for f in fails: print("FAILURE:", f)
+9
View File
@@ -196,6 +196,15 @@ int main() {
int n = unhex(line.substr(6), buf);
bool ok = IwxConnectSendEthernet(buf, (uint32_t)n);
printf("TXETH-OK %d\n", ok ? 1 : 0);
} else if (line.rfind("TXSTATUS ", 0) == 0) {
// TXSTATUS <acked> <count> -- report N transmit outcomes, the way
// IwxTxComplete does off the firmware's TX response. Drives the
// link supervision that notices an access point which stopped
// acknowledging without ever deauthenticating.
unsigned acked, count;
sscanf(line.c_str(), "TXSTATUS %u %u", &acked, &count);
for (unsigned i = 0; i < count; i++) IwxConnectNoteTx(acked != 0);
printf("DONE STATE %d LINK %d\n", IwxConnectState(), IwxLinkUp() ? 1 : 0);
} else if (line.rfind("SERVICE", 0) == 0) {
IwxConnectService();
printf("DONE STATE %d LINK %d\n", IwxConnectState(), IwxLinkUp() ? 1 : 0);