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

This commit is contained in:
2026-08-07 10:36:53 +02:00
parent bbe1df62fd
commit 9eb21eb3e3
67 changed files with 4573 additions and 245 deletions
+24
View File
@@ -224,6 +224,10 @@ namespace montauk::abi {
static constexpr uint64_t SYS_WIFI_INFO = 159; // (WifiInfo*) -> 0, -1 if absent
static constexpr uint64_t SYS_WIFI_CONNECT = 160; // (ssid, password) -> 0, <0 on error
static constexpr uint64_t SYS_WIFI_DISCONNECT = 161; // () -> 0
static constexpr uint64_t SYS_WIFI_SCAN_START = 162; // (timeoutMs) -> 0 started, 1 busy, -1 no adapter
static constexpr uint64_t SYS_WIFI_RESULTS = 163; // (WifiNetwork*, maxCount) -> count, no radio work
static constexpr uint64_t SYS_WIFI_CONNECT_ASYNC = 164; // (ssid, password) -> 0 accepted, <0 on error
static constexpr uint64_t SYS_NETIFS = 165; // (NetIfInfo*, maxCount) -> count
// Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM).
static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz
@@ -633,6 +637,26 @@ namespace montauk::abi {
uint8_t bssid[6];
uint8_t connected; // 1 once the link can carry IP traffic
uint8_t channel;
int32_t lastError; // WIFI_ERR_* from the last async join, 0 = none
uint32_t scanGeneration; // bumped every time a scan finishes
uint8_t joining; // 1 while an async join is in flight
uint8_t _pad[3];
};
// Link-layer interface kinds reported in NetIfInfo.kind.
static constexpr uint8_t NETIF_KIND_ETHERNET = 0;
static constexpr uint8_t NETIF_KIND_WIRELESS = 1;
// One registered link-layer interface (returned by SYS_NETIFS). The IP
// configuration is global to the stack, so it belongs to whichever
// interface reports active = 1.
struct NetIfInfo {
char name[16]; // "eth0", "wlan0"
uint8_t mac[6];
uint8_t kind; // NETIF_KIND_*
uint8_t linkUp;
uint8_t active; // 1 if this is the interface carrying traffic
uint8_t _pad[3];
};
struct ThermalInfo {
+35
View File
@@ -180,6 +180,41 @@ struct DesktopState {
uint64_t net_cfg_last_poll;
Rect net_icon_rect;
// Registered link-layer interfaces. The IP configuration is global to the
// stack, so the wired and wireless popups use `active` to decide which of
// them owns the address currently on screen.
static constexpr int MAX_NETIFS = 4;
montauk::abi::NetIfInfo netifs[MAX_NETIFS];
int netif_count;
bool eth_present;
// ---- Wi-Fi -------------------------------------------------------------
// The panel entry exists only when a supported adapter is present.
static constexpr int MAX_WIFI_NETWORKS = 32;
static constexpr int WIFI_SSID_CAP = 36;
static constexpr int WIFI_PSK_CAP = 72;
SvgIcon icon_wifi;
bool wifi_present;
bool wifi_popup_open;
Rect wifi_icon_rect;
montauk::abi::WifiInfo wifi_info;
montauk::abi::WifiNetwork wifi_networks[MAX_WIFI_NETWORKS];
int wifi_network_count;
uint64_t wifi_last_poll;
uint32_t wifi_scan_generation;
bool wifi_scanning;
int wifi_scroll; // first visible row of the list
bool wifi_boot_scan_started; // the automatic scan at startup
bool wifi_autoconnect_done; // saved-network join already tried
bool wifi_joining; // a join we started is in flight
bool wifi_dhcp_pending; // ask for a lease once the link is up
bool wifi_dhcp_waiting; // dhcp.elf running, no address yet
char wifi_joining_ssid[WIFI_SSID_CAP];
char wifi_status[96]; // last result line in the popup
uint64_t wifi_status_time;
bool vol_popup_open;
Rect vol_icon_rect;
int vol_level; // 0-100
+4
View File
@@ -185,6 +185,10 @@ extern "C" {
#define MTK_SYS_WIFI_INFO 159
#define MTK_SYS_WIFI_CONNECT 160
#define MTK_SYS_WIFI_DISCONNECT 161
#define MTK_SYS_WIFI_SCAN_START 162
#define MTK_SYS_WIFI_RESULTS 163
#define MTK_SYS_WIFI_CONNECT_ASYNC 164
#define MTK_SYS_NETIFS 165
/* @SYSCALLS-END */
#define MTK_SOCK_TCP 1
+29 -2
View File
@@ -607,8 +607,8 @@ namespace montauk {
inline int wifi_info(montauk::abi::WifiInfo* out) {
return (int)syscall1(montauk::abi::SYS_WIFI_INFO, (uint64_t)out);
}
// Association is groundwork only: open networks bring the firmware
// contexts up, encrypted ones are rejected with -2.
// Join a network. Blocks until the link is up or the attempt fails, and
// returns 0 or a WIFI_ERR_* code.
inline int wifi_connect(const char* ssid, const char* password) {
return (int)syscall2(montauk::abi::SYS_WIFI_CONNECT, (uint64_t)ssid,
(uint64_t)password);
@@ -617,6 +617,33 @@ namespace montauk {
return (int)syscall0(montauk::abi::SYS_WIFI_DISCONNECT);
}
// Non-blocking pair for GUI code, which cannot stall for the seconds a
// sweep or a handshake takes. scan_start() kicks off a sweep (0 started,
// 1 one was already running, -1 no adapter); wifi_info().scanning drops
// back to 0 and .scanGeneration moves on when it finishes, and
// wifi_results() copies out the table without touching the radio.
inline int wifi_scan_start(uint32_t timeoutMs) {
return (int)syscall1(montauk::abi::SYS_WIFI_SCAN_START, (uint64_t)timeoutMs);
}
inline int wifi_results(montauk::abi::WifiNetwork* buf, int maxCount) {
return (int)syscall2(montauk::abi::SYS_WIFI_RESULTS, (uint64_t)buf,
(uint64_t)maxCount);
}
// Returns 0 once the join is under way, or a WIFI_ERR_* it failed on
// before any frame went out. Watch wifi_info().joining for progress and
// .lastError for the outcome.
inline int wifi_connect_async(const char* ssid, const char* password) {
return (int)syscall2(montauk::abi::SYS_WIFI_CONNECT_ASYNC, (uint64_t)ssid,
(uint64_t)password);
}
// List the registered link-layer interfaces. The IP configuration is
// global to the stack; it belongs to whichever entry has active = 1.
inline int net_interfaces(montauk::abi::NetIfInfo* buf, int maxCount) {
return (int)syscall2(montauk::abi::SYS_NETIFS, (uint64_t)buf,
(uint64_t)maxCount);
}
// Software-defined radio (Rx). Receivers are identified by index [0, count);
// open() returns a handle used by the rest of the calls. Samples are read
// as interleaved 8-bit unsigned I/Q (CU8) from the device's ring buffer.
+261
View File
@@ -0,0 +1,261 @@
/*
* wifi.h
* Saved Wi-Fi networks (0:/config/wifi.toml) and small formatting helpers
* shared by the desktop panel, the Network app and the wifi command.
*
* The file looks like this:
*
* [wifi]
* autoconnect = true
*
* [network.0]
* ssid = "Home"
* psk = "passphrase"
*
* The passphrase is stored as typed, because that is what the join needs:
* the kernel derives the PMK from it (or takes a 64-character hex string as
* a raw PSK). Anyone who can read 0:/config can read the keys.
*
* No default copy of this file ships in the image, deliberately. Every other
* config an app writes (bluetooth.toml, display.toml, session.toml) is
* created on demand for the same reason: a shipped copy is laid down again
* by anything that refreshes the system files, and it would overwrite the
* networks the user had saved.
*
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <montauk/config.h>
#include <montauk/string.h>
#include <montauk/syscall.h>
namespace montauk {
namespace wifi {
// The scan table the kernel keeps is 64 entries; saving that many networks
// is already far more than a laptop accumulates.
static constexpr int MAX_SAVED = 32;
static constexpr int SSID_CAP = 36;
static constexpr int PSK_CAP = 72; // 64-character hex PSK plus NUL
struct SavedNetwork {
char ssid[SSID_CAP];
char psk[PSK_CAP];
};
struct SavedList {
SavedNetwork items[MAX_SAVED];
int count;
bool autoconnect;
};
// ---- helpers -----------------------------------------------------------
inline void copy_str(char* dst, int cap, const char* src) {
int i = 0;
for (; src && src[i] && i < cap - 1; i++) dst[i] = src[i];
dst[i] = '\0';
}
// "network.<index>.<field>"
inline void network_key(char* out, int cap, int index, const char* field) {
char idx[8];
int n = 0;
if (index == 0) {
idx[n++] = '0';
} else {
char tmp[8];
int t = 0;
for (int v = index; v > 0 && t < (int)sizeof(tmp); v /= 10)
tmp[t++] = (char)('0' + (v % 10));
while (t > 0) idx[n++] = tmp[--t];
}
idx[n] = '\0';
int p = 0;
const char* prefix = "network.";
while (*prefix && p < cap - 1) out[p++] = *prefix++;
for (int i = 0; i < n && p < cap - 1; i++) out[p++] = idx[i];
if (p < cap - 1) out[p++] = '.';
while (field && *field && p < cap - 1) out[p++] = *field++;
out[p] = '\0';
}
// ---- load / store ------------------------------------------------------
inline void saved_load(SavedList* out) {
if (!out) return;
out->count = 0;
out->autoconnect = true;
auto doc = montauk::config::load("wifi");
out->autoconnect = doc.get_bool("wifi.autoconnect", true);
for (int i = 0; i < MAX_SAVED; i++) {
char key[64];
network_key(key, sizeof(key), i, "ssid");
const char* ssid = doc.get_string(key, nullptr);
if (!ssid || !ssid[0]) break; // entries are written contiguously
network_key(key, sizeof(key), i, "psk");
const char* psk = doc.get_string(key, "");
SavedNetwork& n = out->items[out->count++];
copy_str(n.ssid, SSID_CAP, ssid);
copy_str(n.psk, PSK_CAP, psk);
}
doc.destroy();
}
// Rewrite the file from the list. Returns 0 on success.
inline int saved_store(const SavedList* list) {
if (!list) return -1;
montauk::toml::Doc doc;
doc.init();
montauk::config::set_bool(&doc, "wifi.autoconnect", list->autoconnect);
for (int i = 0; i < list->count && i < MAX_SAVED; i++) {
char key[64];
network_key(key, sizeof(key), i, "ssid");
montauk::config::set_string(&doc, key, list->items[i].ssid);
network_key(key, sizeof(key), i, "psk");
montauk::config::set_string(&doc, key, list->items[i].psk);
}
int rc = montauk::config::save("wifi", &doc);
doc.destroy();
return rc;
}
inline int saved_index_of(const SavedList* list, const char* ssid) {
if (!list || !ssid) return -1;
for (int i = 0; i < list->count; i++) {
if (montauk::streq(list->items[i].ssid, ssid)) return i;
}
return -1;
}
inline const SavedNetwork* saved_find(const SavedList* list, const char* ssid) {
int idx = saved_index_of(list, ssid);
return idx < 0 ? nullptr : &list->items[idx];
}
// Add or update an entry in memory. Returns false when the list is full.
inline bool saved_set(SavedList* list, const char* ssid, const char* psk) {
if (!list || !ssid || !ssid[0]) return false;
int idx = saved_index_of(list, ssid);
if (idx < 0) {
if (list->count >= MAX_SAVED) return false;
idx = list->count++;
copy_str(list->items[idx].ssid, SSID_CAP, ssid);
}
copy_str(list->items[idx].psk, PSK_CAP, psk ? psk : "");
return true;
}
inline bool saved_remove(SavedList* list, const char* ssid) {
int idx = saved_index_of(list, ssid);
if (idx < 0) return false;
for (int i = idx; i < list->count - 1; i++) list->items[i] = list->items[i + 1];
list->count--;
return true;
}
// Convenience wrappers that touch the file directly.
inline bool remember(const char* ssid, const char* psk) {
SavedList list;
saved_load(&list);
if (!saved_set(&list, ssid, psk)) return false;
return saved_store(&list) == 0;
}
inline bool forget(const char* ssid) {
SavedList list;
saved_load(&list);
if (!saved_remove(&list, ssid)) return false;
return saved_store(&list) == 0;
}
// Copy the saved passphrase for `ssid` into out. False when not saved.
inline bool lookup(const char* ssid, char* out, int cap) {
SavedList list;
saved_load(&list);
const SavedNetwork* n = saved_find(&list, ssid);
if (!n) return false;
copy_str(out, cap, n->psk);
return true;
}
// ---- presentation ------------------------------------------------------
inline const char* security_name(uint8_t security) {
switch (security) {
case montauk::abi::WIFI_SEC_OPEN: return "Open";
case montauk::abi::WIFI_SEC_WEP: return "WEP";
case montauk::abi::WIFI_SEC_WPA: return "WPA";
case montauk::abi::WIFI_SEC_WPA2: return "WPA2";
case montauk::abi::WIFI_SEC_WPA3: return "WPA3";
default: return "Unknown";
}
}
inline bool needs_key(uint8_t security) {
return security != montauk::abi::WIFI_SEC_OPEN;
}
// 0-4 bars from an RSSI in dBm.
inline int signal_bars(int8_t rssi) {
if (rssi >= -55) return 4;
if (rssi >= -67) return 3;
if (rssi >= -75) return 2;
if (rssi >= -85) return 1;
return 0;
}
inline const char* state_name(uint8_t state) {
switch (state) {
case montauk::abi::WIFI_STATE_ABSENT: return "No adapter";
case montauk::abi::WIFI_STATE_DETECTED: return "Loading firmware";
case montauk::abi::WIFI_STATE_BOOTING: return "Starting";
case montauk::abi::WIFI_STATE_RUNNING: return "Ready";
case montauk::abi::WIFI_STATE_ERROR: return "Adapter error";
case montauk::abi::WIFI_STATE_RFKILL: return "Radio off";
default: return "Unknown";
}
}
// What the join is doing right now, for a progress line.
inline const char* conn_state_name(uint32_t connState) {
switch (connState) {
case montauk::abi::WIFI_CONN_IDLE: return "Not connected";
case montauk::abi::WIFI_CONN_CONTEXTS_UP: return "Preparing radio...";
case montauk::abi::WIFI_CONN_AUTHENTICATING: return "Authenticating...";
case montauk::abi::WIFI_CONN_AUTHENTICATED: return "Authenticated";
case montauk::abi::WIFI_CONN_ASSOCIATING: return "Associating...";
case montauk::abi::WIFI_CONN_ASSOCIATED: return "Associated";
case montauk::abi::WIFI_CONN_HANDSHAKING: return "Exchanging keys...";
case montauk::abi::WIFI_CONN_CONNECTED: return "Connected";
case montauk::abi::WIFI_CONN_FAILED: return "Connection failed";
default: return "";
}
}
inline const char* error_message(int err) {
switch (err) {
case 0: return "";
case montauk::abi::WIFI_ERR_NO_ADAPTER: return "No Wi-Fi adapter is ready";
case montauk::abi::WIFI_ERR_NOT_FOUND: return "That network is out of range";
case montauk::abi::WIFI_ERR_NEED_KEY: return "This network needs a password";
case montauk::abi::WIFI_ERR_UNSUPPORTED: return "This security type is not supported";
case montauk::abi::WIFI_ERR_AUTH: return "Wrong password";
case montauk::abi::WIFI_ERR_TIMEOUT: return "The network did not respond";
default: return "Could not join the network";
}
}
} // namespace wifi
} // namespace montauk