Add FindMy Flipper to system apps

This commit is contained in:
MatthewKuKanich
2024-03-08 13:10:14 -05:00
parent a17efe7117
commit 13fe7b3bfc
14 changed files with 736 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
App(
appid="findmy",
name="FindMy Flipper",
apptype=FlipperAppType.EXTERNAL,
entry_point="findmy_main",
requires=["gui"],
stack_size=1 * 1024,
order=35,
fap_icon="location_icon.png",
fap_category="Bluetooth",
fap_author="@MatthewKuKanich",
fap_version="1.0",
fap_description="BLE FindMy Location Beacon",
)

View File

@@ -0,0 +1,164 @@
#include "findmy_i.h"
static bool findmy_custom_event_callback(void* context, uint32_t event) {
furi_assert(context);
FindMy* app = context;
return scene_manager_handle_custom_event(app->scene_manager, event);
}
static bool findmy_back_event_callback(void* context) {
furi_assert(context);
FindMy* app = context;
return scene_manager_handle_back_event(app->scene_manager);
}
static FindMy* findmy_app_alloc() {
FindMy* app = malloc(sizeof(FindMy));
app->gui = furi_record_open(RECORD_GUI);
app->view_dispatcher = view_dispatcher_alloc();
view_dispatcher_enable_queue(app->view_dispatcher);
app->scene_manager = scene_manager_alloc(&findmy_scene_handlers, app);
view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
view_dispatcher_set_custom_event_callback(app->view_dispatcher, findmy_custom_event_callback);
view_dispatcher_set_navigation_event_callback(
app->view_dispatcher, findmy_back_event_callback);
app->findmy_main = findmy_main_alloc(app);
view_dispatcher_add_view(
app->view_dispatcher, FindMyViewMain, findmy_main_get_view(app->findmy_main));
app->byte_input = byte_input_alloc();
view_dispatcher_add_view(
app->view_dispatcher, FindMyViewByteInput, byte_input_get_view(app->byte_input));
app->var_item_list = variable_item_list_alloc();
view_dispatcher_add_view(
app->view_dispatcher,
FindMyViewVarItemList,
variable_item_list_get_view(app->var_item_list));
view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
app->beacon_active = false;
findmy_main_update_active(app->findmy_main, app->beacon_active);
app->broadcast_interval = 5;
findmy_main_update_interval(app->findmy_main, app->broadcast_interval);
app->transmit_power = 6;
app->apple = true;
findmy_main_update_apple(app->findmy_main, app->apple);
return app;
}
static void findmy_app_free(FindMy* app) {
furi_assert(app);
view_dispatcher_remove_view(app->view_dispatcher, FindMyViewVarItemList);
variable_item_list_free(app->var_item_list);
view_dispatcher_remove_view(app->view_dispatcher, FindMyViewByteInput);
byte_input_free(app->byte_input);
view_dispatcher_remove_view(app->view_dispatcher, FindMyViewMain);
findmy_main_free(app->findmy_main);
view_dispatcher_free(app->view_dispatcher);
scene_manager_free(app->scene_manager);
furi_record_close(RECORD_GUI);
free(app);
}
static void findmy_start(FindMy* app) {
furi_hal_bt_extra_beacon_stop(); // Stop any running beacon
app->config.min_adv_interval_ms = app->broadcast_interval * 1000; // Converting s to ms
app->config.max_adv_interval_ms = (app->broadcast_interval * 1000) + 150;
app->config.adv_channel_map = GapAdvChannelMapAll;
app->config.adv_power_level = GapAdvPowerLevel_0dBm + app->transmit_power;
app->config.address_type = GapAddressTypePublic;
uint8_t mac[EXTRA_BEACON_MAC_ADDR_SIZE] = {0x4D, 0x61, 0x74, 0x4B, 0x75, 0x4B};
furi_hal_bt_reverse_mac_addr(mac);
memcpy(&app->config.address, mac, sizeof(app->config.address));
furi_check(furi_hal_bt_extra_beacon_set_config(&app->config));
uint8_t data[EXTRA_BEACON_MAX_DATA_SIZE];
uint8_t* it = data;
// For Apple AirTags
*it++ = 0x1E; // Length
*it++ = 0xFF; // Manufacturer Specific Data
*it++ = 0x4C; // Company ID (Apple, Inc.)
*it++ = 0x00; // State
*it++ = 0x12; // Data - Public Key without the MAC address
*it++ = 0x81; // ...
*it++ = 0xB9; // ...
*it++ = 0x02; // First 2 bits are the version, the rest is the battery level
*it++ = 0x7E; // Hint (0x00)
furi_check(furi_hal_bt_extra_beacon_set_data(data, it - data));
}
int32_t findmy_main(void* p) {
UNUSED(p);
FindMy* app = findmy_app_alloc();
findmy_start(app);
scene_manager_next_scene(app->scene_manager, FindMySceneMain);
view_dispatcher_run(app->view_dispatcher);
findmy_app_free(app);
return 0;
}
void findmy_change_broadcast_interval(FindMy* app, uint8_t value) {
if(value > 10 || value < 1) {
return;
}
app->broadcast_interval = value;
findmy_main_update_interval(app->findmy_main, app->broadcast_interval);
if(app->beacon_active) {
// Always check if beacon is active before changing config
furi_check(furi_hal_bt_extra_beacon_stop());
}
app->config.min_adv_interval_ms = app->broadcast_interval * 1000;
app->config.max_adv_interval_ms = app->config.min_adv_interval_ms + 150;
furi_check(furi_hal_bt_extra_beacon_set_config(&app->config));
if(app->beacon_active) {
furi_check(furi_hal_bt_extra_beacon_start());
}
}
void findmy_change_transmit_power(FindMy* app, uint8_t value) {
if(value > 6) {
return;
}
app->transmit_power = value;
if(app->beacon_active) {
furi_check(furi_hal_bt_extra_beacon_stop());
}
app->config.adv_power_level = GapAdvPowerLevel_0dBm + app->transmit_power;
furi_check(furi_hal_bt_extra_beacon_set_config(&app->config));
if(app->beacon_active) {
furi_check(furi_hal_bt_extra_beacon_start());
}
}
void findmy_toggle_beacon(FindMy* app) {
app->beacon_active = !app->beacon_active;
findmy_main_update_active(app->findmy_main, app->beacon_active);
findmy_main_update_apple(app->findmy_main, app->apple);
if(app->beacon_active) {
furi_hal_bt_extra_beacon_start();
} else {
furi_hal_bt_extra_beacon_stop();
}
}

