From 071b666090faea0c0154369ea81dd948805053e1 Mon Sep 17 00:00:00 2001
From: MX <10697207+xMasterX@users.noreply.github.com>
Date: Mon, 11 Mar 2024 03:47:10 +0300
Subject: [PATCH] add findmy flipper app

by @MatthewKuKanich

https://github.com/MatthewKuKanich/FindMyFlipper
---
 applications/system/find_my_flipper/README.md | 108 +++++++++
 .../system/find_my_flipper/application.fam    |  15 ++
 applications/system/find_my_flipper/findmy.c  | 167 +++++++++++++
 applications/system/find_my_flipper/findmy.h  |   5 +
 .../system/find_my_flipper/findmy_i.h         |  59 +++++
 .../system/find_my_flipper/findmy_startup.c   |  11 +
 .../system/find_my_flipper/findmy_state.c     | 130 +++++++++++
 .../system/find_my_flipper/findmy_state.h     |  28 +++
 .../system/find_my_flipper/helpers/base64.c   | 141 +++++++++++
 .../system/find_my_flipper/helpers/base64.h   |  21 ++
 .../icons/DolphinDone_80x58.png               | Bin 0 -> 1664 bytes
 .../system/find_my_flipper/icons/Lock_7x8.png | Bin 0 -> 3597 bytes
 .../find_my_flipper/icons/Ok_btn_9x9.png      | Bin 0 -> 3605 bytes
 .../icons/WarningDolphinFlip_45x42.png        | Bin 0 -> 1437 bytes
 .../find_my_flipper/icons/text_10px.png       | Bin 0 -> 158 bytes
 .../system/find_my_flipper/location_icon.png  | Bin 0 -> 6750 bytes
 .../find_my_flipper/scenes/findmy_scene.c     |  31 +++
 .../find_my_flipper/scenes/findmy_scene.h     |  30 +++
 .../scenes/findmy_scene_config.c              | 110 +++++++++
 .../scenes/findmy_scene_config_import.c       | 220 ++++++++++++++++++
 .../findmy_scene_config_import_result.c       |  58 +++++
 .../scenes/findmy_scene_config_mac.c          |  67 ++++++
 .../scenes/findmy_scene_config_packet.c       |  61 +++++
 .../scenes/findmy_scene_main.c                |  63 +++++
 .../find_my_flipper/scenes/findmy_scenes.h    |   6 +
 .../find_my_flipper/views/findmy_main.c       | 150 ++++++++++++
 .../find_my_flipper/views/findmy_main.h       |  29 +++
 27 files changed, 1510 insertions(+)
 create mode 100644 applications/system/find_my_flipper/README.md
 create mode 100644 applications/system/find_my_flipper/application.fam
 create mode 100644 applications/system/find_my_flipper/findmy.c
 create mode 100644 applications/system/find_my_flipper/findmy.h
 create mode 100644 applications/system/find_my_flipper/findmy_i.h
 create mode 100644 applications/system/find_my_flipper/findmy_startup.c
 create mode 100644 applications/system/find_my_flipper/findmy_state.c
 create mode 100644 applications/system/find_my_flipper/findmy_state.h
 create mode 100644 applications/system/find_my_flipper/helpers/base64.c
 create mode 100644 applications/system/find_my_flipper/helpers/base64.h
 create mode 100644 applications/system/find_my_flipper/icons/DolphinDone_80x58.png
 create mode 100644 applications/system/find_my_flipper/icons/Lock_7x8.png
 create mode 100644 applications/system/find_my_flipper/icons/Ok_btn_9x9.png
 create mode 100644 applications/system/find_my_flipper/icons/WarningDolphinFlip_45x42.png
 create mode 100644 applications/system/find_my_flipper/icons/text_10px.png
 create mode 100644 applications/system/find_my_flipper/location_icon.png
 create mode 100644 applications/system/find_my_flipper/scenes/findmy_scene.c
 create mode 100644 applications/system/find_my_flipper/scenes/findmy_scene.h
 create mode 100644 applications/system/find_my_flipper/scenes/findmy_scene_config.c
 create mode 100644 applications/system/find_my_flipper/scenes/findmy_scene_config_import.c
 create mode 100644 applications/system/find_my_flipper/scenes/findmy_scene_config_import_result.c
 create mode 100644 applications/system/find_my_flipper/scenes/findmy_scene_config_mac.c
 create mode 100644 applications/system/find_my_flipper/scenes/findmy_scene_config_packet.c
 create mode 100644 applications/system/find_my_flipper/scenes/findmy_scene_main.c
 create mode 100644 applications/system/find_my_flipper/scenes/findmy_scenes.h
 create mode 100644 applications/system/find_my_flipper/views/findmy_main.c
 create mode 100644 applications/system/find_my_flipper/views/findmy_main.h

