From 7fe1972d281f936d859c5e3e5351e4cdcaabd652 Mon Sep 17 00:00:00 2001 From: andy Date: Fri, 11 Sep 2026 14:08:30 +0100 Subject: [PATCH] Add BLE GATT Server/emulator app Brings up the Flipper as a connectable serial GATT peripheral via the bt service profile API and echoes what a central writes. Restores the default profile on exit. --- ble_gattsrv/application.fam | 15 ++++ ble_gattsrv/ble_gattsrv.c | 147 ++++++++++++++++++++++++++++++++++++ ble_gattsrv/icon.png | Bin 0 -> 95 bytes build.sh | 2 +- 4 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 ble_gattsrv/application.fam create mode 100644 ble_gattsrv/ble_gattsrv.c create mode 100644 ble_gattsrv/icon.png diff --git a/ble_gattsrv/application.fam b/ble_gattsrv/application.fam new file mode 100644 index 0000000..8f682e3 --- /dev/null +++ b/ble_gattsrv/application.fam @@ -0,0 +1,15 @@ +# Needs firmware from ../build.sh (full BLE stack + exported peripheral API). +App( + appid="ble_gattsrv", + name="BLE GATT Server", + apptype=FlipperAppType.EXTERNAL, + entry_point="ble_gattsrv_app", + requires=["gui", "bt"], + stack_size=2 * 1024, + fap_category="Bluetooth", + fap_icon="icon.png", + fap_version="1.0", + fap_description="Advertise a connectable serial GATT peripheral and echo received data", + fap_author="andydixon", + fap_weburl="https://github.com/andydixon/flipper-bt-inspector", +) diff --git a/ble_gattsrv/ble_gattsrv.c b/ble_gattsrv/ble_gattsrv.c new file mode 100644 index 0000000..9f7e830 --- /dev/null +++ b/ble_gattsrv/ble_gattsrv.c @@ -0,0 +1,147 @@ +// BLE GATT Server / emulator: bring up the Flipper as a connectable BLE serial +// peripheral (a custom 128-bit GATT service with RX/TX characteristics) via the +// bt service's profile API, and echo whatever a central writes back to it as a +// notification. Connect from a phone (e.g. nRF Connect) to see the service, +// write to RX and receive the echo on TX. Restores the default profile on exit. +#include +#include +#include +#include +#include +#include +#include + +typedef struct { + Gui* gui; + ViewPort* vp; + FuriMutex* mutex; + FuriMessageQueue* input; + Bt* bt; + FuriHalBleProfileBase* profile; + + BtStatus status; + uint32_t rx_bytes, tx_bytes; + char last[40]; + bool echo; +} App; + +static void status_cb(BtStatus status, void* ctx) { + App* app = ctx; + furi_mutex_acquire(app->mutex, FuriWaitForever); + app->status = status; + furi_mutex_release(app->mutex); + if(app->vp) view_port_update(app->vp); +} + +// Runs on the BLE stack thread: echo received data back as a TX notification. +static uint16_t serial_cb(SerialServiceEvent event, void* ctx) { + App* app = ctx; + if(event.event == SerialServiceEventTypeDataReceived) { + furi_mutex_acquire(app->mutex, FuriWaitForever); + app->rx_bytes += event.data.size; + uint16_t n = event.data.size < sizeof(app->last) - 1 ? event.data.size : sizeof(app->last) - 1; + memcpy(app->last, event.data.buffer, n); + app->last[n] = 0; + for(uint16_t i = 0; i < n; i++) + if(app->last[i] < 32 || app->last[i] > 126) app->last[i] = '.'; + bool echo = app->echo; + furi_mutex_release(app->mutex); + if(echo && app->profile) { + if(ble_profile_serial_tx(app->profile, event.data.buffer, event.data.size)) + app->tx_bytes += event.data.size; + } + if(app->vp) view_port_update(app->vp); + } + return event.data.size; +} + +static void draw_cb(Canvas* c, void* ctx) { + App* app = ctx; + furi_mutex_acquire(app->mutex, FuriWaitForever); + char buf[48]; + canvas_clear(c); + canvas_set_font(c, FontPrimary); + canvas_draw_str(c, 2, 9, "GATT Server"); + canvas_set_font(c, FontSecondary); + const char* st = app->status == BtStatusConnected ? "connected" : + app->status == BtStatusAdvertising ? "advertising" : + app->status == BtStatusOff ? "off" : + "unavailable"; + canvas_draw_str_aligned(c, 126, 9, AlignRight, AlignBottom, st); + canvas_draw_line(c, 0, 11, 127, 11); + canvas_draw_str(c, 2, 22, "Serial GATT service (RX/TX)"); + snprintf(buf, sizeof(buf), "RX %lu B TX %lu B", (unsigned long)app->rx_bytes, (unsigned long)app->tx_bytes); + canvas_draw_str(c, 2, 34, buf); + snprintf(buf, sizeof(buf), "Echo: %s (OK toggles)", app->echo ? "on" : "off"); + canvas_draw_str(c, 2, 45, buf); + if(app->last[0]) { + snprintf(buf, sizeof(buf), "last: %s", app->last); + canvas_draw_str(c, 2, 57, buf); + } + furi_mutex_release(app->mutex); +} + +static void input_cb(InputEvent* e, void* ctx) { + App* app = ctx; + furi_message_queue_put(app->input, e, FuriWaitForever); +} + +int32_t ble_gattsrv_app(void* p) { + UNUSED(p); + App* app = malloc(sizeof(App)); + memset(app, 0, sizeof(App)); + app->mutex = furi_mutex_alloc(FuriMutexTypeNormal); + app->input = furi_message_queue_alloc(8, sizeof(InputEvent)); + app->echo = true; + + app->bt = furi_record_open(RECORD_BT); + bt_set_status_changed_callback(app->bt, status_cb, app); + // Swap the default RPC serial profile for our own instance so we own RX/TX. + app->profile = bt_profile_start(app->bt, ble_profile_serial, NULL); + if(app->profile) + ble_profile_serial_set_event_callback( + app->profile, BLE_PROFILE_SERIAL_PACKET_SIZE_MAX, serial_cb, app); + + app->gui = furi_record_open(RECORD_GUI); + app->vp = view_port_alloc(); + view_port_draw_callback_set(app->vp, draw_cb, app); + view_port_input_callback_set(app->vp, input_cb, app); + gui_add_view_port(app->gui, app->vp, GuiLayerFullscreen); + + bool run = app->profile != NULL; + if(!run) { + // Could not start the profile (wrong stack?). Show a hint until Back. + furi_mutex_acquire(app->mutex, FuriWaitForever); + strncpy(app->last, "profile start failed", sizeof(app->last) - 1); + furi_mutex_release(app->mutex); + view_port_update(app->vp); + } + bool active = true; + while(active) { + InputEvent e; + if(furi_message_queue_get(app->input, &e, FuriWaitForever) != FuriStatusOk) continue; + if(e.type != InputTypeShort) continue; + if(e.key == InputKeyBack) { + active = false; + } else if(e.key == InputKeyOk) { + furi_mutex_acquire(app->mutex, FuriWaitForever); + app->echo = !app->echo; + furi_mutex_release(app->mutex); + view_port_update(app->vp); + } + } + + gui_remove_view_port(app->gui, app->vp); + view_port_free(app->vp); + furi_record_close(RECORD_GUI); + if(app->profile) { + ble_profile_serial_set_event_callback(app->profile, 0, NULL, NULL); + bt_profile_restore_default(app->bt); + } + bt_set_status_changed_callback(app->bt, NULL, NULL); + furi_record_close(RECORD_BT); + furi_message_queue_free(app->input); + furi_mutex_free(app->mutex); + free(app); + return 0; +} diff --git a/ble_gattsrv/icon.png b/ble_gattsrv/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2d6f2fbc8f428fe0b6c579297e80bee719714064 GIT binary patch literal 95 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2VkYHF5IUx~9sd>6MhE&{2{=v_}^Z%g=GjkK? rvI8nYO`T0s4qY)y2>mVZse2`cq5H7+*7zpl8jwy;S3j3^P6