View File

@@ -0,0 +1,3 @@
#pragma once
typedef struct FindMy FindMy;

View File

@@ -0,0 +1,42 @@
#pragma once
#include "findmy.h"
#include <furi_hal_bt.h>
#include <extra_beacon.h>
#include <assets_icons.h>
#include <gui/gui.h>
#include <gui/scene_manager.h>
#include <gui/view_dispatcher.h>
#include "views/findmy_main.h"
#include <gui/modules/byte_input.h>
#include <gui/modules/variable_item_list.h>
#include "scenes/findmy_scene.h"
struct FindMy {
Gui* gui;
SceneManager* scene_manager;
ViewDispatcher* view_dispatcher;
FindMyMain* findmy_main;
ByteInput* byte_input;
VariableItemList* var_item_list;
uint8_t mac_buf[EXTRA_BEACON_MAC_ADDR_SIZE];
uint8_t packet_buf[EXTRA_BEACON_MAX_DATA_SIZE];
GapExtraBeaconConfig config;
bool apple;
bool beacon_active;
uint8_t broadcast_interval;
uint8_t transmit_power;
};
typedef enum {
FindMyViewMain,
FindMyViewByteInput,
FindMyViewVarItemList,
} FindMyView;
void findmy_change_broadcast_interval(FindMy* app, uint8_t value);
void findmy_change_transmit_power(FindMy* app, uint8_t value);
void findmy_toggle_beacon(FindMy* app);

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -0,0 +1,31 @@
#include "findmy_scene.h"
#include "findmy.h"
// Generate scene on_enter handlers array
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
void (*const findmy_on_enter_handlers[])(void*) = {
#include "findmy_scenes.h"
};
#undef ADD_SCENE
// Generate scene on_event handlers array
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
bool (*const findmy_on_event_handlers[])(void* context, SceneManagerEvent event) = {
#include "findmy_scenes.h"
};
#undef ADD_SCENE
// Generate scene on_exit handlers array
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
void (*const findmy_on_exit_handlers[])(void* context) = {
#include "findmy_scenes.h"
};
#undef ADD_SCENE
// Initialize scene handlers configuration structure
const SceneManagerHandlers findmy_scene_handlers = {
.on_enter_handlers = findmy_on_enter_handlers,
.on_event_handlers = findmy_on_event_handlers,
.on_exit_handlers = findmy_on_exit_handlers,
.scene_num = FindMySceneNum,
};

