From 474ce234a6ee20650d79154d587e3f13c95c7739 Mon Sep 17 00:00:00 2001 From: andy Date: Fri, 14 Aug 2026 01:31:47 +0100 Subject: [PATCH] Add BLE Beacon Toolkit app Broadcasts an iBeacon or a raw advertising payload with a chosen random MAC via the exported extra-beacon API. Needs the full BLE stack. --- ble_beacon/application.fam | 15 ++++ ble_beacon/ble_beacon.c | 174 +++++++++++++++++++++++++++++++++++++ ble_beacon/icon.png | Bin 0 -> 95 bytes build.sh | 2 +- 4 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 ble_beacon/application.fam create mode 100644 ble_beacon/ble_beacon.c create mode 100644 ble_beacon/icon.png diff --git a/ble_beacon/application.fam b/ble_beacon/application.fam new file mode 100644 index 0000000..968a0a1 --- /dev/null +++ b/ble_beacon/application.fam @@ -0,0 +1,15 @@ +# Needs firmware from ../build.sh (full BLE stack + exported peripheral API). +App( + appid="ble_beacon", + name="BLE Beacon Toolkit", + apptype=FlipperAppType.EXTERNAL, + entry_point="ble_beacon_app", + requires=["gui"], + stack_size=2 * 1024, + fap_category="Bluetooth", + fap_icon="icon.png", + fap_version="1.0", + fap_description="Broadcast iBeacon or raw BLE advertisements with a chosen MAC", + fap_author="andydixon", + fap_weburl="https://github.com/andydixon/flipper-bt-inspector", +) diff --git a/ble_beacon/ble_beacon.c b/ble_beacon/ble_beacon.c new file mode 100644 index 0000000..2205775 --- /dev/null +++ b/ble_beacon/ble_beacon.c @@ -0,0 +1,174 @@ +// BLE Beacon Toolkit: broadcast an iBeacon or a raw advertising payload with a +// chosen (random) MAC, using the firmware's extra-beacon API. Needs the full +// BLE stack. iBeacon mode wraps 21 entered bytes (16 UUID + 2 major + 2 minor + +// 1 Tx) into an Apple manufacturer AD; Raw mode sends the entered bytes as-is. +#include +#include +#include +#include +#include +#include +#include +#include + +typedef enum { ViewMenu, ViewByteInput } ViewId; +typedef enum { ModeRaw, ModeIBeacon } Mode; +typedef enum { EditMac, EditPayload } EditWhat; + +// Menu row indices. +enum { RowStartStop, RowState, RowMode, RowMac, RowPayload }; + +typedef struct { + Gui* gui; + ViewDispatcher* vd; + VariableItemList* menu; + ByteInput* byte_input; + VariableItem* it_mode; + VariableItem* it_state; + + Mode mode; + EditWhat editing; + bool running; + uint8_t mac[EXTRA_BEACON_MAC_ADDR_SIZE]; // little-endian, as the stack wants + uint8_t payload[EXTRA_BEACON_MAX_DATA_SIZE]; + uint8_t payload_len; // Raw: full AD; iBeacon: 21 significant bytes +} App; + +static void beacon_stop(App* app) { + if(app->running) { + furi_hal_bt_extra_beacon_stop(); + app->running = false; + } +} + +static uint8_t build_data(App* app, uint8_t* out) { + if(app->mode == ModeIBeacon) { + static const uint8_t hdr[] = {0x1A, 0xFF, 0x4C, 0x00, 0x02, 0x15}; + memcpy(out, hdr, sizeof(hdr)); + memcpy(out + sizeof(hdr), app->payload, 21); + return sizeof(hdr) + 21; // 27 + } + memcpy(out, app->payload, app->payload_len); + return app->payload_len; +} + +static bool beacon_start(App* app) { + GapExtraBeaconConfig cfg = { + .min_adv_interval_ms = 100, + .max_adv_interval_ms = 150, + .adv_channel_map = GapAdvChannelMapAll, + .adv_power_level = GapAdvPowerLevel_0dBm, + .address_type = GapAddressTypeRandom, + }; + memcpy(cfg.address, app->mac, EXTRA_BEACON_MAC_ADDR_SIZE); + if(!furi_hal_bt_extra_beacon_set_config(&cfg)) return false; + uint8_t data[EXTRA_BEACON_MAX_DATA_SIZE]; + uint8_t len = build_data(app, data); + if(len > EXTRA_BEACON_MAX_DATA_SIZE) len = EXTRA_BEACON_MAX_DATA_SIZE; + if(!furi_hal_bt_extra_beacon_set_data(data, len)) return false; + if(!furi_hal_bt_extra_beacon_start()) return false; + app->running = true; + return true; +} + +static void refresh_state(App* app) { + variable_item_set_current_value_text(app->it_state, app->running ? "RUNNING" : "stopped"); + variable_item_set_current_value_text(app->it_mode, app->mode == ModeIBeacon ? "iBeacon" : "Raw"); +} + +static void mode_changed(VariableItem* item) { + App* app = variable_item_get_context(item); + app->mode = variable_item_get_current_value_index(item); + beacon_stop(app); + refresh_state(app); +} + +static void open_byte_input(App* app, EditWhat what) { + app->editing = what; + if(what == EditMac) { + byte_input_set_header_text(app->byte_input, "MAC (LE, 6 bytes)"); + byte_input_set_result_callback( + app->byte_input, NULL, NULL, app, app->mac, EXTRA_BEACON_MAC_ADDR_SIZE); + } else { + if(app->mode == ModeIBeacon) { + byte_input_set_header_text(app->byte_input, "UUID16 major2 minor2 tx1"); + app->payload_len = 21; + } else { + byte_input_set_header_text(app->byte_input, "Raw AD payload"); + if(app->payload_len == 0 || app->payload_len > EXTRA_BEACON_MAX_DATA_SIZE) + app->payload_len = 8; + } + byte_input_set_result_callback( + app->byte_input, NULL, NULL, app, app->payload, app->payload_len); + } + view_dispatcher_switch_to_view(app->vd, ViewByteInput); +} + +static void menu_enter(void* ctx, uint32_t index) { + App* app = ctx; + switch(index) { + case RowStartStop: + if(app->running) + beacon_stop(app); + else + beacon_start(app); + refresh_state(app); + break; + case RowMac: open_byte_input(app, EditMac); break; + case RowPayload: open_byte_input(app, EditPayload); break; + default: break; // State/Mode rows: no enter action + } +} + +static uint32_t to_menu(void* ctx) { + UNUSED(ctx); + return ViewMenu; +} +static uint32_t exit_cb(void* ctx) { + UNUSED(ctx); + return VIEW_NONE; +} + +int32_t ble_beacon_app(void* p) { + UNUSED(p); + App* app = malloc(sizeof(App)); + memset(app, 0, sizeof(App)); + uint8_t mac[] = {0x66, 0x55, 0x44, 0x33, 0x22, 0x11}; + memcpy(app->mac, mac, 6); + uint8_t demo[] = {0x02, 0x01, 0x06, 0x08, 0x09, 'F', 'l', 'i', 'p', 'p', 'e', 'r'}; + memcpy(app->payload, demo, sizeof(demo)); + app->payload_len = sizeof(demo); + + app->gui = furi_record_open(RECORD_GUI); + app->vd = view_dispatcher_alloc(); + view_dispatcher_attach_to_gui(app->vd, app->gui, ViewDispatcherTypeFullscreen); + + app->menu = variable_item_list_alloc(); + variable_item_list_add(app->menu, "Start / Stop", 1, NULL, NULL); + app->it_state = variable_item_list_add(app->menu, "State", 1, NULL, NULL); + app->it_mode = variable_item_list_add(app->menu, "Mode", 2, mode_changed, app); + variable_item_set_values_count(app->it_mode, 2); + variable_item_list_add(app->menu, "Edit MAC", 1, NULL, NULL); + variable_item_list_add(app->menu, "Edit payload", 1, NULL, NULL); + variable_item_list_set_enter_callback(app->menu, menu_enter, app); + refresh_state(app); + view_set_previous_callback(variable_item_list_get_view(app->menu), exit_cb); + view_dispatcher_add_view(app->vd, ViewMenu, variable_item_list_get_view(app->menu)); + + app->byte_input = byte_input_alloc(); + view_set_previous_callback(byte_input_get_view(app->byte_input), to_menu); + view_dispatcher_add_view(app->vd, ViewByteInput, byte_input_get_view(app->byte_input)); + + view_dispatcher_switch_to_view(app->vd, ViewMenu); + view_dispatcher_run(app->vd); + + beacon_stop(app); + view_dispatcher_remove_view(app->vd, ViewMenu); + view_dispatcher_remove_view(app->vd, ViewByteInput); + variable_item_list_free(app->menu); + byte_input_free(app->byte_input); + view_dispatcher_free(app->vd); + furi_record_close(RECORD_GUI); + free(app); + return 0; +} diff --git a/ble_beacon/icon.png b/ble_beacon/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