diff --git a/applications/system/find_my_flipper/README.md b/applications/system/find_my_flipper/README.md
new file mode 100644
index 0000000000..d3fc8f5d8c
--- /dev/null
+++ b/applications/system/find_my_flipper/README.md
@@ -0,0 +1,108 @@
+# FindMy Flipper - FindMy SmartTag Emulator
+
+This app extends the functionality of the FlipperZero's bluetooth capabilities, enabling it to act as an Apple AirTag or Samsung SmartTag, or even both simultaneously. It utilizes the FlipperZero's BLE beacon to broadcast a SmartTag signal to be picked up by the FindMy Network. I made this to serve as a versatile tool for tracking purposes, offering the ability to clone existing tags, generate OpenHaystack key pairs for integration with Apple's FindMy network, and tune the device's beacon broadcast settings.
+
+## Features
+
+1. Tag Emulation: Clone your existing Apple AirTag or Samsung SmartTag to the FlipperZero, or generate a key pair for use with the FindMy network without owning an actual AirTag.
+2. Customization: Users can adjust the interval between beacon broadcasts and modify the transmit power to suit their needs, optimizing for both visibility and battery life.
+3. Efficient Background Operation: The app is optimized to run in the background, ensuring that your FlipperZero can still be tracked with minimal battery usage and without stopping normal use.
+
+## Usage Guide
+
+### Step 1: Installation
+- **Option A:** Use the released/precompiled firmware appropriate (FAP) for your device.
+- **Option B:** Build the firmware yourself using `fbt/ufbt`.
+- Both Installation options require you to be running a dev build of firmware. When release gets access to the extra BLE beacon this will change, thank you!
+- All firmware should now work with main branch, including icons
+  
+### Step 2: Obtaining SmartTag Data
+
+#### Option A: Cloning Existing Tag (Preferred and allows you to track without additional setup)
+1. **Pair a Tag:** First, pair an AirTag or Samsung SmartTag with your device.
+2. **Enter 'Lost' Mode:** Keep the tag away from the device it's registered to for approximately 15 minutes.
+3. **Download nrfConnect:** Install nrfConnect from the Google Play Store. (Apple version doesn't reveal the needed Raw data, looking for a workaround)
+4. **Filter and Scan:**
+   - Open the app, click on filters, and exclude all except for the brand of your tag (Apple/Samsung).
+   - Adjust the RSSI to the lowest setting (-40 dBm).
+   - Initiate a scan. Wait for your SmartTag to appear as a "FindMy" device.
+5. **Capture Data:** Click **Raw** or **View Raw** to capture your **payload** and note your tag's **MAC Address**. Immediately remove the tag's battery to prevent key/MAC rotation.
+6. **Enter Data in FlipperZero App:** Input the captured **payload** and **MAC Address** into the FlipperZero app.
+
+#### Option B: Open Haystack Method
+1. **Generate a Tag:** Download the `generate_keys.py` file and execute it in your terminal. (You will need cryptography ```python3 -m pip install cryptography```)
+2. **Follow Prompts:** During execution, you'll be prompted for inputs. By the end, you'll obtain a **Private Key**, **Public Key**, **Payload**, and **MAC Address**.
+   - **Private Key** is necessary to receive location reports from Apple.
+   - **MAC Address** should be registered in the FlipperZero app:
+     1. Open the app and navigate to the config menu.
+     2. Choose "register tag" and enter the MAC Address when prompted.
+     3. A payload dialog will appear next. Enter your **Payload** here.
+     4. Click save.
+3. **Configuration Completion:** With this setup, your device is ready for Open Haystack. Proceed with the specific steps for Open Haystack or MaclessHaystack based on your setup.
+   - Don't Own a Mac: https://github.com/dchristl/macless-haystack or https://github.com/Chapoly1305/FindMy
+   - Own a Mac: https://github.com/seemoo-lab/openhaystack
+
+## Setting Up on Mac with OpenHayStack (OHS) App -- If you own a Mac instructions
+
+Follow these steps to get everything working on a Mac using the latest version of the OpenHayStack app.
+Thanks to Wr3nch for the help
+
+### Step 1: Create a New Device
+- Start by creating a new device in the OpenHayStack app, but **do not deploy** it immediately after creation.
+
+### Step 2: Export Configuration
+- Choose to **EXPORT** the configuration by selecting "all accessories as file." To simplify, ensure you only have one entry in the list before exporting.
+- It is crucial that the export format is in JSON.
+
+### Step 3: Modify the JSON File
+Open the exported JSON file in a text editor and make the following changes:
+- **Left OHS, Right keys from my ```generate_keys.py``` script:**
+    - `symmetricKey` should be set to the `Hashed adv key`.
+    - `privateKey` should be replaced with your `Private Key`.
+    - `oldestRelevantSymmetricKey` should also use the `Hashed adv key`.
+- Additionally, update the following attributes to `true`:
+    - `"isDeployed": true`
+    - `"isActive": true`
+
+### Step 4: Re-import the Configuration
+- After saving your changes to the JSON file, re-import it back into OpenHayStack.
+
+### Step 5: Adjust Settings in OHS App
+- In the OpenHayStack Mac App, navigate to the top bar and change the time setting from `1 Day` to `30min`.
+- Give it some time to process and apply the new settings.
+
+By following these steps, you should have your device set up and ready to go with OpenHayStack on a Mac.
+****
+
+### Step 3: Configuration on the FlipperZero
+- Upon launching the app, open the config menu and either click ```Import Tag From File``` or ```Register Tag Manually```. Put your generated .keys file onto the FlipperZero SD card inside the AppsData/FindMyFlipper folder to import from file. Or you can manually enter the tag information. When using the cloning method, you can export a .txt file from nrfConnect (click save button) amd place that in the same folder in order to import.
+
+### Step 4: Tracking
+- Once the app is configured, your FlipperZero can be tracked using the relevant platform's tracking service (FindMy app for Apple devices, SmartThings for Samsung devices, and respective web browsers). If using generated keys and OpenHaystack then you can track on the OHS app or via the Macless Haystack setup. Links to both are above
+
+
+Customization
+
+- Beacon Interval: Adjust how frequently your FlipperZero broadcasts its presence.
+- Transmit Power: Increase or decrease the signal strength to balance between tracking range and battery life.
+
+Background Use
+
+The app is designed to have a negligible impact on battery life, even when running in the background. This allows for continuous tracking without the need for frequent recharging.
+
+Compatibility
+
+- Apple devices for AirTag tracking via the FindMy network.
+- Any device that supports Samsung SmartTag tracking, including web browsers (previously FindMyMobile).
+
+Thanks
+
+- Huge thanks to all the people that contributed to the OpenHaystack project, supporting projects, and guides on the subject. This wouldn't be a thing without any of you! Special thanks to WillyJL for helping get the app input working and overall overhaul of the apps functions!
+
+Legal and Privacy
+
+This app is intended for personal and educational use. Users are responsible for complying with local privacy laws and regulations regarding tracking devices. The cloning and emulation of tracking tags should be done responsibly and with respect to the ownership of the original devices.
+
+Disclaimer
+
+This project is not affiliated with Apple Inc. or Samsung. All product names, logos, and brands are property of their respective owners. Use this app responsibly and ethically.
diff --git a/applications/system/find_my_flipper/application.fam b/applications/system/find_my_flipper/application.fam
new file mode 100644
index 0000000000..2ef49310f0
--- /dev/null
+++ b/applications/system/find_my_flipper/application.fam
@@ -0,0 +1,15 @@
+App(
+    appid="findmy",
+    name="FindMy Flipper",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="findmy_main",
+    requires=["gui"],
+    stack_size=2 * 1024,
+    fap_icon="location_icon.png",
+    fap_icon_assets="icons",
+    fap_category="Bluetooth",
+    fap_author="@MatthewKuKanich",
+    fap_weburl="https://github.com/MatthewKuKanich/FindMyFlipper",
+    fap_version="1.0",
+    fap_description="BLE FindMy Location Beacon",
+)
diff --git a/applications/system/find_my_flipper/findmy.c b/applications/system/find_my_flipper/findmy.c
new file mode 100644
index 0000000000..f5460e1ce6
--- /dev/null
+++ b/applications/system/find_my_flipper/findmy.c
@@ -0,0 +1,167 @@
+#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->storage = furi_record_open(RECORD_STORAGE);
+    app->dialogs = furi_record_open(RECORD_DIALOGS);
+
+    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));
+
+    app->popup = popup_alloc();
+    view_dispatcher_add_view(app->view_dispatcher, FindMyViewPopup, popup_get_view(app->popup));
+
+    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+
+    findmy_state_load(&app->state);
+    findmy_state_apply(&app->state);
+
+    findmy_main_update_active(app->findmy_main, furi_hal_bt_extra_beacon_is_active());
+    findmy_main_update_interval(app->findmy_main, app->state.broadcast_interval);
+    findmy_main_update_type(app->findmy_main, findmy_data_get_type(app->state.data));
+
+    return app;
+}
+
+static void findmy_app_free(FindMy* app) {
+    furi_assert(app);
+
+    view_dispatcher_remove_view(app->view_dispatcher, FindMyViewPopup);
+    popup_free(app->popup);
+
+    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_DIALOGS);
+    furi_record_close(RECORD_STORAGE);
+    furi_record_close(RECORD_GUI);
+
+    free(app);
+}
+
+int32_t findmy_main(void* p) {
+    UNUSED(p);
+    FindMy* app = findmy_app_alloc();
+
+    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->state.broadcast_interval = value;
+    findmy_state_sync_config(&app->state);
+    findmy_state_save(&app->state);
+    findmy_main_update_interval(app->findmy_main, app->state.broadcast_interval);
+    if(furi_hal_bt_extra_beacon_is_active()) {
+        // Always check if beacon is active before changing config
+        furi_check(furi_hal_bt_extra_beacon_stop());
+    }
+    furi_check(furi_hal_bt_extra_beacon_set_config(&app->state.config));
+    if(app->state.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->state.transmit_power = value;
+    findmy_state_sync_config(&app->state);
+    findmy_state_save(&app->state);
+    if(furi_hal_bt_extra_beacon_is_active()) {
+        furi_check(furi_hal_bt_extra_beacon_stop());
+    }
+    furi_check(furi_hal_bt_extra_beacon_set_config(&app->state.config));
+    if(app->state.beacon_active) {
+        furi_check(furi_hal_bt_extra_beacon_start());
+    }
+}
+
+void findmy_toggle_beacon(FindMy* app) {
+    app->state.beacon_active = !app->state.beacon_active;
+    findmy_state_save(&app->state);
+    if(furi_hal_bt_extra_beacon_is_active()) {
+        furi_check(furi_hal_bt_extra_beacon_stop());
+    }
+    if(app->state.beacon_active) {
+        furi_check(furi_hal_bt_extra_beacon_start());
+    }
+    findmy_main_update_active(app->findmy_main, furi_hal_bt_extra_beacon_is_active());
+}
+
+FindMyType findmy_data_get_type(uint8_t data[EXTRA_BEACON_MAX_DATA_SIZE]) {
+    if(data[0] == 0x1E && // Length
+       data[1] == 0xFF && // Manufacturer Specific Data
+       data[2] == 0x4C && // Company ID (Apple, Inc.)
+       data[3] == 0x00 && // ...
+       data[4] == 0x12 && // Type (FindMy)
+       data[5] == 0x19 // Length
+    ) {
+        return FindMyTypeApple;
+    } else {
+        return FindMyTypeSamsung;
+    }
+}
+
+#if FW_ORIGIN_Official
+void furi_hal_bt_reverse_mac_addr(uint8_t mac_addr[GAP_MAC_ADDR_SIZE]) {
+    uint8_t tmp;
+    for(size_t i = 0; i < GAP_MAC_ADDR_SIZE / 2; i++) {
+        tmp = mac_addr[i];
+        mac_addr[i] = mac_addr[GAP_MAC_ADDR_SIZE - 1 - i];
+        mac_addr[GAP_MAC_ADDR_SIZE - 1 - i] = tmp;
+    }
+}
+#endif
\ No newline at end of file
diff --git a/applications/system/find_my_flipper/findmy.h b/applications/system/find_my_flipper/findmy.h
new file mode 100644
index 0000000000..a5135f2d70
--- /dev/null
+++ b/applications/system/find_my_flipper/findmy.h
@@ -0,0 +1,5 @@
+#pragma once
+
+typedef struct FindMy FindMy;
+
+typedef enum FindMyType FindMyType;
\ No newline at end of file
diff --git a/applications/system/find_my_flipper/findmy_i.h b/applications/system/find_my_flipper/findmy_i.h
new file mode 100644
index 0000000000..111c85a479
--- /dev/null
+++ b/applications/system/find_my_flipper/findmy_i.h
@@ -0,0 +1,59 @@
+#pragma once
+
+#include "findmy.h"
+#include "findmy_state.h"
+#include <furi_hal_bt.h>
+#include <extra_beacon.h>
+#include "findmy_icons.h"
+#include <toolbox/stream/file_stream.h>
+#include <toolbox/hex.h>
+#include <toolbox/path.h>
+#include <gui/gui.h>
+#include <storage/storage.h>
+#include <dialogs/dialogs.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 <gui/modules/popup.h>
+#include "scenes/findmy_scene.h"
+#include "helpers/base64.h"
+#if FW_ORIGIN_Official
+void furi_hal_bt_reverse_mac_addr(uint8_t mac_addr[GAP_MAC_ADDR_SIZE]);
+#endif
+
+struct FindMy {
+    Gui* gui;
+    Storage* storage;
+    DialogsApp* dialogs;
+    SceneManager* scene_manager;
+    ViewDispatcher* view_dispatcher;
+
+    FindMyMain* findmy_main;
+    ByteInput* byte_input;
+    VariableItemList* var_item_list;
+    Popup* popup;
+
+    uint8_t mac_buf[EXTRA_BEACON_MAC_ADDR_SIZE];
+    uint8_t packet_buf[EXTRA_BEACON_MAX_DATA_SIZE];
+
+    FindMyState state;
+};
+
+typedef enum {
+    FindMyViewMain,
+    FindMyViewByteInput,
+    FindMyViewVarItemList,
+    FindMyViewPopup,
+} FindMyView;
+
+enum FindMyType {
+    FindMyTypeApple,
+    FindMyTypeSamsung,
+};
+
+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);
+FindMyType findmy_data_get_type(uint8_t data[EXTRA_BEACON_MAX_DATA_SIZE]);
diff --git a/applications/system/find_my_flipper/findmy_startup.c b/applications/system/find_my_flipper/findmy_startup.c
new file mode 100644
index 0000000000..c24eaa7ce1
--- /dev/null
+++ b/applications/system/find_my_flipper/findmy_startup.c
@@ -0,0 +1,11 @@
+#include "findmy_state.h"
+#include <furi_hal.h>
+
+void findmy_startup() {
+    if(!furi_hal_is_normal_boot()) return;
+
+    FindMyState state;
+    if(findmy_state_load(&state)) {
+        findmy_state_apply(&state);
+    }
+}
diff --git a/applications/system/find_my_flipper/findmy_state.c b/applications/system/find_my_flipper/findmy_state.c
new file mode 100644
index 0000000000..12e8cbed21
--- /dev/null
+++ b/applications/system/find_my_flipper/findmy_state.c
@@ -0,0 +1,130 @@
+#include "findmy_state.h"
+
+#include <string.h>
+#include <stddef.h>
+#include <furi_hal_bt.h>
+#include <flipper_format/flipper_format.h>
+
+bool findmy_state_load(FindMyState* out_state) {
+    FindMyState state;
+
+    // Try to load from file
+    bool loaded_from_file = false;
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    if(storage_file_exists(storage, FINDMY_STATE_PATH)) {
+        FlipperFormat* file = flipper_format_file_alloc(storage);
+        do {
+            uint32_t tmp;
+            FuriString* str = furi_string_alloc();
+            if(!flipper_format_file_open_existing(file, FINDMY_STATE_PATH)) break;
+            if(!flipper_format_read_header(file, str, &tmp)) break;
+            if(furi_string_cmp_str(str, FINDMY_STATE_HEADER)) break;
+            if(tmp != FINDMY_STATE_VER) break;
+
+            if(!flipper_format_read_bool(file, "beacon_active", &state.beacon_active, 1)) break;
+
+            if(!flipper_format_read_uint32(file, "broadcast_interval", &tmp, 1)) break;
+            state.broadcast_interval = tmp;
+
+            if(!flipper_format_read_uint32(file, "transmit_power", &tmp, 1)) break;
+            state.transmit_power = tmp;
+
+            if(!flipper_format_read_hex(file, "mac", state.mac, sizeof(state.mac))) break;
+
+            if(!flipper_format_read_hex(file, "data", state.data, sizeof(state.data))) break;
+
+            loaded_from_file = true;
+        } while(0);
+        flipper_format_free(file);
+    }
+    furi_record_close(RECORD_STORAGE);
+
+    // Otherwise set default values
+    if(!loaded_from_file) {
+        state.beacon_active = false;
+        state.broadcast_interval = 5;
+        state.transmit_power = 6;
+
+        // Set default mac
+        uint8_t default_mac[EXTRA_BEACON_MAC_ADDR_SIZE] = {0x66, 0x55, 0x44, 0x33, 0x22, 0x11};
+        memcpy(state.mac, default_mac, sizeof(state.mac));
+
+        // Set default empty AirTag data
+        uint8_t* data = state.data;
+        *data++ = 0x1E; // Length
+        *data++ = 0xFF; // Manufacturer Specific Data
+        *data++ = 0x4C; // Company ID (Apple, Inc.)
+        *data++ = 0x00; // ...
+        *data++ = 0x12; // Type (FindMy)
+        *data++ = 0x19; // Length
+        *data++ = 0x00; // Status
+        // Placeholder Empty Public Key without the MAC address
+        for(size_t i = 0; i < 22; ++i) {
+            *data++ = 0x00;
+        }
+        *data++ = 0x00; // First 2 bits are the version, the rest is the battery level
+        *data++ = 0x00; // Hint (0x00)
+    }
+
+    // Sync values to config
+    findmy_state_sync_config(&state);
+
+    // Set constants
+    state.config.adv_channel_map = GapAdvChannelMapAll;
+    state.config.address_type = GapAddressTypePublic;
+
+    // Copy to caller state before popping stack
+    memcpy(out_state, &state, sizeof(state));
+
+    // Return if active, can be used to start after loading in an if statement
+    return state.beacon_active;
+}
+
+void findmy_state_apply(FindMyState* state) {
+    // Stop any running beacon
+    if(furi_hal_bt_extra_beacon_is_active()) {
+        furi_check(furi_hal_bt_extra_beacon_stop());
+    }
+
+    furi_check(furi_hal_bt_extra_beacon_set_config(&state->config));
+
+    furi_check(furi_hal_bt_extra_beacon_set_data(state->data, sizeof(state->data)));
+
+    if(state->beacon_active) {
+        furi_check(furi_hal_bt_extra_beacon_start());
+    }
+}
+
+void findmy_state_sync_config(FindMyState* state) {
+    state->config.min_adv_interval_ms = state->broadcast_interval * 1000; // Converting s to ms
+    state->config.max_adv_interval_ms = (state->broadcast_interval * 1000) + 150;
+    state->config.adv_power_level = GapAdvPowerLevel_0dBm + state->transmit_power;
+    memcpy(state->config.address, state->mac, sizeof(state->config.address));
+}
+
+void findmy_state_save(FindMyState* state) {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    storage_simply_mkdir(storage, FINDMY_STATE_DIR);
+    FlipperFormat* file = flipper_format_file_alloc(storage);
+
+    do {
+        uint32_t tmp;
+        if(!flipper_format_file_open_always(file, FINDMY_STATE_PATH)) break;
+        if(!flipper_format_write_header_cstr(file, FINDMY_STATE_HEADER, FINDMY_STATE_VER)) break;
+
+        if(!flipper_format_write_bool(file, "beacon_active", &state->beacon_active, 1)) break;
+
+        tmp = state->broadcast_interval;
+        if(!flipper_format_write_uint32(file, "broadcast_interval", &tmp, 1)) break;
+
+        tmp = state->transmit_power;
+        if(!flipper_format_write_uint32(file, "transmit_power", &tmp, 1)) break;
+
+        if(!flipper_format_write_hex(file, "mac", state->mac, sizeof(state->mac))) break;
+
+        if(!flipper_format_write_hex(file, "data", state->data, sizeof(state->data))) break;
+    } while(0);
+
+    flipper_format_free(file);
+    furi_record_close(RECORD_STORAGE);
+}
diff --git a/applications/system/find_my_flipper/findmy_state.h b/applications/system/find_my_flipper/findmy_state.h
new file mode 100644
index 0000000000..d11313e588
--- /dev/null
+++ b/applications/system/find_my_flipper/findmy_state.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include <extra_beacon.h>
+
+#define FINDMY_STATE_HEADER "FindMy Flipper State"
+#define FINDMY_STATE_VER 1
+#define FINDMY_STATE_DIR EXT_PATH("apps_data/findmy")
+#define FINDMY_STATE_PATH FINDMY_STATE_DIR "/findmy_state.txt"
+
+typedef struct {
+    bool beacon_active;
+    uint8_t broadcast_interval;
+    uint8_t transmit_power;
+
+    uint8_t mac[EXTRA_BEACON_MAC_ADDR_SIZE];
+    uint8_t data[EXTRA_BEACON_MAX_DATA_SIZE];
+
+    // Generated from the other state values
+    GapExtraBeaconConfig config;
+} FindMyState;
+
+bool findmy_state_load(FindMyState* out_state);
+
+void findmy_state_apply(FindMyState* state);
+
+void findmy_state_sync_config(FindMyState* state);
+
+void findmy_state_save(FindMyState* state);
diff --git a/applications/system/find_my_flipper/helpers/base64.c b/applications/system/find_my_flipper/helpers/base64.c
new file mode 100644
index 0000000000..f1fb718706
--- /dev/null
+++ b/applications/system/find_my_flipper/helpers/base64.c
@@ -0,0 +1,141 @@
+/*
+ * Base64 encoding/decoding (RFC1341)
+ * Copyright (c) 2005-2011, Jouni Malinen <j@w1.fi>
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+// https://web.mit.edu/freebsd/head/contrib/wpa/src/utils/base64.c
+
+#include "base64.h"
+
+#define os_malloc malloc
+#define os_free free
+#define os_memset memset
+
+static const unsigned char base64_table[65] =
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+/**
+ * base64_encode - Base64 encode
+ * @src: Data to be encoded
+ * @len: Length of the data to be encoded
+ * @out_len: Pointer to output length variable, or %NULL if not used
+ * Returns: Allocated buffer of out_len bytes of encoded data,
+ * or %NULL on failure
+ *
+ * Caller is responsible for freeing the returned buffer. Returned buffer is
+ * nul terminated to make it easier to use as a C string. The nul terminator is
+ * not included in out_len.
+ */
+unsigned char* base64_encode(const unsigned char* src, size_t len, size_t* out_len) {
+    unsigned char *out, *pos;
+    const unsigned char *end, *in;
+    size_t olen;
+    int line_len;
+
+    olen = len * 4 / 3 + 4; /* 3-byte blocks to 4-byte */
+    olen += olen / 72; /* line feeds */
+    olen++; /* nul termination */
+    if(olen < len) return NULL; /* integer overflow */
+    out = os_malloc(olen);
+    if(out == NULL) return NULL;
+
+    end = src + len;
+    in = src;
+    pos = out;
+    line_len = 0;
+    while(end - in >= 3) {
+        *pos++ = base64_table[in[0] >> 2];
+        *pos++ = base64_table[((in[0] & 0x03) << 4) | (in[1] >> 4)];
+        *pos++ = base64_table[((in[1] & 0x0f) << 2) | (in[2] >> 6)];
+        *pos++ = base64_table[in[2] & 0x3f];
+        in += 3;
+        line_len += 4;
+        if(line_len >= 72) {
+            *pos++ = '\n';
+            line_len = 0;
+        }
+    }
+
+    if(end - in) {
+        *pos++ = base64_table[in[0] >> 2];
+        if(end - in == 1) {
+            *pos++ = base64_table[(in[0] & 0x03) << 4];
+            *pos++ = '=';
+        } else {
+            *pos++ = base64_table[((in[0] & 0x03) << 4) | (in[1] >> 4)];
+            *pos++ = base64_table[(in[1] & 0x0f) << 2];
+        }
+        *pos++ = '=';
+        line_len += 4;
+    }
+
+    if(line_len) *pos++ = '\n';
+
+    *pos = '\0';
+    if(out_len) *out_len = pos - out;
+    return out;
+}
+
+/**
+ * base64_decode - Base64 decode
+ * @src: Data to be decoded
+ * @len: Length of the data to be decoded
+ * @out_len: Pointer to output length variable
+ * Returns: Allocated buffer of out_len bytes of decoded data,
+ * or %NULL on failure
+ *
+ * Caller is responsible for freeing the returned buffer.
+ */
+unsigned char* base64_decode(const unsigned char* src, size_t len, size_t* out_len) {
+    unsigned char dtable[256], *out, *pos, block[4], tmp;
+    size_t i, count, olen;
+    int pad = 0;
+
+    os_memset(dtable, 0x80, 256);
+    for(i = 0; i < sizeof(base64_table) - 1; i++) dtable[base64_table[i]] = (unsigned char)i;
+    dtable['='] = 0;
+
+    count = 0;
+    for(i = 0; i < len; i++) {
+        if(dtable[src[i]] != 0x80) count++;
+    }
+
+    if(count == 0 || count % 4) return NULL;
+
+    olen = count / 4 * 3;
+    pos = out = os_malloc(olen);
+    if(out == NULL) return NULL;
+
+    count = 0;
+    for(i = 0; i < len; i++) {
+        tmp = dtable[src[i]];
+        if(tmp == 0x80) continue;
+
+        if(src[i] == '=') pad++;
+        block[count] = tmp;
+        count++;
+        if(count == 4) {
+            *pos++ = (block[0] << 2) | (block[1] >> 4);
+            *pos++ = (block[1] << 4) | (block[2] >> 2);
+            *pos++ = (block[2] << 6) | block[3];
+            count = 0;
+            if(pad) {
+                if(pad == 1)
+                    pos--;
+                else if(pad == 2)
+                    pos -= 2;
+                else {
+                    /* Invalid padding */
+                    os_free(out);
+                    return NULL;
+                }
+                break;
+            }
+        }
+    }
+
+    *out_len = pos - out;
+    return out;
+}
diff --git a/applications/system/find_my_flipper/helpers/base64.h b/applications/system/find_my_flipper/helpers/base64.h
new file mode 100644
index 0000000000..333f82dc1c
--- /dev/null
+++ b/applications/system/find_my_flipper/helpers/base64.h
@@ -0,0 +1,21 @@
+/*
+ * Base64 encoding/decoding (RFC1341)
+ * Copyright (c) 2005, Jouni Malinen <j@w1.fi>
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+// https://web.mit.edu/freebsd/head/contrib/wpa/src/utils/base64.h
+
+#ifndef BASE64_H
+#define BASE64_H
+
+#include <stdint.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+
+unsigned char* base64_encode(const unsigned char* src, size_t len, size_t* out_len);
+unsigned char* base64_decode(const unsigned char* src, size_t len, size_t* out_len);
+
+#endif /* BASE64_H */
\ No newline at end of file
diff --git a/applications/system/find_my_flipper/icons/DolphinDone_80x58.png b/applications/system/find_my_flipper/icons/DolphinDone_80x58.png
new file mode 100644
index 0000000000000000000000000000000000000000..594d62d5294997399bc3b256fbe7685ce10b146d
GIT binary patch
literal 1664
zcmbVNc~BE)6wheY2!uK<P!ZH^sKqPUBNrQp6bONU91{T}AhOwPU?s`M%|Z^98IQqp
z6smY2TBV8{;)zy^#aggT9qJUp3vFxVw5@>FLY*phgJAoI;~(9b-S2(h_kQpF-Zi@^
zF+PgtHqDL0;qcVaN)5Xvvag&wj{R2ntG{IzUnWw^ETRmI4WkK8n4Z!RfZBv*5gG#1
zJ63#0gm5_H9b}T0(Z(&5<CIB=xpahflZ8cdI3b~S3x?+q44@;jNV9_9Uw4TQka`7w
zu_O+PvqTWtWVDke7CGaSaAyvV>iMAyfDpT!HDDqb46vJwW~<z;;E&jqv$E?L<O3rr
zOpb#8ij+1k5s08@0zieZ0EZwLkg<|jiUgskOeUKTAP^z~At{JR1c*p3k;!2(F#7OW
zkF?$(*C<t^{@9&@pUp59ISATpHla-<r06UVMo|=m5D-BGtcJkqFf*84V73N~Sx^#I
zoF**{NtpqcMNCKKG73J+_Ei`rOI+M*!)EJfve>kNcFY38LI^aOT(OO4TNw@UFO4^9
zTaz3X0@M&zDwoFDnivAcz-<2B?#QLcvXLjyBwHBFsHE^*6Jci5N(G<25$Z|3oFF79
zt{0&K1d|yAVyOrc=nxShkm0BVg>_OwC(@1Cc@tix3X)1BkVqB;1;KD+Br265;Soxe
zN-Rdg!lmdKR&BO2m>DO=e3Pv2Q7rOStUQ7yFovR&D9Sk235nShLs_#a3xG(35HLFq
z!%4I2WR9y!uYy(*G?_=}RWxM+M$#-N-#`HpAu<?)B?1&ebOJFVh6O>80Tmd;G97`!
zdI?Mr{87CA|E3RQNrA3j`A_eR9kC7R5?@aPyLmlNgqa;8nw^%VblFu7XWV|ZGAzm7
z-ns8C-3V|CWLx`RUVV5?e=~JLXGNd*gA2VSy}M^liFW=t^rC%5`mI^MKJ^z*yC+9$
zK8y8opgw0i#OZbK-@bIOr+sL3X*0ny?F_5$?^u;L5LsJ%K{s=fe|&Jv>M52PH5+{E
zciwC+?Q1wXec-Dn?aE=B+ip?UBV`Bm*T(s!wWZ~plEuod;*9$(j?$d6^(EcKbW!X<
z&nIP6%$5Z)#Sc{zYqC;eHfJr#r`xJt{34$f<9D)}sVN9J6SzfMQhTpr?kB2_2ge#4
zPMiznH%5_<QYtmU<8SyK;iYwX1#kbu=2l~SoN3rT9Xx#h*}jAbkEyGWi__QiMf5K7
z<fXRHzEW75mK43q^ZB}{TY{D!?l?4E#c)`4>{H{8XZyCE8Z^qt3(nNamZCKd_dTt1
zybD4vFW|RcM-rOAUY>VP`H4R@hUX>mS_}#FURP5QO}9;kzD&7Mf0X!18#*Cy#s=lb
zo<R-PJUCVhY#Pv3hV_-T_H3S*abT4^bE)Fq-bvFMj1v?kr&Nv<z2o+;>G<iYwVylp
z&kNul?7k{)oRd`WY}nuZy2tVz{odxNf!LyBPli_-C%1W7eMKwPcPzi>CF8teZgFA$
z+RoI3!Q}^!<oRfg<jy0?vr!q}ySqI-PzZMo;%B}IZ{?BiwRvsaoRK-Zx2|x;Z|gt2
zU6Y*P<&*c(qUw;qyt`fgCciyfs}x^2HJ7?EKf|F1H?Le27gaaCyX{4D<nW=~rswJ0
z*5)PjpjX=5%gN>2oJhss<8vOJLqi_j*V;@D?$qOzS!qAdI~fh>f!$Rlk&;`Vf`4<%
z;twaDuf~1bzjhOXb@_XjZ49oi7<idnG%vlZw5eQs&ar*h`JSBiQ}GK=547~>zcZBs
XY(V!OcIS4x{t4>Hc;)f%%(edjvT}hx

literal 0
HcmV?d00001

diff --git a/applications/system/find_my_flipper/icons/Lock_7x8.png b/applications/system/find_my_flipper/icons/Lock_7x8.png
new file mode 100644
index 0000000000000000000000000000000000000000..f7c9ca2c702f1b93d7d06bd12ae708655c79d7c8
GIT binary patch
literal 3597
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
a9|QyVQ~@!E@+ey_4zMz}H7hmoyzn0`d`!~-

literal 0
HcmV?d00001

diff --git a/applications/system/find_my_flipper/icons/Ok_btn_9x9.png b/applications/system/find_my_flipper/icons/Ok_btn_9x9.png
new file mode 100644
index 0000000000000000000000000000000000000000..9a1539da2049f12f7b25f96b11a9c40cd8227302
GIT binary patch
literal 3605
zcmaJ@c{r5q+kR|?vSeS9G2*Q(Gqz$f_GQ#q8r!JE7=ytqjlqnNNGaK}Wlbolp-q`&
zs|bxHiiEP0&{#s&zVZIv-rx7f*Y_O9^W67+-RF5;*L_{ra~$^-2RmyaK{-JH0EBE1
z7AVdru>JD$aK0bym%#uaXpT2Gcd#)x2azcxAABGV0BC)Aj-lw(6)B^^6`Y8RS?}DV
z%)ko(See1!Eb3M$dL6)A6csaRjExg?k&xVzi*Rm;?iNJk#f=mkVEUR~jXN3dd|Lmz
z;y}sMh%ol-?E1&`>dD;6jdps6NYoxN)s%@sf4~40YY6LAOtMEbwA4g#OCpANL823^
zSH66W05Hcxr$tg98gFntAOYL}xm$C;Skv&Ym?{TVR{)d(41vWacX1`7fM!jnW(lBK
z26*WB#9I(Z1Ast!xEUC@Cj`v=urcBTdP`FWq=DYTy`}s>0vC{Vz<NKo$qT^K`2tFv
zyZ#kWz%T)8^46DgMY%oGw&sqi;R8Yv(a9nvo<LNVnR+V#(&q-u_ldXhfRH=|(5||n
zQV()06Tt=4v(no#k9X-vFw@`3g)YnujVO)03VdXSkv=HuBQOTe&CF32M@;e{+jj$i
z+-m2`Eq&wU$%(Py37^S8_K(KGt$;Za*hp!9aI5+nH!mP+*TL@d7$5K9AvbY746X|(
zJq7f+0Dgn3?mGPf1t$gHg{{WZ^*2oR<==8>HdNRvxNFy}ir1|g=xDsrFP&l1P<-Sv
zXLqYVYz{b^ZIV@1Ulg->7DEgvM<q_Oc+IqkE0fAwO|j=U6AyNd=sr5FE_rnxeTC@&
zwr!$6N2OROwWN=TO`X@CSIpQHNDUPN7?<qp?Vjy?$_lKH{LaN4FO2RA0ZbwnHLl6+
zN&!TDb+zd$0FYX@nq;EJ1+--rj{-pTSDDjy)1;Aa_yNG8;KHGYraO1Plwi~g=e|^^
zu9xP<?65G|y}#a2)NIFC)Ly|`G8v|_PW7-}-lq?F<O=p=4~tg3)Y2E491=TyVVW$x
z!&4^zl}z;|0bbK)esPQvEN+iiVlTLzUpgz17mPvZUXLmUO~px^05>*Min&Y8{8QW!
z$_pA434?^wCTq$4%^>Zo8&|8XwbCv;KEd;WJJ{s;T}8R8Zwi7ssk$QWQ5l5+opKfX
z;8D*COFEB#4W<GSR}-;Oom#>^*FIrRU%PDSc?B(}+9ZV?N9(yH>0uSnM?xg!>+>;e
z{{7tXQQ|ZFXD*7q3XD!pwnih-=66+Qlq<xl_+15JE^58{3!Hk_zCtBTY_);r1$&f?
zwqmG$SK6nPuX5%uVR4UC#D3wH>tl9;N-D|PHoI&B5d8>^V#i{mE>V0gQgu3+(DG%B
z|8W!pl$lbQERt-0eZA%NSfvE4F>VAYP`DpeoF;Zm4`)2id;6xgSysWl6K$pWANcRZ
z!ET<kAryo-KpZcZit7evtF+}vnX`&Din`xr<9+cfevm0@mB8nieU_Q;GP?r4?wp#O
ziun<`Tj-KdOcTQEfNX&5IoT4KN?Ax2@($no%&cZ7=tph&EZ-X<H%QLtL(7N4B~#vt
zrNh!)(j(I+?>RXKIU9G=@9lEB?<{ivj7!8FE9WN;qoo2Lr0#c@DmcF=JzU<73PmM3
zbe!-gs`c26Uc(AKz7%U!a0yZ5gsprdo1i51MjJPeHtV6d@Jy=*+_3dJ^>}p#8N#<N
z9|AdtbTkjT=fm(x@yVMw+r}mQ=F!lc!Kd6~?Mm%J?dLRjG>kPK_4t?hltq>u=?m+t
z?em(Y%u3Bp_pyV?c_w-4c}p+?Y$aHr>TuPGs@SUj;Er!b@3GVLDS@T8OTts1JFS-p
zKZ=&5zp;DRor*`Gy8MTeWdpVJv2(4-*slRM@XXG+i^F&Ku>7i08vKenZHoS4s(!!h
zJE}*MHu7PR_IfdNzu*P}3^87K?f&A1;>NMsgKcR6**;aB74NC7t<CBKj9x~|Ork+|
zZa_Y!_EUBLMnddu%iP9e@rUD^GWs&YS$r9`87qt%E=DeNmnD~v44a<Q)9TZN9@jtz
zdDooXIj|hov%6(EWep)Ygh7cBiKK&F2bUa)eOfacGlDbOGdpGtmNCmJ%Y@|)h1R`d
z3Oz@uN5d)9;MR~pa`klau<q;9hSJwluD<fVw}SE(^`!3{PiBc_hGa@R5=yR?5O0RD
z(#Pe#S9Lvozf_d-B4?oN6XC<>R(NB?{dHT-9QhXa*KoG!kGU1}$l2D>ypo)fSBuG$
zkTW4?+|I1m?6ZH8tD4^fB{cUpoEoZOo%4hl!EtNtQ#?j*jJR)x-Mn0TrxrX2uT_rh
ziOh=Jxsktqbd9x{^s{c5z92Pk$LGoQl53o+=7QXXCp-Z>io998w|DCCCGfr20oiRN
zX|`KH$W4)wN~)J$kYB~>4EU;NcS^qH&yzeUzXokpMegg_lX$6ve^4}%bY~Sg)%uJ-
zZpb<?j;V;3&5~Y?kT`fq`<~{v6BUlsv(y#!9>$p4x^GS5d{XJP=STbfpHV`58UBH&
zK<rEoGm3eSd4YEMr{#R67*%|~!0eT=hTaDFmC+#X$-r!#+_7w@3@WyKe4y~T^u$y<
zD?ilGP0uYkTtD`PSmtBn@feVq14PDtyo6}0Aii)hC)VDwwlJaa#G^{)F~jQ<c`oV8
z`k#+Rn6LR=vbPF;qa$FCQlqEHUVpB<XzOCp^M%pz>Fg&BgS6bV+#-|^KBGeIBee2B
zrM-`uTB^_(eS+{-KK1h3l`-Yjpv8X4z*uBwQ3a~pL0Ae2xvNGyC3A|#MARToe$W~8
z+4{DsyenENye9df1M}gNUM9_Leh6G=`9exL-cdSKQ_CGy<xRuSbw#lJSdTL{9Y)Y7
z=LLWIz(OJ_rt9`?bG2w!-SpD578bGm$I0hdNld!8GuuD2`;^Q1L(=IU3^L<x`Ok+|
z%!v_KJCE-0B|4$6@5`~ux3klT>EdZ3W5uoR!Lb^D)9!bd=7h@R=M%=|JqX9XP;Z6#
zFD15Bw7qTP(ZlG?o@#x@=wG;XxM(>n@4P$9WwY#lW$h=`zMi_zq30HbV-zHheqpE0
zR6kXtxdzl&Ml2D#zDIvflJk<Us+)-X%(M=*Z_H0Rcl+GVWlQ$7Ka~_X@Sv{lu>b*e
zIAI?GMjp?JBK76WW`{l{pFAY|%5?nYUxRnT&y6~Kz19AD;C0(z*7?dM{%HhVtqWEc
z%+M$z6u@uQu)kg_%2PO_U|n1JE0V1>iVbekOLEOG$U6X^Umc519WC)L$<MPhhJVN!
zpsye|vI5*sxYziacjOyOQ@<lCJyu#4zf2~%&sCFufLFZwRVt5{!OP*+k>t%`#Di0$
zY1|5H*440_`onhmXeayq`8EIg?x2r9KWe()q}Q<D%*a;~RX^`#s#Gog3|XF)4i)^4
z9B>ayqCMEC?c4meb4}#i`HHPaxO&3SPtSVKj@ND?Y+-@R`CDnf-d`T>vTn8RR<=@3
zNXk=Gloyh#S@3R89WHrXBHr;f(&ZO@I_Uo7;O5Bs@ecGx@7%7{_>Q`Adg&sCeZTYp
ztVy{^vAUfOpTDzF*4`h%X0odWn`#uZ4s4igIV^UrVVg?c*{>K)hHq^^RxU2CM;WN>
z;oK@^sg`J}BguyvilN{DQ*V+N4rD{X_~KAFj5qyk3(gP#cvSIDXe!zk3B!^InwV{j
zCXGPmumQl(m`28618`K37tR+?goD{H>cAkpHyrG$XA89@o8$cOh%gGyG0e^h8y0{y
z@CF+jfedLdjsO8i#eispKw=P#1_%GG3**eU%@8o?ZwNI24*pM2Xj=!6If;S;9nsX%
zz(S!=&=CVoZ;TfP>*b{m(uQhlL7=)2EnN*L6sBVU)71t2^ME<-DBeCWl!etl&NwSL
z*pEsj!yu5*&``}#9ZeF&7oufgU;u$?L$tLuI0%g(I+2Q@X%K^ye=Atvg0K`knTjV7
zLEDNLFH$fS4(5dVpED51|H=}B{>c+3V-OmK4AIhrZlCEl(AM_<hY|??qJyX?+<)W!
zKZS#w=wuuOg$p7DQ?Q)M`6zF@BE!rnI1H6UaUzidf2YyGk3=N}`H{#VZ5>T0=zuK-
zizjYd4*pHCwT0ObgQyrH7H4At2XjO;@px~TsgAA%R9|05PuEIcOUu&SOwUTs^00xK
zshI`T;)sF%Z>|Li8%)3vslU12|K;lbk-Oav1Tx371&)Fb!FgLzNCeQ|r-tGG9E;W;
z_5R^{|2Y=zKXM_QU?AJI{a>~IZQ?Z0_VnM@<t+YfejJh0@f1#LKd@@(;rtdm9FR_y
zoblIHpf7ckbI%6$+gO-7(fYhRy}bc05Z^@*P%K*?Ey@j!kLObs4I7@|;*|k*lmjY`
SQ;S_3JYZvKXHjnE8T~)%EKoN9

literal 0
HcmV?d00001

diff --git a/applications/system/find_my_flipper/icons/WarningDolphinFlip_45x42.png b/applications/system/find_my_flipper/icons/WarningDolphinFlip_45x42.png
new file mode 100644
index 0000000000000000000000000000000000000000..2ba54afce0249303787fb3d94a78cc167a81a253
GIT binary patch
literal 1437
zcmeAS@N?(olHy`uVBq!ia0vp^x<IVO!3HGN<u54#DaPU;cPEB*=VV?2IV|apzK#qG
z8~eHcB(eheoCO|{#S9F5hd`K7RKu$QC@5Lt8c`CQpH@<ySd_}(n3A8As^FQMn4TJx
znwU~qcrw+7fq_{gGbExU!q>+tIX_n~5u`@1BDVmjn}NZ`zM>#8IXksPAt^OIGtXA(
z{qFrr3YjUkO5vuy2EGN(sTr9bRYj@6RemAKRoTgwDN6Qs3N{s1Km&49OA-|-a&z*E
zttxDlz~)*3*&tzkB?YjOl5ATgh@&EW0~DO|i&7QL^$c~B4Gatv%q{g&Qxc7mjMEa6
zbrg&Yj12V+fyi9f(A>(%*vimS0Sc6W78a$XSp~VcL9GMwY?U%fN(!v>^~=l4^~#O)
z@{7{-4J|D#^$m>ljf`}GDs+o0^GXscbn}XpVJ5hw7AF^F7L;V>=P7_pOiaozEwNPs
zIu_!K+yY-;xWReF(69oAntnxMfxe-hfqrf-$ZKHL#U(+h2xnkbT^v$bkg6Y)TAW{6
zlnjiLG-a4(VDRC$2&53`8Y`Fl<kH;Kyb@0rTP2`~UP@+)m6@5Dg{h0Pi@Axpi=m;b
zv!k1-qm!9|iK&^Po2jvZ2~4kNUU5lcUUDi-Zze*oGhV$`&PAz-CHX}m`T04p2n@)`
zFUc>?$S+VZGS)Lx(C|%6&ddXeXo5l)>e$qx%(B!Jx1#)91#s|KWnyuHfvcmlo299R
znSrq(*!kwBrk1WoW~Q!gE(Qk1mP$~)DOkJ?)oY1UuRhQ*`k=T)iffn<Fad#>@Wcz`
zz>|M!9x%-p0TcJDoOQE-Iq{~ai(^QH`_*ZU>zWmKTucA|KmWe+?mErbXs(XSlV`YU
zyj{3y=hyt?pBvV_R?6ew^D+M8uNu3qtmT_y-D0L)TyCc0dsA%UJkv=5Rzh;RJXOUd
zb2IbOTqF$GpKcbJdrqLVzGd1T*X2+9*4{lY#C9V4>K2V9OE2i`>{wIr%jf)?BJJZd
zuUTJdQogkH&a9}lvA4LA6npx)qIWK?x%%_%t!Ix}6uLep6h2dsS$*~Ev(@QR+>tKg
z{Rx5VCNfp6n;I+CU0M0%6tiXxoBrH_wi@r8w!N3b)U#YQ?77X3J+inwuOh!a;>s!M
zv#A>UI^rh96)w2m$Hn}af3o_`pcBH`g5Q$P&bQHDxm}W5pZ!?$r-&Js9`F?M>z7Y^
zY$~^~+n{bs1@D!OSqrT8+&A~yr*>6)e(dx~*IxLhbfzrnP<P$6B>jRGZ^@Z|QqP|R
zTf{vU*uQ$uw_jB|PcZzAILGiaHBNU)cG13w&3AXCSuJ$kaL2{#H7|p$nykgUeK8Iy
zEju&pn3Sd&9GD@pm18L<>(i^uuF=;{N93ug){1WBn;h+#e&AT0TJLc_#>&d$ZvF9E
zzJWrXr!3AsoxANwWh&>=#qMTJV%skmUV2;@?^2MR`M%R;)nyx{?bH9g`?l#1bH%|O
Uzg^}hzX270p00i_>zopr0LnWHWB>pF

literal 0
HcmV?d00001

diff --git a/applications/system/find_my_flipper/icons/text_10px.png b/applications/system/find_my_flipper/icons/text_10px.png
new file mode 100644
index 0000000000000000000000000000000000000000..8e8a6183dd50535729dc9c9b4f220a12dd4c600f
GIT binary patch
literal 158
zcmeAS@N?(olHy`uVBq!ia0vp^AT}2VGmzZ%#=aj&u?6^qxB}__|Nk$&IsYz@rRC}3
z7*a7OIiZ2U&CSi=;0cBn1vTatM&Z;3u7g(^G9`qQn09G2aWeNXaKC0S=Q~tg57Z@F
z;u=vBoS#-wo>-L1;E+?AmspUPnOCA;ke9BToS%}K{MA`f4ycg9)78&qol`;+00Iau
A9smFU

literal 0
HcmV?d00001

diff --git a/applications/system/find_my_flipper/location_icon.png b/applications/system/find_my_flipper/location_icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..841787a2a6131015634e906ff1cd4f189e0855aa
GIT binary patch
literal 6750
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

literal 0
HcmV?d00001

diff --git a/applications/system/find_my_flipper/scenes/findmy_scene.c b/applications/system/find_my_flipper/scenes/findmy_scene.c
new file mode 100644
index 0000000000..dca1adbb4f
--- /dev/null
+++ b/applications/system/find_my_flipper/scenes/findmy_scene.c
@@ -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,
+};
\ No newline at end of file
diff --git a/applications/system/find_my_flipper/scenes/findmy_scene.h b/applications/system/find_my_flipper/scenes/findmy_scene.h
new file mode 100644
index 0000000000..993b9d0542
--- /dev/null
+++ b/applications/system/find_my_flipper/scenes/findmy_scene.h
@@ -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
\ No newline at end of file
diff --git a/applications/system/find_my_flipper/scenes/findmy_scene_config.c b/applications/system/find_my_flipper/scenes/findmy_scene_config.c
new file mode 100644
index 0000000000..eba1a85a2b
--- /dev/null
+++ b/applications/system/find_my_flipper/scenes/findmy_scene_config.c
@@ -0,0 +1,110 @@
+#include "../findmy_i.h"
+
+enum VarItemListIndex {
+    VarItemListIndexBroadcastInterval,
+    VarItemListIndexTransmitPower,
+    VarItemListIndexImportTagFromFile,
+    VarItemListIndexRegisterTagManually,
+    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->state.broadcast_interval);
+    variable_item_set_current_value_text(item, str);
+    variable_item_set_current_value_index(item, app->state.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->state.transmit_power);
+    variable_item_set_current_value_text(item, str);
+    variable_item_set_current_value_index(item, app->state.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->state.broadcast_interval - 1);
+    char interval_str[5];
+    snprintf(interval_str, sizeof(interval_str), "%ds", app->state.broadcast_interval);
+    variable_item_set_current_value_text(item, interval_str);
+
+    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->state.transmit_power);
+    char power_str[7];
+    snprintf(power_str, sizeof(power_str), "%ddBm", app->state.transmit_power);
+    variable_item_set_current_value_text(item, power_str);
+
+    item = variable_item_list_add(var_item_list, "Import Tag From File", 0, NULL, NULL);
+
+    item = variable_item_list_add(var_item_list, "Register Tag Manually", 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 VarItemListIndexImportTagFromFile:
+            scene_manager_next_scene(app->scene_manager, FindMySceneConfigImport);
+            break;
+        case VarItemListIndexRegisterTagManually:
+            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);
+}
\ No newline at end of file
diff --git a/applications/system/find_my_flipper/scenes/findmy_scene_config_import.c b/applications/system/find_my_flipper/scenes/findmy_scene_config_import.c
new file mode 100644
index 0000000000..aedd210668
--- /dev/null
+++ b/applications/system/find_my_flipper/scenes/findmy_scene_config_import.c
@@ -0,0 +1,220 @@
+#include "../findmy_i.h"
+
+enum VarItemListIndex {
+    VarItemListIndexNrfConnect,
+    VarItemListIndexOpenHaystack,
+};
+
+static const char* parse_nrf_connect(FindMy* app, const char* path) {
+    const char* error = NULL;
+
+    Stream* stream = file_stream_alloc(app->storage);
+    FuriString* line = furi_string_alloc();
+    do {
+        // XX-XX-XX-XX-XX-XX_YYYY-MM-DD HH_MM_SS.txt
+        error = "Filename must\nhave MAC\naddress";
+        uint8_t mac[EXTRA_BEACON_MAC_ADDR_SIZE];
+        path_extract_filename_no_ext(path, line);
+        if(furi_string_size(line) < sizeof(mac) * 3 - 1) break;
+        error = NULL;
+        for(size_t i = 0; i < sizeof(mac); i++) {
+            char a = furi_string_get_char(line, i * 3);
+            char b = furi_string_get_char(line, i * 3 + 1);
+            if((a < 'A' && a > 'F') || (a < '0' && a > '9') || (b < 'A' && b > 'F') ||
+               (b < '0' && b > '9') || !hex_char_to_uint8(a, b, &mac[i])) {
+                error = "Filename must\nhave MAC\naddress";
+                break;
+            }
+        }
+        if(error) break;
+        furi_hal_bt_reverse_mac_addr(mac);
+
+        error = "Can't open file";
+        if(!file_stream_open(stream, path, FSAM_READ, FSOM_OPEN_EXISTING)) break;
+
+        // YYYY-MM-DD HH:MM:SS.ms, XX dBm, 0xXXXXX
+        error = "Wrong file format";
+        if(!stream_read_line(stream, line)) break;
+        const char* marker = " dBm, 0x";
+        size_t pos = furi_string_search(line, marker);
+        if(pos == FURI_STRING_FAILURE) break;
+        furi_string_right(line, pos + strlen(marker));
+        furi_string_trim(line);
+
+        error = "Wrong payload size";
+        uint8_t data[EXTRA_BEACON_MAX_DATA_SIZE];
+        if(furi_string_size(line) != sizeof(data) * 2) break;
+        error = NULL;
+        for(size_t i = 0; i < sizeof(data); i++) {
+            char a = furi_string_get_char(line, i * 2);
+            char b = furi_string_get_char(line, i * 2 + 1);
+            if((a < 'A' && a > 'F') || (a < '0' && a > '9') || (b < 'A' && b > 'F') ||
+               (b < '0' && b > '9') || !hex_char_to_uint8(a, b, &data[i])) {
+                error = "Invalid payload";
+                break;
+            }
+        }
+        if(error) break;
+
+        memcpy(app->state.mac, mac, sizeof(app->state.mac));
+        memcpy(app->state.data, data, sizeof(app->state.data));
+        findmy_state_sync_config(&app->state);
+        findmy_state_save(&app->state);
+
+        error = NULL;
+
+    } while(false);
+    furi_string_free(line);
+    file_stream_close(stream);
+    stream_free(stream);
+
+    return error;
+}
+
+static const char* parse_open_haystack(FindMy* app, const char* path) {
+    const char* error = NULL;
+
+    Stream* stream = file_stream_alloc(app->storage);
+    FuriString* line = furi_string_alloc();
+    do {
+        error = "Can't open file";
+        if(!file_stream_open(stream, path, FSAM_READ, FSOM_OPEN_EXISTING)) break;
+
+        error = "Wrong file format";
+        while(stream_read_line(stream, line)) {
+            if(furi_string_start_with(line, "Public key: ") ||
+               furi_string_start_with(line, "Advertisement key: ")) {
+                error = NULL;
+                break;
+            }
+        }
+        if(error) break;
+
+        furi_string_right(line, furi_string_search_char(line, ':') + 2);
+        furi_string_trim(line);
+
+        error = "Base64 failed";
+        size_t decoded_len;
+        uint8_t* public_key = base64_decode(
+            (uint8_t*)furi_string_get_cstr(line), furi_string_size(line), &decoded_len);
+        if(decoded_len != 28) {
+            free(public_key);
+            break;
+        }
+
+        memcpy(app->state.mac, public_key, sizeof(app->state.mac));
+        app->state.mac[0] |= 0b11000000;
+        furi_hal_bt_reverse_mac_addr(app->state.mac);
+
+        uint8_t advertisement_template[EXTRA_BEACON_MAX_DATA_SIZE] = {
+            0x1e, // length (30)
+            0xff, // manufacturer specific data
+            0x4c, 0x00, // company ID (Apple)
+            0x12, 0x19, // offline finding type and length
+            0x00, //state
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, // first two bits of key[0]
+            0x00, // hint
+        };
+        memcpy(app->state.data, advertisement_template, sizeof(app->state.data));
+        memcpy(&app->state.data[7], &public_key[6], decoded_len - 6);
+        app->state.data[29] = public_key[0] >> 6;
+        findmy_state_sync_config(&app->state);
+        findmy_state_save(&app->state);
+
+        free(public_key);
+        error = NULL;
+
+    } while(false);
+    furi_string_free(line);
+    file_stream_close(stream);
+    stream_free(stream);
+
+    return error;
+}
+
+void findmy_scene_config_import_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_import_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, "nRF Connect .txt", 0, NULL, NULL);
+
+    item = variable_item_list_add(var_item_list, "OpenHaystack .keys", 0, NULL, NULL);
+
+    // This scene acts more like a submenu than a var item list tbh
+    UNUSED(item);
+
+    variable_item_list_set_enter_callback(var_item_list, findmy_scene_config_import_callback, app);
+
+    variable_item_list_set_selected_item(
+        var_item_list, scene_manager_get_scene_state(app->scene_manager, FindMySceneConfigImport));
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, FindMyViewVarItemList);
+}
+
+bool findmy_scene_config_import_on_event(void* context, SceneManagerEvent event) {
+    FindMy* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        scene_manager_set_scene_state(app->scene_manager, FindMySceneConfigImport, event.event);
+        consumed = true;
+
+        const char* extension = NULL;
+        switch(event.event) {
+        case VarItemListIndexNrfConnect:
+            extension = ".txt";
+            break;
+        case VarItemListIndexOpenHaystack:
+            extension = ".keys";
+            break;
+        default:
+            break;
+        }
+        if(!extension) {
+            return consumed;
+        }
+
+        const DialogsFileBrowserOptions browser_options = {
+            .extension = extension,
+            .icon = &I_text_10px,
+            .base_path = FINDMY_STATE_DIR,
+        };
+        storage_simply_mkdir(app->storage, browser_options.base_path);
+        FuriString* path = furi_string_alloc_set_str(browser_options.base_path);
+        if(dialog_file_browser_show(app->dialogs, path, path, &browser_options)) {
+            // The parse functions return the error text, or NULL for success
+            // Used in result to show success or error message
+            const char* error = NULL;
+            switch(event.event) {
+            case VarItemListIndexNrfConnect:
+                error = parse_nrf_connect(app, furi_string_get_cstr(path));
+                break;
+            case VarItemListIndexOpenHaystack:
+                error = parse_open_haystack(app, furi_string_get_cstr(path));
+                break;
+            }
+            scene_manager_set_scene_state(
+                app->scene_manager, FindMySceneConfigImportResult, (uint32_t)error);
+            scene_manager_next_scene(app->scene_manager, FindMySceneConfigImportResult);
+        }
+        furi_string_free(path);
+    }
+
+    return consumed;
+}
+
+void findmy_scene_config_import_on_exit(void* context) {
+    FindMy* app = context;
+    VariableItemList* var_item_list = app->var_item_list;
+
+    variable_item_list_reset(var_item_list);
+}
\ No newline at end of file
diff --git a/applications/system/find_my_flipper/scenes/findmy_scene_config_import_result.c b/applications/system/find_my_flipper/scenes/findmy_scene_config_import_result.c
new file mode 100644
index 0000000000..f15f55c069
--- /dev/null
+++ b/applications/system/find_my_flipper/scenes/findmy_scene_config_import_result.c
@@ -0,0 +1,58 @@
+#include "../findmy_i.h"
+
+enum PopupEvent {
+    PopupEventExit,
+};
+
+static void findmy_scene_config_import_result_callback(void* context) {
+    FindMy* app = context;
+
+    view_dispatcher_send_custom_event(app->view_dispatcher, PopupEventExit);
+}
+
+void findmy_scene_config_import_result_on_enter(void* context) {
+    FindMy* app = context;
+    Popup* popup = app->popup;
+
+    const char* error = (const char*)scene_manager_get_scene_state(
+        app->scene_manager, FindMySceneConfigImportResult);
+    if(error) {
+        popup_set_icon(popup, 83, 22, &I_WarningDolphinFlip_45x42);
+        popup_set_header(popup, "Error!", 13, 22, AlignLeft, AlignBottom);
+        popup_set_text(popup, error, 6, 26, AlignLeft, AlignTop);
+        popup_disable_timeout(popup);
+    } else {
+        popup_set_icon(popup, 36, 5, &I_DolphinDone_80x58);
+        popup_set_header(popup, "Imported!", 7, 14, AlignLeft, AlignBottom);
+        popup_enable_timeout(popup);
+    }
+    popup_set_timeout(popup, 1500);
+    popup_set_context(popup, app);
+    popup_set_callback(popup, findmy_scene_config_import_result_callback);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, FindMyViewPopup);
+}
+
+bool findmy_scene_config_import_result_on_event(void* context, SceneManagerEvent event) {
+    FindMy* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        consumed = true;
+        switch(event.event) {
+        case PopupEventExit:
+            scene_manager_search_and_switch_to_previous_scene(
+                app->scene_manager, FindMySceneConfig);
+            break;
+        default:
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+void findmy_scene_config_import_result_on_exit(void* context) {
+    FindMy* app = context;
+    popup_reset(app->popup);
+}
diff --git a/applications/system/find_my_flipper/scenes/findmy_scene_config_mac.c b/applications/system/find_my_flipper/scenes/findmy_scene_config_mac.c
new file mode 100644
index 0000000000..1b72d19278
--- /dev/null
+++ b/applications/system/find_my_flipper/scenes/findmy_scene_config_mac.c
@@ -0,0 +1,67 @@
+#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->state.mac, 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->state.mac, app->mac_buf, sizeof(app->state.mac));
+            findmy_state_sync_config(&app->state);
+            findmy_state_save(&app->state);
+            if(furi_hal_bt_extra_beacon_is_active()) {
+                furi_check(furi_hal_bt_extra_beacon_stop());
+            }
+            furi_check(furi_hal_bt_extra_beacon_set_config(&app->state.config));
+            if(app->state.beacon_active) {
+                furi_check(furi_hal_bt_extra_beacon_start());
+            }
+            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, "");
+}
\ No newline at end of file
diff --git a/applications/system/find_my_flipper/scenes/findmy_scene_config_packet.c b/applications/system/find_my_flipper/scenes/findmy_scene_config_packet.c
new file mode 100644
index 0000000000..9d77fb1d79
--- /dev/null
+++ b/applications/system/find_my_flipper/scenes/findmy_scene_config_packet.c
@@ -0,0 +1,61 @@
+#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:");
+
+    memcpy(app->packet_buf, app->state.data, sizeof(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);
+            memcpy(app->state.data, app->packet_buf, sizeof(app->state.data));
+            findmy_state_save(&app->state);
+            furi_check(
+                furi_hal_bt_extra_beacon_set_data(app->state.data, sizeof(app->state.data)));
+            findmy_main_update_type(app->findmy_main, findmy_data_get_type(app->state.data));
+            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, "");
+}
\ No newline at end of file
diff --git a/applications/system/find_my_flipper/scenes/findmy_scene_main.c b/applications/system/find_my_flipper/scenes/findmy_scene_main.c
new file mode 100644
index 0000000000..e70b59fc54
--- /dev/null
+++ b/applications/system/find_my_flipper/scenes/findmy_scene_main.c
@@ -0,0 +1,63 @@
+#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:
+            app->state.beacon_active = true;
+            findmy_state_save(&app->state);
+            if(!furi_hal_bt_extra_beacon_is_active()) {
+                furi_check(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->state.broadcast_interval + 1);
+            break;
+        case FindMyMainEventIntervalDown:
+            findmy_change_broadcast_interval(app, app->state.broadcast_interval - 1);
+            break;
+        case FindMyMainEventQuit:
+            app->state.beacon_active = false;
+            findmy_state_save(&app->state);
+            if(furi_hal_bt_extra_beacon_is_active()) {
+                furi_check(furi_hal_bt_extra_beacon_stop());
+            }
+            break;
+        default:
+            consumed = false;
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+void findmy_scene_main_on_exit(void* context) {
+    FindMy* app = context;
+    UNUSED(app);
+}
\ No newline at end of file
diff --git a/applications/system/find_my_flipper/scenes/findmy_scenes.h b/applications/system/find_my_flipper/scenes/findmy_scenes.h
new file mode 100644
index 0000000000..9a35c519d9
--- /dev/null
+++ b/applications/system/find_my_flipper/scenes/findmy_scenes.h
@@ -0,0 +1,6 @@
+ADD_SCENE(findmy, main, Main)
+ADD_SCENE(findmy, config, Config)
+ADD_SCENE(findmy, config_import, ConfigImport)
+ADD_SCENE(findmy, config_import_result, ConfigImportResult)
+ADD_SCENE(findmy, config_mac, ConfigMac)
+ADD_SCENE(findmy, config_packet, ConfigPacket)
diff --git a/applications/system/find_my_flipper/views/findmy_main.c b/applications/system/find_my_flipper/views/findmy_main.c
new file mode 100644
index 0000000000..9f9f599055
--- /dev/null
+++ b/applications/system/find_my_flipper/views/findmy_main.c
@@ -0,0 +1,150 @@
+#include "findmy_main.h"
+#include "../findmy_i.h"
+
+struct FindMyMain {
+    View* view;
+    FindMyMainCallback callback;
+    void* context;
+};
+
+typedef struct {
+    bool active;
+    uint8_t interval;
+    FindMyType type;
+} 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);
+    switch(model->type) {
+    case FindMyTypeApple:
+        canvas_draw_str(canvas, 4, 32, "Apple Network");
+        canvas_draw_icon(canvas, 80, 24, &I_Lock_7x8);
+        break;
+    case FindMyTypeSamsung:
+        canvas_draw_str(canvas, 4, 32, "Samsung Network");
+        canvas_draw_icon(canvas, 97, 24, &I_Lock_7x8);
+        break;
+    default:
+        break;
+    }
+    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;
+        FindMyMainEvent cb_event;
+
+        switch(event->key) {
+        case InputKeyBack:
+            cb_event = FindMyMainEventQuit;
+            break;
+        case InputKeyOk:
+            cb_event = FindMyMainEventToggle;
+            break;
+        case InputKeyLeft:
+            cb_event = FindMyMainEventBackground;
+            break;
+        case InputKeyRight:
+            cb_event = FindMyMainEventConfig;
+            break;
+        case InputKeyUp:
+            cb_event = FindMyMainEventIntervalUp;
+            break;
+        case InputKeyDown:
+            cb_event = FindMyMainEventIntervalDown;
+            break;
+        default:
+            return consumed;
+        }
+
+        findmy_main->callback(cb_event, findmy_main->context);
+    }
+
+    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->state.beacon_active;
+            model->interval = app->state.broadcast_interval;
+            model->type = findmy_data_get_type(app->state.data);
+        },
+        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_type(FindMyMain* findmy_main, FindMyType type) {
+    furi_assert(findmy_main);
+    with_view_model(
+        findmy_main->view, FindMyMainModel * model, { model->type = type; }, true);
+}
diff --git a/applications/system/find_my_flipper/views/findmy_main.h b/applications/system/find_my_flipper/views/findmy_main.h
new file mode 100644
index 0000000000..92a9170ec2
--- /dev/null
+++ b/applications/system/find_my_flipper/views/findmy_main.h
@@ -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_type(FindMyMain* findmy_main, FindMyType type);
\ No newline at end of file