View File

@@ -0,0 +1,30 @@
#pragma once
#include <gui/scene_manager.h>
#include "findmy.h"
// Generate scene id and total number
#define ADD_SCENE(prefix, name, id) FindMyScene##id,
typedef enum {
#include "findmy_scenes.h"
FindMySceneNum,
} FindMyScene;
#undef ADD_SCENE
extern const SceneManagerHandlers findmy_scene_handlers;
// Generate scene on_enter handlers declaration
#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
#include "findmy_scenes.h"
#undef ADD_SCENE
// Generate scene on_event handlers declaration
#define ADD_SCENE(prefix, name, id) \
bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event);
#include "findmy_scenes.h"
#undef ADD_SCENE
// Generate scene on_exit handlers declaration
#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context);
#include "findmy_scenes.h"
#undef ADD_SCENE

View File

@@ -0,0 +1,98 @@
#include "../findmy_i.h"
enum VarItemListIndex {
VarItemListIndexBroadcastInterval,
VarItemListIndexTransmitPower,
VarItemListIndexRegisterTag,
VarItemListIndexAbout,
};
void findmy_scene_config_broadcast_interval_changed(VariableItem* item) {
FindMy* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
findmy_change_broadcast_interval(app, index + 1);
char str[5];
snprintf(str, sizeof(str), "%ds", app->broadcast_interval);
variable_item_set_current_value_text(item, str);
variable_item_set_current_value_index(item, app->broadcast_interval - 1);
}
void findmy_scene_config_transmit_power_changed(VariableItem* item) {
FindMy* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
findmy_change_transmit_power(app, index);
char str[7];
snprintf(str, sizeof(str), "%ddBm", app->transmit_power);
variable_item_set_current_value_text(item, str);
variable_item_set_current_value_index(item, app->transmit_power);
}
void findmy_scene_config_callback(void* context, uint32_t index) {
furi_assert(context);
FindMy* app = context;
view_dispatcher_send_custom_event(app->view_dispatcher, index);
}
void findmy_scene_config_on_enter(void* context) {
FindMy* app = context;
VariableItemList* var_item_list = app->var_item_list;
VariableItem* item;
item = variable_item_list_add(
var_item_list,
"Broadcast Interval",
10,
findmy_scene_config_broadcast_interval_changed,
app);
// Broadcast Interval is 1-10, so use 0-9 and offset indexes by 1
variable_item_set_current_value_index(item, app->broadcast_interval - 1);
char broadcast_interval_s[5];
snprintf(broadcast_interval_s, sizeof(broadcast_interval_s), "%ds", app->broadcast_interval);
variable_item_set_current_value_text(item, broadcast_interval_s);
item = variable_item_list_add(
var_item_list, "Transmit Power", 7, findmy_scene_config_transmit_power_changed, app);
variable_item_set_current_value_index(item, app->transmit_power);
char transmit_power_s[7];
snprintf(transmit_power_s, sizeof(transmit_power_s), "%ddBm", app->transmit_power);
variable_item_set_current_value_text(item, transmit_power_s);
item = variable_item_list_add(var_item_list, "Register Tag", 0, NULL, NULL);
item = variable_item_list_add(var_item_list, "Matthew KuKanich, Thanks to Chapoly1305, WillyJL, OpenHaystack, Testers", 1, NULL, NULL);
variable_item_set_current_value_text(item, "Credits");
variable_item_list_set_enter_callback(var_item_list, findmy_scene_config_callback, app);
variable_item_list_set_selected_item(
var_item_list, scene_manager_get_scene_state(app->scene_manager, FindMySceneConfig));
view_dispatcher_switch_to_view(app->view_dispatcher, FindMyViewVarItemList);
}
bool findmy_scene_config_on_event(void* context, SceneManagerEvent event) {
FindMy* app = context;
bool consumed = false;
if(event.type == SceneManagerEventTypeCustom) {
scene_manager_set_scene_state(app->scene_manager, FindMySceneConfig, event.event);
consumed = true;
switch(event.event) {
case VarItemListIndexRegisterTag:
scene_manager_next_scene(app->scene_manager, FindMySceneConfigMac);
break;
case VarItemListIndexAbout:
break;
default:
break;
}
}
return consumed;
}
void findmy_scene_config_on_exit(void* context) {
FindMy* app = context;
VariableItemList* var_item_list = app->var_item_list;
variable_item_list_reset(var_item_list);
}

