Files
MontaukOS/programs/src/bluetooth/main.cpp
T

668 lines
20 KiB
C++

/*
* main.cpp
* MontaukOS Bluetooth Manager
* Copyright (c) 2026 Daniel Hammer
*/
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <montauk/heap.h>
#include <gui/mtk.hpp>
#include <gui/standalone.hpp>
extern "C" {
#include <string.h>
#include <stdio.h>
}
using namespace gui;
// ============================================================================
// Constants
// ============================================================================
static constexpr int WIN_W = 360;
static constexpr int WIN_H = 420;
static constexpr int ROW_H = 56;
static constexpr int TAB_H = 36;
static constexpr int PAD = 16;
static constexpr int BTN_W = 90;
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 MAX_SCAN = 16;
static constexpr int MAX_CONNECTED = 8;
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 DIM_TEXT = Color::from_rgb(0x66, 0x66, 0x66);
static constexpr Color ACCENT = Color::from_rgb(0x36, 0x7B, 0xF0);
static constexpr Color ROW_HOVER = Color::from_rgb(0xF0, 0xF4, 0xFF);
static constexpr Color BORDER = Color::from_rgb(0xCC, 0xCC, 0xCC);
static constexpr Color WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF);
static constexpr Color GREEN = Color::from_rgb(0x2E, 0xA0, 0x43);
static constexpr Color RED = Color::from_rgb(0xCC, 0x33, 0x33);
// ============================================================================
// State
// ============================================================================
enum Tab {
TAB_DEVICES = 0,
TAB_SCAN = 1,
TAB_COUNT = 2,
};
static const char* g_tab_labels[TAB_COUNT] = {
"Devices",
"Scan",
};
static int g_win_w = WIN_W;
static int g_win_h = WIN_H;
static Tab g_tab = TAB_DEVICES;
static int g_mouse_x = -1;
static int g_mouse_y = -1;
static int g_hover_row = -1;
// Adapter info
static Montauk::BtAdapterInfo g_adapter;
static bool g_adapter_ok = false;
// Connected/paired devices
static Montauk::BtDevInfo g_devices[MAX_CONNECTED];
static char g_device_names[MAX_CONNECTED][64];
static int g_device_count = 0;
// Scan results
static Montauk::BtScanResult g_scan[MAX_SCAN];
static int g_scan_count = 0;
static bool g_scanning = false;
// Scroll
static int g_scroll = 0;
// Status message
static char g_status[80];
static uint64_t g_status_time = 0;
// ============================================================================
// Theme and layout
// ============================================================================
static mtk::Theme bt_theme() {
mtk::Theme theme = mtk::make_theme();
theme.window_bg = BG_COLOR;
theme.surface = Color::from_rgb(0xF5, 0xF5, 0xF5);
theme.surface_alt = Color::from_rgb(0xF8, 0xF8, 0xF8);
theme.surface_hover = ROW_HOVER;
theme.border = BORDER;
theme.text = TEXT_COLOR;
theme.text_muted = Color::from_rgb(0x88, 0x88, 0x88);
theme.text_subtle = DIM_TEXT;
theme.accent_soft = ROW_HOVER;
theme.danger = RED;
theme.danger_hover = mtk::darken(RED, 18);
theme.disabled_bg = Color::from_rgb(0xE8, 0xE8, 0xE8);
theme.disabled_fg = Color::from_rgb(0x9A, 0x9A, 0x9A);
return theme;
}
static Rect tab_bar_rect() {
return {0, 0, g_win_w, TAB_H};
}
static Rect content_rect() {
return {0, TAB_H, g_win_w, gui_max(g_win_h - TAB_H - STATUS_H, 0)};
}
static Rect status_bar_rect() {
return {0, g_win_h - STATUS_H, g_win_w, STATUS_H};
}
static Rect scan_button_rect() {
Rect content = content_rect();
return {(g_win_w - SCAN_BTN_W) / 2, content.y + 12, SCAN_BTN_W, BTN_H};
}
static int devices_list_top() {
return content_rect().y;
}
static int devices_list_bottom() {
return status_bar_rect().y;
}
static int scan_list_top() {
Rect scan_btn = scan_button_rect();
return scan_btn.y + scan_btn.h + 12;
}
static int scan_list_bottom() {
return status_bar_rect().y;
}
static Rect devices_row_rect(int index) {
return {0, devices_list_top() + index * ROW_H - g_scroll, g_win_w, ROW_H};
}
static Rect scan_row_rect(int index) {
return {0, scan_list_top() + index * ROW_H - g_scroll, g_win_w, ROW_H};
}
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};
}
static int current_row_count() {
return g_tab == TAB_DEVICES ? g_device_count : g_scan_count;
}
static int current_list_top() {
return g_tab == TAB_DEVICES ? devices_list_top() : scan_list_top();
}
static int current_list_bottom() {
return g_tab == TAB_DEVICES ? devices_list_bottom() : scan_list_bottom();
}
static int max_scroll() {
int visible_h = gui_max(current_list_bottom() - current_list_top(), 0);
int total_h = current_row_count() * ROW_H;
if (total_h <= visible_h) return 0;
return total_h - visible_h;
}
static void clamp_scroll() {
g_scroll = gui_clamp(g_scroll, 0, max_scroll());
}
static void draw_text_fit(Canvas& c, int x, int y,
const char* text, int max_w, Color color) {
if (!text || !text[0] || max_w <= 4) return;
if (text_width(text) <= max_w) {
c.text(x, y, text, color);
return;
}
const char* ellipsis = "...";
int ell_w = text_width(ellipsis);
if (ell_w >= max_w) return;
char buf[96];
int out = 0;
while (text[out] && out < (int)sizeof(buf) - 4) {
buf[out] = text[out];
buf[out + 1] = '\0';
if (text_width(buf) + ell_w > max_w)
break;
out++;
}
if (out <= 0) return;
buf[out] = '.';
buf[out + 1] = '.';
buf[out + 2] = '.';
buf[out + 3] = '\0';
c.text(x, y, buf, color);
}
static void draw_centered_text(Canvas& c, int area_y, int area_h, const char* text, Color color) {
int tw = text_width(text);
int th = system_font_height();
c.text((g_win_w - tw) / 2, area_y + (area_h - th) / 2, text, color);
}
static bool mouse_in_rect(const Rect& rect) {
return rect.contains(g_mouse_x, g_mouse_y);
}
static mtk::WidgetState button_state(const Rect& rect, bool enabled, bool active = false) {
return mtk::widget_state(active, mouse_in_rect(rect), enabled);
}
// ============================================================================
// Bluetooth helpers
// ============================================================================
static void set_status(const char* msg) {
snprintf(g_status, sizeof(g_status), "%s", msg);
g_status_time = montauk::get_milliseconds();
}
static void refresh_adapter() {
montauk::memset(&g_adapter, 0, sizeof(g_adapter));
int r = montauk::bt_info(&g_adapter);
g_adapter_ok = (r >= 0 && g_adapter.initialized);
}
static void refresh_devices() {
g_device_count = montauk::bt_list(g_devices, MAX_CONNECTED);
if (g_device_count < 0) g_device_count = 0;
for (int i = 0; i < g_device_count; i++) {
bool found = false;
for (int j = 0; j < g_scan_count; j++) {
if (memcmp(g_devices[i].bdAddr, g_scan[j].bdAddr, 6) == 0 && g_scan[j].name[0]) {
montauk::memcpy(g_device_names[i], g_scan[j].name, 64);
found = true;
break;
}
}
if (!found) {
snprintf(g_device_names[i], 64, "%02X:%02X:%02X:%02X:%02X:%02X",
g_devices[i].bdAddr[0], g_devices[i].bdAddr[1],
g_devices[i].bdAddr[2], g_devices[i].bdAddr[3],
g_devices[i].bdAddr[4], g_devices[i].bdAddr[5]);
}
}
}
static void do_scan() {
g_scanning = true;
g_scan_count = 0;
g_scroll = 0;
g_hover_row = -1;
set_status("Scanning...");
}
static void finish_scan() {
int r = montauk::bt_scan(g_scan, MAX_SCAN, 3000);
g_scan_count = r > 0 ? r : 0;
g_scanning = false;
clamp_scroll();
char msg[64];
snprintf(msg, sizeof(msg), "Found %d device%s", g_scan_count, g_scan_count == 1 ? "" : "s");
set_status(msg);
}
static void do_connect(const uint8_t* addr) {
int r = montauk::bt_connect(addr);
if (r >= 0) set_status("Connected");
else set_status("Connection failed");
refresh_devices();
clamp_scroll();
}
static void do_disconnect(const uint8_t* addr) {
int r = montauk::bt_disconnect(addr);
if (r >= 0) set_status("Disconnected");
else set_status("Disconnect 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)
return true;
}
return false;
}
// ============================================================================
// Format helpers
// ============================================================================
static const char* device_class_str(uint32_t cod) {
uint32_t major = (cod >> 8) & 0x1F;
switch (major) {
case 1: return "Computer";
case 2: return "Phone";
case 3: return "Network";
case 4: return "Audio/Video";
case 5: return "Peripheral";
case 6: return "Imaging";
case 7: return "Wearable";
default: return "Device";
}
}
static void format_addr(char* buf, int len, const uint8_t* addr) {
snprintf(buf, len, "%02X:%02X:%02X:%02X:%02X:%02X",
addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
}
static const char* rssi_bar(int8_t rssi) {
if (rssi >= -50) return "Strong";
if (rssi >= -70) return "Good";
if (rssi >= -85) return "Weak";
return "Very weak";
}
static void set_tab(Tab tab) {
if (tab == g_tab) return;
g_tab = tab;
g_scroll = 0;
g_hover_row = -1;
clamp_scroll();
}
// ============================================================================
// Hover and hit testing
// ============================================================================
static int get_hover_row(int mx, int my) {
if (g_tab == TAB_DEVICES) {
for (int i = 0; i < g_device_count; i++) {
if (devices_row_rect(i).contains(mx, my))
return i;
}
} else {
for (int i = 0; i < g_scan_count; i++) {
if (scan_row_rect(i).contains(mx, my))
return i;
}
}
return -1;
}
static bool handle_click(int mx, int my) {
int tab = mtk::hit_tab_bar(tab_bar_rect(), TAB_COUNT, mx, my);
if (tab >= 0) {
set_tab((Tab)tab);
return true;
}
if (g_tab == TAB_SCAN) {
Rect scan_btn = scan_button_rect();
if (!g_scanning && scan_btn.contains(mx, my)) {
do_scan();
return true;
}
for (int i = 0; i < g_scan_count; i++) {
Rect row = scan_row_rect(i);
if (!row.contains(mx, my)) continue;
Rect action = row_action_button_rect(row);
if (action.contains(mx, my)) {
if (is_connected(g_scan[i].bdAddr))
do_disconnect(g_scan[i].bdAddr);
else
do_connect(g_scan[i].bdAddr);
return true;
}
}
} else {
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;
}
}
}
return false;
}
// ============================================================================
// Render
// ============================================================================
static void render_devices_tab(Canvas& canvas, const mtk::Theme& theme) {
Rect list = {0, devices_list_top(), g_win_w, gui_max(devices_list_bottom() - devices_list_top(), 0)};
int fh = system_font_height();
if (g_device_count == 0) {
draw_centered_text(canvas, list.y, list.h, "No connected devices", theme.text_subtle);
return;
}
for (int i = 0; i < g_device_count; i++) {
Rect row = devices_row_rect(i);
if (row.y + row.h <= list.y || row.y >= list.y + list.h) continue;
if (g_hover_row == i)
canvas.fill_rect(row.x, row.y, row.w, row.h, theme.surface_hover);
else
mtk::draw_list_row(canvas, row, false, (i & 1) != 0, theme);
Color dot_color = g_devices[i].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);
int text_x = PAD + 20;
int text_w_max = (g_devices[i].connected ? action.x : g_win_w - PAD) - 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) {
mtk::draw_button(canvas, action, "Disconnect", mtk::BUTTON_DANGER,
button_state(action, true), theme);
}
mtk::draw_separator(canvas, PAD, row.y + ROW_H - 1, g_win_w - 2 * PAD, theme);
}
}
static void render_scan_tab(Canvas& canvas, const mtk::Theme& theme) {
Rect scan_btn = scan_button_rect();
Rect list = {0, scan_list_top(), g_win_w, gui_max(scan_list_bottom() - scan_list_top(), 0)};
int fh = system_font_height();
if (g_scanning) {
mtk::draw_button(canvas, scan_btn, "Scanning...", mtk::BUTTON_SECONDARY,
button_state(scan_btn, false), theme);
} else {
mtk::draw_button(canvas, scan_btn, "Scan", mtk::BUTTON_PRIMARY,
button_state(scan_btn, true), theme);
}
if (g_scan_count == 0 && !g_scanning) {
draw_centered_text(canvas, list.y, list.h, "Press Scan to find devices", theme.text_subtle);
return;
}
for (int i = 0; i < g_scan_count; i++) {
Rect row = scan_row_rect(i);
if (row.y + row.h <= list.y || row.y >= list.y + list.h) continue;
if (g_hover_row == i)
canvas.fill_rect(row.x, row.y, row.w, row.h, theme.surface_hover);
else
mtk::draw_list_row(canvas, row, false, (i & 1) != 0, theme);
fill_circle(canvas, PAD + 6, row.y + ROW_H / 2, 5, theme.accent);
Rect action = row_action_button_rect(row);
int text_x = PAD + 20;
int text_w_max = action.x - text_x - 10;
const char* display_name = g_scan[i].name[0] ? g_scan[i].name : "Unknown Device";
draw_text_fit(canvas, text_x, row.y + 4, display_name, text_w_max, theme.text);
char detail[80];
char addr_str[24];
format_addr(addr_str, sizeof(addr_str), g_scan[i].bdAddr);
snprintf(detail, sizeof(detail), "%s | %s | %s",
device_class_str(g_scan[i].classOfDevice), addr_str, rssi_bar(g_scan[i].rssi));
draw_text_fit(canvas, text_x, row.y + 4 + fh + 2, detail, text_w_max, theme.text_subtle);
bool conn = is_connected(g_scan[i].bdAddr);
mtk::draw_button(canvas, action, conn ? "Disconnect" : "Connect",
conn ? mtk::BUTTON_DANGER : mtk::BUTTON_PRIMARY,
button_state(action, true), theme);
mtk::draw_separator(canvas, PAD, row.y + ROW_H - 1, g_win_w - 2 * PAD, theme);
}
}
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;
uint64_t now = montauk::get_milliseconds();
canvas.fill_rect(status.x, status.y, status.w, status.h, theme.surface);
mtk::draw_separator(canvas, 0, status.y, g_win_w, theme);
int status_w = 0;
bool show_status = g_status[0] && (now - g_status_time) < 5000;
if (show_status)
status_w = text_width(g_status);
int left_max = g_win_w - PAD * 2 - (show_status ? status_w + 12 : 0);
if (g_adapter_ok) {
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);
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);
}
if (show_status)
canvas.text(g_win_w - PAD - status_w, sy, g_status, theme.text_subtle);
}
static void render(Canvas& canvas) {
mtk::Theme theme = bt_theme();
canvas.fill(theme.window_bg);
mtk::draw_tab_bar(canvas, tab_bar_rect(), g_tab_labels, TAB_COUNT, g_tab, theme);
if (g_tab == TAB_DEVICES)
render_devices_tab(canvas, theme);
else
render_scan_tab(canvas, theme);
render_status(canvas, theme);
}
// ============================================================================
// Entry point
// ============================================================================
extern "C" void _start() {
if (!fonts::init())
montauk::exit(1);
refresh_adapter();
refresh_devices();
clamp_scroll();
g_status[0] = 0;
WsWindow win;
if (!win.create("Bluetooth", WIN_W, WIN_H))
montauk::exit(1);
mtk::StandaloneHost host(&win);
Canvas canvas = host.canvas();
render(canvas);
host.present();
uint64_t last_refresh = montauk::get_milliseconds();
while (true) {
Montauk::WinEvent ev;
int r = win.poll(&ev);
if (r < 0) break;
if (g_scanning) {
canvas = host.canvas();
render(canvas);
host.present();
finish_scan();
refresh_devices();
g_hover_row = get_hover_row(g_mouse_x, g_mouse_y);
canvas = host.canvas();
render(canvas);
host.present();
continue;
}
if (r == 0) {
uint64_t now = montauk::get_milliseconds();
if (now - last_refresh >= 5000) {
refresh_adapter();
refresh_devices();
clamp_scroll();
g_hover_row = get_hover_row(g_mouse_x, g_mouse_y);
last_refresh = now;
canvas = host.canvas();
render(canvas);
host.present();
}
montauk::sleep_ms(16);
continue;
}
bool redraw = false;
if (ev.type == 3) break;
if (ev.type == 2) {
g_win_w = win.width;
g_win_h = win.height;
clamp_scroll();
g_hover_row = get_hover_row(g_mouse_x, g_mouse_y);
redraw = true;
}
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 (ev.type == 1) {
bool moved = (ev.mouse.x != g_mouse_x) || (ev.mouse.y != g_mouse_y);
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 (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;
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 (redraw) {
canvas = host.canvas();
render(canvas);
host.present();
}
}
win.destroy();
montauk::exit(0);
}