feat: allow MAC spoofing in Bluetooth stack, show paired devices in Bluetooth app
This commit is contained in:
@@ -56,6 +56,40 @@ namespace Montauk {
|
|||||||
return (int64_t)Drivers::USB::Bluetooth::Disconnect(bdAddr);
|
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) {
|
static int64_t Sys_BtList(BtDevInfo* buf, int maxCount) {
|
||||||
if (!buf || maxCount <= 0) return 0;
|
if (!buf || maxCount <= 0) return 0;
|
||||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
|
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
|
||||||
|
|||||||
@@ -12,4 +12,4 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define MONTAUK_BUILD_NUMBER 22
|
#define MONTAUK_BUILD_NUMBER 24
|
||||||
|
|||||||
@@ -377,6 +377,16 @@ namespace Montauk {
|
|||||||
case SYS_BTDISCONNECT:
|
case SYS_BTDISCONNECT:
|
||||||
if (!UserMemory::Range(frame->arg1, 6, false)) return -1;
|
if (!UserMemory::Range(frame->arg1, 6, false)) return -1;
|
||||||
return Sys_BtDisconnect((const uint8_t*)frame->arg1);
|
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:
|
case SYS_BTLIST:
|
||||||
if ((int64_t)frame->arg2 < 0) return -1;
|
if ((int64_t)frame->arg2 < 0) return -1;
|
||||||
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtDevInfo), true)) return -1;
|
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtDevInfo), true)) return -1;
|
||||||
|
|||||||
@@ -258,6 +258,13 @@ namespace Montauk {
|
|||||||
/* Filesystem.hpp -- paginated directory read (path, names, max, startIndex) */
|
/* Filesystem.hpp -- paginated directory read (path, names, max, startIndex) */
|
||||||
static constexpr uint64_t SYS_READDIR_AT = 136;
|
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
|
// Graceful power-off request actions (SYS_POWER_REQUEST). The desktop posts
|
||||||
// a pending action and exits; login.elf reads it, runs the shutdown stages,
|
// a pending action and exits; login.elf reads it, runs the shutdown stages,
|
||||||
// then issues the matching SYS_SHUTDOWN / SYS_RESET.
|
// then issues the matching SYS_SHUTDOWN / SYS_RESET.
|
||||||
@@ -508,6 +515,12 @@ namespace Montauk {
|
|||||||
char name[64];
|
char name[64];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Bluetooth bonded (paired) device (returned by SYS_BTBONDS)
|
||||||
|
struct BtBondInfo {
|
||||||
|
uint8_t bdAddr[6];
|
||||||
|
uint8_t _pad[2];
|
||||||
|
};
|
||||||
|
|
||||||
struct ThermalInfo {
|
struct ThermalInfo {
|
||||||
char name[32]; // short zone name (e.g. "THRM", "TZ00")
|
char name[32]; // short zone name (e.g. "THRM", "TZ00")
|
||||||
int32_t temperature; // tenths of degrees Celsius, or -1 if unavailable
|
int32_t temperature; // tenths of degrees Celsius, or -1 if unavailable
|
||||||
|
|||||||
@@ -37,6 +37,94 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
// Forward declaration: firmware-dependent HCI bring-up, run once VFS is up.
|
// Forward declaration: firmware-dependent HCI bring-up, run once VFS is up.
|
||||||
static void CompleteInit();
|
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
|
// Intel Bluetooth device IDs
|
||||||
static bool IsIntelBt(uint16_t vid, uint16_t pid) {
|
static bool IsIntelBt(uint16_t vid, uint16_t pid) {
|
||||||
if (vid != 0x8087) return false;
|
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
|
// Read BD_ADDR
|
||||||
if (Hci::ReadBdAddr(g_bdAddr)) {
|
if (Hci::ReadBdAddr(g_bdAddr)) {
|
||||||
KernelLogStream(OK, "BT") << "BD_ADDR: "
|
KernelLogStream(OK, "BT") << "BD_ADDR: "
|
||||||
@@ -307,6 +400,65 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
return g_bdAddr;
|
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
|
// Scan — blocking inquiry
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -477,4 +629,21 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
return count;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,16 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
// Get Bluetooth device address (6 bytes)
|
// Get Bluetooth device address (6 bytes)
|
||||||
const uint8_t* GetBdAddr();
|
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)
|
// Scan for nearby devices (blocking, up to timeoutMs)
|
||||||
// Returns number of devices found; results written to buf
|
// Returns number of devices found; results written to buf
|
||||||
int Scan(Hci::InquiryDevice* buf, int maxCount, uint32_t timeoutMs);
|
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
|
// Returns number of connected devices; info written to buf
|
||||||
int ListConnected(Hci::ConnectionInfo* buf, int maxCount);
|
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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,6 +256,30 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
KernelLogStream(OK, "BT-HCI") << "Link key store persisted";
|
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
|
// USB transfer callback
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -145,6 +145,11 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
|
|
||||||
constexpr int MAX_CONNECTIONS = 4;
|
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
|
// Intel Bluetooth version info
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -252,6 +257,14 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
void LoadLinkKeys();
|
void LoadLinkKeys();
|
||||||
void FlushLinkKeys();
|
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
|
// Non-blocking peek at the most recent 0xFF/0x06 secure-send result without
|
||||||
// consuming it. Returns true if one has arrived since the last
|
// consuming it. Returns true if one has arrived since the last
|
||||||
// ClearSecureSendResult(). The payload loop uses this to catch a mid-stream
|
// ClearSecureSendResult(). The payload loop uses this to catch a mid-stream
|
||||||
|
|||||||
@@ -182,6 +182,13 @@ namespace Montauk {
|
|||||||
// Paginated directory read (path, names, max, startIndex)
|
// Paginated directory read (path, names, max, startIndex)
|
||||||
static constexpr uint64_t SYS_READDIR_AT = 136;
|
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
|
// Graceful power-off request actions (SYS_POWER_REQUEST). The desktop posts
|
||||||
// a pending action and exits; login.elf reads it, runs the shutdown stages,
|
// a pending action and exits; login.elf reads it, runs the shutdown stages,
|
||||||
// then issues the matching SYS_SHUTDOWN / SYS_RESET.
|
// then issues the matching SYS_SHUTDOWN / SYS_RESET.
|
||||||
@@ -433,6 +440,12 @@ namespace Montauk {
|
|||||||
char name[64];
|
char name[64];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Bluetooth bonded (paired) device (returned by SYS_BTBONDS)
|
||||||
|
struct BtBondInfo {
|
||||||
|
uint8_t bdAddr[6];
|
||||||
|
uint8_t _pad[2];
|
||||||
|
};
|
||||||
|
|
||||||
struct ThermalInfo {
|
struct ThermalInfo {
|
||||||
char name[32]; // short zone name (e.g. "THRM", "TZ00")
|
char name[32]; // short zone name (e.g. "THRM", "TZ00")
|
||||||
int32_t temperature; // tenths of degrees Celsius, or -1 if unavailable
|
int32_t temperature; // tenths of degrees Celsius, or -1 if unavailable
|
||||||
|
|||||||
@@ -536,6 +536,20 @@ namespace montauk {
|
|||||||
inline int bt_disconnect(const uint8_t* bdAddr) {
|
inline int bt_disconnect(const uint8_t* bdAddr) {
|
||||||
return (int)syscall1(Montauk::SYS_BTDISCONNECT, (uint64_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) {
|
inline int bt_list(Montauk::BtDevInfo* buf, int maxCount) {
|
||||||
return (int)syscall2(Montauk::SYS_BTLIST, (uint64_t)buf, (uint64_t)maxCount);
|
return (int)syscall2(Montauk::SYS_BTLIST, (uint64_t)buf, (uint64_t)maxCount);
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
+361
-13
@@ -7,6 +7,7 @@
|
|||||||
#include <montauk/syscall.h>
|
#include <montauk/syscall.h>
|
||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
#include <montauk/heap.h>
|
#include <montauk/heap.h>
|
||||||
|
#include <montauk/config.h>
|
||||||
#include <gui/mtk.hpp>
|
#include <gui/mtk.hpp>
|
||||||
#include <gui/standalone.hpp>
|
#include <gui/standalone.hpp>
|
||||||
|
|
||||||
@@ -31,9 +32,13 @@ static constexpr int BTN_H = 28;
|
|||||||
static constexpr int STATUS_H = 28;
|
static constexpr int STATUS_H = 28;
|
||||||
static constexpr int SCAN_BTN_W = 100;
|
static constexpr int SCAN_BTN_W = 100;
|
||||||
static constexpr int SCROLL_STEP = 20;
|
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_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 BG_COLOR = Color::from_rgb(0xFF, 0xFF, 0xFF);
|
||||||
static constexpr Color TEXT_COLOR = Color::from_rgb(0x33, 0x33, 0x33);
|
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 {
|
enum Tab {
|
||||||
TAB_DEVICES = 0,
|
TAB_DEVICES = 0,
|
||||||
TAB_SCAN = 1,
|
TAB_SCAN = 1,
|
||||||
TAB_COUNT = 2,
|
TAB_ADAPTER = 2,
|
||||||
|
TAB_COUNT = 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const char* g_tab_labels[TAB_COUNT] = {
|
static const char* g_tab_labels[TAB_COUNT] = {
|
||||||
"Devices",
|
"Devices",
|
||||||
"Scan",
|
"Scan",
|
||||||
|
"Adapter",
|
||||||
};
|
};
|
||||||
|
|
||||||
static int g_win_w = WIN_W;
|
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 int g_scan_count = 0;
|
||||||
static bool g_scanning = false;
|
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
|
// Scroll
|
||||||
static int g_scroll = 0;
|
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};
|
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() {
|
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() {
|
static int current_list_top() {
|
||||||
@@ -240,8 +262,29 @@ static void refresh_adapter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void refresh_devices() {
|
static void refresh_devices() {
|
||||||
g_device_count = montauk::bt_list(g_devices, MAX_CONNECTED);
|
int active = montauk::bt_list(g_devices, MAX_CONNECTED);
|
||||||
if (g_device_count < 0) g_device_count = 0;
|
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++) {
|
for (int i = 0; i < g_device_count; i++) {
|
||||||
bool found = false;
|
bool found = false;
|
||||||
@@ -295,6 +338,14 @@ static void do_disconnect(const uint8_t* addr) {
|
|||||||
clamp_scroll();
|
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) {
|
static bool is_connected(const uint8_t* addr) {
|
||||||
for (int i = 0; i < g_device_count; i++) {
|
for (int i = 0; i < g_device_count; i++) {
|
||||||
if (memcmp(g_devices[i].bdAddr, addr, 6) == 0 && g_devices[i].connected)
|
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_tab = tab;
|
||||||
g_scroll = 0;
|
g_scroll = 0;
|
||||||
g_hover_row = -1;
|
g_hover_row = -1;
|
||||||
|
if (tab != TAB_ADAPTER)
|
||||||
|
g_mac_focus = false;
|
||||||
clamp_scroll();
|
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
|
// 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))
|
if (devices_row_rect(i).contains(mx, my))
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
} else {
|
} else if (g_tab == TAB_SCAN) {
|
||||||
for (int i = 0; i < g_scan_count; i++) {
|
for (int i = 0; i < g_scan_count; i++) {
|
||||||
if (scan_row_rect(i).contains(mx, my))
|
if (scan_row_rect(i).contains(mx, my))
|
||||||
return i;
|
return i;
|
||||||
@@ -391,19 +577,94 @@ static bool handle_click(int mx, int my) {
|
|||||||
for (int i = 0; i < g_device_count; i++) {
|
for (int i = 0; i < g_device_count; i++) {
|
||||||
Rect row = devices_row_rect(i);
|
Rect row = devices_row_rect(i);
|
||||||
if (!row.contains(mx, my)) continue;
|
if (!row.contains(mx, my)) continue;
|
||||||
if (!g_devices[i].connected) continue;
|
|
||||||
|
|
||||||
Rect action = row_action_button_rect(row);
|
Rect action = row_action_button_rect(row);
|
||||||
|
if (g_devices[i].connected) {
|
||||||
if (action.contains(mx, my)) {
|
if (action.contains(mx, my)) {
|
||||||
do_disconnect(g_devices[i].bdAddr);
|
do_disconnect(g_devices[i].bdAddr);
|
||||||
return true;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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
|
// Render
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -413,7 +674,7 @@ static void render_devices_tab(Canvas& canvas, const mtk::Theme& theme) {
|
|||||||
int fh = system_font_height();
|
int fh = system_font_height();
|
||||||
|
|
||||||
if (g_device_count == 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,21 +687,30 @@ static void render_devices_tab(Canvas& canvas, const mtk::Theme& theme) {
|
|||||||
else
|
else
|
||||||
mtk::draw_list_row(canvas, row, false, (i & 1) != 0, theme);
|
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);
|
fill_circle(canvas, PAD + 6, row.y + ROW_H / 2, 5, dot_color);
|
||||||
|
|
||||||
Rect action = row_action_button_rect(row);
|
Rect action = row_action_button_rect(row);
|
||||||
|
Rect forget = row_forget_button_rect(row);
|
||||||
int text_x = PAD + 20;
|
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);
|
draw_text_fit(canvas, text_x, row.y + 8, g_device_names[i], text_w_max, theme.text);
|
||||||
|
|
||||||
char addr_str[24];
|
char addr_str[24];
|
||||||
format_addr(addr_str, sizeof(addr_str), g_devices[i].bdAddr);
|
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);
|
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,
|
mtk::draw_button(canvas, action, "Disconnect", mtk::BUTTON_DANGER,
|
||||||
button_state(action, true), theme);
|
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);
|
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) {
|
static void render_status(Canvas& canvas, const mtk::Theme& theme) {
|
||||||
Rect status = status_bar_rect();
|
Rect status = status_bar_rect();
|
||||||
int sy = status.y + (status.h - system_font_height()) / 2;
|
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);
|
int left_max = g_win_w - PAD * 2 - (show_status ? status_w + 12 : 0);
|
||||||
if (g_adapter_ok) {
|
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 adapter_info[96];
|
||||||
char addr_str[24];
|
char addr_str[24];
|
||||||
format_addr(addr_str, sizeof(addr_str), g_adapter.bdAddr);
|
format_addr(addr_str, sizeof(addr_str), g_adapter.bdAddr);
|
||||||
const char* name = g_adapter.name[0] ? g_adapter.name : "Adapter";
|
const char* name = g_adapter.name[0] ? g_adapter.name : "Adapter";
|
||||||
snprintf(adapter_info, sizeof(adapter_info), "%s | %s | %d connected",
|
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);
|
draw_text_fit(canvas, PAD, sy, adapter_info, left_max, theme.text_subtle);
|
||||||
} else {
|
} else {
|
||||||
draw_text_fit(canvas, PAD, sy, "No adapter found", left_max, theme.danger);
|
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)
|
if (g_tab == TAB_DEVICES)
|
||||||
render_devices_tab(canvas, theme);
|
render_devices_tab(canvas, theme);
|
||||||
else
|
else if (g_tab == TAB_SCAN)
|
||||||
render_scan_tab(canvas, theme);
|
render_scan_tab(canvas, theme);
|
||||||
|
else
|
||||||
|
render_adapter_tab(canvas, theme);
|
||||||
|
|
||||||
render_status(canvas, theme);
|
render_status(canvas, theme);
|
||||||
}
|
}
|
||||||
@@ -552,6 +867,7 @@ extern "C" void _start() {
|
|||||||
|
|
||||||
refresh_adapter();
|
refresh_adapter();
|
||||||
refresh_devices();
|
refresh_devices();
|
||||||
|
load_mac_config();
|
||||||
clamp_scroll();
|
clamp_scroll();
|
||||||
g_status[0] = 0;
|
g_status[0] = 0;
|
||||||
|
|
||||||
@@ -614,6 +930,17 @@ extern "C" void _start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ev.type == 0 && ev.key.pressed) {
|
if (ev.type == 0 && ev.key.pressed) {
|
||||||
|
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.scancode == 0x01) break;
|
||||||
if ((ev.key.ascii == 's' || ev.key.ascii == 'S') && !g_scanning) {
|
if ((ev.key.ascii == 's' || ev.key.ascii == 'S') && !g_scanning) {
|
||||||
do_scan();
|
do_scan();
|
||||||
@@ -627,6 +954,11 @@ extern "C" void _start() {
|
|||||||
set_tab(TAB_SCAN);
|
set_tab(TAB_SCAN);
|
||||||
redraw = true;
|
redraw = true;
|
||||||
}
|
}
|
||||||
|
if (ev.key.ascii == '3') {
|
||||||
|
set_tab(TAB_ADAPTER);
|
||||||
|
redraw = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ev.type == 1) {
|
if (ev.type == 1) {
|
||||||
@@ -634,6 +966,21 @@ extern "C" void _start() {
|
|||||||
g_mouse_x = ev.mouse.x;
|
g_mouse_x = ev.mouse.x;
|
||||||
g_mouse_y = ev.mouse.y;
|
g_mouse_y = ev.mouse.y;
|
||||||
|
|
||||||
|
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);
|
int new_hover = get_hover_row(g_mouse_x, g_mouse_y);
|
||||||
if (new_hover != g_hover_row) {
|
if (new_hover != g_hover_row) {
|
||||||
g_hover_row = new_hover;
|
g_hover_row = new_hover;
|
||||||
@@ -654,6 +1001,7 @@ extern "C" void _start() {
|
|||||||
redraw = true;
|
redraw = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (redraw) {
|
if (redraw) {
|
||||||
canvas = host.canvas();
|
canvas = host.canvas();
|
||||||
|
|||||||
Reference in New Issue
Block a user