View File

@@ -0,0 +1,60 @@
#include "../findmy_i.h"
enum ByteInputResult {
ByteInputResultOk,
};
static void findmy_scene_config_mac_callback(void* context) {
FindMy* app = context;
view_dispatcher_send_custom_event(app->view_dispatcher, ByteInputResultOk);
}
void findmy_scene_config_mac_on_enter(void* context) {
FindMy* app = context;
ByteInput* byte_input = app->byte_input;
byte_input_set_header_text(byte_input, "Enter Bluetooth MAC:");
memcpy(app->mac_buf, &app->config.address, sizeof(app->mac_buf));
furi_hal_bt_reverse_mac_addr(app->mac_buf);
byte_input_set_result_callback(
byte_input,
findmy_scene_config_mac_callback,
NULL,
app,
app->mac_buf,
sizeof(app->mac_buf));
view_dispatcher_switch_to_view(app->view_dispatcher, FindMyViewByteInput);
}
bool findmy_scene_config_mac_on_event(void* context, SceneManagerEvent event) {
FindMy* app = context;
bool consumed = false;
if(event.type == SceneManagerEventTypeCustom) {
consumed = true;
switch(event.event) {
case ByteInputResultOk:
furi_hal_bt_reverse_mac_addr(app->mac_buf);
memcpy(&app->config.address, app->mac_buf, sizeof(app->config.address));
furi_hal_bt_extra_beacon_set_config(&app->config);
scene_manager_next_scene(app->scene_manager, FindMySceneConfigPacket);
break;
default:
break;
}
}
return consumed;
}
void findmy_scene_config_mac_on_exit(void* context) {
FindMy* app = context;
byte_input_set_result_callback(app->byte_input, NULL, NULL, NULL, NULL, 0);
byte_input_set_header_text(app->byte_input, "");
}

View File

@@ -0,0 +1,64 @@
#include "../findmy_i.h"
enum ByteInputResult {
ByteInputResultOk,
};
static void findmy_scene_config_packet_callback(void* context) {
FindMy* app = context;
view_dispatcher_send_custom_event(app->view_dispatcher, ByteInputResultOk);
}
void findmy_scene_config_packet_on_enter(void* context) {
FindMy* app = context;
ByteInput* byte_input = app->byte_input;
byte_input_set_header_text(byte_input, "Enter Bluetooth Payload:");
memset(app->packet_buf, 0, sizeof(app->packet_buf));
furi_hal_bt_extra_beacon_get_data(app->packet_buf);
byte_input_set_result_callback(
byte_input,
findmy_scene_config_packet_callback,
NULL,
app,
app->packet_buf,
sizeof(app->packet_buf));
view_dispatcher_switch_to_view(app->view_dispatcher, FindMyViewByteInput);
}
bool findmy_scene_config_packet_on_event(void* context, SceneManagerEvent event) {
FindMy* app = context;
bool consumed = false;
if(event.type == SceneManagerEventTypeCustom) {
consumed = true;
switch(event.event) {
case ByteInputResultOk:
scene_manager_search_and_switch_to_previous_scene(
app->scene_manager, FindMySceneConfig);
furi_check(furi_hal_bt_extra_beacon_set_data(app->packet_buf, sizeof(app->packet_buf)));
if (app->packet_buf[0] == 0x1E && app->packet_buf[3] == 0x00) {
app->apple = true; // Checks payload data for Apple identifier
} else {
app->apple = false;
}
findmy_main_update_apple(app->findmy_main, app->apple);
break;
default:
break;
}
}
return consumed;
}
void findmy_scene_config_packet_on_exit(void* context) {
FindMy* app = context;
byte_input_set_result_callback(app->byte_input, NULL, NULL, NULL, NULL, 0);
byte_input_set_header_text(app->byte_input, "");
}

