Working build of App - can use dpad not just centre button

This commit is contained in:
Jon
2026-05-02 22:21:41 +01:00
commit ef0ecf97e1
12 changed files with 396 additions and 0 deletions

2
CHANGELOG.md Normal file
View File

@@ -0,0 +1,2 @@
0.1
Initial release

30
README.md Normal file
View File

@@ -0,0 +1,30 @@
![bthome app](screenshots/bthome_app.png)
This application turns the Flipper Zero into a [BTHome](https://bthome.io)
beacon, and can be used to integrate the Flipper with home automation systems
that support BTHome, such as Home Assistant.
[BTHome](https://bthome.io) is an open standard for broadcasting sensor data
over Bluetooth Low Energy (BLE). It allows devices to transmit sensor readings
(temperature, humidity, battery level, button events, etc.) without requiring
pairing or an active connection.
The BTHome beacon is triggered when pressing the Flipper Zero "OK" button
and currently includes the Flipper's battery percentage as a sensor.
Unfortunately, due to the limited size of BLE packets the Flipper Zero can send
there isn't much room left to add more sensors without removing the existing
one.
### Home Assistant setup
The first step is setting up the [BTHome](https://www.home-assistant.io/integrations/bthome/) integration in Home Assistant.
Once setup, the Flipper should be automatically discovered after pressing the
center button once:
![home assistant discovered](screenshots/home_assistant_discovered.png)
The device's page will then show the current battery status and its events:
![home assistant paired](screenshots/home_assistant_paired.png)

14
application.fam Normal file
View File

@@ -0,0 +1,14 @@
App(
appid="bt_home_controller",
name="BT Home Controller",
apptype=FlipperAppType.EXTERNAL,
entry_point="bt_home_controller_app",
stack_size=2 * 1024,
fap_category="Bluetooth",
fap_version="0.1",
fap_icon="bthome.png",
fap_description="BTHome D-pad controller for the Flipper Zero",
fap_author="Alessandro Ghedini",
fap_weburl="https://github.com/ghedo/flipper-bthome",
fap_icon_assets="assets",
)

0
assets/.gitkeep Normal file
View File

BIN
assets/Ok_btn_9x9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

BIN
assets/bthome_123x52.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
bthome.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

BIN
screenshots/bthome_app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

350
src/bt_home_controller.c Normal file
View File

@@ -0,0 +1,350 @@
#include <furi.h>
#include <furi_hal_bt.h>
#include <furi_hal_power.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
// BTHome AD types.
#define BTHOME_AD_TYPE_FLAGS 0x01
#define BTHOME_AD_TYPE_COMPLETE_LOCAL_NAME 0x09
#define BTHOME_AD_TYPE_SERVICE_DATA 0x16
// BTHome object IDs.
#define BTHOME_OBJ_PACKET_ID 0x00
#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
// App values.
#define APP_LOG_TAG "BTHOME"
#define APP_VIEW_MAIN 0
#define APP_BEACON_TIMER 1000
// D-pad button indices — order determines BTHome button numbering (button, button_2 … button_5).
#define BTN_OK 0
#define BTN_UP 1
#define BTN_DOWN 2
#define BTN_LEFT 3
#define BTN_RIGHT 4
#define BTN_COUNT 5
// Payload offsets.
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 {
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;
typedef struct {
InputKey active_key;
bool is_pressed;
} BtHomeModel;
static int bthome_key_to_btn(InputKey key) {
switch (key) {
case InputKeyOk: return BTN_OK;
case InputKeyUp: return BTN_UP;
case InputKeyDown: return BTN_DOWN;
case InputKeyLeft: return BTN_LEFT;
case InputKeyRight: return BTN_RIGHT;
default: return -1;
}
}
// 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 void bthome_draw_callback(Canvas *canvas, void *ctx) {
BtHomeModel *model = ctx;
furi_assert(model);
canvas_clear(canvas);
canvas_set_bitmap_mode(canvas, true);
// Logo moved to y=0 to leave 12px at the bottom for controls.
canvas_draw_icon(canvas, 3, 0, &I_bthome_123x52);
canvas_set_font(canvas, FontSecondary);
canvas_draw_str_aligned(canvas, 3, 54, AlignLeft, AlignTop, "D-pad to send");
bthome_draw_dpad(canvas, 107, 53, model->is_pressed, model->active_key);
}
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;
}
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))
{
with_view_model(
app->main_view,
BtHomeModel * model,
{
model->active_key = event->key;
model->is_pressed = (event->type == InputTypePress);
},
true
);
}
return true;
}
default:
break;
}
return false;
}
static void bthome_beacon_timer_callback(void *ctx) {
BtHomeApp *app = ctx;
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) {
size_t i = 0;
// Flags data.
payload[i++] = 2; // Flags length.
payload[i++] = BTHOME_AD_TYPE_FLAGS;
payload[i++] = 0x06; // LE General Discoverable Mode + BR/EDR Not Supported.
// Service data.
// Length = 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.
// 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++) {
payload[i++] = BTHOME_OBJ_BUTTON;
payload_button_off[btn] = i;
payload[i++] = BTHOME_BUTTON_EVENT_NONE;
}
// Complete local name.
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) {
name_len = EXTRA_BEACON_MAX_DATA_SIZE - (i + 2);
FURI_LOG_E(APP_LOG_TAG, "name len %zu", name_len);
}
payload[i++] = 1 + name_len; // Name object length.
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");
return NULL;
}
app->packet_id = 0;
bthome_init_beacon_payload(app->beacon_payload, &app->beacon_payload_len);
app->beacon_timer = furi_timer_alloc(bthome_beacon_timer_callback,
FuriTimerTypeOnce,
app);
furi_hal_bt_extra_beacon_stop();
GapExtraBeaconConfig config = {
.min_adv_interval_ms = 100,
.max_adv_interval_ms = 200,
.adv_channel_map = GapAdvChannelMapAll,
.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");
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");
free(app);
return NULL;
}
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);
view_dispatcher_add_view(app->view_dispatcher, APP_VIEW_MAIN, app->main_view);
return app;
}
static void bthome_app_free(BtHomeApp *app) {
furi_hal_bt_extra_beacon_stop();
furi_timer_stop(app->beacon_timer);
furi_timer_free(app->beacon_timer);
view_dispatcher_remove_view(app->view_dispatcher, APP_VIEW_MAIN);
view_free_model(app->main_view);
view_free(app->main_view);
view_dispatcher_free(app->view_dispatcher);
free(app);
}
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);
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;
}