Reusable ble_core (+ optional LVGL ble_ui), flashable firmware/cyd, aligned with wifitest as a base feature. Co-authored-by: Cursor <cursoragent@cursor.com>
278 lines
8.1 KiB
C++
278 lines
8.1 KiB
C++
#include "ble_ui.h"
|
|
#include "ble_core.h"
|
|
#include "pins.h"
|
|
|
|
#include <Arduino.h>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <lvgl.h>
|
|
|
|
static constexpr uint32_t COL_BG = 0x0A1016;
|
|
static constexpr uint32_t COL_INK = 0xEAF0F6;
|
|
static constexpr uint32_t COL_MUTED = 0x7A8B9C;
|
|
static constexpr uint32_t COL_OK = 0x3DDC97;
|
|
static constexpr uint32_t COL_WARN = 0xF0C040;
|
|
static constexpr uint32_t COL_BAD = 0xE85D4C;
|
|
static constexpr uint32_t COL_ACCENT = 0x4FC3F7;
|
|
static constexpr uint32_t COL_BTN = 0x243140;
|
|
static constexpr uint32_t COL_BTN_ACC = 0x1A4A6E;
|
|
static constexpr uint32_t COL_BTN_WARN = 0x6B3A1E;
|
|
|
|
static lv_obj_t *scr = nullptr;
|
|
static lv_obj_t *lbl_title = nullptr;
|
|
static lv_obj_t *lbl_state = nullptr;
|
|
static lv_obj_t *lbl_name = nullptr;
|
|
static lv_obj_t *lbl_addr = nullptr;
|
|
static lv_obj_t *lbl_peer = nullptr;
|
|
static lv_obj_t *lbl_echo = nullptr;
|
|
static lv_obj_t *lbl_log = nullptr;
|
|
static lv_obj_t *btn_adv = nullptr;
|
|
static lv_obj_t *lbl_adv = nullptr;
|
|
|
|
static BleCoreState g_last_state = BLE_CORE_IDLE;
|
|
static bool g_last_echo = true;
|
|
static char g_log[160] = "Waiting for phone...";
|
|
static uint32_t g_last_refresh_ms = 0;
|
|
|
|
static void style_btn(lv_obj_t *btn, uint32_t bg) {
|
|
lv_obj_set_style_radius(btn, 8, 0);
|
|
lv_obj_set_style_bg_color(btn, lv_color_hex(bg), 0);
|
|
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_shadow_width(btn, 0, 0);
|
|
lv_obj_set_style_border_width(btn, 0, 0);
|
|
lv_obj_set_style_pad_all(btn, 0, 0);
|
|
}
|
|
|
|
static lv_obj_t *make_btn(lv_obj_t *parent, const char *text, uint32_t bg,
|
|
lv_event_cb_t cb, int w, int h) {
|
|
lv_obj_t *btn = lv_button_create(parent);
|
|
lv_obj_set_size(btn, w, h);
|
|
style_btn(btn, bg);
|
|
lv_obj_t *lab = lv_label_create(btn);
|
|
lv_label_set_text(lab, text);
|
|
lv_obj_set_style_text_color(lab, lv_color_hex(COL_INK), 0);
|
|
lv_obj_set_style_text_font(lab, &lv_font_montserrat_14, 0);
|
|
lv_obj_center(lab);
|
|
if (cb) {
|
|
lv_obj_add_event_cb(btn, cb, LV_EVENT_CLICKED, nullptr);
|
|
}
|
|
return btn;
|
|
}
|
|
|
|
static void append_log(const char *line) {
|
|
if (!line) {
|
|
return;
|
|
}
|
|
char next[160];
|
|
snprintf(next, sizeof(next), "%s", line);
|
|
// 保留最近两行:旧半 + 新
|
|
char combined[160];
|
|
snprintf(combined, sizeof(combined), "%s\n%s", g_log, next);
|
|
// 若太长,只留新内容
|
|
if (strlen(combined) >= sizeof(g_log)) {
|
|
snprintf(g_log, sizeof(g_log), "%s", next);
|
|
} else {
|
|
snprintf(g_log, sizeof(g_log), "%s", combined);
|
|
}
|
|
if (lbl_log) {
|
|
lv_label_set_text(lbl_log, g_log);
|
|
}
|
|
}
|
|
|
|
static void refresh_static() {
|
|
if (lbl_name) {
|
|
char buf[48];
|
|
snprintf(buf, sizeof(buf), "Name %s", ble_core_name());
|
|
lv_label_set_text(lbl_name, buf);
|
|
}
|
|
if (lbl_addr) {
|
|
char buf[56];
|
|
String a = ble_core_address();
|
|
snprintf(buf, sizeof(buf), "Addr %s", a.length() ? a.c_str() : "-");
|
|
lv_label_set_text(lbl_addr, buf);
|
|
}
|
|
}
|
|
|
|
static void refresh_state() {
|
|
const BleCoreState st = ble_core_state();
|
|
g_last_state = st;
|
|
|
|
uint32_t color = COL_MUTED;
|
|
const char *text = "Idle";
|
|
if (st == BLE_CORE_ADVERTISING) {
|
|
color = COL_WARN;
|
|
text = "Advertising";
|
|
} else if (st == BLE_CORE_CONNECTED) {
|
|
color = COL_OK;
|
|
text = "Connected";
|
|
}
|
|
|
|
if (lbl_state) {
|
|
char buf[40];
|
|
snprintf(buf, sizeof(buf), "State %s", text);
|
|
lv_label_set_text(lbl_state, buf);
|
|
lv_obj_set_style_text_color(lbl_state, lv_color_hex(color), 0);
|
|
}
|
|
|
|
if (lbl_peer) {
|
|
char buf[56];
|
|
if (ble_core_is_connected()) {
|
|
String p = ble_core_peer_address();
|
|
snprintf(buf, sizeof(buf), "Peer %s", p.c_str());
|
|
} else {
|
|
snprintf(buf, sizeof(buf), "Peer -");
|
|
}
|
|
lv_label_set_text(lbl_peer, buf);
|
|
}
|
|
|
|
if (lbl_adv) {
|
|
lv_label_set_text(lbl_adv,
|
|
ble_core_is_advertising() || ble_core_is_connected()
|
|
? "Stop Adv"
|
|
: "Start Adv");
|
|
}
|
|
|
|
if (lbl_echo) {
|
|
char buf[32];
|
|
snprintf(buf, sizeof(buf), "Echo %s", ble_core_echo() ? "ON" : "OFF");
|
|
lv_label_set_text(lbl_echo, buf);
|
|
lv_obj_set_style_text_color(
|
|
lbl_echo, lv_color_hex(ble_core_echo() ? COL_OK : COL_MUTED), 0);
|
|
}
|
|
g_last_echo = ble_core_echo();
|
|
}
|
|
|
|
static void on_adv(lv_event_t *e) {
|
|
(void)e;
|
|
if (ble_core_is_connected()) {
|
|
// 已连接时 Stop Adv 无意义;改为提示
|
|
append_log("Connected — disconnect on phone first");
|
|
return;
|
|
}
|
|
if (ble_core_is_advertising()) {
|
|
ble_core_advertise_stop();
|
|
append_log("Advertising stopped");
|
|
} else {
|
|
ble_core_advertise_start();
|
|
append_log("Advertising started");
|
|
}
|
|
refresh_state();
|
|
}
|
|
|
|
static void on_echo(lv_event_t *e) {
|
|
(void)e;
|
|
ble_core_set_echo(!ble_core_echo());
|
|
append_log(ble_core_echo() ? "Echo ON" : "Echo OFF");
|
|
refresh_state();
|
|
}
|
|
|
|
static void on_ping(lv_event_t *e) {
|
|
(void)e;
|
|
if (!ble_core_is_connected()) {
|
|
append_log("Not connected");
|
|
return;
|
|
}
|
|
if (ble_core_send_str("Hello from CYD\n")) {
|
|
append_log("TX: Hello from CYD");
|
|
} else {
|
|
append_log("TX failed");
|
|
}
|
|
}
|
|
|
|
static void on_clear(lv_event_t *e) {
|
|
(void)e;
|
|
snprintf(g_log, sizeof(g_log), "(cleared)");
|
|
if (lbl_log) {
|
|
lv_label_set_text(lbl_log, g_log);
|
|
}
|
|
}
|
|
|
|
void ble_ui_create() {
|
|
scr = lv_obj_create(nullptr);
|
|
lv_obj_set_style_bg_color(scr, lv_color_hex(COL_BG), 0);
|
|
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
|
|
lv_obj_clear_flag(scr, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_screen_load(scr);
|
|
|
|
lbl_title = lv_label_create(scr);
|
|
lv_label_set_text(lbl_title, "CYD BLE");
|
|
lv_obj_set_style_text_color(lbl_title, lv_color_hex(COL_ACCENT), 0);
|
|
lv_obj_set_style_text_font(lbl_title, &lv_font_montserrat_20, 0);
|
|
lv_obj_set_pos(lbl_title, 10, 6);
|
|
|
|
lbl_state = lv_label_create(scr);
|
|
lv_obj_set_style_text_font(lbl_state, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_pos(lbl_state, 10, 34);
|
|
|
|
lbl_name = lv_label_create(scr);
|
|
lv_obj_set_style_text_color(lbl_name, lv_color_hex(COL_INK), 0);
|
|
lv_obj_set_style_text_font(lbl_name, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_pos(lbl_name, 10, 54);
|
|
|
|
lbl_addr = lv_label_create(scr);
|
|
lv_obj_set_style_text_color(lbl_addr, lv_color_hex(COL_MUTED), 0);
|
|
lv_obj_set_style_text_font(lbl_addr, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_pos(lbl_addr, 10, 74);
|
|
|
|
lbl_peer = lv_label_create(scr);
|
|
lv_obj_set_style_text_color(lbl_peer, lv_color_hex(COL_MUTED), 0);
|
|
lv_obj_set_style_text_font(lbl_peer, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_pos(lbl_peer, 10, 94);
|
|
|
|
lbl_echo = lv_label_create(scr);
|
|
lv_obj_set_style_text_font(lbl_echo, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_pos(lbl_echo, 10, 114);
|
|
|
|
lv_obj_t *hint = lv_label_create(scr);
|
|
lv_label_set_text(hint, "Phone: nRF Connect / Serial BT Terminal");
|
|
lv_obj_set_style_text_color(hint, lv_color_hex(COL_MUTED), 0);
|
|
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_pos(hint, 10, 134);
|
|
|
|
lbl_log = lv_label_create(scr);
|
|
lv_label_set_text(lbl_log, g_log);
|
|
lv_label_set_long_mode(lbl_log, LV_LABEL_LONG_WRAP);
|
|
lv_obj_set_width(lbl_log, UI_HOR_RES - 20);
|
|
lv_obj_set_style_text_color(lbl_log, lv_color_hex(COL_INK), 0);
|
|
lv_obj_set_style_text_font(lbl_log, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_pos(lbl_log, 10, 156);
|
|
|
|
const int bw = 72;
|
|
const int bh = 36;
|
|
const int y = UI_VER_RES - bh - 8;
|
|
btn_adv = make_btn(scr, "Stop Adv", COL_BTN_ACC, on_adv, bw, bh);
|
|
lbl_adv = lv_obj_get_child(btn_adv, 0);
|
|
lv_obj_set_pos(btn_adv, 8, y);
|
|
|
|
lv_obj_t *btn_echo = make_btn(scr, "Echo", COL_BTN, on_echo, bw, bh);
|
|
lv_obj_set_pos(btn_echo, 8 + bw + 6, y);
|
|
|
|
lv_obj_t *btn_ping = make_btn(scr, "Ping", COL_BTN_ACC, on_ping, bw, bh);
|
|
lv_obj_set_pos(btn_ping, 8 + 2 * (bw + 6), y);
|
|
|
|
lv_obj_t *btn_clear = make_btn(scr, "Clear", COL_BTN_WARN, on_clear, bw, bh);
|
|
lv_obj_set_pos(btn_clear, 8 + 3 * (bw + 6), y);
|
|
|
|
refresh_static();
|
|
refresh_state();
|
|
}
|
|
|
|
void ble_ui_tick() {
|
|
BleRxPacket pkt;
|
|
while (ble_core_poll_rx(&pkt)) {
|
|
char line[BLE_CORE_RX_MAX + 8];
|
|
snprintf(line, sizeof(line), "RX: %s", pkt.data);
|
|
append_log(line);
|
|
}
|
|
|
|
const uint32_t now = millis();
|
|
if (now - g_last_refresh_ms < 300) {
|
|
return;
|
|
}
|
|
g_last_refresh_ms = now;
|
|
|
|
if (ble_core_state() != g_last_state || ble_core_echo() != g_last_echo) {
|
|
refresh_state();
|
|
}
|
|
}
|