Add double/long press, redesign UI to v0.3 with circular d-pad

- Double press and long press detection per button (5 buttons × 3 event
  types = 15 distinct BTHome events)
- New circular d-pad UI: large ring with chevron arrows, numbered buttons
  (1=centre, 2=up, 3=down, 4=left, 5=right) outside/inside the ring
- Bluetooth icon asset (bluetooth_10x10.png) in top-left via canvas_draw_icon
- Back arrow icon (Pin_back_arrow_10x8.png) bottom-right as exit hint
- Version v0.3 bottom-left; direction + event label centred when active
- Thread-safe timer callbacks via view_dispatcher_send_custom_event

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYNXvRg4xjHHE9oPH9kYYU
This commit is contained in:
Jon
2026-06-29 14:48:18 +01:00
parent 87c8251c17
commit 96b5884a87
6 changed files with 369 additions and 184 deletions

View File

@@ -5,8 +5,8 @@ App(
entry_point="bt_home_controller_app", entry_point="bt_home_controller_app",
stack_size=2 * 1024, stack_size=2 * 1024,
fap_category="Bluetooth", fap_category="Bluetooth",
fap_version="0.1", fap_version="0.3",
fap_icon="bthome.png", fap_icon="assets/bluetooth_10x10.png",
fap_description="BTHome D-pad controller for the Flipper Zero", fap_description="BTHome D-pad controller for the Flipper Zero",
fap_author="Alessandro Ghedini", fap_author="Alessandro Ghedini",
fap_weburl="https://github.com/ghedo/flipper-bthome", fap_weburl="https://github.com/ghedo/flipper-bthome",

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

BIN
assets/bluetooth_10x10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

BIN
assets/hid_ble_10px.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

View File

@@ -1,13 +1,12 @@
#include <furi.h> #include <furi.h>
#include <furi_hal_bt.h> #include <furi_hal_bt.h>
#include <furi_hal_power.h> #include <furi_hal_power.h>
#include <bt_home_controller_icons.h>
#include <gui/gui.h> #include <gui/gui.h>
#include <gui/view_dispatcher.h> #include <gui/view_dispatcher.h>
#include <gui/view.h> #include <gui/view.h>
#include <bt_home_controller_icons.h>
// BTHome UUID: 0xFCD2 (little-endian). // BTHome UUID: 0xFCD2 (little-endian).
#define BTHOME_UUID_LSB 0xD2 #define BTHOME_UUID_LSB 0xD2
#define BTHOME_UUID_MSB 0xFC #define BTHOME_UUID_MSB 0xFC
@@ -22,18 +21,19 @@
#define BTHOME_OBJ_BATTERY 0x01 #define BTHOME_OBJ_BATTERY 0x01
#define BTHOME_OBJ_BUTTON 0x3A #define BTHOME_OBJ_BUTTON 0x3A
// Button event values. // BTHome v2 button event values.
#define BTHOME_BUTTON_EVENT_NONE 0x00 #define BTHOME_BUTTON_NONE 0x00
#define BTHOME_BUTTON_EVENT_PRESS 0x01 #define BTHOME_BUTTON_PRESS 0x01
#define BTHOME_BUTTON_EVENT_LONG_PRESS 0x04 #define BTHOME_BUTTON_DOUBLE 0x02
#define BTHOME_BUTTON_LONG 0x04
// App values.
#define APP_LOG_TAG "BTHOME" #define APP_LOG_TAG "BTHOME"
#define APP_VIEW_MAIN 0 #define APP_VIEW_MAIN 0
#define APP_BEACON_TIMER 1000 #define APP_BEACON_MS 1000 // how long to keep beacon alive after event
#define DOUBLE_PRESS_MS 350 // window for detecting a second tap
// D-pad button indices — order determines BTHome button numbering (button, button_2 … button_5). // D-pad button indices — order determines BTHome button numbering
// (button, button_2 ... button_5 in HA).
#define BTN_OK 0 #define BTN_OK 0
#define BTN_UP 1 #define BTN_UP 1
#define BTN_DOWN 2 #define BTN_DOWN 2
@@ -41,31 +41,59 @@
#define BTN_RIGHT 4 #define BTN_RIGHT 4
#define BTN_COUNT 5 #define BTN_COUNT 5
// Payload offsets. // Custom events routed through ViewDispatcher (thread-safe).
// Values 0..BTN_COUNT-1 = double-press window expired for that button.
// Value BTN_COUNT = beacon clear.
#define APP_EV_DOUBLE_TIMEOUT(btn) ((uint32_t)(btn))
#define APP_EV_BEACON_OFF ((uint32_t)BTN_COUNT)
// Payload byte offsets — filled by bthome_init_beacon_payload().
static size_t payload_battery_value_off = 0; static size_t payload_battery_value_off = 0;
static size_t payload_packet_id_value_off = 0; static size_t payload_packet_id_value_off = 0;
static size_t payload_button_off[BTN_COUNT] = {0}; static size_t payload_button_off[BTN_COUNT] = {0};
typedef struct BtHomeApp BtHomeApp;
// Per-button context for double-press timer callbacks.
typedef struct { typedef struct {
ViewDispatcher *view_dispatcher; BtHomeApp* app;
int btn;
View *main_view; } BtnCtx;
struct BtHomeApp {
ViewDispatcher* view_dispatcher;
View* main_view;
uint8_t packet_id; uint8_t packet_id;
uint8_t beacon_payload[EXTRA_BEACON_MAX_DATA_SIZE]; uint8_t beacon_payload[EXTRA_BEACON_MAX_DATA_SIZE];
size_t beacon_payload_len; size_t beacon_payload_len;
FuriTimer* beacon_timer;
FuriTimer *beacon_timer; FuriTimer* double_timers[BTN_COUNT];
} BtHomeApp; BtnCtx btn_ctx[BTN_COUNT];
bool btn_pending[BTN_COUNT]; // true while double-press window is open
};
typedef struct { typedef struct {
InputKey active_key; InputKey active_key;
bool is_pressed; bool is_pressed;
uint8_t last_event; // last BTHOME_BUTTON_* value dispatched
InputKey last_btn_key; // which key the last event was for
} BtHomeModel; } BtHomeModel;
// Circular d-pad geometry. Screen 128×64; no title bar; label strip y=54..63.
#define DPAD_CX 64 // centre x — horizontally centred
#define DPAD_CY 32 // centre y — near vertical centre, leaves strip room at bottom
#define DPAD_R 24 // outer circle radius (enlarged to fill freed title-bar space)
#define DPAD_R_OK 6 // centre (OK) button radius
#define DPAD_DISC_D 15 // distance from centre to active-segment disc centre
#define DPAD_DISC_R 8 // active-segment disc radius
#define DPAD_ARR_D 19 // distance from centre to arrow tip
#define DPAD_ARR_B 14 // distance from centre to arrow base (closer than tip)
#define DPAD_ARR_W 5 // half-width of arrow at its base
/* ── helpers ─────────────────────────────────────────────────── */
static int bthome_key_to_btn(InputKey key) { static int bthome_key_to_btn(InputKey key) {
switch (key) { switch(key) {
case InputKeyOk: return BTN_OK; case InputKeyOk: return BTN_OK;
case InputKeyUp: return BTN_UP; case InputKeyUp: return BTN_UP;
case InputKeyDown: return BTN_DOWN; case InputKeyDown: return BTN_DOWN;
@@ -75,110 +103,277 @@ static int bthome_key_to_btn(InputKey key) {
} }
} }
// Draws a 11x11 D-pad at (x, y). The active cell is filled; others are outlined. static InputKey bthome_btn_to_key(int btn) {
// Layout (each cell 3x3, 1px gap): switch(btn) {
// [U] case BTN_OK: return InputKeyOk;
// [L][O][R] case BTN_UP: return InputKeyUp;
// [D] case BTN_DOWN: return InputKeyDown;
static void bthome_draw_dpad(Canvas *canvas, int x, int y, bool is_pressed, InputKey active_key) { case BTN_LEFT: return InputKeyLeft;
const struct { InputKey key; int dx; int dy; } cells[BTN_COUNT] = { case BTN_RIGHT: return InputKeyRight;
{ InputKeyOk, 4, 4 }, default: return InputKeyOk;
{ InputKeyUp, 4, 0 },
{ InputKeyDown, 4, 8 },
{ InputKeyLeft, 0, 4 },
{ InputKeyRight, 8, 4 },
};
for (int i = 0; i < BTN_COUNT; i++) {
int cx = x + cells[i].dx;
int cy = y + cells[i].dy;
if (is_pressed && active_key == cells[i].key) {
canvas_draw_box(canvas, cx, cy, 3, 3);
} else {
canvas_draw_frame(canvas, cx, cy, 3, 3);
}
} }
} }
static void bthome_draw_callback(Canvas *canvas, void *ctx) { static const char* bthome_key_to_direction(InputKey key) {
BtHomeModel *model = ctx; switch(key) {
case InputKeyOk: return "centre";
case InputKeyUp: return "up";
case InputKeyDown: return "down";
case InputKeyLeft: return "left";
case InputKeyRight: return "right";
default: return "";
}
}
// Circular d-pad centred at (DPAD_CX, DPAD_CY).
// Draw order ensures a clean segmented look:
// 1. Outer circle border
// 2. Cross dividers (horizontal + vertical)
// 3. Active-segment filled disc (covers cross lines in that quadrant)
// 4. Centre OK circle (outline or filled)
// 5. Chevron arrows (white on active segment, black elsewhere)
static void bthome_draw_dpad(Canvas* canvas, InputKey active_key, uint8_t ev) {
const int cx = DPAD_CX, cy = DPAD_CY;
bool has_ev = (ev != BTHOME_BUTTON_NONE);
bool ok_active = has_ev && (active_key == InputKeyOk);
canvas_set_color(canvas, ColorBlack);
// 1. Outer circle.
canvas_draw_circle(canvas, cx, cy, DPAD_R);
// 2. Cross dividers — omitted for cleaner look.
// 3. Filled disc for the active directional button.
if(has_ev && !ok_active) {
int dx = 0, dy = 0;
if(active_key == InputKeyUp) dy = -DPAD_DISC_D;
else if(active_key == InputKeyDown) dy = +DPAD_DISC_D;
else if(active_key == InputKeyLeft) dx = -DPAD_DISC_D;
else if(active_key == InputKeyRight) dx = +DPAD_DISC_D;
canvas_draw_disc(canvas, cx + dx, cy + dy, DPAD_DISC_R);
}
// 4. Centre OK circle.
if(ok_active)
canvas_draw_disc(canvas, cx, cy, DPAD_R_OK);
else
canvas_draw_circle(canvas, cx, cy, DPAD_R_OK);
// 5. Chevron arrows — white when that direction is active.
const struct {
InputKey key;
int tx, ty; // tip
int lx, ly, rx, ry; // left-arm end, right-arm end
} arrows[4] = {
{InputKeyUp, cx, cy - DPAD_ARR_D,
cx - DPAD_ARR_W, cy - DPAD_ARR_B,
cx + DPAD_ARR_W, cy - DPAD_ARR_B},
{InputKeyDown, cx, cy + DPAD_ARR_D,
cx - DPAD_ARR_W, cy + DPAD_ARR_B,
cx + DPAD_ARR_W, cy + DPAD_ARR_B},
{InputKeyLeft, cx - DPAD_ARR_D, cy,
cx - DPAD_ARR_B, cy - DPAD_ARR_W,
cx - DPAD_ARR_B, cy + DPAD_ARR_W},
{InputKeyRight, cx + DPAD_ARR_D, cy,
cx + DPAD_ARR_B, cy - DPAD_ARR_W,
cx + DPAD_ARR_B, cy + DPAD_ARR_W},
};
for(int i = 0; i < 4; i++) {
bool active = has_ev && (active_key == arrows[i].key);
canvas_set_color(canvas, active ? ColorWhite : ColorBlack);
canvas_draw_line(canvas, arrows[i].tx, arrows[i].ty, arrows[i].lx, arrows[i].ly);
canvas_draw_line(canvas, arrows[i].tx, arrows[i].ty, arrows[i].rx, arrows[i].ry);
}
// 6. Button numbers.
canvas_set_font(canvas, FontSecondary);
// "1" inside OK circle — inverts white when active.
canvas_set_color(canvas, ok_active ? ColorWhite : ColorBlack);
canvas_draw_str_aligned(canvas, cx, cy, AlignCenter, AlignCenter, "1");
// 2-5 outside the ring — always black (on white background).
canvas_set_color(canvas, ColorBlack);
canvas_draw_str_aligned(canvas, cx, cy - DPAD_R - 4, AlignCenter, AlignCenter, "2");
canvas_draw_str_aligned(canvas, cx, cy + DPAD_R + 5, AlignCenter, AlignCenter, "3");
canvas_draw_str_aligned(canvas, cx - DPAD_R - 5, cy, AlignCenter, AlignCenter, "4");
canvas_draw_str_aligned(canvas, cx + DPAD_R + 5, cy, AlignCenter, AlignCenter, "5");
}
/* ── beacon helpers ──────────────────────────────────────────── */
// Must only be called from the ViewDispatcher (GUI) thread.
static void bthome_send_event(BtHomeApp* app, int btn, uint8_t ev_val) {
app->beacon_payload[payload_packet_id_value_off] = app->packet_id++;
app->beacon_payload[payload_battery_value_off] = furi_hal_power_get_pct();
for(int i = 0; i < BTN_COUNT; i++)
app->beacon_payload[payload_button_off[i]] = BTHOME_BUTTON_NONE;
app->beacon_payload[payload_button_off[btn]] = ev_val;
furi_hal_bt_extra_beacon_set_data(app->beacon_payload, app->beacon_payload_len);
if(!furi_hal_bt_extra_beacon_is_active())
furi_hal_bt_extra_beacon_start();
furi_timer_start(app->beacon_timer, APP_BEACON_MS);
with_view_model(
app->main_view,
BtHomeModel* model,
{
model->last_event = ev_val;
model->last_btn_key = bthome_btn_to_key(btn);
},
true);
}
/* ── draw ────────────────────────────────────────────────────── */
static void bthome_draw_callback(Canvas* canvas, void* ctx) {
BtHomeModel* model = ctx;
furi_assert(model); furi_assert(model);
canvas_clear(canvas); canvas_clear(canvas);
canvas_set_color(canvas, ColorBlack);
canvas_set_bitmap_mode(canvas, true); // BT icon — top left, no title text, no separator.
canvas_draw_icon(canvas, 2, 1, &I_bluetooth_10x10);
// Logo moved to y=0 to leave 12px at the bottom for controls. // D-pad: show last-sent event (1 s) → physical hold → idle.
canvas_draw_icon(canvas, 3, 0, &I_bthome_123x52); InputKey highlight_key;
uint8_t highlight_ev;
if(model->last_event != BTHOME_BUTTON_NONE) {
highlight_key = model->last_btn_key;
highlight_ev = model->last_event;
} else if(model->is_pressed) {
highlight_key = model->active_key;
highlight_ev = BTHOME_BUTTON_PRESS;
} else {
highlight_key = InputKeyOk;
highlight_ev = BTHOME_BUTTON_NONE;
}
bthome_draw_dpad(canvas, highlight_key, highlight_ev);
canvas_set_font(canvas, FontSecondary); canvas_set_font(canvas, FontSecondary);
canvas_draw_str_aligned(canvas, 3, 54, AlignLeft, AlignTop, "D-pad to send"); // Left: version.
canvas_draw_str_aligned(canvas, 2, 56, AlignLeft, AlignTop, "v0.3");
bthome_draw_dpad(canvas, 107, 53, model->is_pressed, model->active_key); // Right: back-arrow icon (exit hint).
canvas_draw_icon(canvas, 116, 56, &I_Pin_back_arrow_10x8);
// Centre: direction + event type when active.
if(model->last_event != BTHOME_BUTTON_NONE) {
const char* dir = bthome_key_to_direction(model->last_btn_key);
const char* ev_type =
model->last_event == BTHOME_BUTTON_DOUBLE ? "double press" :
model->last_event == BTHOME_BUTTON_LONG ? "long press" : "press";
char ev_label[32];
snprintf(ev_label, sizeof(ev_label), "%s %s", dir, ev_type);
canvas_draw_str_aligned(canvas, 64, 56, AlignCenter, AlignTop, ev_label);
}
} }
static bool bthome_input_callback(InputEvent *event, void *ctx) {
BtHomeApp *app = ctx;
switch (event->key) {
case InputKeyBack: {
if (event->type == InputTypeShort) { /* ── timer callbacks (run on timer thread) ───────────────────── */
view_dispatcher_stop(app->view_dispatcher);
static void bthome_double_timer_cb(void* ctx) {
BtnCtx* bc = ctx;
// Thread-safe: just enqueue; don't touch shared state here.
view_dispatcher_send_custom_event(
bc->app->view_dispatcher, APP_EV_DOUBLE_TIMEOUT(bc->btn));
}
static void bthome_beacon_timer_callback(void* ctx) {
BtHomeApp* app = ctx;
view_dispatcher_send_custom_event(app->view_dispatcher, APP_EV_BEACON_OFF);
}
/* ── custom event callback (runs on ViewDispatcher / GUI thread) */
static bool bthome_custom_callback(void* ctx, uint32_t event) {
BtHomeApp* app = ctx;
if(event == APP_EV_BEACON_OFF) {
if(furi_hal_bt_extra_beacon_is_active())
furi_hal_bt_extra_beacon_stop();
for(int i = 0; i < BTN_COUNT; i++)
app->beacon_payload[payload_button_off[i]] = BTHOME_BUTTON_NONE;
with_view_model(
app->main_view,
BtHomeModel* model,
{
model->last_event = BTHOME_BUTTON_NONE;
model->last_btn_key = InputKeyOk;
},
true);
return true; return true;
} }
break; // Double-press window expired — fire the deferred single press.
if(event < (uint32_t)BTN_COUNT) {
int btn = (int)event;
if(app->btn_pending[btn]) {
app->btn_pending[btn] = false;
bthome_send_event(app, btn, BTHOME_BUTTON_PRESS);
} }
return true;
}
return false;
}
/* ── input callback (runs on ViewDispatcher / GUI thread) ────── */
static bool bthome_input_callback(InputEvent* event, void* ctx) {
BtHomeApp* app = ctx;
switch(event->key) {
case InputKeyBack:
if(event->type == InputTypeShort) {
view_dispatcher_stop(app->view_dispatcher);
return true;
}
break;
case InputKeyOk: case InputKeyOk:
case InputKeyUp: case InputKeyUp:
case InputKeyDown: case InputKeyDown:
case InputKeyLeft: case InputKeyLeft:
case InputKeyRight: { case InputKeyRight: {
if ((event->type == InputTypeShort) ||
(event->type == InputTypeLong))
{
int btn = bthome_key_to_btn(event->key); int btn = bthome_key_to_btn(event->key);
app->beacon_payload[payload_packet_id_value_off] = if(event->type == InputTypeShort) {
app->packet_id++; if(app->btn_pending[btn]) {
// Second tap within window → double press.
app->beacon_payload[payload_battery_value_off] = furi_timer_stop(app->double_timers[btn]);
furi_hal_power_get_pct(); app->btn_pending[btn] = false;
bthome_send_event(app, btn, BTHOME_BUTTON_DOUBLE);
for (int i = 0; i < BTN_COUNT; i++) { } else {
app->beacon_payload[payload_button_off[i]] = // First tap — open double-press window.
BTHOME_BUTTON_EVENT_NONE; app->btn_pending[btn] = true;
furi_timer_start(app->double_timers[btn], DOUBLE_PRESS_MS);
}
} else if(event->type == InputTypeLong) {
// Long press — cancel any pending single and fire long.
if(app->btn_pending[btn]) {
furi_timer_stop(app->double_timers[btn]);
app->btn_pending[btn] = false;
}
bthome_send_event(app, btn, BTHOME_BUTTON_LONG);
} }
app->beacon_payload[payload_button_off[btn]] = // Visual feedback: fill the d-pad cell while the key is held.
event->type == InputTypeShort ? if(event->type == InputTypePress || event->type == InputTypeRelease) {
BTHOME_BUTTON_EVENT_PRESS :
BTHOME_BUTTON_EVENT_LONG_PRESS;
furi_hal_bt_extra_beacon_set_data(app->beacon_payload,
app->beacon_payload_len);
if (!furi_hal_bt_extra_beacon_is_active()) {
furi_hal_bt_extra_beacon_start();
furi_timer_start(app->beacon_timer, APP_BEACON_TIMER);
}
}
if ((event->type == InputTypePress) ||
(event->type == InputTypeRelease))
{
with_view_model( with_view_model(
app->main_view, app->main_view,
BtHomeModel * model, BtHomeModel* model,
{ {
model->active_key = event->key; model->active_key = event->key;
model->is_pressed = (event->type == InputTypePress); model->is_pressed = (event->type == InputTypePress);
}, },
true true);
);
} }
return true; return true;
@@ -191,89 +386,76 @@ static bool bthome_input_callback(InputEvent *event, void *ctx) {
return false; return false;
} }
static void bthome_beacon_timer_callback(void *ctx) { /* ── payload init ────────────────────────────────────────────── */
BtHomeApp *app = ctx;
if (furi_hal_bt_extra_beacon_is_active()) { static void bthome_init_beacon_payload(uint8_t* payload, size_t* payload_len) {
furi_hal_bt_extra_beacon_stop();
}
for (int i = 0; i < BTN_COUNT; i++) {
app->beacon_payload[payload_button_off[i]] = BTHOME_BUTTON_EVENT_NONE;
}
}
static void bthome_init_beacon_payload(uint8_t *payload, size_t *payload_len) {
size_t i = 0; size_t i = 0;
// Flags data. // Flags.
payload[i++] = 2; // Flags length. payload[i++] = 2;
payload[i++] = BTHOME_AD_TYPE_FLAGS; payload[i++] = BTHOME_AD_TYPE_FLAGS;
payload[i++] = 0x06; // LE General Discoverable Mode + BR/EDR Not Supported. payload[i++] = 0x06; // LE General Discoverable + BR/EDR Not Supported
// Service data. // Service data:
// Length = 1 (type) + 2 (UUID) + 1 (device_info) + 2 (packet_id) + 2 (battery) + BTN_COUNT * 2 // 1 (type) + 2 (UUID) + 1 (device_info) + 2 (packet_id) + 2 (battery) + BTN_COUNT*2
payload[i++] = 8 + BTN_COUNT * 2; payload[i++] = 8 + BTN_COUNT * 2;
payload[i++] = BTHOME_AD_TYPE_SERVICE_DATA; payload[i++] = BTHOME_AD_TYPE_SERVICE_DATA;
payload[i++] = BTHOME_UUID_LSB; payload[i++] = BTHOME_UUID_LSB;
payload[i++] = BTHOME_UUID_MSB; payload[i++] = BTHOME_UUID_MSB;
payload[i++] = 0x44; // no encryption, trigger=true, version=2. payload[i++] = 0x44; // no encryption, trigger-based, version 2
// Packet ID object data.
payload[i++] = BTHOME_OBJ_PACKET_ID; payload[i++] = BTHOME_OBJ_PACKET_ID;
payload_packet_id_value_off = i; payload_packet_id_value_off = i;
payload[i++] = 0x00; payload[i++] = 0x00;
// Battery object data.
payload[i++] = BTHOME_OBJ_BATTERY; payload[i++] = BTHOME_OBJ_BATTERY;
payload_battery_value_off = i; payload_battery_value_off = i;
payload[i++] = 0x00; payload[i++] = 0x00;
// Button event objects: OK, Up, Down, Left, Right. // Button objects: OK=button, Up=button_2, Down=button_3, Left=button_4, Right=button_5.
// Home Assistant names them button, button_2, button_3, button_4, button_5. for(int btn = 0; btn < BTN_COUNT; btn++) {
for (int btn = 0; btn < BTN_COUNT; btn++) {
payload[i++] = BTHOME_OBJ_BUTTON; payload[i++] = BTHOME_OBJ_BUTTON;
payload_button_off[btn] = i; payload_button_off[btn] = i;
payload[i++] = BTHOME_BUTTON_EVENT_NONE; payload[i++] = BTHOME_BUTTON_NONE;
} }
// Complete local name. // Complete local name (truncated if it doesn't fit).
const char *name = furi_hal_version_get_device_name_ptr(); const char* name = furi_hal_version_get_device_name_ptr();
size_t name_len = strlen(name); size_t name_len = strlen(name);
if(i + 2 + name_len > EXTRA_BEACON_MAX_DATA_SIZE) {
// Truncate name if not enough space left.
//
// 2 bytes for the header + the actual name length.
if (i + 2 + name_len > EXTRA_BEACON_MAX_DATA_SIZE) {
name_len = EXTRA_BEACON_MAX_DATA_SIZE - (i + 2); name_len = EXTRA_BEACON_MAX_DATA_SIZE - (i + 2);
FURI_LOG_E(APP_LOG_TAG, "name truncated to %zu", name_len);
FURI_LOG_E(APP_LOG_TAG, "name len %zu", name_len);
} }
payload[i++] = 1 + name_len;
payload[i++] = 1 + name_len; // Name object length.
payload[i++] = BTHOME_AD_TYPE_COMPLETE_LOCAL_NAME; payload[i++] = BTHOME_AD_TYPE_COMPLETE_LOCAL_NAME;
memcpy(&payload[i], name, name_len); memcpy(&payload[i], name, name_len);
i += name_len; i += name_len;
*payload_len = i; *payload_len = i;
} }
static BtHomeApp *bthome_app_alloc(void) { /* ── lifecycle ───────────────────────────────────────────────── */
BtHomeApp *app = malloc(sizeof(BtHomeApp));
if (!app) {
FURI_LOG_E(APP_LOG_TAG, "Failed to allocate app");
static BtHomeApp* bthome_app_alloc(void) {
BtHomeApp* app = malloc(sizeof(BtHomeApp));
if(!app) {
FURI_LOG_E(APP_LOG_TAG, "alloc failed");
return NULL; return NULL;
} }
app->packet_id = 0; app->packet_id = 0;
memset(app->btn_pending, 0, sizeof(app->btn_pending));
bthome_init_beacon_payload(app->beacon_payload, &app->beacon_payload_len); bthome_init_beacon_payload(app->beacon_payload, &app->beacon_payload_len);
app->beacon_timer = furi_timer_alloc(bthome_beacon_timer_callback, app->beacon_timer =
FuriTimerTypeOnce, furi_timer_alloc(bthome_beacon_timer_callback, FuriTimerTypeOnce, app);
app);
for(int i = 0; i < BTN_COUNT; i++) {
app->btn_ctx[i] = (BtnCtx){.app = app, .btn = i};
app->double_timers[i] =
furi_timer_alloc(bthome_double_timer_cb, FuriTimerTypeOnce, &app->btn_ctx[i]);
}
furi_hal_bt_extra_beacon_stop(); furi_hal_bt_extra_beacon_stop();
@@ -284,27 +466,26 @@ static BtHomeApp *bthome_app_alloc(void) {
.adv_power_level = GapAdvPowerLevel_6dBm, .adv_power_level = GapAdvPowerLevel_6dBm,
.address_type = GapAddressTypePublic, .address_type = GapAddressTypePublic,
}; };
memcpy(config.address, furi_hal_version_get_ble_mac(), sizeof(config.address)); memcpy(config.address, furi_hal_version_get_ble_mac(), sizeof(config.address));
if (!furi_hal_bt_extra_beacon_set_config(&config)) { if(!furi_hal_bt_extra_beacon_set_config(&config)) {
FURI_LOG_E("BTHOME", "Failed to set beacon config"); FURI_LOG_E(APP_LOG_TAG, "beacon config failed");
free(app); free(app);
return NULL; return NULL;
} }
app->view_dispatcher = view_dispatcher_alloc(); app->view_dispatcher = view_dispatcher_alloc();
if (!app->view_dispatcher) { if(!app->view_dispatcher) {
FURI_LOG_E(APP_LOG_TAG, "Failed to allocate view dispatcher"); FURI_LOG_E(APP_LOG_TAG, "view_dispatcher alloc failed");
free(app); free(app);
return NULL; return NULL;
} }
view_dispatcher_set_custom_event_callback(app->view_dispatcher, bthome_custom_callback);
view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
app->main_view = view_alloc(); app->main_view = view_alloc();
view_allocate_model(app->main_view, ViewModelTypeLocking, sizeof(BtHomeModel)); view_allocate_model(app->main_view, ViewModelTypeLocking, sizeof(BtHomeModel));
view_set_draw_callback(app->main_view, bthome_draw_callback); view_set_draw_callback(app->main_view, bthome_draw_callback);
view_set_input_callback(app->main_view, bthome_input_callback); view_set_input_callback(app->main_view, bthome_input_callback);
view_set_context(app->main_view, app); view_set_context(app->main_view, app);
@@ -314,12 +495,19 @@ static BtHomeApp *bthome_app_alloc(void) {
return app; return app;
} }
static void bthome_app_free(BtHomeApp *app) { static void bthome_app_free(BtHomeApp* app) {
furi_hal_bt_extra_beacon_stop(); furi_hal_bt_extra_beacon_stop();
// Stop all timers before freeing the ViewDispatcher so no in-flight
// callback can post to a freed queue.
furi_timer_stop(app->beacon_timer); furi_timer_stop(app->beacon_timer);
furi_timer_free(app->beacon_timer); furi_timer_free(app->beacon_timer);
for(int i = 0; i < BTN_COUNT; i++) {
furi_timer_stop(app->double_timers[i]);
furi_timer_free(app->double_timers[i]);
}
view_dispatcher_remove_view(app->view_dispatcher, APP_VIEW_MAIN); view_dispatcher_remove_view(app->view_dispatcher, APP_VIEW_MAIN);
view_free_model(app->main_view); view_free_model(app->main_view);
view_free(app->main_view); view_free(app->main_view);
@@ -328,23 +516,20 @@ static void bthome_app_free(BtHomeApp *app) {
free(app); free(app);
} }
int32_t bt_home_controller_app(void *p) { /* ── entry point ─────────────────────────────────────────────── */
int32_t bt_home_controller_app(void* p) {
UNUSED(p); UNUSED(p);
BtHomeApp *app = bthome_app_alloc(); BtHomeApp* app = bthome_app_alloc();
if (!app) { if(!app) return -1;
return -1;
}
Gui *gui = furi_record_open(RECORD_GUI);
Gui* gui = furi_record_open(RECORD_GUI);
view_dispatcher_attach_to_gui(app->view_dispatcher, gui, ViewDispatcherTypeFullscreen); view_dispatcher_attach_to_gui(app->view_dispatcher, gui, ViewDispatcherTypeFullscreen);
view_dispatcher_switch_to_view(app->view_dispatcher, APP_VIEW_MAIN); view_dispatcher_switch_to_view(app->view_dispatcher, APP_VIEW_MAIN);
view_dispatcher_run(app->view_dispatcher); view_dispatcher_run(app->view_dispatcher);
furi_record_close(RECORD_GUI); furi_record_close(RECORD_GUI);
bthome_app_free(app); bthome_app_free(app);
return 0; return 0;
} }