View File

@@ -0,0 +1,54 @@
#include "../findmy_i.h"
void findmy_scene_main_callback(FindMyMainEvent event, void* context) {
furi_assert(context);
FindMy* app = context;
view_dispatcher_send_custom_event(app->view_dispatcher, event);
}
void findmy_scene_main_on_enter(void* context) {
FindMy* app = context;
findmy_main_set_callback(app->findmy_main, findmy_scene_main_callback, app);
view_dispatcher_switch_to_view(app->view_dispatcher, FindMyViewMain);
}
bool findmy_scene_main_on_event(void* context, SceneManagerEvent event) {
FindMy* app = context;
bool consumed = false;
if(event.type == SceneManagerEventTypeCustom) {
consumed = true;
switch(event.event) {
case FindMyMainEventToggle:
findmy_toggle_beacon(app);
break;
case FindMyMainEventBackground:
furi_hal_bt_extra_beacon_start();
view_dispatcher_stop(app->view_dispatcher);
break;
case FindMyMainEventConfig:
scene_manager_next_scene(app->scene_manager, FindMySceneConfig);
break;
case FindMyMainEventIntervalUp:
findmy_change_broadcast_interval(app, app->broadcast_interval + 1);
break;
case FindMyMainEventIntervalDown:
findmy_change_broadcast_interval(app, app->broadcast_interval - 1);
break;
case FindMyMainEventQuit:
break;
default:
consumed = false;
break;
}
}
return consumed;
}
void findmy_scene_main_on_exit(void* context) {
FindMy* app = context;
UNUSED(app);
}

View File

@@ -0,0 +1,4 @@
ADD_SCENE(findmy, main, Main)
ADD_SCENE(findmy, config, Config)
ADD_SCENE(findmy, config_mac, ConfigMac)
ADD_SCENE(findmy, config_packet, ConfigPacket)

View File

