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:
@@ -5,8 +5,8 @@ App(
|
||||
entry_point="bt_home_controller_app",
|
||||
stack_size=2 * 1024,
|
||||
fap_category="Bluetooth",
|
||||
fap_version="0.1",
|
||||
fap_icon="bthome.png",
|
||||
fap_version="0.3",
|
||||
fap_icon="assets/bluetooth_10x10.png",
|
||||
fap_description="BTHome D-pad controller for the Flipper Zero",
|
||||
fap_author="Alessandro Ghedini",
|
||||
fap_weburl="https://github.com/ghedo/flipper-bthome",
|
||||
|
||||
BIN
assets/Pin_back_arrow_10x10.png
Normal file
BIN
assets/Pin_back_arrow_10x10.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 90 B |
BIN
assets/Pin_back_arrow_10x8.png
Normal file
BIN
assets/Pin_back_arrow_10x8.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 90 B |
BIN
assets/bluetooth_10x10.png
Normal file
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
BIN
assets/hid_ble_10px.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 B |
@@ -1,13 +1,12 @@
|
||||
#include <furi.h>
|
||||
#include <furi_hal_bt.h>
|
||||
#include <furi_hal_power.h>
|
||||
#include <bt_home_controller_icons.h>
|
||||
|
||||
#include <gui/gui.h>
|
||||
#include <gui/view_dispatcher.h>
|
||||
#include <gui/view.h>
|
||||
|
||||
#include <bt_home_controller_icons.h>
|
||||
|
||||
// BTHome UUID: 0xFCD2 (little-endian).
|
||||
#define BTHOME_UUID_LSB 0xD2
|
||||
#define BTHOME_UUID_MSB 0xFC
|
||||
@@ -22,18 +21,19 @@
|
||||
#define BTHOME_OBJ_BATTERY 0x01
|
||||
#define BTHOME_OBJ_BUTTON 0x3A
|
||||
|
||||
// Button event values.
|
||||
#define BTHOME_BUTTON_EVENT_NONE 0x00
|
||||
#define BTHOME_BUTTON_EVENT_PRESS 0x01
|
||||
#define BTHOME_BUTTON_EVENT_LONG_PRESS 0x04
|
||||
// BTHome v2 button event values.
|
||||
#define BTHOME_BUTTON_NONE 0x00
|
||||
#define BTHOME_BUTTON_PRESS 0x01
|
||||
#define BTHOME_BUTTON_DOUBLE 0x02
|
||||
#define BTHOME_BUTTON_LONG 0x04
|
||||
|
||||
// App values.
|
||||
#define APP_LOG_TAG "BTHOME"
|
||||
|
||||
#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_UP 1
|
||||
#define BTN_DOWN 2
|
||||
@@ -41,31 +41,59 @@
|
||||
#define BTN_RIGHT 4
|
||||
#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_packet_id_value_off = 0;
|
||||
static size_t payload_button_off[BTN_COUNT] = {0};
|
||||
|
||||
typedef struct BtHomeApp BtHomeApp;
|
||||
|
||||
// Per-button context for double-press timer callbacks.
|
||||
typedef struct {
|
||||
ViewDispatcher *view_dispatcher;
|
||||
|
||||
View *main_view;
|
||||
BtHomeApp* app;
|
||||
int btn;
|
||||
} BtnCtx;
|
||||
|
||||
struct BtHomeApp {
|
||||
ViewDispatcher* view_dispatcher;
|
||||
View* main_view;
|
||||
uint8_t packet_id;
|
||||
|
||||
uint8_t beacon_payload[EXTRA_BEACON_MAX_DATA_SIZE];
|
||||
size_t beacon_payload_len;
|
||||
|
||||
FuriTimer *beacon_timer;
|
||||
} BtHomeApp;
|
||||
FuriTimer* beacon_timer;
|
||||
FuriTimer* double_timers[BTN_COUNT];
|
||||
BtnCtx btn_ctx[BTN_COUNT];
|
||||
bool btn_pending[BTN_COUNT]; // true while double-press window is open
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
InputKey active_key;
|
||||
bool is_pressed;
|
||||
uint8_t last_event; // last BTHOME_BUTTON_* value dispatched
|
||||
InputKey last_btn_key; // which key the last event was for
|
||||
} 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) {
|
||||
switch (key) {
|
||||
switch(key) {
|
||||
case InputKeyOk: return BTN_OK;
|
||||
case InputKeyUp: return BTN_UP;
|
||||
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.
|
||||
// Layout (each cell 3x3, 1px gap):
|
||||
// [U]
|
||||
// [L][O][R]
|
||||
// [D]
|
||||
static void bthome_draw_dpad(Canvas *canvas, int x, int y, bool is_pressed, InputKey active_key) {
|
||||
const struct { InputKey key; int dx; int dy; } cells[BTN_COUNT] = {
|
||||
{ InputKeyOk, 4, 4 },
|
||||
{ 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 InputKey bthome_btn_to_key(int btn) {
|
||||
switch(btn) {
|
||||
case BTN_OK: return InputKeyOk;
|
||||
case BTN_UP: return InputKeyUp;
|
||||
case BTN_DOWN: return InputKeyDown;
|
||||
case BTN_LEFT: return InputKeyLeft;
|
||||
case BTN_RIGHT: return InputKeyRight;
|
||||
default: return InputKeyOk;
|
||||
}
|
||||
}
|
||||
|
||||
static void bthome_draw_callback(Canvas *canvas, void *ctx) {
|
||||
BtHomeModel *model = ctx;
|
||||
static const char* bthome_key_to_direction(InputKey key) {
|
||||
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);
|
||||
|
||||
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.
|
||||
canvas_draw_icon(canvas, 3, 0, &I_bthome_123x52);
|
||||
// D-pad: show last-sent event (1 s) → physical hold → idle.
|
||||
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_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) {
|
||||
view_dispatcher_stop(app->view_dispatcher);
|
||||
|
||||
|
||||
/* ── timer callbacks (run on timer thread) ───────────────────── */
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 InputKeyUp:
|
||||
case InputKeyDown:
|
||||
case InputKeyLeft:
|
||||
case InputKeyRight: {
|
||||
if ((event->type == InputTypeShort) ||
|
||||
(event->type == InputTypeLong))
|
||||
{
|
||||
int btn = bthome_key_to_btn(event->key);
|
||||
|
||||
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_EVENT_NONE;
|
||||
if(event->type == InputTypeShort) {
|
||||
if(app->btn_pending[btn]) {
|
||||
// Second tap within window → double press.
|
||||
furi_timer_stop(app->double_timers[btn]);
|
||||
app->btn_pending[btn] = false;
|
||||
bthome_send_event(app, btn, BTHOME_BUTTON_DOUBLE);
|
||||
} else {
|
||||
// First tap — open double-press window.
|
||||
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]] =
|
||||
event->type == InputTypeShort ?
|
||||
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))
|
||||
{
|
||||
// Visual feedback: fill the d-pad cell while the key is held.
|
||||
if(event->type == InputTypePress || event->type == InputTypeRelease) {
|
||||
with_view_model(
|
||||
app->main_view,
|
||||
BtHomeModel * model,
|
||||
BtHomeModel* model,
|
||||
{
|
||||
model->active_key = event->key;
|
||||
model->is_pressed = (event->type == InputTypePress);
|
||||
},
|
||||
true
|
||||
);
|
||||
true);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -191,89 +386,76 @@ static bool bthome_input_callback(InputEvent *event, void *ctx) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static void bthome_beacon_timer_callback(void *ctx) {
|
||||
BtHomeApp *app = ctx;
|
||||
/* ── payload init ────────────────────────────────────────────── */
|
||||
|
||||
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_EVENT_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
static void bthome_init_beacon_payload(uint8_t *payload, size_t *payload_len) {
|
||||
static void bthome_init_beacon_payload(uint8_t* payload, size_t* payload_len) {
|
||||
size_t i = 0;
|
||||
|
||||
// Flags data.
|
||||
payload[i++] = 2; // Flags length.
|
||||
// Flags.
|
||||
payload[i++] = 2;
|
||||
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.
|
||||
// Length = 1 (type) + 2 (UUID) + 1 (device_info) + 2 (packet_id) + 2 (battery) + BTN_COUNT * 2
|
||||
// Service data:
|
||||
// 1 (type) + 2 (UUID) + 1 (device_info) + 2 (packet_id) + 2 (battery) + BTN_COUNT*2
|
||||
payload[i++] = 8 + BTN_COUNT * 2;
|
||||
payload[i++] = BTHOME_AD_TYPE_SERVICE_DATA;
|
||||
payload[i++] = BTHOME_UUID_LSB;
|
||||
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_packet_id_value_off = i;
|
||||
payload[i++] = 0x00;
|
||||
|
||||
// Battery object data.
|
||||
payload[i++] = BTHOME_OBJ_BATTERY;
|
||||
payload_battery_value_off = i;
|
||||
payload[i++] = 0x00;
|
||||
|
||||
// Button event objects: OK, Up, Down, Left, Right.
|
||||
// Home Assistant names them button, button_2, button_3, button_4, button_5.
|
||||
for (int btn = 0; btn < BTN_COUNT; btn++) {
|
||||
// Button objects: OK=button, Up=button_2, Down=button_3, Left=button_4, Right=button_5.
|
||||
for(int btn = 0; btn < BTN_COUNT; btn++) {
|
||||
payload[i++] = BTHOME_OBJ_BUTTON;
|
||||
payload_button_off[btn] = i;
|
||||
payload[i++] = BTHOME_BUTTON_EVENT_NONE;
|
||||
payload[i++] = BTHOME_BUTTON_NONE;
|
||||
}
|
||||
|
||||
// Complete local name.
|
||||
const char *name = furi_hal_version_get_device_name_ptr();
|
||||
// Complete local name (truncated if it doesn't fit).
|
||||
const char* name = furi_hal_version_get_device_name_ptr();
|
||||
size_t name_len = strlen(name);
|
||||
|
||||
// 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) {
|
||||
if(i + 2 + name_len > EXTRA_BEACON_MAX_DATA_SIZE) {
|
||||
name_len = EXTRA_BEACON_MAX_DATA_SIZE - (i + 2);
|
||||
|
||||
FURI_LOG_E(APP_LOG_TAG, "name len %zu", name_len);
|
||||
FURI_LOG_E(APP_LOG_TAG, "name truncated to %zu", name_len);
|
||||
}
|
||||
|
||||
payload[i++] = 1 + name_len; // Name object length.
|
||||
payload[i++] = 1 + name_len;
|
||||
payload[i++] = BTHOME_AD_TYPE_COMPLETE_LOCAL_NAME;
|
||||
|
||||
memcpy(&payload[i], name, name_len);
|
||||
i += name_len;
|
||||
|
||||
*payload_len = i;
|
||||
}
|
||||
|
||||
static BtHomeApp *bthome_app_alloc(void) {
|
||||
BtHomeApp *app = malloc(sizeof(BtHomeApp));
|
||||
if (!app) {
|
||||
FURI_LOG_E(APP_LOG_TAG, "Failed to allocate app");
|
||||
/* ── lifecycle ───────────────────────────────────────────────── */
|
||||
|
||||
static BtHomeApp* bthome_app_alloc(void) {
|
||||
BtHomeApp* app = malloc(sizeof(BtHomeApp));
|
||||
if(!app) {
|
||||
FURI_LOG_E(APP_LOG_TAG, "alloc failed");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
app->packet_id = 0;
|
||||
memset(app->btn_pending, 0, sizeof(app->btn_pending));
|
||||
|
||||
bthome_init_beacon_payload(app->beacon_payload, &app->beacon_payload_len);
|
||||
|
||||
app->beacon_timer = furi_timer_alloc(bthome_beacon_timer_callback,
|
||||
FuriTimerTypeOnce,
|
||||
app);
|
||||
app->beacon_timer =
|
||||
furi_timer_alloc(bthome_beacon_timer_callback, FuriTimerTypeOnce, 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();
|
||||
|
||||
@@ -284,27 +466,26 @@ static BtHomeApp *bthome_app_alloc(void) {
|
||||
.adv_power_level = GapAdvPowerLevel_6dBm,
|
||||
.address_type = GapAddressTypePublic,
|
||||
};
|
||||
|
||||
memcpy(config.address, furi_hal_version_get_ble_mac(), sizeof(config.address));
|
||||
|
||||
if (!furi_hal_bt_extra_beacon_set_config(&config)) {
|
||||
FURI_LOG_E("BTHOME", "Failed to set beacon config");
|
||||
if(!furi_hal_bt_extra_beacon_set_config(&config)) {
|
||||
FURI_LOG_E(APP_LOG_TAG, "beacon config failed");
|
||||
free(app);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
app->view_dispatcher = view_dispatcher_alloc();
|
||||
if (!app->view_dispatcher) {
|
||||
FURI_LOG_E(APP_LOG_TAG, "Failed to allocate view dispatcher");
|
||||
|
||||
if(!app->view_dispatcher) {
|
||||
FURI_LOG_E(APP_LOG_TAG, "view_dispatcher alloc failed");
|
||||
free(app);
|
||||
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();
|
||||
|
||||
view_allocate_model(app->main_view, ViewModelTypeLocking, sizeof(BtHomeModel));
|
||||
|
||||
view_set_draw_callback(app->main_view, bthome_draw_callback);
|
||||
view_set_input_callback(app->main_view, bthome_input_callback);
|
||||
view_set_context(app->main_view, app);
|
||||
@@ -314,12 +495,19 @@ static BtHomeApp *bthome_app_alloc(void) {
|
||||
return app;
|
||||
}
|
||||
|
||||
static void bthome_app_free(BtHomeApp *app) {
|
||||
static void bthome_app_free(BtHomeApp* app) {
|
||||
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_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_free_model(app->main_view);
|
||||
view_free(app->main_view);
|
||||
@@ -328,23 +516,20 @@ static void bthome_app_free(BtHomeApp *app) {
|
||||
free(app);
|
||||
}
|
||||
|
||||
int32_t bt_home_controller_app(void *p) {
|
||||
/* ── entry point ─────────────────────────────────────────────── */
|
||||
|
||||
int32_t bt_home_controller_app(void* p) {
|
||||
UNUSED(p);
|
||||
|
||||
BtHomeApp *app = bthome_app_alloc();
|
||||
if (!app) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
Gui *gui = furi_record_open(RECORD_GUI);
|
||||
BtHomeApp* app = bthome_app_alloc();
|
||||
if(!app) return -1;
|
||||
|
||||
Gui* gui = furi_record_open(RECORD_GUI);
|
||||
view_dispatcher_attach_to_gui(app->view_dispatcher, gui, ViewDispatcherTypeFullscreen);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, APP_VIEW_MAIN);
|
||||
view_dispatcher_run(app->view_dispatcher);
|
||||
|
||||
furi_record_close(RECORD_GUI);
|
||||
|
||||
bthome_app_free(app);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user