feat: wi-fi - expand support and fix issues, add GUI components
This commit is contained in:
+161
@@ -12,8 +12,12 @@ wifi scan list nearby networks
|
||||
wifi connect <ssid> <passphrase> join one
|
||||
dhcp pick up an address
|
||||
wifi status what you are connected to
|
||||
wifi saved / wifi forget <ssid> networks remembered for next time
|
||||
```
|
||||
|
||||
There is a graphical path too: a Wi-Fi entry in the desktop panel and a
|
||||
Wi-Fi tab in the Network app. See "The desktop side" below.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
@@ -147,6 +151,163 @@ removes the MAC and drops the PHY context - the order
|
||||
`iwl_mvm_mld_vif_cfg_changed_station` and the paths below it use on the way
|
||||
down. `tests/wifi/ap_mlme.py` pins that order.
|
||||
|
||||
### A bad access point must not become a bad computer
|
||||
|
||||
Three separate defects turned "the Wi-Fi connection went wrong" into "the whole
|
||||
machine went wrong". They are worth keeping straight because they have nothing
|
||||
to do with each other beyond sharing a trigger.
|
||||
|
||||
**The command wait is a busy spin on a core the scheduler has been told not to
|
||||
touch.** `ServiceEvents()` runs from `ApicTimer::ServiceDeferredWork()`, which
|
||||
sets `cpu->reservedForKernelWork` so a bottom half holding a process-context
|
||||
mutex cannot be preempted into the process that would wait on it. The
|
||||
scheduler honours that by refusing to place any process on that core and by
|
||||
skipping it for the reschedule IPI, and on the BSP `RunBspMaintenance()` - the
|
||||
thing that wakes sleeping processes - is not reached until the pass returns.
|
||||
`IwxSendCmd` then waits for the firmware by spinning on `IwxDelayUs`, up to a
|
||||
second. One pass of `TearDown()` is eight commands. Against an adapter that has
|
||||
stopped answering, that pinned a core for the better part of ten seconds. The
|
||||
symptom is not a Wi-Fi symptom at all: the cursor crawls, windows stop
|
||||
repainting, everything stutters.
|
||||
|
||||
So a service pass now carries a budget - `IwxBeginServicePass()` /
|
||||
`IwxEndServicePass()` - of roughly one command's worth of waiting in total.
|
||||
Commands that do not fit are not sent at all (an abandoned command still holds
|
||||
its ring slot, and a late answer would be misread as the *next* command's
|
||||
completion) and are retried on the next trip round the idle loop. The bring-up
|
||||
path does not bracket itself: it reserves its CPU deliberately and has nothing
|
||||
to starve, so it keeps the full per-command timeout.
|
||||
|
||||
**Counting consecutive failures never fires on the failure that matters.** The
|
||||
give-up rule was three unanswered commands in a row, with any success resetting
|
||||
the count. An adapter that answers some commands and drops others - which is
|
||||
exactly what a marginal link leaves the firmware doing - therefore never
|
||||
reached the cutoff, while every drop still cost a full timeout. The stall was
|
||||
not a one-off; it repeated indefinitely, and the driver never concluded
|
||||
anything was wrong. It is a leaky bucket now: a failure adds one, a success
|
||||
drains one, so a firmware failing even a fraction of its commands trips the
|
||||
cutoff in bounded time.
|
||||
|
||||
**`IwxFwState::Error` was a one-way door.** Nothing anywhere cleared it. A
|
||||
single firmware assert left `StartJoin()` returning `WIFI_ERR_NO_ADAPTER` -
|
||||
which the user sees as *no adapter is ready* - for the rest of the uptime, and
|
||||
the only way back was a reboot. Disconnecting and reconnecting could not help:
|
||||
there was nothing to reconnect with. Since `IwxReadFirmware()` is idempotent
|
||||
and the parsed image stays resident, the cure is to stop the device and run the
|
||||
same bring-up again, which `ServiceRecovery()` now does. It is rate-limited to
|
||||
one attempt per five seconds and capped at three, because the reset has its own
|
||||
multi-second handshakes and an adapter that will not come back after three
|
||||
tries is genuinely broken - repeating it forever would be its own kind of
|
||||
stall.
|
||||
|
||||
### The link is up only while the access point says so
|
||||
|
||||
`IwxLinkUp()` was nothing but `g_state == Connected`, and nothing moved it off
|
||||
that state unless the access point was polite enough to send a
|
||||
deauthentication frame. An access point that simply stops being there - a phone
|
||||
hotspot that sleeps, wanders off channel, or drops the station without saying
|
||||
so - left the link reported as up indefinitely. `NetIf::Active()` went on
|
||||
choosing `wlan0`, every packet went into the void, and the desktop showed a
|
||||
healthy connection while nothing resolved and nothing connected.
|
||||
|
||||
Beacon loss cannot be used to notice this: once associated, `MacConfigCmd`
|
||||
stops asking for beacons and the firmware tracks them itself. What is
|
||||
observable is that our own frames stop being acknowledged, since `IwxTxComplete`
|
||||
gets a status per frame. A long enough run of failures - with any
|
||||
acknowledgement, or any frame received from the BSS, restarting the count -
|
||||
means the access point is gone, and the link comes down so the stack can fall
|
||||
back to a cable and the panel can report the truth.
|
||||
|
||||
The threshold is counted in frames rather than seconds on purpose: a link that
|
||||
is merely idle has nothing to send and must not be torn down for it. And
|
||||
`IwxTxComplete` runs inside the RX pump, so it only sets a flag; the teardown
|
||||
happens in `IwxConnectService()`, where sending the commands it needs is
|
||||
allowed. `tests/wifi/ap_mlme.py` pins both halves of that.
|
||||
|
||||
## The desktop side
|
||||
|
||||
### Nothing in the GUI may block
|
||||
|
||||
`SYS_WIFI_SCAN` sweeps for up to twenty seconds and `SYS_WIFI_CONNECT` waits
|
||||
out a whole handshake. Either one called from `desktop.elf` would freeze the
|
||||
compositor - and with it the mouse, the panel and every window - for seconds at
|
||||
a time. So the same work has a second, non-blocking entry point:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `SYS_WIFI_SCAN_START` | starts a sweep and returns immediately |
|
||||
| `SYS_WIFI_RESULTS` | copies the scan table out without touching the radio |
|
||||
| `SYS_WIFI_CONNECT_ASYNC` | starts a join and returns immediately |
|
||||
| `SYS_NETIFS` | lists the registered interfaces (see below) |
|
||||
|
||||
`WifiInfo` grew the fields that make polling enough to follow along:
|
||||
`scanning` and `scanGeneration` for the sweep, `joining` and `connState` for
|
||||
the handshake, and `lastError` for how the last join ended. The GUI reads
|
||||
`SYS_WIFI_INFO` every 400 ms while something is in flight and every three
|
||||
seconds otherwise.
|
||||
|
||||
The deadlines belong to the kernel, not the caller: `ServiceAsync()` runs from
|
||||
`ServiceEvents()` - after the RX pump has returned, so it is allowed to send
|
||||
commands - and aborts a scan that overruns, tears down a join that stalls, and
|
||||
records `lastError` when one fails. A failed join is unwound the moment it
|
||||
fails, so `connState` is back to idle by the time anyone looks; `lastError` is
|
||||
what survives to be reported.
|
||||
|
||||
### Wired and wireless are separate on the panel
|
||||
|
||||
The IP configuration is global to the stack, so "which interface does this
|
||||
address belong to?" is not a question `SYS_GETNETCFG` can answer. `SYS_NETIFS`
|
||||
reports each registered interface with its name, MAC, link state and an
|
||||
`active` flag - the one `NetIf::Active()` currently sends through. The Ethernet
|
||||
popup shows the address only while the wired interface is the active one, and
|
||||
says "Not in use" when the cable is up but Wi-Fi is carrying the traffic; the
|
||||
Wi-Fi popup does the mirror image. Each icon appears only when its hardware
|
||||
does: no wired interface, no Ethernet icon; no adapter, no Wi-Fi icon.
|
||||
|
||||
The Wi-Fi icon stays white whatever the radio is doing. State belongs in the
|
||||
popup, and an icon that changes colour next to the clock is just noise.
|
||||
|
||||
Picking a network that needs a passphrase opens a real window - created with
|
||||
`desktop_create_window()` and the four callbacks, exactly like the reboot and
|
||||
shutdown dialogs in `dialogs.cpp` - rather than something painted into the
|
||||
panel overlay. It therefore has a title bar, can be dragged and closed, appears
|
||||
in the window list, and gets its text field, checkboxes and buttons from the
|
||||
same `mtk` widgets the settings apps use.
|
||||
|
||||
### What runs at startup
|
||||
|
||||
`desktop.elf` keeps looking for an adapter until one appears (firmware loads
|
||||
well after login), starts one scan as soon as the firmware reports ready, and
|
||||
when the results land joins the strongest saved network that is in range. Once
|
||||
the link is up, and only if no address is configured, it spawns `dhcp.elf`.
|
||||
|
||||
### Saved networks
|
||||
|
||||
`0:/config/wifi.toml` holds them:
|
||||
|
||||
```toml
|
||||
[wifi]
|
||||
autoconnect = true
|
||||
|
||||
[network.0]
|
||||
ssid = "Home"
|
||||
psk = "passphrase"
|
||||
```
|
||||
|
||||
**No default copy of this file ships in the image.** It is created on the first
|
||||
save, the way `bluetooth.toml`, `display.toml` and `session.toml` are. A shipped
|
||||
default looks harmless - it only documents the schema - but it is laid down
|
||||
again by anything that refreshes the system files, and it takes the user's saved
|
||||
networks with it when it lands. That is exactly what happened the first time
|
||||
this was written, and it is why the schema is documented here instead.
|
||||
|
||||
`programs/include/montauk/wifi.h` is the one implementation of reading,
|
||||
writing and searching that file, shared by the panel, the Network app and the
|
||||
`wifi` command, so all three agree on the schema. The passphrase is stored as
|
||||
typed because that is what the join needs - the kernel derives the PMK from it,
|
||||
or takes a 64-character hex string as a raw PSK. There is no key store to hide
|
||||
it in: anyone who can read `0:/config` can read the passphrases.
|
||||
|
||||
## The interface registry
|
||||
|
||||
`Net::NetIf` replaced the Ethernet layer's direct calls into the E1000
|
||||
|
||||
@@ -12,4 +12,4 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define MONTAUK_BUILD_NUMBER 69
|
||||
#define MONTAUK_BUILD_NUMBER 84
|
||||
|
||||
@@ -160,6 +160,38 @@ namespace montauk::abi {
|
||||
}
|
||||
}
|
||||
|
||||
// List the registered link-layer interfaces. The panel needs this to keep
|
||||
// wired and wireless status apart: the IP configuration is global, so the
|
||||
// only way to say which interface owns it is `active`.
|
||||
static int Sys_NetIfs(NetIfInfo* out, int maxCount) {
|
||||
if (out == nullptr || maxCount <= 0) return -1;
|
||||
|
||||
const auto* active = ::Net::NetIf::Active();
|
||||
int count = ::Net::NetIf::Count();
|
||||
int n = 0;
|
||||
|
||||
for (int i = 0; i < count && n < maxCount; i++) {
|
||||
const auto* iface = ::Net::NetIf::At(i);
|
||||
if (iface == nullptr) continue;
|
||||
|
||||
NetIfInfo& info = out[n];
|
||||
for (uint64_t k = 0; k < sizeof(info.name); k++) info.name[k] = '\0';
|
||||
for (uint64_t k = 0; k + 1 < sizeof(info.name) && iface->Name && iface->Name[k]; k++)
|
||||
info.name[k] = iface->Name[k];
|
||||
|
||||
const uint8_t* mac = iface->GetMac ? iface->GetMac() : nullptr;
|
||||
for (int k = 0; k < 6; k++) info.mac[k] = mac ? mac[k] : 0;
|
||||
|
||||
info.kind = iface->Type == ::Net::NetIf::Kind::Wireless
|
||||
? NETIF_KIND_WIRELESS : NETIF_KIND_ETHERNET;
|
||||
info.linkUp = (iface->IsLinkUp && iface->IsLinkUp()) ? 1 : 0;
|
||||
info.active = iface == active ? 1 : 0;
|
||||
info._pad[0] = info._pad[1] = info._pad[2] = 0;
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static int Sys_SetNetCfg(const NetCfg* in) {
|
||||
if (in == nullptr) return -1;
|
||||
Net::SetIpAddress(in->ipAddress);
|
||||
|
||||
@@ -457,6 +457,20 @@ namespace montauk::abi {
|
||||
return Sys_WifiConnect((const char*)frame->arg1, (const char*)frame->arg2);
|
||||
case SYS_WIFI_DISCONNECT:
|
||||
return Sys_WifiDisconnect();
|
||||
case SYS_WIFI_SCAN_START:
|
||||
return Sys_WifiScanStart((uint32_t)frame->arg1);
|
||||
case SYS_WIFI_RESULTS:
|
||||
if ((int64_t)frame->arg2 < 0) return -1;
|
||||
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(WifiNetwork), true)) return -1;
|
||||
return Sys_WifiResults((WifiNetwork*)frame->arg1, (int)frame->arg2);
|
||||
case SYS_WIFI_CONNECT_ASYNC:
|
||||
if (!UserMemory::String(frame->arg1, 64)) return -1;
|
||||
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, 128)) return -1;
|
||||
return Sys_WifiConnectAsync((const char*)frame->arg1, (const char*)frame->arg2);
|
||||
case SYS_NETIFS:
|
||||
if ((int64_t)frame->arg2 < 0) return -1;
|
||||
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(NetIfInfo), true)) return -1;
|
||||
return Sys_NetIfs((NetIfInfo*)frame->arg1, (int)frame->arg2);
|
||||
case SYS_SUSPEND:
|
||||
return Sys_Suspend();
|
||||
case SYS_SETTZ:
|
||||
|
||||
@@ -302,6 +302,10 @@ namespace montauk::abi {
|
||||
static constexpr uint64_t SYS_WIFI_INFO = 159; // (WifiInfo*) -> 0, -1 if absent
|
||||
static constexpr uint64_t SYS_WIFI_CONNECT = 160; // (ssid, password) -> 0, <0 on error
|
||||
static constexpr uint64_t SYS_WIFI_DISCONNECT = 161; // () -> 0
|
||||
static constexpr uint64_t SYS_WIFI_SCAN_START = 162; // (timeoutMs) -> 0 started, 1 busy, -1 no adapter
|
||||
static constexpr uint64_t SYS_WIFI_RESULTS = 163; // (WifiNetwork*, maxCount) -> count, no radio work
|
||||
static constexpr uint64_t SYS_WIFI_CONNECT_ASYNC = 164; // (ssid, password) -> 0 accepted, <0 on error
|
||||
static constexpr uint64_t SYS_NETIFS = 165; // (NetIfInfo*, maxCount) -> count
|
||||
|
||||
// Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM).
|
||||
static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz
|
||||
@@ -708,6 +712,26 @@ namespace montauk::abi {
|
||||
uint8_t bssid[6];
|
||||
uint8_t connected; // 1 once the link can carry IP traffic
|
||||
uint8_t channel;
|
||||
int32_t lastError; // WIFI_ERR_* from the last async join, 0 = none
|
||||
uint32_t scanGeneration; // bumped every time a scan finishes
|
||||
uint8_t joining; // 1 while an async join is in flight
|
||||
uint8_t _pad[3];
|
||||
};
|
||||
|
||||
// Link-layer interface kinds reported in NetIfInfo.kind.
|
||||
static constexpr uint8_t NETIF_KIND_ETHERNET = 0;
|
||||
static constexpr uint8_t NETIF_KIND_WIRELESS = 1;
|
||||
|
||||
// One registered link-layer interface (returned by SYS_NETIFS). The IP
|
||||
// configuration is global to the stack, so it belongs to whichever
|
||||
// interface reports active = 1.
|
||||
struct NetIfInfo {
|
||||
char name[16]; // "eth0", "wlan0"
|
||||
uint8_t mac[6];
|
||||
uint8_t kind; // NETIF_KIND_*
|
||||
uint8_t linkUp;
|
||||
uint8_t active; // 1 if this is the interface carrying traffic
|
||||
uint8_t _pad[3];
|
||||
};
|
||||
|
||||
struct ThermalInfo {
|
||||
|
||||
@@ -23,11 +23,26 @@ namespace montauk::abi {
|
||||
return (int64_t)Drivers::Net::Wifi::GetInfo(buf);
|
||||
}
|
||||
|
||||
static int64_t Sys_WifiScanStart(uint32_t timeoutMs) {
|
||||
return (int64_t)Drivers::Net::Wifi::StartScan(timeoutMs);
|
||||
}
|
||||
|
||||
static int64_t Sys_WifiResults(WifiNetwork* buf, int maxCount) {
|
||||
if (!buf || maxCount <= 0) return -1;
|
||||
if (maxCount > 64) maxCount = 64;
|
||||
return (int64_t)Drivers::Net::Wifi::GetResults(buf, maxCount);
|
||||
}
|
||||
|
||||
static int64_t Sys_WifiConnect(const char* ssid, const char* password) {
|
||||
if (!ssid) return -1;
|
||||
return (int64_t)Drivers::Net::Wifi::Connect(ssid, password);
|
||||
}
|
||||
|
||||
static int64_t Sys_WifiConnectAsync(const char* ssid, const char* password) {
|
||||
if (!ssid) return -1;
|
||||
return (int64_t)Drivers::Net::Wifi::ConnectAsync(ssid, password);
|
||||
}
|
||||
|
||||
static int64_t Sys_WifiDisconnect() {
|
||||
return (int64_t)Drivers::Net::Wifi::Disconnect();
|
||||
}
|
||||
|
||||
@@ -302,6 +302,21 @@ namespace Drivers::Net::Wifi {
|
||||
bool IwxSendCmdStatus(uint32_t id, const void* data, uint32_t len,
|
||||
uint32_t* statusOut);
|
||||
|
||||
// Bracket a pass of the idle-loop service work. The wait inside
|
||||
// IwxSendCmd is a busy spin on a core the scheduler has been told not to
|
||||
// touch, so a pass is given one command's worth of waiting in total and
|
||||
// stops sending once that is gone; the rest is picked up next pass. The
|
||||
// bring-up path does not bracket itself and keeps the full per-command
|
||||
// budget. See the comment above IwxCmdWaitBudgetMs().
|
||||
// Returns false if another core already owns this pass; only the owner
|
||||
// may end it.
|
||||
bool IwxBeginServicePass();
|
||||
void IwxEndServicePass();
|
||||
|
||||
// Forget the accumulated command-failure score and the one-shot error
|
||||
// dump latch. Called after the adapter has been brought back up.
|
||||
void IwxResetCmdHealth();
|
||||
|
||||
// Poll interrupt causes + drain the RX/notification ring. Safe to call
|
||||
// from any process/idle context; self-guarded against reentry.
|
||||
void IwxProcessEvents();
|
||||
@@ -387,6 +402,14 @@ namespace Drivers::Net::Wifi {
|
||||
const uint8_t* rsnIe, uint32_t rsnIeLen,
|
||||
uint16_t beaconInterval, uint8_t dtimPeriod);
|
||||
void IwxConnectAbort();
|
||||
// Drop all connection state without talking to the firmware. For the
|
||||
// recovery path, where the adapter is being reset out from under the state
|
||||
// machine and IwxConnectAbort()'s teardown commands would only burn
|
||||
// timeouts against a device that is about to be reinitialised anyway.
|
||||
void IwxConnectReset();
|
||||
// Per-frame transmit outcome, reported by the TX completion path. Feeds
|
||||
// the link supervision described in IwxConnect.cpp.
|
||||
void IwxConnectNoteTx(bool acked);
|
||||
// Why the last IwxConnectStart() refused: false means the radio or the
|
||||
// firmware failed, true means the network's security is unsupported. The
|
||||
// two need very different advice, so they must not be conflated.
|
||||
|
||||
@@ -75,6 +75,28 @@ namespace Drivers::Net::Wifi {
|
||||
static volatile bool g_postAssocPending = false;
|
||||
static volatile bool g_teardownPending = false;
|
||||
static volatile bool g_sendAssocPending = false;
|
||||
static volatile bool g_linkLostPending = false;
|
||||
|
||||
// Link supervision.
|
||||
//
|
||||
// IwxLinkUp() is nothing but "the state machine reached Connected", and
|
||||
// until now nothing moved it back off that state unless the access point
|
||||
// was polite enough to send a deauthentication frame. An access point that
|
||||
// simply stops being there -- a phone hotspot that sleeps, wanders off
|
||||
// channel, or drops the station without saying so -- left the link reported
|
||||
// as up indefinitely. NetIf::Active() went on selecting wlan0, every
|
||||
// packet went into the void, and the desktop showed a healthy connection
|
||||
// while nothing resolved and nothing connected.
|
||||
//
|
||||
// Beacon loss is not observable from here: once associated, MacConfigCmd
|
||||
// stops asking for beacons and the firmware tracks them itself. What is
|
||||
// observable is that our own frames stop being acknowledged -- IwxTxComplete
|
||||
// gets a per-frame status. A long enough run of failures with nothing
|
||||
// heard from the BSS in between means the access point is gone. The
|
||||
// threshold is in frames rather than time because it must not fire on an
|
||||
// idle link that simply has nothing to send.
|
||||
static volatile uint32_t g_txFailStreak = 0;
|
||||
static constexpr uint32_t TX_FAIL_STREAK_LIMIT = 16;
|
||||
|
||||
// Timers for retransmission and give-up.
|
||||
static uint64_t g_stateEnteredMs = 0;
|
||||
@@ -585,6 +607,10 @@ namespace Drivers::Net::Wifi {
|
||||
|
||||
g_iwx.RxDataPackets++;
|
||||
|
||||
// Anything at all arriving from the BSS proves the access point is
|
||||
// still there, so the transmit-failure streak starts over.
|
||||
g_txFailStreak = 0;
|
||||
|
||||
if (etherType == ETHERTYPE_EAPOL) {
|
||||
KernelLogStream(INFO, "WiFi") << "EAPOL frame received ("
|
||||
<< (uint64_t)payloadLen << " bytes)";
|
||||
@@ -810,6 +836,46 @@ namespace Drivers::Net::Wifi {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Called from the TX completion path (inside the RX pump), so it may only
|
||||
// set a flag; ServiceLocked() does the actual teardown, where sending the
|
||||
// firmware commands it needs is allowed.
|
||||
void IwxConnectNoteTx(bool acked) {
|
||||
if (acked) { g_txFailStreak = 0; return; }
|
||||
if (g_state != ConnState::Connected) return;
|
||||
if (++g_txFailStreak >= TX_FAIL_STREAK_LIMIT) {
|
||||
g_txFailStreak = 0;
|
||||
g_linkLostPending = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Drop everything without touching the firmware. Used by the recovery
|
||||
// path, which is about to reinitialise the adapter: the teardown commands
|
||||
// IwxConnectAbort() would send have nothing to talk to and would only burn
|
||||
// a timeout each.
|
||||
void IwxConnectReset() {
|
||||
WpaReset();
|
||||
g_keysInstalled = false;
|
||||
g_ptkKey = {};
|
||||
g_gtkKey = {};
|
||||
g_phyActive = false;
|
||||
g_macActive = false;
|
||||
g_macAssoc = false;
|
||||
g_bindingActive = false;
|
||||
g_linkActive = false;
|
||||
g_staActive = false;
|
||||
g_aid = 0;
|
||||
g_eapolHead = 0;
|
||||
g_eapolTail = 0;
|
||||
g_txFailStreak = 0;
|
||||
g_postAssocPending = false;
|
||||
g_teardownPending = false;
|
||||
g_sendAssocPending = false;
|
||||
g_linkLostPending = false;
|
||||
memset(&g_wpaCfg, 0, sizeof(g_wpaCfg));
|
||||
g_secured = false;
|
||||
g_state = ConnState::Idle;
|
||||
}
|
||||
|
||||
void IwxConnectAbort() {
|
||||
if (g_state == ConnState::Idle) return;
|
||||
// Tell the AP we are leaving, but only while the station context (and
|
||||
@@ -884,6 +950,22 @@ namespace Drivers::Net::Wifi {
|
||||
return;
|
||||
}
|
||||
|
||||
// The access point stopped acknowledging anything. Bring the link down
|
||||
// rather than leaving the stack transmitting into a hole: NetIf can
|
||||
// then fall back to a wired interface, and the desktop reports the
|
||||
// truth instead of a connection that only exists on paper.
|
||||
if (g_linkLostPending) {
|
||||
g_linkLostPending = false;
|
||||
if (g_state != ConnState::Idle) {
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "\"" << g_ssid << "\" stopped acknowledging frames; "
|
||||
<< "dropping the link";
|
||||
TearDown();
|
||||
g_state = ConnState::Idle;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_state == ConnState::Idle || g_state == ConnState::Failed) return;
|
||||
|
||||
if (g_sendAssocPending) {
|
||||
|
||||
@@ -869,11 +869,84 @@ namespace Drivers::Net::Wifi {
|
||||
}
|
||||
|
||||
void IwxDumpFwError();
|
||||
static uint32_t g_cmdTimeouts = 0; // consecutive unanswered commands
|
||||
|
||||
// Counting *consecutive* unanswered commands is not enough. An adapter
|
||||
// that answers some commands and drops others resets a consecutive counter
|
||||
// on every success and so never reaches the cutoff, while each drop still
|
||||
// costs a full timeout of busy-waiting -- and that is exactly the state a
|
||||
// marginal access point leaves the firmware in. The result was a machine
|
||||
// that stalled a second at a time, indefinitely, with the driver never
|
||||
// concluding anything was wrong. Count in a leaky bucket instead: a
|
||||
// failure adds one, a success drains one, so a firmware failing even a
|
||||
// fraction of its commands still trips the cutoff in bounded time.
|
||||
static uint32_t g_cmdFailScore = 0;
|
||||
static bool g_fwErrorDumped = false;
|
||||
static constexpr uint32_t CMD_FAIL_SCORE_MAX = 6;
|
||||
|
||||
void IwxResetCmdHealth() {
|
||||
g_cmdFailScore = 0;
|
||||
g_fwErrorDumped = false;
|
||||
}
|
||||
|
||||
// How long one command may wait, and whether it may be sent at all.
|
||||
//
|
||||
// The bring-up path reserves its CPU deliberately and has nothing to
|
||||
// starve, so it keeps the full second iwlwifi allows. The idle service
|
||||
// pass is a different animal: ServiceDeferredWork() sets
|
||||
// reservedForKernelWork, which makes that core ineligible to run any
|
||||
// process and immune to the reschedule IPI (Scheduler.cpp), and on the BSP
|
||||
// it also defers RunBspMaintenance() -- the thing that wakes sleeping
|
||||
// processes. The wait below is a busy spin, not a sleep. So a pass that
|
||||
// issues eight commands (TearDown does exactly that) against an
|
||||
// unresponsive firmware pins a core for eight seconds, which is what makes
|
||||
// the mouse crawl when an access point goes bad.
|
||||
//
|
||||
// A pass therefore gets one command's worth of waiting in total.
|
||||
static constexpr uint32_t IWX_CMD_TIMEOUT_MS = 1000;
|
||||
static constexpr uint32_t IWX_PASS_BUDGET_MS = 1000;
|
||||
// Below this there is no point starting a command: it would be abandoned
|
||||
// almost immediately, and an abandoned command still holds its ring slot
|
||||
// and may be answered later, which the *next* command would misread as its
|
||||
// own completion.
|
||||
static constexpr uint32_t IWX_CMD_MIN_WAIT_MS = 50;
|
||||
|
||||
static uint64_t g_passDeadline = 0; // 0 = not inside a pass
|
||||
static volatile bool g_passOwned = false;
|
||||
|
||||
// Every idling core runs ServiceEvents(), so the bracket needs an owner:
|
||||
// otherwise the second core to arrive would clear the first core's deadline
|
||||
// on its way out and hand it back the unbounded wait this exists to
|
||||
// prevent. A core that does not win still runs under the winner's
|
||||
// deadline, which is strictly tighter than none.
|
||||
bool IwxBeginServicePass() {
|
||||
if (__atomic_test_and_set(&g_passOwned, __ATOMIC_ACQUIRE)) return false;
|
||||
g_passDeadline = Timekeeping::GetMilliseconds() + IWX_PASS_BUDGET_MS;
|
||||
return true;
|
||||
}
|
||||
|
||||
void IwxEndServicePass() {
|
||||
g_passDeadline = 0;
|
||||
__atomic_clear(&g_passOwned, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
// Remaining wait allowance, clamped to the per-command timeout.
|
||||
static uint32_t IwxCmdWaitBudgetMs() {
|
||||
if (!g_passDeadline) return IWX_CMD_TIMEOUT_MS;
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
if (now >= g_passDeadline) return 0;
|
||||
uint64_t left = g_passDeadline - now;
|
||||
return left > IWX_CMD_TIMEOUT_MS ? IWX_CMD_TIMEOUT_MS : (uint32_t)left;
|
||||
}
|
||||
|
||||
bool IwxSendCmd(IwxHostCmd& hcmd) {
|
||||
if (g_iwx.State == IwxFwState::Error) return false;
|
||||
|
||||
// Out of budget for this pass. Fail quietly -- the firmware has done
|
||||
// nothing wrong, so this must not count against it -- and let the
|
||||
// caller's state machine retry on the next pass.
|
||||
uint32_t waitMs = IwxCmdWaitBudgetMs();
|
||||
if (waitMs < IWX_CMD_MIN_WAIT_MS) return false;
|
||||
|
||||
IwxTxRing& ring = g_iwx.CmdQ;
|
||||
|
||||
g_iwx.CmdLock.Acquire();
|
||||
@@ -961,12 +1034,16 @@ namespace Drivers::Net::Wifi {
|
||||
constexpr uint32_t MAX_SPINS = 20000; // ~2 s at 100 us
|
||||
bool ok = false;
|
||||
bool died = false;
|
||||
// Re-read the allowance: acquiring CmdLock above can itself have waited
|
||||
// out another core's command, and the budget is for the pass, not for
|
||||
// each caller's view of it when it arrived.
|
||||
waitMs = IwxCmdWaitBudgetMs();
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
for (uint32_t spins = 0; spins < MAX_SPINS; spins++) {
|
||||
IwxProcessEvents();
|
||||
if (g_iwx.CmdDone) { ok = true; break; }
|
||||
if (g_iwx.State == IwxFwState::Error) { died = true; break; }
|
||||
if (Timekeeping::GetMilliseconds() - start >= 1000) break;
|
||||
if (Timekeeping::GetMilliseconds() - start >= waitMs) break;
|
||||
IwxDelayUs(100);
|
||||
}
|
||||
|
||||
@@ -979,20 +1056,26 @@ namespace Drivers::Net::Wifi {
|
||||
if (!died) {
|
||||
// Dump on the first silence: the firmware's error table names
|
||||
// the command that asserted, and it is overwritten as later
|
||||
// commands go unanswered.
|
||||
if (++g_cmdTimeouts == 1) IwxDumpFwError();
|
||||
// Repeated silence means it is wedged and every later command
|
||||
// would burn the same timeout, so stop trying. A single late
|
||||
// response is not worth disabling the adapter over.
|
||||
if (g_cmdTimeouts >= 3) {
|
||||
// commands go unanswered. Latched separately from the score,
|
||||
// which now goes up and down and would otherwise re-dump every
|
||||
// time it passed through one.
|
||||
g_cmdFailScore++;
|
||||
if (!g_fwErrorDumped) {
|
||||
g_fwErrorDumped = true;
|
||||
IwxDumpFwError();
|
||||
}
|
||||
// Enough net silence means it is wedged and every later command
|
||||
// would burn the same timeout, so stop trying and let
|
||||
// ServiceRecovery() put the adapter back together.
|
||||
if (g_cmdFailScore >= CMD_FAIL_SCORE_MAX) {
|
||||
KernelLogStream(ERROR, "WiFi")
|
||||
<< "Firmware stopped responding to host commands";
|
||||
g_iwx.FwErrors++;
|
||||
g_iwx.State = IwxFwState::Error;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
g_cmdTimeouts = 0;
|
||||
} else if (g_cmdFailScore) {
|
||||
g_cmdFailScore--;
|
||||
}
|
||||
|
||||
g_iwx.CmdWantResp = false;
|
||||
@@ -1272,10 +1355,15 @@ namespace Drivers::Net::Wifi {
|
||||
if (ring->Queued > 0) ring->Queued--;
|
||||
g_iwx.TxLock.Release();
|
||||
|
||||
if (status == IWX_TX_STATUS_SUCCESS || status == IWX_TX_STATUS_DIRECT_DONE)
|
||||
g_iwx.TxPackets++;
|
||||
else
|
||||
g_iwx.TxFailures++;
|
||||
bool acked = status == IWX_TX_STATUS_SUCCESS
|
||||
|| status == IWX_TX_STATUS_DIRECT_DONE;
|
||||
if (acked) g_iwx.TxPackets++;
|
||||
else g_iwx.TxFailures++;
|
||||
|
||||
// Feeds the link supervision in IwxConnect.cpp: an access point that
|
||||
// vanishes without deauthenticating is only visible as our frames
|
||||
// going unacknowledged.
|
||||
IwxConnectNoteTx(acked);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
||||
@@ -61,6 +61,22 @@ namespace Drivers::Net::Wifi {
|
||||
static int g_resultCount = 0;
|
||||
static kcp::Spinlock g_resultLock;
|
||||
|
||||
// =========================================================================
|
||||
// Non-blocking scan / join state
|
||||
//
|
||||
// The GUI cannot afford the blocking Scan() and Connect() below: they take
|
||||
// seconds, and the compositor calling them would freeze the whole desktop.
|
||||
// The async entry points start the same work and return immediately; the
|
||||
// deadlines and the final connect result are looked after by ServiceAsync()
|
||||
// from the idle loop, and the caller polls GetInfo().
|
||||
// =========================================================================
|
||||
|
||||
static uint64_t g_scanDeadline = 0; // 0 = no async scan outstanding
|
||||
static uint32_t g_scanGeneration = 0; // bumped when a scan finishes
|
||||
static bool g_asyncConnect = false;
|
||||
static uint64_t g_asyncConnectDeadline = 0;
|
||||
static int g_lastError = 0; // result of the last async join
|
||||
|
||||
static void ClearResults() {
|
||||
g_resultLock.Acquire();
|
||||
for (int i = 0; i < MAX_SCAN_RESULTS; i++) g_results[i].Used = false;
|
||||
@@ -305,6 +321,102 @@ namespace Drivers::Net::Wifi {
|
||||
<< g_iwx.Fw.Version << ")";
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Recovery from a wedged adapter
|
||||
//
|
||||
// IwxFwState::Error used to be a one-way door. Nothing anywhere cleared
|
||||
// it, so a single firmware assert -- which a flaky access point provokes
|
||||
// readily enough -- left StartJoin() returning WIFI_ERR_NO_ADAPTER ("no
|
||||
// adapter is ready") for the rest of the uptime, and the only way back was
|
||||
// a reboot.
|
||||
//
|
||||
// The firmware image is still parsed and resident (IwxReadFirmware() is
|
||||
// idempotent and keeps g_iwx.Fw.Raw), so the cure is simply to stop the
|
||||
// device and run the same bring-up again. It is not free -- the handshakes
|
||||
// inside take a couple of seconds with this core reserved -- so it is
|
||||
// rate-limited and capped. If the adapter will not come back after a few
|
||||
// tries it is genuinely broken and repeating the reset would be its own
|
||||
// kind of stall.
|
||||
// =========================================================================
|
||||
|
||||
static uint32_t g_recoveryAttempts = 0;
|
||||
static uint64_t g_lastRecoveryMs = 0;
|
||||
static bool g_recoveryGaveUp = false;
|
||||
static constexpr uint32_t MAX_RECOVERY_ATTEMPTS = 3;
|
||||
static constexpr uint64_t RECOVERY_BACKOFF_MS = 5000;
|
||||
|
||||
static void ServiceRecovery() {
|
||||
if (g_iwx.State != IwxFwState::Error || g_recoveryGaveUp) return;
|
||||
// An adapter that never finished its first bring-up is ServiceDeferredInit's
|
||||
// problem, not this one.
|
||||
if (!g_initialized) return;
|
||||
|
||||
// Every idling core reaches here, and two of them resetting the device
|
||||
// at once would be considerably worse than the fault being recovered
|
||||
// from. Same test-and-set gate ServiceAsync() and IwxConnectService()
|
||||
// use; the loser has nothing to do.
|
||||
static volatile bool inRecovery = false;
|
||||
if (__atomic_test_and_set(&inRecovery, __ATOMIC_ACQUIRE)) return;
|
||||
struct Guard {
|
||||
volatile bool* flag;
|
||||
~Guard() { __atomic_clear(flag, __ATOMIC_RELEASE); }
|
||||
} guard{&inRecovery};
|
||||
|
||||
// Re-check under the gate: the winner of the race may have just
|
||||
// finished a reset that fixed things.
|
||||
if (g_iwx.State != IwxFwState::Error) return;
|
||||
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
if (g_lastRecoveryMs && now - g_lastRecoveryMs < RECOVERY_BACKOFF_MS) return;
|
||||
g_lastRecoveryMs = now;
|
||||
|
||||
if (++g_recoveryAttempts > MAX_RECOVERY_ATTEMPTS) {
|
||||
g_recoveryGaveUp = true;
|
||||
KernelLogStream(ERROR, "WiFi")
|
||||
<< "Adapter did not come back after " << (uint64_t)MAX_RECOVERY_ATTEMPTS
|
||||
<< " resets; leaving it down";
|
||||
return;
|
||||
}
|
||||
|
||||
KernelLogStream(WARNING, "WiFi") << "Resetting the adapter after a firmware error"
|
||||
<< " (attempt " << (uint64_t)g_recoveryAttempts << " of "
|
||||
<< (uint64_t)MAX_RECOVERY_ATTEMPTS << ")";
|
||||
|
||||
// Nothing may be mid-command while the device is torn down. Taking
|
||||
// CmdLock is enough: it is what serializes every sender, and the RX
|
||||
// pump has its own reentrancy guard.
|
||||
g_iwx.CmdLock.Acquire();
|
||||
|
||||
g_initialized = false;
|
||||
g_asyncConnect = false;
|
||||
g_scanDeadline = 0;
|
||||
g_lastError = WIFI_ERR_NO_ADAPTER;
|
||||
g_iwx.ScanActive = false;
|
||||
|
||||
// Not IwxConnectAbort(): its teardown commands would be sent to a
|
||||
// device that has already stopped answering, costing a timeout each for
|
||||
// contexts that the reset below discards anyway.
|
||||
IwxConnectReset();
|
||||
ClearResults();
|
||||
|
||||
IwxStopDevice();
|
||||
IwxResetCmdHealth();
|
||||
// Detected, not Absent: the PCI device is still claimed and mapped, and
|
||||
// IsPresent() keys off Absent -- the Wi-Fi icon must not blink out of
|
||||
// the panel every time the adapter is reset.
|
||||
g_iwx.State = IwxFwState::Detected;
|
||||
|
||||
g_iwx.CmdLock.Release();
|
||||
|
||||
CompleteInit();
|
||||
|
||||
if (g_initialized) {
|
||||
KernelLogStream(OK, "WiFi") << "Adapter recovered";
|
||||
g_recoveryAttempts = 0;
|
||||
g_lastError = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void ServiceDeferredInit() {
|
||||
if (!g_initPending.load(std::memory_order_relaxed) || g_initialized) return;
|
||||
if (!Fs::Vfs::IsDriveRegistered(0)) return; // ramdisk not mounted yet
|
||||
@@ -325,12 +437,36 @@ namespace Drivers::Net::Wifi {
|
||||
if (cpu) cpu->reservedForKernelWork = wasReserved;
|
||||
}
|
||||
|
||||
static void ServiceAsync();
|
||||
static void ServiceRecovery();
|
||||
|
||||
void ServiceEvents() {
|
||||
if (!g_iwx.Mmio) return;
|
||||
if (g_iwx.WorkPending) IwxProcessEvents();
|
||||
|
||||
// Everything below can send firmware commands, and each one waits by
|
||||
// busy-spinning. This runs from ServiceDeferredWork(), which has set
|
||||
// reservedForKernelWork on this core -- so the scheduler will not run a
|
||||
// process here and, on the BSP, RunBspMaintenance() is not reached
|
||||
// until we return. The pass budget caps the whole group at roughly one
|
||||
// command's wait; whatever does not fit is retried next time round the
|
||||
// idle loop. Without it a teardown against a wedged adapter held a
|
||||
// core for the better part of ten seconds, which is what the stalled
|
||||
// cursor and the stuttering desktop actually were.
|
||||
bool ownsPass = IwxBeginServicePass();
|
||||
|
||||
// Firmware commands the RX path deferred (it runs under the event
|
||||
// pump's reentrancy guard and cannot wait for a completion itself).
|
||||
IwxConnectService();
|
||||
// Deadlines for the non-blocking scan/join the GUI drives. Runs after
|
||||
// the pump returns, never inside it, because both paths send commands.
|
||||
ServiceAsync();
|
||||
|
||||
if (ownsPass) IwxEndServicePass();
|
||||
|
||||
// Deliberately outside the budget: a reset is a bring-up, not a pass of
|
||||
// routine servicing, and it has its own wall-clock handshakes to run.
|
||||
ServiceRecovery();
|
||||
}
|
||||
|
||||
bool IsInitialized() { return g_initialized; }
|
||||
@@ -340,6 +476,35 @@ namespace Drivers::Net::Wifi {
|
||||
// Public operations
|
||||
// =========================================================================
|
||||
|
||||
// Copy the current scan table out. No radio work: whatever the last scan
|
||||
// (blocking or not) left behind is what the caller sees.
|
||||
static int CopyResults(WifiNetwork* out, int maxCount) {
|
||||
g_resultLock.Acquire();
|
||||
int n = 0;
|
||||
for (int i = 0; i < MAX_SCAN_RESULTS && n < maxCount; i++) {
|
||||
if (!g_results[i].Used) continue;
|
||||
const ScanEntry& e = g_results[i];
|
||||
WifiNetwork& w = out[n];
|
||||
memset(&w, 0, sizeof(w));
|
||||
for (int k = 0; k < 32 && e.Ssid[k]; k++) w.ssid[k] = e.Ssid[k];
|
||||
memcpy(w.bssid, e.Bssid, 6);
|
||||
w.channel = e.Channel;
|
||||
w.rssi = e.Rssi;
|
||||
w.band = e.Band;
|
||||
w.security = e.Security;
|
||||
w.beaconInterval = e.BeaconInterval;
|
||||
n++;
|
||||
}
|
||||
g_resultLock.Release();
|
||||
return n;
|
||||
}
|
||||
|
||||
int GetResults(WifiNetwork* out, int maxCount) {
|
||||
if (!out || maxCount <= 0) return -1;
|
||||
if (!g_initialized) return -1;
|
||||
return CopyResults(out, maxCount);
|
||||
}
|
||||
|
||||
int Scan(WifiNetwork* out, int maxCount, uint32_t timeoutMs) {
|
||||
if (!out || maxCount <= 0) return -1;
|
||||
if (!g_initialized) return -1;
|
||||
@@ -365,24 +530,23 @@ namespace Drivers::Net::Wifi {
|
||||
while (Timekeeping::GetMilliseconds() - t0 < 200) IwxProcessEvents();
|
||||
}
|
||||
|
||||
g_resultLock.Acquire();
|
||||
int n = 0;
|
||||
for (int i = 0; i < MAX_SCAN_RESULTS && n < maxCount; i++) {
|
||||
if (!g_results[i].Used) continue;
|
||||
const ScanEntry& e = g_results[i];
|
||||
WifiNetwork& w = out[n];
|
||||
memset(&w, 0, sizeof(w));
|
||||
for (int k = 0; k < 32 && e.Ssid[k]; k++) w.ssid[k] = e.Ssid[k];
|
||||
memcpy(w.bssid, e.Bssid, 6);
|
||||
w.channel = e.Channel;
|
||||
w.rssi = e.Rssi;
|
||||
w.band = e.Band;
|
||||
w.security = e.Security;
|
||||
w.beaconInterval = e.BeaconInterval;
|
||||
n++;
|
||||
g_scanGeneration++;
|
||||
return CopyResults(out, maxCount);
|
||||
}
|
||||
g_resultLock.Release();
|
||||
return n;
|
||||
|
||||
int StartScan(uint32_t timeoutMs) {
|
||||
if (!g_initialized) return -1;
|
||||
if (g_iwx.State != IwxFwState::Running) return -1;
|
||||
if (g_iwx.ScanActive) return 1; // already sweeping
|
||||
|
||||
if (timeoutMs < 1000) timeoutMs = 1000;
|
||||
if (timeoutMs > 20000) timeoutMs = 20000;
|
||||
|
||||
ClearResults();
|
||||
if (!IwxStartScan(nullptr)) return -1;
|
||||
|
||||
g_scanDeadline = Timekeeping::GetMilliseconds() + timeoutMs;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GetInfo(WifiInfo* out) {
|
||||
@@ -403,6 +567,9 @@ namespace Drivers::Net::Wifi {
|
||||
out->fwErrors = (uint32_t)g_iwx.FwErrors;
|
||||
out->connState = (uint32_t)IwxConnectState();
|
||||
out->connected = IwxLinkUp() ? 1 : 0;
|
||||
out->lastError = (int32_t)g_lastError;
|
||||
out->scanGeneration = g_scanGeneration;
|
||||
out->joining = g_asyncConnect ? 1 : 0;
|
||||
|
||||
if (IwxConnectState() != (int)IwxConnStateId::Idle) {
|
||||
const char* ssid = IwxConnectSsid();
|
||||
@@ -455,7 +622,12 @@ namespace Drivers::Net::Wifi {
|
||||
return WIFI_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
int Connect(const char* ssid, const char* password) {
|
||||
// Everything a join needs before the exchange with the AP starts: find the
|
||||
// BSS, check the ciphers are ones the supplicant implements, and hand the
|
||||
// firmware its contexts. Returns 0 once the state machine is running, or a
|
||||
// WIFI_ERR_* value. `rescan` controls whether an SSID missing from the
|
||||
// scan table is worth a (blocking) sweep to look for it.
|
||||
static int StartJoin(const char* ssid, const char* password, bool rescan) {
|
||||
if (!g_initialized || !ssid) return WIFI_ERR_NO_ADAPTER;
|
||||
if (g_iwx.State != IwxFwState::Running) return WIFI_ERR_NO_ADAPTER;
|
||||
|
||||
@@ -476,7 +648,8 @@ namespace Drivers::Net::Wifi {
|
||||
// joined through the best AP rather than whichever answered first.
|
||||
int8_t bestRssi = -128;
|
||||
|
||||
for (int attempt = 0; attempt < 2 && !found; attempt++) {
|
||||
int attempts = rescan ? 2 : 1;
|
||||
for (int attempt = 0; attempt < attempts && !found; attempt++) {
|
||||
if (attempt == 1) {
|
||||
// Nothing matched: the caller may never have scanned, or the
|
||||
// results may predate this network appearing.
|
||||
@@ -547,15 +720,104 @@ namespace Drivers::Net::Wifi {
|
||||
return security_ ? WIFI_ERR_UNSUPPORTED : WIFI_ERR_FAILED;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Connect(const char* ssid, const char* password) {
|
||||
int rc = StartJoin(ssid, password, true);
|
||||
if (rc != 0) return rc;
|
||||
return WaitForConnection(15000);
|
||||
}
|
||||
|
||||
int ConnectAsync(const char* ssid, const char* password) {
|
||||
// A join already in flight owns the firmware contexts; tear it down
|
||||
// rather than stacking a second one on top.
|
||||
if (g_asyncConnect || (IwxConnectState() != (int)IwxConnStateId::Idle))
|
||||
IwxConnectAbort();
|
||||
|
||||
g_asyncConnect = false;
|
||||
g_lastError = 0;
|
||||
|
||||
// No blocking rescan here: the caller has a scan table on screen, and
|
||||
// the point of this entry point is that it returns immediately.
|
||||
int rc = StartJoin(ssid, password, false);
|
||||
if (rc != 0) {
|
||||
g_lastError = rc;
|
||||
return rc;
|
||||
}
|
||||
|
||||
g_asyncConnect = true;
|
||||
g_asyncConnectDeadline = Timekeeping::GetMilliseconds() + 20000;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Disconnect() {
|
||||
if (!g_initialized) return -1;
|
||||
g_asyncConnect = false;
|
||||
g_lastError = 0;
|
||||
IwxConnectAbort();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Deadlines and completion for the non-blocking entry points. Called from
|
||||
// ServiceEvents() after the RX pump has returned, so sending commands (the
|
||||
// scan abort, the connect teardown) is safe here.
|
||||
//
|
||||
// Every idling core calls this, so it takes the same test-and-set gate
|
||||
// IwxConnectService() uses: two cores both deciding a join has failed would
|
||||
// tear the contexts down twice. A core that loses the race has nothing to
|
||||
// do - the winner is already doing it.
|
||||
static void ServiceAsync() {
|
||||
static volatile bool inService = false;
|
||||
if (__atomic_test_and_set(&inService, __ATOMIC_ACQUIRE)) return;
|
||||
struct Guard {
|
||||
volatile bool* flag;
|
||||
~Guard() { __atomic_clear(flag, __ATOMIC_RELEASE); }
|
||||
} guard{&inService};
|
||||
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
|
||||
if (g_scanDeadline) {
|
||||
if (!g_iwx.ScanActive) {
|
||||
g_scanDeadline = 0;
|
||||
g_scanGeneration++;
|
||||
} else if (now >= g_scanDeadline) {
|
||||
IwxAbortScan();
|
||||
g_scanDeadline = 0;
|
||||
g_scanGeneration++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!g_asyncConnect) return;
|
||||
|
||||
auto state = (IwxConnStateId)IwxConnectState();
|
||||
if (state == IwxConnStateId::Connected) {
|
||||
g_asyncConnect = false;
|
||||
g_lastError = 0;
|
||||
return;
|
||||
}
|
||||
if (state == IwxConnStateId::Failed) {
|
||||
// Same reading as the blocking path: a handshake that exchanged
|
||||
// EAPOL frames and then failed is almost always a wrong passphrase.
|
||||
g_lastError = WpaGetState() == WpaState::Failed
|
||||
? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
|
||||
g_asyncConnect = false;
|
||||
IwxConnectAbort();
|
||||
return;
|
||||
}
|
||||
if (state == IwxConnStateId::Idle || g_iwx.State == IwxFwState::Error) {
|
||||
g_lastError = WIFI_ERR_FAILED;
|
||||
g_asyncConnect = false;
|
||||
if (state != IwxConnStateId::Idle) IwxConnectAbort();
|
||||
return;
|
||||
}
|
||||
if (now >= g_asyncConnectDeadline) {
|
||||
g_lastError = WIFI_ERR_TIMEOUT;
|
||||
g_asyncConnect = false;
|
||||
IwxConnectAbort();
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Network interface
|
||||
// =========================================================================
|
||||
|
||||
@@ -29,6 +29,14 @@ namespace Drivers::Net::Wifi {
|
||||
// Returns the number of entries written, or -1 on error.
|
||||
int Scan(montauk::abi::WifiNetwork* out, int maxCount, uint32_t timeoutMs);
|
||||
|
||||
// Start a scan and return at once: 0 started, 1 one was already running,
|
||||
// -1 no adapter. GetInfo().scanning falls back to 0 when it finishes and
|
||||
// GetInfo().scanGeneration moves on; the results are read with GetResults.
|
||||
int StartScan(uint32_t timeoutMs);
|
||||
|
||||
// Copy out the current scan table without touching the radio.
|
||||
int GetResults(montauk::abi::WifiNetwork* out, int maxCount);
|
||||
|
||||
// Fill in adapter/firmware status.
|
||||
int GetInfo(montauk::abi::WifiInfo* out);
|
||||
|
||||
@@ -36,6 +44,12 @@ namespace Drivers::Net::Wifi {
|
||||
// or the attempt fails. Returns 0 on success, or a negative WIFI_ERR_*
|
||||
// value describing why it could not connect.
|
||||
int Connect(const char* ssid, const char* password);
|
||||
|
||||
// Start a join and return at once: 0 accepted, or a WIFI_ERR_* the attempt
|
||||
// failed on before any frame went out. Progress shows up in
|
||||
// GetInfo().connState / .joining, and the outcome in .lastError.
|
||||
int ConnectAsync(const char* ssid, const char* password);
|
||||
|
||||
int Disconnect();
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
* wifi.h
|
||||
* Saved Wi-Fi networks (0:/config/wifi.toml) and small formatting helpers
|
||||
* shared by the desktop panel, the Network app and the wifi command.
|
||||
*
|
||||
* The file looks like this:
|
||||
*
|
||||
* [wifi]
|
||||
* autoconnect = true
|
||||
*
|
||||
* [network.0]
|
||||
* ssid = "Home"
|
||||
* psk = "passphrase"
|
||||
*
|
||||
* The passphrase is stored as typed, because that is what the join needs:
|
||||
* the kernel derives the PMK from it (or takes a 64-character hex string as
|
||||
* a raw PSK). Anyone who can read 0:/config can read the keys.
|
||||
*
|
||||
* No default copy of this file ships in the image, deliberately. Every other
|
||||
* config an app writes (bluetooth.toml, display.toml, session.toml) is
|
||||
* created on demand for the same reason: a shipped copy is laid down again
|
||||
* by anything that refreshes the system files, and it would overwrite the
|
||||
* networks the user had saved.
|
||||
*
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <montauk/config.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/syscall.h>
|
||||
|
||||
namespace montauk {
|
||||
namespace wifi {
|
||||
|
||||
// The scan table the kernel keeps is 64 entries; saving that many networks
|
||||
// is already far more than a laptop accumulates.
|
||||
static constexpr int MAX_SAVED = 32;
|
||||
static constexpr int SSID_CAP = 36;
|
||||
static constexpr int PSK_CAP = 72; // 64-character hex PSK plus NUL
|
||||
|
||||
struct SavedNetwork {
|
||||
char ssid[SSID_CAP];
|
||||
char psk[PSK_CAP];
|
||||
};
|
||||
|
||||
struct SavedList {
|
||||
SavedNetwork items[MAX_SAVED];
|
||||
int count;
|
||||
bool autoconnect;
|
||||
};
|
||||
|
||||
// ---- helpers -----------------------------------------------------------
|
||||
|
||||
inline void copy_str(char* dst, int cap, const char* src) {
|
||||
int i = 0;
|
||||
for (; src && src[i] && i < cap - 1; i++) dst[i] = src[i];
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
// "network.<index>.<field>"
|
||||
inline void network_key(char* out, int cap, int index, const char* field) {
|
||||
char idx[8];
|
||||
int n = 0;
|
||||
if (index == 0) {
|
||||
idx[n++] = '0';
|
||||
} else {
|
||||
char tmp[8];
|
||||
int t = 0;
|
||||
for (int v = index; v > 0 && t < (int)sizeof(tmp); v /= 10)
|
||||
tmp[t++] = (char)('0' + (v % 10));
|
||||
while (t > 0) idx[n++] = tmp[--t];
|
||||
}
|
||||
idx[n] = '\0';
|
||||
|
||||
int p = 0;
|
||||
const char* prefix = "network.";
|
||||
while (*prefix && p < cap - 1) out[p++] = *prefix++;
|
||||
for (int i = 0; i < n && p < cap - 1; i++) out[p++] = idx[i];
|
||||
if (p < cap - 1) out[p++] = '.';
|
||||
while (field && *field && p < cap - 1) out[p++] = *field++;
|
||||
out[p] = '\0';
|
||||
}
|
||||
|
||||
// ---- load / store ------------------------------------------------------
|
||||
|
||||
inline void saved_load(SavedList* out) {
|
||||
if (!out) return;
|
||||
out->count = 0;
|
||||
out->autoconnect = true;
|
||||
|
||||
auto doc = montauk::config::load("wifi");
|
||||
out->autoconnect = doc.get_bool("wifi.autoconnect", true);
|
||||
|
||||
for (int i = 0; i < MAX_SAVED; i++) {
|
||||
char key[64];
|
||||
network_key(key, sizeof(key), i, "ssid");
|
||||
const char* ssid = doc.get_string(key, nullptr);
|
||||
if (!ssid || !ssid[0]) break; // entries are written contiguously
|
||||
|
||||
network_key(key, sizeof(key), i, "psk");
|
||||
const char* psk = doc.get_string(key, "");
|
||||
|
||||
SavedNetwork& n = out->items[out->count++];
|
||||
copy_str(n.ssid, SSID_CAP, ssid);
|
||||
copy_str(n.psk, PSK_CAP, psk);
|
||||
}
|
||||
|
||||
doc.destroy();
|
||||
}
|
||||
|
||||
// Rewrite the file from the list. Returns 0 on success.
|
||||
inline int saved_store(const SavedList* list) {
|
||||
if (!list) return -1;
|
||||
|
||||
montauk::toml::Doc doc;
|
||||
doc.init();
|
||||
montauk::config::set_bool(&doc, "wifi.autoconnect", list->autoconnect);
|
||||
|
||||
for (int i = 0; i < list->count && i < MAX_SAVED; i++) {
|
||||
char key[64];
|
||||
network_key(key, sizeof(key), i, "ssid");
|
||||
montauk::config::set_string(&doc, key, list->items[i].ssid);
|
||||
network_key(key, sizeof(key), i, "psk");
|
||||
montauk::config::set_string(&doc, key, list->items[i].psk);
|
||||
}
|
||||
|
||||
int rc = montauk::config::save("wifi", &doc);
|
||||
doc.destroy();
|
||||
return rc;
|
||||
}
|
||||
|
||||
inline int saved_index_of(const SavedList* list, const char* ssid) {
|
||||
if (!list || !ssid) return -1;
|
||||
for (int i = 0; i < list->count; i++) {
|
||||
if (montauk::streq(list->items[i].ssid, ssid)) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
inline const SavedNetwork* saved_find(const SavedList* list, const char* ssid) {
|
||||
int idx = saved_index_of(list, ssid);
|
||||
return idx < 0 ? nullptr : &list->items[idx];
|
||||
}
|
||||
|
||||
// Add or update an entry in memory. Returns false when the list is full.
|
||||
inline bool saved_set(SavedList* list, const char* ssid, const char* psk) {
|
||||
if (!list || !ssid || !ssid[0]) return false;
|
||||
int idx = saved_index_of(list, ssid);
|
||||
if (idx < 0) {
|
||||
if (list->count >= MAX_SAVED) return false;
|
||||
idx = list->count++;
|
||||
copy_str(list->items[idx].ssid, SSID_CAP, ssid);
|
||||
}
|
||||
copy_str(list->items[idx].psk, PSK_CAP, psk ? psk : "");
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool saved_remove(SavedList* list, const char* ssid) {
|
||||
int idx = saved_index_of(list, ssid);
|
||||
if (idx < 0) return false;
|
||||
for (int i = idx; i < list->count - 1; i++) list->items[i] = list->items[i + 1];
|
||||
list->count--;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Convenience wrappers that touch the file directly.
|
||||
|
||||
inline bool remember(const char* ssid, const char* psk) {
|
||||
SavedList list;
|
||||
saved_load(&list);
|
||||
if (!saved_set(&list, ssid, psk)) return false;
|
||||
return saved_store(&list) == 0;
|
||||
}
|
||||
|
||||
inline bool forget(const char* ssid) {
|
||||
SavedList list;
|
||||
saved_load(&list);
|
||||
if (!saved_remove(&list, ssid)) return false;
|
||||
return saved_store(&list) == 0;
|
||||
}
|
||||
|
||||
// Copy the saved passphrase for `ssid` into out. False when not saved.
|
||||
inline bool lookup(const char* ssid, char* out, int cap) {
|
||||
SavedList list;
|
||||
saved_load(&list);
|
||||
const SavedNetwork* n = saved_find(&list, ssid);
|
||||
if (!n) return false;
|
||||
copy_str(out, cap, n->psk);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- presentation ------------------------------------------------------
|
||||
|
||||
inline const char* security_name(uint8_t security) {
|
||||
switch (security) {
|
||||
case montauk::abi::WIFI_SEC_OPEN: return "Open";
|
||||
case montauk::abi::WIFI_SEC_WEP: return "WEP";
|
||||
case montauk::abi::WIFI_SEC_WPA: return "WPA";
|
||||
case montauk::abi::WIFI_SEC_WPA2: return "WPA2";
|
||||
case montauk::abi::WIFI_SEC_WPA3: return "WPA3";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
inline bool needs_key(uint8_t security) {
|
||||
return security != montauk::abi::WIFI_SEC_OPEN;
|
||||
}
|
||||
|
||||
// 0-4 bars from an RSSI in dBm.
|
||||
inline int signal_bars(int8_t rssi) {
|
||||
if (rssi >= -55) return 4;
|
||||
if (rssi >= -67) return 3;
|
||||
if (rssi >= -75) return 2;
|
||||
if (rssi >= -85) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline const char* state_name(uint8_t state) {
|
||||
switch (state) {
|
||||
case montauk::abi::WIFI_STATE_ABSENT: return "No adapter";
|
||||
case montauk::abi::WIFI_STATE_DETECTED: return "Loading firmware";
|
||||
case montauk::abi::WIFI_STATE_BOOTING: return "Starting";
|
||||
case montauk::abi::WIFI_STATE_RUNNING: return "Ready";
|
||||
case montauk::abi::WIFI_STATE_ERROR: return "Adapter error";
|
||||
case montauk::abi::WIFI_STATE_RFKILL: return "Radio off";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// What the join is doing right now, for a progress line.
|
||||
inline const char* conn_state_name(uint32_t connState) {
|
||||
switch (connState) {
|
||||
case montauk::abi::WIFI_CONN_IDLE: return "Not connected";
|
||||
case montauk::abi::WIFI_CONN_CONTEXTS_UP: return "Preparing radio...";
|
||||
case montauk::abi::WIFI_CONN_AUTHENTICATING: return "Authenticating...";
|
||||
case montauk::abi::WIFI_CONN_AUTHENTICATED: return "Authenticated";
|
||||
case montauk::abi::WIFI_CONN_ASSOCIATING: return "Associating...";
|
||||
case montauk::abi::WIFI_CONN_ASSOCIATED: return "Associated";
|
||||
case montauk::abi::WIFI_CONN_HANDSHAKING: return "Exchanging keys...";
|
||||
case montauk::abi::WIFI_CONN_CONNECTED: return "Connected";
|
||||
case montauk::abi::WIFI_CONN_FAILED: return "Connection failed";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
inline const char* error_message(int err) {
|
||||
switch (err) {
|
||||
case 0: return "";
|
||||
case montauk::abi::WIFI_ERR_NO_ADAPTER: return "No Wi-Fi adapter is ready";
|
||||
case montauk::abi::WIFI_ERR_NOT_FOUND: return "That network is out of range";
|
||||
case montauk::abi::WIFI_ERR_NEED_KEY: return "This network needs a password";
|
||||
case montauk::abi::WIFI_ERR_UNSUPPORTED: return "This security type is not supported";
|
||||
case montauk::abi::WIFI_ERR_AUTH: return "Wrong password";
|
||||
case montauk::abi::WIFI_ERR_TIMEOUT: return "The network did not respond";
|
||||
default: return "Could not join the network";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace wifi
|
||||
} // namespace montauk
|
||||
+34
-5
@@ -7,7 +7,10 @@
|
||||
wifi info
|
||||
wifi debug
|
||||
wifi connect <ssid> [passphrase]
|
||||
wifi status
|
||||
wifi disconnect
|
||||
wifi saved
|
||||
wifi forget <ssid>
|
||||
|
||||
.SH DESCRIPTION
|
||||
Drives the Intel Wi-Fi adapter. With no arguments, runs a five
|
||||
@@ -33,13 +36,24 @@
|
||||
security tallies. Use this when a scan finds nothing.
|
||||
|
||||
connect <ssid> [passphrase]
|
||||
Brings the firmware contexts (PHY, MAC, binding, station) up
|
||||
for a network from the last scan. The 802.11 authentication
|
||||
and association exchange is not implemented, so this does not
|
||||
produce a usable link; encrypted networks are refused outright.
|
||||
Joins a network from the last scan: open, or WPA2/WPA3-PSK
|
||||
with CCMP or GCMP. Blocks until the link is up or the attempt
|
||||
fails, and says which it was. A passphrase given here is
|
||||
remembered in 0:/config/wifi.toml; given only an SSID, the
|
||||
remembered one is used. Run dhcp(1) afterwards for an address.
|
||||
|
||||
status
|
||||
The network currently joined, with signal and cipher.
|
||||
|
||||
disconnect
|
||||
Tears those contexts back down.
|
||||
Leaves the network and tears the firmware contexts down.
|
||||
|
||||
saved
|
||||
Lists the networks in 0:/config/wifi.toml and whether the
|
||||
desktop rejoins them by itself at startup.
|
||||
|
||||
forget <ssid>
|
||||
Removes a network from 0:/config/wifi.toml.
|
||||
|
||||
.SH OUTPUT
|
||||
SSID SIGNAL RSSI CH BAND SECURITY
|
||||
@@ -59,6 +73,21 @@
|
||||
yet)". The PNVM file carries regulatory data; without it the
|
||||
firmware falls back to conservative built-in limits.
|
||||
|
||||
.SH SAVED NETWORKS
|
||||
0:/config/wifi.toml holds the networks the system may rejoin
|
||||
without being asked, as [network.<n>] tables of ssid and psk, and
|
||||
a wifi.autoconnect flag. The desktop writes it when "Remember this
|
||||
network" is ticked, and reads it after the automatic scan it runs
|
||||
at startup. Passphrases are stored as typed; anyone who can read
|
||||
0:/config can read them.
|
||||
|
||||
.SH GRAPHICAL USE
|
||||
The desktop panel carries a Wi-Fi icon whenever a supported
|
||||
adapter is present, separate from the Ethernet one. Its menu lists
|
||||
the networks in range, asks for a passphrase when one is needed,
|
||||
and shows the address once the link is up. The Network app has the
|
||||
same list under its Wi-Fi tab, along with Forget.
|
||||
|
||||
.SH DIAGNOSTICS
|
||||
no supported Wi-Fi adapter found
|
||||
No matching device, or its RF type is not GF.
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -141,26 +141,49 @@ 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->eth_present) {
|
||||
ds->net_icon_rect = {next_icon_x, icon_y, 16, 16};
|
||||
next_icon_x -= 16 + 10;
|
||||
|
||||
if (ds->icon_network.pixels) {
|
||||
if (ds->cached_net_cfg.ipAddress == 0) {
|
||||
// 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;
|
||||
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;
|
||||
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(net_icon_x, net_icon_y, 16, 16, tinted);
|
||||
fb.blit_alpha(ds->net_icon_rect.x, 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);
|
||||
fb.blit_alpha(ds->net_icon_rect.x, 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);
|
||||
|
||||
@@ -0,0 +1,867 @@
|
||||
/*
|
||||
* wifi.cpp
|
||||
* Wi-Fi panel entry: the network list popup, the passphrase dialog window,
|
||||
* and the automatic scan and saved-network join that run at startup.
|
||||
*
|
||||
* Everything here goes through the non-blocking Wi-Fi syscalls
|
||||
* (wifi_scan_start / wifi_results / wifi_connect_async). The blocking pair
|
||||
* takes seconds, and the compositor cannot stand still for that long.
|
||||
*
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "desktop_internal.hpp"
|
||||
#include <montauk/wifi.h>
|
||||
#include <gui/mtk.hpp>
|
||||
|
||||
using namespace gui;
|
||||
|
||||
// ============================================================================
|
||||
// Geometry
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int WIFI_POPUP_W = 264;
|
||||
static constexpr int WIFI_ROW_H = 30;
|
||||
static constexpr int WIFI_VISIBLE = 6; // network rows on screen at once
|
||||
static constexpr int WIFI_BTN_H = 26;
|
||||
|
||||
static int wifi_popup_x(const DesktopState* ds) {
|
||||
int x = ds->wifi_icon_rect.x + ds->wifi_icon_rect.w - WIFI_POPUP_W;
|
||||
return x < 4 ? 4 : x;
|
||||
}
|
||||
|
||||
static int wifi_visible_rows(const DesktopState* ds) {
|
||||
int rows = ds->wifi_network_count;
|
||||
if (rows > WIFI_VISIBLE) rows = WIFI_VISIBLE;
|
||||
if (rows < 1) rows = 1; // the "no networks" line
|
||||
return rows;
|
||||
}
|
||||
|
||||
static int wifi_popup_h(const DesktopState* ds) {
|
||||
int fh = system_font_height();
|
||||
int header = fh + 10; // title + status
|
||||
int detail = ds->wifi_info.connected ? (fh + 6) * 3 + 8 : 0;
|
||||
int list = wifi_visible_rows(ds) * WIFI_ROW_H;
|
||||
int footer = WIFI_BTN_H + 14 + fh + 6;
|
||||
return header + 10 + detail + list + 10 + footer;
|
||||
}
|
||||
|
||||
static Rect wifi_popup_rect(const DesktopState* ds) {
|
||||
return {wifi_popup_x(ds), PANEL_HEIGHT + 2, WIFI_POPUP_W, wifi_popup_h(ds)};
|
||||
}
|
||||
|
||||
// y of the first network row
|
||||
static int wifi_list_y(const DesktopState* ds) {
|
||||
int fh = system_font_height();
|
||||
int y = PANEL_HEIGHT + 2 + 10 + fh + 10;
|
||||
if (ds->wifi_info.connected) y += (fh + 6) * 3 + 8;
|
||||
return y;
|
||||
}
|
||||
|
||||
static Rect wifi_scan_button(const DesktopState* ds) {
|
||||
Rect popup = wifi_popup_rect(ds);
|
||||
int fh = system_font_height();
|
||||
int y = popup.y + popup.h - 10 - fh - 6 - WIFI_BTN_H;
|
||||
return {popup.x + 12, y, 76, WIFI_BTN_H};
|
||||
}
|
||||
|
||||
static Rect wifi_action_button(const DesktopState* ds) {
|
||||
Rect scan = wifi_scan_button(ds);
|
||||
Rect popup = wifi_popup_rect(ds);
|
||||
int w = 96;
|
||||
return {popup.x + popup.w - 12 - w, scan.y, w, WIFI_BTN_H};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// State helpers
|
||||
// ============================================================================
|
||||
|
||||
// The saved-network list lives here rather than on the stack: it is several
|
||||
// kilobytes, and the compositor's stack is 32 KiB shared with the TrueType
|
||||
// rasteriser. One copy also saves re-parsing the file on every click.
|
||||
static montauk::wifi::SavedList g_saved;
|
||||
|
||||
static void wifi_reload_saved() {
|
||||
montauk::wifi::saved_load(&g_saved);
|
||||
}
|
||||
|
||||
static const char* wifi_saved_psk(const char* ssid) {
|
||||
const montauk::wifi::SavedNetwork* entry =
|
||||
montauk::wifi::saved_find(&g_saved, ssid);
|
||||
return (entry && entry->psk[0]) ? entry->psk : nullptr;
|
||||
}
|
||||
|
||||
static void wifi_set_status(DesktopState* ds, const char* msg) {
|
||||
montauk::strncpy(ds->wifi_status, msg ? msg : "", sizeof(ds->wifi_status));
|
||||
ds->wifi_status_time = montauk::get_milliseconds();
|
||||
}
|
||||
|
||||
// Sorted strongest-first so the list reads the way a user expects.
|
||||
static void wifi_sort_results(DesktopState* ds) {
|
||||
for (int i = 1; i < ds->wifi_network_count; i++) {
|
||||
montauk::abi::WifiNetwork key = ds->wifi_networks[i];
|
||||
int j = i - 1;
|
||||
while (j >= 0 && ds->wifi_networks[j].rssi < key.rssi) {
|
||||
ds->wifi_networks[j + 1] = ds->wifi_networks[j];
|
||||
j--;
|
||||
}
|
||||
ds->wifi_networks[j + 1] = key;
|
||||
}
|
||||
}
|
||||
|
||||
static void wifi_refresh_results(DesktopState* ds) {
|
||||
montauk::abi::WifiNetwork raw[DesktopState::MAX_WIFI_NETWORKS];
|
||||
int n = montauk::wifi_results(raw, DesktopState::MAX_WIFI_NETWORKS);
|
||||
if (n < 0) n = 0;
|
||||
|
||||
// One row per network rather than one per access point: a house with a
|
||||
// repeater answers on several BSSIDs under the same name, and the strongest
|
||||
// is the one worth joining. Hidden networks have no name to join by, so
|
||||
// they are left out of the list entirely.
|
||||
ds->wifi_network_count = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (!raw[i].ssid[0]) continue;
|
||||
|
||||
int existing = -1;
|
||||
for (int k = 0; k < ds->wifi_network_count; k++) {
|
||||
if (montauk::streq(ds->wifi_networks[k].ssid, raw[i].ssid)) {
|
||||
existing = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (existing >= 0) {
|
||||
if (raw[i].rssi > ds->wifi_networks[existing].rssi)
|
||||
ds->wifi_networks[existing] = raw[i];
|
||||
continue;
|
||||
}
|
||||
ds->wifi_networks[ds->wifi_network_count++] = raw[i];
|
||||
}
|
||||
|
||||
wifi_sort_results(ds);
|
||||
if (ds->wifi_scroll > ds->wifi_network_count - 1) ds->wifi_scroll = 0;
|
||||
}
|
||||
|
||||
static void wifi_start_scan(DesktopState* ds) {
|
||||
int rc = montauk::wifi_scan_start(6000);
|
||||
if (rc < 0) {
|
||||
wifi_set_status(ds, "The adapter is not ready yet");
|
||||
return;
|
||||
}
|
||||
ds->wifi_scanning = true;
|
||||
// No status line here: the button reads "Scanning" and the list says so
|
||||
// too, and three copies of the same word is not progress reporting.
|
||||
ds->wifi_status[0] = '\0';
|
||||
}
|
||||
|
||||
static void wifi_begin_join(DesktopState* ds, const char* ssid, const char* psk) {
|
||||
int rc = montauk::wifi_connect_async(ssid, psk);
|
||||
if (rc < 0) {
|
||||
wifi_set_status(ds, montauk::wifi::error_message(rc));
|
||||
ds->wifi_joining = false;
|
||||
return;
|
||||
}
|
||||
ds->wifi_joining = true;
|
||||
ds->wifi_dhcp_pending = true;
|
||||
montauk::strncpy(ds->wifi_joining_ssid, ssid, sizeof(ds->wifi_joining_ssid));
|
||||
|
||||
char msg[96];
|
||||
snprintf(msg, sizeof(msg), "Connecting to %s...", ssid);
|
||||
wifi_set_status(ds, msg);
|
||||
}
|
||||
|
||||
// Defined below: the passphrase dialog is a real desktop window, created the
|
||||
// same way the reboot and shutdown dialogs are.
|
||||
static void wifi_open_password_dialog(DesktopState* ds,
|
||||
const montauk::abi::WifiNetwork& net);
|
||||
|
||||
// Join the network under the cursor: straight away when it is open or its
|
||||
// passphrase is already saved, otherwise ask for the key.
|
||||
static void wifi_select_network(DesktopState* ds, int index) {
|
||||
if (index < 0 || index >= ds->wifi_network_count) return;
|
||||
const montauk::abi::WifiNetwork& net = ds->wifi_networks[index];
|
||||
|
||||
if (!montauk::wifi::needs_key(net.security)) {
|
||||
wifi_begin_join(ds, net.ssid, "");
|
||||
return;
|
||||
}
|
||||
|
||||
const char* saved = wifi_saved_psk(net.ssid);
|
||||
if (saved) {
|
||||
wifi_begin_join(ds, net.ssid, saved);
|
||||
return;
|
||||
}
|
||||
|
||||
wifi_open_password_dialog(ds, net);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Startup and polling
|
||||
// ============================================================================
|
||||
|
||||
void desktop_wifi_init(DesktopState* ds) {
|
||||
ds->wifi_popup_open = false;
|
||||
ds->wifi_icon_rect = {0, 0, 0, 0};
|
||||
ds->wifi_network_count = 0;
|
||||
ds->wifi_scroll = 0;
|
||||
ds->wifi_scanning = false;
|
||||
ds->wifi_boot_scan_started = false;
|
||||
ds->wifi_autoconnect_done = false;
|
||||
ds->wifi_joining = false;
|
||||
ds->wifi_dhcp_pending = false;
|
||||
ds->wifi_dhcp_waiting = false;
|
||||
ds->wifi_joining_ssid[0] = '\0';
|
||||
ds->wifi_status[0] = '\0';
|
||||
ds->wifi_status_time = 0;
|
||||
ds->wifi_scan_generation = 0;
|
||||
ds->wifi_last_poll = 0;
|
||||
wifi_reload_saved();
|
||||
|
||||
montauk::memset(&ds->wifi_info, 0, sizeof(ds->wifi_info));
|
||||
ds->wifi_present = montauk::wifi_info(&ds->wifi_info) == 0 && ds->wifi_info.present;
|
||||
}
|
||||
|
||||
// Try the strongest saved network that is actually in range.
|
||||
static void wifi_try_autoconnect(DesktopState* ds) {
|
||||
if (!g_saved.autoconnect || g_saved.count == 0) {
|
||||
ds->wifi_autoconnect_done = true;
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < ds->wifi_network_count; i++) { // strongest first
|
||||
const montauk::abi::WifiNetwork& net = ds->wifi_networks[i];
|
||||
const montauk::wifi::SavedNetwork* entry =
|
||||
montauk::wifi::saved_find(&g_saved, net.ssid);
|
||||
if (!entry) continue;
|
||||
wifi_begin_join(ds, entry->ssid, entry->psk);
|
||||
break;
|
||||
}
|
||||
|
||||
ds->wifi_autoconnect_done = true;
|
||||
}
|
||||
|
||||
// Called from the panel refresh. Returns true when something on screen moved.
|
||||
bool desktop_wifi_poll(DesktopState* ds, uint64_t now) {
|
||||
// The Network app writes the same file, so pick up its edits whenever the
|
||||
// popup is opened rather than trusting the copy read at startup.
|
||||
static bool popup_was_open = false;
|
||||
if (ds->wifi_popup_open && !popup_was_open) wifi_reload_saved();
|
||||
popup_was_open = ds->wifi_popup_open;
|
||||
|
||||
// The adapter finishes its firmware load well after login, so keep looking
|
||||
// for it until it shows up rather than deciding once at startup.
|
||||
if (!ds->wifi_present) {
|
||||
if (now - ds->wifi_last_poll < 3000) return false;
|
||||
ds->wifi_last_poll = now;
|
||||
montauk::abi::WifiInfo info;
|
||||
if (montauk::wifi_info(&info) != 0 || !info.present) return false;
|
||||
ds->wifi_info = info;
|
||||
ds->wifi_present = true;
|
||||
return true; // the icon appears now
|
||||
}
|
||||
|
||||
// Poll faster while something is in flight so progress text keeps up.
|
||||
bool busy = ds->wifi_scanning || ds->wifi_joining || ds->wifi_popup_open;
|
||||
if (now - ds->wifi_last_poll < (busy ? 400u : 3000u)) return false;
|
||||
ds->wifi_last_poll = now;
|
||||
|
||||
montauk::abi::WifiInfo info;
|
||||
if (montauk::wifi_info(&info) != 0) return false;
|
||||
|
||||
bool changed = info.connected != ds->wifi_info.connected
|
||||
|| info.connState != ds->wifi_info.connState
|
||||
|| info.scanning != ds->wifi_info.scanning
|
||||
|| info.scanGeneration != ds->wifi_info.scanGeneration
|
||||
|| info.state != ds->wifi_info.state;
|
||||
ds->wifi_info = info;
|
||||
|
||||
// The first scan runs by itself once the firmware is up, so the list is
|
||||
// already populated the first time the user opens the popup.
|
||||
if (!ds->wifi_boot_scan_started && info.state == montauk::abi::WIFI_STATE_RUNNING) {
|
||||
ds->wifi_boot_scan_started = true;
|
||||
wifi_start_scan(ds);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (info.scanGeneration != ds->wifi_scan_generation) {
|
||||
ds->wifi_scan_generation = info.scanGeneration;
|
||||
ds->wifi_scanning = false;
|
||||
wifi_refresh_results(ds);
|
||||
changed = true;
|
||||
|
||||
char msg[64];
|
||||
if (ds->wifi_network_count > 0) {
|
||||
snprintf(msg, sizeof(msg), "Found %d network%s",
|
||||
ds->wifi_network_count, ds->wifi_network_count == 1 ? "" : "s");
|
||||
} else {
|
||||
snprintf(msg, sizeof(msg), "No networks found");
|
||||
}
|
||||
wifi_set_status(ds, msg);
|
||||
|
||||
// Rejoin whatever was saved as soon as the first sweep lands.
|
||||
if (!ds->wifi_autoconnect_done && !info.connected && !ds->wifi_joining) {
|
||||
wifi_reload_saved();
|
||||
wifi_try_autoconnect(ds);
|
||||
}
|
||||
} else if (ds->wifi_scanning && !info.scanning) {
|
||||
ds->wifi_scanning = false;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (ds->wifi_joining) {
|
||||
if (info.connected) {
|
||||
ds->wifi_joining = false;
|
||||
char msg[96];
|
||||
snprintf(msg, sizeof(msg), "Connected to %s", info.ssid);
|
||||
wifi_set_status(ds, msg);
|
||||
changed = true;
|
||||
} else if (info.lastError != 0 && !info.joining) {
|
||||
ds->wifi_joining = false;
|
||||
ds->wifi_dhcp_pending = false;
|
||||
ds->wifi_dhcp_waiting = false;
|
||||
wifi_set_status(ds, montauk::wifi::error_message(info.lastError));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// A wireless link with no address of its own needs a lease; the wired
|
||||
// interface, if there is one, keeps whatever it already had.
|
||||
if (ds->wifi_dhcp_pending && info.connected && ds->cached_net_cfg.ipAddress == 0) {
|
||||
ds->wifi_dhcp_pending = false;
|
||||
ds->wifi_dhcp_waiting = true;
|
||||
montauk::spawn("0:/os/dhcp.elf");
|
||||
wifi_set_status(ds, "Requesting an address...");
|
||||
changed = true;
|
||||
} else if (ds->wifi_dhcp_pending && info.connected) {
|
||||
ds->wifi_dhcp_pending = false;
|
||||
}
|
||||
|
||||
// "Requesting an address..." is only true until the lease lands. Leaving it
|
||||
// up afterwards contradicts the address row further up the popup, so it is
|
||||
// replaced the moment there is an address (or the link goes away).
|
||||
if (ds->wifi_dhcp_waiting && (ds->cached_net_cfg.ipAddress != 0 || !info.connected)) {
|
||||
bool leased = ds->cached_net_cfg.ipAddress != 0;
|
||||
ds->wifi_dhcp_waiting = false;
|
||||
if (leased) {
|
||||
char msg[96];
|
||||
snprintf(msg, sizeof(msg), "Connected to %s", info.ssid);
|
||||
wifi_set_status(ds, msg);
|
||||
} else {
|
||||
ds->wifi_status[0] = '\0';
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Drawing
|
||||
// ============================================================================
|
||||
|
||||
static void draw_signal_bars(Framebuffer& fb, int x, int y, int8_t rssi, Color on, Color off) {
|
||||
int bars = montauk::wifi::signal_bars(rssi);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int h = 3 + i * 3;
|
||||
fb.fill_rect(x + i * 4, y + 12 - h, 3, h, i < bars ? on : off);
|
||||
}
|
||||
}
|
||||
|
||||
// A small padlock, so encrypted networks read at a glance.
|
||||
static void draw_lock(Framebuffer& fb, int x, int y, Color c) {
|
||||
fb.fill_rect(x + 1, y + 5, 8, 6, c);
|
||||
fb.fill_rect(x + 2, y + 2, 1, 3, c);
|
||||
fb.fill_rect(x + 7, y + 2, 1, 3, c);
|
||||
fb.fill_rect(x + 3, y + 1, 4, 1, c);
|
||||
}
|
||||
|
||||
static void draw_kv_row(Framebuffer& fb, int lx, int vx, int y,
|
||||
const char* label, const char* value, Color dim, Color text) {
|
||||
draw_text(fb, lx, y, label, dim);
|
||||
draw_text(fb, vx, y, value, text);
|
||||
}
|
||||
|
||||
void desktop_draw_wifi_popup(DesktopState* ds) {
|
||||
Framebuffer& fb = ds->fb;
|
||||
Rect popup = wifi_popup_rect(ds);
|
||||
int fh = system_font_height();
|
||||
Color dim = Color::from_rgb(0x66, 0x66, 0x66);
|
||||
Color faint = Color::from_rgb(0xCC, 0xCC, 0xCC);
|
||||
|
||||
draw_shadow(fb, popup.x, popup.y, popup.w, popup.h, 4, colors::SHADOW);
|
||||
fill_rounded_rect(fb, popup.x, popup.y, popup.w, popup.h, 8, colors::MENU_BG);
|
||||
draw_rect(fb, popup.x, popup.y, popup.w, popup.h, colors::BORDER);
|
||||
|
||||
int lx = popup.x + 14;
|
||||
int ty = popup.y + 10;
|
||||
|
||||
// Header: "Wi-Fi" + connection state
|
||||
draw_text(fb, lx, ty, "Wi-Fi", colors::TEXT_COLOR);
|
||||
|
||||
const char* status_str;
|
||||
if (ds->wifi_info.connected) status_str = "Connected";
|
||||
else if (ds->wifi_joining) status_str = "Connecting";
|
||||
else if (ds->wifi_info.state == montauk::abi::WIFI_STATE_RFKILL) status_str = "Radio off";
|
||||
else status_str = "Not connected";
|
||||
|
||||
Color dot_color = ds->wifi_info.connected
|
||||
? Color::from_rgb(0x4C, 0xAF, 0x50)
|
||||
: (ds->wifi_joining ? Color::from_rgb(0xD0, 0x9A, 0x2E)
|
||||
: Color::from_rgb(0xCC, 0x33, 0x33));
|
||||
int sw = text_width(status_str);
|
||||
int status_x = popup.x + popup.w - 14 - sw;
|
||||
fill_circle(fb, status_x - 9, ty + fh / 2, 4, dot_color);
|
||||
draw_text(fb, status_x, ty, status_str, dim);
|
||||
|
||||
ty += fh + 10;
|
||||
|
||||
// What this interface has, when it has it. The address belongs to the
|
||||
// wireless interface only while that is the one carrying traffic.
|
||||
if (ds->wifi_info.connected) {
|
||||
int vx = popup.x + 76;
|
||||
char value[40];
|
||||
|
||||
montauk::strncpy(value, ds->wifi_info.ssid[0] ? ds->wifi_info.ssid : "-", sizeof(value));
|
||||
draw_kv_row(fb, lx, vx, ty, "Network", value, dim, colors::TEXT_COLOR);
|
||||
ty += fh + 6;
|
||||
|
||||
bool wireless_active = false;
|
||||
for (int i = 0; i < ds->netif_count; i++) {
|
||||
if (ds->netifs[i].kind == montauk::abi::NETIF_KIND_WIRELESS && ds->netifs[i].active)
|
||||
wireless_active = true;
|
||||
}
|
||||
|
||||
if (wireless_active && ds->cached_net_cfg.ipAddress != 0)
|
||||
format_ip(value, ds->cached_net_cfg.ipAddress);
|
||||
else
|
||||
montauk::strcpy(value, "No address");
|
||||
draw_kv_row(fb, lx, vx, ty, "IP", value, dim, colors::TEXT_COLOR);
|
||||
ty += fh + 6;
|
||||
|
||||
snprintf(value, sizeof(value), "Channel %d", (int)ds->wifi_info.channel);
|
||||
draw_kv_row(fb, lx, vx, ty, "Radio", value, dim, colors::TEXT_COLOR);
|
||||
ty += fh + 6 + 8;
|
||||
}
|
||||
|
||||
// Network list
|
||||
int list_y = wifi_list_y(ds);
|
||||
int mx = ds->mouse.x, my = ds->mouse.y;
|
||||
|
||||
if (ds->wifi_network_count == 0) {
|
||||
const char* empty = ds->wifi_scanning ? "Looking for networks..."
|
||||
: "No networks found";
|
||||
draw_text(fb, lx, list_y + (WIFI_ROW_H - fh) / 2, empty, dim);
|
||||
}
|
||||
|
||||
int rows = wifi_visible_rows(ds);
|
||||
for (int r = 0; r < rows && (ds->wifi_scroll + r) < ds->wifi_network_count; r++) {
|
||||
int idx = ds->wifi_scroll + r;
|
||||
const montauk::abi::WifiNetwork& net = ds->wifi_networks[idx];
|
||||
Rect row = {popup.x + 6, list_y + r * WIFI_ROW_H, popup.w - 12, WIFI_ROW_H};
|
||||
|
||||
bool current = ds->wifi_info.connected
|
||||
&& montauk::streq(net.ssid, ds->wifi_info.ssid);
|
||||
if (row.contains(mx, my))
|
||||
fill_rounded_rect(fb, row.x, row.y, row.w, row.h, 4,
|
||||
gui::mtk::accent_hover_tint(ds->settings.accent_color));
|
||||
else if (current)
|
||||
fill_rounded_rect(fb, row.x, row.y, row.w, row.h, 4,
|
||||
Color::from_rgb(0xEC, 0xF2, 0xFB));
|
||||
|
||||
draw_signal_bars(fb, row.x + 8, row.y + (WIFI_ROW_H - 12) / 2, net.rssi,
|
||||
current ? ds->settings.accent_color : Color::from_rgb(0x55, 0x55, 0x55),
|
||||
faint);
|
||||
|
||||
int text_x = row.x + 32;
|
||||
int text_y = row.y + (WIFI_ROW_H - fh) / 2;
|
||||
int text_max = row.w - 32 - 26;
|
||||
|
||||
char label[40];
|
||||
montauk::strncpy(label, net.ssid[0] ? net.ssid : "(hidden)", sizeof(label));
|
||||
while (label[0] && text_width(label) > text_max) {
|
||||
int n = montauk::slen(label);
|
||||
label[n - 1] = '\0';
|
||||
}
|
||||
draw_text(fb, text_x, text_y, label, colors::TEXT_COLOR);
|
||||
|
||||
if (montauk::wifi::needs_key(net.security))
|
||||
draw_lock(fb, row.x + row.w - 20, row.y + (WIFI_ROW_H - 12) / 2, dim);
|
||||
}
|
||||
|
||||
// A slim scrollbar rather than a line of text: the list scrolls with the
|
||||
// wheel, and there is no room under it for a hint.
|
||||
if (ds->wifi_network_count > WIFI_VISIBLE) {
|
||||
int track_x = popup.x + popup.w - 8;
|
||||
int track_y = list_y + 2;
|
||||
int track_h = rows * WIFI_ROW_H - 4;
|
||||
fb.fill_rect(track_x, track_y, 3, track_h, faint);
|
||||
|
||||
int thumb_h = track_h * WIFI_VISIBLE / ds->wifi_network_count;
|
||||
if (thumb_h < 12) thumb_h = 12;
|
||||
int span = ds->wifi_network_count - WIFI_VISIBLE;
|
||||
int thumb_y = track_y + (span > 0 ? (track_h - thumb_h) * ds->wifi_scroll / span : 0);
|
||||
fb.fill_rect(track_x, thumb_y, 3, thumb_h, Color::from_rgb(0x99, 0x99, 0x99));
|
||||
}
|
||||
|
||||
// Footer: scan / disconnect and the last status line
|
||||
Rect scan = wifi_scan_button(ds);
|
||||
Rect action = wifi_action_button(ds);
|
||||
|
||||
auto draw_btn = [&](const Rect& r, const char* label, bool primary) {
|
||||
Color bg = primary ? ds->settings.accent_color : Color::from_rgb(0xE0, 0xE0, 0xE0);
|
||||
if (r.contains(mx, my))
|
||||
bg = primary ? gui::mtk::darken(ds->settings.accent_color, 32)
|
||||
: Color::from_rgb(0xD0, 0xD0, 0xD0);
|
||||
fill_rounded_rect(fb, r.x, r.y, r.w, r.h, 6, bg);
|
||||
int tw = text_width(label);
|
||||
draw_text(fb, r.x + (r.w - tw) / 2, r.y + (r.h - fh) / 2, label,
|
||||
primary ? colors::WHITE : colors::TEXT_COLOR);
|
||||
};
|
||||
|
||||
draw_btn(scan, ds->wifi_scanning ? "Scanning" : "Scan", false);
|
||||
draw_btn(action, ds->wifi_info.connected ? "Disconnect" : "Rescan",
|
||||
ds->wifi_info.connected);
|
||||
|
||||
// A result worth reading now, not one left over from ten minutes ago.
|
||||
bool status_fresh = ds->wifi_status[0]
|
||||
&& montauk::get_milliseconds() - ds->wifi_status_time < 20000;
|
||||
const char* line = status_fresh ? ds->wifi_status : "";
|
||||
if (ds->wifi_joining && ds->wifi_info.connState != montauk::abi::WIFI_CONN_IDLE)
|
||||
line = montauk::wifi::conn_state_name(ds->wifi_info.connState);
|
||||
if (line && line[0]) {
|
||||
char fitted[64];
|
||||
montauk::strncpy(fitted, line, sizeof(fitted));
|
||||
while (fitted[0] && text_width(fitted) > popup.w - 28) {
|
||||
int n = montauk::slen(fitted);
|
||||
fitted[n - 1] = '\0';
|
||||
}
|
||||
draw_text(fb, lx, scan.y + scan.h + 8, fitted, dim);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Input
|
||||
// ============================================================================
|
||||
|
||||
// Returns true when the click was consumed.
|
||||
bool desktop_wifi_handle_mouse(DesktopState* ds, int mx, int my,
|
||||
bool left_pressed, int scroll) {
|
||||
if (!ds->wifi_popup_open) return false;
|
||||
|
||||
Rect popup = wifi_popup_rect(ds);
|
||||
|
||||
if (scroll != 0 && popup.contains(mx, my)) {
|
||||
int max_scroll = ds->wifi_network_count - WIFI_VISIBLE;
|
||||
if (max_scroll < 0) max_scroll = 0;
|
||||
ds->wifi_scroll -= scroll;
|
||||
if (ds->wifi_scroll < 0) ds->wifi_scroll = 0;
|
||||
if (ds->wifi_scroll > max_scroll) ds->wifi_scroll = max_scroll;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!left_pressed) return false;
|
||||
|
||||
if (!popup.contains(mx, my)) {
|
||||
if (!ds->wifi_icon_rect.contains(mx, my)) ds->wifi_popup_open = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (wifi_scan_button(ds).contains(mx, my)) {
|
||||
if (!ds->wifi_scanning) wifi_start_scan(ds);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (wifi_action_button(ds).contains(mx, my)) {
|
||||
if (ds->wifi_info.connected) {
|
||||
montauk::wifi_disconnect();
|
||||
ds->wifi_joining = false;
|
||||
ds->wifi_dhcp_pending = false;
|
||||
ds->wifi_dhcp_waiting = false;
|
||||
wifi_set_status(ds, "Disconnected");
|
||||
} else if (!ds->wifi_scanning) {
|
||||
wifi_start_scan(ds);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int list_y = wifi_list_y(ds);
|
||||
if (my >= list_y && my < list_y + wifi_visible_rows(ds) * WIFI_ROW_H) {
|
||||
int row = (my - list_y) / WIFI_ROW_H;
|
||||
wifi_select_network(ds, ds->wifi_scroll + row);
|
||||
return true;
|
||||
}
|
||||
|
||||
return true; // a click inside the popup never falls through
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Passphrase dialog
|
||||
//
|
||||
// A real desktop window, created and driven exactly like the reboot and
|
||||
// shutdown dialogs in dialogs.cpp: desktop_create_window() plus the four
|
||||
// callbacks. It gets a title bar, dragging, a close button and a place in the
|
||||
// window list for free, and the compositor draws it in the window layer rather
|
||||
// than in the panel overlay.
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int PWD_W = 400;
|
||||
static constexpr int PWD_PAD = 20;
|
||||
static constexpr int PWD_FIELD_H = 32;
|
||||
static constexpr int PWD_BTN_W = 96;
|
||||
static constexpr int PWD_BTN_H = 32;
|
||||
|
||||
// The vertical rhythm, in one place, so the window height computed at open
|
||||
// time and the rects drawn into it cannot drift apart.
|
||||
static constexpr int PWD_TOP = 18; // content top to the heading
|
||||
static constexpr int PWD_HEAD_GAP = 6; // heading to the network line
|
||||
static constexpr int PWD_FIELD_GAP = 22; // network line to the field label
|
||||
static constexpr int PWD_CHECK_GAP = 20; // field to the first checkbox
|
||||
static constexpr int PWD_ROW_GAP = 6; // between the checkboxes
|
||||
static constexpr int PWD_BTN_GAP = 30; // last checkbox to the buttons
|
||||
|
||||
struct WifiPasswordDialog {
|
||||
DesktopState* ds;
|
||||
char ssid[DesktopState::WIFI_SSID_CAP];
|
||||
char password[DesktopState::WIFI_PSK_CAP];
|
||||
uint8_t security;
|
||||
bool reveal;
|
||||
bool remember;
|
||||
mtk::TextInputState input;
|
||||
};
|
||||
|
||||
static mtk::Theme wifi_dialog_theme(const DesktopState* ds) {
|
||||
return mtk::make_theme(ds->settings.accent_color);
|
||||
}
|
||||
|
||||
// Content-relative layout. Everything above the buttons is measured down from
|
||||
// the top and everything below them up from the bottom, so the two meet in the
|
||||
// middle wherever the window is sized.
|
||||
static int pwd_row_h() {
|
||||
return system_font_height() + 10; // a checkbox row
|
||||
}
|
||||
|
||||
static int pwd_label_y() {
|
||||
int fh = system_font_height();
|
||||
return PWD_TOP + fh + PWD_HEAD_GAP + fh + PWD_FIELD_GAP;
|
||||
}
|
||||
|
||||
static Rect pwd_field_rect(int cw, const mtk::Theme& theme) {
|
||||
return mtk::labeled_text_input_rect(PWD_PAD, pwd_label_y(), cw - PWD_PAD * 2,
|
||||
theme, PWD_FIELD_H);
|
||||
}
|
||||
|
||||
static Rect pwd_show_rect(int cw, const mtk::Theme& theme) {
|
||||
Rect field = pwd_field_rect(cw, theme);
|
||||
return {PWD_PAD, field.y + field.h + PWD_CHECK_GAP, 190, pwd_row_h()};
|
||||
}
|
||||
|
||||
static Rect pwd_remember_rect(int cw, const mtk::Theme& theme) {
|
||||
Rect show = pwd_show_rect(cw, theme);
|
||||
return {show.x, show.y + show.h + PWD_ROW_GAP, 240, show.h};
|
||||
}
|
||||
|
||||
// The height the content needs for all of that plus the button row.
|
||||
static int pwd_content_h(const mtk::Theme& theme) {
|
||||
Rect remember = pwd_remember_rect(PWD_W, theme);
|
||||
return remember.y + remember.h + PWD_BTN_GAP + PWD_BTN_H + PWD_PAD;
|
||||
}
|
||||
|
||||
static Rect pwd_join_rect(const Canvas& c) {
|
||||
return {c.w - PWD_PAD - PWD_BTN_W, c.h - PWD_PAD - PWD_BTN_H,
|
||||
PWD_BTN_W, PWD_BTN_H};
|
||||
}
|
||||
|
||||
static Rect pwd_cancel_rect(const Canvas& c) {
|
||||
Rect join = pwd_join_rect(c);
|
||||
return {join.x - 12 - PWD_BTN_W, join.y, PWD_BTN_W, PWD_BTN_H};
|
||||
}
|
||||
|
||||
static void wifi_dialog_close(WifiPasswordDialog* pd) {
|
||||
if (!pd) return;
|
||||
for (int i = 0; i < pd->ds->window_count; i++) {
|
||||
if (pd->ds->windows[i].app_data == pd) {
|
||||
desktop_close_window(pd->ds, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void wifi_dialog_submit(WifiPasswordDialog* pd) {
|
||||
if (!pd->password[0]) return;
|
||||
|
||||
// Join either way; a passphrase that cannot be written down still works
|
||||
// for this session.
|
||||
bool save_failed = false;
|
||||
if (pd->remember) {
|
||||
montauk::wifi::saved_set(&g_saved, pd->ssid, pd->password);
|
||||
save_failed = montauk::wifi::saved_store(&g_saved) != 0;
|
||||
}
|
||||
|
||||
wifi_begin_join(pd->ds, pd->ssid, pd->password);
|
||||
if (save_failed)
|
||||
wifi_set_status(pd->ds, "Connected, but the password could not be saved");
|
||||
wifi_dialog_close(pd);
|
||||
}
|
||||
|
||||
static void wifi_dialog_on_draw(Window* win, Framebuffer& fb) {
|
||||
(void)fb;
|
||||
auto* pd = (WifiPasswordDialog*)win->app_data;
|
||||
if (!pd) return;
|
||||
|
||||
Canvas c(win);
|
||||
mtk::Theme theme = wifi_dialog_theme(pd->ds);
|
||||
c.fill(theme.window_bg);
|
||||
|
||||
int fh = system_font_height();
|
||||
int mx = pd->ds->mouse.x - win->content_rect().x;
|
||||
int my = pd->ds->mouse.y - win->content_rect().y;
|
||||
|
||||
char line[96];
|
||||
snprintf(line, sizeof(line), "Enter the password for %s", pd->ssid);
|
||||
while (line[0] && text_width(line) > c.w - PWD_PAD * 2) {
|
||||
int n = montauk::slen(line);
|
||||
line[n - 1] = '\0';
|
||||
}
|
||||
c.text(PWD_PAD, PWD_TOP, line, theme.text);
|
||||
|
||||
char sub[64];
|
||||
snprintf(sub, sizeof(sub), "%s network",
|
||||
montauk::wifi::security_name(pd->security));
|
||||
c.text(PWD_PAD, PWD_TOP + fh + PWD_HEAD_GAP, sub, theme.text_subtle);
|
||||
|
||||
Rect field = pwd_field_rect(c.w, theme);
|
||||
mtk::draw_labeled_text_field(c, PWD_PAD, field.y - fh - theme.gap_xs,
|
||||
c.w - PWD_PAD * 2, "Password", pd->password,
|
||||
pd->input.cursor, true, !pd->reveal, theme,
|
||||
PWD_FIELD_H, pd->input.selection_anchor);
|
||||
|
||||
Rect show = pwd_show_rect(c.w, theme);
|
||||
mtk::draw_checkbox(c, show, "Show password",
|
||||
mtk::check_state(pd->reveal), theme, true,
|
||||
show.contains(mx, my));
|
||||
|
||||
Rect remember = pwd_remember_rect(c.w, theme);
|
||||
mtk::draw_checkbox(c, remember, "Remember this network",
|
||||
mtk::check_state(pd->remember), theme, true,
|
||||
remember.contains(mx, my));
|
||||
|
||||
Rect cancel = pwd_cancel_rect(c);
|
||||
Rect join = pwd_join_rect(c);
|
||||
mtk::draw_button(c, cancel, "Cancel", mtk::BUTTON_SECONDARY,
|
||||
mtk::widget_state(false, cancel.contains(mx, my), true), theme);
|
||||
mtk::draw_button(c, join, "Join", mtk::BUTTON_PRIMARY,
|
||||
mtk::widget_state(false, join.contains(mx, my),
|
||||
pd->password[0] != '\0'), theme);
|
||||
}
|
||||
|
||||
static void wifi_dialog_on_mouse(Window* win, MouseEvent& ev) {
|
||||
auto* pd = (WifiPasswordDialog*)win->app_data;
|
||||
if (!pd) return;
|
||||
|
||||
Canvas c(win);
|
||||
mtk::Theme theme = wifi_dialog_theme(pd->ds);
|
||||
Rect cr = win->content_rect();
|
||||
int mx = ev.x - cr.x;
|
||||
int my = ev.y - cr.y;
|
||||
|
||||
// The field owns the pointer while a drag or its context menu is running.
|
||||
Rect field = pwd_field_rect(c.w, theme);
|
||||
if (pd->input.context.open || pd->input.dragging || field.contains(mx, my)) {
|
||||
mtk::text_input_handle_mouse(pd->input, field, pd->password,
|
||||
(int)sizeof(pd->password), mx, my,
|
||||
ev.buttons, ev.prev_buttons, c.w, c.h,
|
||||
true, !pd->reveal, nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ev.left_pressed()) return;
|
||||
|
||||
if (pwd_show_rect(c.w, theme).contains(mx, my)) {
|
||||
pd->reveal = !pd->reveal;
|
||||
return;
|
||||
}
|
||||
if (pwd_remember_rect(c.w, theme).contains(mx, my)) {
|
||||
pd->remember = !pd->remember;
|
||||
return;
|
||||
}
|
||||
if (pwd_cancel_rect(c).contains(mx, my)) {
|
||||
wifi_dialog_close(pd);
|
||||
return;
|
||||
}
|
||||
if (pwd_join_rect(c).contains(mx, my)) {
|
||||
wifi_dialog_submit(pd);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void wifi_dialog_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
||||
auto* pd = (WifiPasswordDialog*)win->app_data;
|
||||
if (!pd || !key.pressed) return;
|
||||
|
||||
if (key.scancode == 0x01) { // Escape
|
||||
wifi_dialog_close(pd);
|
||||
return;
|
||||
}
|
||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
wifi_dialog_submit(pd);
|
||||
return;
|
||||
}
|
||||
mtk::text_input_key(pd->input, pd->password, (int)sizeof(pd->password),
|
||||
key, nullptr);
|
||||
}
|
||||
|
||||
static void wifi_dialog_on_close(Window* win) {
|
||||
if (!win->app_data) return;
|
||||
// Do not leave the passphrase behind in freed memory.
|
||||
auto* pd = (WifiPasswordDialog*)win->app_data;
|
||||
montauk::memset(pd, 0, sizeof(*pd));
|
||||
montauk::mfree(win->app_data);
|
||||
win->app_data = nullptr;
|
||||
}
|
||||
|
||||
static void wifi_open_password_dialog(DesktopState* ds,
|
||||
const montauk::abi::WifiNetwork& net) {
|
||||
// One dialog at a time: asking twice for the same key helps nobody.
|
||||
for (int i = 0; i < ds->window_count; i++) {
|
||||
if (ds->windows[i].on_close == wifi_dialog_on_close
|
||||
&& ds->windows[i].state != WIN_CLOSED) {
|
||||
desktop_raise_window(ds, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
char title[MAX_TITLE_LEN];
|
||||
snprintf(title, sizeof(title), "Join %s", net.ssid);
|
||||
|
||||
// Height comes from the layout above rather than a constant: at the shipped
|
||||
// UI size a fixed 250px window put the checkboxes under the buttons.
|
||||
int pwd_h = pwd_content_h(wifi_dialog_theme(ds))
|
||||
+ TITLEBAR_HEIGHT + BORDER_WIDTH;
|
||||
|
||||
int wx = (ds->screen_w - PWD_W) / 2;
|
||||
int wy = (ds->screen_h - pwd_h) / 2;
|
||||
if (wy < PANEL_HEIGHT + 8) wy = PANEL_HEIGHT + 8;
|
||||
int idx = desktop_create_window(ds, title, wx, wy, PWD_W, pwd_h);
|
||||
if (idx < 0) return;
|
||||
|
||||
auto* pd = (WifiPasswordDialog*)montauk::malloc(sizeof(WifiPasswordDialog));
|
||||
if (!pd) {
|
||||
desktop_close_window(ds, idx);
|
||||
return;
|
||||
}
|
||||
montauk::memset(pd, 0, sizeof(*pd));
|
||||
pd->ds = ds;
|
||||
montauk::strncpy(pd->ssid, net.ssid, sizeof(pd->ssid));
|
||||
pd->security = net.security;
|
||||
pd->remember = true;
|
||||
mtk::text_input_reset(pd->input, 0);
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
win->app_data = pd;
|
||||
win->on_draw = wifi_dialog_on_draw;
|
||||
win->on_mouse = wifi_dialog_on_mouse;
|
||||
win->on_key = wifi_dialog_on_key;
|
||||
win->on_close = wifi_dialog_on_close;
|
||||
|
||||
// The popup has done its job; the dialog is where the interaction is now.
|
||||
ds->wifi_popup_open = false;
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/wifi.h>
|
||||
#include <gui/mtk.hpp>
|
||||
#include <gui/mtk/settings.hpp>
|
||||
#include <gui/standalone.hpp>
|
||||
@@ -17,8 +18,8 @@ extern "C" {
|
||||
|
||||
using namespace gui;
|
||||
|
||||
static constexpr int WIN_W = 560;
|
||||
static constexpr int WIN_H = 420;
|
||||
static constexpr int WIN_W = 620;
|
||||
static constexpr int WIN_H = 480;
|
||||
static constexpr int TAB_H = 36;
|
||||
static constexpr int FOOTER_H = 44;
|
||||
static constexpr int PAD = 16;
|
||||
@@ -31,8 +32,9 @@ static constexpr int FIELD_H = 32;
|
||||
|
||||
enum Tab {
|
||||
TAB_STATUS = 0,
|
||||
TAB_CONFIG = 1,
|
||||
TAB_COUNT = 2,
|
||||
TAB_WIFI = 1,
|
||||
TAB_CONFIG = 2,
|
||||
TAB_COUNT = 3,
|
||||
};
|
||||
|
||||
struct Field {
|
||||
@@ -43,6 +45,7 @@ struct Field {
|
||||
|
||||
static const char* const kTabLabels[TAB_COUNT] = {
|
||||
"Status",
|
||||
"Wi-Fi",
|
||||
"Configure",
|
||||
};
|
||||
|
||||
@@ -59,6 +62,26 @@ static Field g_fields[FIELD_COUNT] = {
|
||||
{"Gateway", {}, {}},
|
||||
{"DNS Server", {}, {}},
|
||||
};
|
||||
|
||||
// ---- Wi-Fi tab state -------------------------------------------------------
|
||||
|
||||
static constexpr int WIFI_MAX_NETWORKS = 32;
|
||||
static constexpr int WIFI_ROW_H = 34;
|
||||
|
||||
static montauk::abi::WifiInfo g_wifi = {};
|
||||
static montauk::abi::WifiNetwork g_wifi_nets[WIFI_MAX_NETWORKS];
|
||||
static int g_wifi_count = 0;
|
||||
static int g_wifi_selected = -1;
|
||||
static int g_wifi_scroll = 0; // first visible row
|
||||
static uint32_t g_wifi_generation = 0;
|
||||
static bool g_wifi_scanning = false;
|
||||
static bool g_wifi_joining = false;
|
||||
static char g_wifi_psk[montauk::wifi::PSK_CAP] = {};
|
||||
static mtk::TextInputState g_wifi_psk_input = {};
|
||||
static bool g_wifi_psk_focus = false;
|
||||
static bool g_wifi_remember = true;
|
||||
static montauk::wifi::SavedList g_wifi_saved = {};
|
||||
|
||||
static bool g_dirty = false;
|
||||
static char g_status[128] = {};
|
||||
static uint64_t g_status_time = 0;
|
||||
@@ -246,12 +269,15 @@ static void copy_cfg_to_fields() {
|
||||
g_dirty = false;
|
||||
}
|
||||
|
||||
static void wifi_refresh();
|
||||
|
||||
static void refresh_state(bool update_fields) {
|
||||
montauk::get_netcfg(&g_cfg);
|
||||
if (montauk::net_status(&g_net) < 0) {
|
||||
montauk::memset(&g_net, 0, sizeof(g_net));
|
||||
snprintf(g_net.driver, sizeof(g_net.driver), "Unavailable");
|
||||
}
|
||||
wifi_refresh();
|
||||
if (update_fields) copy_cfg_to_fields();
|
||||
g_last_refresh = montauk::get_milliseconds();
|
||||
}
|
||||
@@ -387,6 +413,434 @@ static void draw_status_tab(Canvas& c, const mtk::Theme& theme) {
|
||||
draw_kv(c, &y, "TX Packets", tx, theme);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Wi-Fi tab
|
||||
//
|
||||
// Laid out the way the Display app lays out its mode list: borderless rows on
|
||||
// the window background, a radio for the selection, and state as right-aligned
|
||||
// text rather than a pill. The block under the list keeps a fixed height so the
|
||||
// row count does not change as the selection moves between open and encrypted
|
||||
// networks.
|
||||
// ============================================================================
|
||||
|
||||
static int wifi_status_y() {
|
||||
return TAB_H + 20;
|
||||
}
|
||||
|
||||
static int wifi_separator_y() {
|
||||
return wifi_status_y() + system_font_height() * 2 + 26;
|
||||
}
|
||||
|
||||
static int wifi_list_header_y() {
|
||||
return wifi_separator_y() + 16;
|
||||
}
|
||||
|
||||
static int wifi_list_y() {
|
||||
return wifi_list_header_y() + system_font_height() + 10;
|
||||
}
|
||||
|
||||
// The passphrase field, the checkbox under it, and the padding around them.
|
||||
static int wifi_action_h() {
|
||||
return mtk::labeled_text_height(app_theme(), FIELD_H) + 10
|
||||
+ system_font_height() + 6;
|
||||
}
|
||||
|
||||
static Rect wifi_action_rect() {
|
||||
int h = wifi_action_h();
|
||||
return {PAD, footer_rect().y - 14 - h, g_win.width - PAD * 2, h};
|
||||
}
|
||||
|
||||
static Rect wifi_psk_input_rect() {
|
||||
Rect action = wifi_action_rect();
|
||||
return mtk::labeled_text_input_rect(action.x, action.y, action.w,
|
||||
app_theme(), FIELD_H);
|
||||
}
|
||||
|
||||
static Rect wifi_remember_rect() {
|
||||
Rect action = wifi_action_rect();
|
||||
int block = mtk::labeled_text_height(app_theme(), FIELD_H);
|
||||
return {action.x, action.y + block + 10, 240, system_font_height() + 6};
|
||||
}
|
||||
|
||||
static int wifi_visible_rows() {
|
||||
int available = wifi_action_rect().y - 14 - wifi_list_y();
|
||||
int rows = available / WIFI_ROW_H;
|
||||
if (rows < 1) rows = 1;
|
||||
if (rows > WIFI_MAX_NETWORKS) rows = WIFI_MAX_NETWORKS;
|
||||
return rows;
|
||||
}
|
||||
|
||||
static Rect wifi_row_rect(int visibleIndex) {
|
||||
return {PAD, wifi_list_y() + visibleIndex * WIFI_ROW_H,
|
||||
g_win.width - PAD * 2, WIFI_ROW_H};
|
||||
}
|
||||
|
||||
static void wifi_meta_text(char* out, size_t cap,
|
||||
const montauk::abi::WifiNetwork& net) {
|
||||
snprintf(out, cap, "%s - ch %d - %d dBm",
|
||||
montauk::wifi::security_name(net.security),
|
||||
(int)net.channel, (int)net.rssi);
|
||||
}
|
||||
|
||||
static void wifi_clamp_scroll() {
|
||||
int max_scroll = g_wifi_count - wifi_visible_rows();
|
||||
if (max_scroll < 0) max_scroll = 0;
|
||||
if (g_wifi_scroll > max_scroll) g_wifi_scroll = max_scroll;
|
||||
if (g_wifi_scroll < 0) g_wifi_scroll = 0;
|
||||
}
|
||||
|
||||
static Rect wifi_forget_button() {
|
||||
Rect refresh = status_refresh_button();
|
||||
return {refresh.x - (GAP + BUTTON_W) * 2, refresh.y, BUTTON_W, BUTTON_H};
|
||||
}
|
||||
|
||||
static Rect wifi_scan_button() {
|
||||
Rect refresh = status_refresh_button();
|
||||
return {refresh.x - GAP - BUTTON_W, refresh.y, BUTTON_W, BUTTON_H};
|
||||
}
|
||||
|
||||
static Rect wifi_connect_button() {
|
||||
return status_refresh_button();
|
||||
}
|
||||
|
||||
static bool wifi_selection_valid() {
|
||||
return g_wifi_selected >= 0 && g_wifi_selected < g_wifi_count;
|
||||
}
|
||||
|
||||
static bool wifi_selection_is_current() {
|
||||
return wifi_selection_valid() && g_wifi.connected
|
||||
&& montauk::streq(g_wifi_nets[g_wifi_selected].ssid, g_wifi.ssid);
|
||||
}
|
||||
|
||||
// True when joining the selection would need a passphrase typed in: encrypted,
|
||||
// and nothing remembered for it.
|
||||
static bool wifi_selection_needs_key() {
|
||||
if (!wifi_selection_valid()) return false;
|
||||
const montauk::abi::WifiNetwork& net = g_wifi_nets[g_wifi_selected];
|
||||
return montauk::wifi::needs_key(net.security);
|
||||
}
|
||||
|
||||
static void wifi_sort() {
|
||||
for (int i = 1; i < g_wifi_count; i++) {
|
||||
montauk::abi::WifiNetwork key = g_wifi_nets[i];
|
||||
int j = i - 1;
|
||||
while (j >= 0 && g_wifi_nets[j].rssi < key.rssi) {
|
||||
g_wifi_nets[j + 1] = g_wifi_nets[j];
|
||||
j--;
|
||||
}
|
||||
g_wifi_nets[j + 1] = key;
|
||||
}
|
||||
}
|
||||
|
||||
static void wifi_load_results() {
|
||||
char previous[montauk::wifi::SSID_CAP] = {};
|
||||
if (wifi_selection_valid())
|
||||
montauk::strncpy(previous, g_wifi_nets[g_wifi_selected].ssid, sizeof(previous));
|
||||
|
||||
montauk::abi::WifiNetwork raw[WIFI_MAX_NETWORKS];
|
||||
int n = montauk::wifi_results(raw, WIFI_MAX_NETWORKS);
|
||||
if (n < 0) n = 0;
|
||||
|
||||
// One row per network, not one per access point: a house with a repeater
|
||||
// answers on several BSSIDs and the strongest is the one worth joining.
|
||||
// Hidden networks have no SSID to join by, so they are left out entirely.
|
||||
g_wifi_count = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (!raw[i].ssid[0]) continue;
|
||||
|
||||
int existing = -1;
|
||||
for (int k = 0; k < g_wifi_count; k++) {
|
||||
if (montauk::streq(g_wifi_nets[k].ssid, raw[i].ssid)) { existing = k; break; }
|
||||
}
|
||||
if (existing >= 0) {
|
||||
if (raw[i].rssi > g_wifi_nets[existing].rssi)
|
||||
g_wifi_nets[existing] = raw[i];
|
||||
continue;
|
||||
}
|
||||
g_wifi_nets[g_wifi_count++] = raw[i];
|
||||
}
|
||||
wifi_sort();
|
||||
|
||||
// Keep the selection on the network it was on, wherever it moved to.
|
||||
g_wifi_selected = -1;
|
||||
if (previous[0]) {
|
||||
for (int i = 0; i < g_wifi_count; i++) {
|
||||
if (montauk::streq(g_wifi_nets[i].ssid, previous)) { g_wifi_selected = i; break; }
|
||||
}
|
||||
}
|
||||
wifi_clamp_scroll();
|
||||
}
|
||||
|
||||
// Keeps the adapter snapshot, the scan table and the saved list in step.
|
||||
static void wifi_refresh() {
|
||||
if (montauk::wifi_info(&g_wifi) != 0) {
|
||||
montauk::memset(&g_wifi, 0, sizeof(g_wifi));
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_wifi.scanGeneration != g_wifi_generation) {
|
||||
g_wifi_generation = g_wifi.scanGeneration;
|
||||
g_wifi_scanning = false;
|
||||
wifi_load_results();
|
||||
char msg[64];
|
||||
snprintf(msg, sizeof(msg), "Found %d network%s", g_wifi_count,
|
||||
g_wifi_count == 1 ? "" : "s");
|
||||
set_status(msg);
|
||||
} else if (g_wifi_scanning && !g_wifi.scanning) {
|
||||
g_wifi_scanning = false;
|
||||
}
|
||||
|
||||
if (g_wifi_joining) {
|
||||
if (g_wifi.connected) {
|
||||
g_wifi_joining = false;
|
||||
char msg[96];
|
||||
snprintf(msg, sizeof(msg), "Connected to %s", g_wifi.ssid);
|
||||
set_status(msg);
|
||||
} else if (g_wifi.lastError != 0 && !g_wifi.joining) {
|
||||
g_wifi_joining = false;
|
||||
set_status(montauk::wifi::error_message(g_wifi.lastError));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void wifi_start_scan() {
|
||||
int rc = montauk::wifi_scan_start(6000);
|
||||
if (rc < 0) {
|
||||
set_status("The Wi-Fi adapter is not ready");
|
||||
return;
|
||||
}
|
||||
g_wifi_scanning = true;
|
||||
set_status("Scanning for networks...");
|
||||
}
|
||||
|
||||
static void wifi_join_selected() {
|
||||
if (!wifi_selection_valid()) {
|
||||
set_status("Select a network first");
|
||||
return;
|
||||
}
|
||||
|
||||
const montauk::abi::WifiNetwork& net = g_wifi_nets[g_wifi_selected];
|
||||
const char* psk = "";
|
||||
if (montauk::wifi::needs_key(net.security)) {
|
||||
if (g_wifi_psk[0]) {
|
||||
psk = g_wifi_psk;
|
||||
} else {
|
||||
const montauk::wifi::SavedNetwork* saved =
|
||||
montauk::wifi::saved_find(&g_wifi_saved, net.ssid);
|
||||
if (saved && saved->psk[0]) {
|
||||
psk = saved->psk;
|
||||
} else {
|
||||
g_wifi_psk_focus = true;
|
||||
set_status("This network needs a password");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool save_failed = false;
|
||||
if (montauk::wifi::needs_key(net.security) && g_wifi_remember && psk[0]) {
|
||||
montauk::wifi::saved_set(&g_wifi_saved, net.ssid, psk);
|
||||
save_failed = montauk::wifi::saved_store(&g_wifi_saved) != 0;
|
||||
}
|
||||
|
||||
int rc = montauk::wifi_connect_async(net.ssid, psk);
|
||||
if (rc < 0) {
|
||||
set_status(montauk::wifi::error_message(rc));
|
||||
return;
|
||||
}
|
||||
|
||||
g_wifi_joining = true;
|
||||
char msg[96];
|
||||
if (save_failed) {
|
||||
snprintf(msg, sizeof(msg),
|
||||
"Connecting to %s, but the password could not be saved", net.ssid);
|
||||
} else {
|
||||
snprintf(msg, sizeof(msg), "Connecting to %s...", net.ssid);
|
||||
}
|
||||
set_status(msg);
|
||||
}
|
||||
|
||||
static void wifi_forget_selected() {
|
||||
if (!wifi_selection_valid()) return;
|
||||
const char* ssid = g_wifi_nets[g_wifi_selected].ssid;
|
||||
if (!montauk::wifi::saved_remove(&g_wifi_saved, ssid)) {
|
||||
set_status("That network was not saved");
|
||||
return;
|
||||
}
|
||||
bool save_failed = montauk::wifi::saved_store(&g_wifi_saved) != 0;
|
||||
g_wifi_psk[0] = '\0';
|
||||
mtk::text_input_reset(g_wifi_psk_input, 0);
|
||||
|
||||
char msg[96];
|
||||
if (save_failed) {
|
||||
snprintf(msg, sizeof(msg), "Could not update the saved network list");
|
||||
} else {
|
||||
snprintf(msg, sizeof(msg), "Forgot %s", ssid);
|
||||
}
|
||||
set_status(msg);
|
||||
}
|
||||
|
||||
static void draw_signal(Canvas& c, int x, int y, int8_t rssi, Color on, Color off) {
|
||||
int bars = montauk::wifi::signal_bars(rssi);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int h = 3 + i * 3;
|
||||
c.fill_rect(x + i * 4, y + 12 - h, 3, h, i < bars ? on : off);
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_wifi_tab(Canvas& c, const mtk::Theme& theme) {
|
||||
int fh = system_font_height();
|
||||
int y = wifi_status_y();
|
||||
|
||||
bool present = g_wifi.present != 0;
|
||||
bool ready = present && g_wifi.state == montauk::abi::WIFI_STATE_RUNNING;
|
||||
|
||||
Color dot = !present ? theme.danger
|
||||
: (g_wifi.connected ? Color::from_rgb(0x27, 0xA0, 0x58)
|
||||
: Color::from_rgb(0xD0, 0x9A, 0x2E));
|
||||
fill_circle(c, PAD + 6, y + fh / 2, 6, dot);
|
||||
|
||||
char headline[128];
|
||||
if (!present) {
|
||||
snprintf(headline, sizeof(headline), "No Wi-Fi adapter");
|
||||
} else if (g_wifi.connected) {
|
||||
snprintf(headline, sizeof(headline), "Connected to %s", g_wifi.ssid);
|
||||
} else if (g_wifi_joining) {
|
||||
snprintf(headline, sizeof(headline), "%s",
|
||||
montauk::wifi::conn_state_name(g_wifi.connState));
|
||||
} else {
|
||||
snprintf(headline, sizeof(headline), "%s",
|
||||
montauk::wifi::state_name(g_wifi.state));
|
||||
}
|
||||
draw_text_fit(c, PAD + 22, y, headline, g_win.width - PAD * 2 - 22,
|
||||
present ? theme.text : theme.danger);
|
||||
|
||||
char sub[160];
|
||||
if (!present) {
|
||||
snprintf(sub, sizeof(sub),
|
||||
"Nothing to configure until a supported adapter is fitted.");
|
||||
} else if (g_wifi.connected) {
|
||||
snprintf(sub, sizeof(sub), "Channel %d - firmware %s",
|
||||
(int)g_wifi.channel,
|
||||
g_wifi.fwVersion[0] ? g_wifi.fwVersion : "unknown");
|
||||
} else {
|
||||
snprintf(sub, sizeof(sub), "Firmware %s",
|
||||
g_wifi.fwVersion[0] ? g_wifi.fwVersion : "not loaded");
|
||||
}
|
||||
draw_text_fit(c, PAD + 22, y + fh + 3, sub, g_win.width - PAD * 2 - 22,
|
||||
theme.text_subtle);
|
||||
|
||||
mtk::draw_separator(c, PAD, wifi_separator_y(), g_win.width - PAD * 2, theme);
|
||||
|
||||
// List header, with the page indicator where the Display app puts its hint.
|
||||
int header_y = wifi_list_header_y();
|
||||
c.text(PAD, header_y, "NETWORKS IN RANGE", theme.text_muted);
|
||||
|
||||
int rows = wifi_visible_rows();
|
||||
char hint[48];
|
||||
hint[0] = '\0';
|
||||
if (g_wifi_scanning) {
|
||||
snprintf(hint, sizeof(hint), "Scanning...");
|
||||
} else if (g_wifi_count > rows) {
|
||||
int last = g_wifi_scroll + rows;
|
||||
if (last > g_wifi_count) last = g_wifi_count;
|
||||
snprintf(hint, sizeof(hint), "%d-%d of %d (scroll)",
|
||||
g_wifi_scroll + 1, last, g_wifi_count);
|
||||
}
|
||||
if (hint[0])
|
||||
c.text(g_win.width - PAD - text_width(hint), header_y, hint, theme.text_muted);
|
||||
|
||||
if (g_wifi_count == 0) {
|
||||
const char* empty = !ready ? "The adapter is not ready yet."
|
||||
: (g_wifi_scanning
|
||||
? "Looking for networks..."
|
||||
: "No networks found. Press Scan to look again.");
|
||||
c.text(PAD + 4, wifi_list_y() + 9, empty, theme.text_subtle);
|
||||
}
|
||||
|
||||
// Fixed right-hand column for the row state, so the signal/channel detail
|
||||
// beside it can never be drawn over the word "Connected".
|
||||
static constexpr int STATE_W = 84;
|
||||
|
||||
// The detail column starts at one x for every row, sized from the widest
|
||||
// line in the whole list: right-aligning each row instead would make short
|
||||
// strings ("ch 1") look indented, and sizing it from the visible rows only
|
||||
// would make the column jump about while scrolling.
|
||||
int meta_w = 0;
|
||||
for (int index = 0; index < g_wifi_count; index++) {
|
||||
char meta[48];
|
||||
wifi_meta_text(meta, sizeof(meta), g_wifi_nets[index]);
|
||||
int w = text_width(meta);
|
||||
if (w > meta_w) meta_w = w;
|
||||
}
|
||||
Rect list_row = wifi_row_rect(0);
|
||||
int meta_x = list_row.x + list_row.w - STATE_W - 12 - meta_w;
|
||||
|
||||
for (int row = 0; row < rows; row++) {
|
||||
int index = g_wifi_scroll + row;
|
||||
if (index >= g_wifi_count) break;
|
||||
|
||||
const montauk::abi::WifiNetwork& net = g_wifi_nets[index];
|
||||
Rect option = wifi_row_rect(row);
|
||||
bool selected = index == g_wifi_selected;
|
||||
bool current = g_wifi.connected && montauk::streq(net.ssid, g_wifi.ssid);
|
||||
|
||||
if (selected)
|
||||
c.fill_rounded_rect(option.x, option.y + 2, option.w, option.h - 4,
|
||||
theme.radius_md, theme.accent_soft);
|
||||
else if (option.contains(g_mouse_x, g_mouse_y))
|
||||
c.fill_rounded_rect(option.x, option.y + 2, option.w, option.h - 4,
|
||||
theme.radius_md, theme.surface_hover);
|
||||
|
||||
Rect radio = {option.x + 4, option.y, 18, option.h};
|
||||
mtk::draw_radio(c, radio, "", selected, theme);
|
||||
|
||||
int text_y = option.y + (option.h - fh) / 2;
|
||||
draw_signal(c, option.x + 34, option.y + (option.h - 12) / 2, net.rssi,
|
||||
current ? theme.accent : theme.text_muted, theme.border);
|
||||
|
||||
const char* state = current ? "Connected"
|
||||
: (montauk::wifi::saved_find(&g_wifi_saved, net.ssid)
|
||||
? "Saved" : nullptr);
|
||||
if (state)
|
||||
c.text(option.x + option.w - STATE_W, text_y, state,
|
||||
current ? theme.accent : theme.text_muted);
|
||||
|
||||
char meta[48];
|
||||
wifi_meta_text(meta, sizeof(meta), net);
|
||||
c.text(meta_x, text_y, meta, theme.text_subtle);
|
||||
|
||||
int ssid_x = option.x + 60;
|
||||
draw_text_fit(c, ssid_x, text_y, net.ssid, meta_x - 12 - ssid_x, theme.text);
|
||||
}
|
||||
|
||||
// The block below the list keeps its height whatever is in it: a passphrase
|
||||
// field when one is wanted, and a line explaining why not when it is not.
|
||||
Rect action = wifi_action_rect();
|
||||
if (wifi_selection_needs_key()) {
|
||||
const montauk::wifi::SavedNetwork* saved =
|
||||
montauk::wifi::saved_find(&g_wifi_saved,
|
||||
g_wifi_nets[g_wifi_selected].ssid);
|
||||
mtk::draw_labeled_text_field(c, action.x, action.y, action.w,
|
||||
saved ? "Password (saved)" : "Password",
|
||||
g_wifi_psk, g_wifi_psk_input.cursor,
|
||||
g_wifi_psk_focus, true, theme, FIELD_H,
|
||||
g_wifi_psk_input.selection_anchor);
|
||||
|
||||
Rect remember = wifi_remember_rect();
|
||||
mtk::draw_checkbox(c, remember, "Remember this network",
|
||||
mtk::check_state(g_wifi_remember), theme, true,
|
||||
remember.contains(g_mouse_x, g_mouse_y));
|
||||
} else {
|
||||
const char* line;
|
||||
if (!wifi_selection_valid())
|
||||
line = "";
|
||||
else
|
||||
line = "Open network - no password needed.";
|
||||
if (line[0]) c.text(action.x, action.y, line, theme.text_subtle);
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_config_tab(Canvas& c, const mtk::Theme& theme) {
|
||||
int y = config_header_y();
|
||||
c.text(PAD, y, "IPv4 Configuration", theme.text);
|
||||
@@ -418,10 +872,33 @@ static void draw_footer(Canvas& c, const mtk::Theme& theme) {
|
||||
const char* msg = status_visible()
|
||||
? g_status
|
||||
: (g_dirty ? "Unsaved static IPv4 changes" : "Network settings ready");
|
||||
int text_right = (g_tab == TAB_STATUS ? status_clear_button().x : config_dhcp_button().x) - GAP;
|
||||
int text_right = PAD;
|
||||
if (g_tab == TAB_STATUS) text_right = status_clear_button().x - GAP;
|
||||
else if (g_tab == TAB_WIFI) text_right = wifi_forget_button().x - GAP;
|
||||
else text_right = config_dhcp_button().x - GAP;
|
||||
draw_text_fit(c, PAD, foot.y + (FOOTER_H - system_font_height()) / 2,
|
||||
msg, text_right - PAD, g_dirty ? theme.text : theme.text_subtle);
|
||||
|
||||
if (g_tab == TAB_WIFI) {
|
||||
Rect forget = wifi_forget_button();
|
||||
Rect scan = wifi_scan_button();
|
||||
Rect connect = wifi_connect_button();
|
||||
bool saved = wifi_selection_valid()
|
||||
&& montauk::wifi::saved_find(&g_wifi_saved,
|
||||
g_wifi_nets[g_wifi_selected].ssid) != nullptr;
|
||||
bool connected = wifi_selection_is_current() || (g_wifi.connected && !wifi_selection_valid());
|
||||
|
||||
mtk::draw_button(c, forget, "Forget", mtk::BUTTON_SECONDARY,
|
||||
button_state(forget, saved), theme);
|
||||
mtk::draw_button(c, scan, g_wifi_scanning ? "Scanning" : "Scan",
|
||||
mtk::BUTTON_SECONDARY,
|
||||
button_state(scan, !g_wifi_scanning && g_wifi.present), theme);
|
||||
mtk::draw_button(c, connect, connected ? "Disconnect" : "Connect",
|
||||
mtk::BUTTON_PRIMARY,
|
||||
button_state(connect, g_wifi.present != 0), theme);
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_tab == TAB_STATUS) {
|
||||
Rect clear = status_clear_button();
|
||||
Rect dhcp = status_dhcp_button();
|
||||
@@ -455,12 +932,20 @@ static void render() {
|
||||
|
||||
if (g_tab == TAB_STATUS) {
|
||||
draw_status_tab(c, theme);
|
||||
} else if (g_tab == TAB_WIFI) {
|
||||
draw_wifi_tab(c, theme);
|
||||
} else {
|
||||
draw_config_tab(c, theme);
|
||||
}
|
||||
draw_footer(c, theme);
|
||||
if (g_tab == TAB_CONFIG)
|
||||
draw_config_context_menus(c, theme);
|
||||
if (g_tab == TAB_WIFI) {
|
||||
mtk::draw_text_input_context_menu(
|
||||
c, g_wifi_psk_input, theme,
|
||||
mtk::text_input_has_selection(g_wifi_psk_input, g_wifi_psk,
|
||||
(int)sizeof(g_wifi_psk)));
|
||||
}
|
||||
|
||||
host.present();
|
||||
}
|
||||
@@ -523,6 +1008,19 @@ static void set_tab(Tab tab) {
|
||||
return;
|
||||
}
|
||||
g_tab = tab;
|
||||
if (tab == TAB_WIFI) {
|
||||
g_focus_field = -1;
|
||||
g_wifi_psk_focus = false;
|
||||
montauk::wifi::saved_load(&g_wifi_saved);
|
||||
wifi_refresh();
|
||||
wifi_load_results();
|
||||
// Nothing on screen yet and an adapter that can look: go and find out.
|
||||
if (g_wifi_count == 0 && !g_wifi_scanning
|
||||
&& g_wifi.state == montauk::abi::WIFI_STATE_RUNNING) {
|
||||
wifi_start_scan();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (tab == TAB_CONFIG) {
|
||||
focus_field(0);
|
||||
} else {
|
||||
@@ -546,6 +1044,80 @@ static bool handle_mouse(int mx, int my, uint8_t buttons, uint8_t prev_buttons)
|
||||
}
|
||||
}
|
||||
|
||||
if (g_tab == TAB_WIFI) {
|
||||
// The passphrase field owns the click while a drag or its context menu
|
||||
// is in progress, the same rule the IPv4 fields follow.
|
||||
if (g_wifi_psk_input.context.open || g_wifi_psk_input.dragging) {
|
||||
int result = mtk::text_input_handle_mouse(
|
||||
g_wifi_psk_input, wifi_psk_input_rect(), g_wifi_psk,
|
||||
(int)sizeof(g_wifi_psk), mx, my, buttons, prev_buttons,
|
||||
g_win.width, g_win.height, g_wifi_psk_focus, true, nullptr);
|
||||
return result != mtk::TEXT_INPUT_NONE;
|
||||
}
|
||||
|
||||
if (!left_pressed && !right_pressed) return false;
|
||||
|
||||
if (wifi_selection_needs_key() && wifi_psk_input_rect().contains(mx, my)) {
|
||||
g_wifi_psk_focus = true;
|
||||
mtk::text_input_handle_mouse(g_wifi_psk_input, wifi_psk_input_rect(),
|
||||
g_wifi_psk, (int)sizeof(g_wifi_psk),
|
||||
mx, my, buttons, prev_buttons,
|
||||
g_win.width, g_win.height, true, true, nullptr);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!left_pressed) return false;
|
||||
|
||||
int rows = wifi_visible_rows();
|
||||
for (int row = 0; row < rows; row++) {
|
||||
int idx = g_wifi_scroll + row;
|
||||
if (idx >= g_wifi_count) break;
|
||||
if (!wifi_row_rect(row).contains(mx, my)) continue;
|
||||
|
||||
if (idx != g_wifi_selected) {
|
||||
g_wifi_selected = idx;
|
||||
// A different network means a different key; do not carry the
|
||||
// previous one over, but do offer the remembered one.
|
||||
g_wifi_psk[0] = '\0';
|
||||
mtk::text_input_reset(g_wifi_psk_input, 0);
|
||||
const montauk::wifi::SavedNetwork* saved =
|
||||
montauk::wifi::saved_find(&g_wifi_saved, g_wifi_nets[idx].ssid);
|
||||
if (saved) {
|
||||
montauk::strncpy(g_wifi_psk, saved->psk, sizeof(g_wifi_psk));
|
||||
mtk::text_input_reset(g_wifi_psk_input, str_len(g_wifi_psk));
|
||||
}
|
||||
}
|
||||
g_wifi_psk_focus = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (wifi_selection_needs_key() && wifi_remember_rect().contains(mx, my)) {
|
||||
g_wifi_remember = !g_wifi_remember;
|
||||
return true;
|
||||
}
|
||||
if (wifi_scan_button().contains(mx, my)) {
|
||||
if (!g_wifi_scanning) wifi_start_scan();
|
||||
return true;
|
||||
}
|
||||
if (wifi_forget_button().contains(mx, my)) {
|
||||
wifi_forget_selected();
|
||||
return true;
|
||||
}
|
||||
if (wifi_connect_button().contains(mx, my)) {
|
||||
if (wifi_selection_is_current() || (g_wifi.connected && !wifi_selection_valid())) {
|
||||
montauk::wifi_disconnect();
|
||||
g_wifi_joining = false;
|
||||
set_status("Disconnected");
|
||||
} else {
|
||||
wifi_join_selected();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
g_wifi_psk_focus = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (g_tab == TAB_STATUS) {
|
||||
if (!left_pressed) return false;
|
||||
if (status_refresh_button().contains(mx, my)) {
|
||||
@@ -631,6 +1203,42 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (g_tab == TAB_WIFI) {
|
||||
if (key.ascii == '\t') {
|
||||
set_tab(TAB_CONFIG);
|
||||
return true;
|
||||
}
|
||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
wifi_join_selected();
|
||||
return true;
|
||||
}
|
||||
if (key.scancode == 0x48 || key.scancode == 0x50) { // up / down
|
||||
int step = key.scancode == 0x48 ? -1 : 1;
|
||||
int next = g_wifi_selected < 0 ? 0 : g_wifi_selected + step;
|
||||
if (next < 0) next = 0;
|
||||
if (next >= g_wifi_count) next = g_wifi_count - 1;
|
||||
g_wifi_selected = next;
|
||||
if (g_wifi_selected >= 0) {
|
||||
if (g_wifi_selected < g_wifi_scroll)
|
||||
g_wifi_scroll = g_wifi_selected;
|
||||
if (g_wifi_selected >= g_wifi_scroll + wifi_visible_rows())
|
||||
g_wifi_scroll = g_wifi_selected - wifi_visible_rows() + 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (g_wifi_psk_focus && wifi_selection_needs_key()) {
|
||||
int result = mtk::text_input_key(g_wifi_psk_input, g_wifi_psk,
|
||||
(int)sizeof(g_wifi_psk), key, nullptr);
|
||||
return (result & mtk::TEXT_INPUT_CONSUMED) != 0;
|
||||
}
|
||||
if (key.ascii == 's' || key.ascii == 'S') {
|
||||
if (!g_wifi_scanning) wifi_start_scan();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (key.ascii == '\t') {
|
||||
if (g_tab != TAB_CONFIG) {
|
||||
set_tab(TAB_CONFIG);
|
||||
@@ -690,7 +1298,9 @@ extern "C" void _start() {
|
||||
montauk::exit(1);
|
||||
}
|
||||
|
||||
montauk::wifi::saved_load(&g_wifi_saved);
|
||||
refresh_state(true);
|
||||
wifi_load_results();
|
||||
render();
|
||||
|
||||
while (g_win.id >= 0 && !g_win.closed) {
|
||||
@@ -701,7 +1311,10 @@ extern "C" void _start() {
|
||||
if (r < 0) break;
|
||||
if (r == 0) {
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
if (now - g_last_refresh >= 3000) {
|
||||
// The Wi-Fi tab has work in flight worth watching closely; the rest
|
||||
// of the app is happy with the slower cadence.
|
||||
bool wifi_busy = g_tab == TAB_WIFI && (g_wifi_scanning || g_wifi_joining);
|
||||
if (now - g_last_refresh >= (wifi_busy ? 400u : 3000u)) {
|
||||
refresh_state(!g_dirty);
|
||||
redraw = true;
|
||||
}
|
||||
@@ -718,6 +1331,12 @@ extern "C" void _start() {
|
||||
g_mouse_x = ev.mouse.x;
|
||||
g_mouse_y = ev.mouse.y;
|
||||
redraw = true;
|
||||
if (ev.mouse.scroll != 0 && g_tab == TAB_WIFI
|
||||
&& ev.mouse.y >= wifi_list_y()
|
||||
&& ev.mouse.y < wifi_action_rect().y) {
|
||||
g_wifi_scroll += ev.mouse.scroll > 0 ? -1 : 1;
|
||||
wifi_clamp_scroll();
|
||||
}
|
||||
if (handle_mouse(ev.mouse.x, ev.mouse.y,
|
||||
ev.mouse.buttons, ev.mouse.prev_buttons)) {
|
||||
redraw = true;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/wifi.h>
|
||||
|
||||
using namespace montauk;
|
||||
|
||||
@@ -363,14 +364,30 @@ static int cmd_debug() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_connect(const char* ssid, const char* password) {
|
||||
static int cmd_connect(const char* ssid, const char* password, bool remember) {
|
||||
abi::WifiInfo info;
|
||||
if (!require_adapter(info)) return 1;
|
||||
|
||||
// No passphrase on the command line: fall back to the one the desktop or a
|
||||
// previous run saved, so "wifi connect Home" works on its own.
|
||||
char saved[montauk::wifi::PSK_CAP];
|
||||
if ((!password || !password[0])
|
||||
&& montauk::wifi::lookup(ssid, saved, sizeof(saved)) && saved[0]) {
|
||||
print("Using the saved passphrase for \""); print(ssid); print("\".\n");
|
||||
password = saved;
|
||||
remember = false; // already stored
|
||||
}
|
||||
|
||||
print("Connecting to \""); print(ssid); print("\"...\n");
|
||||
int rc = wifi_connect(ssid, password);
|
||||
|
||||
if (rc == 0) {
|
||||
if (remember && password && password[0]) {
|
||||
if (montauk::wifi::remember(ssid, password))
|
||||
print("Saved to 0:/config/wifi.toml for next time.\n");
|
||||
else
|
||||
print("wifi: could not write 0:/config/wifi.toml.\n");
|
||||
}
|
||||
print("Connected.\n");
|
||||
print("Run \"dhcp\" to get an address, then the network is usable\n");
|
||||
print("just like a wired connection.\n");
|
||||
@@ -410,6 +427,36 @@ static int cmd_connect(const char* ssid, const char* password) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int cmd_saved() {
|
||||
montauk::wifi::SavedList list;
|
||||
montauk::wifi::saved_load(&list);
|
||||
|
||||
if (list.count == 0) {
|
||||
print("No saved networks.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
print("Saved networks (0:/config/wifi.toml):\n");
|
||||
for (int i = 0; i < list.count; i++) {
|
||||
print(" ");
|
||||
print(list.items[i].ssid);
|
||||
print("\n");
|
||||
}
|
||||
print("\nAutomatic reconnect is ");
|
||||
print(list.autoconnect ? "on" : "off");
|
||||
print(".\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_forget(const char* ssid) {
|
||||
if (montauk::wifi::forget(ssid)) {
|
||||
print("Forgot \""); print(ssid); print("\".\n");
|
||||
return 0;
|
||||
}
|
||||
print("wifi: \""); print(ssid); print("\" was not saved.\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void usage() {
|
||||
print("usage: wifi [command]\n\n");
|
||||
print(" scan [seconds] scan and list nearby networks (default 5)\n");
|
||||
@@ -417,7 +464,11 @@ static void usage() {
|
||||
print(" debug diagnostics plus per-BSS scan detail\n");
|
||||
print(" connect <ssid> [key] join a network (open or WPA2/WPA3-PSK)\n");
|
||||
print(" status show the network currently joined\n");
|
||||
print(" disconnect leave the current network\n\n");
|
||||
print(" disconnect leave the current network\n");
|
||||
print(" saved list remembered networks\n");
|
||||
print(" forget <ssid> remove a remembered network\n\n");
|
||||
print("A passphrase given to connect is remembered, and one that was\n");
|
||||
print("remembered is used when connect is given only an SSID.\n\n");
|
||||
print("With no command, runs a 5 second scan.\n");
|
||||
}
|
||||
|
||||
@@ -454,7 +505,16 @@ extern "C" void _start() {
|
||||
while (p[n] && n < (int)sizeof(pass) - 1) { pass[n] = p[n]; n++; }
|
||||
while (n > 0 && pass[n - 1] == ' ') n--;
|
||||
pass[n] = '\0';
|
||||
exit(cmd_connect(ssid, pass));
|
||||
exit(cmd_connect(ssid, pass, true));
|
||||
} else if (streq(cmd, "saved")) {
|
||||
exit(cmd_saved());
|
||||
} else if (streq(cmd, "forget")) {
|
||||
char ssid[40];
|
||||
if (!next_token(&rest, ssid, sizeof(ssid))) {
|
||||
print("wifi: forget needs an SSID\n");
|
||||
exit(1);
|
||||
}
|
||||
exit(cmd_forget(ssid));
|
||||
} else if (streq(cmd, "disconnect")) {
|
||||
wifi_disconnect();
|
||||
print("Disconnected.\n");
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
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 ----
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -9,18 +9,88 @@ extern "C" {
|
||||
|
||||
extern int errno;
|
||||
|
||||
/* 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 EINVAL 22
|
||||
#define ERANGE 34
|
||||
#define ENOSYS 38
|
||||
#define EISDIR 21
|
||||
#define ENOTDIR 20
|
||||
#define EFAULT 14
|
||||
#define EBUSY 16
|
||||
#define EEXIST 17
|
||||
#define EBADF 9
|
||||
#define EPERM 1
|
||||
#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
|
||||
}
|
||||
|
||||
@@ -10,10 +10,24 @@ extern "C" {
|
||||
#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
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#ifndef _LIBC_MEMORY_H
|
||||
#define _LIBC_MEMORY_H
|
||||
|
||||
#pragma once
|
||||
|
||||
/* Traditional alias for string.h. */
|
||||
#include <string.h>
|
||||
|
||||
#endif /* _LIBC_MEMORY_H */
|
||||
@@ -19,10 +19,10 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ====================================================================
|
||||
Syscall numbers
|
||||
==================================================================== */
|
||||
|
||||
/* @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
|
||||
@@ -44,12 +44,13 @@ extern "C" {
|
||||
#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_SETUNIXTIME 153
|
||||
#define MTK_SYS_GETTIME 28
|
||||
#define MTK_SYS_SOCKET 29
|
||||
#define MTK_SYS_CONNECT 30
|
||||
@@ -74,35 +75,52 @@ extern "C" {
|
||||
#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_WINUNMAP 97
|
||||
#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_WINSETFLAGS 126
|
||||
#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_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_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
|
||||
@@ -119,6 +137,59 @@ extern "C" {
|
||||
#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);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -10,7 +12,26 @@ extern "C" {
|
||||
typedef int sig_atomic_t;
|
||||
typedef void (*sighandler_t)(int);
|
||||
|
||||
/* 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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
#ifndef _LIBC_SPAWN_H
|
||||
#define _LIBC_SPAWN_H
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*
|
||||
* posix_spawn for MontaukOS, layered on SYS_SPAWN.
|
||||
*
|
||||
* Limitations (kernel spawn model):
|
||||
* - argv is joined into a single args string; arguments containing
|
||||
* spaces are rejected with EINVAL (no quoting in the kernel).
|
||||
* - envp is ignored (no environment transfer on spawn).
|
||||
* - file actions must be empty: stdio redirection needs kernel
|
||||
* support that does not exist yet, so any recorded action makes
|
||||
* posix_spawn fail with ENOTSUP rather than misbehave silently.
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
short flags;
|
||||
} posix_spawnattr_t;
|
||||
|
||||
typedef struct {
|
||||
int action_count;
|
||||
} posix_spawn_file_actions_t;
|
||||
|
||||
int posix_spawnattr_init(posix_spawnattr_t *attr);
|
||||
int posix_spawnattr_destroy(posix_spawnattr_t *attr);
|
||||
int posix_spawnattr_setflags(posix_spawnattr_t *attr, short flags);
|
||||
int posix_spawnattr_getflags(const posix_spawnattr_t *attr, short *flags);
|
||||
|
||||
int posix_spawn_file_actions_init(posix_spawn_file_actions_t *actions);
|
||||
int posix_spawn_file_actions_destroy(posix_spawn_file_actions_t *actions);
|
||||
int posix_spawn_file_actions_adddup2(posix_spawn_file_actions_t *actions,
|
||||
int fd, int newfd);
|
||||
int posix_spawn_file_actions_addclose(posix_spawn_file_actions_t *actions,
|
||||
int fd);
|
||||
int posix_spawn_file_actions_addopen(posix_spawn_file_actions_t *actions,
|
||||
int fd, const char *path, int oflag,
|
||||
mode_t mode);
|
||||
|
||||
int posix_spawn(pid_t *pid, const char *path,
|
||||
const posix_spawn_file_actions_t *actions,
|
||||
const posix_spawnattr_t *attr,
|
||||
char *const argv[], char *const envp[]);
|
||||
int posix_spawnp(pid_t *pid, const char *file,
|
||||
const posix_spawn_file_actions_t *actions,
|
||||
const posix_spawnattr_t *attr,
|
||||
char *const argv[], char *const envp[]);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* _LIBC_SPAWN_H */
|
||||
@@ -1,6 +1,12 @@
|
||||
#ifndef _LIBC_STDIO_H
|
||||
#define _LIBC_STDIO_H
|
||||
|
||||
/* Conventional guard marker: packages like GMP sniff the libc's stdio
|
||||
include-guard name to detect that FILE is available. */
|
||||
#ifndef _STDIO_H
|
||||
#define _STDIO_H 1
|
||||
#endif
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdarg.h>
|
||||
@@ -36,6 +42,21 @@ extern FILE *stdin;
|
||||
extern FILE *stdout;
|
||||
extern FILE *stderr;
|
||||
|
||||
int putc(int c, FILE *stream);
|
||||
void rewind(FILE *stream);
|
||||
int getchar(void);
|
||||
typedef long fpos_t;
|
||||
|
||||
int fgetpos(FILE *stream, fpos_t *pos);
|
||||
int fsetpos(FILE *stream, const fpos_t *pos);
|
||||
void setbuf(FILE *stream, char *buf);
|
||||
int fscanf(FILE *stream, const char *fmt, ...);
|
||||
int scanf(const char *fmt, ...);
|
||||
int vfscanf(FILE *stream, const char *fmt, va_list ap);
|
||||
|
||||
int fileno(FILE *stream);
|
||||
FILE *fdopen(int fd, const char *mode);
|
||||
|
||||
int printf(const char *fmt, ...);
|
||||
int fprintf(FILE *stream, const char *fmt, ...);
|
||||
int sprintf(char *str, const char *fmt, ...);
|
||||
|
||||
@@ -50,9 +50,23 @@ int putenv(char *string);
|
||||
|
||||
void qsort(void *base, size_t nmemb, size_t size,
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#ifndef _LIBC_SYS_MMAN_H
|
||||
#define _LIBC_SYS_MMAN_H
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Anonymous-memory mmap over SYS_ALLOC. SYS_ALLOC returns
|
||||
* page-aligned process memory, which is exactly what callers like
|
||||
* GCC's page allocator need. File-backed mappings are not
|
||||
* supported and fail with ENODEV.
|
||||
*/
|
||||
|
||||
#define PROT_NONE 0
|
||||
#define PROT_READ 1
|
||||
#define PROT_WRITE 2
|
||||
#define PROT_EXEC 4
|
||||
|
||||
#define MAP_SHARED 0x01
|
||||
#define MAP_PRIVATE 0x02
|
||||
#define MAP_FIXED 0x10
|
||||
#define MAP_ANONYMOUS 0x20
|
||||
#define MAP_ANON MAP_ANONYMOUS
|
||||
|
||||
#define MAP_FAILED ((void *)-1)
|
||||
|
||||
void *mmap(void *addr, size_t length, int prot, int flags, int fd,
|
||||
long offset);
|
||||
int munmap(void *addr, size_t length);
|
||||
int mprotect(void *addr, size_t length, int prot);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* _LIBC_SYS_MMAN_H */
|
||||
@@ -0,0 +1,23 @@
|
||||
#ifndef _LIBC_SYS_PARAM_H
|
||||
#define _LIBC_SYS_PARAM_H
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <limits.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#ifndef MAXPATHLEN
|
||||
#define MAXPATHLEN 4096
|
||||
#endif
|
||||
|
||||
#ifndef MIN
|
||||
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
|
||||
#endif
|
||||
#ifndef MAX
|
||||
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
|
||||
#endif
|
||||
|
||||
#define howmany(x, y) (((x) + ((y) - 1)) / (y))
|
||||
#define roundup(x, y) ((((x) + ((y) - 1)) / (y)) * (y))
|
||||
|
||||
#endif /* _LIBC_SYS_PARAM_H */
|
||||
@@ -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_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 {
|
||||
dev_t st_dev;
|
||||
ino_t st_ino;
|
||||
mode_t st_mode;
|
||||
unsigned long st_size;
|
||||
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 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
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
/* Fixed-width and pointer-sized integer types. Freestanding builds get
|
||||
this from the kernel freestanding headers; the cross toolchain from
|
||||
the GCC-provided stdint.h. Hosted code (e.g. libgcov) expects
|
||||
intptr_t to be visible via the stdio/stdlib include chain. */
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -15,6 +21,13 @@ typedef unsigned int mode_t;
|
||||
typedef unsigned int uid_t;
|
||||
typedef unsigned int gid_t;
|
||||
typedef long time_t;
|
||||
typedef unsigned long ino_t;
|
||||
typedef unsigned long dev_t;
|
||||
typedef unsigned long nlink_t;
|
||||
typedef long blkcnt_t;
|
||||
typedef long blksize_t;
|
||||
typedef long suseconds_t;
|
||||
typedef int clockid_t;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#ifndef _LIBC_SYS_WAIT_H
|
||||
#define _LIBC_SYS_WAIT_H
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* POSIX status decoding. waitpid() encodes a normal exit as code<<8
|
||||
and a killed/crashed child as the signal number in the low bits. */
|
||||
#define WIFEXITED(s) (((s) & 0x7F) == 0)
|
||||
#define WEXITSTATUS(s) (((s) >> 8) & 0xFF)
|
||||
#define WIFSIGNALED(s) (((s) & 0x7F) != 0)
|
||||
#define WTERMSIG(s) ((s) & 0x7F)
|
||||
#define WIFSTOPPED(s) (0)
|
||||
#define WSTOPSIG(s) (0)
|
||||
|
||||
#define WNOHANG 1
|
||||
#define WUNTRACED 2
|
||||
|
||||
pid_t wait(int *status);
|
||||
pid_t waitpid(pid_t pid, int *status, int options);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* _LIBC_SYS_WAIT_H */
|
||||
@@ -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
|
||||
|
||||
@@ -4,11 +4,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define STDIN_FILENO 0
|
||||
#define STDOUT_FILENO 1
|
||||
#define STDERR_FILENO 2
|
||||
|
||||
#define F_OK 0
|
||||
#define X_OK 1
|
||||
#define W_OK 2
|
||||
@@ -22,6 +27,36 @@ int chdir(const char *path);
|
||||
char *getcwd(char *buf, size_t size);
|
||||
int access(const char *path, int mode);
|
||||
int isatty(int fd);
|
||||
int unlink(const char *path);
|
||||
int rmdir(const char *path);
|
||||
int dup(int fd);
|
||||
int dup2(int oldfd, int newfd);
|
||||
pid_t getpid(void);
|
||||
void _exit(int status) __attribute__((noreturn));
|
||||
pid_t fork(void);
|
||||
int execv(const char *path, char *const argv[]);
|
||||
int execve(const char *path, char *const argv[], char *const envp[]);
|
||||
int execvp(const char *file, char *const argv[]);
|
||||
int pipe(int fds[2]);
|
||||
long pathconf(const char *path, int name);
|
||||
int getpagesize(void);
|
||||
long sysconf(int name);
|
||||
|
||||
/* sysconf() names (Linux numbering). */
|
||||
#define _SC_OPEN_MAX 4
|
||||
#define _SC_PAGESIZE 30
|
||||
#define _SC_PAGE_SIZE 30
|
||||
#define _SC_NPROCESSORS_ONLN 84
|
||||
|
||||
/* pathconf() names (Linux numbering). */
|
||||
#define _PC_LINK_MAX 0
|
||||
#define _PC_MAX_CANON 1
|
||||
#define _PC_MAX_INPUT 2
|
||||
#define _PC_NAME_MAX 3
|
||||
#define _PC_PATH_MAX 4
|
||||
#define _PC_PIPE_BUF 5
|
||||
|
||||
extern char **environ;
|
||||
|
||||
unsigned int sleep(unsigned int seconds);
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#ifndef _LIBC_UTIME_H
|
||||
#define _LIBC_UTIME_H
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct utimbuf {
|
||||
time_t actime;
|
||||
time_t modtime;
|
||||
};
|
||||
|
||||
/* Accepted for POSIX compatibility; the Montauk VFS does not support
|
||||
setting file times. */
|
||||
int utime(const char *path, const struct utimbuf *times);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* _LIBC_UTIME_H */
|
||||
@@ -0,0 +1,29 @@
|
||||
#ifndef _LIBC_WCHAR_H
|
||||
#define _LIBC_WCHAR_H
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef unsigned int wint_t;
|
||||
|
||||
/* Opaque shift state for the (byte-oriented) C locale. */
|
||||
typedef struct {
|
||||
unsigned long __opaque;
|
||||
} mbstate_t;
|
||||
|
||||
#define WEOF ((wint_t)-1)
|
||||
|
||||
/* Byte-oriented C locale only: one byte, one character. */
|
||||
size_t mbstowcs(wchar_t *dst, const char *src, size_t n);
|
||||
int mblen(const char *s, size_t n);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* _LIBC_WCHAR_H */
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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); }
|
||||
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
* wifi.h
|
||||
* Saved Wi-Fi networks (0:/config/wifi.toml) and small formatting helpers
|
||||
* shared by the desktop panel, the Network app and the wifi command.
|
||||
*
|
||||
* The file looks like this:
|
||||
*
|
||||
* [wifi]
|
||||
* autoconnect = true
|
||||
*
|
||||
* [network.0]
|
||||
* ssid = "Home"
|
||||
* psk = "passphrase"
|
||||
*
|
||||
* The passphrase is stored as typed, because that is what the join needs:
|
||||
* the kernel derives the PMK from it (or takes a 64-character hex string as
|
||||
* a raw PSK). Anyone who can read 0:/config can read the keys.
|
||||
*
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <montauk/config.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/syscall.h>
|
||||
|
||||
namespace montauk {
|
||||
namespace wifi {
|
||||
|
||||
// The scan table the kernel keeps is 64 entries; saving that many networks
|
||||
// is already far more than a laptop accumulates.
|
||||
static constexpr int MAX_SAVED = 32;
|
||||
static constexpr int SSID_CAP = 36;
|
||||
static constexpr int PSK_CAP = 72; // 64-character hex PSK plus NUL
|
||||
|
||||
struct SavedNetwork {
|
||||
char ssid[SSID_CAP];
|
||||
char psk[PSK_CAP];
|
||||
};
|
||||
|
||||
struct SavedList {
|
||||
SavedNetwork items[MAX_SAVED];
|
||||
int count;
|
||||
bool autoconnect;
|
||||
};
|
||||
|
||||
// ---- helpers -----------------------------------------------------------
|
||||
|
||||
inline void copy_str(char* dst, int cap, const char* src) {
|
||||
int i = 0;
|
||||
for (; src && src[i] && i < cap - 1; i++) dst[i] = src[i];
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
// "network.<index>.<field>"
|
||||
inline void network_key(char* out, int cap, int index, const char* field) {
|
||||
char idx[8];
|
||||
int n = 0;
|
||||
if (index == 0) {
|
||||
idx[n++] = '0';
|
||||
} else {
|
||||
char tmp[8];
|
||||
int t = 0;
|
||||
for (int v = index; v > 0 && t < (int)sizeof(tmp); v /= 10)
|
||||
tmp[t++] = (char)('0' + (v % 10));
|
||||
while (t > 0) idx[n++] = tmp[--t];
|
||||
}
|
||||
idx[n] = '\0';
|
||||
|
||||
int p = 0;
|
||||
const char* prefix = "network.";
|
||||
while (*prefix && p < cap - 1) out[p++] = *prefix++;
|
||||
for (int i = 0; i < n && p < cap - 1; i++) out[p++] = idx[i];
|
||||
if (p < cap - 1) out[p++] = '.';
|
||||
while (field && *field && p < cap - 1) out[p++] = *field++;
|
||||
out[p] = '\0';
|
||||
}
|
||||
|
||||
// ---- load / store ------------------------------------------------------
|
||||
|
||||
inline void saved_load(SavedList* out) {
|
||||
if (!out) return;
|
||||
out->count = 0;
|
||||
out->autoconnect = true;
|
||||
|
||||
auto doc = montauk::config::load("wifi");
|
||||
out->autoconnect = doc.get_bool("wifi.autoconnect", true);
|
||||
|
||||
for (int i = 0; i < MAX_SAVED; i++) {
|
||||
char key[64];
|
||||
network_key(key, sizeof(key), i, "ssid");
|
||||
const char* ssid = doc.get_string(key, nullptr);
|
||||
if (!ssid || !ssid[0]) break; // entries are written contiguously
|
||||
|
||||
network_key(key, sizeof(key), i, "psk");
|
||||
const char* psk = doc.get_string(key, "");
|
||||
|
||||
SavedNetwork& n = out->items[out->count++];
|
||||
copy_str(n.ssid, SSID_CAP, ssid);
|
||||
copy_str(n.psk, PSK_CAP, psk);
|
||||
}
|
||||
|
||||
doc.destroy();
|
||||
}
|
||||
|
||||
// Rewrite the file from the list. Returns 0 on success.
|
||||
inline int saved_store(const SavedList* list) {
|
||||
if (!list) return -1;
|
||||
|
||||
montauk::toml::Doc doc;
|
||||
doc.init();
|
||||
montauk::config::set_bool(&doc, "wifi.autoconnect", list->autoconnect);
|
||||
|
||||
for (int i = 0; i < list->count && i < MAX_SAVED; i++) {
|
||||
char key[64];
|
||||
network_key(key, sizeof(key), i, "ssid");
|
||||
montauk::config::set_string(&doc, key, list->items[i].ssid);
|
||||
network_key(key, sizeof(key), i, "psk");
|
||||
montauk::config::set_string(&doc, key, list->items[i].psk);
|
||||
}
|
||||
|
||||
int rc = montauk::config::save("wifi", &doc);
|
||||
doc.destroy();
|
||||
return rc;
|
||||
}
|
||||
|
||||
inline int saved_index_of(const SavedList* list, const char* ssid) {
|
||||
if (!list || !ssid) return -1;
|
||||
for (int i = 0; i < list->count; i++) {
|
||||
if (montauk::streq(list->items[i].ssid, ssid)) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
inline const SavedNetwork* saved_find(const SavedList* list, const char* ssid) {
|
||||
int idx = saved_index_of(list, ssid);
|
||||
return idx < 0 ? nullptr : &list->items[idx];
|
||||
}
|
||||
|
||||
// Add or update an entry in memory. Returns false when the list is full.
|
||||
inline bool saved_set(SavedList* list, const char* ssid, const char* psk) {
|
||||
if (!list || !ssid || !ssid[0]) return false;
|
||||
int idx = saved_index_of(list, ssid);
|
||||
if (idx < 0) {
|
||||
if (list->count >= MAX_SAVED) return false;
|
||||
idx = list->count++;
|
||||
copy_str(list->items[idx].ssid, SSID_CAP, ssid);
|
||||
}
|
||||
copy_str(list->items[idx].psk, PSK_CAP, psk ? psk : "");
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool saved_remove(SavedList* list, const char* ssid) {
|
||||
int idx = saved_index_of(list, ssid);
|
||||
if (idx < 0) return false;
|
||||
for (int i = idx; i < list->count - 1; i++) list->items[i] = list->items[i + 1];
|
||||
list->count--;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Convenience wrappers that touch the file directly.
|
||||
|
||||
inline bool remember(const char* ssid, const char* psk) {
|
||||
SavedList list;
|
||||
saved_load(&list);
|
||||
if (!saved_set(&list, ssid, psk)) return false;
|
||||
return saved_store(&list) == 0;
|
||||
}
|
||||
|
||||
inline bool forget(const char* ssid) {
|
||||
SavedList list;
|
||||
saved_load(&list);
|
||||
if (!saved_remove(&list, ssid)) return false;
|
||||
return saved_store(&list) == 0;
|
||||
}
|
||||
|
||||
// Copy the saved passphrase for `ssid` into out. False when not saved.
|
||||
inline bool lookup(const char* ssid, char* out, int cap) {
|
||||
SavedList list;
|
||||
saved_load(&list);
|
||||
const SavedNetwork* n = saved_find(&list, ssid);
|
||||
if (!n) return false;
|
||||
copy_str(out, cap, n->psk);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- presentation ------------------------------------------------------
|
||||
|
||||
inline const char* security_name(uint8_t security) {
|
||||
switch (security) {
|
||||
case montauk::abi::WIFI_SEC_OPEN: return "Open";
|
||||
case montauk::abi::WIFI_SEC_WEP: return "WEP";
|
||||
case montauk::abi::WIFI_SEC_WPA: return "WPA";
|
||||
case montauk::abi::WIFI_SEC_WPA2: return "WPA2";
|
||||
case montauk::abi::WIFI_SEC_WPA3: return "WPA3";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
inline bool needs_key(uint8_t security) {
|
||||
return security != montauk::abi::WIFI_SEC_OPEN;
|
||||
}
|
||||
|
||||
// 0-4 bars from an RSSI in dBm.
|
||||
inline int signal_bars(int8_t rssi) {
|
||||
if (rssi >= -55) return 4;
|
||||
if (rssi >= -67) return 3;
|
||||
if (rssi >= -75) return 2;
|
||||
if (rssi >= -85) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline const char* state_name(uint8_t state) {
|
||||
switch (state) {
|
||||
case montauk::abi::WIFI_STATE_ABSENT: return "No adapter";
|
||||
case montauk::abi::WIFI_STATE_DETECTED: return "Loading firmware";
|
||||
case montauk::abi::WIFI_STATE_BOOTING: return "Starting";
|
||||
case montauk::abi::WIFI_STATE_RUNNING: return "Ready";
|
||||
case montauk::abi::WIFI_STATE_ERROR: return "Adapter error";
|
||||
case montauk::abi::WIFI_STATE_RFKILL: return "Radio off";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// What the join is doing right now, for a progress line.
|
||||
inline const char* conn_state_name(uint32_t connState) {
|
||||
switch (connState) {
|
||||
case montauk::abi::WIFI_CONN_IDLE: return "Not connected";
|
||||
case montauk::abi::WIFI_CONN_CONTEXTS_UP: return "Preparing radio...";
|
||||
case montauk::abi::WIFI_CONN_AUTHENTICATING: return "Authenticating...";
|
||||
case montauk::abi::WIFI_CONN_AUTHENTICATED: return "Authenticated";
|
||||
case montauk::abi::WIFI_CONN_ASSOCIATING: return "Associating...";
|
||||
case montauk::abi::WIFI_CONN_ASSOCIATED: return "Associated";
|
||||
case montauk::abi::WIFI_CONN_HANDSHAKING: return "Exchanging keys...";
|
||||
case montauk::abi::WIFI_CONN_CONNECTED: return "Connected";
|
||||
case montauk::abi::WIFI_CONN_FAILED: return "Connection failed";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
inline const char* error_message(int err) {
|
||||
switch (err) {
|
||||
case 0: return "";
|
||||
case montauk::abi::WIFI_ERR_NO_ADAPTER: return "No Wi-Fi adapter is ready";
|
||||
case montauk::abi::WIFI_ERR_NOT_FOUND: return "That network is out of range";
|
||||
case montauk::abi::WIFI_ERR_NEED_KEY: return "This network needs a password";
|
||||
case montauk::abi::WIFI_ERR_UNSUPPORTED: return "This security type is not supported";
|
||||
case montauk::abi::WIFI_ERR_AUTH: return "Wrong password";
|
||||
case montauk::abi::WIFI_ERR_TIMEOUT: return "The network did not respond";
|
||||
default: return "Could not join the network";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace wifi
|
||||
} // namespace montauk
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -633,6 +633,64 @@ check(link_rm is not None and mac_rm is not None and link_rm < mac_rm,
|
||||
"the link is removed before the MAC that owns it")
|
||||
d7.p.stdin.close()
|
||||
|
||||
# =============================================================================
|
||||
# An access point that stops acknowledging brings the link down.
|
||||
#
|
||||
# IwxLinkUp() used to be nothing but "the state machine reached Connected", and
|
||||
# only an explicit deauthentication frame moved it off that state. A hotspot
|
||||
# that simply went away -- slept, changed channel, dropped the station without
|
||||
# saying so -- therefore left the link reported as up forever: NetIf kept
|
||||
# choosing wlan0, every packet vanished, and the desktop showed a healthy
|
||||
# connection while nothing worked. Beacons cannot be used to notice this
|
||||
# (MacConfigCmd stops asking for them once associated), so the driver watches
|
||||
# its own frames going unacknowledged instead.
|
||||
# =============================================================================
|
||||
|
||||
print("\n=== a silent access point takes the link down ===")
|
||||
d8 = Driver()
|
||||
d8.cmd("MAC " + STA.hex())
|
||||
d8.cmd(f"CONNECT {AP.hex()} {CHANNEL} 0 OpenNet - -")
|
||||
d8.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('<HHH', 0, 2, 0)).hex())
|
||||
d8.cmd("SERVICE")
|
||||
d8.cmd("RXMGMT " + mgmt(0x10, STA, AP, AP,
|
||||
struct.pack('<HHH', 0x0421, 0, 7 | 0xc000)).hex())
|
||||
ev = d8.cmd("SERVICE")
|
||||
check(link_of(ev['result']) == 1, "the open network is associated and the link is up")
|
||||
|
||||
# Well short of the threshold: a few unacknowledged frames are ordinary.
|
||||
ev = d8.cmd("TXSTATUS 0 15")
|
||||
check(link_of(ev['result']) == 1,
|
||||
"a handful of unacknowledged frames does not drop the link")
|
||||
ev = d8.cmd("SERVICE")
|
||||
check(link_of(ev['result']) == 1, "and the service pass leaves it alone")
|
||||
|
||||
# One acknowledgement means the access point is still there; the count restarts.
|
||||
d8.cmd("TXSTATUS 1 1")
|
||||
ev = d8.cmd("TXSTATUS 0 15")
|
||||
check(link_of(ev['result']) == 1,
|
||||
"an acknowledgement in between restarts the count")
|
||||
|
||||
# Anything received from the BSS is equally good proof, and also restarts it.
|
||||
d8.cmd("RXDATA " + data_from_ds(STA, AP, AP, 0x0800, b'\x45' * 20).hex())
|
||||
ev = d8.cmd("TXSTATUS 0 15")
|
||||
check(link_of(ev['result']) == 1,
|
||||
"a frame received from the BSS restarts the count too")
|
||||
|
||||
# Now let it run past the threshold with nothing coming back.
|
||||
ev = d8.cmd("TXSTATUS 0 16")
|
||||
check(link_of(ev['result']) == 1,
|
||||
"the transmit path itself does not tear anything down")
|
||||
check(not ev['CMD'],
|
||||
"no firmware command is sent from the completion path")
|
||||
|
||||
ev = d8.cmd("SERVICE")
|
||||
check(state_of(ev['result']) == 0 and link_of(ev['result']) == 0,
|
||||
"the next service pass drops the link")
|
||||
check(any(c == MAC_CONFIG and struct.unpack_from('<I', p, 4)[0] == 3
|
||||
for c, p in ev['CMD']),
|
||||
"and unwinds the firmware contexts")
|
||||
d8.p.stdin.close()
|
||||
|
||||
print()
|
||||
if fails:
|
||||
for f in fails: print("FAILURE:", f)
|
||||
|
||||
@@ -196,6 +196,15 @@ int main() {
|
||||
int n = unhex(line.substr(6), buf);
|
||||
bool ok = IwxConnectSendEthernet(buf, (uint32_t)n);
|
||||
printf("TXETH-OK %d\n", ok ? 1 : 0);
|
||||
} else if (line.rfind("TXSTATUS ", 0) == 0) {
|
||||
// TXSTATUS <acked> <count> -- report N transmit outcomes, the way
|
||||
// IwxTxComplete does off the firmware's TX response. Drives the
|
||||
// link supervision that notices an access point which stopped
|
||||
// acknowledging without ever deauthenticating.
|
||||
unsigned acked, count;
|
||||
sscanf(line.c_str(), "TXSTATUS %u %u", &acked, &count);
|
||||
for (unsigned i = 0; i < count; i++) IwxConnectNoteTx(acked != 0);
|
||||
printf("DONE STATE %d LINK %d\n", IwxConnectState(), IwxLinkUp() ? 1 : 0);
|
||||
} else if (line.rfind("SERVICE", 0) == 0) {
|
||||
IwxConnectService();
|
||||
printf("DONE STATE %d LINK %d\n", IwxConnectState(), IwxLinkUp() ? 1 : 0);
|
||||
|
||||
Reference in New Issue
Block a user