diff --git a/docs/wifi.md b/docs/wifi.md index 601cffc..f897e33 100644 --- a/docs/wifi.md +++ b/docs/wifi.md @@ -12,8 +12,12 @@ wifi scan list nearby networks wifi connect join one dhcp pick up an address wifi status what you are connected to +wifi saved / wifi forget 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 diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 47eb71e..99f8f8a 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 69 +#define MONTAUK_BUILD_NUMBER 84 diff --git a/kernel/src/Api/Net.hpp b/kernel/src/Api/Net.hpp index b13a4dc..9924175 100644 --- a/kernel/src/Api/Net.hpp +++ b/kernel/src/Api/Net.hpp @@ -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); diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index 05798b4..fc00a28 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -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: diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index f9055ad..885eddc 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -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 { diff --git a/kernel/src/Api/WifiSyscall.hpp b/kernel/src/Api/WifiSyscall.hpp index c69389d..89d42e8 100644 --- a/kernel/src/Api/WifiSyscall.hpp +++ b/kernel/src/Api/WifiSyscall.hpp @@ -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(); } diff --git a/kernel/src/Drivers/Net/Wifi/Iwx.hpp b/kernel/src/Drivers/Net/Wifi/Iwx.hpp index 6f679b4..4a327a9 100644 --- a/kernel/src/Drivers/Net/Wifi/Iwx.hpp +++ b/kernel/src/Drivers/Net/Wifi/Iwx.hpp @@ -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. diff --git a/kernel/src/Drivers/Net/Wifi/IwxConnect.cpp b/kernel/src/Drivers/Net/Wifi/IwxConnect.cpp index 14dacaf..fa7779e 100644 --- a/kernel/src/Drivers/Net/Wifi/IwxConnect.cpp +++ b/kernel/src/Drivers/Net/Wifi/IwxConnect.cpp @@ -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) { diff --git a/kernel/src/Drivers/Net/Wifi/IwxTrans.cpp b/kernel/src/Drivers/Net/Wifi/IwxTrans.cpp index 0d17965..c9b18b5 100644 --- a/kernel/src/Drivers/Net/Wifi/IwxTrans.cpp +++ b/kernel/src/Drivers/Net/Wifi/IwxTrans.cpp @@ -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); } // ========================================================================= diff --git a/kernel/src/Drivers/Net/Wifi/Wifi.cpp b/kernel/src/Drivers/Net/Wifi/Wifi.cpp index e5f2e24..1e2861f 100644 --- a/kernel/src/Drivers/Net/Wifi/Wifi.cpp +++ b/kernel/src/Drivers/Net/Wifi/Wifi.cpp @@ -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 // ========================================================================= diff --git a/kernel/src/Drivers/Net/Wifi/Wifi.hpp b/kernel/src/Drivers/Net/Wifi/Wifi.hpp index 4d70a02..96628e0 100644 --- a/kernel/src/Drivers/Net/Wifi/Wifi.hpp +++ b/kernel/src/Drivers/Net/Wifi/Wifi.hpp @@ -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(); // ------------------------------------------------------------------------- diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index 187d49d..9fc1f28 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -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 { diff --git a/programs/include/gui/desktop.hpp b/programs/include/gui/desktop.hpp index f4ba59a..f530362 100644 --- a/programs/include/gui/desktop.hpp +++ b/programs/include/gui/desktop.hpp @@ -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 diff --git a/programs/include/libc/montauk.h b/programs/include/libc/montauk.h index dafaec9..a58800d 100644 --- a/programs/include/libc/montauk.h +++ b/programs/include/libc/montauk.h @@ -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 diff --git a/programs/include/montauk/syscall.h b/programs/include/montauk/syscall.h index 9cce5c1..93efccc 100644 --- a/programs/include/montauk/syscall.h +++ b/programs/include/montauk/syscall.h @@ -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. diff --git a/programs/include/montauk/wifi.h b/programs/include/montauk/wifi.h new file mode 100644 index 0000000..69adc5a --- /dev/null +++ b/programs/include/montauk/wifi.h @@ -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 +#include +#include + +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.." + 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 diff --git a/programs/man/wifi.1 b/programs/man/wifi.1 index ea7bcfc..b27b97f 100644 --- a/programs/man/wifi.1 +++ b/programs/man/wifi.1 @@ -7,7 +7,10 @@ wifi info wifi debug wifi connect [passphrase] + wifi status wifi disconnect + wifi saved + wifi forget .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 [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 + 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.] 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. diff --git a/programs/src/desktop/Makefile b/programs/src/desktop/Makefile index 3973388..ad5cf88 100644 --- a/programs/src/desktop/Makefile +++ b/programs/src/desktop/Makefile @@ -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)) diff --git a/programs/src/desktop/compose.cpp b/programs/src/desktop/compose.cpp index e718079..1af49be 100644 --- a/programs/src/desktop/compose.cpp +++ b/programs/src/desktop/compose.cpp @@ -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); diff --git a/programs/src/desktop/desktop_builtin.cpp b/programs/src/desktop/desktop_builtin.cpp index 8e1547d..e34ec60 100644 --- a/programs/src/desktop/desktop_builtin.cpp +++ b/programs/src/desktop/desktop_builtin.cpp @@ -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 diff --git a/programs/src/desktop/desktop_internal.hpp b/programs/src/desktop/desktop_internal.hpp index f231915..c65c2cd 100644 --- a/programs/src/desktop/desktop_internal.hpp +++ b/programs/src/desktop/desktop_internal.hpp @@ -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); diff --git a/programs/src/desktop/input.cpp b/programs/src/desktop/input.cpp index 4eaff2e..1dbb6b0 100644 --- a/programs/src/desktop/input.cpp +++ b/programs/src/desktop/input.cpp @@ -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; } } diff --git a/programs/src/desktop/launcher.cpp b/programs/src/desktop/launcher.cpp index 81580a8..b59a4c9 100644 --- a/programs/src/desktop/launcher.cpp +++ b/programs/src/desktop/launcher.cpp @@ -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); diff --git a/programs/src/desktop/main.cpp b/programs/src/desktop/main.cpp index 00f2760..1653aaf 100644 --- a/programs/src/desktop/main.cpp +++ b/programs/src/desktop/main.cpp @@ -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 diff --git a/programs/src/desktop/panel.cpp b/programs/src/desktop/panel.cpp index 715f714..1c80221 100644 --- a/programs/src/desktop/panel.cpp +++ b/programs/src/desktop/panel.cpp @@ -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); diff --git a/programs/src/desktop/wifi.cpp b/programs/src/desktop/wifi.cpp new file mode 100644 index 0000000..43fa35f --- /dev/null +++ b/programs/src/desktop/wifi.cpp @@ -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 +#include + +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; +} diff --git a/programs/src/network/main.cpp b/programs/src/network/main.cpp index 591b9d7..868ab27 100644 --- a/programs/src/network/main.cpp +++ b/programs/src/network/main.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -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; diff --git a/programs/src/wifi/main.cpp b/programs/src/wifi/main.cpp index aad1b49..0579977 100644 --- a/programs/src/wifi/main.cpp +++ b/programs/src/wifi/main.cpp @@ -17,6 +17,7 @@ #include #include +#include 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 [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 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"); diff --git a/scripts/copy_icons.sh b/scripts/copy_icons.sh index 2876a8a..29e061a 100755 --- a/scripts/copy_icons.sh +++ b/scripts/copy_icons.sh @@ -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" diff --git a/template/sysroot/include/Api/Syscall.hpp b/template/sysroot/include/Api/Syscall.hpp index aa7edb2..9fc1f28 100644 --- a/template/sysroot/include/Api/Syscall.hpp +++ b/template/sysroot/include/Api/Syscall.hpp @@ -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; diff --git a/template/sysroot/include/gui/canvas.hpp b/template/sysroot/include/gui/canvas.hpp index 873eb75..c38c260 100644 --- a/template/sysroot/include/gui/canvas.hpp +++ b/template/sysroot/include/gui/canvas.hpp @@ -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 ---- diff --git a/template/sysroot/include/gui/desktop.hpp b/template/sysroot/include/gui/desktop.hpp index c112a29..4cc7907 100644 --- a/template/sysroot/include/gui/desktop.hpp +++ b/template/sysroot/include/gui/desktop.hpp @@ -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; diff --git a/template/sysroot/include/gui/font.hpp b/template/sysroot/include/gui/font.hpp index eca26b7..4001c16 100644 --- a/template/sysroot/include/gui/font.hpp +++ b/template/sysroot/include/gui/font.hpp @@ -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() { diff --git a/template/sysroot/include/gui/mtk/widgets.hpp b/template/sysroot/include/gui/mtk/widgets.hpp index e4171aa..b3effcc 100644 --- a/template/sysroot/include/gui/mtk/widgets.hpp +++ b/template/sysroot/include/gui/mtk/widgets.hpp @@ -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}; } diff --git a/template/sysroot/include/gui/terminal.hpp b/template/sysroot/include/gui/terminal.hpp index bd935fe..70f41aa 100644 --- a/template/sysroot/include/gui/terminal.hpp +++ b/template/sysroot/include/gui/terminal.hpp @@ -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; diff --git a/template/sysroot/include/gui/truetype.hpp b/template/sysroot/include/gui/truetype.hpp index 90ac56c..edf3593 100644 --- a/template/sysroot/include/gui/truetype.hpp +++ b/template/sysroot/include/gui/truetype.hpp @@ -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) diff --git a/template/sysroot/include/libc/ctype.h b/template/sysroot/include/libc/ctype.h index efca6ed..46606a2 100644 --- a/template/sysroot/include/libc/ctype.h +++ b/template/sysroot/include/libc/ctype.h @@ -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); diff --git a/template/sysroot/include/libc/dirent.h b/template/sysroot/include/libc/dirent.h index 397d0ca..5e90839 100644 --- a/template/sysroot/include/libc/dirent.h +++ b/template/sysroot/include/libc/dirent.h @@ -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; diff --git a/template/sysroot/include/libc/errno.h b/template/sysroot/include/libc/errno.h index cdbda4d..40fd8b9 100644 --- a/template/sysroot/include/libc/errno.h +++ b/template/sysroot/include/libc/errno.h @@ -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 } diff --git a/template/sysroot/include/libc/fcntl.h b/template/sysroot/include/libc/fcntl.h index 46890eb..dc2206e 100644 --- a/template/sysroot/include/libc/fcntl.h +++ b/template/sysroot/include/libc/fcntl.h @@ -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 } diff --git a/template/sysroot/include/libc/inttypes.h b/template/sysroot/include/libc/inttypes.h index 398a5fc..e2a01b5 100644 --- a/template/sysroot/include/libc/inttypes.h +++ b/template/sysroot/include/libc/inttypes.h @@ -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 */ diff --git a/template/sysroot/include/libc/math.h b/template/sysroot/include/libc/math.h index efbce2f..6f2b779 100644 --- a/template/sysroot/include/libc/math.h +++ b/template/sysroot/include/libc/math.h @@ -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); diff --git a/template/sysroot/include/libc/memory.h b/template/sysroot/include/libc/memory.h new file mode 100644 index 0000000..b99e2ec --- /dev/null +++ b/template/sysroot/include/libc/memory.h @@ -0,0 +1,9 @@ +#ifndef _LIBC_MEMORY_H +#define _LIBC_MEMORY_H + +#pragma once + +/* Traditional alias for string.h. */ +#include + +#endif /* _LIBC_MEMORY_H */ diff --git a/template/sysroot/include/libc/montauk.h b/template/sysroot/include/libc/montauk.h index 5a1e25e..a58800d 100644 --- a/template/sysroot/include/libc/montauk.h +++ b/template/sysroot/include/libc/montauk.h @@ -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); } diff --git a/template/sysroot/include/libc/signal.h b/template/sysroot/include/libc/signal.h index 945a391..cbfcecc 100644 --- a/template/sysroot/include/libc/signal.h +++ b/template/sysroot/include/libc/signal.h @@ -3,6 +3,8 @@ #pragma once +#include + #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 } diff --git a/template/sysroot/include/libc/spawn.h b/template/sysroot/include/libc/spawn.h new file mode 100644 index 0000000..bfe17bc --- /dev/null +++ b/template/sysroot/include/libc/spawn.h @@ -0,0 +1,60 @@ +#ifndef _LIBC_SPAWN_H +#define _LIBC_SPAWN_H + +#pragma once + +#include + +#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 */ diff --git a/template/sysroot/include/libc/stdio.h b/template/sysroot/include/libc/stdio.h index b88194d..a3c9dfd 100644 --- a/template/sysroot/include/libc/stdio.h +++ b/template/sysroot/include/libc/stdio.h @@ -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 @@ -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, ...); diff --git a/template/sysroot/include/libc/stdlib.h b/template/sysroot/include/libc/stdlib.h index 37d1c83..3c8f5d1 100644 --- a/template/sysroot/include/libc/stdlib.h +++ b/template/sysroot/include/libc/stdlib.h @@ -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); diff --git a/template/sysroot/include/libc/string.h b/template/sysroot/include/libc/string.h index d9ff799..ba815d6 100644 --- a/template/sysroot/include/libc/string.h +++ b/template/sysroot/include/libc/string.h @@ -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); diff --git a/template/sysroot/include/libc/sys/mman.h b/template/sysroot/include/libc/sys/mman.h new file mode 100644 index 0000000..b5aa231 --- /dev/null +++ b/template/sysroot/include/libc/sys/mman.h @@ -0,0 +1,41 @@ +#ifndef _LIBC_SYS_MMAN_H +#define _LIBC_SYS_MMAN_H + +#pragma once + +#include + +#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 */ diff --git a/template/sysroot/include/libc/sys/param.h b/template/sysroot/include/libc/sys/param.h new file mode 100644 index 0000000..b72791b --- /dev/null +++ b/template/sysroot/include/libc/sys/param.h @@ -0,0 +1,23 @@ +#ifndef _LIBC_SYS_PARAM_H +#define _LIBC_SYS_PARAM_H + +#pragma once + +#include +#include + +#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 */ diff --git a/template/sysroot/include/libc/sys/stat.h b/template/sysroot/include/libc/sys/stat.h index 60c38f7..7dc995d 100644 --- a/template/sysroot/include/libc/sys/stat.h +++ b/template/sysroot/include/libc/sys/stat.h @@ -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 } diff --git a/template/sysroot/include/libc/sys/types.h b/template/sysroot/include/libc/sys/types.h index 350e0fb..5cb4582 100644 --- a/template/sysroot/include/libc/sys/types.h +++ b/template/sysroot/include/libc/sys/types.h @@ -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 + #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 } diff --git a/template/sysroot/include/libc/sys/wait.h b/template/sysroot/include/libc/sys/wait.h new file mode 100644 index 0000000..8d31a16 --- /dev/null +++ b/template/sysroot/include/libc/sys/wait.h @@ -0,0 +1,31 @@ +#ifndef _LIBC_SYS_WAIT_H +#define _LIBC_SYS_WAIT_H + +#pragma once + +#include + +#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 */ diff --git a/template/sysroot/include/libc/time.h b/template/sysroot/include/libc/time.h index 77c5cca..d88a2cc 100644 --- a/template/sysroot/include/libc/time.h +++ b/template/sysroot/include/libc/time.h @@ -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 diff --git a/template/sysroot/include/libc/unistd.h b/template/sysroot/include/libc/unistd.h index fe665a1..2217bbd 100644 --- a/template/sysroot/include/libc/unistd.h +++ b/template/sysroot/include/libc/unistd.h @@ -4,11 +4,16 @@ #pragma once #include +#include #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); diff --git a/template/sysroot/include/libc/utime.h b/template/sysroot/include/libc/utime.h new file mode 100644 index 0000000..00327b7 --- /dev/null +++ b/template/sysroot/include/libc/utime.h @@ -0,0 +1,25 @@ +#ifndef _LIBC_UTIME_H +#define _LIBC_UTIME_H + +#pragma once + +#include + +#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 */ diff --git a/template/sysroot/include/libc/wchar.h b/template/sysroot/include/libc/wchar.h new file mode 100644 index 0000000..cd90739 --- /dev/null +++ b/template/sysroot/include/libc/wchar.h @@ -0,0 +1,29 @@ +#ifndef _LIBC_WCHAR_H +#define _LIBC_WCHAR_H + +#pragma once + +#include + +#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 */ diff --git a/template/sysroot/include/montauk/heap.h b/template/sysroot/include/montauk/heap.h index 8d306d7..5ed05ce 100644 --- a/template/sysroot/include/montauk/heap.h +++ b/template/sysroot/include/montauk/heap.h @@ -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; } diff --git a/template/sysroot/include/montauk/syscall.h b/template/sysroot/include/montauk/syscall.h index 2663f53..93efccc 100644 --- a/template/sysroot/include/montauk/syscall.h +++ b/template/sysroot/include/montauk/syscall.h @@ -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); } diff --git a/template/sysroot/include/montauk/wifi.h b/template/sysroot/include/montauk/wifi.h new file mode 100644 index 0000000..f909857 --- /dev/null +++ b/template/sysroot/include/montauk/wifi.h @@ -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 +#include +#include + +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.." + 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 diff --git a/template/sysroot/lib/crt1.o b/template/sysroot/lib/crt1.o index 5186a5a..ef5b27c 100644 Binary files a/template/sysroot/lib/crt1.o and b/template/sysroot/lib/crt1.o differ diff --git a/template/sysroot/lib/libjpeg.a b/template/sysroot/lib/libjpeg.a index f1b0586..1b4f728 100644 Binary files a/template/sysroot/lib/libjpeg.a and b/template/sysroot/lib/libjpeg.a differ diff --git a/template/sysroot/lib/liblibc.a b/template/sysroot/lib/liblibc.a index 39e9168..a0e88d4 100644 Binary files a/template/sysroot/lib/liblibc.a and b/template/sysroot/lib/liblibc.a differ diff --git a/template/sysroot/lib/libtls.a b/template/sysroot/lib/libtls.a index 3143169..b4e9ae8 100644 Binary files a/template/sysroot/lib/libtls.a and b/template/sysroot/lib/libtls.a differ diff --git a/tests/wifi/ap_mlme.py b/tests/wifi/ap_mlme.py index 9c11df0..e538ef4 100644 --- a/tests/wifi/ap_mlme.py +++ b/tests/wifi/ap_mlme.py @@ -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(' -- 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);