@@ -0,0 +1,143 @@
#include "findmy_main.h"
#include "../findmy_i.h"
struct FindMyMain {
View* view;
FindMyMainCallback callback;
void* context;
};
typedef struct {
bool active;
bool apple;
uint8_t interval;
} FindMyMainModel;
static void findmy_main_draw_callback(Canvas* canvas, void* _model) {
FindMyMainModel* model = _model;
canvas_clear(canvas);
canvas_set_bitmap_mode(canvas, true);
canvas_set_font(canvas, FontPrimary);
canvas_draw_str(canvas, 4, 11, "FindMy Flipper");
canvas_set_font(canvas, FontSecondary);
if(model->active) {
canvas_draw_str(canvas, 4, 49, "Broadcast Active");
canvas_draw_icon(canvas, 78, 42, &I_Ok_btn_9x9);
} else {
canvas_draw_str(canvas, 4, 49, "Broadcast Inactive");
}
canvas_set_font(canvas, FontSecondary);
canvas_draw_str(canvas, 4, 21, "Press <- to run in background");
canvas_set_font(canvas, FontSecondary);
char interval_str[20];
snprintf(interval_str, sizeof(interval_str), "Ping Interval: %ds", model->interval);
canvas_draw_str(canvas, 4, 62, interval_str);
canvas_set_font(canvas, FontPrimary);
if(model->apple){
canvas_draw_str(canvas, 4, 32, "Apple Network");
canvas_draw_icon(canvas, 80, 24, &I_Lock_7x8);
} else {
canvas_draw_str(canvas, 4, 32, "Samsung Network");
canvas_draw_icon(canvas, 97, 24, &I_Lock_7x8);
}
canvas_set_font(canvas, FontSecondary);
canvas_draw_str(canvas, 100, 61, "Config");
canvas_draw_line(canvas, 100, 51, 127, 51);
canvas_draw_line(canvas, 97, 53, 97, 63);
canvas_draw_line(canvas, 97, 53, 99, 51);
canvas_draw_line(canvas, 3, 52, 87, 52);
}
static bool findmy_main_input_callback(InputEvent* event, void* context) {
furi_assert(context);
FindMyMain* findmy_main = context;
bool consumed = false;
if(event->type == InputTypePress) {
consumed = true;
// FIXME: finish implementing handlers in scene side
switch(event->key) {
case InputKeyBack:
findmy_main->callback(FindMyMainEventQuit, findmy_main->context);
// furi_hal_bt_extra_beacon_stop();
break;
case InputKeyOk:
findmy_main->callback(FindMyMainEventToggle, findmy_main->context);
break;
case InputKeyLeft:
findmy_main->callback(FindMyMainEventBackground, findmy_main->context);
break;
case InputKeyRight:
findmy_main->callback(FindMyMainEventConfig, findmy_main->context);
break;
case InputKeyUp:
findmy_main->callback(FindMyMainEventIntervalUp, findmy_main->context);
break;
case InputKeyDown:
findmy_main->callback(FindMyMainEventIntervalDown, findmy_main->context);
break;
default:
break;
}
}
return consumed;
}
FindMyMain* findmy_main_alloc(FindMy* app) {
FindMyMain* findmy_main = malloc(sizeof(FindMyMain));
findmy_main->view = view_alloc();
view_allocate_model(findmy_main->view, ViewModelTypeLocking, sizeof(FindMyMainModel));
with_view_model(
findmy_main->view,
FindMyMainModel * model,
{
model->active = app->beacon_active;
model->apple = app->apple;
model->interval = app->broadcast_interval;
},
false);
view_set_context(findmy_main->view, findmy_main);
view_set_draw_callback(findmy_main->view, findmy_main_draw_callback);
view_set_input_callback(findmy_main->view, findmy_main_input_callback);
return findmy_main;
}
void findmy_main_free(FindMyMain* findmy_main) {
furi_assert(findmy_main);
view_free(findmy_main->view);
free(findmy_main);
}
View* findmy_main_get_view(FindMyMain* findmy_main) {
furi_assert(findmy_main);
return findmy_main->view;
}
void findmy_main_set_callback(FindMyMain* findmy_main, FindMyMainCallback callback, void* context) {
furi_assert(findmy_main);
furi_assert(callback);
findmy_main->callback = callback;
findmy_main->context = context;
}
void findmy_main_update_active(FindMyMain* findmy_main, bool active) {
furi_assert(findmy_main);
with_view_model(
findmy_main->view, FindMyMainModel * model, { model->active = active; }, true);
}
void findmy_main_update_interval(FindMyMain* findmy_main, uint8_t interval) {
furi_assert(findmy_main);
with_view_model(
findmy_main->view, FindMyMainModel * model, { model->interval = interval; }, true);
}
void findmy_main_update_apple(FindMyMain* findmy_main, bool apple) {
furi_assert(findmy_main);
with_view_model(
findmy_main->view, FindMyMainModel * model, { model->apple = apple; }, true);
}

View File

@@ -0,0 +1,29 @@
#pragma once
#include "../findmy.h"
#include <gui/view.h>
typedef enum {
FindMyMainEventToggle,
FindMyMainEventBackground,
FindMyMainEventConfig,
FindMyMainEventIntervalUp,
FindMyMainEventIntervalDown,
FindMyMainEventQuit,
} FindMyMainEvent;
typedef struct FindMyMain FindMyMain;
typedef void (*FindMyMainCallback)(FindMyMainEvent event, void* context);
// Main functionality
FindMyMain* findmy_main_alloc(FindMy* app);
void findmy_main_free(FindMyMain* findmy_main);
View* findmy_main_get_view(FindMyMain* findmy_main);
// To communicate with scene
void findmy_main_set_callback(FindMyMain* findmy_main, FindMyMainCallback callback, void* context);
// To redraw when info changes
void findmy_main_update_active(FindMyMain* findmy_main, bool active);
void findmy_main_update_interval(FindMyMain* findmy_main, uint8_t interval);
void findmy_main_update_apple(FindMyMain* findmy_main, bool apple);