diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..9436737 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,48 @@ +name: Tests + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +jobs: + web: + name: Web installer (vitest) + runs-on: ubuntu-latest + defaults: + run: + working-directory: web + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: web/package-lock.json + - run: npm ci + - run: npm test + + desktop-helper: + name: Desktop helper (jest) + runs-on: ubuntu-latest + defaults: + run: + working-directory: desktop_helper + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: desktop_helper/package-lock.json + - run: npm ci + - run: npm test + + flipper-host: + name: Flipper C app (host tests) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run host tests + run: make -C src/tests/host run diff --git a/README.md b/README.md index 98e832c..f6d4b53 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,12 @@ FlipDeck transforms your Flipper Zero into a programmable command deck. Store fr - ✅ **Profile Storage** — Save command profiles on the SD card - ✅ **USB HID Keyboard** — Send text, key presses, and shortcuts -- ✅ **Profile Types** — Commands, snippets, shortcuts, and scripts -- ✅ **No WiFi Required** — Works on stock Flipper Zero -- ✅ **Safe Mode** — Confirmation before sending commands -- ✅ **Settings** — Configurable delays and preferences +- ✅ **Favorites + quick-send** — Pin actions; long-press OK to skip confirm +- ✅ **NFC tag triggers** — Bind a tag to an action's confirm screen +- ✅ **Sub-GHz RF triggers** — RX-only 433MHz remote bind/fire (never transmits) +- ✅ **WiFi Dev Board UART** — Optional `wifi_uart` command target +- ✅ **Safe Mode** — Confirmation before sending; critical patterns blocked +- ✅ **Settings** — Configurable delays, confirm, and USB auto-check ## Installation for Flipper Zero users @@ -228,6 +230,8 @@ On the Flipper, the SD card is mounted under `/ext`, so the app reads from `/ext ├── snippets/ # Text snippet templates │ ├── typescript.txt # TS code templates │ └── go.txt # Go code templates +├── nfc_tags.json # NFC tag → action bindings (created on first bind) +├── subghz_remotes.json # Sub-GHz remote → action bindings (created on first bind) ├── logs/ # Session logs └── settings.json # User preferences ``` @@ -302,22 +306,25 @@ flipdeck/ ├── src/ # Flipper app source │ ├── flipdeck_app.c # Main application logic │ ├── flipdeck_app.h -│ ├── flipdeck_ui.c # User interface (browser, favorites, NFC scan, confirm) +│ ├── flipdeck_ui.c # UI (browser, favorites, NFC/Sub-GHz scan, confirm) │ ├── flipdeck_ui.h -│ ├── profile_manager.c # SD card profile system, favorites, NFC tag mappings +│ ├── profile_manager.c # Profiles, favorites, NFC + Sub-GHz mappings │ ├── profile_manager.h │ ├── usb_hid.c # USB HID communication │ ├── usb_hid.h │ ├── uart_bridge.c # UART bridge to the WiFi Dev Board │ ├── uart_bridge.h -│ ├── nfc_bridge.c # NFC tag scan bridge (tag-triggered sends) +│ ├── nfc_bridge.c # NFC tag scan bridge │ ├── nfc_bridge.h +│ ├── subghz_bridge.c # Sub-GHz RX-only remote scan bridge +│ ├── subghz_bridge.h │ └── settings.c # Settings management ├── web/ # Next.js web installer ├── docs/ │ ├── flight_manual.md # Safety and usage guide │ ├── installer-flow.md # Web Serial / ZIP installer walkthrough │ ├── nfc_trigger_spec.md # NFC tag trigger design +│ ├── subghz_trigger_spec.md # Sub-GHz remote trigger design │ ├── security-model.md # Safety rules and confirmation model │ └── ROADMAP.md # Development roadmap └── README.md @@ -346,9 +353,8 @@ See [docs/ROADMAP.md](docs/ROADMAP.md) for planned features. - [ ] Custom profile creation from Flipper UI - [ ] Profile import/export via SD card -- [ ] Key combination support - [ ] Presentation remote mode -- [ ] Desktop companion app for profile sync +- [ ] Desktop companion app for live profile sync - [ ] Momentum firmware integration, optional ## Contributing diff --git a/desktop_helper/src/lib/paths.ts b/desktop_helper/src/lib/paths.ts index f625c28..f115172 100644 --- a/desktop_helper/src/lib/paths.ts +++ b/desktop_helper/src/lib/paths.ts @@ -19,6 +19,10 @@ export const PATHS = { SNIPPETS_DIR: "/ext/apps_data/flipdeck/snippets", // Settings file SETTINGS_FILE: "/ext/apps_data/flipdeck/settings.json", + // NFC tag → action mappings + NFC_TAGS_FILE: "/ext/apps_data/flipdeck/nfc_tags.json", + // Sub-GHz remote → action mappings + SUBGHZ_REMOTES_FILE: "/ext/apps_data/flipdeck/subghz_remotes.json", // Logs directory LOGS_DIR: "/ext/apps_data/flipdeck/logs", // The installed flipdeck.fap app itself diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index d76bc31..faf6ff1 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -22,15 +22,16 @@ ## Phase 3: USB Enhancements - [x] Key combination support (Ctrl, Alt, Shift, Win) -- [x] Delay configuration per profile (`delay_ms` per command) +- [x] Delay configuration per command (`delay_ms` on each command; profile-level default still open — see #42) - [ ] Key press/release timing control -- [ ] USB connection auto-detection +- [x] USB connection auto-detection (settings toggle gates the status poll) - [ ] Multiple keyboard layouts ## Phase 4: Advanced Features - [x] Favorites + quick-send (long-press OK skips confirm; Right toggles favorite) - [x] NFC tag triggers (scan a tag to jump straight to a mapped action's confirm screen) +- [x] Sub-GHz RF remote triggers (RX-only 433MHz bind/fire; always confirms) - [ ] Presentation remote mode (left/right/up/down keys) - [ ] Mouse movement and clicks - [ ] Desktop companion app for profile sync diff --git a/docs/flight_manual.md b/docs/flight_manual.md index 3034308..bbf9996 100644 --- a/docs/flight_manual.md +++ b/docs/flight_manual.md @@ -36,14 +36,14 @@ Default profiles use safe commands (comments, harmless text). You must create or ### Review Default Profiles Default profiles include Git and dev commands. Review them in: ``` -/stor0800/flipdeck/profiles/ +/ext/apps_data/flipdeck/profiles/ ``` ### Backup Your SD Card Before creating many profiles, backup your SD card. Profile data is stored in: ``` -/stor0800/flipdeck/profiles/ -/stor0800/flipdeck/settings.json +/ext/apps_data/flipdeck/profiles/ +/ext/apps_data/flipdeck/settings.json ``` --- @@ -119,7 +119,7 @@ FlipDeck's default flow is Category → Action → Confirm → Send. These short Hold MENU + DOWN during startup to disable USB. ### Factory Reset -Delete `/stor0800/flipdeck/` folder to reset all profiles and settings. +Delete `/ext/apps_data/flipdeck/` folder to reset all profiles and settings. ### Report Issues Open an issue at: https://github.com/mohabbis/flipdeck/issues diff --git a/docs/installer-flow.md b/docs/installer-flow.md index b272f72..fe3b4f2 100644 --- a/docs/installer-flow.md +++ b/docs/installer-flow.md @@ -4,9 +4,23 @@ The web installer exists to make FlipDeck feel like a simple device install inst ## User Flow +There are two install paths. Both write the same on-device layout under +`/ext/apps_data/flipdeck/`. + +### Path A: Web Serial (Chrome / Edge) + +1. Open the deployed FlipDeck installer in Chrome or Edge. +2. Choose the default profiles or a custom subset. +3. Review the browser safety check (critical matches block install). +4. Click **Connect Flipper & Stage Pack**, pick the Flipper in the OS picker. +5. Click **Stage N profiles to Flipper** — files are written over the CLI serial port. +6. Launch FlipDeck from the Flipper apps menu (**Apps → Tools → FlipDeck**). + +### Path B: ZIP + qFlipper (any browser) + 1. Open the deployed FlipDeck installer. 2. Choose the default profiles or a custom subset. -3. Download the install ZIP. +3. Review the browser safety check, then download the install ZIP. 4. Extract the ZIP locally. 5. Drag `apps_data` onto the Flipper SD card root in qFlipper. 6. Launch FlipDeck from the Flipper apps menu. diff --git a/src/flipdeck_app.c b/src/flipdeck_app.c index 03db781..abee751 100644 --- a/src/flipdeck_app.c +++ b/src/flipdeck_app.c @@ -45,8 +45,15 @@ bool flipdeck_app_init(void* furi_void) { // Load categories from SD card flipdeck_load_categories(); - // Check USB connection - g_app_ctx->usb_connected = usb_hid_is_connected(); + // Check USB connection when auto-detect is enabled + if(g_app_ctx->settings.auto_detect_usb) { + g_app_ctx->usb_connected = usb_hid_is_connected(); + } else { + // Off: don't probe — avoid a sticky "NO USB" banner when the user + // has opted out of status polling. Send still checks the live HID + // link at the moment of transmission. + g_app_ctx->usb_connected = true; + } // Initialize UART bridge to the WiFi dev board uart_bridge_init(); @@ -112,9 +119,13 @@ void flipdeck_app_free(void* furi_void) { void flipdeck_app_loop(void* furi_void) { UNUSED(furi_void); - // Poll USB connection status - g_app_ctx->usb_connected = usb_hid_is_connected(); - + // Poll USB connection status when auto-detect is enabled + if(g_app_ctx->settings.auto_detect_usb) { + g_app_ctx->usb_connected = usb_hid_is_connected(); + } else { + g_app_ctx->usb_connected = true; + } + switch(g_app_ctx->state) { case FlipDeckState_Idle: break; diff --git a/src/paths.h b/src/paths.h index 5dfe850..889ccd8 100644 --- a/src/paths.h +++ b/src/paths.h @@ -17,6 +17,8 @@ #define FLIPDECK_PROFILES_PATH "/ext/apps_data/flipdeck/profiles" #define FLIPDECK_SNIPPETS_PATH "/ext/apps_data/flipdeck/snippets" #define FLIPDECK_SETTINGS_PATH "/ext/apps_data/flipdeck/settings.json" +#define FLIPDECK_NFC_TAGS_PATH "/ext/apps_data/flipdeck/nfc_tags.json" +#define FLIPDECK_SUBGHZ_REMOTES_PATH "/ext/apps_data/flipdeck/subghz_remotes.json" #define FLIPDECK_LOGS_PATH "/ext/apps_data/flipdeck/logs" #define FLIPDECK_APP_PATH "/ext/apps/Tools/flipdeck.fap" diff --git a/src/profile_manager.c b/src/profile_manager.c index d2ac981..795700d 100644 --- a/src/profile_manager.c +++ b/src/profile_manager.c @@ -5,6 +5,7 @@ #include "profile_manager.h" #include "flipdeck_app.h" +#include "paths.h" #include #include #include @@ -13,11 +14,10 @@ #include #include -#define FLIPDECK_DATA_PATH "/ext/apps_data/flipdeck" -#define PROFILE_BASE_PATH FLIPDECK_DATA_PATH "/profiles" -#define SETTINGS_PATH FLIPDECK_DATA_PATH "/settings.json" -#define NFC_TAGS_PATH FLIPDECK_DATA_PATH "/nfc_tags.json" -#define SUBGHZ_REMOTES_PATH FLIPDECK_DATA_PATH "/subghz_remotes.json" +#define PROFILE_BASE_PATH FLIPDECK_PROFILES_PATH +#define SETTINGS_PATH FLIPDECK_SETTINGS_PATH +#define NFC_TAGS_PATH FLIPDECK_NFC_TAGS_PATH +#define SUBGHZ_REMOTES_PATH FLIPDECK_SUBGHZ_REMOTES_PATH static void storage_ensure_flipdeck_dirs(Storage* storage) { storage_common_mkdir(storage, "/ext/apps_data"); @@ -794,6 +794,67 @@ bool profile_manager_save_subghz_remotes(const FlipDeckSubghzRemote* remotes, ui * @param value Command string to check * @return true if value is safe */ +/** + * @brief Case-folded substring match where each space in `pattern` matches + * one or more whitespace characters in `haystack`. + * + * Mirrors the `\s+` / `\s*` usage in safety-rules.json so on-device blocking + * catches the same spacing variants the web/desktop auditors reject + * (e.g. `rm -rf`, `dd\tif=`, `> /dev/sda`). + */ +static bool contains_flexible_ws(const char* haystack, const char* pattern) { + if(!haystack || !pattern || !*pattern) return false; + + for(const char* start = haystack; *start; start++) { + const char* h = start; + const char* p = pattern; + bool matched = true; + + while(*p) { + if(*p == ' ') { + if(!isspace((unsigned char)*h)) { + matched = false; + break; + } + while(isspace((unsigned char)*h)) h++; + p++; + continue; + } + if(*h != *p) { + matched = false; + break; + } + h++; + p++; + } + + if(matched) return true; + } + + return false; +} + +/** + * @brief True when haystack contains `|` followed by optional whitespace and + * then a whole-token match of `token` (already uppercased), approximating + * `\\|\\s*(sh|bash)`. Requires a non-alnum boundary after the token so + * `|BASH` does not spuriously match the `SH` token. + */ +static bool contains_pipe_token(const char* haystack, const char* token) { + size_t token_len = strlen(token); + for(const char* p = haystack; *p; p++) { + if(*p != '|') continue; + p++; + while(isspace((unsigned char)*p)) p++; + if(strncmp(p, token, token_len) == 0) { + char next = p[token_len]; + if(next == '\0' || !isalnum((unsigned char)next)) return true; + } + if(!*p) break; + } + return false; +} + bool profile_manager_is_value_safe(const char* value) { if(!value) return false; @@ -808,28 +869,36 @@ bool profile_manager_is_value_safe(const char* value) { } upper_value[len] = '\0'; - // Critical patterns — block the command. + // Critical patterns — block the command. Spaces in these patterns match + // one-or-more whitespace (see contains_flexible_ws), matching the + // `rm\s+-rf` / `dd\s+if=` / `>\s*/dev/sd` rules in safety-rules.json. const char* critical_patterns[] = { - "RM -RF", // Recursive delete + "RM -RF", // Recursive delete (rm\s+-rf) "MKFS", // Format filesystem - "DD IF=", // Raw disk write - "> /DEV/SD", // Raw disk redirect - ":(){ :|:& };:", // Fork bomb (no letters; case-insensitive copy is identical) + "DD IF=", // Raw disk write (dd\s+if=) + "> /DEV/SD", // Raw disk redirect with whitespace (>\s+/dev/sd) + ">/DEV/SD", // Raw disk redirect with no whitespace (>\s*/dev/sd) NULL }; for(int i = 0; critical_patterns[i]; i++) { - if(strstr(upper_value, critical_patterns[i])) { + if(contains_flexible_ws(upper_value, critical_patterns[i])) { FURI_LOG_W("FlipDeck", "Blocked critical command: %s", critical_patterns[i]); return false; } } + // Fork bomb: allow flexible whitespace around the classic shape. + if(strstr(upper_value, ":(){") && strstr(upper_value, ":|:&")) { + FURI_LOG_W("FlipDeck", "Blocked critical command: fork bomb"); + return false; + } + // Remote shell pipe: (curl|wget) ... | (sh|bash). Approximates the regex // "(curl|wget).*\\|\\s*(sh|bash)" — catches real one-liners like // "curl -fsSL https://x | bash" that a literal "curl | sh" match misses. bool has_fetch = strstr(upper_value, "CURL") || strstr(upper_value, "WGET"); - bool has_pipe_shell = strstr(upper_value, "| SH") || strstr(upper_value, "|SH") || - strstr(upper_value, "| BASH") || strstr(upper_value, "|BASH"); + bool has_pipe_shell = + contains_pipe_token(upper_value, "SH") || contains_pipe_token(upper_value, "BASH"); if(has_fetch && has_pipe_shell) { FURI_LOG_W("FlipDeck", "Blocked remote shell pipe"); return false; @@ -837,28 +906,29 @@ bool profile_manager_is_value_safe(const char* value) { // Warning patterns — surfaced in the log but allowed (the user still has to // confirm OK on-device before the command is sent). - const char* warning_patterns[] = { - "SUDO", // Privilege escalation - "CHMOD 777", // Loose permissions - "CHOWN ROOT", // Ownership change to root - NULL - }; - for(int i = 0; warning_patterns[i]; i++) { - if(strstr(upper_value, warning_patterns[i])) { - FURI_LOG_W("FlipDeck", "Warning: command matched %s", warning_patterns[i]); - } + if(strstr(upper_value, "SUDO")) { + FURI_LOG_W("FlipDeck", "Warning: command matched SUDO"); + } + if(contains_flexible_ws(upper_value, "CHMOD 777")) { + FURI_LOG_W("FlipDeck", "Warning: command matched CHMOD 777"); + } + if(contains_flexible_ws(upper_value, "CHOWN ROOT")) { + FURI_LOG_W("FlipDeck", "Warning: command matched CHOWN ROOT"); } - // Credential assignments (NAME=...) — warn only. - if(strchr(upper_value, '=')) { - const char* credential_patterns[] = { - "PASSWORD", "TOKEN", "API_KEY", "SECRET", "PRIVATE_KEY", NULL - }; - for(int i = 0; credential_patterns[i]; i++) { - if(strstr(upper_value, credential_patterns[i])) { - FURI_LOG_W("FlipDeck", "Warning: possible credential assignment"); - break; - } + // Credential assignments (NAME=...) — warn only. Allow whitespace around '=' + // to mirror `(PASSWORD|...)\\s*=` in safety-rules.json. + const char* credential_patterns[] = { + "PASSWORD", "TOKEN", "API_KEY", "SECRET", "PRIVATE_KEY", NULL + }; + for(int i = 0; credential_patterns[i]; i++) { + const char* hit = strstr(upper_value, credential_patterns[i]); + if(!hit) continue; + const char* after = hit + strlen(credential_patterns[i]); + while(isspace((unsigned char)*after)) after++; + if(*after == '=') { + FURI_LOG_W("FlipDeck", "Warning: possible credential assignment"); + break; } } diff --git a/src/tests/host/.gitignore b/src/tests/host/.gitignore new file mode 100644 index 0000000..dc6dd76 --- /dev/null +++ b/src/tests/host/.gitignore @@ -0,0 +1,5 @@ +# Built host-test binaries (rebuilt via `make`) +/test_profile_manager +/test_usb_hid +/test_profile_manager_target +/test_settings diff --git a/src/tests/host/test_profile_manager b/src/tests/host/test_profile_manager deleted file mode 100755 index 9c3d23e..0000000 Binary files a/src/tests/host/test_profile_manager and /dev/null differ diff --git a/src/tests/host/test_profile_manager.c b/src/tests/host/test_profile_manager.c index 00cbb25..f700219 100644 --- a/src/tests/host/test_profile_manager.c +++ b/src/tests/host/test_profile_manager.c @@ -26,6 +26,8 @@ static int g_failures = 0; static void test_dangerous_rm_rf(void) { EXPECT_FALSE(profile_manager_is_value_safe("rm -rf /"), "blocks rm -rf /"); EXPECT_FALSE(profile_manager_is_value_safe("echo x && rm -rf ."), "blocks rm -rf in chain"); + EXPECT_FALSE(profile_manager_is_value_safe("rm -rf /"), "blocks rm -rf (extra spaces)"); + EXPECT_FALSE(profile_manager_is_value_safe("rm\t-rf /tmp"), "blocks rm\\t-rf (tab)"); } static void test_warning_sudo(void) { @@ -41,14 +43,20 @@ static void test_dangerous_remote_exec(void) { profile_manager_is_value_safe("curl -fsSL https://example.com/install.sh | bash"), "blocks real curl|bash one-liner"); EXPECT_FALSE(profile_manager_is_value_safe("CURL https://x | SH"), "blocks curl|sh case-insensitively"); + EXPECT_FALSE(profile_manager_is_value_safe("curl https://x |\tbash"), "blocks curl … |\\tbash"); + EXPECT_FALSE(profile_manager_is_value_safe("wget https://x | sh"), "blocks wget … | sh"); /* "curl.ssh" has no pipe into a shell - it isn't a remote-exec pattern */ EXPECT_TRUE(profile_manager_is_value_safe("curl.ssh user@host"), "allows curl.ssh (no pipe to shell)"); + EXPECT_TRUE(profile_manager_is_value_safe("curl https://x | cat"), "allows curl … | cat"); } static void test_dangerous_disk_ops(void) { EXPECT_FALSE(profile_manager_is_value_safe("mkfs.ext4 /dev/sda"), "blocks mkfs"); EXPECT_FALSE(profile_manager_is_value_safe("dd if=/dev/zero of=/dev/sda"), "blocks dd if="); + EXPECT_FALSE(profile_manager_is_value_safe("dd if=/dev/zero of=/dev/sda"), "blocks dd if= (extra spaces)"); EXPECT_FALSE(profile_manager_is_value_safe("echo data > /dev/sda"), "blocks > /dev/sd"); + EXPECT_FALSE(profile_manager_is_value_safe("echo data >/dev/sda"), "blocks >/dev/sd (no space)"); + EXPECT_FALSE(profile_manager_is_value_safe("echo data > /dev/sdb"), "blocks > /dev/sd (extra spaces)"); } static void test_dangerous_fork_bomb(void) { diff --git a/src/tests/host/test_usb_hid b/src/tests/host/test_usb_hid deleted file mode 100755 index 2b9cb09..0000000 Binary files a/src/tests/host/test_usb_hid and /dev/null differ diff --git a/web/CLAUDE.md b/web/CLAUDE.md index 780f433..7eb89dd 100644 --- a/web/CLAUDE.md +++ b/web/CLAUDE.md @@ -27,7 +27,7 @@ npm test - Main install-pack endpoint: `/api/install-bundle/download` - The generated ZIP must include `README-FIRST.txt`, `apps_data/flipdeck/settings.json`, `apps_data/flipdeck/profiles/*.json`, and `apps_data/flipdeck/snippets/*.txt`. -- `npm run start` serves the standalone Next.js build on `0.0.0.0` for Railway. +- `npm run start` serves the standalone Next.js build on `0.0.0.0` (used for local/production Node hosting). ## Guidelines diff --git a/web/src/lib/flipper-serial.ts b/web/src/lib/flipper-serial.ts index 5aee0c7..e29709f 100644 --- a/web/src/lib/flipper-serial.ts +++ b/web/src/lib/flipper-serial.ts @@ -1,3 +1,5 @@ +import { PATHS } from "@/lib/paths"; + // Minimal Web Serial type declarations for Flipper Zero CLI communication interface FlipperSerialPort { open(options: { baudRate: number; dataBits: number; stopBits: number; parity: string }): Promise; @@ -318,25 +320,25 @@ export class FlipperSerial { const tick = (msg: string) => onProgress(msg, step++ / total); tick("Creating directory…"); - await this.ensureDir("/ext/apps_data/flipdeck"); + await this.ensureDir(PATHS.DEVICE.DATA_ROOT); tick("Creating profiles directory…"); - await this.ensureDir("/ext/apps_data/flipdeck/profiles"); + await this.ensureDir(PATHS.DEVICE.PROFILES_DIR); tick("Writing settings…"); - await this.writeFile("/ext/apps_data/flipdeck/settings.json", settingsJson); + await this.writeFile(PATHS.DEVICE.SETTINGS_FILE, settingsJson); for (const { id, json } of profiles) { tick(`Writing ${id}.json…`); - await this.writeFile(`/ext/apps_data/flipdeck/profiles/${id}.json`, json); + await this.writeFile(`${PATHS.DEVICE.PROFILES_DIR}/${id}.json`, json); } if (snippets.length) { tick("Creating snippets directory…"); - await this.ensureDir("/ext/apps_data/flipdeck/snippets"); + await this.ensureDir(PATHS.DEVICE.SNIPPETS_DIR); for (const { name, content } of snippets) { tick(`Writing snippet ${name}…`); - await this.writeFile(`/ext/apps_data/flipdeck/snippets/${name}`, content); + await this.writeFile(`${PATHS.DEVICE.SNIPPETS_DIR}/${name}`, content); } } @@ -351,7 +353,7 @@ export class FlipperSerial { await this.ensureDirRpc("/ext/apps/Tools"); tick("Installing flipdeck.fap…"); - await this.writeFileRpc("/ext/apps/Tools/flipdeck.fap", appBinary); + await this.writeFileRpc(PATHS.DEVICE.APP_PATH, appBinary); } onProgress(appBinary ? "Done!" : "Profiles staged. Install the app binary separately.", 1); diff --git a/web/src/lib/paths.ts b/web/src/lib/paths.ts index f625c28..f115172 100644 --- a/web/src/lib/paths.ts +++ b/web/src/lib/paths.ts @@ -19,6 +19,10 @@ export const PATHS = { SNIPPETS_DIR: "/ext/apps_data/flipdeck/snippets", // Settings file SETTINGS_FILE: "/ext/apps_data/flipdeck/settings.json", + // NFC tag → action mappings + NFC_TAGS_FILE: "/ext/apps_data/flipdeck/nfc_tags.json", + // Sub-GHz remote → action mappings + SUBGHZ_REMOTES_FILE: "/ext/apps_data/flipdeck/subghz_remotes.json", // Logs directory LOGS_DIR: "/ext/apps_data/flipdeck/logs", // The installed flipdeck.fap app itself