License Flipper Blue++ under GPL-3.0-or-later, matching the Flipper Zero firmware it builds on and links against. Add the verbatim GPLv3 text as LICENSE, an SPDX + copyright header to every original source file and build.sh, and a License section to the README noting the app sources are (C) 2026 Andy Dixon while the upstream firmware/SDK remain (C) Flipper Devices Inc., both GPLv3. Headers are comment-only; the full suite still builds and all faps link. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LEaD4ZeAfvWLBUxXp8G11r
177 lines
6.2 KiB
C
177 lines
6.2 KiB
C
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright (C) 2026 Andy Dixon <andy@dixon.cx>
|
|
// 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 <furi.h>
|
|
#include <furi_hal_bt.h>
|
|
#include <extra_beacon.h>
|
|
#include <gui/gui.h>
|
|
#include <gui/view_dispatcher.h>
|
|
#include <gui/modules/variable_item_list.h>
|
|
#include <gui/modules/byte_input.h>
|
|
#include <string.h>
|
|
|
|
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;
|
|
}
|