diff --git a/kernel/src/Api/BluetoothSyscall.hpp b/kernel/src/Api/BluetoothSyscall.hpp index b9f28a9..59c2b1a 100644 --- a/kernel/src/Api/BluetoothSyscall.hpp +++ b/kernel/src/Api/BluetoothSyscall.hpp @@ -56,6 +56,40 @@ namespace Montauk { return (int64_t)Drivers::USB::Bluetooth::Disconnect(bdAddr); } + static int64_t Sys_BtSetAddr(const uint8_t* bdAddr) { + if (!bdAddr) return -1; + if (!Drivers::USB::Bluetooth::IsInitialized()) return -1; + + // bdAddr is a 6-byte buffer, addr[0] = least-significant octet. + return Drivers::USB::Bluetooth::SetAddress(bdAddr) ? 0 : -1; + } + + static int64_t Sys_BtBonds(BtBondInfo* buf, int maxCount) { + if (!buf || maxCount <= 0) return 0; + if (!Drivers::USB::Bluetooth::IsInitialized()) return 0; + + Drivers::USB::Bluetooth::Hci::BondInfo tmpBuf[8]; + int kMax = maxCount; + if (kMax > 8) kMax = 8; + + int count = Drivers::USB::Bluetooth::ListBonded(tmpBuf, kMax); + + for (int i = 0; i < count; i++) { + memcpy(buf[i].bdAddr, tmpBuf[i].Addr, 6); + buf[i]._pad[0] = 0; + buf[i]._pad[1] = 0; + } + + return (int64_t)count; + } + + static int64_t Sys_BtForget(const uint8_t* bdAddr) { + if (!bdAddr) return -1; + if (!Drivers::USB::Bluetooth::IsInitialized()) return -1; + + return (int64_t)Drivers::USB::Bluetooth::ForgetDevice(bdAddr); + } + static int64_t Sys_BtList(BtDevInfo* buf, int maxCount) { if (!buf || maxCount <= 0) return 0; if (!Drivers::USB::Bluetooth::IsInitialized()) return 0; diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 694c0f8..0163127 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 22 +#define MONTAUK_BUILD_NUMBER 24 diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index 72e7efb..4ae7f65 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -377,6 +377,16 @@ namespace Montauk { case SYS_BTDISCONNECT: if (!UserMemory::Range(frame->arg1, 6, false)) return -1; return Sys_BtDisconnect((const uint8_t*)frame->arg1); + case SYS_BTSETADDR: + if (!UserMemory::Range(frame->arg1, 6, false)) return -1; + return Sys_BtSetAddr((const uint8_t*)frame->arg1); + case SYS_BTBONDS: + if ((int64_t)frame->arg2 < 0) return -1; + if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtBondInfo), true)) return -1; + return Sys_BtBonds((BtBondInfo*)frame->arg1, (int)frame->arg2); + case SYS_BTFORGET: + if (!UserMemory::Range(frame->arg1, 6, false)) return -1; + return Sys_BtForget((const uint8_t*)frame->arg1); case SYS_BTLIST: if ((int64_t)frame->arg2 < 0) return -1; if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtDevInfo), true)) return -1; diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index ec8d527..f42a023 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -258,6 +258,13 @@ namespace Montauk { /* Filesystem.hpp -- paginated directory read (path, names, max, startIndex) */ static constexpr uint64_t SYS_READDIR_AT = 136; + /* Bluetooth.hpp -- set adapter BD_ADDR (6-byte buffer, addr[0] = LSB) */ + static constexpr uint64_t SYS_BTSETADDR = 137; + + /* Bluetooth.hpp -- list bonded (paired) devices / forget a bond */ + static constexpr uint64_t SYS_BTBONDS = 138; + static constexpr uint64_t SYS_BTFORGET = 139; + // Graceful power-off request actions (SYS_POWER_REQUEST). The desktop posts // a pending action and exits; login.elf reads it, runs the shutdown stages, // then issues the matching SYS_SHUTDOWN / SYS_RESET. @@ -508,6 +515,12 @@ namespace Montauk { char name[64]; }; + // Bluetooth bonded (paired) device (returned by SYS_BTBONDS) + struct BtBondInfo { + uint8_t bdAddr[6]; + uint8_t _pad[2]; + }; + struct ThermalInfo { char name[32]; // short zone name (e.g. "THRM", "TZ00") int32_t temperature; // tenths of degrees Celsius, or -1 if unavailable diff --git a/kernel/src/Drivers/USB/Bluetooth/Bluetooth.cpp b/kernel/src/Drivers/USB/Bluetooth/Bluetooth.cpp index 9b5127c..649e12b 100644 --- a/kernel/src/Drivers/USB/Bluetooth/Bluetooth.cpp +++ b/kernel/src/Drivers/USB/Bluetooth/Bluetooth.cpp @@ -37,6 +37,94 @@ namespace Drivers::USB::Bluetooth { // Forward declaration: firmware-dependent HCI bring-up, run once VFS is up. static void CompleteInit(); + // Path to the Bluetooth config (BD_ADDR override). Written by the + // Bluetooth desktop app; read here on boot. See ApplyConfiguredAddress(). + static constexpr const char* BT_CONFIG_PATH = "0:/config/bluetooth.toml"; + + // ========================================================================= + // bluetooth.toml BD_ADDR override + // ========================================================================= + + static bool HexNibble(char c, uint8_t& out) { + if (c >= '0' && c <= '9') { out = (uint8_t)(c - '0'); return true; } + if (c >= 'a' && c <= 'f') { out = (uint8_t)(c - 'a' + 10); return true; } + if (c >= 'A' && c <= 'F') { out = (uint8_t)(c - 'A' + 10); return true; } + return false; + } + + // Parse exactly six ':'/'-'-separated hex octets from a string fragment. + // Fills out[0..5] in written order (out[0] is the first printed octet, + // matching the desktop app's format_addr); returns false on any deviation. + static bool ParseMacStr(const char* s, int len, uint8_t out[6]) { + int byteIdx = 0, i = 0; + while (byteIdx < 6) { + while (i < len && (s[i] == ':' || s[i] == '-' || s[i] == ' ')) i++; + uint8_t hi = 0, lo = 0; + if (i + 1 >= len || !HexNibble(s[i], hi) || !HexNibble(s[i + 1], lo)) + return false; + out[byteIdx++] = (uint8_t)((hi << 4) | lo); + i += 2; + } + return true; + } + + // Read the optional `mac = "XX:XX:XX:XX:XX:XX"` key from bluetooth.toml. + // Minimal line scanner (the kernel has no TOML parser, and the file holds a + // single value); ignores section headers/comments. Returns false when the + // file is absent or the key is missing/malformed. + static bool ReadConfiguredMac(uint8_t out[6]) { + Fs::Vfs::BackendFile f; + if (Fs::Vfs::OpenBackendFile(BT_CONFIG_PATH, f) < 0) return false; + uint64_t size = Fs::Vfs::GetBackendFileSize(f); + if (size == 0) { Fs::Vfs::CloseBackendFile(f); return false; } + + char buf[512]; + uint64_t n = size < sizeof(buf) - 1 ? size : sizeof(buf) - 1; + Fs::Vfs::ReadBackendFile(f, (uint8_t*)buf, 0, n); + Fs::Vfs::CloseBackendFile(f); + buf[n] = '\0'; + + const char* p = buf; + while (*p) { + while (*p == ' ' || *p == '\t') p++; + // Match a bare "mac" key (next char must end the identifier). + if (p[0] == 'm' && p[1] == 'a' && p[2] == 'c' && + (p[3] == ' ' || p[3] == '\t' || p[3] == '=')) { + const char* q = p + 3; + while (*q == ' ' || *q == '\t') q++; + if (*q == '=') { + const char* quote = q + 1; + while (*quote && *quote != '"' && *quote != '\n') quote++; + if (*quote == '"') { + const char* end = quote + 1; + while (*end && *end != '"' && *end != '\n') end++; + if (*end == '"' && + ParseMacStr(quote + 1, (int)(end - quote - 1), out)) + return true; + } + } + } + while (*p && *p != '\n') p++; + if (*p == '\n') p++; + } + return false; + } + + // Apply a configured BD_ADDR override (if any) to the freshly-reset + // controller. Called from CompleteInit BEFORE ReadBdAddr so the rest of + // bring-up uses the overridden address; no HCI Reset must follow (the Intel + // 0xFC31 override is volatile and a reset reverts to the factory address). + static void ApplyConfiguredAddress() { + uint8_t mac[6]; + if (!ReadConfiguredMac(mac)) return; // no override configured + if (Hci::SetBdAddr(mac)) { + KernelLogStream(OK, "BT") << "Applied BD_ADDR override from bluetooth.toml"; + } else { + KernelLogStream(WARNING, "BT") + << "BD_ADDR override from bluetooth.toml rejected by controller"; + } + } + // Intel Bluetooth device IDs static bool IsIntelBt(uint16_t vid, uint16_t pid) { if (vid != 0x8087) return false; @@ -190,6 +278,11 @@ namespace Drivers::USB::Bluetooth { } } + // Apply a persisted BD_ADDR override (0:/config/bluetooth.toml) now, + // after the last reset and before the address is read back, so the + // whole bring-up below uses the overridden address. + ApplyConfiguredAddress(); + // Read BD_ADDR if (Hci::ReadBdAddr(g_bdAddr)) { KernelLogStream(OK, "BT") << "BD_ADDR: " @@ -307,6 +400,65 @@ namespace Drivers::USB::Bluetooth { return g_bdAddr; } + // ========================================================================= + // SetAddress — live BD_ADDR change + // ========================================================================= + + bool SetAddress(const uint8_t* addr) { + if (!g_initialized || !addr) return false; + + // Reject the obviously-invalid addresses (all-zero, broadcast). + bool allZero = true, allOnes = true; + for (int i = 0; i < 6; i++) { + if (addr[i] != 0x00) allZero = false; + if (addr[i] != 0xFF) allOnes = false; + } + if (allZero || allOnes) return false; + + // Drop any active link first: its pairing/encryption was negotiated + // against the old address, so it cannot survive the change. + bool droppedLink = false; + for (int i = 0; i < Hci::MAX_CONNECTIONS; i++) { + auto* conn = Hci::GetConnectionByIndex(i); + if (conn && conn->Active) { + Hci::Disconnect(conn->Handle, 0x13); // Remote User Terminated + droppedLink = true; + } + } + // Let the disconnection(s) complete before reprogramming the address. + if (droppedLink) { + uint64_t t0 = Timekeeping::GetMilliseconds(); + while (Timekeeping::GetMilliseconds() - t0 < 300) { + Xhci::PollEvents(); + Hci::DrainEvents(); + for (int k = 0; k < 200; k++) asm volatile("pause" ::: "memory"); + } + } + + // Program the new address. Do NOT issue an HCI Reset afterwards: the + // Intel 0xFC31 override is volatile and a reset reverts it. + if (!Hci::SetBdAddr(addr)) return false; + + // Re-read to confirm and refresh the cache; fall back to the requested + // bytes if the read fails. + uint8_t readback[6] = {}; + if (Hci::ReadBdAddr(readback)) { + memcpy(g_bdAddr, readback, 6); + } else { + memcpy(g_bdAddr, addr, 6); + } + + // Re-assert discoverable + connectable with the new address (no reset + // happened, so name/CoD/SSP/event-mask all persist). + Hci::WriteScanEnable(0x03); + + for (int i = 0; i < 6; i++) { + if (g_bdAddr[i] != addr[i]) return false; + } + KernelLogStream(OK, "BT") << "BD_ADDR changed at runtime"; + return true; + } + // ========================================================================= // Scan — blocking inquiry // ========================================================================= @@ -477,4 +629,21 @@ namespace Drivers::USB::Bluetooth { return count; } + // ========================================================================= + // ListBonded / ForgetDevice — paired-device management + // ========================================================================= + + int ListBonded(Hci::BondInfo* buf, int maxCount) { + if (!g_initialized || !buf || maxCount <= 0) return 0; + return Hci::ListBonds(buf, maxCount); + } + + int ForgetDevice(const uint8_t* bdAddr) { + if (!g_initialized || !bdAddr) return -1; + // Best-effort: tear down an active link first so we don't keep a live + // connection whose key we just discarded. + Disconnect(bdAddr); + return Hci::ForgetBond(bdAddr) ? 0 : -1; + } + } diff --git a/kernel/src/Drivers/USB/Bluetooth/Bluetooth.hpp b/kernel/src/Drivers/USB/Bluetooth/Bluetooth.hpp index 9bcebf7..d70f628 100644 --- a/kernel/src/Drivers/USB/Bluetooth/Bluetooth.hpp +++ b/kernel/src/Drivers/USB/Bluetooth/Bluetooth.hpp @@ -34,6 +34,16 @@ namespace Drivers::USB::Bluetooth { // Get Bluetooth device address (6 bytes) const uint8_t* GetBdAddr(); + // Change the adapter's BD_ADDR at runtime. Drops any active links (their + // pairing/crypto was bound to the old address), issues the Intel vendor + // write (0xFC31), re-reads the controller's address into the cache and + // re-asserts scan. Returns true only if the controller reports the new + // address. Persisting the choice to 0:/config/bluetooth.toml is the + // caller's job (userspace); the override is re-applied on boot by + // ApplyConfiguredAddress() during init. Bytes are in the same order as + // GetBdAddr()/ReadBdAddr (addr[0] = least-significant octet). + bool SetAddress(const uint8_t* addr); + // Scan for nearby devices (blocking, up to timeoutMs) // Returns number of devices found; results written to buf int Scan(Hci::InquiryDevice* buf, int maxCount, uint32_t timeoutMs); @@ -50,4 +60,12 @@ namespace Drivers::USB::Bluetooth { // Returns number of connected devices; info written to buf int ListConnected(Hci::ConnectionInfo* buf, int maxCount); + // List bonded (paired) devices from the persisted link-key store. + // Returns number written to buf (paired devices may be disconnected). + int ListBonded(Hci::BondInfo* buf, int maxCount); + + // Forget a paired device: drop the link if connected and remove the bond + // (so it must re-pair next time). Returns 0 on success, -1 if not bonded. + int ForgetDevice(const uint8_t* bdAddr); + } diff --git a/kernel/src/Drivers/USB/Bluetooth/Hci.cpp b/kernel/src/Drivers/USB/Bluetooth/Hci.cpp index b19e6f4..a7bd571 100644 --- a/kernel/src/Drivers/USB/Bluetooth/Hci.cpp +++ b/kernel/src/Drivers/USB/Bluetooth/Hci.cpp @@ -256,6 +256,30 @@ namespace Drivers::USB::Bluetooth::Hci { KernelLogStream(OK, "BT-HCI") << "Link key store persisted"; } + int ListBonds(BondInfo* buf, int maxCount) { + if (!buf || maxCount <= 0) return 0; + int n = 0; + for (int i = 0; i < MAX_BONDS && n < maxCount; i++) { + if (g_bonds[i].valid) { + memcpy(buf[n].Addr, g_bonds[i].addr, 6); + n++; + } + } + return n; + } + + bool ForgetBond(const uint8_t* addr) { + if (!addr) return false; + int idx = FindBondIndex(addr); + if (idx < 0) return false; + g_bonds[idx].valid = false; + memset(g_bonds[idx].key, 0, 16); + memset(g_bonds[idx].addr, 0, 6); + g_bondsDirty = true; + FlushLinkKeys(); // persist removal now (caller is process context) + return true; + } + // ========================================================================= // USB transfer callback // ========================================================================= diff --git a/kernel/src/Drivers/USB/Bluetooth/Hci.hpp b/kernel/src/Drivers/USB/Bluetooth/Hci.hpp index fa6c2e7..6abbf87 100644 --- a/kernel/src/Drivers/USB/Bluetooth/Hci.hpp +++ b/kernel/src/Drivers/USB/Bluetooth/Hci.hpp @@ -145,6 +145,11 @@ namespace Drivers::USB::Bluetooth::Hci { constexpr int MAX_CONNECTIONS = 4; + // A persisted (bonded/paired) device, as stored in the link-key store. + struct BondInfo { + uint8_t Addr[6]; + }; + // ========================================================================= // Intel Bluetooth version info // ========================================================================= @@ -252,6 +257,14 @@ namespace Drivers::USB::Bluetooth::Hci { void LoadLinkKeys(); void FlushLinkKeys(); + // Enumerate the stored bonds (paired devices) into buf; returns the count + // written (<= maxCount). Used to list paired-but-disconnected devices. + int ListBonds(BondInfo* buf, int maxCount); + + // Remove a stored bond by BD_ADDR and persist the removal (process context). + // Returns true if a matching bond was found and forgotten. + bool ForgetBond(const uint8_t* addr); + // Non-blocking peek at the most recent 0xFF/0x06 secure-send result without // consuming it. Returns true if one has arrived since the last // ClearSecureSendResult(). The payload loop uses this to catch a mid-stream diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index aa16747..3b2110e 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -182,6 +182,13 @@ namespace Montauk { // 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; + // Graceful power-off request actions (SYS_POWER_REQUEST). The desktop posts // a pending action and exits; login.elf reads it, runs the shutdown stages, // then issues the matching SYS_SHUTDOWN / SYS_RESET. @@ -433,6 +440,12 @@ namespace Montauk { char name[64]; }; + // Bluetooth bonded (paired) device (returned by SYS_BTBONDS) + struct BtBondInfo { + uint8_t bdAddr[6]; + uint8_t _pad[2]; + }; + struct ThermalInfo { char name[32]; // short zone name (e.g. "THRM", "TZ00") int32_t temperature; // tenths of degrees Celsius, or -1 if unavailable diff --git a/programs/include/montauk/syscall.h b/programs/include/montauk/syscall.h index 96b2cb2..d60a445 100644 --- a/programs/include/montauk/syscall.h +++ b/programs/include/montauk/syscall.h @@ -536,6 +536,20 @@ namespace montauk { inline int bt_disconnect(const uint8_t* bdAddr) { return (int)syscall1(Montauk::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::SYS_BTSETADDR, (uint64_t)bdAddr); + } + // List bonded (paired) devices. Returns count written to buf (may be 0). + inline int bt_bonds(Montauk::BtBondInfo* buf, int maxCount) { + return (int)syscall2(Montauk::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::SYS_BTFORGET, (uint64_t)bdAddr); + } inline int bt_list(Montauk::BtDevInfo* buf, int maxCount) { return (int)syscall2(Montauk::SYS_BTLIST, (uint64_t)buf, (uint64_t)maxCount); } diff --git a/programs/libs/libprintersapplet/obj/src/libprintersapplet.o b/programs/libs/libprintersapplet/obj/src/libprintersapplet.o index d00bbb9..a8f2ddc 100644 Binary files a/programs/libs/libprintersapplet/obj/src/libprintersapplet.o and b/programs/libs/libprintersapplet/obj/src/libprintersapplet.o differ diff --git a/programs/libs/libtimezonesapplet/obj/src/libtimezonesapplet.o b/programs/libs/libtimezonesapplet/obj/src/libtimezonesapplet.o index f83bbda..a7ffd50 100644 Binary files a/programs/libs/libtimezonesapplet/obj/src/libtimezonesapplet.o and b/programs/libs/libtimezonesapplet/obj/src/libtimezonesapplet.o differ diff --git a/programs/src/bluetooth/main.cpp b/programs/src/bluetooth/main.cpp index 4750032..04064d2 100644 --- a/programs/src/bluetooth/main.cpp +++ b/programs/src/bluetooth/main.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -31,9 +32,13 @@ static constexpr int BTN_H = 28; static constexpr int STATUS_H = 28; static constexpr int SCAN_BTN_W = 100; static constexpr int SCROLL_STEP = 20; +static constexpr int FIELD_H = 32; +static constexpr int A_PAD_TOP = 18; static constexpr int MAX_SCAN = 16; -static constexpr int MAX_CONNECTED = 8; +// Up to 4 active links + up to 8 stored bonds shown together in the Devices tab. +static constexpr int MAX_CONNECTED = 16; +static constexpr int FORGET_W = 64; static constexpr Color BG_COLOR = Color::from_rgb(0xFF, 0xFF, 0xFF); static constexpr Color TEXT_COLOR = Color::from_rgb(0x33, 0x33, 0x33); @@ -52,12 +57,14 @@ static constexpr Color RED = Color::from_rgb(0xCC, 0x33, 0x33); enum Tab { TAB_DEVICES = 0, TAB_SCAN = 1, - TAB_COUNT = 2, + TAB_ADAPTER = 2, + TAB_COUNT = 3, }; static const char* g_tab_labels[TAB_COUNT] = { "Devices", "Scan", + "Adapter", }; static int g_win_w = WIN_W; @@ -82,6 +89,12 @@ static Montauk::BtScanResult g_scan[MAX_SCAN]; static int g_scan_count = 0; static bool g_scanning = false; +// Adapter MAC editing +static char g_mac_value[24] = {}; +static mtk::TextInputState g_mac_input = {}; +static bool g_mac_focus = false; +static uint64_t g_rng = 0; + // Scroll static int g_scroll = 0; @@ -157,8 +170,17 @@ static Rect row_action_button_rect(const Rect& row) { return {g_win_w - PAD - BTN_W, row.y + (ROW_H - BTN_H) / 2, BTN_W, BTN_H}; } +// Secondary (Forget) button, left of the primary action button. Used for +// paired-but-disconnected rows that show "Connect" + "Forget". +static Rect row_forget_button_rect(const Rect& row) { + Rect a = row_action_button_rect(row); + return {a.x - 8 - FORGET_W, a.y, FORGET_W, BTN_H}; +} + static int current_row_count() { - return g_tab == TAB_DEVICES ? g_device_count : g_scan_count; + if (g_tab == TAB_DEVICES) return g_device_count; + if (g_tab == TAB_SCAN) return g_scan_count; + return 0; // Adapter tab is not a scrollable list } static int current_list_top() { @@ -240,8 +262,29 @@ static void refresh_adapter() { } static void refresh_devices() { - g_device_count = montauk::bt_list(g_devices, MAX_CONNECTED); - if (g_device_count < 0) g_device_count = 0; + int active = montauk::bt_list(g_devices, MAX_CONNECTED); + if (active < 0) active = 0; + g_device_count = active; + + // Append bonded (paired) devices that aren't currently connected, so the + // Devices tab remembers them across sessions/reboots. + Montauk::BtBondInfo bonds[8]; + int nb = montauk::bt_bonds(bonds, 8); + for (int i = 0; i < nb && g_device_count < MAX_CONNECTED; i++) { + bool already = false; + for (int j = 0; j < active; j++) { + if (memcmp(g_devices[j].bdAddr, bonds[i].bdAddr, 6) == 0) { already = true; break; } + } + if (already) continue; + + Montauk::BtDevInfo d = {}; + montauk::memcpy(d.bdAddr, bonds[i].bdAddr, 6); + d.connected = 0; // paired but not connected + d.encrypted = 0; + d.handle = 0; + d.linkType = 0x01; + g_devices[g_device_count++] = d; + } for (int i = 0; i < g_device_count; i++) { bool found = false; @@ -295,6 +338,14 @@ static void do_disconnect(const uint8_t* addr) { clamp_scroll(); } +static void do_forget(const uint8_t* addr) { + int r = montauk::bt_forget(addr); + if (r >= 0) set_status("Device forgotten"); + else set_status("Forget failed"); + refresh_devices(); + clamp_scroll(); +} + static bool is_connected(const uint8_t* addr) { for (int i = 0; i < g_device_count; i++) { if (memcmp(g_devices[i].bdAddr, addr, 6) == 0 && g_devices[i].connected) @@ -338,9 +389,144 @@ static void set_tab(Tab tab) { g_tab = tab; g_scroll = 0; g_hover_row = -1; + if (tab != TAB_ADAPTER) + g_mac_focus = false; clamp_scroll(); } +// ============================================================================ +// Adapter MAC address +// ============================================================================ + +static bool is_mac_char(char ch) { + return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || + (ch >= 'A' && ch <= 'F') || ch == ':'; +} + +static bool accept_mac_char(char ch, void* userdata) { + (void)userdata; + return is_mac_char(ch); +} + +static bool hex_nibble(char c, uint8_t& out) { + if (c >= '0' && c <= '9') { out = (uint8_t)(c - '0'); return true; } + if (c >= 'a' && c <= 'f') { out = (uint8_t)(c - 'a' + 10); return true; } + if (c >= 'A' && c <= 'F') { out = (uint8_t)(c - 'A' + 10); return true; } + return false; +} + +// Parse "XX:XX:XX:XX:XX:XX" (':' or '-' separators, optional) into 6 bytes, +// out[0] = first printed octet. Returns false unless exactly six octets and no +// trailing junk remain. +static bool parse_mac(const char* s, uint8_t out[6]) { + int byteIdx = 0; + const char* p = s; + while (byteIdx < 6) { + while (*p == ':' || *p == '-' || *p == ' ') p++; + uint8_t hi = 0, lo = 0; + if (!hex_nibble(p[0], hi) || !hex_nibble(p[1], lo)) return false; + out[byteIdx++] = (uint8_t)((hi << 4) | lo); + p += 2; + } + while (*p == ':' || *p == '-' || *p == ' ') p++; + return *p == '\0'; +} + +static uint32_t rng_next() { + if (g_rng == 0) g_rng = montauk::get_milliseconds() | 1ULL; + g_rng ^= g_rng << 13; + g_rng ^= g_rng >> 7; + g_rng ^= g_rng << 17; + return (uint32_t)g_rng; +} + +static void randomize_mac() { + uint8_t b[6]; + for (int i = 0; i < 6; i++) b[i] = (uint8_t)(rng_next() & 0xFF); + // First printed octet: locally-administered + unicast (clear multicast bit). + b[0] = (uint8_t)((b[0] & 0xFC) | 0x02); + format_addr(g_mac_value, sizeof(g_mac_value), b); + g_mac_focus = true; + int len = (int)strlen(g_mac_value); + mtk::text_input_set_cursor(g_mac_input, len, len, false); +} + +// Prefill the editor from the saved config (if any), else from the live adapter. +static void load_mac_config() { + montauk::toml::Doc cfg = montauk::config::load("bluetooth"); + const char* m = cfg.get_string("adapter.mac", ""); + if (m && m[0]) { + snprintf(g_mac_value, sizeof(g_mac_value), "%s", m); + } else if (g_adapter_ok) { + format_addr(g_mac_value, sizeof(g_mac_value), g_adapter.bdAddr); + } else { + g_mac_value[0] = '\0'; + } + cfg.destroy(); +} + +static void apply_mac() { + uint8_t bytes[6]; + if (!parse_mac(g_mac_value, bytes)) { + set_status("Invalid MAC address"); + return; + } + + if (montauk::bt_set_addr(bytes) < 0) { + set_status("Failed to set MAC"); + return; + } + + // Normalize and persist to 0:/config/bluetooth.toml so it is re-applied + // on every boot by the kernel. + char norm[24]; + format_addr(norm, sizeof(norm), bytes); + montauk::toml::Doc cfg = montauk::config::load("bluetooth"); + montauk::config::set_string(&cfg, "adapter.mac", norm); + montauk::config::save("bluetooth", &cfg); + cfg.destroy(); + + snprintf(g_mac_value, sizeof(g_mac_value), "%s", norm); + g_mac_focus = false; + set_status("MAC address changed"); + refresh_adapter(); + refresh_devices(); +} + +static void use_default_mac() { + montauk::toml::Doc cfg = montauk::config::load("bluetooth"); + bool had = montauk::config::unset(&cfg, "adapter.mac"); + montauk::config::save("bluetooth", &cfg); + cfg.destroy(); + set_status(had ? "Factory address restored on reboot" : "No custom MAC set"); +} + +// ---- Adapter tab layout ---- + +static int a_cur_label_y() { return content_rect().y + A_PAD_TOP; } +static int a_cur_value_y() { return a_cur_label_y() + system_font_height() + 4; } +static int a_new_label_y() { return a_cur_value_y() + system_font_height() + 20; } + +static Rect adapter_input_rect() { + int y = a_new_label_y() + system_font_height() + 6; + return {PAD, y, g_win_w - PAD * 2, FIELD_H}; +} + +static Rect adapter_apply_btn() { + Rect f = adapter_input_rect(); + return {g_win_w - PAD - BTN_W, f.y + f.h + 16, BTN_W, BTN_H}; +} + +static Rect adapter_random_btn() { + Rect a = adapter_apply_btn(); + return {PAD, a.y, 80, BTN_H}; +} + +static Rect adapter_default_btn() { + Rect r = adapter_random_btn(); + return {r.x + r.w + 8, r.y, 110, BTN_H}; +} + // ============================================================================ // Hover and hit testing // ============================================================================ @@ -351,7 +537,7 @@ static int get_hover_row(int mx, int my) { if (devices_row_rect(i).contains(mx, my)) return i; } - } else { + } else if (g_tab == TAB_SCAN) { for (int i = 0; i < g_scan_count; i++) { if (scan_row_rect(i).contains(mx, my)) return i; @@ -391,12 +577,24 @@ static bool handle_click(int mx, int my) { for (int i = 0; i < g_device_count; i++) { Rect row = devices_row_rect(i); if (!row.contains(mx, my)) continue; - if (!g_devices[i].connected) continue; Rect action = row_action_button_rect(row); - if (action.contains(mx, my)) { - do_disconnect(g_devices[i].bdAddr); - return true; + if (g_devices[i].connected) { + if (action.contains(mx, my)) { + do_disconnect(g_devices[i].bdAddr); + return true; + } + } else { + // Paired but not connected: Connect (primary) + Forget. + Rect forget = row_forget_button_rect(row); + if (action.contains(mx, my)) { + do_connect(g_devices[i].bdAddr); + return true; + } + if (forget.contains(mx, my)) { + do_forget(g_devices[i].bdAddr); + return true; + } } } } @@ -404,6 +602,69 @@ static bool handle_click(int mx, int my) { return false; } +// Adapter-tab pointer handling: text field (focus/cursor/selection/context) +// plus the Apply / Random / Use Default buttons. The tab bar is handled by the +// caller before this runs. Returns true if the view should be redrawn. +static bool handle_adapter_mouse(int mx, int my, uint8_t buttons, + uint8_t prev_buttons, int view_w, int view_h) { + bool left_press = (buttons & 1) && !(prev_buttons & 1); + Rect field = adapter_input_rect(); + + // While a context menu is open or a drag is in progress, let the widget + // consume the event regardless of where the pointer is. + if (g_mac_input.context.open || g_mac_input.dragging) { + int res = mtk::text_input_handle_mouse( + g_mac_input, field, g_mac_value, (int)sizeof(g_mac_value), + mx, my, buttons, prev_buttons, view_w, view_h, + g_mac_focus, false, accept_mac_char); + return res != mtk::TEXT_INPUT_NONE; + } + + if (left_press) { + if (g_adapter_ok && adapter_apply_btn().contains(mx, my)) { apply_mac(); return true; } + if (g_adapter_ok && adapter_random_btn().contains(mx, my)) { randomize_mac(); return true; } + if (adapter_default_btn().contains(mx, my)) { use_default_mac(); return true; } + } + + if (field.contains(mx, my)) { + g_mac_focus = true; + mtk::text_input_handle_mouse( + g_mac_input, field, g_mac_value, (int)sizeof(g_mac_value), + mx, my, buttons, prev_buttons, view_w, view_h, + true, false, accept_mac_char); + return true; + } + + if (left_press) { + g_mac_focus = false; + return true; + } + return false; +} + +static bool handle_adapter_key(const Montauk::KeyEvent& key) { + if (key.ascii == '\n' || key.ascii == '\r') { apply_mac(); return true; } + if (key.ascii == '\t') { g_mac_focus = !g_mac_focus; return true; } + + if (g_mac_focus) { + int res = mtk::text_input_key(g_mac_input, g_mac_value, + (int)sizeof(g_mac_value), key, accept_mac_char); + return res != mtk::TEXT_INPUT_NONE; + } + + // Not editing: number keys still switch tabs; any MAC char starts editing. + if (key.ascii == '1') { set_tab(TAB_DEVICES); return true; } + if (key.ascii == '2') { set_tab(TAB_SCAN); return true; } + if (key.ascii == '3') { return true; } + if (is_mac_char(key.ascii)) { + g_mac_focus = true; + int res = mtk::text_input_key(g_mac_input, g_mac_value, + (int)sizeof(g_mac_value), key, accept_mac_char); + return res != mtk::TEXT_INPUT_NONE; + } + return false; +} + // ============================================================================ // Render // ============================================================================ @@ -413,7 +674,7 @@ static void render_devices_tab(Canvas& canvas, const mtk::Theme& theme) { int fh = system_font_height(); if (g_device_count == 0) { - draw_centered_text(canvas, list.y, list.h, "No connected devices", theme.text_subtle); + draw_centered_text(canvas, list.y, list.h, "No paired devices", theme.text_subtle); return; } @@ -426,21 +687,30 @@ static void render_devices_tab(Canvas& canvas, const mtk::Theme& theme) { else mtk::draw_list_row(canvas, row, false, (i & 1) != 0, theme); - Color dot_color = g_devices[i].connected ? GREEN : theme.text_subtle; + bool connected = g_devices[i].connected; + Color dot_color = connected ? GREEN : theme.text_subtle; fill_circle(canvas, PAD + 6, row.y + ROW_H / 2, 5, dot_color); Rect action = row_action_button_rect(row); + Rect forget = row_forget_button_rect(row); int text_x = PAD + 20; - int text_w_max = (g_devices[i].connected ? action.x : g_win_w - PAD) - text_x - 10; + // Connected rows have one button (Disconnect); paired rows have two + // (Connect + Forget), so the text must stop before the leftmost button. + int text_w_max = (connected ? action.x : forget.x) - text_x - 10; draw_text_fit(canvas, text_x, row.y + 8, g_device_names[i], text_w_max, theme.text); char addr_str[24]; format_addr(addr_str, sizeof(addr_str), g_devices[i].bdAddr); draw_text_fit(canvas, text_x, row.y + 8 + fh + 2, addr_str, text_w_max, theme.text_subtle); - if (g_devices[i].connected) { + if (connected) { mtk::draw_button(canvas, action, "Disconnect", mtk::BUTTON_DANGER, button_state(action, true), theme); + } else { + mtk::draw_button(canvas, action, "Connect", mtk::BUTTON_PRIMARY, + button_state(action, true), theme); + mtk::draw_button(canvas, forget, "Forget", mtk::BUTTON_SECONDARY, + button_state(forget, true), theme); } mtk::draw_separator(canvas, PAD, row.y + ROW_H - 1, g_win_w - 2 * PAD, theme); @@ -499,6 +769,45 @@ static void render_scan_tab(Canvas& canvas, const mtk::Theme& theme) { } } +static void render_adapter_tab(Canvas& canvas, const mtk::Theme& theme) { + // Current address + canvas.text(PAD, a_cur_label_y(), "Current Address", theme.text_subtle); + if (g_adapter_ok) { + char cur[24]; + format_addr(cur, sizeof(cur), g_adapter.bdAddr); + canvas.text(PAD, a_cur_value_y(), cur, theme.text); + } else { + canvas.text(PAD, a_cur_value_y(), "No adapter found", theme.danger); + } + + // Editable new address + canvas.text(PAD, a_new_label_y(), "New Address", theme.text_muted); + Rect field = adapter_input_rect(); + mtk::draw_text_field(canvas, field, g_mac_value, g_mac_input.cursor, + g_mac_focus, false, theme, g_mac_input.selection_anchor); + + bool enabled = g_adapter_ok; + Rect apply = adapter_apply_btn(); + Rect rnd = adapter_random_btn(); + Rect def = adapter_default_btn(); + mtk::draw_button(canvas, apply, "Apply", mtk::BUTTON_PRIMARY, + button_state(apply, enabled), theme); + mtk::draw_button(canvas, rnd, "Random", mtk::BUTTON_SECONDARY, + button_state(rnd, enabled), theme); + mtk::draw_button(canvas, def, "Use Default", mtk::BUTTON_SECONDARY, + button_state(def, true), theme); + + draw_text_fit(canvas, PAD, apply.y + BTN_H + 16, + "Saved to config and re-applied on boot.", + g_win_w - PAD * 2, theme.text_subtle); + + // Right-click context menu (cut/copy/paste) for the field, drawn last. + mtk::draw_text_input_context_menu( + canvas, g_mac_input, theme, + mtk::text_input_has_selection(g_mac_input, g_mac_value, + (int)sizeof(g_mac_value))); +} + static void render_status(Canvas& canvas, const mtk::Theme& theme) { Rect status = status_bar_rect(); int sy = status.y + (status.h - system_font_height()) / 2; @@ -514,12 +823,16 @@ static void render_status(Canvas& canvas, const mtk::Theme& theme) { int left_max = g_win_w - PAD * 2 - (show_status ? status_w + 12 : 0); if (g_adapter_ok) { + int connected = 0; + for (int i = 0; i < g_device_count; i++) + if (g_devices[i].connected) connected++; + char adapter_info[96]; char addr_str[24]; format_addr(addr_str, sizeof(addr_str), g_adapter.bdAddr); const char* name = g_adapter.name[0] ? g_adapter.name : "Adapter"; snprintf(adapter_info, sizeof(adapter_info), "%s | %s | %d connected", - name, addr_str, g_device_count); + name, addr_str, connected); draw_text_fit(canvas, PAD, sy, adapter_info, left_max, theme.text_subtle); } else { draw_text_fit(canvas, PAD, sy, "No adapter found", left_max, theme.danger); @@ -536,8 +849,10 @@ static void render(Canvas& canvas) { if (g_tab == TAB_DEVICES) render_devices_tab(canvas, theme); - else + else if (g_tab == TAB_SCAN) render_scan_tab(canvas, theme); + else + render_adapter_tab(canvas, theme); render_status(canvas, theme); } @@ -552,6 +867,7 @@ extern "C" void _start() { refresh_adapter(); refresh_devices(); + load_mac_config(); clamp_scroll(); g_status[0] = 0; @@ -614,18 +930,34 @@ extern "C" void _start() { } if (ev.type == 0 && ev.key.pressed) { - if (ev.key.scancode == 0x01) break; - if ((ev.key.ascii == 's' || ev.key.ascii == 'S') && !g_scanning) { - do_scan(); - redraw = true; - } - if (ev.key.ascii == '1') { - set_tab(TAB_DEVICES); - redraw = true; - } - if (ev.key.ascii == '2') { - set_tab(TAB_SCAN); - redraw = true; + if (g_tab == TAB_ADAPTER) { + // Esc defocuses the field if editing, otherwise closes. + if (ev.key.scancode == 0x01 && g_mac_focus) { + g_mac_focus = false; + redraw = true; + } else if (ev.key.scancode == 0x01) { + break; + } else if (handle_adapter_key(ev.key)) { + redraw = true; + } + } else { + if (ev.key.scancode == 0x01) break; + if ((ev.key.ascii == 's' || ev.key.ascii == 'S') && !g_scanning) { + do_scan(); + redraw = true; + } + if (ev.key.ascii == '1') { + set_tab(TAB_DEVICES); + redraw = true; + } + if (ev.key.ascii == '2') { + set_tab(TAB_SCAN); + redraw = true; + } + if (ev.key.ascii == '3') { + set_tab(TAB_ADAPTER); + redraw = true; + } } } @@ -634,24 +966,40 @@ extern "C" void _start() { g_mouse_x = ev.mouse.x; g_mouse_y = ev.mouse.y; - int new_hover = get_hover_row(g_mouse_x, g_mouse_y); - if (new_hover != g_hover_row) { - g_hover_row = new_hover; - redraw = true; - } + if (g_tab == TAB_ADAPTER) { + if (moved) redraw = true; + bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); + int tab = clicked + ? mtk::hit_tab_bar(tab_bar_rect(), TAB_COUNT, g_mouse_x, g_mouse_y) + : -1; + if (tab >= 0) { + set_tab((Tab)tab); + redraw = true; + } else if (handle_adapter_mouse(g_mouse_x, g_mouse_y, + ev.mouse.buttons, ev.mouse.prev_buttons, + win.width, win.height)) { + redraw = true; + } + } else { + int new_hover = get_hover_row(g_mouse_x, g_mouse_y); + if (new_hover != g_hover_row) { + g_hover_row = new_hover; + redraw = true; + } - if (moved) - redraw = true; + if (moved) + redraw = true; - bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); - if (clicked && handle_click(ev.mouse.x, ev.mouse.y)) - redraw = true; + bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); + if (clicked && handle_click(ev.mouse.x, ev.mouse.y)) + redraw = true; - if (ev.mouse.scroll) { - g_scroll -= ev.mouse.scroll * SCROLL_STEP; - clamp_scroll(); - g_hover_row = get_hover_row(g_mouse_x, g_mouse_y); - redraw = true; + if (ev.mouse.scroll) { + g_scroll -= ev.mouse.scroll * SCROLL_STEP; + clamp_scroll(); + g_hover_row = get_hover_row(g_mouse_x, g_mouse_y); + redraw = true; + } } }