mirror of
https://github.com/Next-Flip/Momentum-Firmware.git
synced 2026-06-21 20:42:15 -07:00
Merge pull request #270 from Next-Flip/js-backport-of-backport
* Xero initial commit * Xero initial commit * Update README.md with feature goals * Add MFKey to firmware * Update ReadMe.md * remove serial check ui in gangqi [ci skip] * Furi: A Lot of Fixes (#3942) - BT Service: cleanup code - Dialog: correct release order in file browser - Rpc: rollback to pre #3881 state - Kernel: fix inverted behavior in furi_kernel_is_running - Log: properly take mutex when kernel is not running - Thread: rework tread control block scrubbing procedure, ensure that we don't do stupid things in idle task, add new priority for init task - Timer: add control queue flush method, force flush on stop - Furi: system init task now performs thread scrubbing - BleGlue: add some extra checks - FreeRTOSConfig: fix bunch of issues that were preventing configuration from being properly applied and cleanup * Wi-Fi Devboard documentation rework (#3944) * New step-by-step documentation structure for Wi-Fi Devboard * Added a description of working under Windows * Added a description of switching Devboard operation mode (Black Magic, DAPLink) * The images for the documentation are uploaded to the CDN * The text in the sidebar, near the dolphin logo, changed from blue to black/white Co-authored-by: knrn64 <25254561+knrn64@users.noreply.github.com> Co-authored-by: Aleksandr Kutuzov <alleteam@gmail.com> * Fixes Mouse Clicker Should have a "0" value setting for "as fast as possible" #3876 (#3894) * fixes mouse clicking rate * Hid: limit max clicks to 100/s, rewrite code to make it more robust Co-authored-by: あく <alleteam@gmail.com> * [FL-3909] CLI improvements, part I (#3928) * fix: cli top blinking * feat: clear prompt on down key * feat: proper-er ansi escape sequence handling * ci: fix compact build error * Make PVS happy * style: remove magic numbers * style: review suggestions Co-authored-by: あく <alleteam@gmail.com> * Rename #2 * Api adjustements * NFC: iso14443_4a improvements. Canvas: extended icon draw. (#3918) * Now 4a listener invokes upper level callback on Halt and FieldOff * Added new method for drawing mirrored XBM bitmaps * iso14443_4a poller logic enhanced * Function renamed accroding to review suggestions * Rename #2 * Api adjustements * Correct API bump Co-authored-by: あく <alleteam@gmail.com> * heap: increased size (#3924) * reduced reserved memory size for system stack; added temporary markup to monitor usage * fbt: relink elf file on linker script change; removed debug memory fill * Make PVS Happy * Make doxygen happy Co-authored-by: Aleksandr Kutuzov <alleteam@gmail.com> * [FL-3893] JS modules (#3841) * feat: backport js_gpio from unleashed * feat: backport js_keyboard, TextInputModel::minimum_length from unleashed * fix: api version inconsistency * style: js_gpio * build: fix submodule ._ . * refactor: js_gpio * docs: type declarations for gpio * feat: gpio interrupts * fix: js_gpio freeing, resetting and minor stylistic changes * style: js_gpio * style: mlib array, fixme's * feat: js_gpio adc * feat: js_event_loop * docs: js_event_loop * feat: js_event_loop subscription cancellation * feat: js_event_loop + js_gpio integration * fix: js_event_loop memory leak * feat: stop event loop on back button * test: js: basic, math, event_loop * feat: js_event_loop queue * feat: js linkage to previously loaded plugins * build: fix ci errors * feat: js module ordered teardown * feat: js_gui_defer_free * feat: basic hourglass view * style: JS ASS (Argument Schema for Scripts) * fix: js_event_loop mem leaks and lifetime problems * fix: crashing test and pvs false positives * feat: mjs custom obj destructors, gui submenu view * refactor: yank js_gui_defer_free (yuck) * refactor: maybe_unsubscribe * empty_screen, docs, typing fix-ups * docs: navigation event & demo * feat: submenu setHeader * feat: text_input * feat: text_box * docs: text_box availability * ci: silence irrelevant pvs low priority warning * style: use furistring * style: _get_at -> _safe_get * fix: built-in module name assignment * feat: js_dialog; refactor, optimize: js_gui * docs: js_gui * ci: silence pvs warning: Memory allocation is infallible * style: fix storage spelling * feat: foreign pointer signature checks * feat: js_storage * docs: js_storage * fix: my unit test was breaking other tests ;_; * ci: fix ci? * Make doxygen happy * docs: flipper, math, notification, global * style: review suggestions * style: review fixups * fix: badusb demo script * docs: badusb * ci: add nofl * ci: make linter happy * Bump api version Co-authored-by: Aleksandr Kutuzov <alleteam@gmail.com> * NFC: H World Hotel Chain Room Key Parser (#3946) * quick and easy implementation * delete debug artifacts * log level correction (warning -> info) * Merge remote-tracking branch 'OFW/dev' into dev * after merge fix p1 * merge p2 * merge p3 * Small JS fixes (#3950) * [FL-3914] BackUSB (#3951) * Revert "[FL-3896] Split BadUSB into BadUSB and BadBLE (#3931)" This reverts commit0eaad8bf64. * Better USB-BLE switch UX * Format sources * Format images Co-authored-by: あく <alleteam@gmail.com> * New Static Keys for Mifare Classic Dictionary (#3947) Co-authored-by: あく <alleteam@gmail.com> * [BadUSB] Improve ChromeOS and GNOME demo scripts (#3948) * [BadUSB] Gnome Demo: Support most terminals and force sh shell when not using Bash as default * [BadUSB] ChromeOS Demo: Minor improvements, such as exit overview, select omnibox and add a page title Signed-off-by: Kowalski Dragon (kowalski7cc) <5065094+kowalski7cc@users.noreply.github.com> Co-authored-by: Kowalski Dragon (kowalski7cc) <5065094+kowalski7cc@users.noreply.github.com> Co-authored-by: あく <alleteam@gmail.com> * Remove out of date screenshot * implement byte input, add missing args in text input TODO: add missing filebrowser dialog * move examples * badusb js paritally fix * add new nfc key [ci skip] * Read Ultralight block by block * JS: Merge fixes * JS: camelCase changes * Update changelog, JS changes still missing * JS: Docs and types for additions on top of OFW modules * JS: Port byte input example (WIP, has some bugs in C code, JS is fine) * fix zero issues in princeton * merge missing js backports of backports by Willy-JL p1 * JS: Update storage example * JS: Disclaimer about documentation * JS: Types for chr(), .indexOf(), .slice() * JS: More fixes * merge examples fixes [ci skip] by Willy-JL p2 * more js stuff https://github.com/Next-Flip/Momentum-Firmware/commits/js-backport-of-backport/ * JS: Fix load.js example path * JS: Byte/Text Input fixes, gui example improvements * JS: Expose currentView in gui.viewDispatcher * JS: Update interactive.js REPL example * JS: Add byte input example image for docs * JS: Fix toString() with negative numbers * Small fixes in the wifi devboard docs (#3953) * Small fixes in the wifi devboard docs - Several broken numbered lists fixed - Added a skipped link to the uFBT - The Devboard naming fixed * JS: Silence plugin functions warning * merge js upds 1 * merge js 2 * [FL-3916] Require PIN on boot (#3952) Co-authored-by: hedger <hedger@users.noreply.github.com> * upd changelog * NFC Parser for Tianjin Railway Transit (#3954) * fix cli breaking web flipper lab, remove color tags * upd changelog * JS: Backport virtualMount storage API * JS: Backport file picker as new module * Workflows: Upload more build artifacts * Fixed bug with UL reading * upd changelog * Add warning about stealth mode in vibro CLI (#3957) * JS: Add way to check if view has prop * JS: Add numpad keys to badusb typedefs * Revert "[FL-3909] CLI improvements, part I (#3928)" (#3955) This reverts commit0f831412fa. Co-authored-by: あく <alleteam@gmail.com> * JS: Improve REPL script * JS: Custom scope param in load() typedef * Update API versions for OFW RC * JS: Sync small differences from OFW PR * Fix multiple crashes and state machine logic * Update changelog (not final) * JS: C define to move JS runner to flash (still external for now) * JS: First batch of OFW PR review changes * Update apps - Seader: Show error for timeout, fix wrong LRC logging (by bettse) * Fix inconsistent assignment of known key and known key type/sector * Revert api bump * Backdoor known key logic still needs the current key * JS: Second batch of OFW PR changes * IR: Basic script to remove dupes had this lying around from last week PRs and could come useful again, code is meh but works * Fix changelog --------- Signed-off-by: Kowalski Dragon (kowalski7cc) <5065094+kowalski7cc@users.noreply.github.com> Co-authored-by: noproto <noproto@zeroday.engineering> Co-authored-by: Nathan N <noproto@users.noreply.github.com> Co-authored-by: MX <10697207+xMasterX@users.noreply.github.com> Co-authored-by: あく <alleteam@gmail.com> Co-authored-by: Ruslan Nadyrshin <110516632+rnadyrshin@users.noreply.github.com> Co-authored-by: knrn64 <25254561+knrn64@users.noreply.github.com> Co-authored-by: SUMUKH <130692934+sumukhj1219@users.noreply.github.com> Co-authored-by: porta <portasynthinca3@gmail.com> Co-authored-by: RebornedBrain <RebornedBrain@gmail.com> Co-authored-by: RebornedBrain <138568282+RebornedBrain@users.noreply.github.com> Co-authored-by: hedger <hedger@users.noreply.github.com> Co-authored-by: Zinong Li <131403964+zinongli@users.noreply.github.com> Co-authored-by: Astra <93453568+Astrrra@users.noreply.github.com> Co-authored-by: Kowalski Dragon <kowalski7cc@users.noreply.github.com> Co-authored-by: Kowalski Dragon (kowalski7cc) <5065094+kowalski7cc@users.noreply.github.com> Co-authored-by: Mykhailo Shevchuk <myte@ukr.net> Co-authored-by: Ivan Barsukov <ivan.barsukow@gmail.com>
This commit is contained in:
@@ -79,7 +79,7 @@ jobs:
|
||||
- name: "Build the firmware and apps"
|
||||
id: build-fw
|
||||
run: |
|
||||
./fbt TARGET_HW=$TARGET_HW $FBT_BUILD_TYPE updater_package
|
||||
./fbt TARGET_HW=$TARGET_HW $FBT_BUILD_TYPE copro_dist updater_package fap_dist
|
||||
echo "firmware_api=$(./fbt TARGET_HW=$TARGET_HW get_apiversion)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Check for uncommitted changes"
|
||||
@@ -101,7 +101,9 @@ jobs:
|
||||
set -e
|
||||
rm -rf artifacts || true
|
||||
mkdir artifacts
|
||||
cp dist/${TARGET}-*/flipper-z-${TARGET}-{update,sdk}-* artifacts/
|
||||
cp dist/${TARGET}-*/flipper-z-${TARGET}-{update-*.tgz,sdk-*.zip,full-*.dfu} artifacts/
|
||||
tar czpf "artifacts/flipper-z-${TARGET}-resources-${SUFFIX}.tgz" \
|
||||
-C build/latest resources
|
||||
cd dist/${TARGET}-*/${TARGET}-update-*/
|
||||
ARTIFACT_TAG=flipper-z-"$(basename "$(realpath .)")"
|
||||
7z a ../../../artifacts/${ARTIFACT_TAG}.zip .
|
||||
@@ -109,6 +111,14 @@ jobs:
|
||||
VERSION_TAG="$(basename "$(realpath .)" | cut -d- -f3-)"
|
||||
echo "VERSION_TAG=$VERSION_TAG" >> $GITHUB_ENV
|
||||
|
||||
- name: "Copy universal artifacts"
|
||||
env:
|
||||
INDEXER_URL: ${{ secrets.INDEXER_URL }}
|
||||
if: ${{ env.INDEXER_URL != '' && (github.event.pull_request || (github.event_name == 'push' && ((github.ref_name == 'dev' && !contains(github.event.head_commit.message, '--nobuild')) || startsWith(github.ref, 'refs/tags/')))) }}
|
||||
run: |
|
||||
tar czpf "artifacts/flipper-z-any-scripts-${SUFFIX}.tgz" scripts
|
||||
cp build/core2_firmware.tgz "artifacts/flipper-z-any-core2_firmware-${SUFFIX}.tgz"
|
||||
|
||||
- name: "Upload artifacts to update server"
|
||||
env:
|
||||
INDEXER_URL: ${{ secrets.INDEXER_URL }}
|
||||
|
||||
+59
-3
@@ -3,6 +3,49 @@
|
||||
- Reworks how communication with battery guage is done, improves reliability and fixes issues with battery percentage not showing
|
||||
- After installing firmware with this change, downgrading to old firmware will cause battery percentage to be blank
|
||||
- If you must downgrade firmware, use the [Guage Tool app](https://github.com/skotopes/flipperzero_gauge_tool) to unseal the guage
|
||||
- OFW: JS: Modules backport & overhaul (by @portasynthinca3), backport of backport (by @Willy-JL & @xMasterX)
|
||||
- OFW backported some modules we had, added lots of new stuff, and overhauled many other things
|
||||
- Non-exhaustive list of changes to help you fix your scripts:
|
||||
- `badusb`:
|
||||
- `setup()`: `mfr_name`, `prod_name`, `layout_path` parameters renamed to `mfrName`, `prodName`, `layoutPath`
|
||||
- effort required to update old scripts using badusb: very minimal
|
||||
- `dialog`:
|
||||
- removed, now replaced by `gui/dialog` and `gui/file_picker` (see below)
|
||||
- `event_loop`:
|
||||
- new module, allows timer functionality, callbacks and event-driven programming, used heavily alongside gpio and gui modules
|
||||
- `gpio`:
|
||||
- fully overhauled, now you `get()` pin instances and perform actions on them like `.init()`
|
||||
- now supports interrupts, callbacks and more cool things
|
||||
- effort required to update old scripts using gpio: moderate
|
||||
- `gui`:
|
||||
- new module, fully overhauled, replaces dialog, keyboard, submenu, textbox modules
|
||||
- higher barrier to entry than older modules (requires usage of `event_loop` and `gui.viewDispatcher`), but much more flexible, powerful and easier to extend
|
||||
- includes all previously available js gui functionality (except `widget`), and also adds `gui/loading` and `gui/empty_screen` views
|
||||
- currently `gui/file_picker` works different than other new view objects, it is a simple `.pickFile()` synchronous function, but this [may change later](https://github.com/flipperdevices/flipperzero-firmware/pull/3961#discussion_r1805579153)
|
||||
- effort required to update old scripts using gui: extensive
|
||||
- `keyboard`:
|
||||
- removed, now replaced by `gui/text_input` and `gui/byte_input` (see above)
|
||||
- `storage`:
|
||||
- fully overhauled, now you `openFile()`s and perform actions on them like `.read()`
|
||||
- now supports many more operations including different open modes, directories and much more
|
||||
- `virtualInit()`, `virtualMount()`, `virtualQuit()` still work the same
|
||||
- effort required to update old scripts using storage: moderate
|
||||
- `submenu`:
|
||||
- removed, now replaced by `gui/submenu` (see above)
|
||||
- `textbox`:
|
||||
- removed, now replace by `gui/text_box` (see above)
|
||||
- `widget`:
|
||||
- only gui functionality not ported to new gui module, remains unchanged for now but likely to be ported later on
|
||||
- globals:
|
||||
- `__filepath` and `__dirpath` renamed to `__filename` and `__dirname` like in nodejs
|
||||
- `to_string()` renamed and moved to number class as `n.toString()`, now supports optional base parameter
|
||||
- `to_hex_string()` removed, now use `n.toString(16)`
|
||||
- `parse_int()` renamed to `parseInt()`, now supports optional base parameter
|
||||
- `to_upper_case()` and `to_lower_case()` renamed and moved to string class as `s.toUpperCase()` and `s.toLowerCase()`
|
||||
- effort required to update old scripts using these: minimal
|
||||
- Added type definitions (typescript files for type checking in IDE, Flipper does not run typescript)
|
||||
- Documentation is incomplete and deprecated, from now on you should refer to type definitions (`applications/system/js_app/types`), those will always be correct
|
||||
- Type definitions for extra modules we have that OFW doesn't will come later
|
||||
|
||||
### Added:
|
||||
- Apps:
|
||||
@@ -24,6 +67,8 @@
|
||||
- Static encrypted backdoor support: collects static encrypted nonces to be cracked by MFKey using NXP/Fudan backdoor, allowing key recovery of all non-hardened MIFARE Classic tags on-device
|
||||
- Add SmartRider Parser (#203 by @jaylikesbunda)
|
||||
- Add API to enforce ISO15693 mode (#225 by @aaronjamt)
|
||||
- OFW: H World Hotel Chain Room Key Parser and MFC keys (by @zinongli)
|
||||
- OFW: Parser for Tianjin Railway Transit (by @zinongli)
|
||||
- Infrared:
|
||||
- Bluray/DVD Universal Remote (#250 by @jaylikesbunda)
|
||||
- Option to "Load from Library File" for Universal Remotes (#255 by @zxkmm)
|
||||
@@ -33,9 +78,10 @@
|
||||
- Add older qFlipper install demos for windows and macos (by @DXVVAY & @grugnoymeme)
|
||||
- OFW: New layout for es-LA (by @IRecabarren)
|
||||
- OFW: Dolphin: Happy mode in Desktop settings (by @portasynthinca3)
|
||||
- OFW: CLI: Improvements part I, `neofetch` command (by @portasynthinca3), fix for lab.flipper.net (by @xMasterX)
|
||||
- GUI:
|
||||
- OFW: Add up and down button drawing functions to GUI elements (by @DerSkythe)
|
||||
- OFW: Added one new function for drawing mirrored xbm bitmaps (by @RebornedBrain)
|
||||
- OFW: Extended icon draw function in Canvas (by @RebornedBrain)
|
||||
- OFW: RPC: Support 5V on GPIO control for ext. modules (by @gsurkov)
|
||||
- OFW: Toolbox: Proper integer parsing library `strint` (by @portasynthinca3)
|
||||
- OFW: Furi: Put errno into TCB (by @portasynthinca3)
|
||||
@@ -51,17 +97,19 @@
|
||||
- DTMF Dolphin: Add EAS tone support (by @JendrBendr)
|
||||
- NFC Playlist: Error screens for playlist already exists and item already in playlist, general improvements (by @acegoal07), refactor rename/new scene without thread (by @Willy-JL)
|
||||
- CLI-GUI Bridge: Fixes and improvements (by @ranchordo)
|
||||
- Seader: Enable T=1 (by @bettse)
|
||||
- Seader: Enable T=1, show error for timeout, fix wrong LRC logging (by @bettse)
|
||||
- BLE Spam: Fix menu index callback (by @Willy-JL)
|
||||
- Solitaire: App rewrite, Added quick solve, New effects and sounds, Removed hacky canvas manipulation (by @doofy-dev)
|
||||
- CLI-GUI Bridge: Add more symbols to keyboard (#222 by @Willy-JL)
|
||||
- UL: Sub-GHz Bruteforcer: Add new protocols for existing dump option (by @xMasterX), use FW functions for top buttons (by @DerSkythe)
|
||||
- UL: NRF24 Apps: Use string library compatible with OFW SDK (by @xMasterX)
|
||||
- OFW: SPI Mem Manager: Fixed UI rendering bug related to line breaks (by @portasynthinca3)
|
||||
- OFW: USB/BT Remote: Mouse clicker option to click as fast as possible (by @sumukhj1219)
|
||||
- CLI: Print plugin name on load fail (by @Willy-JL)
|
||||
- NFC:
|
||||
- Added 6 new Mifare Classic keys from Bulgaria Hotel (#216 by @z3r0l1nk)
|
||||
- NDEF parser supports NTAG I2C Plus 1k and 2k chips too (by @RocketGod-git)
|
||||
- UL: Add iq aparts hotel key (by @xMasterX)
|
||||
- OFW/UL: Rename 'Detect Reader' to 'Extract MFC Keys' (by @bettse & @xMasterX)
|
||||
- OFW: Plantain parser improvements (by @assasinfil)
|
||||
- OFW: Moscow social card parser (by @assasinfil)
|
||||
@@ -82,6 +130,7 @@
|
||||
- OFW: Add TCL 75S451 to TV universal remote (by @christhetech131)
|
||||
- OFW: Universal remote additions (by @jaylikesbunda)
|
||||
- OFW: Heavily Expand Universal Remotes (by @jaylikesbunda)
|
||||
- OFW: BadKB: Improve ChromeOS and GNOME demo scripts (by @kowalski7cc)
|
||||
- OFW: GUI: Change dialog_ex text ownership model (by @skotopes)
|
||||
- OFW: CCID: App changes and improvements (by @kidbomb)
|
||||
- OFW: API: Exposed `view_dispatcher_get_event_loop` (by @CookiePLMonster)
|
||||
@@ -91,9 +140,11 @@
|
||||
- OFW: Threading, Timers improvements (by @CookiePLMonster)
|
||||
- OFW: FuriTimer uses an event instead of a volatile bool to wait for deletion (by @CookiePLMonster)
|
||||
- OFW: Improve FuriThread state callbacks (by @CookiePLMonster)
|
||||
- OFW: Increased heap size (by @hedger)
|
||||
- Documentation:
|
||||
- OFW: Update and cleanup (by @rnadyrshin)
|
||||
- OFW: Improve bit_buffer.h docs (by @Astrrra)
|
||||
- OFW: Wi-Fi Devboard documentation rework (by @rnadyrshin)
|
||||
|
||||
### Fixed:
|
||||
- RFID:
|
||||
@@ -102,7 +153,9 @@
|
||||
- OFW: GProxII Fix Writing and Rendering Conflict (by @zinongli)
|
||||
- Desktop:
|
||||
- Fallback Poweroff prompt when power settings is unavailable (by @Willy-JL)
|
||||
- Sub-GHz: Fix GPS "Latitute" typo, switch to "Lat" and "Lon" in .sub files (#246 by @m7i-org)
|
||||
- Sub-GHz:
|
||||
- Fix GPS "Latitute" typo, switch to "Lat" and "Lon" in .sub files (#246 by @m7i-org)
|
||||
- UL: Fix zero issues in Princeton (by @xMasterX)
|
||||
- Power: Suppress Shutdown on Idle While Charging / Plugged In (#244 by @luu176)
|
||||
- Storage:
|
||||
- Fallback SD format prompt when storage settings is unavailable (by @Willy-JL)
|
||||
@@ -110,6 +163,7 @@
|
||||
- About: Fix BLE stack version string (by @Willy-JL)
|
||||
- OFW: Loader: Warn about missing SD card for main apps (by @Willy-JL)
|
||||
- NFC:
|
||||
- UL: Read Ultralight block by block (by @mishamyte)
|
||||
- OFW: Fix crash on Ultralight unlock (by @Astrrra)
|
||||
- OFW: FeliCa anti-collision fix (by @RebornedBrain)
|
||||
- OFW: Emulation freeze fixed when pressing OK repeatedly (by @RebornedBrain)
|
||||
@@ -120,6 +174,8 @@
|
||||
- OFW: Clean up of LFS traces (by @hedger)
|
||||
- OFW: Prevent idle priority threads from potentially starving the FreeRTOS idle task (by @CookiePLMonster)
|
||||
- OFW: Wait for RNG ready state and no errors before sampling (by @n1kolasM)
|
||||
- OFW: A Lot of Fixes (by @skotopes)
|
||||
- OFW: CLI: Add warning about stealth mode in vibro command (by @ivanbarsukov)
|
||||
- OFW: Debug: Use proper hook for handle_exit in flipperapps (by @skotopes)
|
||||
- OFW: API: Fix kerel typo in documentation (by @EntranceJew)
|
||||
|
||||
|
||||
@@ -221,6 +221,14 @@ App(
|
||||
requires=["unit_tests"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="test_js",
|
||||
sources=["tests/common/*.c", "tests/js/*.c"],
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="get_api",
|
||||
requires=["unit_tests", "js_app"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="test_strint",
|
||||
sources=["tests/common/*.c", "tests/strint/*.c"],
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
let tests = require("tests");
|
||||
|
||||
tests.assert_eq(1337, 1337);
|
||||
tests.assert_eq("hello", "hello");
|
||||
@@ -0,0 +1,30 @@
|
||||
let tests = require("tests");
|
||||
let event_loop = require("event_loop");
|
||||
|
||||
let ext = {
|
||||
i: 0,
|
||||
received: false,
|
||||
};
|
||||
|
||||
let queue = event_loop.queue(16);
|
||||
|
||||
event_loop.subscribe(queue.input, function (_, item, tests, ext) {
|
||||
tests.assert_eq(123, item);
|
||||
ext.received = true;
|
||||
}, tests, ext);
|
||||
|
||||
event_loop.subscribe(event_loop.timer("periodic", 1), function (_, _item, queue, counter, ext) {
|
||||
ext.i++;
|
||||
queue.send(123);
|
||||
if (counter === 10)
|
||||
event_loop.stop();
|
||||
return [queue, counter + 1, ext];
|
||||
}, queue, 1, ext);
|
||||
|
||||
event_loop.subscribe(event_loop.timer("oneshot", 1000), function (_, _item, tests) {
|
||||
tests.fail("event loop was not stopped");
|
||||
}, tests);
|
||||
|
||||
event_loop.run();
|
||||
tests.assert_eq(10, ext.i);
|
||||
tests.assert_eq(true, ext.received);
|
||||
@@ -0,0 +1,34 @@
|
||||
let tests = require("tests");
|
||||
let math = require("math");
|
||||
|
||||
// math.EPSILON on Flipper Zero is 2.22044604925031308085e-16
|
||||
|
||||
// basics
|
||||
tests.assert_float_close(5, math.abs(-5), math.EPSILON);
|
||||
tests.assert_float_close(0.5, math.abs(-0.5), math.EPSILON);
|
||||
tests.assert_float_close(5, math.abs(5), math.EPSILON);
|
||||
tests.assert_float_close(0.5, math.abs(0.5), math.EPSILON);
|
||||
tests.assert_float_close(3, math.cbrt(27), math.EPSILON);
|
||||
tests.assert_float_close(6, math.ceil(5.3), math.EPSILON);
|
||||
tests.assert_float_close(31, math.clz32(1), math.EPSILON);
|
||||
tests.assert_float_close(5, math.floor(5.7), math.EPSILON);
|
||||
tests.assert_float_close(5, math.max(3, 5), math.EPSILON);
|
||||
tests.assert_float_close(3, math.min(3, 5), math.EPSILON);
|
||||
tests.assert_float_close(-1, math.sign(-5), math.EPSILON);
|
||||
tests.assert_float_close(5, math.trunc(5.7), math.EPSILON);
|
||||
|
||||
// trig
|
||||
tests.assert_float_close(1.0471975511965976, math.acos(0.5), math.EPSILON);
|
||||
tests.assert_float_close(1.3169578969248166, math.acosh(2), math.EPSILON);
|
||||
tests.assert_float_close(0.5235987755982988, math.asin(0.5), math.EPSILON);
|
||||
tests.assert_float_close(1.4436354751788103, math.asinh(2), math.EPSILON);
|
||||
tests.assert_float_close(0.7853981633974483, math.atan(1), math.EPSILON);
|
||||
tests.assert_float_close(0.7853981633974483, math.atan2(1, 1), math.EPSILON);
|
||||
tests.assert_float_close(0.5493061443340549, math.atanh(0.5), math.EPSILON);
|
||||
tests.assert_float_close(-1, math.cos(math.PI), math.EPSILON * 18); // Error 3.77475828372553223744e-15
|
||||
tests.assert_float_close(1, math.sin(math.PI / 2), math.EPSILON * 4.5); // Error 9.99200722162640886381e-16
|
||||
|
||||
// powers
|
||||
tests.assert_float_close(5, math.sqrt(25), math.EPSILON);
|
||||
tests.assert_float_close(8, math.pow(2, 3), math.EPSILON);
|
||||
tests.assert_float_close(2.718281828459045, math.exp(1), math.EPSILON * 2); // Error 4.44089209850062616169e-16
|
||||
@@ -0,0 +1,136 @@
|
||||
let storage = require("storage");
|
||||
let tests = require("tests");
|
||||
|
||||
let baseDir = "/ext/.tmp/unit_tests";
|
||||
|
||||
tests.assert_eq(true, storage.rmrf(baseDir));
|
||||
tests.assert_eq(true, storage.makeDirectory(baseDir));
|
||||
|
||||
// write
|
||||
let file = storage.openFile(baseDir + "/helloworld", "w", "create_always");
|
||||
tests.assert_eq(true, !!file);
|
||||
tests.assert_eq(true, file.isOpen());
|
||||
tests.assert_eq(13, file.write("Hello, World!"));
|
||||
tests.assert_eq(true, file.close());
|
||||
tests.assert_eq(false, file.isOpen());
|
||||
|
||||
// read
|
||||
file = storage.openFile(baseDir + "/helloworld", "r", "open_existing");
|
||||
tests.assert_eq(true, !!file);
|
||||
tests.assert_eq(true, file.isOpen());
|
||||
tests.assert_eq(13, file.size());
|
||||
tests.assert_eq("Hello, World!", file.read("ascii", 128));
|
||||
tests.assert_eq(true, file.close());
|
||||
tests.assert_eq(false, file.isOpen());
|
||||
|
||||
// seek
|
||||
file = storage.openFile(baseDir + "/helloworld", "r", "open_existing");
|
||||
tests.assert_eq(true, !!file);
|
||||
tests.assert_eq(true, file.isOpen());
|
||||
tests.assert_eq(13, file.size());
|
||||
tests.assert_eq("Hello, World!", file.read("ascii", 128));
|
||||
tests.assert_eq(true, file.seekAbsolute(1));
|
||||
tests.assert_eq(true, file.seekRelative(2));
|
||||
tests.assert_eq(3, file.tell());
|
||||
tests.assert_eq(false, file.eof());
|
||||
tests.assert_eq("lo, World!", file.read("ascii", 128));
|
||||
tests.assert_eq(true, file.eof());
|
||||
tests.assert_eq(true, file.close());
|
||||
tests.assert_eq(false, file.isOpen());
|
||||
|
||||
// byte-level copy
|
||||
let src = storage.openFile(baseDir + "/helloworld", "r", "open_existing");
|
||||
let dst = storage.openFile(baseDir + "/helloworld2", "rw", "create_always");
|
||||
tests.assert_eq(true, !!src);
|
||||
tests.assert_eq(true, src.isOpen());
|
||||
tests.assert_eq(true, !!dst);
|
||||
tests.assert_eq(true, dst.isOpen());
|
||||
tests.assert_eq(true, src.copyTo(dst, 10));
|
||||
tests.assert_eq(true, dst.seekAbsolute(0));
|
||||
tests.assert_eq("Hello, Wor", dst.read("ascii", 128));
|
||||
tests.assert_eq(true, src.copyTo(dst, 3));
|
||||
tests.assert_eq(true, dst.seekAbsolute(0));
|
||||
tests.assert_eq("Hello, World!", dst.read("ascii", 128));
|
||||
tests.assert_eq(true, src.eof());
|
||||
tests.assert_eq(true, src.close());
|
||||
tests.assert_eq(false, src.isOpen());
|
||||
tests.assert_eq(true, dst.eof());
|
||||
tests.assert_eq(true, dst.close());
|
||||
tests.assert_eq(false, dst.isOpen());
|
||||
|
||||
// truncate
|
||||
tests.assert_eq(true, storage.copy(baseDir + "/helloworld", baseDir + "/helloworld2"));
|
||||
file = storage.openFile(baseDir + "/helloworld2", "w", "open_existing");
|
||||
tests.assert_eq(true, !!file);
|
||||
tests.assert_eq(true, file.seekAbsolute(5));
|
||||
tests.assert_eq(true, file.truncate());
|
||||
tests.assert_eq(true, file.close());
|
||||
file = storage.openFile(baseDir + "/helloworld2", "r", "open_existing");
|
||||
tests.assert_eq(true, !!file);
|
||||
tests.assert_eq("Hello", file.read("ascii", 128));
|
||||
tests.assert_eq(true, file.close());
|
||||
|
||||
// existence
|
||||
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld"));
|
||||
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld2"));
|
||||
tests.assert_eq(false, storage.fileExists(baseDir + "/sus_amogus_123"));
|
||||
tests.assert_eq(false, storage.directoryExists(baseDir + "/helloworld"));
|
||||
tests.assert_eq(false, storage.fileExists(baseDir));
|
||||
tests.assert_eq(true, storage.directoryExists(baseDir));
|
||||
tests.assert_eq(true, storage.fileOrDirExists(baseDir));
|
||||
tests.assert_eq(true, storage.remove(baseDir + "/helloworld2"));
|
||||
tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld2"));
|
||||
|
||||
// stat
|
||||
let stat = storage.stat(baseDir + "/helloworld");
|
||||
tests.assert_eq(true, !!stat);
|
||||
tests.assert_eq(baseDir + "/helloworld", stat.path);
|
||||
tests.assert_eq(false, stat.isDirectory);
|
||||
tests.assert_eq(13, stat.size);
|
||||
|
||||
// rename
|
||||
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld"));
|
||||
tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld123"));
|
||||
tests.assert_eq(true, storage.rename(baseDir + "/helloworld", baseDir + "/helloworld123"));
|
||||
tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld"));
|
||||
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld123"));
|
||||
tests.assert_eq(true, storage.rename(baseDir + "/helloworld123", baseDir + "/helloworld"));
|
||||
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld"));
|
||||
tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld123"));
|
||||
|
||||
// copy
|
||||
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld"));
|
||||
tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld123"));
|
||||
tests.assert_eq(true, storage.copy(baseDir + "/helloworld", baseDir + "/helloworld123"));
|
||||
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld"));
|
||||
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld123"));
|
||||
|
||||
// next avail
|
||||
tests.assert_eq("helloworld1", storage.nextAvailableFilename(baseDir, "helloworld", "", 20));
|
||||
|
||||
// fs info
|
||||
let fsInfo = storage.fsInfo("/ext");
|
||||
tests.assert_eq(true, !!fsInfo);
|
||||
tests.assert_eq(true, fsInfo.freeSpace < fsInfo.totalSpace); // idk \(-_-)/
|
||||
fsInfo = storage.fsInfo("/int");
|
||||
tests.assert_eq(true, !!fsInfo);
|
||||
tests.assert_eq(true, fsInfo.freeSpace < fsInfo.totalSpace);
|
||||
|
||||
// path operations
|
||||
tests.assert_eq(true, storage.arePathsEqual("/ext/test", "/ext/Test"));
|
||||
tests.assert_eq(false, storage.arePathsEqual("/ext/test", "/ext/Testttt"));
|
||||
tests.assert_eq(true, storage.isSubpathOf("/ext/test", "/ext/test/sub"));
|
||||
tests.assert_eq(false, storage.isSubpathOf("/ext/test/sub", "/ext/test"));
|
||||
|
||||
// dir
|
||||
let entries = storage.readDirectory(baseDir);
|
||||
tests.assert_eq(true, !!entries);
|
||||
// FIXME: (-nofl) this test suite assumes that files are listed by
|
||||
// `readDirectory` in the exact order that they were created, which is not
|
||||
// something that is actually guaranteed.
|
||||
// Possible solution: sort and compare the array.
|
||||
tests.assert_eq("helloworld", entries[0].path);
|
||||
tests.assert_eq("helloworld123", entries[1].path);
|
||||
|
||||
tests.assert_eq(true, storage.rmrf(baseDir));
|
||||
tests.assert_eq(true, storage.makeDirectory(baseDir));
|
||||
@@ -0,0 +1,88 @@
|
||||
#include "../test.h" // IWYU pragma: keep
|
||||
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
#include <furi_hal_random.h>
|
||||
|
||||
#include <storage/storage.h>
|
||||
#include <applications/system/js_app/js_thread.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define JS_SCRIPT_PATH(name) EXT_PATH("unit_tests/js/" name ".js")
|
||||
|
||||
typedef enum {
|
||||
JsTestsFinished = 1,
|
||||
JsTestsError = 2,
|
||||
} JsTestFlag;
|
||||
|
||||
typedef struct {
|
||||
FuriEventFlag* event_flags;
|
||||
FuriString* error_string;
|
||||
} JsTestCallbackContext;
|
||||
|
||||
static void js_test_callback(JsThreadEvent event, const char* msg, void* param) {
|
||||
JsTestCallbackContext* context = param;
|
||||
if(event == JsThreadEventPrint) {
|
||||
FURI_LOG_I("js_test", "%s", msg);
|
||||
} else if(event == JsThreadEventError || event == JsThreadEventErrorTrace) {
|
||||
context->error_string = furi_string_alloc_set_str(msg);
|
||||
furi_event_flag_set(context->event_flags, JsTestsFinished | JsTestsError);
|
||||
} else if(event == JsThreadEventDone) {
|
||||
furi_event_flag_set(context->event_flags, JsTestsFinished);
|
||||
}
|
||||
}
|
||||
|
||||
static void js_test_run(const char* script_path) {
|
||||
JsTestCallbackContext* context = malloc(sizeof(JsTestCallbackContext));
|
||||
context->event_flags = furi_event_flag_alloc();
|
||||
|
||||
JsThread* thread = js_thread_run(script_path, js_test_callback, context);
|
||||
uint32_t flags = furi_event_flag_wait(
|
||||
context->event_flags, JsTestsFinished, FuriFlagWaitAny, FuriWaitForever);
|
||||
if(flags & FuriFlagError) {
|
||||
// getting the flags themselves should not fail
|
||||
furi_crash();
|
||||
}
|
||||
|
||||
FuriString* error_string = context->error_string;
|
||||
|
||||
js_thread_stop(thread);
|
||||
furi_event_flag_free(context->event_flags);
|
||||
free(context);
|
||||
|
||||
if(flags & JsTestsError) {
|
||||
// memory leak: not freeing the FuriString if the tests fail,
|
||||
// because mu_fail executes a return
|
||||
//
|
||||
// who cares tho?
|
||||
mu_fail(furi_string_get_cstr(error_string));
|
||||
}
|
||||
}
|
||||
|
||||
MU_TEST(js_test_basic) {
|
||||
js_test_run(JS_SCRIPT_PATH("basic"));
|
||||
}
|
||||
MU_TEST(js_test_math) {
|
||||
js_test_run(JS_SCRIPT_PATH("math"));
|
||||
}
|
||||
MU_TEST(js_test_event_loop) {
|
||||
js_test_run(JS_SCRIPT_PATH("event_loop"));
|
||||
}
|
||||
MU_TEST(js_test_storage) {
|
||||
js_test_run(JS_SCRIPT_PATH("storage"));
|
||||
}
|
||||
|
||||
MU_TEST_SUITE(test_js) {
|
||||
MU_RUN_TEST(js_test_basic);
|
||||
MU_RUN_TEST(js_test_math);
|
||||
MU_RUN_TEST(js_test_event_loop);
|
||||
MU_RUN_TEST(js_test_storage);
|
||||
}
|
||||
|
||||
int run_minunit_test_js(void) {
|
||||
MU_RUN_SUITE(test_js);
|
||||
return MU_EXIT_CODE;
|
||||
}
|
||||
|
||||
TEST_API_DEFINE(run_minunit_test_js)
|
||||
@@ -31,7 +31,7 @@ extern "C" {
|
||||
#include <Windows.h>
|
||||
#if defined(_MSC_VER) && _MSC_VER < 1900
|
||||
#define snprintf _snprintf
|
||||
#define __func__ __FUNCTION__
|
||||
#define __func__ __FUNCTION__ //-V1059
|
||||
#endif
|
||||
|
||||
#elif defined(__unix__) || defined(__unix) || defined(unix) || \
|
||||
@@ -56,7 +56,7 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
#if __GNUC__ >= 5 && !defined(__STDC_VERSION__)
|
||||
#define __func__ __extension__ __FUNCTION__
|
||||
#define __func__ __extension__ __FUNCTION__ //-V1059
|
||||
#endif
|
||||
|
||||
#else
|
||||
@@ -102,6 +102,7 @@ void minunit_printf_warning(const char* format, ...);
|
||||
MU__SAFE_BLOCK(minunit_setup = setup_fun; minunit_teardown = teardown_fun;)
|
||||
|
||||
/* Test runner */
|
||||
//-V:MU_RUN_TEST:550
|
||||
#define MU_RUN_TEST(test) \
|
||||
MU__SAFE_BLOCK( \
|
||||
if(minunit_real_timer == 0 && minunit_proc_timer == 0) { \
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#include <rpc/rpc_i.h>
|
||||
#include <flipper.pb.h>
|
||||
#include <core/event_loop.h>
|
||||
#include <applications/system/js_app/js_thread.h>
|
||||
|
||||
static constexpr auto unit_tests_api_table = sort(create_array_t<sym_entry>(
|
||||
API_METHOD(resource_manifest_reader_alloc, ResourceManifestReader*, (Storage*)),
|
||||
@@ -33,13 +33,9 @@ static constexpr auto unit_tests_api_table = sort(create_array_t<sym_entry>(
|
||||
xQueueGenericSend,
|
||||
BaseType_t,
|
||||
(QueueHandle_t, const void* const, TickType_t, const BaseType_t)),
|
||||
API_METHOD(furi_event_loop_alloc, FuriEventLoop*, (void)),
|
||||
API_METHOD(furi_event_loop_free, void, (FuriEventLoop*)),
|
||||
API_METHOD(
|
||||
furi_event_loop_subscribe_message_queue,
|
||||
void,
|
||||
(FuriEventLoop*, FuriMessageQueue*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*)),
|
||||
API_METHOD(furi_event_loop_unsubscribe, void, (FuriEventLoop*, FuriEventLoopObject*)),
|
||||
API_METHOD(furi_event_loop_run, void, (FuriEventLoop*)),
|
||||
API_METHOD(furi_event_loop_stop, void, (FuriEventLoop*)),
|
||||
js_thread_run,
|
||||
JsThread*,
|
||||
(const char* script_path, JsThreadCallback callback, void* context)),
|
||||
API_METHOD(js_thread_stop, void, (JsThread * worker)),
|
||||
API_VARIABLE(PB_Main_msg, PB_Main_msg_t)));
|
||||
|
||||
+1
-1
Submodule applications/external updated: 36671ed586...2d045b7336
@@ -42,7 +42,11 @@ const char* archive_get_flipper_app_name(ArchiveFileTypeEnum file_type) {
|
||||
case ArchiveFileTypeDiskImage:
|
||||
return EXT_PATH("apps/USB/mass_storage.fap");
|
||||
case ArchiveFileTypeJS:
|
||||
#ifdef JS_RUNNER_FAP
|
||||
return EXT_PATH("apps/assets/js_app.fap");
|
||||
#else
|
||||
return "JS Runner";
|
||||
#endif
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
REM This is BadUSB demo script for ChromeOS by kowalski7cc
|
||||
REM This is BadUSB demo script for Chrome and ChromeOS by kowalski7cc
|
||||
|
||||
REM Exit from Overview
|
||||
ESC
|
||||
REM Open a new tab
|
||||
CTRL t
|
||||
REM wait for some slower chromebooks
|
||||
DELAY 1000
|
||||
REM Make sure we have omnibox focus
|
||||
CTRL l
|
||||
DELAY 200
|
||||
REM Open an empty editable page
|
||||
DEFAULT_DELAY 50
|
||||
STRING data:text/html, <html contenteditable autofocus><style>body{font-family:monospace;}
|
||||
STRING data:text/html, <html contenteditable autofocus><title>Flipper Zero BadUSB Demo</title><style>body{font-family:monospace;}
|
||||
ENTER
|
||||
DELAY 500
|
||||
|
||||
|
||||
@@ -5,14 +5,22 @@ REM Check the `lsusb` command to know your own devices IDs
|
||||
|
||||
REM This is BadUSB demo script for Linux/Gnome
|
||||
|
||||
REM Exit from Overview
|
||||
ESC
|
||||
DELAY 200
|
||||
REM Open terminal window
|
||||
DELAY 1000
|
||||
ALT F2
|
||||
DELAY 500
|
||||
STRING gnome-terminal --maximize
|
||||
DELAY 500
|
||||
DELAY 1000
|
||||
REM Let's guess user terminal, based on (almost) glib order with ptyxis now default in Fedora 41
|
||||
STRING sh -c "xdg-terminal-exec||kgx||ptyxis||gnome-terminal||mate-terminal||xfce4-terminal||tilix||konsole||xterm"
|
||||
DELAY 300
|
||||
ENTER
|
||||
REM It can take a bit to open the correct terminal
|
||||
DELAY 1500
|
||||
|
||||
REM Make sure we are running in a POSIX-compliant shell
|
||||
STRING env sh
|
||||
ENTER
|
||||
DELAY 750
|
||||
|
||||
REM Clear the screen in case some banner was displayed
|
||||
STRING clear
|
||||
|
||||
@@ -76,30 +76,6 @@ address: 80 02 20 00
|
||||
command: 70 00 00 00
|
||||
#
|
||||
name: POWER
|
||||
type: parsed
|
||||
protocol: RC6
|
||||
address: 00 00 00 00
|
||||
command: 0C 00 00 00
|
||||
#
|
||||
name: SOURCE
|
||||
type: parsed
|
||||
protocol: RC6
|
||||
address: 00 00 00 00
|
||||
command: 38 00 00 00
|
||||
#
|
||||
name: PLAY
|
||||
type: parsed
|
||||
protocol: RC6
|
||||
address: 00 00 00 00
|
||||
command: 2C 00 00 00
|
||||
#
|
||||
name: STOP
|
||||
type: parsed
|
||||
protocol: RC6
|
||||
address: 00 00 00 00
|
||||
command: 31 00 00 00
|
||||
#
|
||||
name: POWER
|
||||
type: raw
|
||||
frequency: 38000
|
||||
duty_cycle: 0.330000
|
||||
|
||||
@@ -281,6 +281,24 @@ App(
|
||||
sources=["plugins/supported_cards/skylanders.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="hworld_parser",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="hworld_plugin_ep",
|
||||
targets=["f7"],
|
||||
requires=["nfc"],
|
||||
sources=["plugins/supported_cards/hworld.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="trt_parser",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="trt_plugin_ep",
|
||||
targets=["f7"],
|
||||
requires=["nfc"],
|
||||
sources=["plugins/supported_cards/trt.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="sonicare_parser",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
// Flipper Zero parser for H World Hotel Key Cards
|
||||
// H World operates around 10,000 hotels, most of which in mainland China
|
||||
// Reverse engineering and parser written by @Torron (Github: @zinongli)
|
||||
#include "nfc_supported_card_plugin.h"
|
||||
#include <flipper_application.h>
|
||||
#include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
|
||||
#include <bit_lib.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define TAG "H World"
|
||||
#define ROOM_SECTOR 1
|
||||
#define VIP_SECTOR 5
|
||||
#define ROOM_SECTOR_KEY_BLOCK 7
|
||||
#define VIP_SECTOR_KEY_BLOCK 23
|
||||
#define ACCESS_INFO_BLOCK 5
|
||||
#define ROOM_NUM_DECIMAL_BLOCK 6
|
||||
#define H_WORLD_YEAR_OFFSET 2000
|
||||
|
||||
typedef struct {
|
||||
uint64_t a;
|
||||
uint64_t b;
|
||||
} MfClassicKeyPair;
|
||||
|
||||
static MfClassicKeyPair hworld_standard_keys[] = {
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 000
|
||||
{.a = 0x543071543071, .b = 0x5F01015F0101}, // 001
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 002
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 003
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 004
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 005
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 006
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 007
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 008
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 009
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 010
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 011
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 012
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 013
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 014
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 015
|
||||
};
|
||||
|
||||
static MfClassicKeyPair hworld_vip_keys[] = {
|
||||
{.a = 0x000000000000, .b = 0xFFFFFFFFFFFF}, // 000
|
||||
{.a = 0x543071543071, .b = 0x5F01015F0101}, // 001
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 002
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 003
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 004
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 005
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 006
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 007
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 008
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 009
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 010
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 011
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 012
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 013
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 014
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 015
|
||||
};
|
||||
|
||||
static bool hworld_verify(Nfc* nfc) {
|
||||
bool verified = false;
|
||||
|
||||
do {
|
||||
const uint8_t block_num = mf_classic_get_first_block_num_of_sector(ROOM_SECTOR);
|
||||
|
||||
MfClassicKey standard_key = {0};
|
||||
bit_lib_num_to_bytes_be(
|
||||
hworld_standard_keys[ROOM_SECTOR].a, COUNT_OF(standard_key.data), standard_key.data);
|
||||
|
||||
MfClassicAuthContext auth_context;
|
||||
MfClassicError standard_error = mf_classic_poller_sync_auth(
|
||||
nfc, block_num, &standard_key, MfClassicKeyTypeA, &auth_context);
|
||||
|
||||
if(standard_error != MfClassicErrorNone) {
|
||||
FURI_LOG_D(TAG, "Failed static key check for block %u", block_num);
|
||||
break;
|
||||
}
|
||||
|
||||
MfClassicKey vip_key = {0};
|
||||
bit_lib_num_to_bytes_be(
|
||||
hworld_vip_keys[VIP_SECTOR].b, COUNT_OF(vip_key.data), vip_key.data);
|
||||
|
||||
MfClassicError vip_error = mf_classic_poller_sync_auth(
|
||||
nfc, block_num, &vip_key, MfClassicKeyTypeB, &auth_context);
|
||||
|
||||
if(vip_error == MfClassicErrorNone) {
|
||||
FURI_LOG_D(TAG, "VIP card detected");
|
||||
} else {
|
||||
FURI_LOG_D(TAG, "Standard card detected");
|
||||
}
|
||||
|
||||
verified = true;
|
||||
} while(false);
|
||||
|
||||
return verified;
|
||||
}
|
||||
|
||||
static bool hworld_read(Nfc* nfc, NfcDevice* device) {
|
||||
furi_assert(nfc);
|
||||
furi_assert(device);
|
||||
|
||||
bool is_read = false;
|
||||
|
||||
MfClassicData* data = mf_classic_alloc();
|
||||
nfc_device_copy_data(device, NfcProtocolMfClassic, data);
|
||||
|
||||
do {
|
||||
MfClassicType type = MfClassicType1k;
|
||||
MfClassicError standard_error = mf_classic_poller_sync_detect_type(nfc, &type);
|
||||
MfClassicError vip_error = MfClassicErrorNotPresent;
|
||||
if(standard_error != MfClassicErrorNone) break;
|
||||
data->type = type;
|
||||
|
||||
MfClassicDeviceKeys standard_keys = {};
|
||||
for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
|
||||
bit_lib_num_to_bytes_be(
|
||||
hworld_standard_keys[i].a, sizeof(MfClassicKey), standard_keys.key_a[i].data);
|
||||
FURI_BIT_SET(standard_keys.key_a_mask, i);
|
||||
bit_lib_num_to_bytes_be(
|
||||
hworld_standard_keys[i].b, sizeof(MfClassicKey), standard_keys.key_b[i].data);
|
||||
FURI_BIT_SET(standard_keys.key_b_mask, i);
|
||||
}
|
||||
|
||||
standard_error = mf_classic_poller_sync_read(nfc, &standard_keys, data);
|
||||
if(standard_error == MfClassicErrorNone) {
|
||||
FURI_LOG_I(TAG, "Standard card successfully read");
|
||||
} else {
|
||||
MfClassicDeviceKeys vip_keys = {};
|
||||
for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
|
||||
bit_lib_num_to_bytes_be(
|
||||
hworld_vip_keys[i].a, sizeof(MfClassicKey), vip_keys.key_a[i].data);
|
||||
FURI_BIT_SET(vip_keys.key_a_mask, i);
|
||||
bit_lib_num_to_bytes_be(
|
||||
hworld_vip_keys[i].b, sizeof(MfClassicKey), vip_keys.key_b[i].data);
|
||||
FURI_BIT_SET(vip_keys.key_b_mask, i);
|
||||
}
|
||||
|
||||
vip_error = mf_classic_poller_sync_read(nfc, &vip_keys, data);
|
||||
|
||||
if(vip_error == MfClassicErrorNone) {
|
||||
FURI_LOG_I(TAG, "VIP card successfully read");
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
nfc_device_set_data(device, NfcProtocolMfClassic, data);
|
||||
|
||||
is_read = (standard_error == MfClassicErrorNone) | (vip_error == MfClassicErrorNone);
|
||||
} while(false);
|
||||
|
||||
mf_classic_free(data);
|
||||
|
||||
return is_read;
|
||||
}
|
||||
|
||||
bool hworld_parse(const NfcDevice* device, FuriString* parsed_data) {
|
||||
furi_assert(device);
|
||||
const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
|
||||
bool parsed = false;
|
||||
|
||||
do {
|
||||
// Check card type
|
||||
if(data->type != MfClassicType1k) break;
|
||||
|
||||
// Check static key for verificaiton
|
||||
const uint8_t* data_room_sec_key_a_ptr = &data->block[ROOM_SECTOR_KEY_BLOCK].data[0];
|
||||
const uint8_t* data_room_sec_key_b_ptr = &data->block[ROOM_SECTOR_KEY_BLOCK].data[10];
|
||||
uint64_t data_room_sec_key_a = bit_lib_get_bits_64(data_room_sec_key_a_ptr, 0, 48);
|
||||
uint64_t data_room_sec_key_b = bit_lib_get_bits_64(data_room_sec_key_b_ptr, 0, 48);
|
||||
if((data_room_sec_key_a != hworld_standard_keys[ROOM_SECTOR].a) |
|
||||
(data_room_sec_key_b != hworld_standard_keys[ROOM_SECTOR].b))
|
||||
break;
|
||||
|
||||
// Check whether this card is VIP
|
||||
const uint8_t* data_vip_sec_key_b_ptr = &data->block[VIP_SECTOR_KEY_BLOCK].data[10];
|
||||
uint64_t data_vip_sec_key_b = bit_lib_get_bits_64(data_vip_sec_key_b_ptr, 0, 48);
|
||||
bool is_hworld_vip = (data_vip_sec_key_b == hworld_vip_keys[VIP_SECTOR].b);
|
||||
uint8_t room_floor = data->block[ACCESS_INFO_BLOCK].data[13];
|
||||
uint8_t room_num = data->block[ACCESS_INFO_BLOCK].data[14];
|
||||
|
||||
// Check in date & time
|
||||
uint16_t check_in_year = data->block[ACCESS_INFO_BLOCK].data[2] + H_WORLD_YEAR_OFFSET;
|
||||
uint8_t check_in_month = data->block[ACCESS_INFO_BLOCK].data[3];
|
||||
uint8_t check_in_day = data->block[ACCESS_INFO_BLOCK].data[4];
|
||||
uint8_t check_in_hour = data->block[ACCESS_INFO_BLOCK].data[5];
|
||||
uint8_t check_in_minute = data->block[ACCESS_INFO_BLOCK].data[6];
|
||||
|
||||
// Expire date & time
|
||||
uint16_t expire_year = data->block[ACCESS_INFO_BLOCK].data[7] + H_WORLD_YEAR_OFFSET;
|
||||
uint8_t expire_month = data->block[ACCESS_INFO_BLOCK].data[8];
|
||||
uint8_t expire_day = data->block[ACCESS_INFO_BLOCK].data[9];
|
||||
uint8_t expire_hour = data->block[ACCESS_INFO_BLOCK].data[10];
|
||||
uint8_t expire_minute = data->block[ACCESS_INFO_BLOCK].data[11];
|
||||
|
||||
furi_string_cat_printf(parsed_data, "\e#H World Card\n");
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "%s\n", is_hworld_vip ? "VIP card" : "Standard room key");
|
||||
furi_string_cat_printf(parsed_data, "Room Num: %u%02u\n", room_floor, room_num);
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Check-in Date: \n%04u-%02d-%02d\n%02d:%02d:00\n",
|
||||
check_in_year,
|
||||
check_in_month,
|
||||
check_in_day,
|
||||
check_in_hour,
|
||||
check_in_minute);
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Expiration Date: \n%04u-%02d-%02d\n%02d:%02d:00",
|
||||
expire_year,
|
||||
expire_month,
|
||||
expire_day,
|
||||
expire_hour,
|
||||
expire_minute);
|
||||
parsed = true;
|
||||
} while(false);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/* Actual implementation of app<>plugin interface */
|
||||
static const NfcSupportedCardsPlugin hworld_plugin = {
|
||||
.protocol = NfcProtocolMfClassic,
|
||||
.verify = hworld_verify,
|
||||
.read = hworld_read,
|
||||
.parse = hworld_parse,
|
||||
};
|
||||
|
||||
/* Plugin descriptor to comply with basic plugin specification */
|
||||
static const FlipperAppPluginDescriptor hworld_plugin_descriptor = {
|
||||
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
|
||||
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
|
||||
.entry_point = &hworld_plugin,
|
||||
};
|
||||
|
||||
/* Plugin entry point - must return a pointer to const descriptor */
|
||||
const FlipperAppPluginDescriptor* hworld_plugin_ep(void) {
|
||||
return &hworld_plugin_descriptor;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Flipper Zero parser for for Tianjin Railway Transit (TRT)
|
||||
// https://en.wikipedia.org/wiki/Tianjin_Metro
|
||||
// Reverse engineering and parser development by @Torron (Github: @zinongli)
|
||||
|
||||
#include "nfc_supported_card_plugin.h"
|
||||
#include <flipper_application.h>
|
||||
#include <nfc/protocols/mf_ultralight/mf_ultralight.h>
|
||||
#include <bit_lib.h>
|
||||
|
||||
#define TAG "TrtParser"
|
||||
#define LATEST_SALE_MARKER 0x02
|
||||
#define FULL_SALE_TIME_STAMP_PAGE 0x09
|
||||
#define BALANCE_PAGE 0x08
|
||||
#define SALE_RECORD_TIME_STAMP_A 0x0C
|
||||
#define SALE_RECORD_TIME_STAMP_B 0x0E
|
||||
#define SALE_YEAR_OFFSET 2000
|
||||
|
||||
static bool trt_parse(const NfcDevice* device, FuriString* parsed_data) {
|
||||
furi_assert(device);
|
||||
furi_assert(parsed_data);
|
||||
|
||||
const MfUltralightData* data = nfc_device_get_data(device, NfcProtocolMfUltralight);
|
||||
|
||||
bool parsed = false;
|
||||
|
||||
do {
|
||||
uint8_t latest_sale_page = 0;
|
||||
|
||||
// Look for sale record signature
|
||||
if(data->page[SALE_RECORD_TIME_STAMP_A].data[0] == LATEST_SALE_MARKER) {
|
||||
latest_sale_page = SALE_RECORD_TIME_STAMP_A;
|
||||
} else if(data->page[SALE_RECORD_TIME_STAMP_B].data[0] == LATEST_SALE_MARKER) {
|
||||
latest_sale_page = SALE_RECORD_TIME_STAMP_B;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if the sale record was backed up
|
||||
const uint8_t* partial_record_pointer = &data->page[latest_sale_page - 1].data[0];
|
||||
const uint8_t* full_record_pointer = &data->page[FULL_SALE_TIME_STAMP_PAGE].data[0];
|
||||
uint32_t latest_sale_record = bit_lib_get_bits_32(partial_record_pointer, 3, 20);
|
||||
uint32_t latest_sale_full_record = bit_lib_get_bits_32(full_record_pointer, 0, 27);
|
||||
if(latest_sale_record != (latest_sale_full_record & 0xFFFFF)) break;
|
||||
|
||||
// Parse date
|
||||
// yyy yyyymmmm dddddhhh hhnnnnnn
|
||||
uint16_t sale_year = ((latest_sale_full_record & 0x7F00000) >> 20) + SALE_YEAR_OFFSET;
|
||||
uint8_t sale_month = (latest_sale_full_record & 0xF0000) >> 16;
|
||||
uint8_t sale_day = (latest_sale_full_record & 0xF800) >> 11;
|
||||
uint8_t sale_hour = (latest_sale_full_record & 0x7C0) >> 6;
|
||||
uint8_t sale_minute = latest_sale_full_record & 0x3F;
|
||||
|
||||
// Parse balance
|
||||
uint16_t balance = bit_lib_get_bits_16(&data->page[BALANCE_PAGE].data[2], 0, 16);
|
||||
uint16_t balance_yuan = balance / 100;
|
||||
uint8_t balance_cent = balance % 100;
|
||||
|
||||
// Format string for rendering
|
||||
furi_string_cat_printf(parsed_data, "\e#TRT Tianjin Metro\n");
|
||||
furi_string_cat_printf(parsed_data, "Single-Use Ticket\n");
|
||||
furi_string_cat_printf(parsed_data, "Balance: %u.%02u RMB\n", balance_yuan, balance_cent);
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Sale Date: \n%04u-%02d-%02d %02d:%02d",
|
||||
sale_year,
|
||||
sale_month,
|
||||
sale_day,
|
||||
sale_hour,
|
||||
sale_minute);
|
||||
parsed = true;
|
||||
} while(false);
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/* Actual implementation of app<>plugin interface */
|
||||
static const NfcSupportedCardsPlugin trt_plugin = {
|
||||
.protocol = NfcProtocolMfUltralight,
|
||||
.verify = NULL,
|
||||
.read = NULL,
|
||||
.parse = trt_parse,
|
||||
};
|
||||
|
||||
/* Plugin descriptor to comply with basic plugin specification */
|
||||
static const FlipperAppPluginDescriptor trt_plugin_descriptor = {
|
||||
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
|
||||
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
|
||||
.entry_point = &trt_plugin,
|
||||
};
|
||||
|
||||
/* Plugin entry point - must return a pointer to const descriptor */
|
||||
const FlipperAppPluginDescriptor* trt_plugin_ep(void) {
|
||||
return &trt_plugin_descriptor;
|
||||
}
|
||||
@@ -2321,6 +2321,10 @@ D881B675D881
|
||||
# Volgograd (Russia) Volna transport cards keys
|
||||
2B787A063D5D
|
||||
D37C8F1793F7
|
||||
# H World Hotel Chain Room Keys
|
||||
543071543071
|
||||
5F01015F0101
|
||||
200510241234
|
||||
# +----------------------------------------------------------------------------------------------------------------------------+
|
||||
# | https://github.com/DarkFlippers/unleashed-firmware/blob/dev/applications/main/nfc/resources/nfc/assets/mf_classic_dict.nfc |
|
||||
# +----------------------------------------------------------------------------------------------------------------------------+
|
||||
@@ -2400,6 +2404,8 @@ FE98F38F3EE2
|
||||
555D8BBC2D3E
|
||||
78DF1176C8FD
|
||||
ADC169F922CB
|
||||
# iq aparts hotel
|
||||
505209266A1F
|
||||
# +------------------------------------------------------------------------------------------------------------------------+
|
||||
# | https://github.com/Flipper-XFW/Xtreme-Firmware/blob/dev/applications/main/nfc/resources/nfc/assets/mf_classic_dict.nfc |
|
||||
# +------------------------------------------------------------------------------------------------------------------------+
|
||||
|
||||
@@ -998,13 +998,12 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) {
|
||||
chat_event = subghz_chat_worker_get_event_chat(subghz_chat);
|
||||
switch(chat_event.event) {
|
||||
case SubGhzChatEventInputData:
|
||||
if(chat_event.c == CliSymbolAsciiETX) {
|
||||
if(chat_event.c == CliKeyETX) {
|
||||
printf("\r\n");
|
||||
chat_event.event = SubGhzChatEventUserExit;
|
||||
subghz_chat_worker_put_event_chat(subghz_chat, &chat_event);
|
||||
break;
|
||||
} else if(
|
||||
(chat_event.c == CliSymbolAsciiBackspace) || (chat_event.c == CliSymbolAsciiDel)) {
|
||||
} else if((chat_event.c == CliKeyBackspace) || (chat_event.c == CliKeyDEL)) {
|
||||
size_t len = furi_string_utf8_length(input);
|
||||
if(len > furi_string_utf8_length(name)) {
|
||||
printf("%s", "\e[D\e[1P");
|
||||
@@ -1026,7 +1025,7 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) {
|
||||
}
|
||||
furi_string_set(input, sysmsg);
|
||||
}
|
||||
} else if(chat_event.c == CliSymbolAsciiCR) {
|
||||
} else if(chat_event.c == CliKeyCR) {
|
||||
printf("\r\n");
|
||||
furi_string_push_back(input, '\r');
|
||||
furi_string_push_back(input, '\n');
|
||||
@@ -1040,7 +1039,7 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) {
|
||||
furi_string_printf(input, "%s", furi_string_get_cstr(name));
|
||||
printf("%s", furi_string_get_cstr(input));
|
||||
fflush(stdout);
|
||||
} else if(chat_event.c == CliSymbolAsciiLF) {
|
||||
} else if(chat_event.c == CliKeyLF) {
|
||||
//cut out the symbol \n
|
||||
} else {
|
||||
putc(chat_event.c, stdout);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <cli/cli.h>
|
||||
#include <cli/cli_ansi.h>
|
||||
|
||||
void subghz_on_system_start(void);
|
||||
|
||||
@@ -445,13 +445,11 @@ static void bt_change_profile(Bt* bt, BtMessage* message) {
|
||||
*message->profile_instance = NULL;
|
||||
}
|
||||
}
|
||||
if(message->lock) api_lock_unlock(message->lock);
|
||||
}
|
||||
|
||||
static void bt_close_connection(Bt* bt, BtMessage* message) {
|
||||
static void bt_close_connection(Bt* bt) {
|
||||
bt_close_rpc_connection(bt);
|
||||
furi_hal_bt_stop_advertising();
|
||||
if(message->lock) api_lock_unlock(message->lock);
|
||||
}
|
||||
|
||||
static void bt_apply_settings(Bt* bt) {
|
||||
@@ -499,19 +497,13 @@ static void bt_load_settings(Bt* bt) {
|
||||
}
|
||||
|
||||
static void bt_handle_get_settings(Bt* bt, BtMessage* message) {
|
||||
furi_assert(message->lock);
|
||||
*message->data.settings = bt->bt_settings;
|
||||
api_lock_unlock(message->lock);
|
||||
}
|
||||
|
||||
static void bt_handle_set_settings(Bt* bt, BtMessage* message) {
|
||||
furi_assert(message->lock);
|
||||
bt->bt_settings = *message->data.csettings;
|
||||
|
||||
bt_apply_settings(bt);
|
||||
bt_settings_save(&bt->bt_settings);
|
||||
|
||||
api_lock_unlock(message->lock);
|
||||
}
|
||||
|
||||
static void bt_handle_reload_keys_settings(Bt* bt) {
|
||||
@@ -576,6 +568,12 @@ int32_t bt_srv(void* p) {
|
||||
while(1) {
|
||||
furi_check(
|
||||
furi_message_queue_get(bt->message_queue, &message, FuriWaitForever) == FuriStatusOk);
|
||||
FURI_LOG_D(
|
||||
TAG,
|
||||
"call %d, lock 0x%p, result 0x%p",
|
||||
message.type,
|
||||
(void*)message.lock,
|
||||
(void*)message.result);
|
||||
if(message.type == BtMessageTypeUpdateStatus) {
|
||||
// Update view ports
|
||||
bt_statusbar_update(bt);
|
||||
@@ -599,7 +597,7 @@ int32_t bt_srv(void* p) {
|
||||
} else if(message.type == BtMessageTypeSetProfile) {
|
||||
bt_change_profile(bt, &message);
|
||||
} else if(message.type == BtMessageTypeDisconnect) {
|
||||
bt_close_connection(bt, &message);
|
||||
bt_close_connection(bt);
|
||||
} else if(message.type == BtMessageTypeForgetBondedDevices) {
|
||||
bt_keys_storage_delete(bt->keys_storage);
|
||||
} else if(message.type == BtMessageTypeGetSettings) {
|
||||
@@ -609,6 +607,8 @@ int32_t bt_srv(void* p) {
|
||||
} else if(message.type == BtMessageTypeReloadKeysSettings) {
|
||||
bt_handle_reload_keys_settings(bt);
|
||||
}
|
||||
|
||||
if(message.lock) api_lock_unlock(message.lock);
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
+142
-44
@@ -1,6 +1,7 @@
|
||||
#include "cli_i.h"
|
||||
#include "cli_commands.h"
|
||||
#include "cli_vcp.h"
|
||||
#include "cli_ansi.h"
|
||||
#include <furi_hal_version.h>
|
||||
#include <loader/loader.h>
|
||||
|
||||
@@ -11,6 +12,8 @@
|
||||
#define TAG "CliSrv"
|
||||
|
||||
#define CLI_INPUT_LEN_LIMIT 256
|
||||
#define CLI_PROMPT ">: " // qFlipper does not recognize us if we use escape sequences :(
|
||||
#define CLI_PROMPT_LENGTH 3 // printable characters
|
||||
|
||||
Cli* cli_alloc(void) {
|
||||
Cli* cli = malloc(sizeof(Cli));
|
||||
@@ -89,7 +92,7 @@ bool cli_cmd_interrupt_received(Cli* cli) {
|
||||
char c = '\0';
|
||||
if(cli_is_connected(cli)) {
|
||||
if(cli->session->rx((uint8_t*)&c, 1, 0) == 1) {
|
||||
return c == CliSymbolAsciiETX;
|
||||
return c == CliKeyETX;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
@@ -146,7 +149,7 @@ void cli_nl(Cli* cli) {
|
||||
|
||||
void cli_prompt(Cli* cli) {
|
||||
UNUSED(cli);
|
||||
printf("\r\n>: %s", furi_string_get_cstr(cli->line));
|
||||
printf("\r\n" CLI_PROMPT "%s", furi_string_get_cstr(cli->line));
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
@@ -169,7 +172,7 @@ static void cli_handle_backspace(Cli* cli) {
|
||||
|
||||
cli->cursor_position--;
|
||||
} else {
|
||||
cli_putc(cli, CliSymbolAsciiBell);
|
||||
cli_putc(cli, CliKeyBell);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +248,7 @@ static void cli_handle_enter(Cli* cli) {
|
||||
printf(
|
||||
"`%s` command not found, use `help` or `?` to list all available commands",
|
||||
furi_string_get_cstr(command));
|
||||
cli_putc(cli, CliSymbolAsciiBell);
|
||||
cli_putc(cli, CliKeyBell);
|
||||
}
|
||||
|
||||
cli_reset(cli);
|
||||
@@ -309,8 +312,85 @@ static void cli_handle_autocomplete(Cli* cli) {
|
||||
cli_prompt(cli);
|
||||
}
|
||||
|
||||
static void cli_handle_escape(Cli* cli, char c) {
|
||||
if(c == 'A') {
|
||||
typedef enum {
|
||||
CliCharClassWord,
|
||||
CliCharClassSpace,
|
||||
CliCharClassOther,
|
||||
} CliCharClass;
|
||||
|
||||
/**
|
||||
* @brief Determines the class that a character belongs to
|
||||
*
|
||||
* The return value of this function should not be used on its own; it should
|
||||
* only be used for comparing it with other values returned by this function.
|
||||
* This function is used internally in `cli_skip_run`.
|
||||
*/
|
||||
static CliCharClass cli_char_class(char c) {
|
||||
if((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') {
|
||||
return CliCharClassWord;
|
||||
} else if(c == ' ') {
|
||||
return CliCharClassSpace;
|
||||
} else {
|
||||
return CliCharClassOther;
|
||||
}
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
CliSkipDirectionLeft,
|
||||
CliSkipDirectionRight,
|
||||
} CliSkipDirection;
|
||||
|
||||
/**
|
||||
* @brief Skips a run of a class of characters
|
||||
*
|
||||
* @param string Input string
|
||||
* @param original_pos Position to start the search at
|
||||
* @param direction Direction in which to perform the search
|
||||
* @returns The position at which the run ends
|
||||
*/
|
||||
static size_t cli_skip_run(FuriString* string, size_t original_pos, CliSkipDirection direction) {
|
||||
if(furi_string_size(string) == 0) return original_pos;
|
||||
if(direction == CliSkipDirectionLeft && original_pos == 0) return original_pos;
|
||||
if(direction == CliSkipDirectionRight && original_pos == furi_string_size(string))
|
||||
return original_pos;
|
||||
|
||||
int8_t look_offset = (direction == CliSkipDirectionLeft) ? -1 : 0;
|
||||
int8_t increment = (direction == CliSkipDirectionLeft) ? -1 : 1;
|
||||
int32_t position = original_pos;
|
||||
CliCharClass start_class =
|
||||
cli_char_class(furi_string_get_char(string, position + look_offset));
|
||||
|
||||
while(true) {
|
||||
position += increment;
|
||||
if(position < 0) break;
|
||||
if(position >= (int32_t)furi_string_size(string)) break;
|
||||
if(cli_char_class(furi_string_get_char(string, position + look_offset)) != start_class)
|
||||
break;
|
||||
}
|
||||
|
||||
return MAX(0, position);
|
||||
}
|
||||
|
||||
void cli_process_input(Cli* cli) {
|
||||
CliKeyCombo combo = cli_read_ansi_key_combo(cli);
|
||||
FURI_LOG_T(TAG, "code=0x%02x, mod=0x%x\r\n", combo.key, combo.modifiers);
|
||||
|
||||
if(combo.key == CliKeyTab) {
|
||||
cli_handle_autocomplete(cli);
|
||||
|
||||
} else if(combo.key == CliKeySOH) {
|
||||
furi_delay_ms(33); // We are too fast, Minicom is not ready yet
|
||||
cli_motd();
|
||||
cli_prompt(cli);
|
||||
|
||||
} else if(combo.key == CliKeyETX) {
|
||||
cli_reset(cli);
|
||||
cli_prompt(cli);
|
||||
|
||||
} else if(combo.key == CliKeyEOT) {
|
||||
cli_reset(cli);
|
||||
|
||||
} else if(combo.key == CliKeyUp && combo.modifiers == CliModKeyNo) {
|
||||
// Use previous command if line buffer is empty
|
||||
if(furi_string_size(cli->line) == 0 && furi_string_cmp(cli->line, cli->last_line) != 0) {
|
||||
// Set line buffer and cursor position
|
||||
@@ -319,67 +399,85 @@ static void cli_handle_escape(Cli* cli, char c) {
|
||||
// Show new line to user
|
||||
printf("%s", furi_string_get_cstr(cli->line));
|
||||
}
|
||||
} else if(c == 'B') {
|
||||
} else if(c == 'C') {
|
||||
|
||||
} else if(combo.key == CliKeyDown && combo.modifiers == CliModKeyNo) {
|
||||
// Clear input buffer
|
||||
furi_string_reset(cli->line);
|
||||
cli->cursor_position = 0;
|
||||
printf("\r" CLI_PROMPT "\e[0K");
|
||||
|
||||
} else if(combo.key == CliKeyRight && combo.modifiers == CliModKeyNo) {
|
||||
// Move right
|
||||
if(cli->cursor_position < furi_string_size(cli->line)) {
|
||||
cli->cursor_position++;
|
||||
printf("\e[C");
|
||||
}
|
||||
} else if(c == 'D') {
|
||||
|
||||
} else if(combo.key == CliKeyLeft && combo.modifiers == CliModKeyNo) {
|
||||
// Move left
|
||||
if(cli->cursor_position > 0) {
|
||||
cli->cursor_position--;
|
||||
printf("\e[D");
|
||||
}
|
||||
}
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
void cli_process_input(Cli* cli) {
|
||||
char in_chr = cli_getc(cli);
|
||||
size_t rx_len;
|
||||
} else if(combo.key == CliKeyHome && combo.modifiers == CliModKeyNo) {
|
||||
// Move to beginning of line
|
||||
cli->cursor_position = 0;
|
||||
printf("\e[%uG", CLI_PROMPT_LENGTH + 1); // columns start at 1 \(-_-)/
|
||||
|
||||
} else if(combo.key == CliKeyEnd && combo.modifiers == CliModKeyNo) {
|
||||
// Move to end of line
|
||||
cli->cursor_position = furi_string_size(cli->line);
|
||||
printf("\e[%zuG", CLI_PROMPT_LENGTH + cli->cursor_position + 1);
|
||||
|
||||
if(in_chr == CliSymbolAsciiTab) {
|
||||
cli_handle_autocomplete(cli);
|
||||
} else if(in_chr == CliSymbolAsciiSOH) {
|
||||
furi_delay_ms(33); // We are too fast, Minicom is not ready yet
|
||||
cli_motd();
|
||||
cli_prompt(cli);
|
||||
} else if(in_chr == CliSymbolAsciiETX) {
|
||||
cli_reset(cli);
|
||||
cli_prompt(cli);
|
||||
} else if(in_chr == CliSymbolAsciiEOT) {
|
||||
cli_reset(cli);
|
||||
} else if(in_chr == CliSymbolAsciiEsc) {
|
||||
rx_len = cli_read(cli, (uint8_t*)&in_chr, 1);
|
||||
if((rx_len > 0) && (in_chr == '[')) {
|
||||
cli_read(cli, (uint8_t*)&in_chr, 1);
|
||||
cli_handle_escape(cli, in_chr);
|
||||
} else {
|
||||
cli_putc(cli, CliSymbolAsciiBell);
|
||||
}
|
||||
} else if(in_chr == CliSymbolAsciiBackspace || in_chr == CliSymbolAsciiDel) {
|
||||
cli_handle_backspace(cli);
|
||||
} else if(in_chr == CliSymbolAsciiCR) {
|
||||
cli_handle_enter(cli);
|
||||
} else if(
|
||||
(in_chr >= 0x20 && in_chr < 0x7F) && //-V560
|
||||
combo.modifiers == CliModKeyCtrl &&
|
||||
(combo.key == CliKeyLeft || combo.key == CliKeyRight)) {
|
||||
// Skip run of similar chars to the left or right
|
||||
CliSkipDirection direction = (combo.key == CliKeyLeft) ? CliSkipDirectionLeft :
|
||||
CliSkipDirectionRight;
|
||||
cli->cursor_position = cli_skip_run(cli->line, cli->cursor_position, direction);
|
||||
printf("\e[%zuG", CLI_PROMPT_LENGTH + cli->cursor_position + 1);
|
||||
|
||||
} else if(combo.key == CliKeyBackspace || combo.key == CliKeyDEL) {
|
||||
cli_handle_backspace(cli);
|
||||
|
||||
} else if(combo.key == CliKeyETB) { // Ctrl + Backspace
|
||||
// Delete run of similar chars to the left
|
||||
size_t run_start = cli_skip_run(cli->line, cli->cursor_position, CliSkipDirectionLeft);
|
||||
furi_string_replace_at(cli->line, run_start, cli->cursor_position - run_start, "");
|
||||
cli->cursor_position = run_start;
|
||||
printf(
|
||||
"\e[%zuG%s\e[0K\e[%zuG", // move cursor, print second half of line, erase remains, move cursor again
|
||||
CLI_PROMPT_LENGTH + cli->cursor_position + 1,
|
||||
furi_string_get_cstr(cli->line) + run_start,
|
||||
CLI_PROMPT_LENGTH + run_start + 1);
|
||||
|
||||
} else if(combo.key == CliKeyCR) {
|
||||
cli_handle_enter(cli);
|
||||
|
||||
} else if(
|
||||
(combo.key >= 0x20 && combo.key < 0x7F) && //-V560
|
||||
(furi_string_size(cli->line) < CLI_INPUT_LEN_LIMIT)) {
|
||||
if(cli->cursor_position == furi_string_size(cli->line)) {
|
||||
furi_string_push_back(cli->line, in_chr);
|
||||
cli_putc(cli, in_chr);
|
||||
furi_string_push_back(cli->line, combo.key);
|
||||
cli_putc(cli, combo.key);
|
||||
} else {
|
||||
// Insert character to line buffer
|
||||
const char in_str[2] = {in_chr, 0};
|
||||
const char in_str[2] = {combo.key, 0};
|
||||
furi_string_replace_at(cli->line, cli->cursor_position, 0, in_str);
|
||||
|
||||
// Print character in replace mode
|
||||
printf("\e[4h%c\e[4l", in_chr);
|
||||
printf("\e[4h%c\e[4l", combo.key);
|
||||
fflush(stdout);
|
||||
}
|
||||
cli->cursor_position++;
|
||||
|
||||
} else {
|
||||
cli_putc(cli, CliSymbolAsciiBell);
|
||||
cli_putc(cli, CliKeyBell);
|
||||
}
|
||||
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
void cli_add_command(
|
||||
|
||||
@@ -10,26 +10,12 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
CliSymbolAsciiSOH = 0x01,
|
||||
CliSymbolAsciiETX = 0x03,
|
||||
CliSymbolAsciiEOT = 0x04,
|
||||
CliSymbolAsciiBell = 0x07,
|
||||
CliSymbolAsciiBackspace = 0x08,
|
||||
CliSymbolAsciiTab = 0x09,
|
||||
CliSymbolAsciiLF = 0x0A,
|
||||
CliSymbolAsciiCR = 0x0D,
|
||||
CliSymbolAsciiEsc = 0x1B,
|
||||
CliSymbolAsciiUS = 0x1F,
|
||||
CliSymbolAsciiSpace = 0x20,
|
||||
CliSymbolAsciiDel = 0x7F,
|
||||
} CliSymbols;
|
||||
|
||||
typedef enum {
|
||||
CliCommandFlagDefault = 0, /**< Default, loader lock is used */
|
||||
CliCommandFlagParallelSafe =
|
||||
(1 << 0), /**< Safe to run in parallel with other apps, loader lock is not used */
|
||||
CliCommandFlagInsomniaSafe = (1 << 1), /**< Safe to run with insomnia mode on */
|
||||
CliCommandFlagHidden = (1 << 2), /**< Not shown in `help` */
|
||||
} CliCommandFlag;
|
||||
|
||||
#define RECORD_CLI "cli"
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#include "cli_ansi.h"
|
||||
|
||||
/**
|
||||
* @brief Converts a single character representing a special key into the enum
|
||||
* representation
|
||||
*/
|
||||
static CliKey cli_ansi_key_from_mnemonic(char c) {
|
||||
switch(c) {
|
||||
case 'A':
|
||||
return CliKeyUp;
|
||||
case 'B':
|
||||
return CliKeyDown;
|
||||
case 'C':
|
||||
return CliKeyRight;
|
||||
case 'D':
|
||||
return CliKeyLeft;
|
||||
case 'F':
|
||||
return CliKeyEnd;
|
||||
case 'H':
|
||||
return CliKeyHome;
|
||||
default:
|
||||
return CliKeyUnrecognized;
|
||||
}
|
||||
}
|
||||
|
||||
CliKeyCombo cli_read_ansi_key_combo(Cli* cli) {
|
||||
char ch = cli_getc(cli);
|
||||
|
||||
if(ch != CliKeyEsc)
|
||||
return (CliKeyCombo){
|
||||
.modifiers = CliModKeyNo,
|
||||
.key = ch,
|
||||
};
|
||||
|
||||
ch = cli_getc(cli);
|
||||
|
||||
// ESC ESC -> ESC
|
||||
if(ch == '\e')
|
||||
return (CliKeyCombo){
|
||||
.modifiers = CliModKeyNo,
|
||||
.key = '\e',
|
||||
};
|
||||
|
||||
// ESC <char> -> Alt + <char>
|
||||
if(ch != '[')
|
||||
return (CliKeyCombo){
|
||||
.modifiers = CliModKeyAlt,
|
||||
.key = cli_getc(cli),
|
||||
};
|
||||
|
||||
ch = cli_getc(cli);
|
||||
|
||||
// ESC [ 1
|
||||
if(ch == '1') {
|
||||
// ESC [ 1 ; <modifier bitfield> <key mnemonic>
|
||||
if(cli_getc(cli) == ';') {
|
||||
CliModKey modifiers = (cli_getc(cli) - '0'); // convert following digit to a number
|
||||
modifiers &= ~1;
|
||||
return (CliKeyCombo){
|
||||
.modifiers = modifiers,
|
||||
.key = cli_ansi_key_from_mnemonic(cli_getc(cli)),
|
||||
};
|
||||
}
|
||||
|
||||
return (CliKeyCombo){
|
||||
.modifiers = CliModKeyNo,
|
||||
.key = CliKeyUnrecognized,
|
||||
};
|
||||
}
|
||||
|
||||
// ESC [ <key mnemonic>
|
||||
return (CliKeyCombo){
|
||||
.modifiers = CliModKeyNo,
|
||||
.key = cli_ansi_key_from_mnemonic(ch),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#include "cli.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define ANSI_RESET "\e[0m"
|
||||
#define ANSI_BOLD "\e[1m"
|
||||
#define ANSI_FAINT "\e[2m"
|
||||
|
||||
#define ANSI_FG_BLACK "\e[30m"
|
||||
#define ANSI_FG_RED "\e[31m"
|
||||
#define ANSI_FG_GREEN "\e[32m"
|
||||
#define ANSI_FG_YELLOW "\e[33m"
|
||||
#define ANSI_FG_BLUE "\e[34m"
|
||||
#define ANSI_FG_MAGENTA "\e[35m"
|
||||
#define ANSI_FG_CYAN "\e[36m"
|
||||
#define ANSI_FG_WHITE "\e[37m"
|
||||
#define ANSI_FG_BR_BLACK "\e[90m"
|
||||
#define ANSI_FG_BR_RED "\e[91m"
|
||||
#define ANSI_FG_BR_GREEN "\e[92m"
|
||||
#define ANSI_FG_BR_YELLOW "\e[93m"
|
||||
#define ANSI_FG_BR_BLUE "\e[94m"
|
||||
#define ANSI_FG_BR_MAGENTA "\e[95m"
|
||||
#define ANSI_FG_BR_CYAN "\e[96m"
|
||||
#define ANSI_FG_BR_WHITE "\e[97m"
|
||||
|
||||
#define ANSI_BG_BLACK "\e[40m"
|
||||
#define ANSI_BG_RED "\e[41m"
|
||||
#define ANSI_BG_GREEN "\e[42m"
|
||||
#define ANSI_BG_YELLOW "\e[43m"
|
||||
#define ANSI_BG_BLUE "\e[44m"
|
||||
#define ANSI_BG_MAGENTA "\e[45m"
|
||||
#define ANSI_BG_CYAN "\e[46m"
|
||||
#define ANSI_BG_WHITE "\e[47m"
|
||||
#define ANSI_BG_BR_BLACK "\e[100m"
|
||||
#define ANSI_BG_BR_RED "\e[101m"
|
||||
#define ANSI_BG_BR_GREEN "\e[102m"
|
||||
#define ANSI_BG_BR_YELLOW "\e[103m"
|
||||
#define ANSI_BG_BR_BLUE "\e[104m"
|
||||
#define ANSI_BG_BR_MAGENTA "\e[105m"
|
||||
#define ANSI_BG_BR_CYAN "\e[106m"
|
||||
#define ANSI_BG_BR_WHITE "\e[107m"
|
||||
|
||||
#define ANSI_FLIPPER_BRAND_ORANGE "\e[38;2;255;130;0m"
|
||||
|
||||
typedef enum {
|
||||
CliKeyUnrecognized = 0,
|
||||
|
||||
CliKeySOH = 0x01,
|
||||
CliKeyETX = 0x03,
|
||||
CliKeyEOT = 0x04,
|
||||
CliKeyBell = 0x07,
|
||||
CliKeyBackspace = 0x08,
|
||||
CliKeyTab = 0x09,
|
||||
CliKeyLF = 0x0A,
|
||||
CliKeyCR = 0x0D,
|
||||
CliKeyETB = 0x17,
|
||||
CliKeyEsc = 0x1B,
|
||||
CliKeyUS = 0x1F,
|
||||
CliKeySpace = 0x20,
|
||||
CliKeyDEL = 0x7F,
|
||||
|
||||
CliKeySpecial = 0x80,
|
||||
CliKeyLeft,
|
||||
CliKeyRight,
|
||||
CliKeyUp,
|
||||
CliKeyDown,
|
||||
CliKeyHome,
|
||||
CliKeyEnd,
|
||||
} CliKey;
|
||||
|
||||
typedef enum {
|
||||
CliModKeyNo = 0,
|
||||
CliModKeyAlt = 2,
|
||||
CliModKeyCtrl = 4,
|
||||
CliModKeyMeta = 8,
|
||||
} CliModKey;
|
||||
|
||||
typedef struct {
|
||||
CliModKey modifiers;
|
||||
CliKey key;
|
||||
} CliKeyCombo;
|
||||
|
||||
/**
|
||||
* @brief Reads a key or key combination
|
||||
*/
|
||||
CliKeyCombo cli_read_ansi_key_combo(Cli* cli);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "cli_commands.h"
|
||||
#include "cli_command_gpio.h"
|
||||
#include "cli_ansi.h"
|
||||
|
||||
#include <core/thread.h>
|
||||
#include <furi_hal.h>
|
||||
@@ -7,9 +8,11 @@
|
||||
#include <task_control_block.h>
|
||||
#include <time.h>
|
||||
#include <notification/notification_messages.h>
|
||||
#include <notification/notification_app.h>
|
||||
#include <loader/loader.h>
|
||||
#include <lib/toolbox/args.h>
|
||||
#include <lib/toolbox/strint.h>
|
||||
#include <storage/storage.h>
|
||||
|
||||
// Close to ISO, `date +'%Y-%m-%d %H:%M:%S %u'`
|
||||
#define CLI_DATE_FORMAT "%.4d-%.2d-%.2d %.2d:%.2d:%.2d %d"
|
||||
@@ -52,35 +55,194 @@ void cli_command_info(Cli* cli, FuriString* args, void* context) {
|
||||
}
|
||||
}
|
||||
|
||||
void cli_command_help(Cli* cli, FuriString* args, void* context) {
|
||||
// Lil Easter egg :>
|
||||
void cli_command_neofetch(Cli* cli, FuriString* args, void* context) {
|
||||
UNUSED(cli);
|
||||
UNUSED(args);
|
||||
UNUSED(context);
|
||||
|
||||
static const char* const neofetch_logo[] = {
|
||||
" _.-------.._ -,",
|
||||
" .-\"```\"--..,,_/ /`-, -, \\ ",
|
||||
" .:\" /:/ /'\\ \\ ,_..., `. | |",
|
||||
" / ,----/:/ /`\\ _\\~`_-\"` _;",
|
||||
" ' / /`\"\"\"'\\ \\ \\.~`_-' ,-\"'/ ",
|
||||
" | | | 0 | | .-' ,/` /",
|
||||
" | ,..\\ \\ ,.-\"` ,/` /",
|
||||
"; : `/`\"\"\\` ,/--==,/-----,",
|
||||
"| `-...| -.___-Z:_______J...---;",
|
||||
": ` _-'",
|
||||
};
|
||||
#define NEOFETCH_COLOR ANSI_FLIPPER_BRAND_ORANGE
|
||||
|
||||
// Determine logo parameters
|
||||
size_t logo_height = COUNT_OF(neofetch_logo), logo_width = 0;
|
||||
for(size_t i = 0; i < logo_height; i++)
|
||||
logo_width = MAX(logo_width, strlen(neofetch_logo[i]));
|
||||
logo_width += 4; // space between logo and info
|
||||
|
||||
// Format hostname delimiter
|
||||
const size_t size_of_hostname = 4 + strlen(furi_hal_version_get_name_ptr());
|
||||
char delimiter[64];
|
||||
memset(delimiter, '-', size_of_hostname);
|
||||
delimiter[size_of_hostname] = '\0';
|
||||
|
||||
// Get heap info
|
||||
size_t heap_total = memmgr_get_total_heap();
|
||||
size_t heap_used = heap_total - memmgr_get_free_heap();
|
||||
uint16_t heap_percent = (100 * heap_used) / heap_total;
|
||||
|
||||
// Get storage info
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
uint64_t ext_total, ext_free, ext_used, ext_percent;
|
||||
storage_common_fs_info(storage, "/ext", &ext_total, &ext_free);
|
||||
ext_used = ext_total - ext_free;
|
||||
ext_percent = (100 * ext_used) / ext_total;
|
||||
ext_used /= 1024 * 1024;
|
||||
ext_total /= 1024 * 1024;
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
// Get battery info
|
||||
uint16_t charge_percent = furi_hal_power_get_pct();
|
||||
const char* charge_state;
|
||||
if(furi_hal_power_is_charging()) {
|
||||
if((charge_percent < 100) && (!furi_hal_power_is_charging_done())) {
|
||||
charge_state = "charging";
|
||||
} else {
|
||||
charge_state = "charged";
|
||||
}
|
||||
} else {
|
||||
charge_state = "discharging";
|
||||
}
|
||||
|
||||
// Get misc info
|
||||
uint32_t uptime = furi_get_tick() / furi_kernel_get_tick_frequency();
|
||||
const Version* version = version_get();
|
||||
uint16_t major, minor;
|
||||
furi_hal_info_get_api_version(&major, &minor);
|
||||
|
||||
// Print ASCII art with info
|
||||
const size_t info_height = 16;
|
||||
for(size_t i = 0; i < MAX(logo_height, info_height); i++) {
|
||||
printf(NEOFETCH_COLOR "%-*s", logo_width, (i < logo_height) ? neofetch_logo[i] : "");
|
||||
switch(i) {
|
||||
case 0: // you@<hostname>
|
||||
printf("you" ANSI_RESET "@" NEOFETCH_COLOR "%s", furi_hal_version_get_name_ptr());
|
||||
break;
|
||||
case 1: // delimiter
|
||||
printf(ANSI_RESET "%s", delimiter);
|
||||
break;
|
||||
case 2: // OS: FURI <edition> <branch> <version> <commit> (SDK <maj>.<min>)
|
||||
printf(
|
||||
"OS" ANSI_RESET ": FURI %s %s %s %s (SDK %hu.%hu)",
|
||||
version_get_version(version),
|
||||
version_get_gitbranch(version),
|
||||
version_get_version(version),
|
||||
version_get_githash(version),
|
||||
major,
|
||||
minor);
|
||||
break;
|
||||
case 3: // Host: <model> <hostname>
|
||||
printf(
|
||||
"Host" ANSI_RESET ": %s %s",
|
||||
furi_hal_version_get_model_code(),
|
||||
furi_hal_version_get_device_name_ptr());
|
||||
break;
|
||||
case 4: // Kernel: FreeRTOS <maj>.<min>.<build>
|
||||
printf(
|
||||
"Kernel" ANSI_RESET ": FreeRTOS %d.%d.%d",
|
||||
tskKERNEL_VERSION_MAJOR,
|
||||
tskKERNEL_VERSION_MINOR,
|
||||
tskKERNEL_VERSION_BUILD);
|
||||
break;
|
||||
case 5: // Uptime: ?h?m?s
|
||||
printf(
|
||||
"Uptime" ANSI_RESET ": %luh%lum%lus",
|
||||
uptime / 60 / 60,
|
||||
uptime / 60 % 60,
|
||||
uptime % 60);
|
||||
break;
|
||||
case 6: // ST7567 128x64 @ 1 bpp in 1.4"
|
||||
printf("Display" ANSI_RESET ": ST7567 128x64 @ 1 bpp in 1.4\"");
|
||||
break;
|
||||
case 7: // DE: GuiSrv
|
||||
printf("DE" ANSI_RESET ": GuiSrv");
|
||||
break;
|
||||
case 8: // Shell: CliSrv
|
||||
printf("Shell" ANSI_RESET ": CliSrv");
|
||||
break;
|
||||
case 9: // CPU: STM32WB55RG @ 64 MHz
|
||||
printf("CPU" ANSI_RESET ": STM32WB55RG @ 64 MHz");
|
||||
break;
|
||||
case 10: // Memory: <used> / <total> B (??%)
|
||||
printf(
|
||||
"Memory" ANSI_RESET ": %zu / %zu B (%hu%%)", heap_used, heap_total, heap_percent);
|
||||
break;
|
||||
case 11: // Disk (/ext): <used> / <total> MiB (??%)
|
||||
printf(
|
||||
"Disk (/ext)" ANSI_RESET ": %llu / %llu MiB (%llu%%)",
|
||||
ext_used,
|
||||
ext_total,
|
||||
ext_percent);
|
||||
break;
|
||||
case 12: // Battery: ??% (<state>)
|
||||
printf("Battery" ANSI_RESET ": %hu%% (%s)" ANSI_RESET, charge_percent, charge_state);
|
||||
break;
|
||||
case 13: // empty space
|
||||
break;
|
||||
case 14: // Colors (line 1)
|
||||
for(size_t j = 30; j <= 37; j++)
|
||||
printf("\e[%dm███", j);
|
||||
break;
|
||||
case 15: // Colors (line 2)
|
||||
for(size_t j = 90; j <= 97; j++)
|
||||
printf("\e[%dm███", j);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
printf("\r\n");
|
||||
}
|
||||
printf(ANSI_RESET);
|
||||
#undef NEOFETCH_COLOR
|
||||
}
|
||||
|
||||
void cli_command_help(Cli* cli, FuriString* args, void* context) {
|
||||
UNUSED(context);
|
||||
printf("Commands available:");
|
||||
|
||||
// Command count
|
||||
const size_t commands_count = CliCommandTree_size(cli->commands);
|
||||
const size_t commands_count_mid = commands_count / 2 + commands_count % 2;
|
||||
// Count non-hidden commands
|
||||
CliCommandTree_it_t it_count;
|
||||
CliCommandTree_it(it_count, cli->commands);
|
||||
size_t commands_count = 0;
|
||||
while(!CliCommandTree_end_p(it_count)) {
|
||||
if(!(CliCommandTree_cref(it_count)->value_ptr->flags & CliCommandFlagHidden))
|
||||
commands_count++;
|
||||
CliCommandTree_next(it_count);
|
||||
}
|
||||
|
||||
// Use 2 iterators from start and middle to show 2 columns
|
||||
CliCommandTree_it_t it_left;
|
||||
CliCommandTree_it(it_left, cli->commands);
|
||||
CliCommandTree_it_t it_right;
|
||||
CliCommandTree_it(it_right, cli->commands);
|
||||
for(size_t i = 0; i < commands_count_mid; i++)
|
||||
CliCommandTree_next(it_right);
|
||||
// Create iterators starting at different positions
|
||||
const size_t columns = 3;
|
||||
const size_t commands_per_column = (commands_count / columns) + (commands_count % columns);
|
||||
CliCommandTree_it_t iterators[columns];
|
||||
for(size_t c = 0; c < columns; c++) {
|
||||
CliCommandTree_it(iterators[c], cli->commands);
|
||||
for(size_t i = 0; i < c * commands_per_column; i++)
|
||||
CliCommandTree_next(iterators[c]);
|
||||
}
|
||||
|
||||
// Iterate throw tree
|
||||
for(size_t i = 0; i < commands_count_mid; i++) {
|
||||
// Print commands
|
||||
for(size_t r = 0; r < commands_per_column; r++) {
|
||||
printf("\r\n");
|
||||
// Left Column
|
||||
if(!CliCommandTree_end_p(it_left)) {
|
||||
printf("%-30s", furi_string_get_cstr(*CliCommandTree_ref(it_left)->key_ptr));
|
||||
CliCommandTree_next(it_left);
|
||||
}
|
||||
// Right Column
|
||||
if(!CliCommandTree_end_p(it_right)) {
|
||||
printf("%s", furi_string_get_cstr(*CliCommandTree_ref(it_right)->key_ptr));
|
||||
CliCommandTree_next(it_right);
|
||||
|
||||
for(size_t c = 0; c < columns; c++) {
|
||||
if(!CliCommandTree_end_p(iterators[c])) {
|
||||
const CliCommandTree_itref_t* item = CliCommandTree_cref(iterators[c]);
|
||||
if(!(item->value_ptr->flags & CliCommandFlagHidden)) {
|
||||
printf("%-30s", furi_string_get_cstr(*item->key_ptr));
|
||||
}
|
||||
CliCommandTree_next(iterators[c]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,13 +488,24 @@ void cli_command_sysctl(Cli* cli, FuriString* args, void* context) {
|
||||
void cli_command_vibro(Cli* cli, FuriString* args, void* context) {
|
||||
UNUSED(cli);
|
||||
UNUSED(context);
|
||||
|
||||
if(!furi_string_cmp(args, "0")) {
|
||||
NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
|
||||
notification_message_block(notification, &sequence_reset_vibro);
|
||||
furi_record_close(RECORD_NOTIFICATION);
|
||||
} else if(!furi_string_cmp(args, "1")) {
|
||||
if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagStealthMode)) {
|
||||
printf("Flipper is in stealth mode. Unmute the device to control vibration.");
|
||||
return;
|
||||
}
|
||||
|
||||
NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
|
||||
notification_message_block(notification, &sequence_set_vibro_on);
|
||||
if(notification->settings.vibro_on) {
|
||||
notification_message_block(notification, &sequence_set_vibro_on);
|
||||
} else {
|
||||
printf("Vibro is disabled in settings. Enable it to control vibration.");
|
||||
}
|
||||
|
||||
furi_record_close(RECORD_NOTIFICATION);
|
||||
} else {
|
||||
cli_print_usage("vibro", "<1|0>", furi_string_get_cstr(args));
|
||||
@@ -401,16 +574,18 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) {
|
||||
int interval = 1000;
|
||||
args_read_int_and_trim(args, &interval);
|
||||
|
||||
if(interval) printf("\e[2J\e[?25l"); // Clear display, hide cursor
|
||||
|
||||
FuriThreadList* thread_list = furi_thread_list_alloc();
|
||||
while(!cli_cmd_interrupt_received(cli)) {
|
||||
uint32_t tick = furi_get_tick();
|
||||
furi_thread_enumerate(thread_list);
|
||||
|
||||
if(interval) printf("\e[2J\e[0;0f"); // Clear display and return to 0
|
||||
if(interval) printf("\e[0;0f"); // Return to 0,0
|
||||
|
||||
uint32_t uptime = tick / furi_kernel_get_tick_frequency();
|
||||
printf(
|
||||
"Threads: %zu, ISR Time: %0.2f%%, Uptime: %luh%lum%lus\r\n",
|
||||
"\rThreads: %zu, ISR Time: %0.2f%%, Uptime: %luh%lum%lus\e[0K\r\n",
|
||||
furi_thread_list_size(thread_list),
|
||||
(double)furi_thread_list_get_isr_time(thread_list),
|
||||
uptime / 60 / 60,
|
||||
@@ -418,14 +593,14 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) {
|
||||
uptime % 60);
|
||||
|
||||
printf(
|
||||
"Heap: total %zu, free %zu, minimum %zu, max block %zu\r\n\r\n",
|
||||
"\rHeap: total %zu, free %zu, minimum %zu, max block %zu\e[0K\r\n\r\n",
|
||||
memmgr_get_total_heap(),
|
||||
memmgr_get_free_heap(),
|
||||
memmgr_get_minimum_free_heap(),
|
||||
memmgr_heap_get_max_free_block());
|
||||
|
||||
printf(
|
||||
"%-17s %-20s %-10s %5s %12s %6s %10s %7s %5s\r\n",
|
||||
"\r%-17s %-20s %-10s %5s %12s %6s %10s %7s %5s\e[0K\r\n",
|
||||
"AppID",
|
||||
"Name",
|
||||
"State",
|
||||
@@ -439,7 +614,7 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) {
|
||||
for(size_t i = 0; i < furi_thread_list_size(thread_list); i++) {
|
||||
const FuriThreadListItem* item = furi_thread_list_get_at(thread_list, i);
|
||||
printf(
|
||||
"%-17s %-20s %-10s %5d 0x%08lx %6lu %10lu %7zu %5.1f\r\n",
|
||||
"\r%-17s %-20s %-10s %5d 0x%08lx %6lu %10lu %7zu %5.1f\e[0K\r\n",
|
||||
item->app_id,
|
||||
item->name,
|
||||
item->state,
|
||||
@@ -458,6 +633,8 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) {
|
||||
}
|
||||
}
|
||||
furi_thread_list_free(thread_list);
|
||||
|
||||
if(interval) printf("\e[?25h"); // Show cursor
|
||||
}
|
||||
|
||||
void cli_command_free(Cli* cli, FuriString* args, void* context) {
|
||||
@@ -509,8 +686,14 @@ void cli_commands_init(Cli* cli) {
|
||||
cli_add_command(cli, "!", CliCommandFlagParallelSafe, cli_command_info, (void*)true);
|
||||
cli_add_command(cli, "info", CliCommandFlagParallelSafe, cli_command_info, NULL);
|
||||
cli_add_command(cli, "device_info", CliCommandFlagParallelSafe, cli_command_info, (void*)true);
|
||||
cli_add_command(cli, "src", CliCommandFlagParallelSafe, cli_command_src, NULL);
|
||||
cli_add_command(cli, "source", CliCommandFlagParallelSafe, cli_command_src, NULL);
|
||||
cli_add_command(cli, "src", CliCommandFlagParallelSafe, cli_command_src, NULL);
|
||||
cli_add_command(
|
||||
cli,
|
||||
"neofetch",
|
||||
CliCommandFlagParallelSafe | CliCommandFlagHidden,
|
||||
cli_command_neofetch,
|
||||
NULL);
|
||||
|
||||
cli_add_command(cli, "?", CliCommandFlagParallelSafe, cli_command_help, NULL);
|
||||
cli_add_command(cli, "help", CliCommandFlagParallelSafe, cli_command_help, NULL);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <lib/toolbox/args.h>
|
||||
#include <cli/cli.h>
|
||||
#include <cli/cli_ansi.h>
|
||||
|
||||
void crypto_cli_print_usage(void) {
|
||||
printf("Usage:\r\n");
|
||||
@@ -45,14 +46,14 @@ void crypto_cli_encrypt(Cli* cli, FuriString* args) {
|
||||
input = furi_string_alloc();
|
||||
char c;
|
||||
while(cli_read(cli, (uint8_t*)&c, 1) == 1) {
|
||||
if(c == CliSymbolAsciiETX) {
|
||||
if(c == CliKeyETX) {
|
||||
printf("\r\n");
|
||||
break;
|
||||
} else if(c >= 0x20 && c < 0x7F) {
|
||||
putc(c, stdout);
|
||||
fflush(stdout);
|
||||
furi_string_push_back(input, c);
|
||||
} else if(c == CliSymbolAsciiCR) {
|
||||
} else if(c == CliKeyCR) {
|
||||
printf("\r\n");
|
||||
furi_string_cat(input, "\r\n");
|
||||
}
|
||||
@@ -120,14 +121,14 @@ void crypto_cli_decrypt(Cli* cli, FuriString* args) {
|
||||
hex_input = furi_string_alloc();
|
||||
char c;
|
||||
while(cli_read(cli, (uint8_t*)&c, 1) == 1) {
|
||||
if(c == CliSymbolAsciiETX) {
|
||||
if(c == CliKeyETX) {
|
||||
printf("\r\n");
|
||||
break;
|
||||
} else if(c >= 0x20 && c < 0x7F) {
|
||||
putc(c, stdout);
|
||||
fflush(stdout);
|
||||
furi_string_push_back(hex_input, c);
|
||||
} else if(c == CliSymbolAsciiCR) {
|
||||
} else if(c == CliKeyCR) {
|
||||
printf("\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,11 +54,14 @@ bool dialogs_app_process_module_file_browser(const DialogsAppMessageDataFileBrow
|
||||
ret = file_browser_context->result;
|
||||
|
||||
view_holder_set_view(view_holder, NULL);
|
||||
view_holder_free(view_holder);
|
||||
file_browser_stop(file_browser);
|
||||
|
||||
file_browser_free(file_browser);
|
||||
view_holder_free(view_holder);
|
||||
|
||||
api_lock_free(file_browser_context->lock);
|
||||
free(file_browser_context);
|
||||
|
||||
furi_record_close(RECORD_GUI);
|
||||
|
||||
return ret;
|
||||
|
||||
@@ -556,12 +556,10 @@ void canvas_draw_xbm(
|
||||
size_t height,
|
||||
const uint8_t* bitmap) {
|
||||
furi_check(canvas);
|
||||
x += canvas->offset_x;
|
||||
y += canvas->offset_y;
|
||||
canvas_draw_u8g2_bitmap(&canvas->fb, x, y, width, height, bitmap, IconRotation0);
|
||||
canvas_draw_xbm_ex(canvas, x, y, width, height, IconRotation0, bitmap);
|
||||
}
|
||||
|
||||
void canvas_draw_xbm_custom(
|
||||
void canvas_draw_xbm_ex(
|
||||
Canvas* canvas,
|
||||
int32_t x,
|
||||
int32_t y,
|
||||
|
||||
@@ -298,16 +298,15 @@ void canvas_draw_xbm(
|
||||
|
||||
/** Draw rotated XBM bitmap
|
||||
*
|
||||
* @param canvas Canvas instance
|
||||
* @param x x coordinate
|
||||
* @param y y coordinate
|
||||
* @param[in] width bitmap width
|
||||
* @param[in] height bitmap height
|
||||
* @param[in] rotation bitmap rotation
|
||||
* @param bitmap pointer to XBM bitmap data
|
||||
* @param canvas Canvas instance
|
||||
* @param x x coordinate
|
||||
* @param y y coordinate
|
||||
* @param[in] width bitmap width
|
||||
* @param[in] height bitmap height
|
||||
* @param[in] rotation bitmap rotation
|
||||
* @param bitmap_data pointer to XBM bitmap data
|
||||
*/
|
||||
|
||||
void canvas_draw_xbm_custom(
|
||||
void canvas_draw_xbm_ex(
|
||||
Canvas* canvas,
|
||||
int32_t x,
|
||||
int32_t y,
|
||||
|
||||
@@ -24,6 +24,7 @@ typedef struct {
|
||||
const char* header;
|
||||
char* text_buffer;
|
||||
size_t text_buffer_size;
|
||||
size_t minimum_length;
|
||||
bool clear_default_text;
|
||||
|
||||
TextInputCallback callback;
|
||||
@@ -37,7 +38,6 @@ typedef struct {
|
||||
FuriString* validator_text;
|
||||
bool validator_message_visible;
|
||||
|
||||
size_t minimum_length;
|
||||
char extra_symbols[9];
|
||||
bool cursor_select;
|
||||
size_t cursor_pos;
|
||||
|
||||
@@ -65,13 +65,18 @@ void text_input_set_result_callback(
|
||||
size_t text_buffer_size,
|
||||
bool clear_default_text);
|
||||
|
||||
/**
|
||||
* @brief Sets the minimum length of a TextInput
|
||||
* @param [in] text_input TextInput
|
||||
* @param [in] minimum_length Minimum input length
|
||||
*/
|
||||
void text_input_set_minimum_length(TextInput* text_input, size_t minimum_length);
|
||||
|
||||
void text_input_set_validator(
|
||||
TextInput* text_input,
|
||||
TextInputValidatorCallback callback,
|
||||
void* callback_context);
|
||||
|
||||
void text_input_set_minimum_length(TextInput* text_input, size_t minimum_length);
|
||||
|
||||
// Add up to 9 extra characters for symbol keyboard
|
||||
void text_input_add_extra_symbol(TextInput* text_input, char symbol);
|
||||
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
#define VIEW_DISPATCHER_QUEUE_LEN (16U)
|
||||
|
||||
ViewDispatcher* view_dispatcher_alloc(void) {
|
||||
ViewDispatcher* dispatcher = view_dispatcher_alloc_ex(furi_event_loop_alloc());
|
||||
dispatcher->is_event_loop_owned = true;
|
||||
return dispatcher;
|
||||
}
|
||||
|
||||
ViewDispatcher* view_dispatcher_alloc_ex(FuriEventLoop* loop) {
|
||||
ViewDispatcher* view_dispatcher = malloc(sizeof(ViewDispatcher));
|
||||
|
||||
view_dispatcher->view_port = view_port_alloc();
|
||||
@@ -18,7 +24,7 @@ ViewDispatcher* view_dispatcher_alloc(void) {
|
||||
|
||||
ViewDict_init(view_dispatcher->views);
|
||||
|
||||
view_dispatcher->event_loop = furi_event_loop_alloc();
|
||||
view_dispatcher->event_loop = loop;
|
||||
|
||||
view_dispatcher->input_queue =
|
||||
furi_message_queue_alloc(VIEW_DISPATCHER_QUEUE_LEN, sizeof(InputEvent));
|
||||
@@ -70,7 +76,7 @@ void view_dispatcher_free(ViewDispatcher* view_dispatcher) {
|
||||
furi_message_queue_free(view_dispatcher->ascii_queue);
|
||||
furi_message_queue_free(view_dispatcher->event_queue);
|
||||
|
||||
furi_event_loop_free(view_dispatcher->event_loop);
|
||||
if(view_dispatcher->is_event_loop_owned) furi_event_loop_free(view_dispatcher->event_loop);
|
||||
// Free dispatcher
|
||||
free(view_dispatcher);
|
||||
}
|
||||
@@ -98,6 +104,7 @@ void view_dispatcher_set_tick_event_callback(
|
||||
ViewDispatcherTickEventCallback callback,
|
||||
uint32_t tick_period) {
|
||||
furi_check(view_dispatcher);
|
||||
furi_check(view_dispatcher->is_event_loop_owned);
|
||||
view_dispatcher->tick_event_callback = callback;
|
||||
view_dispatcher->tick_period = tick_period;
|
||||
}
|
||||
@@ -119,11 +126,12 @@ void view_dispatcher_run(ViewDispatcher* view_dispatcher) {
|
||||
uint32_t tick_period = view_dispatcher->tick_period == 0 ? FuriWaitForever :
|
||||
view_dispatcher->tick_period;
|
||||
|
||||
furi_event_loop_tick_set(
|
||||
view_dispatcher->event_loop,
|
||||
tick_period,
|
||||
view_dispatcher_handle_tick_event,
|
||||
view_dispatcher);
|
||||
if(view_dispatcher->is_event_loop_owned)
|
||||
furi_event_loop_tick_set(
|
||||
view_dispatcher->event_loop,
|
||||
tick_period,
|
||||
view_dispatcher_handle_tick_event,
|
||||
view_dispatcher);
|
||||
|
||||
furi_event_loop_run(view_dispatcher->event_loop);
|
||||
|
||||
|
||||
@@ -47,6 +47,15 @@ typedef void (*ViewDispatcherTickEventCallback)(void* context);
|
||||
*/
|
||||
ViewDispatcher* view_dispatcher_alloc(void);
|
||||
|
||||
/** Allocate ViewDispatcher instance with an externally owned event loop. If
|
||||
* this constructor is used instead of `view_dispatcher_alloc`, the burden of
|
||||
* freeing the event loop is placed on the caller.
|
||||
*
|
||||
* @param loop pointer to FuriEventLoop instance
|
||||
* @return pointer to ViewDispatcher instance
|
||||
*/
|
||||
ViewDispatcher* view_dispatcher_alloc_ex(FuriEventLoop* loop);
|
||||
|
||||
/** Free ViewDispatcher instance
|
||||
*
|
||||
* @warning All added views MUST be removed using view_dispatcher_remove_view()
|
||||
@@ -97,6 +106,10 @@ void view_dispatcher_set_navigation_event_callback(
|
||||
|
||||
/** Set tick event handler
|
||||
*
|
||||
* @warning Requires the event loop to be owned by the view dispatcher, i.e.
|
||||
* it should have been instantiated with `view_dispatcher_alloc`, not
|
||||
* `view_dispatcher_alloc_ex`.
|
||||
*
|
||||
* @param view_dispatcher ViewDispatcher instance
|
||||
* @param callback ViewDispatcherTickEventCallback
|
||||
* @param tick_period callback call period
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
DICT_DEF2(ViewDict, uint32_t, M_DEFAULT_OPLIST, View*, M_PTR_OPLIST) // NOLINT
|
||||
|
||||
struct ViewDispatcher {
|
||||
bool is_event_loop_owned;
|
||||
FuriEventLoop* event_loop;
|
||||
FuriMessageQueue* input_queue;
|
||||
FuriMessageQueue* event_queue;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <furi.h>
|
||||
#include <cli/cli.h>
|
||||
#include <cli/cli_ansi.h>
|
||||
#include <toolbox/args.h>
|
||||
|
||||
static void input_cli_usage(void) {
|
||||
@@ -73,12 +74,12 @@ static void input_cli_keyboard(Cli* cli, FuriString* args, FuriPubSub* event_pub
|
||||
FuriPubSub* ascii_pubsub = furi_record_open(RECORD_ASCII_EVENTS);
|
||||
while(cli_is_connected(cli)) {
|
||||
char in_chr = cli_getc(cli);
|
||||
if(in_chr == CliSymbolAsciiETX) break;
|
||||
if(in_chr == CliKeyETX) break;
|
||||
InputKey send_key = InputKeyMAX;
|
||||
uint8_t send_ascii = AsciiValueNUL;
|
||||
|
||||
switch(in_chr) {
|
||||
case CliSymbolAsciiEsc: // Escape code for arrows
|
||||
case CliKeyEsc: // Escape code for arrows
|
||||
if(!cli_read(cli, (uint8_t*)&in_chr, 1) || in_chr != '[') break;
|
||||
if(!cli_read(cli, (uint8_t*)&in_chr, 1)) break;
|
||||
if(in_chr >= 'A' && in_chr <= 'D') { // Arrows = Dpad
|
||||
@@ -89,8 +90,8 @@ static void input_cli_keyboard(Cli* cli, FuriString* args, FuriPubSub* event_pub
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CliSymbolAsciiBackspace: // (minicom) Backspace = Back
|
||||
case CliSymbolAsciiDel: // (putty/picocom) Backspace = Back
|
||||
case CliKeyBackspace: // (minicom) Backspace = Back
|
||||
case CliKeyDEL: // (putty/picocom) Backspace = Back
|
||||
if(hold) {
|
||||
send_key = InputKeyBack;
|
||||
} else {
|
||||
@@ -104,14 +105,14 @@ static void input_cli_keyboard(Cli* cli, FuriString* args, FuriPubSub* event_pub
|
||||
send_ascii = AsciiValueESC;
|
||||
}
|
||||
break;
|
||||
case CliSymbolAsciiCR: // Enter = Ok
|
||||
case CliKeyCR: // Enter = Ok
|
||||
if(hold) {
|
||||
send_key = InputKeyOk;
|
||||
} else {
|
||||
send_ascii = AsciiValueCR;
|
||||
}
|
||||
break;
|
||||
case CliSymbolAsciiSpace: // Space = Toggle hold next key
|
||||
case CliKeySpace: // Space = Toggle hold next key
|
||||
if(hold) {
|
||||
send_ascii = ' ';
|
||||
} else {
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
|
||||
#define TAG "LoaderApplications"
|
||||
|
||||
#ifdef JS_RUNNER_FAP
|
||||
#define JS_RUNNER_APP EXT_PATH("apps/assets/js_app.fap")
|
||||
#else
|
||||
#define JS_RUNNER_APP "JS Runner"
|
||||
#endif
|
||||
|
||||
struct LoaderApplications {
|
||||
FuriThread* thread;
|
||||
|
||||
@@ -69,7 +69,7 @@ static RpcSystemCallbacks rpc_systems[] = {
|
||||
struct RpcSession {
|
||||
Rpc* rpc;
|
||||
|
||||
FuriThreadId thread_id;
|
||||
FuriThread* thread;
|
||||
|
||||
RpcHandlerDict_t handlers;
|
||||
FuriStreamBuffer* stream;
|
||||
@@ -175,7 +175,7 @@ size_t rpc_session_feed(
|
||||
|
||||
size_t bytes_sent = furi_stream_buffer_send(session->stream, encoded_bytes, size, timeout);
|
||||
|
||||
furi_thread_flags_set(session->thread_id, RpcEvtNewData);
|
||||
furi_thread_flags_set(furi_thread_get_id(session->thread), RpcEvtNewData);
|
||||
|
||||
return bytes_sent;
|
||||
}
|
||||
@@ -223,7 +223,7 @@ bool rpc_pb_stream_read(pb_istream_t* istream, pb_byte_t* buf, size_t count) {
|
||||
break;
|
||||
} else {
|
||||
/* Save disconnect flag and continue reading buffer */
|
||||
furi_thread_flags_set(session->thread_id, RpcEvtDisconnect);
|
||||
furi_thread_flags_set(furi_thread_get_id(session->thread), RpcEvtDisconnect);
|
||||
}
|
||||
} else if(flags & RpcEvtNewData) {
|
||||
// Just wake thread up
|
||||
@@ -350,32 +350,37 @@ static int32_t rpc_session_worker(void* context) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void rpc_session_thread_release_callback(
|
||||
FuriThread* thread,
|
||||
FuriThreadState thread_state,
|
||||
void* context) {
|
||||
if(thread_state == FuriThreadStateStopped) {
|
||||
RpcSession* session = (RpcSession*)context;
|
||||
static void rpc_session_thread_pending_callback(void* context, uint32_t arg) {
|
||||
UNUSED(arg);
|
||||
RpcSession* session = (RpcSession*)context;
|
||||
|
||||
for(size_t i = 0; i < COUNT_OF(rpc_systems); ++i) {
|
||||
if(rpc_systems[i].free) {
|
||||
(rpc_systems[i].free)(session->system_contexts[i]);
|
||||
}
|
||||
for(size_t i = 0; i < COUNT_OF(rpc_systems); ++i) {
|
||||
if(rpc_systems[i].free) {
|
||||
(rpc_systems[i].free)(session->system_contexts[i]);
|
||||
}
|
||||
free(session->system_contexts);
|
||||
free(session->decoded_message);
|
||||
RpcHandlerDict_clear(session->handlers);
|
||||
furi_stream_buffer_free(session->stream);
|
||||
}
|
||||
free(session->system_contexts);
|
||||
free(session->decoded_message);
|
||||
RpcHandlerDict_clear(session->handlers);
|
||||
furi_stream_buffer_free(session->stream);
|
||||
|
||||
furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever);
|
||||
if(session->terminated_callback) {
|
||||
session->terminated_callback(session->context);
|
||||
}
|
||||
furi_mutex_release(session->callbacks_mutex);
|
||||
furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever);
|
||||
if(session->terminated_callback) {
|
||||
session->terminated_callback(session->context);
|
||||
}
|
||||
furi_mutex_release(session->callbacks_mutex);
|
||||
|
||||
furi_mutex_free(session->callbacks_mutex);
|
||||
furi_thread_free(thread);
|
||||
free(session);
|
||||
furi_mutex_free(session->callbacks_mutex);
|
||||
furi_thread_join(session->thread);
|
||||
furi_thread_free(session->thread);
|
||||
free(session);
|
||||
}
|
||||
|
||||
static void
|
||||
rpc_session_thread_state_callback(FuriThread* thread, FuriThreadState state, void* context) {
|
||||
UNUSED(thread);
|
||||
if(state == FuriThreadStateStopped) {
|
||||
furi_timer_pending_callback(rpc_session_thread_pending_callback, context, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,14 +416,12 @@ RpcSession* rpc_session_open(Rpc* rpc, RpcOwner owner) {
|
||||
};
|
||||
rpc_add_handler(session, PB_Main_stop_session_tag, &rpc_handler);
|
||||
|
||||
FuriThread* thread =
|
||||
furi_thread_alloc_ex("RpcSessionWorker", 3072, rpc_session_worker, session);
|
||||
session->thread_id = furi_thread_get_id(thread);
|
||||
session->thread = furi_thread_alloc_ex("RpcSessionWorker", 3072, rpc_session_worker, session);
|
||||
|
||||
furi_thread_set_state_context(thread, session);
|
||||
furi_thread_set_state_callback(thread, rpc_session_thread_release_callback);
|
||||
furi_thread_set_state_context(session->thread, session);
|
||||
furi_thread_set_state_callback(session->thread, rpc_session_thread_state_callback);
|
||||
|
||||
furi_thread_start(thread);
|
||||
furi_thread_start(session->thread);
|
||||
|
||||
rpc->sessions_count++;
|
||||
|
||||
@@ -434,7 +437,7 @@ void rpc_session_close(RpcSession* session) {
|
||||
rpc_session_set_send_bytes_callback(session, NULL);
|
||||
rpc_session_set_close_callback(session, NULL);
|
||||
rpc_session_set_buffer_is_empty_callback(session, NULL);
|
||||
furi_thread_flags_set(session->thread_id, RpcEvtDisconnect);
|
||||
furi_thread_flags_set(furi_thread_get_id(session->thread), RpcEvtDisconnect);
|
||||
}
|
||||
|
||||
void rpc_on_system_start(void* p) {
|
||||
|
||||
@@ -404,7 +404,7 @@ void storage_common_resolve_path_and_ensure_app_directory(Storage* storage, Furi
|
||||
* @param storage pointer to a storage API instance.
|
||||
* @param source pointer to a zero-terminated string containing the source path.
|
||||
* @param dest pointer to a zero-terminated string containing the destination path.
|
||||
* @return FSE_OK if the migration was successfull completed, any other error code on failure.
|
||||
* @return FSE_OK if the migration was successfully completed, any other error code on failure.
|
||||
*/
|
||||
FS_Error storage_common_migrate(Storage* storage, const char* source, const char* dest);
|
||||
|
||||
@@ -452,7 +452,7 @@ bool storage_common_is_subdir(Storage* storage, const char* parent, const char*
|
||||
/******************* Error Functions *******************/
|
||||
|
||||
/**
|
||||
* @brief Get the textual description of a numeric error identifer.
|
||||
* @brief Get the textual description of a numeric error identifier.
|
||||
*
|
||||
* @param error_id numeric identifier of the error in question.
|
||||
* @return pointer to a statically allocated zero-terminated string containing the respective error text.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <furi_hal.h>
|
||||
|
||||
#include <cli/cli.h>
|
||||
#include <cli/cli_ansi.h>
|
||||
#include <lib/toolbox/args.h>
|
||||
#include <lib/toolbox/dir_walk.h>
|
||||
#include <lib/toolbox/md5_calc.h>
|
||||
@@ -224,7 +225,7 @@ static void storage_cli_write(Cli* cli, FuriString* path, FuriString* args) {
|
||||
while(true) {
|
||||
uint8_t symbol = cli_getc(cli);
|
||||
|
||||
if(symbol == CliSymbolAsciiETX) {
|
||||
if(symbol == CliKeyETX) {
|
||||
size_t write_size = read_index % buffer_size;
|
||||
|
||||
if(write_size > 0) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#define TAG "HidMouseClicker"
|
||||
|
||||
#define DEFAULT_CLICK_RATE 1
|
||||
#define MAXIMUM_CLICK_RATE 60
|
||||
#define MAXIMUM_CLICK_RATE 100
|
||||
|
||||
struct HidMouseClicker {
|
||||
View* view;
|
||||
@@ -34,7 +34,9 @@ static void hid_mouse_clicker_start_or_restart_timer(void* context) {
|
||||
HidMouseClickerModel * model,
|
||||
{
|
||||
furi_timer_start(
|
||||
hid_mouse_clicker->timer, furi_kernel_get_tick_frequency() / model->rate);
|
||||
hid_mouse_clicker->timer,
|
||||
furi_kernel_get_tick_frequency() /
|
||||
((model->rate) ? model->rate : MAXIMUM_CLICK_RATE));
|
||||
},
|
||||
true);
|
||||
}
|
||||
@@ -75,7 +77,11 @@ static void hid_mouse_clicker_draw_callback(Canvas* canvas, void* context) {
|
||||
|
||||
// Clicks/s
|
||||
char label[20];
|
||||
snprintf(label, sizeof(label), "%d clicks/s", model->rate);
|
||||
if(model->rate) {
|
||||
snprintf(label, sizeof(label), "%d clicks/s", model->rate);
|
||||
} else {
|
||||
snprintf(label, sizeof(label), "max clicks/s");
|
||||
}
|
||||
elements_multiline_text_aligned(canvas, 28, 37, AlignCenter, AlignBottom, label);
|
||||
|
||||
canvas_draw_icon(canvas, 25, 20, &I_ButtonUp_7x4);
|
||||
@@ -139,7 +145,7 @@ static bool hid_mouse_clicker_input_callback(InputEvent* event, void* context) {
|
||||
consumed = true;
|
||||
break;
|
||||
case InputKeyDown:
|
||||
if(model->rate > 1) {
|
||||
if(model->rate > 0) {
|
||||
model->rate--;
|
||||
}
|
||||
rate_changed = true;
|
||||
|
||||
@@ -3,6 +3,8 @@ App(
|
||||
name="JS Runner",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="js_app",
|
||||
cdefines=["JS_RUNNER_FAP"],
|
||||
# Sources separation breaks linking when internal, comment as needed
|
||||
sources=[
|
||||
"*.c*",
|
||||
"!modules",
|
||||
@@ -37,11 +39,86 @@ App(
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_dialog",
|
||||
appid="js_event_loop",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_dialog_ep",
|
||||
entry_point="js_event_loop_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_dialog.c"],
|
||||
sources=[
|
||||
"modules/js_event_loop/js_event_loop.c",
|
||||
"modules/js_event_loop/js_event_loop_api_table.cpp",
|
||||
],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_gui",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_gui_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_gui/js_gui.c", "modules/js_gui/js_gui_api_table.cpp"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_gui__loading",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_view_loading_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_gui/loading.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_gui__empty_screen",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_view_empty_screen_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_gui/empty_screen.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_gui__submenu",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_view_submenu_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_gui/submenu.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_gui__text_input",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_view_text_input_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_gui/text_input.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_gui__byte_input",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_view_byte_input_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_gui/byte_input.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_gui__text_box",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_view_text_box_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_gui/text_box.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_gui__dialog",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_view_dialog_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_gui/dialog.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_gui__file_picker",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_gui_file_picker_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_gui/file_picker.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
@@ -69,35 +146,11 @@ App(
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_storage",
|
||||
appid="js_gpio",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_storage_ep",
|
||||
entry_point="js_gpio_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_storage.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_usbdisk",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_usbdisk_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_usbdisk/*.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_submenu",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_submenu_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_submenu.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_blebeacon",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_blebeacon_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_blebeacon.c"],
|
||||
sources=["modules/js_gpio.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
@@ -109,35 +162,11 @@ App(
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_keyboard",
|
||||
appid="js_storage",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_keyboard_ep",
|
||||
entry_point="js_storage_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_keyboard.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_subghz",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_subghz_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_subghz/*.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_gpio",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_gpio_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_gpio.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_textbox",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_textbox_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_textbox.c"],
|
||||
sources=["modules/js_storage.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
@@ -155,3 +184,27 @@ App(
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_vgm/*.c", "modules/js_vgm/ICM42688P/*.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_subghz",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_subghz_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_subghz/*.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_blebeacon",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_blebeacon_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_blebeacon.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_usbdisk",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_usbdisk_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_usbdisk/*.c"],
|
||||
)
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
// This is an example of how to use the analog pins (ADC) on the Flipper Zero.
|
||||
// The example uses a reference voltage of 2048mV (2.048V), but you can also use 2500mV (2.5V).
|
||||
// The example reads the values of the analog pins A7, A6, and A4 and prints them to the console.
|
||||
// The example also checks if the value of A7 is twice the value of A6 and breaks the loop if it is.
|
||||
// The example uses the analog pins A7, A6, and A4, but you can also use PC3, PC1, and PC0.
|
||||
|
||||
let gpio = require("gpio");
|
||||
|
||||
// initialize pins A7, A6, A4 as analog (you can also use PC3, PC1, PC0)
|
||||
gpio.init("PA7", "analog", "no"); // pin, mode, pull
|
||||
gpio.init("PA6", "analog", "no"); // pin, mode, pull
|
||||
gpio.init("PA4", "analog", "no"); // pin, mode, pull
|
||||
|
||||
gpio.startAnalog(2048); // vRef = 2.048V (you can also use 2500 for a 2.5V reference voltage)
|
||||
|
||||
while (true) {
|
||||
let pa7_value = gpio.readAnalog("PA7");
|
||||
let pa6_value = gpio.readAnalog("PA6");
|
||||
let pa4_value = gpio.readAnalog("PA4");
|
||||
print("A7: " + to_string(pa7_value) + " A6: " + to_string(pa6_value) + " A4: " + to_string(pa4_value));
|
||||
delay(100);
|
||||
if (pa7_value === pa6_value * 2) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
print("A7 is twice A6!");
|
||||
|
||||
gpio.stopAnalog();
|
||||
|
||||
// possible analog pins https://docs.flipper.net/gpio-and-modules#miFsS
|
||||
// "PA7" aka 2
|
||||
// "PA6" aka 3
|
||||
// "PA4" aka 4
|
||||
// "PC3" aka 7
|
||||
// "PC1" aka 15
|
||||
// "PC0" aka 16
|
||||
|
||||
// possible modes
|
||||
// "analog"
|
||||
|
||||
// possible pull
|
||||
// "no"
|
||||
@@ -1,48 +1,73 @@
|
||||
let badusb = require("badusb");
|
||||
let notify = require("notification");
|
||||
let flipper = require("flipper");
|
||||
let dialog = require("dialog");
|
||||
let eventLoop = require("event_loop");
|
||||
let gui = require("gui");
|
||||
let dialog = require("gui/dialog");
|
||||
|
||||
let views = {
|
||||
dialog: dialog.makeWith({
|
||||
header: "BadUSB demo",
|
||||
text: "Press OK to start",
|
||||
center: "Start",
|
||||
}),
|
||||
};
|
||||
|
||||
badusb.setup({
|
||||
vid: 0xAAAA,
|
||||
pid: 0xBBBB,
|
||||
mfr_name: "Flipper",
|
||||
prod_name: "Zero",
|
||||
layout_path: "/ext/badusb/assets/layouts/en-US.kl"
|
||||
mfrName: "Flipper",
|
||||
prodName: "Zero",
|
||||
layoutPath: "/ext/badusb/assets/layouts/en-US.kl"
|
||||
});
|
||||
dialog.message("BadUSB demo", "Press OK to start");
|
||||
|
||||
if (badusb.isConnected()) {
|
||||
notify.blink("green", "short");
|
||||
print("USB is connected");
|
||||
eventLoop.subscribe(views.dialog.input, function (_sub, button, eventLoop, gui) {
|
||||
if (button !== "center")
|
||||
return;
|
||||
|
||||
badusb.println("Hello, world!");
|
||||
gui.viewDispatcher.sendTo("back");
|
||||
|
||||
badusb.press("CTRL", "a");
|
||||
badusb.press("CTRL", "c");
|
||||
badusb.press("DOWN");
|
||||
delay(1000);
|
||||
badusb.press("CTRL", "v");
|
||||
delay(1000);
|
||||
badusb.press("CTRL", "v");
|
||||
if (badusb.isConnected()) {
|
||||
notify.blink("green", "short");
|
||||
print("USB is connected");
|
||||
|
||||
badusb.println("1234", 200);
|
||||
badusb.println("Hello, world!");
|
||||
|
||||
badusb.println("Flipper Model: " + flipper.getModel());
|
||||
badusb.println("Flipper Name: " + flipper.getName());
|
||||
badusb.println("Battery level: " + to_string(flipper.getBatteryCharge()) + "%");
|
||||
badusb.press("CTRL", "a");
|
||||
badusb.press("CTRL", "c");
|
||||
badusb.press("DOWN");
|
||||
delay(1000);
|
||||
badusb.press("CTRL", "v");
|
||||
delay(1000);
|
||||
badusb.press("CTRL", "v");
|
||||
|
||||
// Alt+Numpad method works only on Windows!!!
|
||||
badusb.altPrintln("This was printed with Alt+Numpad method!");
|
||||
badusb.println("1234", 200);
|
||||
|
||||
// There's also badusb.print() and badusb.altPrint()
|
||||
// which don't add the return at the end
|
||||
badusb.println("Flipper Model: " + flipper.getModel());
|
||||
badusb.println("Flipper Name: " + flipper.getName());
|
||||
badusb.println("Battery level: " + flipper.getBatteryCharge().toString() + "%");
|
||||
|
||||
notify.success();
|
||||
} else {
|
||||
print("USB not connected");
|
||||
notify.error();
|
||||
}
|
||||
// Alt+Numpad method works only on Windows!!!
|
||||
badusb.altPrintln("This was printed with Alt+Numpad method!");
|
||||
|
||||
// Optional, but allows to interchange with usbdisk
|
||||
badusb.quit();
|
||||
// There's also badusb.print() and badusb.altPrint()
|
||||
// which don't add the return at the end
|
||||
|
||||
notify.success();
|
||||
} else {
|
||||
print("USB not connected");
|
||||
notify.error();
|
||||
}
|
||||
|
||||
// Optional, but allows to unlock usb interface to switch profile
|
||||
badusb.quit();
|
||||
|
||||
eventLoop.stop();
|
||||
}, eventLoop, gui);
|
||||
|
||||
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _item, eventLoop) {
|
||||
eventLoop.stop();
|
||||
}, eventLoop);
|
||||
|
||||
gui.viewDispatcher.switchTo(views.dialog);
|
||||
eventLoop.run();
|
||||
|
||||
@@ -45,7 +45,7 @@ function sendRandomModelAdvertisement() {
|
||||
|
||||
blebeacon.start();
|
||||
|
||||
print("Sent data for model ID " + to_string(model));
|
||||
print("Sent data for model ID " + model.toString());
|
||||
|
||||
currentIndex = (currentIndex + 1) % watchValues.length;
|
||||
|
||||
|
||||
@@ -6,4 +6,4 @@ print("2");
|
||||
delay(1000)
|
||||
print("3");
|
||||
delay(1000)
|
||||
print("end");
|
||||
print("end");
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
let dialog = require("dialog");
|
||||
|
||||
let result1 = dialog.message("Dialog demo", "Press OK to start");
|
||||
print(result1);
|
||||
|
||||
let dialog_params = ({
|
||||
header: "Test_header",
|
||||
text: "Test_text",
|
||||
button_left: "Left",
|
||||
button_right: "Files",
|
||||
button_center: "OK"
|
||||
});
|
||||
|
||||
let result2 = dialog.custom(dialog_params);
|
||||
if (result2 === "") {
|
||||
print("Back is pressed");
|
||||
} else if (result2 === "Files") {
|
||||
let result3 = dialog.pickFile("/ext", "*");
|
||||
print("Selected", result3);
|
||||
} else {
|
||||
print(result2, "is pressed");
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
let eventLoop = require("event_loop");
|
||||
|
||||
// print a string after 1337 milliseconds
|
||||
eventLoop.subscribe(eventLoop.timer("oneshot", 1337), function (_subscription, _item) {
|
||||
print("Hi after 1337 ms");
|
||||
});
|
||||
|
||||
// count up to 5 with a delay of 100ms between increments
|
||||
eventLoop.subscribe(eventLoop.timer("periodic", 100), function (subscription, _item, counter) {
|
||||
print("Counter two:", counter);
|
||||
if (counter === 5)
|
||||
subscription.cancel();
|
||||
return [counter + 1];
|
||||
}, 0);
|
||||
|
||||
// count up to 15 with a delay of 100ms between increments
|
||||
// and stop the program when the count reaches 15
|
||||
eventLoop.subscribe(eventLoop.timer("periodic", 100), function (subscription, _item, event_loop, counter) {
|
||||
print("Counter one:", counter);
|
||||
if (counter === 15)
|
||||
event_loop.stop();
|
||||
return [event_loop, counter + 1];
|
||||
}, eventLoop, 0);
|
||||
|
||||
eventLoop.run();
|
||||
@@ -1,31 +1,29 @@
|
||||
let eventLoop = require("event_loop");
|
||||
let gpio = require("gpio");
|
||||
|
||||
// initialize pins
|
||||
gpio.init("PC3", "outputPushPull", "up"); // pin, mode, pull
|
||||
print("PC3 is initialized as outputPushPull with pull-up");
|
||||
let led = gpio.get("pc3"); // same as `gpio.get(7)`
|
||||
let pot = gpio.get("pc0"); // same as `gpio.get(16)`
|
||||
let button = gpio.get("pc1"); // same as `gpio.get(15)`
|
||||
led.init({ direction: "out", outMode: "push_pull" });
|
||||
pot.init({ direction: "in", inMode: "analog" });
|
||||
button.init({ direction: "in", pull: "up", inMode: "interrupt", edge: "falling" });
|
||||
|
||||
gpio.init("PC1", "input", "down"); // pin, mode, pull
|
||||
print("PC1 is initialized as input with pull-down");
|
||||
// blink led
|
||||
print("Commencing blinking (PC3)");
|
||||
eventLoop.subscribe(eventLoop.timer("periodic", 1000), function (_, _item, led, state) {
|
||||
led.write(state);
|
||||
return [led, !state];
|
||||
}, led, true);
|
||||
|
||||
// let led on PC3 blink
|
||||
gpio.write("PC3", true); // high
|
||||
delay(1000);
|
||||
gpio.write("PC3", false); // low
|
||||
delay(1000);
|
||||
gpio.write("PC3", true); // high
|
||||
delay(1000);
|
||||
gpio.write("PC3", false); // low
|
||||
|
||||
// read value from PC1 and write it to PC3
|
||||
while (true) {
|
||||
let value = gpio.read("PC1");
|
||||
gpio.write("PC3", value);
|
||||
|
||||
value ? print("PC1 is high") : print("PC1 is low");
|
||||
|
||||
delay(100);
|
||||
}
|
||||
// read potentiometer when button is pressed
|
||||
print("Press the button (PC1)");
|
||||
eventLoop.subscribe(button.interrupt(), function (_, _item, pot) {
|
||||
print("PC0 is at", pot.read_analog(), "mV");
|
||||
}, pot);
|
||||
|
||||
// the program will just exit unless this is here
|
||||
eventLoop.run();
|
||||
|
||||
// possible pins https://docs.flipper.net/gpio-and-modules#miFsS
|
||||
// "PA7" aka 2
|
||||
@@ -43,20 +41,17 @@ while (true) {
|
||||
// "PB14" aka 17
|
||||
|
||||
// possible modes
|
||||
// "input"
|
||||
// "outputPushPull"
|
||||
// "outputOpenDrain"
|
||||
// "altFunctionPushPull"
|
||||
// "altFunctionOpenDrain"
|
||||
// "analog"
|
||||
// "interruptRise"
|
||||
// "interruptFall"
|
||||
// "interruptRiseFall"
|
||||
// "eventRise"
|
||||
// "eventFall"
|
||||
// "eventRiseFall"
|
||||
|
||||
// possible pull
|
||||
// "no"
|
||||
// "up"
|
||||
// "down"
|
||||
// { direction: "out", outMode: "push_pull" }
|
||||
// { direction: "out", outMode: "open_drain" }
|
||||
// { direction: "out", outMode: "push_pull", altFn: true }
|
||||
// { direction: "out", outMode: "open_drain", altFn: true }
|
||||
// { direction: "in", inMode: "analog" }
|
||||
// { direction: "in", inMode: "plain_digital" }
|
||||
// { direction: "in", inMode: "interrupt", edge: "rising" }
|
||||
// { direction: "in", inMode: "interrupt", edge: "falling" }
|
||||
// { direction: "in", inMode: "interrupt", edge: "both" }
|
||||
// { direction: "in", inMode: "event", edge: "rising" }
|
||||
// { direction: "in", inMode: "event", edge: "falling" }
|
||||
// { direction: "in", inMode: "event", edge: "both" }
|
||||
// all variants support an optional `pull` field which can either be undefined,
|
||||
// "up" or "down"
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// import modules
|
||||
let eventLoop = require("event_loop");
|
||||
let gui = require("gui");
|
||||
let loadingView = require("gui/loading");
|
||||
let submenuView = require("gui/submenu");
|
||||
let emptyView = require("gui/empty_screen");
|
||||
let textInputView = require("gui/text_input");
|
||||
let byteInputView = require("gui/byte_input");
|
||||
let textBoxView = require("gui/text_box");
|
||||
let dialogView = require("gui/dialog");
|
||||
let filePicker = require("gui/file_picker");
|
||||
let flipper = require("flipper");
|
||||
|
||||
// declare view instances
|
||||
let views = {
|
||||
loading: loadingView.make(),
|
||||
empty: emptyView.make(),
|
||||
keyboard: textInputView.makeWith({
|
||||
header: "Enter your name",
|
||||
defaultText: flipper.getName(),
|
||||
defaultTextClear: true,
|
||||
// Props for makeWith() are passed in reverse order, so maxLength must be after defaultText
|
||||
minLength: 0,
|
||||
maxLength: 32,
|
||||
}),
|
||||
helloDialog: dialogView.make(),
|
||||
bytekb: byteInputView.makeWith({
|
||||
header: "Look ma, I'm a header text!",
|
||||
defaultData: Uint8Array([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]),
|
||||
// Props for makeWith() are passed in reverse order, so length must be after defaultData
|
||||
length: 8,
|
||||
}),
|
||||
longText: textBoxView.makeWith({
|
||||
text: "This is a very long string that demonstrates the TextBox view. Use the D-Pad to scroll backwards and forwards.\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rhoncus est malesuada quam egestas ultrices. Maecenas non eros a nulla eleifend vulputate et ut risus. Quisque in mauris mattis, venenatis risus eget, aliquam diam. Fusce pretium feugiat mauris, ut faucibus ex volutpat in. Phasellus volutpat ex sed gravida consectetur. Aliquam sed lectus feugiat, tristique lectus et, bibendum lacus. Ut sit amet augue eu sapien elementum aliquam quis vitae tortor. Vestibulum quis commodo odio. In elementum fermentum massa, eu pellentesque nibh cursus at. Integer eleifend lacus nec purus elementum sodales. Nulla elementum neque urna, non vulputate massa semper sed. Fusce ut nisi vitae dui blandit congue pretium vitae turpis.",
|
||||
}),
|
||||
demos: submenuView.makeWith({
|
||||
header: "Choose a demo",
|
||||
items: [
|
||||
"Hourglass screen",
|
||||
"Empty screen",
|
||||
"Text input & Dialog",
|
||||
"Byte input",
|
||||
"Text box",
|
||||
"File picker",
|
||||
"Exit app",
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
// demo selector
|
||||
eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, views) {
|
||||
if (index === 0) {
|
||||
gui.viewDispatcher.switchTo(views.loading);
|
||||
// the loading view captures all back events, preventing our navigation callback from firing
|
||||
// switch to the demo chooser after a second
|
||||
eventLoop.subscribe(eventLoop.timer("oneshot", 1000), function (_sub, _, gui, views) {
|
||||
gui.viewDispatcher.switchTo(views.demos);
|
||||
}, gui, views);
|
||||
} else if (index === 1) {
|
||||
gui.viewDispatcher.switchTo(views.empty);
|
||||
} else if (index === 2) {
|
||||
gui.viewDispatcher.switchTo(views.keyboard);
|
||||
} else if (index === 3) {
|
||||
gui.viewDispatcher.switchTo(views.bytekb);
|
||||
} else if (index === 4) {
|
||||
gui.viewDispatcher.switchTo(views.longText);
|
||||
} else if (index === 5) {
|
||||
let path = filePicker.pickFile("/ext", "*");
|
||||
if (path) {
|
||||
views.helloDialog.set("text", "You selected:\n" + path);
|
||||
} else {
|
||||
views.helloDialog.set("text", "You didn't select a file");
|
||||
}
|
||||
views.helloDialog.set("center", "Nice!");
|
||||
gui.viewDispatcher.switchTo(views.helloDialog);
|
||||
} else if (index === 6) {
|
||||
eventLoop.stop();
|
||||
}
|
||||
}, gui, eventLoop, views);
|
||||
|
||||
// say hi after keyboard input
|
||||
eventLoop.subscribe(views.keyboard.input, function (_sub, name, gui, views) {
|
||||
views.keyboard.set("defaultText", name); // Remember for next usage
|
||||
views.helloDialog.set("text", "Hi " + name + "! :)");
|
||||
views.helloDialog.set("center", "Hi Flipper! :)");
|
||||
gui.viewDispatcher.switchTo(views.helloDialog);
|
||||
}, gui, views);
|
||||
|
||||
// go back after the greeting dialog
|
||||
eventLoop.subscribe(views.helloDialog.input, function (_sub, button, gui, views) {
|
||||
if (button === "center")
|
||||
gui.viewDispatcher.switchTo(views.demos);
|
||||
}, gui, views);
|
||||
|
||||
// show data after byte input
|
||||
eventLoop.subscribe(views.bytekb.input, function (_sub, data, gui, views) {
|
||||
let data_view = Uint8Array(data);
|
||||
let text = "0x";
|
||||
for (let i = 0; i < data_view.length; i++) {
|
||||
text += data_view[i].toString(16);
|
||||
}
|
||||
views.helloDialog.set("text", "You typed:\n" + text);
|
||||
views.helloDialog.set("center", "Cool!");
|
||||
gui.viewDispatcher.switchTo(views.helloDialog);
|
||||
}, gui, views);
|
||||
|
||||
// go to the demo chooser screen when the back key is pressed
|
||||
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, gui, views, eventLoop) {
|
||||
if (gui.viewDispatcher.currentView === views.demos) {
|
||||
eventLoop.stop();
|
||||
return;
|
||||
}
|
||||
gui.viewDispatcher.switchTo(views.demos);
|
||||
}, gui, views, eventLoop);
|
||||
|
||||
// run UI
|
||||
gui.viewDispatcher.switchTo(views.demos);
|
||||
eventLoop.run();
|
||||
@@ -1,23 +0,0 @@
|
||||
let keyboard = require("keyboard");
|
||||
|
||||
keyboard.setHeader("Example Text Input");
|
||||
|
||||
// Default text is optional
|
||||
let text = keyboard.text(100, "Default text", true);
|
||||
// Returns undefined when pressing back
|
||||
print("Got text:", text);
|
||||
|
||||
keyboard.setHeader("Example Byte Input");
|
||||
|
||||
// Default data is optional
|
||||
let result = keyboard.byte(6, Uint8Array([1, 2, 3, 4, 5, 6]));
|
||||
// Returns undefined when pressing back
|
||||
if (result !== undefined) {
|
||||
let data = Uint8Array(result);
|
||||
result = "0x";
|
||||
for (let i = 0; i < data.byteLength; i++) {
|
||||
if (data[i] < 0x10) result += "0";
|
||||
result += to_hex_string(data[i]);
|
||||
}
|
||||
}
|
||||
print("Got data:", result);
|
||||
@@ -1,3 +1,3 @@
|
||||
let math = load("/ext/apps/Scripts/load_api.js");
|
||||
let math = load(__dirname + "/load_api.js");
|
||||
let result = math.add(5, 10);
|
||||
print(result);
|
||||
print(result);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
({
|
||||
add: function (a, b) { return a + b; },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -22,48 +22,3 @@ print("math.sign(-5):", math.sign(-5));
|
||||
print("math.sin(math.PI/2):", math.sin(math.PI / 2));
|
||||
print("math.sqrt(25):", math.sqrt(25));
|
||||
print("math.trunc(5.7):", math.trunc(5.7));
|
||||
|
||||
// Unit tests. Please add more if you have time and knowledge.
|
||||
// math.EPSILON on Flipper Zero is 2.22044604925031308085e-16
|
||||
|
||||
let succeeded = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(text, result, expected, epsilon) {
|
||||
let is_equal = math.is_equal(result, expected, epsilon);
|
||||
if (is_equal) {
|
||||
succeeded += 1;
|
||||
} else {
|
||||
failed += 1;
|
||||
print(text, "expected", expected, "got", result);
|
||||
}
|
||||
}
|
||||
|
||||
test("math.abs(5)", math.abs(-5), 5, math.EPSILON);
|
||||
test("math.abs(0.5)", math.abs(-0.5), 0.5, math.EPSILON);
|
||||
test("math.abs(5)", math.abs(5), 5, math.EPSILON);
|
||||
test("math.abs(-0.5)", math.abs(0.5), 0.5, math.EPSILON);
|
||||
test("math.acos(0.5)", math.acos(0.5), 1.0471975511965976, math.EPSILON);
|
||||
test("math.acosh(2)", math.acosh(2), 1.3169578969248166, math.EPSILON);
|
||||
test("math.asin(0.5)", math.asin(0.5), 0.5235987755982988, math.EPSILON);
|
||||
test("math.asinh(2)", math.asinh(2), 1.4436354751788103, math.EPSILON);
|
||||
test("math.atan(1)", math.atan(1), 0.7853981633974483, math.EPSILON);
|
||||
test("math.atan2(1, 1)", math.atan2(1, 1), 0.7853981633974483, math.EPSILON);
|
||||
test("math.atanh(0.5)", math.atanh(0.5), 0.5493061443340549, math.EPSILON);
|
||||
test("math.cbrt(27)", math.cbrt(27), 3, math.EPSILON);
|
||||
test("math.ceil(5.3)", math.ceil(5.3), 6, math.EPSILON);
|
||||
test("math.clz32(1)", math.clz32(1), 31, math.EPSILON);
|
||||
test("math.floor(5.7)", math.floor(5.7), 5, math.EPSILON);
|
||||
test("math.max(3, 5)", math.max(3, 5), 5, math.EPSILON);
|
||||
test("math.min(3, 5)", math.min(3, 5), 3, math.EPSILON);
|
||||
test("math.pow(2, 3)", math.pow(2, 3), 8, math.EPSILON);
|
||||
test("math.sign(-5)", math.sign(-5), -1, math.EPSILON);
|
||||
test("math.sqrt(25)", math.sqrt(25), 5, math.EPSILON);
|
||||
test("math.trunc(5.7)", math.trunc(5.7), 5, math.EPSILON);
|
||||
test("math.cos(math.PI)", math.cos(math.PI), -1, math.EPSILON * 18); // Error 3.77475828372553223744e-15
|
||||
test("math.exp(1)", math.exp(1), 2.718281828459045, math.EPSILON * 2); // Error 4.44089209850062616169e-16
|
||||
test("math.sin(math.PI / 2)", math.sin(math.PI / 2), 1, math.EPSILON * 4.5); // Error 9.99200722162640886381e-16
|
||||
|
||||
if (failed > 0) {
|
||||
print("!!!", failed, "Unit tests failed !!!");
|
||||
}
|
||||
@@ -6,4 +6,4 @@ delay(1000);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
notify.blink("red", "short");
|
||||
delay(500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
let storage = require("storage");
|
||||
|
||||
print("script has __dirpath of" + __dirpath);
|
||||
print("script has __filepath of" + __filepath);
|
||||
if (storage.exists(__dirpath + "/math.js")) {
|
||||
print("script has __dirname of" + __dirname);
|
||||
print("script has __filename of" + __filename);
|
||||
if (storage.fileExists(__dirname + "/math.js")) {
|
||||
print("math.js exist here.");
|
||||
} else {
|
||||
print("math.js does not exist here.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,29 @@
|
||||
let storage = require("storage");
|
||||
let path = "/ext/storage.test";
|
||||
|
||||
function arraybuf_to_string(arraybuf) {
|
||||
let string = "";
|
||||
let data_view = Uint8Array(arraybuf);
|
||||
for (let i = 0; i < data_view.length; i++) {
|
||||
string += chr(data_view[i]);
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
print("File exists:", storage.exists(path));
|
||||
print("File exists:", storage.fileExists(path));
|
||||
|
||||
print("Writing...");
|
||||
// write(path, data, offset)
|
||||
// If offset is specified, the file is not cleared, content is kept and data is written at specified offset
|
||||
// Takes both strings and array buffers
|
||||
storage.write(path, "Hello ");
|
||||
let file = storage.openFile(path, "w", "create_always");
|
||||
file.write("Hello ");
|
||||
file.close();
|
||||
|
||||
print("File exists:", storage.exists(path));
|
||||
print("File exists:", storage.fileExists(path));
|
||||
|
||||
// Append will create the file even if it doesnt exist!
|
||||
// Takes both strings and array buffers
|
||||
storage.append(path, "World!");
|
||||
file = storage.openFile(path, "w", "open_append");
|
||||
file.write("World!");
|
||||
file.close();
|
||||
|
||||
print("Reading...");
|
||||
// read(path, size, offset)
|
||||
// If no size specified, total filesize is used
|
||||
// If offset is specified, size is capped at (filesize - offset)
|
||||
let data = storage.read(path);
|
||||
// read returns an array buffer, to allow proper usage of raw binary data
|
||||
print(arraybuf_to_string(data));
|
||||
file = storage.openFile(path, "r", "open_existing");
|
||||
let text = file.read("ascii", 128);
|
||||
file.close();
|
||||
print(text);
|
||||
|
||||
print("Removing...")
|
||||
storage.remove(path);
|
||||
|
||||
print("Done")
|
||||
|
||||
// There's also:
|
||||
// storage.copy(old_path, new_path);
|
||||
// storage.move(old_path, new_path);
|
||||
// storage.mkdir(path);
|
||||
// storage.virtualInit(path);
|
||||
// storage.virtualMount();
|
||||
// storage.virtualQuit();
|
||||
// You don't need to close the file after each operation, this is just to show some different ways to use the API
|
||||
// There's also many more functions and options, check type definitions in firmware repo
|
||||
@@ -1,6 +1,6 @@
|
||||
let sampleText = "Hello, World!";
|
||||
|
||||
let lengthOfText = "Length of text: " + to_string(sampleText.length);
|
||||
let lengthOfText = "Length of text: " + sampleText.length.toString();
|
||||
print(lengthOfText);
|
||||
|
||||
let start = 7;
|
||||
@@ -9,11 +9,11 @@ let substringResult = sampleText.slice(start, end);
|
||||
print(substringResult);
|
||||
|
||||
let searchStr = "World";
|
||||
let result2 = to_string(sampleText.indexOf(searchStr));
|
||||
let result2 = sampleText.indexOf(searchStr).toString();
|
||||
print(result2);
|
||||
|
||||
let upperCaseText = "Text in upper case: " + to_upper_case(sampleText);
|
||||
let upperCaseText = "Text in upper case: " + sampleText.toUpperCase();
|
||||
print(upperCaseText);
|
||||
|
||||
let lowerCaseText = "Text in lower case: " + to_lower_case(sampleText);
|
||||
let lowerCaseText = "Text in lower case: " + sampleText.toLowerCase();
|
||||
print(lowerCaseText);
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
let submenu = require("submenu");
|
||||
|
||||
submenu.addItem("Item 1", 0);
|
||||
submenu.addItem("Item 2", 1);
|
||||
submenu.addItem("Item 3", 2);
|
||||
|
||||
submenu.setHeader("Select an option:");
|
||||
|
||||
let result = submenu.show();
|
||||
// Returns undefined when pressing back
|
||||
print("Result:", result);
|
||||
@@ -1,30 +0,0 @@
|
||||
let textbox = require("textbox");
|
||||
|
||||
// You should set config before adding text
|
||||
// Focus (start / end), Font (text / hex)
|
||||
textbox.setConfig("end", "text");
|
||||
|
||||
// Can make sure it's cleared before showing, in case of reusing in same script
|
||||
// (Closing textbox already clears the text, but maybe you added more in a loop for example)
|
||||
textbox.clearText();
|
||||
|
||||
// Add default text
|
||||
textbox.addText("Example dynamic updating textbox\n");
|
||||
|
||||
// Non-blocking, can keep updating text after, can close in JS or in GUI
|
||||
textbox.show();
|
||||
|
||||
let i = 0;
|
||||
while (textbox.isOpen() && i < 20) {
|
||||
print("console", i++);
|
||||
|
||||
// Add text to textbox buffer
|
||||
textbox.addText("textbox " + to_string(i) + "\n");
|
||||
|
||||
delay(500);
|
||||
}
|
||||
|
||||
// If not closed by user (instead i < 20 is false above), close forcefully
|
||||
if (textbox.isOpen()) {
|
||||
textbox.close();
|
||||
}
|
||||
@@ -2,11 +2,11 @@ let serial = require("serial");
|
||||
serial.setup("usart", 230400);
|
||||
|
||||
while (1) {
|
||||
let rx_data = serial.readBytes(1, 0);
|
||||
let rx_data = serial.readBytes(1, 1000);
|
||||
if (rx_data !== undefined) {
|
||||
serial.write(rx_data);
|
||||
let data_view = Uint8Array(rx_data);
|
||||
print("0x" + to_hex_string(data_view[0]));
|
||||
print("0x" + data_view[0].toString(16));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,94 @@
|
||||
let dialog = require("dialog");
|
||||
let keyboard = require("keyboard");
|
||||
let eventLoop = require("event_loop");
|
||||
let gui = require("gui");
|
||||
let dialog = require("gui/dialog");
|
||||
let textInput = require("gui/text_input");
|
||||
let loading = require("gui/loading");
|
||||
let storage = require("storage");
|
||||
|
||||
// Need to run code from file, and filename must be unique
|
||||
let tmp_template = "/ext/apps_data/js_app/.interactive.tmp.";
|
||||
let tmp_number = 0;
|
||||
// No eval() or exec() so need to run code from file, and filename must be unique
|
||||
storage.makeDirectory("/ext/.tmp");
|
||||
storage.makeDirectory("/ext/.tmp/js");
|
||||
storage.rmrf("/ext/.tmp/js/repl")
|
||||
storage.makeDirectory("/ext/.tmp/js/repl")
|
||||
let ctx = {
|
||||
tmpTemplate: "/ext/.tmp/js/repl/",
|
||||
tmpNumber: 0,
|
||||
persistentScope: {},
|
||||
};
|
||||
|
||||
let result = "Run JavaScript Code";
|
||||
while (dialog.message("Interactive Console", result)) {
|
||||
keyboard.setHeader("Type JavaScript Code");
|
||||
let input = keyboard.text(256);
|
||||
if (!input) break;
|
||||
let views = {
|
||||
dialog: dialog.makeWith({
|
||||
header: "Interactive Console",
|
||||
text: "Press OK to Start",
|
||||
center: "Run Some JS"
|
||||
}),
|
||||
textInput: textInput.makeWith({
|
||||
header: "Type JavaScript Code:",
|
||||
defaultText: "2+2",
|
||||
defaultTextClear: true,
|
||||
// Props for makeWith() are passed in reverse order, so maxLength must be after defaultText
|
||||
minLength: 0,
|
||||
maxLength: 256,
|
||||
}),
|
||||
loading: loading.make(),
|
||||
};
|
||||
|
||||
let path = tmp_template + to_string(tmp_number++);
|
||||
storage.write(path, "({run:function(){return " + input + ";},})");
|
||||
result = load(path).run();
|
||||
eventLoop.subscribe(views.dialog.input, function (_sub, button, gui, views) {
|
||||
if (button === "center") {
|
||||
gui.viewDispatcher.switchTo(views.textInput);
|
||||
}
|
||||
}, gui, views);
|
||||
|
||||
eventLoop.subscribe(views.textInput.input, function (_sub, text, gui, views, ctx) {
|
||||
gui.viewDispatcher.switchTo(views.loading);
|
||||
|
||||
let path = ctx.tmpTemplate + (ctx.tmpNumber++).toString();
|
||||
let file = storage.openFile(path, "w", "create_always");
|
||||
file.write(text);
|
||||
file.close();
|
||||
|
||||
// Hide GUI before running, we want to see console and avoid deadlock if code fails
|
||||
gui.viewDispatcher.sendTo("back");
|
||||
let result = load(path, ctx.persistentScope); // Load runs JS and returns last value on stack
|
||||
storage.remove(path);
|
||||
|
||||
// Must convert to string explicitly
|
||||
if (typeof result === "number") {
|
||||
result = to_string(result);
|
||||
} else if (typeof result === "undefined") {
|
||||
result = "undefined";
|
||||
if (result === null) { // mJS: typeof null === "null", ECMAScript: typeof null === "object", IDE complains when checking "null" type
|
||||
result = "null";
|
||||
} else if (typeof result === "string") {
|
||||
result = "'" + result + "'";
|
||||
} else if (typeof result === "number") {
|
||||
result = result.toString();
|
||||
} else if (typeof result === "bigint") { // mJS doesn't support BigInt() but might aswell check
|
||||
result = "bigint";
|
||||
} else if (typeof result === "boolean") {
|
||||
result = result ? "true" : "false";
|
||||
} else if (typeof result === "symbol") { // mJS doesn't support Symbol() but might aswell check
|
||||
result = "symbol";
|
||||
} else if (typeof result === "undefined") {
|
||||
result = "undefined";
|
||||
} else if (typeof result === "object") {
|
||||
result = JSON.stringify(result);
|
||||
result = "object"; // JSON.stringify() is not implemented
|
||||
} else if (typeof result === "function") {
|
||||
result = "function";
|
||||
} else {
|
||||
result = "unknown type: " + typeof result;
|
||||
}
|
||||
}
|
||||
|
||||
gui.viewDispatcher.sendTo("front");
|
||||
views.dialog.set("header", "JS Returned:");
|
||||
views.dialog.set("text", result);
|
||||
gui.viewDispatcher.switchTo(views.dialog);
|
||||
views.textInput.set("defaultText", text);
|
||||
}, gui, views, ctx);
|
||||
|
||||
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, eventLoop) {
|
||||
eventLoop.stop();
|
||||
}, eventLoop);
|
||||
|
||||
gui.viewDispatcher.switchTo(views.dialog);
|
||||
|
||||
// Message behind GUI if something breaks
|
||||
print("If you're stuck here, something went wrong, re-run the script")
|
||||
eventLoop.run();
|
||||
print("\n\nFinished correctly :)")
|
||||
|
||||
@@ -114,7 +114,7 @@ int32_t js_app(void* arg) {
|
||||
FuriString* start_text =
|
||||
furi_string_alloc_printf("Running %s", furi_string_get_cstr(name));
|
||||
console_view_print(app->console_view, furi_string_get_cstr(start_text));
|
||||
console_view_print(app->console_view, "------------");
|
||||
console_view_print(app->console_view, "-------------");
|
||||
furi_string_free(name);
|
||||
furi_string_free(start_text);
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
#include <core/common_defines.h>
|
||||
#include "js_modules.h"
|
||||
#include <m-dict.h>
|
||||
#include <m-array.h>
|
||||
|
||||
#include "modules/js_flipper.h"
|
||||
#ifdef FW_CFG_unit_tests
|
||||
#include "modules/js_tests.h"
|
||||
#endif
|
||||
|
||||
#define TAG "JS modules"
|
||||
|
||||
@@ -9,54 +13,72 @@
|
||||
#define MODULES_PATH "/ext/apps_data/js_app/plugins"
|
||||
|
||||
typedef struct {
|
||||
JsModeConstructor create;
|
||||
JsModeDestructor destroy;
|
||||
FuriString* name;
|
||||
const JsModuleConstructor create;
|
||||
const JsModuleDestructor destroy;
|
||||
void* context;
|
||||
} JsModuleData;
|
||||
|
||||
DICT_DEF2(JsModuleDict, FuriString*, FURI_STRING_OPLIST, JsModuleData, M_POD_OPLIST);
|
||||
// not using:
|
||||
// - a dict because ordering is required
|
||||
// - a bptree because it forces a sorted ordering
|
||||
// - an rbtree because i deemed it more tedious to implement, and with the
|
||||
// amount of modules in use (under 10 in the overwhelming majority of cases)
|
||||
// i bet it's going to be slower than a plain array
|
||||
ARRAY_DEF(JsModuleArray, JsModuleData, M_POD_OPLIST);
|
||||
#define M_OPL_JsModuleArray_t() ARRAY_OPLIST(JsModuleArray)
|
||||
|
||||
static const JsModuleDescriptor modules_builtin[] = {
|
||||
{"flipper", js_flipper_create, NULL},
|
||||
{"flipper", js_flipper_create, NULL, NULL},
|
||||
#ifdef FW_CFG_unit_tests
|
||||
{"tests", js_tests_create, NULL, NULL},
|
||||
#endif
|
||||
};
|
||||
|
||||
struct JsModules {
|
||||
struct mjs* mjs;
|
||||
JsModuleDict_t module_dict;
|
||||
JsModuleArray_t modules;
|
||||
PluginManager* plugin_manager;
|
||||
CompositeApiResolver* resolver;
|
||||
};
|
||||
|
||||
JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver) {
|
||||
JsModules* modules = malloc(sizeof(JsModules));
|
||||
modules->mjs = mjs;
|
||||
JsModuleDict_init(modules->module_dict);
|
||||
JsModuleArray_init(modules->modules);
|
||||
|
||||
modules->plugin_manager = plugin_manager_alloc(
|
||||
PLUGIN_APP_ID, PLUGIN_API_VERSION, composite_api_resolver_get(resolver));
|
||||
|
||||
modules->resolver = resolver;
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
void js_modules_destroy(JsModules* modules) {
|
||||
JsModuleDict_it_t it;
|
||||
for(JsModuleDict_it(it, modules->module_dict); !JsModuleDict_end_p(it);
|
||||
JsModuleDict_next(it)) {
|
||||
const JsModuleDict_itref_t* module_itref = JsModuleDict_cref(it);
|
||||
if(module_itref->value.destroy) {
|
||||
module_itref->value.destroy(module_itref->value.context);
|
||||
void js_modules_destroy(JsModules* instance) {
|
||||
for
|
||||
M_EACH(module, instance->modules, JsModuleArray_t) {
|
||||
FURI_LOG_T(TAG, "Tearing down %s", furi_string_get_cstr(module->name));
|
||||
if(module->destroy) module->destroy(module->context);
|
||||
furi_string_free(module->name);
|
||||
}
|
||||
}
|
||||
plugin_manager_free(modules->plugin_manager);
|
||||
JsModuleDict_clear(modules->module_dict);
|
||||
free(modules);
|
||||
plugin_manager_free(instance->plugin_manager);
|
||||
JsModuleArray_clear(instance->modules);
|
||||
free(instance);
|
||||
}
|
||||
|
||||
JsModuleData* js_find_loaded_module(JsModules* instance, const char* name) {
|
||||
for
|
||||
M_EACH(module, instance->modules, JsModuleArray_t) {
|
||||
if(furi_string_cmp_str(module->name, name) == 0) return module;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_len) {
|
||||
FuriString* module_name = furi_string_alloc_set_str(name);
|
||||
// Check if module is already installed
|
||||
JsModuleData* module_inst = JsModuleDict_get(modules->module_dict, module_name);
|
||||
JsModuleData* module_inst = js_find_loaded_module(modules, name);
|
||||
if(module_inst) { //-V547
|
||||
furi_string_free(module_name);
|
||||
mjs_prepend_errorf(
|
||||
modules->mjs, MJS_BAD_ARGS_ERROR, "\"%s\" module is already installed", name);
|
||||
return MJS_UNDEFINED;
|
||||
@@ -73,8 +95,11 @@ mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_le
|
||||
|
||||
if(strncmp(name, modules_builtin[i].name, name_compare_len) == 0) {
|
||||
JsModuleData module = {
|
||||
.create = modules_builtin[i].create, .destroy = modules_builtin[i].destroy};
|
||||
JsModuleDict_set_at(modules->module_dict, module_name, module);
|
||||
.create = modules_builtin[i].create,
|
||||
.destroy = modules_builtin[i].destroy,
|
||||
.name = furi_string_alloc_set_str(name),
|
||||
};
|
||||
JsModuleArray_push_at(modules->modules, 0, module);
|
||||
module_found = true;
|
||||
FURI_LOG_I(TAG, "Using built-in module %s", name);
|
||||
break;
|
||||
@@ -83,39 +108,57 @@ mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_le
|
||||
|
||||
// External module load
|
||||
if(!module_found) {
|
||||
FuriString* deslashed_name = furi_string_alloc_set_str(name);
|
||||
furi_string_replace_all_str(deslashed_name, "/", "__");
|
||||
FuriString* module_path = furi_string_alloc();
|
||||
furi_string_printf(module_path, "%s/js_%s.fal", MODULES_PATH, name);
|
||||
FURI_LOG_I(TAG, "Loading external module %s", furi_string_get_cstr(module_path));
|
||||
furi_string_printf(
|
||||
module_path, "%s/js_%s.fal", MODULES_PATH, furi_string_get_cstr(deslashed_name));
|
||||
FURI_LOG_I(
|
||||
TAG, "Loading external module %s from %s", name, furi_string_get_cstr(module_path));
|
||||
do {
|
||||
uint32_t plugin_cnt_last = plugin_manager_get_count(modules->plugin_manager);
|
||||
PluginManagerError load_error = plugin_manager_load_single(
|
||||
modules->plugin_manager, furi_string_get_cstr(module_path));
|
||||
if(load_error != PluginManagerErrorNone) {
|
||||
FURI_LOG_E(
|
||||
TAG,
|
||||
"Module %s load error. It may depend on other modules that are not yet loaded.",
|
||||
name);
|
||||
break;
|
||||
}
|
||||
const JsModuleDescriptor* plugin =
|
||||
plugin_manager_get_ep(modules->plugin_manager, plugin_cnt_last);
|
||||
furi_assert(plugin);
|
||||
|
||||
if(strncmp(name, plugin->name, name_len) != 0) {
|
||||
FURI_LOG_E(TAG, "Module name missmatch %s", plugin->name);
|
||||
if(furi_string_cmp_str(deslashed_name, plugin->name) != 0) {
|
||||
FURI_LOG_E(TAG, "Module name mismatch %s", plugin->name);
|
||||
break;
|
||||
}
|
||||
JsModuleData module = {.create = plugin->create, .destroy = plugin->destroy};
|
||||
JsModuleDict_set_at(modules->module_dict, module_name, module);
|
||||
JsModuleData module = {
|
||||
.create = plugin->create,
|
||||
.destroy = plugin->destroy,
|
||||
.name = furi_string_alloc_set_str(name),
|
||||
};
|
||||
JsModuleArray_push_at(modules->modules, 0, module);
|
||||
|
||||
if(plugin->api_interface) {
|
||||
FURI_LOG_I(TAG, "Added module API to composite resolver: %s", plugin->name);
|
||||
composite_api_resolver_add(modules->resolver, plugin->api_interface);
|
||||
}
|
||||
|
||||
module_found = true;
|
||||
} while(0);
|
||||
furi_string_free(module_path);
|
||||
furi_string_free(deslashed_name);
|
||||
}
|
||||
|
||||
// Run module constructor
|
||||
mjs_val_t module_object = MJS_UNDEFINED;
|
||||
if(module_found) {
|
||||
module_inst = JsModuleDict_get(modules->module_dict, module_name);
|
||||
module_inst = js_find_loaded_module(modules, name);
|
||||
furi_assert(module_inst);
|
||||
if(module_inst->create) { //-V779
|
||||
module_inst->context = module_inst->create(modules->mjs, &module_object);
|
||||
module_inst->context = module_inst->create(modules->mjs, &module_object, modules);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +166,12 @@ mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_le
|
||||
mjs_prepend_errorf(modules->mjs, MJS_BAD_ARGS_ERROR, "\"%s\" module load fail", name);
|
||||
}
|
||||
|
||||
furi_string_free(module_name);
|
||||
|
||||
return module_object;
|
||||
}
|
||||
|
||||
void* js_module_get(JsModules* modules, const char* name) {
|
||||
FuriString* module_name = furi_string_alloc_set_str(name);
|
||||
JsModuleData* module_inst = js_find_loaded_module(modules, name);
|
||||
furi_string_free(module_name);
|
||||
return module_inst ? module_inst->context : NULL;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "js_thread_i.h"
|
||||
#include <flipper_application/flipper_application.h>
|
||||
#include <flipper_application/plugins/plugin_manager.h>
|
||||
@@ -7,19 +9,269 @@
|
||||
#define PLUGIN_APP_ID "js"
|
||||
#define PLUGIN_API_VERSION 1
|
||||
|
||||
typedef void* (*JsModeConstructor)(struct mjs* mjs, mjs_val_t* object);
|
||||
typedef void (*JsModeDestructor)(void* inst);
|
||||
/**
|
||||
* @brief Returns the foreign pointer in `obj["_"]`
|
||||
*/
|
||||
#define JS_GET_INST(mjs, obj) mjs_get_ptr(mjs, mjs_get(mjs, obj, INST_PROP_NAME, ~0))
|
||||
/**
|
||||
* @brief Returns the foreign pointer in `this["_"]`
|
||||
*/
|
||||
#define JS_GET_CONTEXT(mjs) JS_GET_INST(mjs, mjs_get_this(mjs))
|
||||
|
||||
/**
|
||||
* @brief Syntax sugar for constructing an object
|
||||
*
|
||||
* @example
|
||||
* ```c
|
||||
* mjs_val_t my_obj = mjs_mk_object(mjs);
|
||||
* JS_ASSIGN_MULTI(mjs, my_obj) {
|
||||
* JS_FIELD("method1", MJS_MK_FN(js_storage_file_is_open));
|
||||
* JS_FIELD("method2", MJS_MK_FN(js_storage_file_is_open));
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
#define JS_ASSIGN_MULTI(mjs, object) \
|
||||
for(struct { \
|
||||
struct mjs* mjs; \
|
||||
mjs_val_t val; \
|
||||
int i; \
|
||||
} _ass_multi = {mjs, object, 0}; \
|
||||
_ass_multi.i == 0; \
|
||||
_ass_multi.i++)
|
||||
#define JS_FIELD(name, value) mjs_set(_ass_multi.mjs, _ass_multi.val, name, ~0, value)
|
||||
|
||||
/**
|
||||
* @brief The first word of structures that foreign pointer JS values point to
|
||||
*
|
||||
* This is used to detect situations where JS code mistakenly passes an opaque
|
||||
* foreign pointer of one type as an argument to a native function which expects
|
||||
* a struct of another type.
|
||||
*
|
||||
* It is recommended to use this functionality in conjunction with the following
|
||||
* convenience verification macros:
|
||||
* - `JS_ARG_STRUCT()`
|
||||
* - `JS_ARG_OBJ_WITH_STRUCT()`
|
||||
*
|
||||
* @warning In order for the mechanism to work properly, your struct must store
|
||||
* the magic value in the first word.
|
||||
*/
|
||||
typedef enum {
|
||||
JsForeignMagicStart = 0x15BAD000,
|
||||
JsForeignMagic_JsEventLoopContract,
|
||||
} JsForeignMagic;
|
||||
|
||||
// Are you tired of your silly little JS+C glue code functions being 75%
|
||||
// argument validation code and 25% actual logic? Introducing: ASS (Argument
|
||||
// Schema for Scripts)! ASS is a set of macros that reduce the typical
|
||||
// boilerplate code of "check argument count, get arguments, validate arguments,
|
||||
// extract C values from arguments" down to just one line!
|
||||
|
||||
/**
|
||||
* When passed as the second argument to `JS_FETCH_ARGS_OR_RETURN`, signifies
|
||||
* that the function requires exactly as many arguments as were specified.
|
||||
*/
|
||||
#define JS_EXACTLY ==
|
||||
/**
|
||||
* When passed as the second argument to `JS_FETCH_ARGS_OR_RETURN`, signifies
|
||||
* that the function requires at least as many arguments as were specified.
|
||||
*/
|
||||
#define JS_AT_LEAST >=
|
||||
|
||||
#define JS_ENUM_MAP(var_name, ...) \
|
||||
static const JsEnumMapping var_name##_mapping[] = { \
|
||||
{NULL, sizeof(var_name)}, \
|
||||
__VA_ARGS__, \
|
||||
{NULL, 0}, \
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
const char* name;
|
||||
size_t value;
|
||||
} JsEnumMapping;
|
||||
|
||||
typedef struct {
|
||||
void* out;
|
||||
int (*validator)(mjs_val_t);
|
||||
void (*converter)(struct mjs*, mjs_val_t*, void* out, const void* extra);
|
||||
const char* expected_type;
|
||||
bool (*extended_validator)(struct mjs*, mjs_val_t, const void* extra);
|
||||
const void* extra_data;
|
||||
} _js_arg_decl;
|
||||
|
||||
static inline void _js_to_int32(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) {
|
||||
UNUSED(extra);
|
||||
*(int32_t*)out = mjs_get_int32(mjs, *in);
|
||||
}
|
||||
#define JS_ARG_INT32(out) ((_js_arg_decl){out, mjs_is_number, _js_to_int32, "number", NULL, NULL})
|
||||
|
||||
static inline void _js_to_ptr(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) {
|
||||
UNUSED(extra);
|
||||
*(void**)out = mjs_get_ptr(mjs, *in);
|
||||
}
|
||||
#define JS_ARG_PTR(out) \
|
||||
((_js_arg_decl){out, mjs_is_foreign, _js_to_ptr, "opaque pointer", NULL, NULL})
|
||||
|
||||
static inline void _js_to_string(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) {
|
||||
UNUSED(extra);
|
||||
*(const char**)out = mjs_get_string(mjs, in, NULL);
|
||||
}
|
||||
#define JS_ARG_STR(out) ((_js_arg_decl){out, mjs_is_string, _js_to_string, "string", NULL, NULL})
|
||||
|
||||
static inline void _js_to_bool(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) {
|
||||
UNUSED(extra);
|
||||
*(bool*)out = !!mjs_get_bool(mjs, *in);
|
||||
}
|
||||
#define JS_ARG_BOOL(out) ((_js_arg_decl){out, mjs_is_boolean, _js_to_bool, "boolean", NULL, NULL})
|
||||
|
||||
static inline void _js_passthrough(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) {
|
||||
UNUSED(extra);
|
||||
UNUSED(mjs);
|
||||
*(mjs_val_t*)out = *in;
|
||||
}
|
||||
#define JS_ARG_ANY(out) ((_js_arg_decl){out, NULL, _js_passthrough, "any", NULL, NULL})
|
||||
#define JS_ARG_OBJ(out) ((_js_arg_decl){out, mjs_is_object, _js_passthrough, "any", NULL, NULL})
|
||||
#define JS_ARG_FN(out) \
|
||||
((_js_arg_decl){out, mjs_is_function, _js_passthrough, "function", NULL, NULL})
|
||||
#define JS_ARG_ARR(out) ((_js_arg_decl){out, mjs_is_array, _js_passthrough, "array", NULL, NULL})
|
||||
|
||||
static inline bool _js_validate_struct(struct mjs* mjs, mjs_val_t val, const void* extra) {
|
||||
JsForeignMagic expected_magic = (JsForeignMagic)(size_t)extra;
|
||||
JsForeignMagic struct_magic = *(JsForeignMagic*)mjs_get_ptr(mjs, val);
|
||||
return struct_magic == expected_magic;
|
||||
}
|
||||
#define JS_ARG_STRUCT(type, out) \
|
||||
((_js_arg_decl){ \
|
||||
out, \
|
||||
mjs_is_foreign, \
|
||||
_js_to_ptr, \
|
||||
#type, \
|
||||
_js_validate_struct, \
|
||||
(void*)JsForeignMagic##_##type})
|
||||
|
||||
static inline bool _js_validate_obj_w_struct(struct mjs* mjs, mjs_val_t val, const void* extra) {
|
||||
JsForeignMagic expected_magic = (JsForeignMagic)(size_t)extra;
|
||||
JsForeignMagic struct_magic = *(JsForeignMagic*)JS_GET_INST(mjs, val);
|
||||
return struct_magic == expected_magic;
|
||||
}
|
||||
#define JS_ARG_OBJ_WITH_STRUCT(type, out) \
|
||||
((_js_arg_decl){ \
|
||||
out, \
|
||||
mjs_is_object, \
|
||||
_js_passthrough, \
|
||||
#type, \
|
||||
_js_validate_obj_w_struct, \
|
||||
(void*)JsForeignMagic##_##type})
|
||||
|
||||
static inline bool _js_validate_enum(struct mjs* mjs, mjs_val_t val, const void* extra) {
|
||||
for(const JsEnumMapping* mapping = (JsEnumMapping*)extra + 1; mapping->name; mapping++)
|
||||
if(strcmp(mapping->name, mjs_get_string(mjs, &val, NULL)) == 0) return true;
|
||||
return false;
|
||||
}
|
||||
static inline void
|
||||
_js_convert_enum(struct mjs* mjs, mjs_val_t* val, void* out, const void* extra) {
|
||||
const JsEnumMapping* mapping = (JsEnumMapping*)extra;
|
||||
size_t size = mapping->value; // get enum size from first entry
|
||||
for(mapping++; mapping->name; mapping++) {
|
||||
if(strcmp(mapping->name, mjs_get_string(mjs, val, NULL)) == 0) {
|
||||
if(size == 1)
|
||||
*(uint8_t*)out = mapping->value;
|
||||
else if(size == 2)
|
||||
*(uint16_t*)out = mapping->value;
|
||||
else if(size == 4)
|
||||
*(uint32_t*)out = mapping->value;
|
||||
else if(size == 8)
|
||||
*(uint64_t*)out = mapping->value;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// unreachable, thanks to _js_validate_enum
|
||||
}
|
||||
#define JS_ARG_ENUM(var_name, name) \
|
||||
((_js_arg_decl){ \
|
||||
&var_name, \
|
||||
mjs_is_string, \
|
||||
_js_convert_enum, \
|
||||
name " enum", \
|
||||
_js_validate_enum, \
|
||||
var_name##_mapping})
|
||||
|
||||
//-V:JS_FETCH_ARGS_OR_RETURN:1008
|
||||
/**
|
||||
* @brief Fetches and validates the arguments passed to a JS function
|
||||
*
|
||||
* Example: `int32_t my_arg; JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&my_arg));`
|
||||
*
|
||||
* @warning This macro executes `return;` by design in case of an argument count
|
||||
* mismatch or a validation failure
|
||||
*/
|
||||
#define JS_FETCH_ARGS_OR_RETURN(mjs, arg_operator, ...) \
|
||||
_js_arg_decl _js_args[] = {__VA_ARGS__}; \
|
||||
int _js_arg_cnt = COUNT_OF(_js_args); \
|
||||
mjs_val_t _js_arg_vals[_js_arg_cnt]; \
|
||||
if(!(mjs_nargs(mjs) arg_operator _js_arg_cnt)) \
|
||||
JS_ERROR_AND_RETURN( \
|
||||
mjs, \
|
||||
MJS_BAD_ARGS_ERROR, \
|
||||
"expected %s%d arguments, got %d", \
|
||||
#arg_operator, \
|
||||
_js_arg_cnt, \
|
||||
mjs_nargs(mjs)); \
|
||||
for(int _i = 0; _i < _js_arg_cnt; _i++) { \
|
||||
_js_arg_vals[_i] = mjs_arg(mjs, _i); \
|
||||
if(_js_args[_i].validator) \
|
||||
if(!_js_args[_i].validator(_js_arg_vals[_i])) \
|
||||
JS_ERROR_AND_RETURN( \
|
||||
mjs, \
|
||||
MJS_BAD_ARGS_ERROR, \
|
||||
"argument %d: expected %s", \
|
||||
_i, \
|
||||
_js_args[_i].expected_type); \
|
||||
if(_js_args[_i].extended_validator) \
|
||||
if(!_js_args[_i].extended_validator(mjs, _js_arg_vals[_i], _js_args[_i].extra_data)) \
|
||||
JS_ERROR_AND_RETURN( \
|
||||
mjs, \
|
||||
MJS_BAD_ARGS_ERROR, \
|
||||
"argument %d: expected %s", \
|
||||
_i, \
|
||||
_js_args[_i].expected_type); \
|
||||
_js_args[_i].converter( \
|
||||
mjs, &_js_arg_vals[_i], _js_args[_i].out, _js_args[_i].extra_data); \
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Prepends an error, sets the JS return value to `undefined` and returns
|
||||
* from the C function
|
||||
* @warning This macro executes `return;` by design
|
||||
*/
|
||||
#define JS_ERROR_AND_RETURN(mjs, error_code, ...) \
|
||||
do { \
|
||||
mjs_prepend_errorf(mjs, error_code, __VA_ARGS__); \
|
||||
mjs_return(mjs, MJS_UNDEFINED); \
|
||||
return; \
|
||||
} while(0)
|
||||
|
||||
typedef struct JsModules JsModules;
|
||||
|
||||
typedef void* (*JsModuleConstructor)(struct mjs* mjs, mjs_val_t* object, JsModules* modules);
|
||||
typedef void (*JsModuleDestructor)(void* inst);
|
||||
|
||||
typedef struct {
|
||||
char* name;
|
||||
JsModeConstructor create;
|
||||
JsModeDestructor destroy;
|
||||
JsModuleConstructor create;
|
||||
JsModuleDestructor destroy;
|
||||
const ElfApiInterface* api_interface;
|
||||
} JsModuleDescriptor;
|
||||
|
||||
typedef struct JsModules JsModules;
|
||||
|
||||
JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver);
|
||||
|
||||
void js_modules_destroy(JsModules* modules);
|
||||
|
||||
mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_len);
|
||||
|
||||
/**
|
||||
* @brief Gets a module instance by its name
|
||||
* This is useful when a module wants to access a stateful API of another
|
||||
* module.
|
||||
* @returns Pointer to module context, NULL if the module is not instantiated
|
||||
*/
|
||||
void* js_module_get(JsModules* modules, const char* name);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <common/cs_dbg.h>
|
||||
#include <toolbox/path.h>
|
||||
#include <toolbox/stream/file_stream.h>
|
||||
#include <toolbox/strint.h>
|
||||
#include <loader/firmware_api/firmware_api.h>
|
||||
#include <flipper_application/api_hashtable/api_hashtable.h>
|
||||
#include <flipper_application/plugins/composite_resolver.h>
|
||||
@@ -195,104 +196,21 @@ static void js_require(struct mjs* mjs) {
|
||||
mjs_return(mjs, req_object);
|
||||
}
|
||||
|
||||
static void js_global_to_string(struct mjs* mjs) {
|
||||
double num = mjs_get_double(mjs, mjs_arg(mjs, 0));
|
||||
char tmp_str[] = "-2147483648";
|
||||
itoa(num, tmp_str, 10);
|
||||
mjs_val_t ret = mjs_mk_string(mjs, tmp_str, ~0, true);
|
||||
mjs_return(mjs, ret);
|
||||
}
|
||||
|
||||
static void js_global_to_hex_string(struct mjs* mjs) {
|
||||
double num = mjs_get_int(mjs, mjs_arg(mjs, 0));
|
||||
char tmp_str[] = "-FFFFFFFF";
|
||||
itoa(num, tmp_str, 16);
|
||||
mjs_val_t ret = mjs_mk_string(mjs, tmp_str, ~0, true);
|
||||
mjs_return(mjs, ret);
|
||||
}
|
||||
|
||||
static void js_parse_int(struct mjs* mjs) {
|
||||
mjs_val_t arg = mjs_arg(mjs, 0);
|
||||
if(!mjs_is_string(arg)) {
|
||||
mjs_return(mjs, mjs_mk_number(mjs, 0));
|
||||
return;
|
||||
const char* str;
|
||||
int32_t base = 10;
|
||||
if(mjs_nargs(mjs) == 1) {
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&str));
|
||||
} else {
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&str), JS_ARG_INT32(&base));
|
||||
}
|
||||
size_t str_len = 0;
|
||||
const char* str = mjs_get_string(mjs, &arg, &str_len);
|
||||
if((str_len == 0) || (str == NULL)) {
|
||||
mjs_return(mjs, mjs_mk_number(mjs, 0));
|
||||
return;
|
||||
int32_t num;
|
||||
if(strint_to_int32(str, NULL, &num, base) != StrintParseNoError) {
|
||||
num = 0;
|
||||
}
|
||||
|
||||
int32_t num = 0;
|
||||
int32_t sign = 1;
|
||||
size_t i = 0;
|
||||
|
||||
if(str[0] == '-') {
|
||||
sign = -1;
|
||||
i = 1;
|
||||
} else if(str[0] == '+') {
|
||||
i = 1;
|
||||
}
|
||||
|
||||
for(; i < str_len; i++) {
|
||||
if(str[i] >= '0' && str[i] <= '9') {
|
||||
num = num * 10 + (str[i] - '0');
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
num *= sign;
|
||||
|
||||
mjs_return(mjs, mjs_mk_number(mjs, num));
|
||||
}
|
||||
|
||||
static void js_to_upper_case(struct mjs* mjs) {
|
||||
mjs_val_t arg0 = mjs_arg(mjs, 0);
|
||||
|
||||
size_t str_len;
|
||||
const char* str = NULL;
|
||||
if(mjs_is_string(arg0)) {
|
||||
str = mjs_get_string(mjs, &arg0, &str_len);
|
||||
}
|
||||
if(!str) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
char* upperStr = strdup(str);
|
||||
for(size_t i = 0; i < str_len; i++) {
|
||||
upperStr[i] = toupper(upperStr[i]);
|
||||
}
|
||||
|
||||
mjs_val_t resultStr = mjs_mk_string(mjs, upperStr, ~0, true);
|
||||
free(upperStr);
|
||||
mjs_return(mjs, resultStr);
|
||||
}
|
||||
|
||||
static void js_to_lower_case(struct mjs* mjs) {
|
||||
mjs_val_t arg0 = mjs_arg(mjs, 0);
|
||||
|
||||
size_t str_len;
|
||||
const char* str = NULL;
|
||||
if(mjs_is_string(arg0)) {
|
||||
str = mjs_get_string(mjs, &arg0, &str_len);
|
||||
}
|
||||
if(!str) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
char* lowerStr = strdup(str);
|
||||
for(size_t i = 0; i < str_len; i++) {
|
||||
lowerStr[i] = tolower(lowerStr[i]);
|
||||
}
|
||||
|
||||
mjs_val_t resultStr = mjs_mk_string(mjs, lowerStr, ~0, true);
|
||||
free(lowerStr);
|
||||
mjs_return(mjs, resultStr);
|
||||
}
|
||||
|
||||
#ifdef JS_DEBUG
|
||||
static void js_dump_write_callback(void* ctx, const char* format, ...) {
|
||||
File* file = ctx;
|
||||
@@ -326,27 +244,23 @@ static int32_t js_thread(void* arg) {
|
||||
mjs_set(
|
||||
mjs,
|
||||
global,
|
||||
"__filepath",
|
||||
"__filename",
|
||||
~0,
|
||||
mjs_mk_string(
|
||||
mjs, furi_string_get_cstr(worker->path), furi_string_size(worker->path), true));
|
||||
mjs_set(
|
||||
mjs,
|
||||
global,
|
||||
"__dirpath",
|
||||
"__dirname",
|
||||
~0,
|
||||
mjs_mk_string(mjs, furi_string_get_cstr(dirpath), furi_string_size(dirpath), true));
|
||||
furi_string_free(dirpath);
|
||||
}
|
||||
mjs_set(mjs, global, "print", ~0, MJS_MK_FN(js_print));
|
||||
mjs_set(mjs, global, "delay", ~0, MJS_MK_FN(js_delay));
|
||||
mjs_set(mjs, global, "to_string", ~0, MJS_MK_FN(js_global_to_string));
|
||||
mjs_set(mjs, global, "to_hex_string", ~0, MJS_MK_FN(js_global_to_hex_string));
|
||||
mjs_set(mjs, global, "ffi_address", ~0, MJS_MK_FN(js_ffi_address));
|
||||
mjs_set(mjs, global, "require", ~0, MJS_MK_FN(js_require));
|
||||
mjs_set(mjs, global, "parse_int", ~0, MJS_MK_FN(js_parse_int));
|
||||
mjs_set(mjs, global, "to_upper_case", ~0, MJS_MK_FN(js_to_upper_case));
|
||||
mjs_set(mjs, global, "to_lower_case", ~0, MJS_MK_FN(js_to_lower_case));
|
||||
mjs_set(mjs, global, "parseInt", ~0, MJS_MK_FN(js_parse_int));
|
||||
|
||||
mjs_val_t console_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, console_obj, "log", ~0, MJS_MK_FN(js_console_log));
|
||||
@@ -400,8 +314,8 @@ static int32_t js_thread(void* arg) {
|
||||
}
|
||||
}
|
||||
|
||||
js_modules_destroy(worker->modules);
|
||||
mjs_destroy(mjs);
|
||||
js_modules_destroy(worker->modules);
|
||||
|
||||
composite_api_resolver_free(worker->resolver);
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct JsThread JsThread;
|
||||
|
||||
typedef enum {
|
||||
@@ -14,3 +18,7 @@ typedef void (*JsThreadCallback)(JsThreadEvent event, const char* msg, void* con
|
||||
JsThread* js_thread_run(const char* script_path, JsThreadCallback callback, void* context);
|
||||
|
||||
void js_thread_stop(JsThread* worker);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -102,9 +102,9 @@ static bool setup_parse_params(
|
||||
}
|
||||
mjs_val_t vid_obj = mjs_get(mjs, arg, "vid", ~0);
|
||||
mjs_val_t pid_obj = mjs_get(mjs, arg, "pid", ~0);
|
||||
mjs_val_t mfr_obj = mjs_get(mjs, arg, "mfr_name", ~0);
|
||||
mjs_val_t prod_obj = mjs_get(mjs, arg, "prod_name", ~0);
|
||||
mjs_val_t layout_obj = mjs_get(mjs, arg, "layout_path", ~0);
|
||||
mjs_val_t mfr_obj = mjs_get(mjs, arg, "mfrName", ~0);
|
||||
mjs_val_t prod_obj = mjs_get(mjs, arg, "prodName", ~0);
|
||||
mjs_val_t layout_obj = mjs_get(mjs, arg, "layoutPath", ~0);
|
||||
|
||||
if(mjs_is_number(vid_obj) && mjs_is_number(pid_obj)) {
|
||||
hid_cfg->vid = mjs_get_int32(mjs, vid_obj);
|
||||
@@ -221,7 +221,7 @@ static void js_badusb_is_connected(struct mjs* mjs) {
|
||||
|
||||
uint16_t get_keycode_by_name(JsBadusbInst* badusb, const char* key_name, size_t name_len) {
|
||||
if(name_len == 1) { // Single char
|
||||
return ASCII_TO_KEY(badusb->layout, key_name[0]);
|
||||
return (ASCII_TO_KEY(badusb->layout, key_name[0]));
|
||||
}
|
||||
|
||||
for(size_t i = 0; i < COUNT_OF(key_codes); i++) {
|
||||
@@ -486,7 +486,8 @@ static void js_badusb_alt_println(struct mjs* mjs) {
|
||||
badusb_print(mjs, true, true);
|
||||
}
|
||||
|
||||
static void* js_badusb_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
static void* js_badusb_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
JsBadusbInst* badusb = malloc(sizeof(JsBadusbInst));
|
||||
mjs_val_t badusb_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, badusb_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, badusb));
|
||||
@@ -514,6 +515,7 @@ static const JsModuleDescriptor js_badusb_desc = {
|
||||
"badusb",
|
||||
js_badusb_create,
|
||||
js_badusb_destroy,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
|
||||
@@ -193,7 +193,8 @@ static void js_blebeacon_keep_alive(struct mjs* mjs) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void* js_blebeacon_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
static void* js_blebeacon_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
JsBlebeaconInst* blebeacon = malloc(sizeof(JsBlebeaconInst));
|
||||
mjs_val_t blebeacon_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, blebeacon_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, blebeacon));
|
||||
@@ -231,6 +232,7 @@ static const JsModuleDescriptor js_blebeacon_desc = {
|
||||
"blebeacon",
|
||||
js_blebeacon_create,
|
||||
js_blebeacon_destroy,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
#include <core/common_defines.h>
|
||||
#include "../js_modules.h"
|
||||
#include <dialogs/dialogs.h>
|
||||
#include <assets_icons.h>
|
||||
|
||||
static bool js_dialog_msg_parse_params(struct mjs* mjs, const char** hdr, const char** msg) {
|
||||
size_t num_args = mjs_nargs(mjs);
|
||||
if(num_args != 2) {
|
||||
return false;
|
||||
}
|
||||
mjs_val_t header_obj = mjs_arg(mjs, 0);
|
||||
mjs_val_t msg_obj = mjs_arg(mjs, 1);
|
||||
if((!mjs_is_string(header_obj)) || (!mjs_is_string(msg_obj))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t arg_len = 0;
|
||||
*hdr = mjs_get_string(mjs, &header_obj, &arg_len);
|
||||
if(arg_len == 0) {
|
||||
*hdr = NULL;
|
||||
}
|
||||
|
||||
*msg = mjs_get_string(mjs, &msg_obj, &arg_len);
|
||||
if(arg_len == 0) {
|
||||
*msg = NULL;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void js_dialog_message(struct mjs* mjs) {
|
||||
const char* dialog_header = NULL;
|
||||
const char* dialog_msg = NULL;
|
||||
if(!js_dialog_msg_parse_params(mjs, &dialog_header, &dialog_msg)) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
|
||||
DialogMessage* message = dialog_message_alloc();
|
||||
dialog_message_set_buttons(message, NULL, "OK", NULL);
|
||||
if(dialog_header) {
|
||||
dialog_message_set_header(message, dialog_header, 64, 3, AlignCenter, AlignTop);
|
||||
}
|
||||
if(dialog_msg) {
|
||||
dialog_message_set_text(message, dialog_msg, 64, 26, AlignCenter, AlignTop);
|
||||
}
|
||||
DialogMessageButton result = dialog_message_show(dialogs, message);
|
||||
dialog_message_free(message);
|
||||
furi_record_close(RECORD_DIALOGS);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, result == DialogMessageButtonCenter));
|
||||
}
|
||||
|
||||
static void js_dialog_custom(struct mjs* mjs) {
|
||||
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
|
||||
DialogMessage* message = dialog_message_alloc();
|
||||
|
||||
bool params_correct = false;
|
||||
|
||||
do {
|
||||
if(mjs_nargs(mjs) != 1) {
|
||||
break;
|
||||
}
|
||||
mjs_val_t params_obj = mjs_arg(mjs, 0);
|
||||
if(!mjs_is_object(params_obj)) {
|
||||
break;
|
||||
}
|
||||
|
||||
mjs_val_t text_obj = mjs_get(mjs, params_obj, "header", ~0);
|
||||
size_t arg_len = 0;
|
||||
const char* text_str = mjs_get_string(mjs, &text_obj, &arg_len);
|
||||
if(arg_len == 0) {
|
||||
text_str = NULL;
|
||||
}
|
||||
if(text_str) {
|
||||
dialog_message_set_header(message, text_str, 64, 3, AlignCenter, AlignTop);
|
||||
}
|
||||
|
||||
text_obj = mjs_get(mjs, params_obj, "text", ~0);
|
||||
text_str = mjs_get_string(mjs, &text_obj, &arg_len);
|
||||
if(arg_len == 0) {
|
||||
text_str = NULL;
|
||||
}
|
||||
if(text_str) {
|
||||
dialog_message_set_text(message, text_str, 64, 26, AlignCenter, AlignTop);
|
||||
}
|
||||
|
||||
mjs_val_t btn_obj[3] = {
|
||||
mjs_get(mjs, params_obj, "button_left", ~0),
|
||||
mjs_get(mjs, params_obj, "button_center", ~0),
|
||||
mjs_get(mjs, params_obj, "button_right", ~0),
|
||||
};
|
||||
const char* btn_text[3] = {NULL, NULL, NULL};
|
||||
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
if(!mjs_is_string(btn_obj[i])) {
|
||||
continue;
|
||||
}
|
||||
btn_text[i] = mjs_get_string(mjs, &btn_obj[i], &arg_len);
|
||||
if(arg_len == 0) {
|
||||
btn_text[i] = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
dialog_message_set_buttons(message, btn_text[0], btn_text[1], btn_text[2]);
|
||||
|
||||
DialogMessageButton result = dialog_message_show(dialogs, message);
|
||||
mjs_val_t return_obj = MJS_UNDEFINED;
|
||||
if(result == DialogMessageButtonLeft) {
|
||||
return_obj = mjs_mk_string(mjs, btn_text[0], ~0, true);
|
||||
} else if(result == DialogMessageButtonCenter) {
|
||||
return_obj = mjs_mk_string(mjs, btn_text[1], ~0, true);
|
||||
} else if(result == DialogMessageButtonRight) {
|
||||
return_obj = mjs_mk_string(mjs, btn_text[2], ~0, true);
|
||||
} else {
|
||||
return_obj = mjs_mk_string(mjs, "", ~0, true);
|
||||
}
|
||||
|
||||
mjs_return(mjs, return_obj);
|
||||
params_correct = true;
|
||||
} while(0);
|
||||
|
||||
dialog_message_free(message);
|
||||
furi_record_close(RECORD_DIALOGS);
|
||||
|
||||
if(!params_correct) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
}
|
||||
|
||||
static void js_dialog_pick_file(struct mjs* mjs) {
|
||||
if(mjs_nargs(mjs) != 2) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Wrong arguments");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
mjs_val_t base_path_obj = mjs_arg(mjs, 0);
|
||||
if(!mjs_is_string(base_path_obj)) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Base path must be a string");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
size_t base_path_len = 0;
|
||||
const char* base_path = mjs_get_string(mjs, &base_path_obj, &base_path_len);
|
||||
if((base_path_len == 0) || (base_path == NULL)) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Bad base path argument");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
mjs_val_t extension_obj = mjs_arg(mjs, 1);
|
||||
if(!mjs_is_string(extension_obj)) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Extension must be a string");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
size_t extension_len = 0;
|
||||
const char* extension = mjs_get_string(mjs, &extension_obj, &extension_len);
|
||||
if((extension_len == 0) || (extension == NULL)) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Bad extension argument");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
|
||||
const DialogsFileBrowserOptions browser_options = {
|
||||
.extension = extension,
|
||||
.icon = &I_file_10px,
|
||||
.base_path = base_path,
|
||||
};
|
||||
FuriString* path = furi_string_alloc_set(base_path);
|
||||
if(dialog_file_browser_show(dialogs, path, path, &browser_options)) {
|
||||
mjs_return(mjs, mjs_mk_string(mjs, furi_string_get_cstr(path), ~0, true));
|
||||
} else {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
furi_string_free(path);
|
||||
furi_record_close(RECORD_DIALOGS);
|
||||
}
|
||||
|
||||
static void* js_dialog_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
mjs_val_t dialog_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, dialog_obj, "message", ~0, MJS_MK_FN(js_dialog_message));
|
||||
mjs_set(mjs, dialog_obj, "custom", ~0, MJS_MK_FN(js_dialog_custom));
|
||||
mjs_set(mjs, dialog_obj, "pickFile", ~0, MJS_MK_FN(js_dialog_pick_file));
|
||||
*object = dialog_obj;
|
||||
|
||||
return (void*)1;
|
||||
}
|
||||
|
||||
static const JsModuleDescriptor js_dialog_desc = {
|
||||
"dialog",
|
||||
js_dialog_create,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
.appid = PLUGIN_APP_ID,
|
||||
.ep_api_version = PLUGIN_API_VERSION,
|
||||
.entry_point = &js_dialog_desc,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* js_dialog_ep(void) {
|
||||
return &plugin_descriptor;
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
#include "js_event_loop.h"
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include <expansion/expansion.h>
|
||||
#include <mlib/m-array.h>
|
||||
|
||||
/**
|
||||
* @brief Number of arguments that callbacks receive from this module that they can't modify
|
||||
*/
|
||||
#define SYSTEM_ARGS 2
|
||||
|
||||
/**
|
||||
* @brief Context passed to the generic event callback
|
||||
*/
|
||||
typedef struct {
|
||||
JsEventLoopObjectType object_type;
|
||||
|
||||
struct mjs* mjs;
|
||||
mjs_val_t callback;
|
||||
// NOTE: not using an mlib array because resizing is not needed.
|
||||
mjs_val_t* arguments;
|
||||
size_t arity;
|
||||
|
||||
JsEventLoopTransformer transformer;
|
||||
void* transformer_context;
|
||||
} JsEventLoopCallbackContext;
|
||||
|
||||
/**
|
||||
* @brief Contains data needed to cancel a subscription
|
||||
*/
|
||||
typedef struct {
|
||||
FuriEventLoop* loop;
|
||||
JsEventLoopObjectType object_type;
|
||||
FuriEventLoopObject* object;
|
||||
JsEventLoopCallbackContext* context;
|
||||
JsEventLoopContract* contract;
|
||||
void* subscriptions; // SubscriptionArray_t, which we can't reference in this definition
|
||||
} JsEventLoopSubscription;
|
||||
|
||||
typedef struct {
|
||||
FuriEventLoop* loop;
|
||||
struct mjs* mjs;
|
||||
} JsEventLoopTickContext;
|
||||
|
||||
ARRAY_DEF(SubscriptionArray, JsEventLoopSubscription*, M_PTR_OPLIST); //-V575
|
||||
ARRAY_DEF(ContractArray, JsEventLoopContract*, M_PTR_OPLIST); //-V575
|
||||
|
||||
/**
|
||||
* @brief Per-module instance control structure
|
||||
*/
|
||||
struct JsEventLoop {
|
||||
FuriEventLoop* loop;
|
||||
SubscriptionArray_t subscriptions;
|
||||
ContractArray_t owned_contracts; //<! Contracts that were produced by this module
|
||||
JsEventLoopTickContext* tick_context;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Generic event callback, handles all events by calling the JS callbacks
|
||||
*/
|
||||
static void js_event_loop_callback_generic(void* param) {
|
||||
JsEventLoopCallbackContext* context = param;
|
||||
mjs_val_t result;
|
||||
mjs_apply(
|
||||
context->mjs,
|
||||
&result,
|
||||
context->callback,
|
||||
MJS_UNDEFINED,
|
||||
context->arity,
|
||||
context->arguments);
|
||||
|
||||
// save returned args for next call
|
||||
if(mjs_array_length(context->mjs, result) != context->arity - SYSTEM_ARGS) return;
|
||||
for(size_t i = 0; i < context->arity - SYSTEM_ARGS; i++) {
|
||||
mjs_disown(context->mjs, &context->arguments[i + SYSTEM_ARGS]);
|
||||
context->arguments[i + SYSTEM_ARGS] = mjs_array_get(context->mjs, result, i);
|
||||
mjs_own(context->mjs, &context->arguments[i + SYSTEM_ARGS]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handles non-timer events
|
||||
*/
|
||||
static bool js_event_loop_callback(void* object, void* param) {
|
||||
JsEventLoopCallbackContext* context = param;
|
||||
|
||||
if(context->transformer) {
|
||||
mjs_disown(context->mjs, &context->arguments[1]);
|
||||
context->arguments[1] =
|
||||
context->transformer(context->mjs, object, context->transformer_context);
|
||||
mjs_own(context->mjs, &context->arguments[1]);
|
||||
} else {
|
||||
// default behavior: take semaphores and mutexes
|
||||
switch(context->object_type) {
|
||||
case JsEventLoopObjectTypeSemaphore: {
|
||||
FuriSemaphore* semaphore = object;
|
||||
furi_check(furi_semaphore_acquire(semaphore, 0) == FuriStatusOk);
|
||||
} break;
|
||||
default:
|
||||
// the corresponding check has been performed when we were given the contract
|
||||
furi_crash();
|
||||
}
|
||||
}
|
||||
|
||||
js_event_loop_callback_generic(param);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cancels an event subscription
|
||||
*/
|
||||
static void js_event_loop_subscription_cancel(struct mjs* mjs) {
|
||||
JsEventLoopSubscription* subscription = JS_GET_CONTEXT(mjs);
|
||||
|
||||
if(subscription->object_type == JsEventLoopObjectTypeTimer) {
|
||||
furi_event_loop_timer_stop(subscription->object);
|
||||
} else {
|
||||
furi_event_loop_unsubscribe(subscription->loop, subscription->object);
|
||||
}
|
||||
|
||||
free(subscription->context->arguments);
|
||||
free(subscription->context);
|
||||
|
||||
// find and remove ourselves from the array
|
||||
SubscriptionArray_it_t iterator;
|
||||
for(SubscriptionArray_it(iterator, subscription->subscriptions);
|
||||
!SubscriptionArray_end_p(iterator);
|
||||
SubscriptionArray_next(iterator)) {
|
||||
JsEventLoopSubscription* item = *SubscriptionArray_cref(iterator);
|
||||
if(item == subscription) break;
|
||||
}
|
||||
SubscriptionArray_remove(subscription->subscriptions, iterator);
|
||||
free(subscription);
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Subscribes a JavaScript function to an event
|
||||
*/
|
||||
static void js_event_loop_subscribe(struct mjs* mjs) {
|
||||
JsEventLoop* module = JS_GET_CONTEXT(mjs);
|
||||
|
||||
// get arguments
|
||||
JsEventLoopContract* contract;
|
||||
mjs_val_t callback;
|
||||
JS_FETCH_ARGS_OR_RETURN(
|
||||
mjs, JS_AT_LEAST, JS_ARG_STRUCT(JsEventLoopContract, &contract), JS_ARG_FN(&callback));
|
||||
|
||||
// create subscription object
|
||||
JsEventLoopSubscription* subscription = malloc(sizeof(JsEventLoopSubscription));
|
||||
JsEventLoopCallbackContext* context = malloc(sizeof(JsEventLoopCallbackContext));
|
||||
subscription->loop = module->loop;
|
||||
subscription->object_type = contract->object_type;
|
||||
subscription->context = context;
|
||||
subscription->subscriptions = module->subscriptions;
|
||||
if(contract->object_type == JsEventLoopObjectTypeTimer) subscription->contract = contract;
|
||||
mjs_val_t subscription_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, subscription_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, subscription));
|
||||
mjs_set(mjs, subscription_obj, "cancel", ~0, MJS_MK_FN(js_event_loop_subscription_cancel));
|
||||
|
||||
// create callback context
|
||||
context->object_type = contract->object_type;
|
||||
context->arity = mjs_nargs(mjs) - SYSTEM_ARGS + 2;
|
||||
context->arguments = calloc(context->arity, sizeof(mjs_val_t));
|
||||
context->arguments[0] = subscription_obj;
|
||||
context->arguments[1] = MJS_UNDEFINED;
|
||||
for(size_t i = SYSTEM_ARGS; i < context->arity; i++) {
|
||||
mjs_val_t arg = mjs_arg(mjs, i - SYSTEM_ARGS + 2);
|
||||
context->arguments[i] = arg;
|
||||
mjs_own(mjs, &context->arguments[i]);
|
||||
}
|
||||
context->mjs = mjs;
|
||||
context->callback = callback;
|
||||
mjs_own(mjs, &context->callback);
|
||||
mjs_own(mjs, &context->arguments[0]);
|
||||
mjs_own(mjs, &context->arguments[1]);
|
||||
|
||||
// queue and stream contracts must have a transform callback, others are allowed to delegate
|
||||
// the obvious default behavior to this module
|
||||
if(contract->object_type == JsEventLoopObjectTypeQueue ||
|
||||
contract->object_type == JsEventLoopObjectTypeStream) {
|
||||
furi_check(contract->non_timer.transformer);
|
||||
}
|
||||
context->transformer = contract->non_timer.transformer;
|
||||
context->transformer_context = contract->non_timer.transformer_context;
|
||||
|
||||
// subscribe
|
||||
switch(contract->object_type) {
|
||||
case JsEventLoopObjectTypeTimer: {
|
||||
FuriEventLoopTimer* timer = furi_event_loop_timer_alloc(
|
||||
module->loop, js_event_loop_callback_generic, contract->timer.type, context);
|
||||
furi_event_loop_timer_start(timer, contract->timer.interval_ticks);
|
||||
contract->object = timer;
|
||||
} break;
|
||||
case JsEventLoopObjectTypeSemaphore:
|
||||
furi_event_loop_subscribe_semaphore(
|
||||
module->loop,
|
||||
contract->object,
|
||||
contract->non_timer.event,
|
||||
js_event_loop_callback,
|
||||
context);
|
||||
break;
|
||||
case JsEventLoopObjectTypeQueue:
|
||||
furi_event_loop_subscribe_message_queue(
|
||||
module->loop,
|
||||
contract->object,
|
||||
contract->non_timer.event,
|
||||
js_event_loop_callback,
|
||||
context);
|
||||
break;
|
||||
default:
|
||||
furi_crash("unimplemented");
|
||||
}
|
||||
|
||||
subscription->object = contract->object;
|
||||
SubscriptionArray_push_back(module->subscriptions, subscription);
|
||||
mjs_return(mjs, subscription_obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Runs the event loop until it is stopped
|
||||
*/
|
||||
static void js_event_loop_run(struct mjs* mjs) {
|
||||
JsEventLoop* module = JS_GET_CONTEXT(mjs);
|
||||
furi_event_loop_run(module->loop);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stops a running event loop
|
||||
*/
|
||||
static void js_event_loop_stop(struct mjs* mjs) {
|
||||
JsEventLoop* module = JS_GET_CONTEXT(mjs);
|
||||
furi_event_loop_stop(module->loop);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Creates a timer event that can be subscribed to just like any other
|
||||
* event
|
||||
*/
|
||||
static void js_event_loop_timer(struct mjs* mjs) {
|
||||
// get arguments
|
||||
const char* mode_str;
|
||||
int32_t interval;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&mode_str), JS_ARG_INT32(&interval));
|
||||
JsEventLoop* module = JS_GET_CONTEXT(mjs);
|
||||
|
||||
FuriEventLoopTimerType mode;
|
||||
if(strcasecmp(mode_str, "periodic") == 0) {
|
||||
mode = FuriEventLoopTimerTypePeriodic;
|
||||
} else if(strcasecmp(mode_str, "oneshot") == 0) {
|
||||
mode = FuriEventLoopTimerTypeOnce;
|
||||
} else {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "argument 0: unknown mode");
|
||||
}
|
||||
|
||||
// make timer contract
|
||||
JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract));
|
||||
*contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object_type = JsEventLoopObjectTypeTimer,
|
||||
.object = NULL,
|
||||
.timer =
|
||||
{
|
||||
.interval_ticks = furi_ms_to_ticks((uint32_t)interval),
|
||||
.type = mode,
|
||||
},
|
||||
};
|
||||
ContractArray_push_back(module->owned_contracts, contract);
|
||||
mjs_return(mjs, mjs_mk_foreign(mjs, contract));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Queue transformer. Takes `mjs_val_t` pointers out of a queue and
|
||||
* returns their dereferenced value
|
||||
*/
|
||||
static mjs_val_t
|
||||
js_event_loop_queue_transformer(struct mjs* mjs, FuriEventLoopObject* object, void* context) {
|
||||
UNUSED(context);
|
||||
mjs_val_t* message_ptr;
|
||||
furi_check(furi_message_queue_get(object, &message_ptr, 0) == FuriStatusOk);
|
||||
mjs_val_t message = *message_ptr;
|
||||
mjs_disown(mjs, message_ptr);
|
||||
free(message_ptr);
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sends a message to a queue
|
||||
*/
|
||||
static void js_event_loop_queue_send(struct mjs* mjs) {
|
||||
// get arguments
|
||||
mjs_val_t message;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&message));
|
||||
JsEventLoopContract* contract = JS_GET_CONTEXT(mjs);
|
||||
|
||||
// send message
|
||||
mjs_val_t* message_ptr = malloc(sizeof(mjs_val_t));
|
||||
*message_ptr = message;
|
||||
mjs_own(mjs, message_ptr);
|
||||
furi_message_queue_put(contract->object, &message_ptr, 0);
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Creates a queue
|
||||
*/
|
||||
static void js_event_loop_queue(struct mjs* mjs) {
|
||||
// get arguments
|
||||
int32_t length;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&length));
|
||||
JsEventLoop* module = JS_GET_CONTEXT(mjs);
|
||||
|
||||
// make queue contract
|
||||
JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract));
|
||||
*contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object_type = JsEventLoopObjectTypeQueue,
|
||||
// we could store `mjs_val_t`s in the queue directly if not for mJS' requirement to have consistent pointers to owned values
|
||||
.object = furi_message_queue_alloc((size_t)length, sizeof(mjs_val_t*)),
|
||||
.non_timer =
|
||||
{
|
||||
.event = FuriEventLoopEventIn,
|
||||
.transformer = js_event_loop_queue_transformer,
|
||||
},
|
||||
};
|
||||
ContractArray_push_back(module->owned_contracts, contract);
|
||||
|
||||
// return object with control methods
|
||||
mjs_val_t queue = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, queue, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, contract));
|
||||
mjs_set(mjs, queue, "input", ~0, mjs_mk_foreign(mjs, contract));
|
||||
mjs_set(mjs, queue, "send", ~0, MJS_MK_FN(js_event_loop_queue_send));
|
||||
mjs_return(mjs, queue);
|
||||
}
|
||||
|
||||
static void js_event_loop_tick(void* param) {
|
||||
JsEventLoopTickContext* context = param;
|
||||
uint32_t flags = furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny | FuriFlagNoClear, 0);
|
||||
if(flags & FuriFlagError) {
|
||||
return;
|
||||
}
|
||||
if(flags & ThreadEventStop) {
|
||||
furi_event_loop_stop(context->loop);
|
||||
mjs_exit(context->mjs);
|
||||
}
|
||||
}
|
||||
|
||||
static void* js_event_loop_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
mjs_val_t event_loop_obj = mjs_mk_object(mjs);
|
||||
JsEventLoop* module = malloc(sizeof(JsEventLoop));
|
||||
JsEventLoopTickContext* tick_ctx = malloc(sizeof(JsEventLoopTickContext));
|
||||
module->loop = furi_event_loop_alloc();
|
||||
tick_ctx->loop = module->loop;
|
||||
tick_ctx->mjs = mjs;
|
||||
module->tick_context = tick_ctx;
|
||||
furi_event_loop_tick_set(module->loop, 10, js_event_loop_tick, tick_ctx);
|
||||
SubscriptionArray_init(module->subscriptions);
|
||||
ContractArray_init(module->owned_contracts);
|
||||
|
||||
mjs_set(mjs, event_loop_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, module));
|
||||
mjs_set(mjs, event_loop_obj, "subscribe", ~0, MJS_MK_FN(js_event_loop_subscribe));
|
||||
mjs_set(mjs, event_loop_obj, "run", ~0, MJS_MK_FN(js_event_loop_run));
|
||||
mjs_set(mjs, event_loop_obj, "stop", ~0, MJS_MK_FN(js_event_loop_stop));
|
||||
mjs_set(mjs, event_loop_obj, "timer", ~0, MJS_MK_FN(js_event_loop_timer));
|
||||
mjs_set(mjs, event_loop_obj, "queue", ~0, MJS_MK_FN(js_event_loop_queue));
|
||||
|
||||
*object = event_loop_obj;
|
||||
return module;
|
||||
}
|
||||
|
||||
static void js_event_loop_destroy(void* inst) {
|
||||
if(inst) {
|
||||
JsEventLoop* module = inst;
|
||||
furi_event_loop_stop(module->loop);
|
||||
|
||||
// free subscriptions
|
||||
SubscriptionArray_it_t sub_iterator;
|
||||
for(SubscriptionArray_it(sub_iterator, module->subscriptions);
|
||||
!SubscriptionArray_end_p(sub_iterator);
|
||||
SubscriptionArray_next(sub_iterator)) {
|
||||
JsEventLoopSubscription* const* sub = SubscriptionArray_cref(sub_iterator);
|
||||
free((*sub)->context->arguments);
|
||||
free((*sub)->context);
|
||||
free(*sub);
|
||||
}
|
||||
SubscriptionArray_clear(module->subscriptions);
|
||||
|
||||
// free owned contracts
|
||||
ContractArray_it_t iterator;
|
||||
for(ContractArray_it(iterator, module->owned_contracts); !ContractArray_end_p(iterator);
|
||||
ContractArray_next(iterator)) {
|
||||
// unsubscribe object
|
||||
JsEventLoopContract* contract = *ContractArray_cref(iterator);
|
||||
if(contract->object_type == JsEventLoopObjectTypeTimer) {
|
||||
furi_event_loop_timer_stop(contract->object);
|
||||
} else {
|
||||
furi_event_loop_unsubscribe(module->loop, contract->object);
|
||||
}
|
||||
|
||||
// free object
|
||||
switch(contract->object_type) {
|
||||
case JsEventLoopObjectTypeTimer:
|
||||
furi_event_loop_timer_free(contract->object);
|
||||
break;
|
||||
case JsEventLoopObjectTypeSemaphore:
|
||||
furi_semaphore_free(contract->object);
|
||||
break;
|
||||
case JsEventLoopObjectTypeQueue:
|
||||
furi_message_queue_free(contract->object);
|
||||
break;
|
||||
default:
|
||||
furi_crash("unimplemented");
|
||||
}
|
||||
|
||||
free(contract);
|
||||
}
|
||||
ContractArray_clear(module->owned_contracts);
|
||||
|
||||
furi_event_loop_free(module->loop);
|
||||
free(module->tick_context);
|
||||
free(module);
|
||||
}
|
||||
}
|
||||
|
||||
extern const ElfApiInterface js_event_loop_hashtable_api_interface;
|
||||
|
||||
static const JsModuleDescriptor js_event_loop_desc = {
|
||||
"event_loop",
|
||||
js_event_loop_create,
|
||||
js_event_loop_destroy,
|
||||
&js_event_loop_hashtable_api_interface,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
.appid = PLUGIN_APP_ID,
|
||||
.ep_api_version = PLUGIN_API_VERSION,
|
||||
.entry_point = &js_event_loop_desc,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* js_event_loop_ep(void) {
|
||||
return &plugin_descriptor;
|
||||
}
|
||||
|
||||
FuriEventLoop* js_event_loop_get_loop(JsEventLoop* loop) {
|
||||
// porta: not the proudest function that i ever wrote
|
||||
furi_check(loop);
|
||||
return loop->loop;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include <furi/core/event_loop.h>
|
||||
#include <furi/core/event_loop_timer.h>
|
||||
|
||||
/**
|
||||
* @file js_event_loop.h
|
||||
*
|
||||
* In JS interpreter code, `js_event_loop` always creates and maintains the
|
||||
* event loop. There are two ways in which other modules can integrate with this
|
||||
* loop:
|
||||
* - Via contracts: The user of your module would have to acquire an opaque
|
||||
* JS value from you and pass it to `js_event_loop`. This is useful for
|
||||
* events that they user may be interested in. For more info, look at
|
||||
* `JsEventLoopContract`. Also look at `js_event_loop_get_loop`, which
|
||||
* you will need to unsubscribe the event loop from your object.
|
||||
* - Directly: When your module is created, you can acquire an instance of
|
||||
* `JsEventLoop` which you can use to acquire an instance of
|
||||
* `FuriEventLoop` that you can manipulate directly, without the JS
|
||||
* programmer having to pass contracts around. This is useful for
|
||||
* "behind-the-scenes" events that the user does not need to know about. For
|
||||
* more info, look at `js_event_loop_get_loop`.
|
||||
*
|
||||
* In both cases, your module is responsible for both instantiating,
|
||||
* unsubscribing and freeing the object that the event loop subscribes to.
|
||||
*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct JsEventLoop JsEventLoop;
|
||||
|
||||
typedef enum {
|
||||
JsEventLoopObjectTypeTimer,
|
||||
JsEventLoopObjectTypeQueue,
|
||||
JsEventLoopObjectTypeMutex,
|
||||
JsEventLoopObjectTypeSemaphore,
|
||||
JsEventLoopObjectTypeStream,
|
||||
} JsEventLoopObjectType;
|
||||
|
||||
typedef mjs_val_t (
|
||||
*JsEventLoopTransformer)(struct mjs* mjs, FuriEventLoopObject* object, void* context);
|
||||
|
||||
typedef struct {
|
||||
FuriEventLoopEvent event;
|
||||
JsEventLoopTransformer transformer;
|
||||
void* transformer_context;
|
||||
} JsEventLoopNonTimerContract;
|
||||
|
||||
typedef struct {
|
||||
FuriEventLoopTimerType type;
|
||||
uint32_t interval_ticks;
|
||||
} JsEventLoopTimerContract;
|
||||
|
||||
/**
|
||||
* @brief Adapter for other JS modules that wish to integrate with the event
|
||||
* loop JS module
|
||||
*
|
||||
* If another module wishes to integrate with `js_event_loop`, it needs to
|
||||
* implement a function callable from JS that returns an mJS foreign pointer to
|
||||
* an instance of this structure. This value is then read by `event_loop`'s
|
||||
* `subscribe` function.
|
||||
*
|
||||
* There are two fundamental variants of this structure:
|
||||
* - `object_type` is `JsEventLoopObjectTypeTimer`: the `timer` field is
|
||||
* valid, and the `non_timer` field is invalid.
|
||||
* - `object_type` is something else: the `timer` field is invalid, and the
|
||||
* `non_timer` field is valid. `non_timer.event` will be passed to
|
||||
* `furi_event_loop_subscribe`. `non_timer.transformer` will be called to
|
||||
* transform an object into a JS value (called an item) that's passed to the
|
||||
* JS callback. This is useful for example to take an item out of a message
|
||||
* queue and pass it to JS code in a convenient format. If
|
||||
* `non_timer.transformer` is NULL, the event loop will take semaphores and
|
||||
* mutexes on its own.
|
||||
*
|
||||
* The producer of the contract is responsible for freeing both the contract and
|
||||
* the object that it points to when the interpreter is torn down.
|
||||
*/
|
||||
typedef struct {
|
||||
JsForeignMagic magic; // <! `JsForeignMagic_JsEventLoopContract`
|
||||
JsEventLoopObjectType object_type;
|
||||
FuriEventLoopObject* object;
|
||||
union {
|
||||
JsEventLoopNonTimerContract non_timer;
|
||||
JsEventLoopTimerContract timer;
|
||||
};
|
||||
} JsEventLoopContract;
|
||||
|
||||
static_assert(offsetof(JsEventLoopContract, magic) == 0);
|
||||
|
||||
/**
|
||||
* @brief Gets the FuriEventLoop owned by a JsEventLoop
|
||||
*
|
||||
* This function is useful in case your JS module wishes to integrate with
|
||||
* the event loop without passing contracts through JS code. Your module will be
|
||||
* dynamically linked to this one if you use this function, but only if JS code
|
||||
* imports `event_loop` _before_ your module. An instance of `JsEventLoop` may
|
||||
* be obtained via `js_module_get`.
|
||||
*/
|
||||
FuriEventLoop* js_event_loop_get_loop(JsEventLoop* loop);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,16 @@
|
||||
#include <flipper_application/api_hashtable/api_hashtable.h>
|
||||
#include <flipper_application/api_hashtable/compilesort.hpp>
|
||||
|
||||
#include "js_event_loop_api_table_i.h"
|
||||
|
||||
static_assert(!has_hash_collisions(js_event_loop_api_table), "Detected API method hash collision!");
|
||||
|
||||
extern "C" constexpr HashtableApiInterface js_event_loop_hashtable_api_interface{
|
||||
{
|
||||
.api_version_major = 0,
|
||||
.api_version_minor = 0,
|
||||
.resolver_callback = &elf_resolve_from_hashtable,
|
||||
},
|
||||
js_event_loop_api_table.cbegin(),
|
||||
js_event_loop_api_table.cend(),
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
#include "js_event_loop.h"
|
||||
|
||||
static constexpr auto js_event_loop_api_table = sort(
|
||||
create_array_t<sym_entry>(API_METHOD(js_event_loop_get_loop, FuriEventLoop*, (JsEventLoop*))));
|
||||
@@ -25,7 +25,8 @@ static void js_flipper_get_battery(struct mjs* mjs) {
|
||||
mjs_return(mjs, mjs_mk_number(mjs, info.charge));
|
||||
}
|
||||
|
||||
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
mjs_val_t flipper_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, flipper_obj, "getModel", ~0, MJS_MK_FN(js_flipper_get_model));
|
||||
mjs_set(mjs, flipper_obj, "getName", ~0, MJS_MK_FN(js_flipper_get_name));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma once
|
||||
#include "../js_thread_i.h"
|
||||
|
||||
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object);
|
||||
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules);
|
||||
|
||||
@@ -1,387 +1,337 @@
|
||||
#include "../js_modules.h"
|
||||
#include "../js_modules.h" // IWYU pragma: keep
|
||||
#include "./js_event_loop/js_event_loop.h"
|
||||
#include <furi_hal_gpio.h>
|
||||
#include <furi_hal_resources.h>
|
||||
#include <expansion/expansion.h>
|
||||
#include <limits.h>
|
||||
#include <mlib/m-array.h>
|
||||
|
||||
typedef struct {
|
||||
FuriHalAdcHandle* handle;
|
||||
} JsGpioInst;
|
||||
#define INTERRUPT_QUEUE_LEN 16
|
||||
|
||||
/**
|
||||
* Per-pin control structure
|
||||
*/
|
||||
typedef struct {
|
||||
const GpioPin* pin;
|
||||
const char* name;
|
||||
const FuriHalAdcChannel channel;
|
||||
} GpioPinCtx;
|
||||
bool had_interrupt;
|
||||
FuriSemaphore* interrupt_semaphore;
|
||||
JsEventLoopContract* interrupt_contract;
|
||||
FuriHalAdcChannel adc_channel;
|
||||
FuriHalAdcHandle* adc_handle;
|
||||
} JsGpioPinInst;
|
||||
|
||||
static const GpioPinCtx js_gpio_pins[] = {
|
||||
{.pin = &gpio_ext_pa7, .name = "PA7", .channel = FuriHalAdcChannel12}, // 2
|
||||
{.pin = &gpio_ext_pa6, .name = "PA6", .channel = FuriHalAdcChannel11}, // 3
|
||||
{.pin = &gpio_ext_pa4, .name = "PA4", .channel = FuriHalAdcChannel9}, // 4
|
||||
{.pin = &gpio_ext_pb3, .name = "PB3", .channel = FuriHalAdcChannelNone}, // 5
|
||||
{.pin = &gpio_ext_pb2, .name = "PB2", .channel = FuriHalAdcChannelNone}, // 6
|
||||
{.pin = &gpio_ext_pc3, .name = "PC3", .channel = FuriHalAdcChannel4}, // 7
|
||||
{.pin = &gpio_swclk, .name = "PA14", .channel = FuriHalAdcChannelNone}, // 10
|
||||
{.pin = &gpio_swdio, .name = "PA13", .channel = FuriHalAdcChannelNone}, // 12
|
||||
{.pin = &gpio_usart_tx, .name = "PB6", .channel = FuriHalAdcChannelNone}, // 13
|
||||
{.pin = &gpio_usart_rx, .name = "PB7", .channel = FuriHalAdcChannelNone}, // 14
|
||||
{.pin = &gpio_ext_pc1, .name = "PC1", .channel = FuriHalAdcChannel2}, // 15
|
||||
{.pin = &gpio_ext_pc0, .name = "PC0", .channel = FuriHalAdcChannel1}, // 16
|
||||
{.pin = &gpio_ibutton, .name = "PB14", .channel = FuriHalAdcChannelNone}, // 17
|
||||
};
|
||||
ARRAY_DEF(ManagedPinsArray, JsGpioPinInst*, M_PTR_OPLIST); //-V575
|
||||
|
||||
bool js_gpio_get_gpio_pull(const char* pull, GpioPull* value) {
|
||||
if(strcmp(pull, "no") == 0) {
|
||||
*value = GpioPullNo;
|
||||
return true;
|
||||
} else if(strcmp(pull, "up") == 0) {
|
||||
*value = GpioPullUp;
|
||||
return true;
|
||||
} else if(strcmp(pull, "down") == 0) {
|
||||
*value = GpioPullDown;
|
||||
return true;
|
||||
} else {
|
||||
*value = GpioPullNo;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool js_gpio_get_gpio_mode(const char* mode, GpioMode* value) {
|
||||
if(strcmp(mode, "input") == 0) {
|
||||
*value = GpioModeInput;
|
||||
return true;
|
||||
} else if(strcmp(mode, "outputPushPull") == 0) {
|
||||
*value = GpioModeOutputPushPull;
|
||||
return true;
|
||||
} else if(strcmp(mode, "outputOpenDrain") == 0) {
|
||||
*value = GpioModeOutputOpenDrain;
|
||||
return true;
|
||||
} else if(strcmp(mode, "altFunctionPushPull") == 0) {
|
||||
*value = GpioModeAltFunctionPushPull;
|
||||
return true;
|
||||
} else if(strcmp(mode, "altFunctionOpenDrain") == 0) {
|
||||
*value = GpioModeAltFunctionOpenDrain;
|
||||
return true;
|
||||
} else if(strcmp(mode, "analog") == 0) {
|
||||
*value = GpioModeAnalog;
|
||||
return true;
|
||||
} else if(strcmp(mode, "interruptRise") == 0) {
|
||||
*value = GpioModeInterruptRise;
|
||||
return true;
|
||||
} else if(strcmp(mode, "interruptFall") == 0) {
|
||||
*value = GpioModeInterruptFall;
|
||||
return true;
|
||||
} else if(strcmp(mode, "interruptRiseFall") == 0) {
|
||||
*value = GpioModeInterruptRiseFall;
|
||||
return true;
|
||||
} else if(strcmp(mode, "eventRise") == 0) {
|
||||
*value = GpioModeEventRise;
|
||||
return true;
|
||||
} else if(strcmp(mode, "eventFall") == 0) {
|
||||
*value = GpioModeEventFall;
|
||||
return true;
|
||||
} else if(strcmp(mode, "eventRiseFall") == 0) {
|
||||
*value = GpioModeEventRiseFall;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const GpioPin* js_gpio_get_gpio_pin(const char* name) {
|
||||
for(size_t i = 0; i < COUNT_OF(js_gpio_pins); i++) {
|
||||
if(strcmp(js_gpio_pins[i].name, name) == 0) {
|
||||
return js_gpio_pins[i].pin;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
FuriHalAdcChannel js_gpio_get_gpio_channel(const char* name) {
|
||||
for(size_t i = 0; i < COUNT_OF(js_gpio_pins); i++) {
|
||||
if(strcmp(js_gpio_pins[i].name, name) == 0) {
|
||||
return js_gpio_pins[i].channel;
|
||||
}
|
||||
}
|
||||
return FuriHalAdcChannelNone;
|
||||
/**
|
||||
* Per-module instance control structure
|
||||
*/
|
||||
typedef struct {
|
||||
FuriEventLoop* loop;
|
||||
ManagedPinsArray_t managed_pins;
|
||||
FuriHalAdcHandle* adc_handle;
|
||||
} JsGpioInst;
|
||||
|
||||
/**
|
||||
* @brief Interrupt callback
|
||||
*/
|
||||
static void js_gpio_int_cb(void* arg) {
|
||||
furi_assert(arg);
|
||||
FuriSemaphore* semaphore = arg;
|
||||
furi_semaphore_release(semaphore);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initializes a GPIO pin according to the provided mode object
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```js
|
||||
* let gpio = require("gpio");
|
||||
* let led = gpio.get("pc3");
|
||||
* led.init({ direction: "out", outMode: "push_pull" });
|
||||
* ```
|
||||
*/
|
||||
static void js_gpio_init(struct mjs* mjs) {
|
||||
mjs_val_t pin_arg = mjs_arg(mjs, 0);
|
||||
mjs_val_t mode_arg = mjs_arg(mjs, 1);
|
||||
mjs_val_t pull_arg = mjs_arg(mjs, 2);
|
||||
// deconstruct mode object
|
||||
mjs_val_t mode_arg;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&mode_arg));
|
||||
mjs_val_t direction_arg = mjs_get(mjs, mode_arg, "direction", ~0);
|
||||
mjs_val_t out_mode_arg = mjs_get(mjs, mode_arg, "outMode", ~0);
|
||||
mjs_val_t in_mode_arg = mjs_get(mjs, mode_arg, "inMode", ~0);
|
||||
mjs_val_t edge_arg = mjs_get(mjs, mode_arg, "edge", ~0);
|
||||
mjs_val_t pull_arg = mjs_get(mjs, mode_arg, "pull", ~0);
|
||||
|
||||
if(!mjs_is_string(pin_arg)) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
// get strings
|
||||
const char* direction = mjs_get_string(mjs, &direction_arg, NULL);
|
||||
const char* out_mode = mjs_get_string(mjs, &out_mode_arg, NULL);
|
||||
const char* in_mode = mjs_get_string(mjs, &in_mode_arg, NULL);
|
||||
const char* edge = mjs_get_string(mjs, &edge_arg, NULL);
|
||||
const char* pull = mjs_get_string(mjs, &pull_arg, NULL);
|
||||
if(!direction)
|
||||
JS_ERROR_AND_RETURN(
|
||||
mjs, MJS_BAD_ARGS_ERROR, "Expected string in \"direction\" field of mode object");
|
||||
if(!out_mode) out_mode = "open_drain";
|
||||
if(!in_mode) in_mode = "plain_digital";
|
||||
if(!edge) edge = "rising";
|
||||
|
||||
// convert strings to mode
|
||||
GpioMode mode;
|
||||
if(strcmp(direction, "out") == 0) {
|
||||
if(strcmp(out_mode, "push_pull") == 0)
|
||||
mode = GpioModeOutputPushPull;
|
||||
else if(strcmp(out_mode, "open_drain") == 0)
|
||||
mode = GpioModeOutputOpenDrain;
|
||||
else
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid outMode");
|
||||
} else if(strcmp(direction, "in") == 0) {
|
||||
if(strcmp(in_mode, "analog") == 0) {
|
||||
mode = GpioModeAnalog;
|
||||
} else if(strcmp(in_mode, "plain_digital") == 0) {
|
||||
mode = GpioModeInput;
|
||||
} else if(strcmp(in_mode, "interrupt") == 0) {
|
||||
if(strcmp(edge, "rising") == 0)
|
||||
mode = GpioModeInterruptRise;
|
||||
else if(strcmp(edge, "falling") == 0)
|
||||
mode = GpioModeInterruptFall;
|
||||
else if(strcmp(edge, "both") == 0)
|
||||
mode = GpioModeInterruptRiseFall;
|
||||
else
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid edge");
|
||||
} else if(strcmp(in_mode, "event") == 0) {
|
||||
if(strcmp(edge, "rising") == 0)
|
||||
mode = GpioModeEventRise;
|
||||
else if(strcmp(edge, "falling") == 0)
|
||||
mode = GpioModeEventFall;
|
||||
else if(strcmp(edge, "both") == 0)
|
||||
mode = GpioModeEventRiseFall;
|
||||
else
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid edge");
|
||||
} else {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid inMode");
|
||||
}
|
||||
} else {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid direction");
|
||||
}
|
||||
|
||||
const char* pin_name = mjs_get_string(mjs, &pin_arg, NULL);
|
||||
if(!pin_name) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pin name");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
// convert pull
|
||||
GpioPull pull_mode;
|
||||
if(!pull) {
|
||||
pull_mode = GpioPullNo;
|
||||
} else if(strcmp(pull, "up") == 0) {
|
||||
pull_mode = GpioPullUp;
|
||||
} else if(strcmp(pull, "down") == 0) {
|
||||
pull_mode = GpioPullDown;
|
||||
} else {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid pull");
|
||||
}
|
||||
|
||||
if(!mjs_is_string(mode_arg)) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
const char* mode_name = mjs_get_string(mjs, &mode_arg, NULL);
|
||||
if(!mode_name) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get mode name");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!mjs_is_string(pull_arg)) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
const char* pull_name = mjs_get_string(mjs, &pull_arg, NULL);
|
||||
if(!pull_name) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pull name");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
const GpioPin* gpio_pin = js_gpio_get_gpio_pin(pin_name);
|
||||
if(gpio_pin == NULL) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pin name");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
GpioMode gpio_mode;
|
||||
if(!js_gpio_get_gpio_mode(mode_name, &gpio_mode)) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid mode name");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
GpioPull gpio_pull;
|
||||
if(!js_gpio_get_gpio_pull(pull_name, &gpio_pull)) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pull name");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
expansion_disable(furi_record_open(RECORD_EXPANSION));
|
||||
furi_record_close(RECORD_EXPANSION);
|
||||
|
||||
furi_hal_gpio_init(gpio_pin, gpio_mode, gpio_pull, GpioSpeedVeryHigh);
|
||||
|
||||
// init GPIO
|
||||
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||
furi_hal_gpio_init(manager_data->pin, mode, pull_mode, GpioSpeedVeryHigh);
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Writes a logic value to a GPIO pin
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```js
|
||||
* let gpio = require("gpio");
|
||||
* let led = gpio.get("pc3");
|
||||
* led.init({ direction: "out", outMode: "push_pull" });
|
||||
* led.write(true);
|
||||
* ```
|
||||
*/
|
||||
static void js_gpio_write(struct mjs* mjs) {
|
||||
mjs_val_t pin_arg = mjs_arg(mjs, 0);
|
||||
mjs_val_t value_arg = mjs_arg(mjs, 1);
|
||||
|
||||
if(!mjs_is_string(pin_arg)) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
const char* pin_name = mjs_get_string(mjs, &pin_arg, NULL);
|
||||
if(!pin_name) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pin name");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!mjs_is_boolean(value_arg)) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a boolean");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
bool value = mjs_get_bool(mjs, value_arg);
|
||||
|
||||
const GpioPin* gpio_pin = js_gpio_get_gpio_pin(pin_name);
|
||||
|
||||
if(gpio_pin == NULL) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pin name");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
furi_hal_gpio_write(gpio_pin, value);
|
||||
|
||||
bool level;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_BOOL(&level));
|
||||
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||
furi_hal_gpio_write(manager_data->pin, level);
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reads a logic value from a GPIO pin
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```js
|
||||
* let gpio = require("gpio");
|
||||
* let button = gpio.get("pc1");
|
||||
* button.init({ direction: "in" });
|
||||
* if(button.read())
|
||||
* print("hi button!!!!!");
|
||||
* ```
|
||||
*/
|
||||
static void js_gpio_read(struct mjs* mjs) {
|
||||
mjs_val_t pin_arg = mjs_arg(mjs, 0);
|
||||
|
||||
if(!mjs_is_string(pin_arg)) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
const char* pin_name = mjs_get_string(mjs, &pin_arg, NULL);
|
||||
if(!pin_name) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pin name");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
const GpioPin* gpio_pin = js_gpio_get_gpio_pin(pin_name);
|
||||
|
||||
if(gpio_pin == NULL) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pin name");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
bool value = furi_hal_gpio_read(gpio_pin);
|
||||
|
||||
// get level
|
||||
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||
bool value = furi_hal_gpio_read(manager_data->pin);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a event loop contract that can be used to listen to interrupts
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```js
|
||||
* let gpio = require("gpio");
|
||||
* let button = gpio.get("pc1");
|
||||
* let event_loop = require("event_loop");
|
||||
* button.init({ direction: "in", pull: "up", inMode: "interrupt", edge: "falling" });
|
||||
* event_loop.subscribe(button.interrupt(), function (_) { print("Hi!"); });
|
||||
* event_loop.run();
|
||||
* ```
|
||||
*/
|
||||
static void js_gpio_interrupt(struct mjs* mjs) {
|
||||
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||
|
||||
// interrupt handling
|
||||
if(!manager_data->had_interrupt) {
|
||||
furi_hal_gpio_add_int_callback(
|
||||
manager_data->pin, js_gpio_int_cb, manager_data->interrupt_semaphore);
|
||||
furi_hal_gpio_enable_int_callback(manager_data->pin);
|
||||
manager_data->had_interrupt = true;
|
||||
}
|
||||
|
||||
// make contract
|
||||
JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract));
|
||||
*contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object_type = JsEventLoopObjectTypeSemaphore,
|
||||
.object = manager_data->interrupt_semaphore,
|
||||
.non_timer =
|
||||
{
|
||||
.event = FuriEventLoopEventIn,
|
||||
},
|
||||
};
|
||||
manager_data->interrupt_contract = contract;
|
||||
mjs_return(mjs, mjs_mk_foreign(mjs, contract));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reads a voltage from a GPIO pin in analog mode
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```js
|
||||
* let gpio = require("gpio");
|
||||
* let pot = gpio.get("pc0");
|
||||
* pot.init({ direction: "in", inMode: "analog" });
|
||||
* print("voltage:" pot.read_analog(), "mV");
|
||||
* ```
|
||||
*/
|
||||
static void js_gpio_read_analog(struct mjs* mjs) {
|
||||
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
|
||||
JsGpioInst* gpio = mjs_get_ptr(mjs, obj_inst);
|
||||
furi_assert(gpio);
|
||||
|
||||
if(gpio->handle == NULL) {
|
||||
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Analog mode not started");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
mjs_val_t pin_arg = mjs_arg(mjs, 0);
|
||||
|
||||
if(!mjs_is_string(pin_arg)) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
const char* pin_name = mjs_get_string(mjs, &pin_arg, NULL);
|
||||
if(!pin_name) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pin name");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
FuriHalAdcChannel channel = js_gpio_get_gpio_channel(pin_name);
|
||||
if(channel == FuriHalAdcChannelNone) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pin name");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t adc_value = furi_hal_adc_read(gpio->handle, channel);
|
||||
float adc_mv = furi_hal_adc_convert_to_voltage(gpio->handle, adc_value);
|
||||
|
||||
mjs_return(mjs, mjs_mk_number(mjs, adc_mv));
|
||||
// get mV (ADC is configured for 12 bits and 2048 mV max)
|
||||
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||
uint16_t millivolts =
|
||||
furi_hal_adc_read(manager_data->adc_handle, manager_data->adc_channel) / 2;
|
||||
mjs_return(mjs, mjs_mk_number(mjs, (double)millivolts));
|
||||
}
|
||||
|
||||
static void js_gpio_start_analog(struct mjs* mjs) {
|
||||
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
|
||||
JsGpioInst* gpio = mjs_get_ptr(mjs, obj_inst);
|
||||
furi_assert(gpio);
|
||||
/**
|
||||
* @brief Returns an object that manages a specified pin.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```js
|
||||
* let gpio = require("gpio");
|
||||
* let led = gpio.get("pc3");
|
||||
* ```
|
||||
*/
|
||||
static void js_gpio_get(struct mjs* mjs) {
|
||||
mjs_val_t name_arg;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&name_arg));
|
||||
const char* name_string = mjs_get_string(mjs, &name_arg, NULL);
|
||||
const GpioPinRecord* pin_record = NULL;
|
||||
|
||||
FuriHalAdcScale scale = FuriHalAdcScale2048;
|
||||
if(mjs_nargs(mjs) > 0) {
|
||||
mjs_val_t scale_arg = mjs_arg(mjs, 0);
|
||||
|
||||
if(!mjs_is_number(scale_arg)) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a number");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t scale_num = mjs_get_int32(mjs, scale_arg);
|
||||
if(scale_num == 2048 || scale_num == 2000) { // 2 volt reference
|
||||
scale = FuriHalAdcScale2048;
|
||||
} else if(scale_num == 2500) { // 2.5 volt reference
|
||||
scale = FuriHalAdcScale2500;
|
||||
} else {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid scale");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
// parse input argument to a pin pointer
|
||||
if(name_string) {
|
||||
pin_record = furi_hal_resources_pin_by_name(name_string);
|
||||
} else if(mjs_is_number(name_arg)) {
|
||||
int name_int = mjs_get_int(mjs, name_arg);
|
||||
pin_record = furi_hal_resources_pin_by_number(name_int);
|
||||
} else {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Must be either a string or a number");
|
||||
}
|
||||
|
||||
if(gpio->handle != NULL) {
|
||||
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Analog mode already started");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
if(!pin_record) JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Pin not found on device");
|
||||
if(pin_record->debug)
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Pin is used for debugging");
|
||||
|
||||
gpio->handle = furi_hal_adc_acquire();
|
||||
furi_hal_adc_configure_ex(
|
||||
gpio->handle,
|
||||
scale,
|
||||
FuriHalAdcClockSync64,
|
||||
FuriHalAdcOversample64,
|
||||
FuriHalAdcSamplingtime247_5);
|
||||
// return pin manager object
|
||||
JsGpioInst* module = JS_GET_CONTEXT(mjs);
|
||||
mjs_val_t manager = mjs_mk_object(mjs);
|
||||
JsGpioPinInst* manager_data = malloc(sizeof(JsGpioPinInst));
|
||||
manager_data->pin = pin_record->pin;
|
||||
manager_data->interrupt_semaphore = furi_semaphore_alloc(UINT32_MAX, 0);
|
||||
manager_data->adc_handle = module->adc_handle;
|
||||
manager_data->adc_channel = pin_record->channel;
|
||||
mjs_own(mjs, &manager);
|
||||
mjs_set(mjs, manager, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, manager_data));
|
||||
mjs_set(mjs, manager, "init", ~0, MJS_MK_FN(js_gpio_init));
|
||||
mjs_set(mjs, manager, "write", ~0, MJS_MK_FN(js_gpio_write));
|
||||
mjs_set(mjs, manager, "read", ~0, MJS_MK_FN(js_gpio_read));
|
||||
mjs_set(mjs, manager, "read_analog", ~0, MJS_MK_FN(js_gpio_read_analog));
|
||||
mjs_set(mjs, manager, "interrupt", ~0, MJS_MK_FN(js_gpio_interrupt));
|
||||
mjs_return(mjs, manager);
|
||||
|
||||
// remember pin
|
||||
ManagedPinsArray_push_back(module->managed_pins, manager_data);
|
||||
}
|
||||
|
||||
static void js_gpio_stop_analog(struct mjs* mjs) {
|
||||
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
|
||||
JsGpioInst* gpio = mjs_get_ptr(mjs, obj_inst);
|
||||
furi_assert(gpio);
|
||||
static void* js_gpio_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
JsEventLoop* js_loop = js_module_get(modules, "event_loop");
|
||||
if(M_UNLIKELY(!js_loop)) return NULL;
|
||||
FuriEventLoop* loop = js_event_loop_get_loop(js_loop);
|
||||
|
||||
if(gpio->handle == NULL) {
|
||||
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Analog mode not started");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
JsGpioInst* module = malloc(sizeof(JsGpioInst));
|
||||
ManagedPinsArray_init(module->managed_pins);
|
||||
module->adc_handle = furi_hal_adc_acquire();
|
||||
module->loop = loop;
|
||||
furi_hal_adc_configure(module->adc_handle);
|
||||
|
||||
furi_hal_adc_release(gpio->handle);
|
||||
gpio->handle = NULL;
|
||||
}
|
||||
|
||||
static void* js_gpio_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
JsGpioInst* gpio = malloc(sizeof(JsGpioInst));
|
||||
gpio->handle = NULL;
|
||||
mjs_val_t gpio_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, gpio_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, gpio));
|
||||
mjs_set(mjs, gpio_obj, "init", ~0, MJS_MK_FN(js_gpio_init));
|
||||
mjs_set(mjs, gpio_obj, "write", ~0, MJS_MK_FN(js_gpio_write));
|
||||
mjs_set(mjs, gpio_obj, "read", ~0, MJS_MK_FN(js_gpio_read));
|
||||
mjs_set(mjs, gpio_obj, "readAnalog", ~0, MJS_MK_FN(js_gpio_read_analog));
|
||||
mjs_set(mjs, gpio_obj, "startAnalog", ~0, MJS_MK_FN(js_gpio_start_analog));
|
||||
mjs_set(mjs, gpio_obj, "stopAnalog", ~0, MJS_MK_FN(js_gpio_stop_analog));
|
||||
mjs_set(mjs, gpio_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, module));
|
||||
mjs_set(mjs, gpio_obj, "get", ~0, MJS_MK_FN(js_gpio_get));
|
||||
*object = gpio_obj;
|
||||
|
||||
return (void*)gpio;
|
||||
return (void*)module;
|
||||
}
|
||||
|
||||
static void js_gpio_destroy(void* inst) {
|
||||
if(inst != NULL) {
|
||||
JsGpioInst* gpio = (JsGpioInst*)inst;
|
||||
if(gpio->handle != NULL) {
|
||||
furi_hal_adc_release(gpio->handle);
|
||||
gpio->handle = NULL;
|
||||
furi_assert(inst);
|
||||
JsGpioInst* module = (JsGpioInst*)inst;
|
||||
|
||||
// reset pins
|
||||
ManagedPinsArray_it_t iterator;
|
||||
for(ManagedPinsArray_it(iterator, module->managed_pins); !ManagedPinsArray_end_p(iterator);
|
||||
ManagedPinsArray_next(iterator)) {
|
||||
JsGpioPinInst* manager_data = *ManagedPinsArray_cref(iterator);
|
||||
if(manager_data->had_interrupt) {
|
||||
furi_hal_gpio_disable_int_callback(manager_data->pin);
|
||||
furi_hal_gpio_remove_int_callback(manager_data->pin);
|
||||
}
|
||||
free(gpio);
|
||||
furi_hal_gpio_init(manager_data->pin, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
|
||||
furi_event_loop_maybe_unsubscribe(module->loop, manager_data->interrupt_semaphore);
|
||||
furi_semaphore_free(manager_data->interrupt_semaphore);
|
||||
free(manager_data->interrupt_contract);
|
||||
free(manager_data);
|
||||
}
|
||||
|
||||
// loop through all pins and reset them to analog mode
|
||||
for(size_t i = 0; i < COUNT_OF(js_gpio_pins); i++) {
|
||||
furi_hal_gpio_write(js_gpio_pins[i].pin, false);
|
||||
furi_hal_gpio_init(js_gpio_pins[i].pin, GpioModeAnalog, GpioPullNo, GpioSpeedVeryHigh);
|
||||
}
|
||||
|
||||
expansion_enable(furi_record_open(RECORD_EXPANSION));
|
||||
furi_record_close(RECORD_EXPANSION);
|
||||
// free buffers
|
||||
furi_hal_adc_release(module->adc_handle);
|
||||
ManagedPinsArray_clear(module->managed_pins);
|
||||
free(module);
|
||||
}
|
||||
|
||||
static const JsModuleDescriptor js_gpio_desc = {
|
||||
"gpio",
|
||||
js_gpio_create,
|
||||
js_gpio_destroy,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "js_gui.h"
|
||||
#include "../js_event_loop/js_event_loop.h"
|
||||
#include <gui/modules/byte_input.h>
|
||||
|
||||
#define DEFAULT_BUF_SZ 4
|
||||
|
||||
typedef struct {
|
||||
uint8_t* buffer;
|
||||
size_t buffer_size;
|
||||
FuriString* header;
|
||||
FuriSemaphore* input_semaphore;
|
||||
JsEventLoopContract contract;
|
||||
} JsByteKbContext;
|
||||
|
||||
static mjs_val_t
|
||||
input_transformer(struct mjs* mjs, FuriSemaphore* semaphore, JsByteKbContext* context) {
|
||||
furi_check(furi_semaphore_acquire(semaphore, 0) == FuriStatusOk);
|
||||
return mjs_mk_array_buf(mjs, (char*)context->buffer, context->buffer_size);
|
||||
}
|
||||
|
||||
static void input_callback(JsByteKbContext* context) {
|
||||
furi_semaphore_release(context->input_semaphore);
|
||||
}
|
||||
|
||||
static bool header_assign(
|
||||
struct mjs* mjs,
|
||||
ByteInput* input,
|
||||
JsViewPropValue value,
|
||||
JsByteKbContext* context) {
|
||||
UNUSED(mjs);
|
||||
furi_string_set(context->header, value.string);
|
||||
byte_input_set_header_text(input, furi_string_get_cstr(context->header));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
len_assign(struct mjs* mjs, ByteInput* input, JsViewPropValue value, JsByteKbContext* context) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(input);
|
||||
context->buffer_size = (size_t)(value.number);
|
||||
context->buffer = realloc(context->buffer, context->buffer_size); //-V701
|
||||
byte_input_set_result_callback(
|
||||
input,
|
||||
(ByteInputCallback)input_callback,
|
||||
NULL,
|
||||
context,
|
||||
context->buffer,
|
||||
context->buffer_size);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool default_data_assign(
|
||||
struct mjs* mjs,
|
||||
ByteInput* input,
|
||||
JsViewPropValue value,
|
||||
JsByteKbContext* context) {
|
||||
UNUSED(mjs);
|
||||
|
||||
mjs_val_t array_buf = value.array;
|
||||
if(mjs_is_data_view(array_buf)) {
|
||||
array_buf = mjs_dataview_get_buf(mjs, array_buf);
|
||||
}
|
||||
size_t default_data_len = 0;
|
||||
char* default_data = mjs_array_buf_get_ptr(mjs, array_buf, &default_data_len);
|
||||
memcpy(
|
||||
context->buffer,
|
||||
(uint8_t*)default_data,
|
||||
MIN((size_t)context->buffer_size, default_data_len));
|
||||
|
||||
byte_input_set_result_callback(
|
||||
input,
|
||||
(ByteInputCallback)input_callback,
|
||||
NULL,
|
||||
context,
|
||||
context->buffer,
|
||||
context->buffer_size);
|
||||
return true;
|
||||
}
|
||||
|
||||
static JsByteKbContext* ctx_make(struct mjs* mjs, ByteInput* input, mjs_val_t view_obj) {
|
||||
JsByteKbContext* context = malloc(sizeof(JsByteKbContext));
|
||||
*context = (JsByteKbContext){
|
||||
.buffer_size = DEFAULT_BUF_SZ,
|
||||
.buffer = malloc(DEFAULT_BUF_SZ),
|
||||
.header = furi_string_alloc(),
|
||||
.input_semaphore = furi_semaphore_alloc(1, 0),
|
||||
};
|
||||
context->contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object_type = JsEventLoopObjectTypeSemaphore,
|
||||
.object = context->input_semaphore,
|
||||
.non_timer =
|
||||
{
|
||||
.event = FuriEventLoopEventIn,
|
||||
.transformer = (JsEventLoopTransformer)input_transformer,
|
||||
.transformer_context = context,
|
||||
},
|
||||
};
|
||||
byte_input_set_result_callback(
|
||||
input,
|
||||
(ByteInputCallback)input_callback,
|
||||
NULL,
|
||||
context,
|
||||
context->buffer,
|
||||
context->buffer_size);
|
||||
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
|
||||
return context;
|
||||
}
|
||||
|
||||
static void ctx_destroy(ByteInput* input, JsByteKbContext* context, FuriEventLoop* loop) {
|
||||
UNUSED(input);
|
||||
furi_event_loop_maybe_unsubscribe(loop, context->input_semaphore);
|
||||
furi_semaphore_free(context->input_semaphore);
|
||||
furi_string_free(context->header);
|
||||
free(context->buffer);
|
||||
free(context);
|
||||
}
|
||||
|
||||
static const JsViewDescriptor view_descriptor = {
|
||||
.alloc = (JsViewAlloc)byte_input_alloc,
|
||||
.free = (JsViewFree)byte_input_free,
|
||||
.get_view = (JsViewGetView)byte_input_get_view,
|
||||
.custom_make = (JsViewCustomMake)ctx_make,
|
||||
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
|
||||
.prop_cnt = 3,
|
||||
.props = {
|
||||
(JsViewPropDescriptor){
|
||||
.name = "header",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)header_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "length",
|
||||
.type = JsViewPropTypeNumber,
|
||||
.assign = (JsViewPropAssign)len_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "defaultData",
|
||||
.type = JsViewPropTypeTypedArr,
|
||||
.assign = (JsViewPropAssign)default_data_assign},
|
||||
}};
|
||||
|
||||
JS_GUI_VIEW_DEF(byte_input, &view_descriptor);
|
||||
@@ -0,0 +1,129 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "js_gui.h"
|
||||
#include "../js_event_loop/js_event_loop.h"
|
||||
#include <gui/modules/dialog_ex.h>
|
||||
|
||||
#define QUEUE_LEN 2
|
||||
|
||||
typedef struct {
|
||||
FuriMessageQueue* queue;
|
||||
JsEventLoopContract contract;
|
||||
} JsDialogCtx;
|
||||
|
||||
static mjs_val_t
|
||||
input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsDialogCtx* context) {
|
||||
UNUSED(context);
|
||||
DialogExResult result;
|
||||
furi_check(furi_message_queue_get(queue, &result, 0) == FuriStatusOk);
|
||||
const char* string;
|
||||
if(result == DialogExResultLeft) {
|
||||
string = "left";
|
||||
} else if(result == DialogExResultCenter) {
|
||||
string = "center";
|
||||
} else if(result == DialogExResultRight) {
|
||||
string = "right";
|
||||
} else {
|
||||
furi_crash();
|
||||
}
|
||||
return mjs_mk_string(mjs, string, ~0, false);
|
||||
}
|
||||
|
||||
static void input_callback(DialogExResult result, JsDialogCtx* context) {
|
||||
furi_check(furi_message_queue_put(context->queue, &result, 0) == FuriStatusOk);
|
||||
}
|
||||
|
||||
static bool
|
||||
header_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(context);
|
||||
dialog_ex_set_header(dialog, value.string, 64, 0, AlignCenter, AlignTop);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
text_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(context);
|
||||
dialog_ex_set_text(dialog, value.string, 64, 32, AlignCenter, AlignCenter);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
left_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(context);
|
||||
dialog_ex_set_left_button_text(dialog, value.string);
|
||||
return true;
|
||||
}
|
||||
static bool
|
||||
center_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(context);
|
||||
dialog_ex_set_center_button_text(dialog, value.string);
|
||||
return true;
|
||||
}
|
||||
static bool
|
||||
right_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(context);
|
||||
dialog_ex_set_right_button_text(dialog, value.string);
|
||||
return true;
|
||||
}
|
||||
|
||||
static JsDialogCtx* ctx_make(struct mjs* mjs, DialogEx* dialog, mjs_val_t view_obj) {
|
||||
JsDialogCtx* context = malloc(sizeof(JsDialogCtx));
|
||||
context->queue = furi_message_queue_alloc(QUEUE_LEN, sizeof(DialogExResult));
|
||||
context->contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object_type = JsEventLoopObjectTypeQueue,
|
||||
.object = context->queue,
|
||||
.non_timer =
|
||||
{
|
||||
.event = FuriEventLoopEventIn,
|
||||
.transformer = (JsEventLoopTransformer)input_transformer,
|
||||
},
|
||||
};
|
||||
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
|
||||
dialog_ex_set_result_callback(dialog, (DialogExResultCallback)input_callback);
|
||||
dialog_ex_set_context(dialog, context);
|
||||
return context;
|
||||
}
|
||||
|
||||
static void ctx_destroy(DialogEx* input, JsDialogCtx* context, FuriEventLoop* loop) {
|
||||
UNUSED(input);
|
||||
furi_event_loop_maybe_unsubscribe(loop, context->queue);
|
||||
furi_message_queue_free(context->queue);
|
||||
free(context);
|
||||
}
|
||||
|
||||
static const JsViewDescriptor view_descriptor = {
|
||||
.alloc = (JsViewAlloc)dialog_ex_alloc,
|
||||
.free = (JsViewFree)dialog_ex_free,
|
||||
.get_view = (JsViewGetView)dialog_ex_get_view,
|
||||
.custom_make = (JsViewCustomMake)ctx_make,
|
||||
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
|
||||
.prop_cnt = 5,
|
||||
.props = {
|
||||
(JsViewPropDescriptor){
|
||||
.name = "header",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)header_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "text",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)text_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "left",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)left_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "center",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)center_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "right",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)right_assign},
|
||||
}};
|
||||
|
||||
JS_GUI_VIEW_DEF(dialog, &view_descriptor);
|
||||
@@ -0,0 +1,12 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "js_gui.h"
|
||||
#include <gui/modules/empty_screen.h>
|
||||
|
||||
static const JsViewDescriptor view_descriptor = {
|
||||
.alloc = (JsViewAlloc)empty_screen_alloc,
|
||||
.free = (JsViewFree)empty_screen_free,
|
||||
.get_view = (JsViewGetView)empty_screen_get_view,
|
||||
.prop_cnt = 0,
|
||||
.props = {},
|
||||
};
|
||||
JS_GUI_VIEW_DEF(empty_screen, &view_descriptor);
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "../../js_modules.h"
|
||||
#include <dialogs/dialogs.h>
|
||||
#include <assets_icons.h>
|
||||
|
||||
static void js_gui_file_picker_pick_file(struct mjs* mjs) {
|
||||
const char *base_path, *extension;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&base_path), JS_ARG_STR(&extension));
|
||||
|
||||
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
|
||||
const DialogsFileBrowserOptions browser_options = {
|
||||
.extension = extension,
|
||||
.icon = &I_file_10px,
|
||||
.base_path = base_path,
|
||||
};
|
||||
FuriString* path = furi_string_alloc_set(base_path);
|
||||
if(dialog_file_browser_show(dialogs, path, path, &browser_options)) {
|
||||
mjs_return(mjs, mjs_mk_string(mjs, furi_string_get_cstr(path), ~0, true));
|
||||
} else {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
furi_string_free(path);
|
||||
furi_record_close(RECORD_DIALOGS);
|
||||
}
|
||||
|
||||
static void* js_gui_file_picker_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
*object = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, *object, "pickFile", ~0, MJS_MK_FN(js_gui_file_picker_pick_file));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static const JsModuleDescriptor js_gui_file_picker_desc = {
|
||||
"gui__file_picker",
|
||||
js_gui_file_picker_create,
|
||||
NULL,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
.appid = PLUGIN_APP_ID,
|
||||
.ep_api_version = PLUGIN_API_VERSION,
|
||||
.entry_point = &js_gui_file_picker_desc,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* js_gui_file_picker_ep(void) {
|
||||
return &plugin_descriptor;
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "./js_gui.h"
|
||||
#include <furi.h>
|
||||
#include <mlib/m-array.h>
|
||||
#include <gui/view_dispatcher.h>
|
||||
#include "../js_event_loop/js_event_loop.h"
|
||||
#include <m-array.h>
|
||||
|
||||
#define EVENT_QUEUE_SIZE 16
|
||||
|
||||
typedef struct {
|
||||
uint32_t next_view_id;
|
||||
FuriEventLoop* loop;
|
||||
Gui* gui;
|
||||
ViewDispatcher* dispatcher;
|
||||
// event stuff
|
||||
JsEventLoopContract custom_contract;
|
||||
FuriMessageQueue* custom;
|
||||
JsEventLoopContract navigation_contract;
|
||||
FuriSemaphore*
|
||||
navigation; // FIXME: (-nofl) convert into callback once FuriEventLoop starts supporting this
|
||||
} JsGui;
|
||||
|
||||
// Useful for factories
|
||||
static JsGui* js_gui;
|
||||
|
||||
typedef struct {
|
||||
uint32_t id;
|
||||
const JsViewDescriptor* descriptor;
|
||||
void* specific_view;
|
||||
void* custom_data;
|
||||
} JsGuiViewData;
|
||||
|
||||
/**
|
||||
* @brief Transformer for custom events
|
||||
*/
|
||||
static mjs_val_t
|
||||
js_gui_vd_custom_transformer(struct mjs* mjs, FuriEventLoopObject* object, void* context) {
|
||||
UNUSED(context);
|
||||
furi_check(object);
|
||||
FuriMessageQueue* queue = object;
|
||||
uint32_t event;
|
||||
furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk);
|
||||
return mjs_mk_number(mjs, (double)event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ViewDispatcher custom event callback
|
||||
*/
|
||||
static bool js_gui_vd_custom_callback(void* context, uint32_t event) {
|
||||
furi_check(context);
|
||||
JsGui* module = context;
|
||||
furi_check(furi_message_queue_put(module->custom, &event, 0) == FuriStatusOk);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ViewDispatcher navigation event callback
|
||||
*/
|
||||
static bool js_gui_vd_nav_callback(void* context) {
|
||||
furi_check(context);
|
||||
JsGui* module = context;
|
||||
furi_semaphore_release(module->navigation);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `viewDispatcher.sendCustom`
|
||||
*/
|
||||
static void js_gui_vd_send_custom(struct mjs* mjs) {
|
||||
int32_t event;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&event));
|
||||
|
||||
JsGui* module = JS_GET_CONTEXT(mjs);
|
||||
view_dispatcher_send_custom_event(module->dispatcher, (uint32_t)event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `viewDispatcher.sendTo`
|
||||
*/
|
||||
static void js_gui_vd_send_to(struct mjs* mjs) {
|
||||
enum {
|
||||
SendDirToFront,
|
||||
SendDirToBack,
|
||||
} send_direction;
|
||||
JS_ENUM_MAP(send_direction, {"front", SendDirToFront}, {"back", SendDirToBack});
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ENUM(send_direction, "SendDirection"));
|
||||
|
||||
JsGui* module = JS_GET_CONTEXT(mjs);
|
||||
if(send_direction == SendDirToBack) {
|
||||
view_dispatcher_send_to_back(module->dispatcher);
|
||||
} else {
|
||||
view_dispatcher_send_to_front(module->dispatcher);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `viewDispatcher.switchTo`
|
||||
*/
|
||||
static void js_gui_vd_switch_to(struct mjs* mjs) {
|
||||
mjs_val_t view;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&view));
|
||||
JsGuiViewData* view_data = JS_GET_INST(mjs, view);
|
||||
mjs_val_t vd_obj = mjs_get_this(mjs);
|
||||
JsGui* module = JS_GET_INST(mjs, vd_obj);
|
||||
view_dispatcher_switch_to_view(module->dispatcher, (uint32_t)view_data->id);
|
||||
mjs_set(mjs, vd_obj, "currentView", ~0, view);
|
||||
}
|
||||
|
||||
static void* js_gui_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
// get event loop
|
||||
JsEventLoop* js_loop = js_module_get(modules, "event_loop");
|
||||
if(M_UNLIKELY(!js_loop)) return NULL;
|
||||
FuriEventLoop* loop = js_event_loop_get_loop(js_loop);
|
||||
|
||||
// create C object
|
||||
JsGui* module = malloc(sizeof(JsGui));
|
||||
module->loop = loop;
|
||||
module->gui = furi_record_open(RECORD_GUI);
|
||||
module->dispatcher = view_dispatcher_alloc_ex(loop);
|
||||
module->custom = furi_message_queue_alloc(EVENT_QUEUE_SIZE, sizeof(uint32_t));
|
||||
module->navigation = furi_semaphore_alloc(EVENT_QUEUE_SIZE, 0);
|
||||
view_dispatcher_attach_to_gui(module->dispatcher, module->gui, ViewDispatcherTypeFullscreen);
|
||||
view_dispatcher_send_to_front(module->dispatcher);
|
||||
|
||||
// subscribe to events and create contracts
|
||||
view_dispatcher_set_event_callback_context(module->dispatcher, module);
|
||||
view_dispatcher_set_custom_event_callback(module->dispatcher, js_gui_vd_custom_callback);
|
||||
view_dispatcher_set_navigation_event_callback(module->dispatcher, js_gui_vd_nav_callback);
|
||||
module->custom_contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object = module->custom,
|
||||
.object_type = JsEventLoopObjectTypeQueue,
|
||||
.non_timer =
|
||||
{
|
||||
.event = FuriEventLoopEventIn,
|
||||
.transformer = js_gui_vd_custom_transformer,
|
||||
},
|
||||
};
|
||||
module->navigation_contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object = module->navigation,
|
||||
.object_type = JsEventLoopObjectTypeSemaphore,
|
||||
.non_timer =
|
||||
{
|
||||
.event = FuriEventLoopEventIn,
|
||||
},
|
||||
};
|
||||
|
||||
// create viewDispatcher object
|
||||
mjs_val_t view_dispatcher = mjs_mk_object(mjs);
|
||||
JS_ASSIGN_MULTI(mjs, view_dispatcher) {
|
||||
JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, module));
|
||||
JS_FIELD("sendCustom", MJS_MK_FN(js_gui_vd_send_custom));
|
||||
JS_FIELD("sendTo", MJS_MK_FN(js_gui_vd_send_to));
|
||||
JS_FIELD("switchTo", MJS_MK_FN(js_gui_vd_switch_to));
|
||||
JS_FIELD("custom", mjs_mk_foreign(mjs, &module->custom_contract));
|
||||
JS_FIELD("navigation", mjs_mk_foreign(mjs, &module->navigation_contract));
|
||||
JS_FIELD("currentView", MJS_NULL);
|
||||
}
|
||||
|
||||
// create API object
|
||||
mjs_val_t api = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, api, "viewDispatcher", ~0, view_dispatcher);
|
||||
|
||||
*object = api;
|
||||
js_gui = module;
|
||||
return module;
|
||||
}
|
||||
|
||||
static void js_gui_destroy(void* inst) {
|
||||
furi_assert(inst);
|
||||
JsGui* module = inst;
|
||||
|
||||
view_dispatcher_free(module->dispatcher);
|
||||
furi_event_loop_maybe_unsubscribe(module->loop, module->custom);
|
||||
furi_event_loop_maybe_unsubscribe(module->loop, module->navigation);
|
||||
furi_message_queue_free(module->custom);
|
||||
furi_semaphore_free(module->navigation);
|
||||
|
||||
furi_record_close(RECORD_GUI);
|
||||
free(module);
|
||||
js_gui = NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Assigns a `View` property. Not available from JS.
|
||||
*/
|
||||
static bool
|
||||
js_gui_view_assign(struct mjs* mjs, const char* name, mjs_val_t value, JsGuiViewData* data) {
|
||||
const JsViewDescriptor* descriptor = data->descriptor;
|
||||
for(size_t i = 0; i < descriptor->prop_cnt; i++) {
|
||||
JsViewPropDescriptor prop = descriptor->props[i];
|
||||
if(strcmp(prop.name, name) != 0) continue;
|
||||
|
||||
// convert JS value to C
|
||||
JsViewPropValue c_value;
|
||||
const char* expected_type = NULL;
|
||||
switch(prop.type) {
|
||||
case JsViewPropTypeNumber: {
|
||||
if(!mjs_is_number(value)) {
|
||||
expected_type = "number";
|
||||
break;
|
||||
}
|
||||
c_value = (JsViewPropValue){.number = mjs_get_int32(mjs, value)};
|
||||
} break;
|
||||
case JsViewPropTypeString: {
|
||||
if(!mjs_is_string(value)) {
|
||||
expected_type = "string";
|
||||
break;
|
||||
}
|
||||
c_value = (JsViewPropValue){.string = mjs_get_string(mjs, &value, NULL)};
|
||||
} break;
|
||||
case JsViewPropTypeArr: {
|
||||
if(!mjs_is_array(value)) {
|
||||
expected_type = "array";
|
||||
break;
|
||||
}
|
||||
c_value = (JsViewPropValue){.array = value};
|
||||
} break;
|
||||
case JsViewPropTypeTypedArr: {
|
||||
if(!mjs_is_typed_array(value)) {
|
||||
expected_type = "typed_array";
|
||||
break;
|
||||
}
|
||||
c_value = (JsViewPropValue){.array = value};
|
||||
} break;
|
||||
case JsViewPropTypeBool: {
|
||||
if(!mjs_is_boolean(value)) {
|
||||
expected_type = "bool";
|
||||
break;
|
||||
}
|
||||
c_value = (JsViewPropValue){.boolean = mjs_get_bool(mjs, value)};
|
||||
} break;
|
||||
}
|
||||
|
||||
if(expected_type) {
|
||||
mjs_prepend_errorf(
|
||||
mjs, MJS_BAD_ARGS_ERROR, "view prop \"%s\" requires %s value", name, expected_type);
|
||||
return false;
|
||||
} else {
|
||||
return prop.assign(mjs, data->specific_view, c_value, data->custom_data);
|
||||
}
|
||||
}
|
||||
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "view has no prop named \"%s\"", name);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `View.set`
|
||||
*/
|
||||
static void js_gui_view_set(struct mjs* mjs) {
|
||||
const char* name;
|
||||
mjs_val_t value;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&name), JS_ARG_ANY(&value));
|
||||
JsGuiViewData* data = JS_GET_CONTEXT(mjs);
|
||||
bool success = js_gui_view_assign(mjs, name, value, data);
|
||||
UNUSED(success);
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `View` destructor
|
||||
*/
|
||||
static void js_gui_view_destructor(struct mjs* mjs, mjs_val_t obj) {
|
||||
JsGuiViewData* data = JS_GET_INST(mjs, obj);
|
||||
view_dispatcher_remove_view(js_gui->dispatcher, data->id);
|
||||
if(data->descriptor->custom_destroy)
|
||||
data->descriptor->custom_destroy(data->specific_view, data->custom_data, js_gui->loop);
|
||||
data->descriptor->free(data->specific_view);
|
||||
free(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Creates a `View` object from a descriptor. Not available from JS.
|
||||
*/
|
||||
static mjs_val_t js_gui_make_view(struct mjs* mjs, const JsViewDescriptor* descriptor) {
|
||||
void* specific_view = descriptor->alloc();
|
||||
View* view = descriptor->get_view(specific_view);
|
||||
uint32_t view_id = js_gui->next_view_id++;
|
||||
view_dispatcher_add_view(js_gui->dispatcher, view_id, view);
|
||||
|
||||
// generic view API
|
||||
mjs_val_t view_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, view_obj, "set", ~0, MJS_MK_FN(js_gui_view_set));
|
||||
|
||||
// object data
|
||||
JsGuiViewData* data = malloc(sizeof(JsGuiViewData));
|
||||
*data = (JsGuiViewData){
|
||||
.descriptor = descriptor,
|
||||
.id = view_id,
|
||||
.specific_view = specific_view,
|
||||
.custom_data =
|
||||
descriptor->custom_make ? descriptor->custom_make(mjs, specific_view, view_obj) : NULL,
|
||||
};
|
||||
mjs_set(mjs, view_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, data));
|
||||
mjs_set(mjs, view_obj, MJS_DESTRUCTOR_PROP_NAME, ~0, MJS_MK_FN(js_gui_view_destructor));
|
||||
|
||||
return view_obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `ViewFactory.make`
|
||||
*/
|
||||
static void js_gui_vf_make(struct mjs* mjs) {
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||
const JsViewDescriptor* descriptor = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, js_gui_make_view(mjs, descriptor));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `ViewFactory.makeWith`
|
||||
*/
|
||||
static void js_gui_vf_make_with(struct mjs* mjs) {
|
||||
mjs_val_t props;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&props));
|
||||
const JsViewDescriptor* descriptor = JS_GET_CONTEXT(mjs);
|
||||
|
||||
// make the object like normal
|
||||
mjs_val_t view_obj = js_gui_make_view(mjs, descriptor);
|
||||
JsGuiViewData* data = JS_GET_INST(mjs, view_obj);
|
||||
|
||||
// assign properties one by one
|
||||
mjs_val_t key, iter = MJS_UNDEFINED;
|
||||
while((key = mjs_next(mjs, props, &iter)) != MJS_UNDEFINED) {
|
||||
furi_check(mjs_is_string(key));
|
||||
const char* name = mjs_get_string(mjs, &key, NULL);
|
||||
mjs_val_t value = mjs_get(mjs, props, name, ~0);
|
||||
|
||||
if(!js_gui_view_assign(mjs, name, value, data)) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
mjs_return(mjs, view_obj);
|
||||
}
|
||||
|
||||
mjs_val_t js_gui_make_view_factory(struct mjs* mjs, const JsViewDescriptor* view_descriptor) {
|
||||
mjs_val_t factory = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, factory, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, (void*)view_descriptor));
|
||||
mjs_set(mjs, factory, "make", ~0, MJS_MK_FN(js_gui_vf_make));
|
||||
mjs_set(mjs, factory, "makeWith", ~0, MJS_MK_FN(js_gui_vf_make_with));
|
||||
return factory;
|
||||
}
|
||||
|
||||
extern const ElfApiInterface js_gui_hashtable_api_interface;
|
||||
|
||||
static const JsModuleDescriptor js_gui_desc = {
|
||||
"gui",
|
||||
js_gui_create,
|
||||
js_gui_destroy,
|
||||
&js_gui_hashtable_api_interface,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
.appid = PLUGIN_APP_ID,
|
||||
.ep_api_version = PLUGIN_API_VERSION,
|
||||
.entry_point = &js_gui_desc,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* js_gui_ep(void) {
|
||||
return &plugin_descriptor;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
#include "../../js_modules.h"
|
||||
#include <gui/view.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
JsViewPropTypeString,
|
||||
JsViewPropTypeNumber,
|
||||
JsViewPropTypeArr,
|
||||
JsViewPropTypeTypedArr,
|
||||
JsViewPropTypeBool,
|
||||
} JsViewPropType;
|
||||
|
||||
typedef union {
|
||||
const char* string;
|
||||
int32_t number;
|
||||
mjs_val_t array;
|
||||
bool boolean;
|
||||
} JsViewPropValue;
|
||||
|
||||
/**
|
||||
* @brief Assigns a value to a view property
|
||||
*
|
||||
* The name and the type are implicit and defined in the property descriptor
|
||||
*/
|
||||
typedef bool (
|
||||
*JsViewPropAssign)(struct mjs* mjs, void* specific_view, JsViewPropValue value, void* context);
|
||||
|
||||
/** @brief Property descriptor */
|
||||
typedef struct {
|
||||
const char* name; //<! Property name, as visible from JS
|
||||
JsViewPropType type; // <! Property type, ensured by the GUI module
|
||||
JsViewPropAssign assign; // <! Property assignment callback
|
||||
} JsViewPropDescriptor;
|
||||
|
||||
// View method signatures
|
||||
|
||||
/** @brief View's `_alloc` method */
|
||||
typedef void* (*JsViewAlloc)(void);
|
||||
/** @brief View's `_get_view` method */
|
||||
typedef View* (*JsViewGetView)(void* specific_view);
|
||||
/** @brief View's `_free` method */
|
||||
typedef void (*JsViewFree)(void* specific_view);
|
||||
|
||||
// Glue code method signatures
|
||||
|
||||
/** @brief Context instantiation for glue code */
|
||||
typedef void* (*JsViewCustomMake)(struct mjs* mjs, void* specific_view, mjs_val_t view_obj);
|
||||
/** @brief Context destruction for glue code */
|
||||
typedef void (*JsViewCustomDestroy)(void* specific_view, void* custom_state, FuriEventLoop* loop);
|
||||
|
||||
/**
|
||||
* @brief Descriptor for a JS view
|
||||
*
|
||||
* Contains:
|
||||
* - Pointers to generic view methods (`alloc`, `get_view` and `free`)
|
||||
* - Pointers to glue code context ctor/dtor methods (`custom_make`,
|
||||
* `custom_destroy`)
|
||||
* - Descriptors of properties visible from JS (`prop_cnt`, `props`)
|
||||
*
|
||||
* `js_gui` uses this descriptor to produce view factories and views.
|
||||
*/
|
||||
typedef struct {
|
||||
JsViewAlloc alloc;
|
||||
JsViewGetView get_view;
|
||||
JsViewFree free;
|
||||
JsViewCustomMake custom_make; // <! May be NULL
|
||||
JsViewCustomDestroy custom_destroy; // <! May be NULL
|
||||
size_t prop_cnt; //<! Number of properties visible from JS
|
||||
JsViewPropDescriptor props[]; // <! Descriptors of properties visible from JS
|
||||
} JsViewDescriptor;
|
||||
|
||||
// Callback ordering:
|
||||
// alloc -> get_view -> [custom_make (if set)] -> props[i].assign -> [custom_destroy (if_set)] -> free
|
||||
// \_______________ creation ________________/ \___ usage ___/ \_________ destruction _________/
|
||||
|
||||
/**
|
||||
* @brief Creates a JS `ViewFactory` object
|
||||
*
|
||||
* This function is intended to be used by individual view adapter modules that
|
||||
* wish to create a unified JS API interface in a declarative way. Usually this
|
||||
* is done via the `JS_GUI_VIEW_DEF` macro which hides all the boilerplate.
|
||||
*
|
||||
* The `ViewFactory` object exposes two methods, `make` and `makeWith`, each
|
||||
* returning a `View` object. These objects fully comply with the expectations
|
||||
* of the `ViewDispatcher`, TS type definitions and the proposed Flipper JS
|
||||
* coding style.
|
||||
*/
|
||||
mjs_val_t js_gui_make_view_factory(struct mjs* mjs, const JsViewDescriptor* view_descriptor);
|
||||
|
||||
/**
|
||||
* @brief Defines a module implementing `View` glue code
|
||||
*/
|
||||
#define JS_GUI_VIEW_DEF(name, descriptor) \
|
||||
static void* view_mod_ctor(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { \
|
||||
UNUSED(modules); \
|
||||
*object = js_gui_make_view_factory(mjs, descriptor); \
|
||||
return NULL; \
|
||||
} \
|
||||
static const JsModuleDescriptor js_mod_desc = { \
|
||||
"gui__" #name, \
|
||||
view_mod_ctor, \
|
||||
NULL, \
|
||||
NULL, \
|
||||
}; \
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = { \
|
||||
.appid = PLUGIN_APP_ID, \
|
||||
.ep_api_version = PLUGIN_API_VERSION, \
|
||||
.entry_point = &js_mod_desc, \
|
||||
}; \
|
||||
const FlipperAppPluginDescriptor* js_view_##name##_ep(void) { \
|
||||
return &plugin_descriptor; \
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,16 @@
|
||||
#include <flipper_application/api_hashtable/api_hashtable.h>
|
||||
#include <flipper_application/api_hashtable/compilesort.hpp>
|
||||
|
||||
#include "js_gui_api_table_i.h"
|
||||
|
||||
static_assert(!has_hash_collisions(js_gui_api_table), "Detected API method hash collision!");
|
||||
|
||||
extern "C" constexpr HashtableApiInterface js_gui_hashtable_api_interface{
|
||||
{
|
||||
.api_version_major = 0,
|
||||
.api_version_minor = 0,
|
||||
.resolver_callback = &elf_resolve_from_hashtable,
|
||||
},
|
||||
js_gui_api_table.cbegin(),
|
||||
js_gui_api_table.cend(),
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
#include "js_gui.h"
|
||||
|
||||
static constexpr auto js_gui_api_table = sort(create_array_t<sym_entry>(
|
||||
API_METHOD(js_gui_make_view_factory, mjs_val_t, (struct mjs*, const JsViewDescriptor*))));
|
||||
@@ -0,0 +1,12 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "js_gui.h"
|
||||
#include <gui/modules/loading.h>
|
||||
|
||||
static const JsViewDescriptor view_descriptor = {
|
||||
.alloc = (JsViewAlloc)loading_alloc,
|
||||
.free = (JsViewFree)loading_free,
|
||||
.get_view = (JsViewGetView)loading_get_view,
|
||||
.prop_cnt = 0,
|
||||
.props = {},
|
||||
};
|
||||
JS_GUI_VIEW_DEF(loading, &view_descriptor);
|
||||
@@ -0,0 +1,87 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "js_gui.h"
|
||||
#include "../js_event_loop/js_event_loop.h"
|
||||
#include <gui/modules/submenu.h>
|
||||
|
||||
#define QUEUE_LEN 2
|
||||
|
||||
typedef struct {
|
||||
FuriMessageQueue* queue;
|
||||
JsEventLoopContract contract;
|
||||
} JsSubmenuCtx;
|
||||
|
||||
static mjs_val_t choose_transformer(struct mjs* mjs, FuriMessageQueue* queue, void* context) {
|
||||
UNUSED(context);
|
||||
uint32_t index;
|
||||
furi_check(furi_message_queue_get(queue, &index, 0) == FuriStatusOk);
|
||||
return mjs_mk_number(mjs, (double)index);
|
||||
}
|
||||
|
||||
void choose_callback(void* context, uint32_t index) {
|
||||
JsSubmenuCtx* ctx = context;
|
||||
furi_check(furi_message_queue_put(ctx->queue, &index, 0) == FuriStatusOk);
|
||||
}
|
||||
|
||||
static bool
|
||||
header_assign(struct mjs* mjs, Submenu* submenu, JsViewPropValue value, void* context) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(context);
|
||||
submenu_set_header(submenu, value.string);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool items_assign(struct mjs* mjs, Submenu* submenu, JsViewPropValue value, void* context) {
|
||||
UNUSED(mjs);
|
||||
submenu_reset(submenu);
|
||||
size_t len = mjs_array_length(mjs, value.array);
|
||||
for(size_t i = 0; i < len; i++) {
|
||||
mjs_val_t item = mjs_array_get(mjs, value.array, i);
|
||||
if(!mjs_is_string(item)) return false;
|
||||
submenu_add_item(submenu, mjs_get_string(mjs, &item, NULL), i, choose_callback, context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static JsSubmenuCtx* ctx_make(struct mjs* mjs, Submenu* input, mjs_val_t view_obj) {
|
||||
UNUSED(input);
|
||||
JsSubmenuCtx* context = malloc(sizeof(JsSubmenuCtx));
|
||||
context->queue = furi_message_queue_alloc(QUEUE_LEN, sizeof(uint32_t));
|
||||
context->contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object_type = JsEventLoopObjectTypeQueue,
|
||||
.object = context->queue,
|
||||
.non_timer =
|
||||
{
|
||||
.event = FuriEventLoopEventIn,
|
||||
.transformer = (JsEventLoopTransformer)choose_transformer,
|
||||
},
|
||||
};
|
||||
mjs_set(mjs, view_obj, "chosen", ~0, mjs_mk_foreign(mjs, &context->contract));
|
||||
return context;
|
||||
}
|
||||
|
||||
static void ctx_destroy(Submenu* input, JsSubmenuCtx* context, FuriEventLoop* loop) {
|
||||
UNUSED(input);
|
||||
furi_event_loop_maybe_unsubscribe(loop, context->queue);
|
||||
furi_message_queue_free(context->queue);
|
||||
free(context);
|
||||
}
|
||||
|
||||
static const JsViewDescriptor view_descriptor = {
|
||||
.alloc = (JsViewAlloc)submenu_alloc,
|
||||
.free = (JsViewFree)submenu_free,
|
||||
.get_view = (JsViewGetView)submenu_get_view,
|
||||
.custom_make = (JsViewCustomMake)ctx_make,
|
||||
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
|
||||
.prop_cnt = 2,
|
||||
.props = {
|
||||
(JsViewPropDescriptor){
|
||||
.name = "header",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)header_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "items",
|
||||
.type = JsViewPropTypeArr,
|
||||
.assign = (JsViewPropAssign)items_assign},
|
||||
}};
|
||||
JS_GUI_VIEW_DEF(submenu, &view_descriptor);
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "js_gui.h"
|
||||
#include <gui/modules/text_box.h>
|
||||
|
||||
static bool
|
||||
text_assign(struct mjs* mjs, TextBox* text_box, JsViewPropValue value, FuriString* context) {
|
||||
UNUSED(mjs);
|
||||
furi_string_set(context, value.string);
|
||||
text_box_set_text(text_box, furi_string_get_cstr(context));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool font_assign(struct mjs* mjs, TextBox* text_box, JsViewPropValue value, void* context) {
|
||||
UNUSED(context);
|
||||
TextBoxFont font;
|
||||
if(strcasecmp(value.string, "hex") == 0) {
|
||||
font = TextBoxFontHex;
|
||||
} else if(strcasecmp(value.string, "text") == 0) {
|
||||
font = TextBoxFontText;
|
||||
} else {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "must be one of: \"text\", \"hex\"");
|
||||
return false;
|
||||
}
|
||||
text_box_set_font(text_box, font);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
focus_assign(struct mjs* mjs, TextBox* text_box, JsViewPropValue value, void* context) {
|
||||
UNUSED(context);
|
||||
TextBoxFocus focus;
|
||||
if(strcasecmp(value.string, "start") == 0) {
|
||||
focus = TextBoxFocusStart;
|
||||
} else if(strcasecmp(value.string, "end") == 0) {
|
||||
focus = TextBoxFocusEnd;
|
||||
} else {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "must be one of: \"start\", \"end\"");
|
||||
return false;
|
||||
}
|
||||
text_box_set_focus(text_box, focus);
|
||||
return true;
|
||||
}
|
||||
|
||||
FuriString* ctx_make(struct mjs* mjs, TextBox* specific_view, mjs_val_t view_obj) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(specific_view);
|
||||
UNUSED(view_obj);
|
||||
return furi_string_alloc();
|
||||
}
|
||||
|
||||
void ctx_destroy(TextBox* specific_view, FuriString* context, FuriEventLoop* loop) {
|
||||
UNUSED(specific_view);
|
||||
UNUSED(loop);
|
||||
furi_string_free(context);
|
||||
}
|
||||
|
||||
static const JsViewDescriptor view_descriptor = {
|
||||
.alloc = (JsViewAlloc)text_box_alloc,
|
||||
.free = (JsViewFree)text_box_free,
|
||||
.get_view = (JsViewGetView)text_box_get_view,
|
||||
.custom_make = (JsViewCustomMake)ctx_make,
|
||||
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
|
||||
.prop_cnt = 3,
|
||||
.props = {
|
||||
(JsViewPropDescriptor){
|
||||
.name = "text",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)text_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "font",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)font_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "focus",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)focus_assign},
|
||||
}};
|
||||
JS_GUI_VIEW_DEF(text_box, &view_descriptor);
|
||||
@@ -0,0 +1,173 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "js_gui.h"
|
||||
#include "../js_event_loop/js_event_loop.h"
|
||||
#include <gui/modules/text_input.h>
|
||||
|
||||
#define DEFAULT_BUF_SZ 33
|
||||
|
||||
typedef struct {
|
||||
char* buffer;
|
||||
size_t buffer_size;
|
||||
FuriString* header;
|
||||
bool default_text_clear;
|
||||
FuriSemaphore* input_semaphore;
|
||||
JsEventLoopContract contract;
|
||||
} JsKbdContext;
|
||||
|
||||
static mjs_val_t
|
||||
input_transformer(struct mjs* mjs, FuriSemaphore* semaphore, JsKbdContext* context) {
|
||||
furi_check(furi_semaphore_acquire(semaphore, 0) == FuriStatusOk);
|
||||
return mjs_mk_string(mjs, context->buffer, ~0, true);
|
||||
}
|
||||
|
||||
static void input_callback(JsKbdContext* context) {
|
||||
furi_semaphore_release(context->input_semaphore);
|
||||
}
|
||||
|
||||
static bool
|
||||
header_assign(struct mjs* mjs, TextInput* input, JsViewPropValue value, JsKbdContext* context) {
|
||||
UNUSED(mjs);
|
||||
furi_string_set(context->header, value.string);
|
||||
text_input_set_header_text(input, furi_string_get_cstr(context->header));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool min_len_assign(
|
||||
struct mjs* mjs,
|
||||
TextInput* input,
|
||||
JsViewPropValue value,
|
||||
JsKbdContext* context) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(context);
|
||||
text_input_set_minimum_length(input, (size_t)value.number);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool max_len_assign(
|
||||
struct mjs* mjs,
|
||||
TextInput* input,
|
||||
JsViewPropValue value,
|
||||
JsKbdContext* context) {
|
||||
UNUSED(mjs);
|
||||
context->buffer_size = (size_t)(value.number + 1);
|
||||
context->buffer = realloc(context->buffer, context->buffer_size); //-V701
|
||||
text_input_set_result_callback(
|
||||
input,
|
||||
(TextInputCallback)input_callback,
|
||||
context,
|
||||
context->buffer,
|
||||
context->buffer_size,
|
||||
context->default_text_clear);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool default_text_assign(
|
||||
struct mjs* mjs,
|
||||
TextInput* input,
|
||||
JsViewPropValue value,
|
||||
JsKbdContext* context) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(input);
|
||||
|
||||
if(value.string) {
|
||||
strlcpy(context->buffer, value.string, context->buffer_size);
|
||||
text_input_set_result_callback(
|
||||
input,
|
||||
(TextInputCallback)input_callback,
|
||||
context,
|
||||
context->buffer,
|
||||
context->buffer_size,
|
||||
context->default_text_clear);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool default_text_clear_assign(
|
||||
struct mjs* mjs,
|
||||
TextInput* input,
|
||||
JsViewPropValue value,
|
||||
JsKbdContext* context) {
|
||||
UNUSED(mjs);
|
||||
|
||||
context->default_text_clear = value.boolean;
|
||||
text_input_set_result_callback(
|
||||
input,
|
||||
(TextInputCallback)input_callback,
|
||||
context,
|
||||
context->buffer,
|
||||
context->buffer_size,
|
||||
context->default_text_clear);
|
||||
return true;
|
||||
}
|
||||
|
||||
static JsKbdContext* ctx_make(struct mjs* mjs, TextInput* input, mjs_val_t view_obj) {
|
||||
JsKbdContext* context = malloc(sizeof(JsKbdContext));
|
||||
*context = (JsKbdContext){
|
||||
.buffer_size = DEFAULT_BUF_SZ,
|
||||
.buffer = malloc(DEFAULT_BUF_SZ),
|
||||
.header = furi_string_alloc(),
|
||||
.default_text_clear = false,
|
||||
.input_semaphore = furi_semaphore_alloc(1, 0),
|
||||
};
|
||||
context->contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object_type = JsEventLoopObjectTypeSemaphore,
|
||||
.object = context->input_semaphore,
|
||||
.non_timer =
|
||||
{
|
||||
.event = FuriEventLoopEventIn,
|
||||
.transformer = (JsEventLoopTransformer)input_transformer,
|
||||
.transformer_context = context,
|
||||
},
|
||||
};
|
||||
text_input_set_result_callback(
|
||||
input,
|
||||
(TextInputCallback)input_callback,
|
||||
context,
|
||||
context->buffer,
|
||||
context->buffer_size,
|
||||
context->default_text_clear);
|
||||
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
|
||||
return context;
|
||||
}
|
||||
|
||||
static void ctx_destroy(TextInput* input, JsKbdContext* context, FuriEventLoop* loop) {
|
||||
UNUSED(input);
|
||||
furi_event_loop_maybe_unsubscribe(loop, context->input_semaphore);
|
||||
furi_semaphore_free(context->input_semaphore);
|
||||
furi_string_free(context->header);
|
||||
free(context->buffer);
|
||||
free(context);
|
||||
}
|
||||
|
||||
static const JsViewDescriptor view_descriptor = {
|
||||
.alloc = (JsViewAlloc)text_input_alloc,
|
||||
.free = (JsViewFree)text_input_free,
|
||||
.get_view = (JsViewGetView)text_input_get_view,
|
||||
.custom_make = (JsViewCustomMake)ctx_make,
|
||||
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
|
||||
.prop_cnt = 5,
|
||||
.props = {
|
||||
(JsViewPropDescriptor){
|
||||
.name = "header",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)header_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "minLength",
|
||||
.type = JsViewPropTypeNumber,
|
||||
.assign = (JsViewPropAssign)min_len_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "maxLength",
|
||||
.type = JsViewPropTypeNumber,
|
||||
.assign = (JsViewPropAssign)max_len_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "defaultText",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)default_text_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "defaultTextClear",
|
||||
.type = JsViewPropTypeBool,
|
||||
.assign = (JsViewPropAssign)default_text_clear_assign},
|
||||
}};
|
||||
|
||||
JS_GUI_VIEW_DEF(text_input, &view_descriptor);
|
||||
@@ -1,206 +0,0 @@
|
||||
#include "../js_modules.h"
|
||||
#include <gui/modules/text_input.h>
|
||||
#include <gui/modules/byte_input.h>
|
||||
#include <gui/view_holder.h>
|
||||
#include <toolbox/api_lock.h>
|
||||
|
||||
#define membersof(x) (sizeof(x) / sizeof(x[0]))
|
||||
|
||||
typedef struct {
|
||||
TextInput* text_input;
|
||||
ByteInput* byte_input;
|
||||
ViewHolder* view_holder;
|
||||
FuriApiLock lock;
|
||||
char* header;
|
||||
bool accepted;
|
||||
} JsKeyboardInst;
|
||||
|
||||
static void ret_bad_args(struct mjs* mjs, const char* error) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error);
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static JsKeyboardInst* get_this_ctx(struct mjs* mjs) {
|
||||
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
|
||||
JsKeyboardInst* keyboard = mjs_get_ptr(mjs, obj_inst);
|
||||
furi_assert(keyboard);
|
||||
return keyboard;
|
||||
}
|
||||
|
||||
static void keyboard_callback(void* context) {
|
||||
JsKeyboardInst* keyboard = (JsKeyboardInst*)context;
|
||||
keyboard->accepted = true;
|
||||
api_lock_unlock(keyboard->lock);
|
||||
}
|
||||
|
||||
static void keyboard_exit(void* context) {
|
||||
JsKeyboardInst* keyboard = (JsKeyboardInst*)context;
|
||||
keyboard->accepted = false;
|
||||
api_lock_unlock(keyboard->lock);
|
||||
}
|
||||
|
||||
static void js_keyboard_set_header(struct mjs* mjs) {
|
||||
JsKeyboardInst* keyboard = get_this_ctx(mjs);
|
||||
|
||||
mjs_val_t header_arg = mjs_arg(mjs, 0);
|
||||
const char* header = mjs_get_string(mjs, &header_arg, NULL);
|
||||
if(!header) {
|
||||
ret_bad_args(mjs, "Header must be a string");
|
||||
return;
|
||||
}
|
||||
|
||||
if(keyboard->header) {
|
||||
free(keyboard->header);
|
||||
}
|
||||
keyboard->header = strdup(header);
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_keyboard_text(struct mjs* mjs) {
|
||||
JsKeyboardInst* keyboard = get_this_ctx(mjs);
|
||||
|
||||
mjs_val_t input_length_arg = mjs_arg(mjs, 0);
|
||||
if(!mjs_is_number(input_length_arg)) {
|
||||
ret_bad_args(mjs, "Input length must be a number");
|
||||
return;
|
||||
}
|
||||
int32_t input_length = mjs_get_int32(mjs, input_length_arg);
|
||||
char* buffer = malloc(input_length);
|
||||
|
||||
mjs_val_t default_text_arg = mjs_arg(mjs, 1);
|
||||
const char* default_text = mjs_get_string(mjs, &default_text_arg, NULL);
|
||||
bool clear_default = false;
|
||||
if(default_text) {
|
||||
strlcpy(buffer, default_text, input_length);
|
||||
mjs_val_t bool_obj = mjs_arg(mjs, 2);
|
||||
clear_default = mjs_get_bool(mjs, bool_obj);
|
||||
}
|
||||
|
||||
if(keyboard->header) {
|
||||
text_input_set_header_text(keyboard->text_input, keyboard->header);
|
||||
}
|
||||
text_input_set_result_callback(
|
||||
keyboard->text_input, keyboard_callback, keyboard, buffer, input_length, clear_default);
|
||||
text_input_add_illegal_symbols(keyboard->text_input);
|
||||
text_input_set_minimum_length(keyboard->text_input, 0);
|
||||
|
||||
keyboard->lock = api_lock_alloc_locked();
|
||||
Gui* gui = furi_record_open(RECORD_GUI);
|
||||
keyboard->view_holder = view_holder_alloc();
|
||||
view_holder_attach_to_gui(keyboard->view_holder, gui);
|
||||
view_holder_set_back_callback(keyboard->view_holder, keyboard_exit, keyboard);
|
||||
|
||||
view_holder_set_view(keyboard->view_holder, text_input_get_view(keyboard->text_input));
|
||||
api_lock_wait_unlock(keyboard->lock);
|
||||
|
||||
view_holder_set_view(keyboard->view_holder, NULL);
|
||||
view_holder_free(keyboard->view_holder);
|
||||
|
||||
furi_record_close(RECORD_GUI);
|
||||
api_lock_free(keyboard->lock);
|
||||
|
||||
text_input_reset(keyboard->text_input);
|
||||
if(keyboard->header) {
|
||||
free(keyboard->header);
|
||||
keyboard->header = NULL;
|
||||
}
|
||||
if(keyboard->accepted) {
|
||||
mjs_return(mjs, mjs_mk_string(mjs, buffer, ~0, true));
|
||||
} else {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
free(buffer);
|
||||
}
|
||||
|
||||
static void js_keyboard_byte(struct mjs* mjs) {
|
||||
JsKeyboardInst* keyboard = get_this_ctx(mjs);
|
||||
|
||||
mjs_val_t input_length_arg = mjs_arg(mjs, 0);
|
||||
if(!mjs_is_number(input_length_arg)) {
|
||||
ret_bad_args(mjs, "Input length must be a number");
|
||||
return;
|
||||
}
|
||||
int32_t input_length = mjs_get_int32(mjs, input_length_arg);
|
||||
uint8_t* buffer = malloc(input_length);
|
||||
|
||||
mjs_val_t default_data_arg = mjs_arg(mjs, 1);
|
||||
if(mjs_is_typed_array(default_data_arg)) {
|
||||
if(mjs_is_data_view(default_data_arg)) {
|
||||
default_data_arg = mjs_dataview_get_buf(mjs, default_data_arg);
|
||||
}
|
||||
size_t default_data_len = 0;
|
||||
char* default_data = mjs_array_buf_get_ptr(mjs, default_data_arg, &default_data_len);
|
||||
memcpy(buffer, (uint8_t*)default_data, MIN((size_t)input_length, default_data_len));
|
||||
}
|
||||
|
||||
if(keyboard->header) {
|
||||
byte_input_set_header_text(keyboard->byte_input, keyboard->header);
|
||||
}
|
||||
byte_input_set_result_callback(
|
||||
keyboard->byte_input, keyboard_callback, NULL, keyboard, buffer, input_length);
|
||||
|
||||
keyboard->lock = api_lock_alloc_locked();
|
||||
Gui* gui = furi_record_open(RECORD_GUI);
|
||||
keyboard->view_holder = view_holder_alloc();
|
||||
view_holder_attach_to_gui(keyboard->view_holder, gui);
|
||||
view_holder_set_back_callback(keyboard->view_holder, keyboard_exit, keyboard);
|
||||
|
||||
view_holder_set_view(keyboard->view_holder, byte_input_get_view(keyboard->byte_input));
|
||||
api_lock_wait_unlock(keyboard->lock);
|
||||
|
||||
view_holder_set_view(keyboard->view_holder, NULL);
|
||||
view_holder_free(keyboard->view_holder);
|
||||
|
||||
furi_record_close(RECORD_GUI);
|
||||
api_lock_free(keyboard->lock);
|
||||
|
||||
if(keyboard->header) {
|
||||
free(keyboard->header);
|
||||
keyboard->header = NULL;
|
||||
}
|
||||
byte_input_set_result_callback(keyboard->byte_input, NULL, NULL, NULL, NULL, 0);
|
||||
byte_input_set_header_text(keyboard->byte_input, "");
|
||||
if(keyboard->accepted) {
|
||||
mjs_return(mjs, mjs_mk_array_buf(mjs, (char*)buffer, input_length));
|
||||
} else {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
free(buffer);
|
||||
}
|
||||
|
||||
static void* js_keyboard_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
JsKeyboardInst* keyboard = malloc(sizeof(JsKeyboardInst));
|
||||
mjs_val_t keyboard_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, keyboard_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, keyboard));
|
||||
mjs_set(mjs, keyboard_obj, "setHeader", ~0, MJS_MK_FN(js_keyboard_set_header));
|
||||
mjs_set(mjs, keyboard_obj, "text", ~0, MJS_MK_FN(js_keyboard_text));
|
||||
mjs_set(mjs, keyboard_obj, "byte", ~0, MJS_MK_FN(js_keyboard_byte));
|
||||
keyboard->byte_input = byte_input_alloc();
|
||||
keyboard->text_input = text_input_alloc();
|
||||
*object = keyboard_obj;
|
||||
return keyboard;
|
||||
}
|
||||
|
||||
static void js_keyboard_destroy(void* inst) {
|
||||
JsKeyboardInst* keyboard = inst;
|
||||
byte_input_free(keyboard->byte_input);
|
||||
text_input_free(keyboard->text_input);
|
||||
free(keyboard);
|
||||
}
|
||||
|
||||
static const JsModuleDescriptor js_keyboard_desc = {
|
||||
"keyboard",
|
||||
js_keyboard_create,
|
||||
js_keyboard_destroy,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
.appid = PLUGIN_APP_ID,
|
||||
.ep_api_version = PLUGIN_API_VERSION,
|
||||
.entry_point = &js_keyboard_desc,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* js_keyboard_ep(void) {
|
||||
return &plugin_descriptor;
|
||||
}
|
||||
@@ -305,7 +305,8 @@ void js_math_trunc(struct mjs* mjs) {
|
||||
mjs_return(mjs, mjs_mk_number(mjs, x < (double)0. ? ceil(x) : floor(x)));
|
||||
}
|
||||
|
||||
static void* js_math_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
static void* js_math_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
mjs_val_t math_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, math_obj, "is_equal", ~0, MJS_MK_FN(js_math_is_equal));
|
||||
mjs_set(mjs, math_obj, "abs", ~0, MJS_MK_FN(js_math_abs));
|
||||
@@ -342,6 +343,7 @@ static const JsModuleDescriptor js_math_desc = {
|
||||
"math",
|
||||
js_math_create,
|
||||
NULL,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
|
||||
@@ -75,7 +75,8 @@ static void js_notify_blink(struct mjs* mjs) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void* js_notification_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
static void* js_notification_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
|
||||
mjs_val_t notify_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, notify_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, notification));
|
||||
@@ -96,6 +97,7 @@ static const JsModuleDescriptor js_notification_desc = {
|
||||
"notification",
|
||||
js_notification_create,
|
||||
js_notification_destroy,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
|
||||
@@ -658,7 +658,8 @@ static void js_serial_expect(struct mjs* mjs) {
|
||||
}
|
||||
}
|
||||
|
||||
static void* js_serial_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
static void* js_serial_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
JsSerialInst* js_serial = malloc(sizeof(JsSerialInst));
|
||||
js_serial->mjs = mjs;
|
||||
mjs_val_t serial_obj = mjs_mk_object(mjs);
|
||||
@@ -686,6 +687,7 @@ static const JsModuleDescriptor js_serial_desc = {
|
||||
"serial",
|
||||
js_serial_create,
|
||||
js_serial_destroy,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
|
||||
@@ -1,335 +1,460 @@
|
||||
#include "../js_modules.h"
|
||||
#include <storage/storage.h>
|
||||
#include "../js_modules.h" // IWYU pragma: keep
|
||||
#include <path.h>
|
||||
|
||||
typedef struct {
|
||||
Storage* api;
|
||||
File* virtual;
|
||||
} JsStorageInst;
|
||||
// ---=== file ops ===---
|
||||
|
||||
static JsStorageInst* get_this_ctx(struct mjs* mjs) {
|
||||
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
|
||||
JsStorageInst* storage = mjs_get_ptr(mjs, obj_inst);
|
||||
furi_assert(storage);
|
||||
return storage;
|
||||
static void js_storage_file_close(struct mjs* mjs) {
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_close(file)));
|
||||
}
|
||||
|
||||
static void ret_bad_args(struct mjs* mjs, const char* error) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error);
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
static void js_storage_file_is_open(struct mjs* mjs) {
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_is_open(file)));
|
||||
}
|
||||
|
||||
static void ret_int_err(struct mjs* mjs, const char* error) {
|
||||
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "%s", error);
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static bool check_arg_count(struct mjs* mjs, size_t count) {
|
||||
size_t num_args = mjs_nargs(mjs);
|
||||
if(num_args != count) {
|
||||
ret_bad_args(mjs, "Wrong argument count");
|
||||
return false;
|
||||
static void js_storage_file_read(struct mjs* mjs) {
|
||||
enum {
|
||||
ReadModeAscii,
|
||||
ReadModeBinary,
|
||||
} read_mode;
|
||||
JS_ENUM_MAP(read_mode, {"ascii", ReadModeAscii}, {"binary", ReadModeBinary});
|
||||
int32_t length;
|
||||
JS_FETCH_ARGS_OR_RETURN(
|
||||
mjs, JS_EXACTLY, JS_ARG_ENUM(read_mode, "ReadMode"), JS_ARG_INT32(&length));
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
char buffer[length];
|
||||
size_t actually_read = storage_file_read(file, buffer, length);
|
||||
if(read_mode == ReadModeAscii) {
|
||||
mjs_return(mjs, mjs_mk_string(mjs, buffer, actually_read, true));
|
||||
} else if(read_mode == ReadModeBinary) {
|
||||
mjs_return(mjs, mjs_mk_array_buf(mjs, buffer, actually_read));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool get_path_arg(struct mjs* mjs, const char** path, size_t index) {
|
||||
mjs_val_t path_obj = mjs_arg(mjs, index);
|
||||
if(!mjs_is_string(path_obj)) {
|
||||
ret_bad_args(mjs, "Path must be a string");
|
||||
return false;
|
||||
static void js_storage_file_write(struct mjs* mjs) {
|
||||
mjs_val_t data;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&data));
|
||||
const void* buf;
|
||||
size_t len;
|
||||
if(mjs_is_string(data)) {
|
||||
buf = mjs_get_string(mjs, &data, &len);
|
||||
} else if(mjs_is_array_buf(data)) {
|
||||
buf = mjs_array_buf_get_ptr(mjs, data, &len);
|
||||
} else {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "argument 0: expected string or ArrayBuffer");
|
||||
}
|
||||
size_t path_len = 0;
|
||||
*path = mjs_get_string(mjs, &path_obj, &path_len);
|
||||
if((path_len == 0) || (*path == NULL)) {
|
||||
ret_bad_args(mjs, "Bad path argument");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_number(mjs, storage_file_write(file, buf, len)));
|
||||
}
|
||||
|
||||
static void js_storage_read(struct mjs* mjs) {
|
||||
JsStorageInst* storage = get_this_ctx(mjs);
|
||||
static void js_storage_file_seek_relative(struct mjs* mjs) {
|
||||
int32_t offset;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&offset));
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_seek(file, offset, false)));
|
||||
}
|
||||
|
||||
const char* path;
|
||||
if(!get_path_arg(mjs, &path, 0)) return;
|
||||
static void js_storage_file_seek_absolute(struct mjs* mjs) {
|
||||
int32_t offset;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&offset));
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_seek(file, offset, true)));
|
||||
}
|
||||
|
||||
File* file = storage_file_alloc(storage->api);
|
||||
do {
|
||||
if(!storage_file_open(file, path, FSAM_READ, FSOM_OPEN_EXISTING)) {
|
||||
ret_int_err(mjs, storage_file_get_error_desc(file));
|
||||
break;
|
||||
}
|
||||
static void js_storage_file_tell(struct mjs* mjs) {
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_number(mjs, storage_file_tell(file)));
|
||||
}
|
||||
|
||||
uint64_t size = storage_file_size(file);
|
||||
mjs_val_t size_arg = mjs_arg(mjs, 1);
|
||||
if(mjs_is_number(size_arg)) {
|
||||
size = mjs_get_int32(mjs, size_arg);
|
||||
}
|
||||
static void js_storage_file_truncate(struct mjs* mjs) {
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_truncate(file)));
|
||||
}
|
||||
|
||||
mjs_val_t seek_arg = mjs_arg(mjs, 2);
|
||||
if(mjs_is_number(seek_arg)) {
|
||||
storage_file_seek(file, mjs_get_int32(mjs, seek_arg), true);
|
||||
size = MIN(size, storage_file_size(file) - storage_file_tell(file));
|
||||
}
|
||||
static void js_storage_file_size(struct mjs* mjs) {
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_number(mjs, storage_file_size(file)));
|
||||
}
|
||||
|
||||
if(size > memmgr_heap_get_max_free_block()) {
|
||||
ret_int_err(mjs, "Read size too large");
|
||||
break;
|
||||
}
|
||||
static void js_storage_file_eof(struct mjs* mjs) {
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_eof(file)));
|
||||
}
|
||||
|
||||
uint8_t* data = malloc(size);
|
||||
size_t read = storage_file_read(file, data, size);
|
||||
if(read == size) {
|
||||
mjs_return(mjs, mjs_mk_array_buf(mjs, (char*)data, size));
|
||||
} else {
|
||||
ret_int_err(mjs, "File read failed");
|
||||
}
|
||||
free(data);
|
||||
} while(0);
|
||||
static void js_storage_file_copy_to(struct mjs* mjs) {
|
||||
File* source = JS_GET_CONTEXT(mjs);
|
||||
mjs_val_t dest_obj;
|
||||
int32_t bytes;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&dest_obj), JS_ARG_INT32(&bytes));
|
||||
File* destination = JS_GET_INST(mjs, dest_obj);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_copy_to_file(source, destination, bytes)));
|
||||
}
|
||||
|
||||
// ---=== top-level file ops ===---
|
||||
|
||||
// common destructor for file and dir objects
|
||||
static void js_storage_file_destructor(struct mjs* mjs, mjs_val_t obj) {
|
||||
File* file = JS_GET_INST(mjs, obj);
|
||||
storage_file_free(file);
|
||||
}
|
||||
|
||||
static void js_storage_write(struct mjs* mjs) {
|
||||
JsStorageInst* storage = get_this_ctx(mjs);
|
||||
|
||||
static void js_storage_open_file(struct mjs* mjs) {
|
||||
const char* path;
|
||||
if(!get_path_arg(mjs, &path, 0)) return;
|
||||
FS_AccessMode access_mode;
|
||||
FS_OpenMode open_mode;
|
||||
JS_ENUM_MAP(access_mode, {"r", FSAM_READ}, {"w", FSAM_WRITE}, {"rw", FSAM_READ_WRITE});
|
||||
JS_ENUM_MAP(
|
||||
open_mode,
|
||||
{"open_existing", FSOM_OPEN_EXISTING},
|
||||
{"open_always", FSOM_OPEN_ALWAYS},
|
||||
{"open_append", FSOM_OPEN_APPEND},
|
||||
{"create_new", FSOM_CREATE_NEW},
|
||||
{"create_always", FSOM_CREATE_ALWAYS});
|
||||
JS_FETCH_ARGS_OR_RETURN(
|
||||
mjs,
|
||||
JS_EXACTLY,
|
||||
JS_ARG_STR(&path),
|
||||
JS_ARG_ENUM(access_mode, "AccessMode"),
|
||||
JS_ARG_ENUM(open_mode, "OpenMode"));
|
||||
|
||||
mjs_val_t data_arg = mjs_arg(mjs, 1);
|
||||
if(!mjs_is_typed_array(data_arg) && !mjs_is_string(data_arg)) {
|
||||
ret_bad_args(mjs, "Data must be string, arraybuf or dataview");
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
File* file = storage_file_alloc(storage);
|
||||
if(!storage_file_open(file, path, access_mode, open_mode)) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
if(mjs_is_data_view(data_arg)) {
|
||||
data_arg = mjs_dataview_get_buf(mjs, data_arg);
|
||||
|
||||
mjs_val_t file_obj = mjs_mk_object(mjs);
|
||||
JS_ASSIGN_MULTI(mjs, file_obj) {
|
||||
JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, file));
|
||||
JS_FIELD(MJS_DESTRUCTOR_PROP_NAME, MJS_MK_FN(js_storage_file_destructor));
|
||||
JS_FIELD("close", MJS_MK_FN(js_storage_file_close));
|
||||
JS_FIELD("isOpen", MJS_MK_FN(js_storage_file_is_open));
|
||||
JS_FIELD("read", MJS_MK_FN(js_storage_file_read));
|
||||
JS_FIELD("write", MJS_MK_FN(js_storage_file_write));
|
||||
JS_FIELD("seekRelative", MJS_MK_FN(js_storage_file_seek_relative));
|
||||
JS_FIELD("seekAbsolute", MJS_MK_FN(js_storage_file_seek_absolute));
|
||||
JS_FIELD("tell", MJS_MK_FN(js_storage_file_tell));
|
||||
JS_FIELD("truncate", MJS_MK_FN(js_storage_file_truncate));
|
||||
JS_FIELD("size", MJS_MK_FN(js_storage_file_size));
|
||||
JS_FIELD("eof", MJS_MK_FN(js_storage_file_eof));
|
||||
JS_FIELD("copyTo", MJS_MK_FN(js_storage_file_copy_to));
|
||||
}
|
||||
size_t data_len = 0;
|
||||
const char* data = NULL;
|
||||
if(mjs_is_string(data_arg)) {
|
||||
data = mjs_get_string(mjs, &data_arg, &data_len);
|
||||
} else if(mjs_is_typed_array(data_arg)) {
|
||||
data = mjs_array_buf_get_ptr(mjs, data_arg, &data_len);
|
||||
}
|
||||
|
||||
mjs_val_t seek_arg = mjs_arg(mjs, 2);
|
||||
|
||||
File* file = storage_file_alloc(storage->api);
|
||||
if(!storage_file_open(
|
||||
file,
|
||||
path,
|
||||
FSAM_WRITE,
|
||||
mjs_is_number(seek_arg) ? FSOM_OPEN_ALWAYS : FSOM_CREATE_ALWAYS)) {
|
||||
ret_int_err(mjs, storage_file_get_error_desc(file));
|
||||
|
||||
} else {
|
||||
if(mjs_is_number(seek_arg)) {
|
||||
storage_file_seek(file, mjs_get_int32(mjs, seek_arg), true);
|
||||
}
|
||||
|
||||
size_t write = storage_file_write(file, data, data_len);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, write == data_len));
|
||||
}
|
||||
storage_file_free(file);
|
||||
mjs_return(mjs, file_obj);
|
||||
}
|
||||
|
||||
static void js_storage_append(struct mjs* mjs) {
|
||||
JsStorageInst* storage = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 2)) return;
|
||||
|
||||
static void js_storage_file_exists(struct mjs* mjs) {
|
||||
const char* path;
|
||||
if(!get_path_arg(mjs, &path, 0)) return;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_exists(storage, path)));
|
||||
}
|
||||
|
||||
mjs_val_t data_arg = mjs_arg(mjs, 1);
|
||||
if(!mjs_is_typed_array(data_arg) && !mjs_is_string(data_arg)) {
|
||||
ret_bad_args(mjs, "Data must be string, arraybuf or dataview");
|
||||
// ---=== dir ops ===---
|
||||
|
||||
static void js_storage_read_directory(struct mjs* mjs) {
|
||||
const char* path;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
File* dir = storage_file_alloc(storage);
|
||||
if(!storage_dir_open(dir, path)) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
if(mjs_is_data_view(data_arg)) {
|
||||
data_arg = mjs_dataview_get_buf(mjs, data_arg);
|
||||
}
|
||||
size_t data_len = 0;
|
||||
const char* data = NULL;
|
||||
if(mjs_is_string(data_arg)) {
|
||||
data = mjs_get_string(mjs, &data_arg, &data_len);
|
||||
} else if(mjs_is_typed_array(data_arg)) {
|
||||
data = mjs_array_buf_get_ptr(mjs, data_arg, &data_len);
|
||||
|
||||
FileInfo file_info;
|
||||
char name[128];
|
||||
FuriString* file_path = furi_string_alloc_set_str(path);
|
||||
size_t path_size = furi_string_size(file_path);
|
||||
uint32_t timestamp;
|
||||
|
||||
mjs_val_t ret = mjs_mk_array(mjs);
|
||||
while(storage_dir_read(dir, &file_info, name, sizeof(name))) {
|
||||
furi_string_left(file_path, path_size);
|
||||
path_append(file_path, name);
|
||||
furi_check(
|
||||
storage_common_timestamp(storage, furi_string_get_cstr(file_path), ×tamp) ==
|
||||
FSE_OK);
|
||||
mjs_val_t obj = mjs_mk_object(mjs);
|
||||
JS_ASSIGN_MULTI(mjs, obj) {
|
||||
JS_FIELD("path", mjs_mk_string(mjs, name, ~0, true));
|
||||
JS_FIELD("isDirectory", mjs_mk_boolean(mjs, file_info_is_dir(&file_info)));
|
||||
JS_FIELD("size", mjs_mk_number(mjs, file_info.size));
|
||||
JS_FIELD("timestamp", mjs_mk_number(mjs, timestamp));
|
||||
}
|
||||
mjs_array_push(mjs, ret, obj);
|
||||
}
|
||||
|
||||
File* file = storage_file_alloc(storage->api);
|
||||
if(!storage_file_open(file, path, FSAM_WRITE, FSOM_OPEN_APPEND)) {
|
||||
ret_int_err(mjs, storage_file_get_error_desc(file));
|
||||
} else {
|
||||
size_t write = storage_file_write(file, data, data_len);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, write == data_len));
|
||||
}
|
||||
storage_file_free(file);
|
||||
storage_file_free(dir);
|
||||
furi_string_free(file_path);
|
||||
mjs_return(mjs, ret);
|
||||
}
|
||||
|
||||
static void js_storage_exists(struct mjs* mjs) {
|
||||
JsStorageInst* storage = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 1)) return;
|
||||
|
||||
static void js_storage_directory_exists(struct mjs* mjs) {
|
||||
const char* path;
|
||||
if(!get_path_arg(mjs, &path, 0)) return;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_dir_exists(storage, path)));
|
||||
}
|
||||
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_exists(storage->api, path)));
|
||||
static void js_storage_make_directory(struct mjs* mjs) {
|
||||
const char* path;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_mkdir(storage, path)));
|
||||
}
|
||||
|
||||
// ---=== common ops ===---
|
||||
|
||||
static void js_storage_file_or_dir_exists(struct mjs* mjs) {
|
||||
const char* path;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_exists(storage, path)));
|
||||
}
|
||||
|
||||
static void js_storage_stat(struct mjs* mjs) {
|
||||
const char* path;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
FileInfo file_info;
|
||||
uint32_t timestamp;
|
||||
if((storage_common_stat(storage, path, &file_info) |
|
||||
storage_common_timestamp(storage, path, ×tamp)) != FSE_OK) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
mjs_val_t ret = mjs_mk_object(mjs);
|
||||
JS_ASSIGN_MULTI(mjs, ret) {
|
||||
JS_FIELD("path", mjs_mk_string(mjs, path, ~0, 1));
|
||||
JS_FIELD("isDirectory", mjs_mk_boolean(mjs, file_info_is_dir(&file_info)));
|
||||
JS_FIELD("size", mjs_mk_number(mjs, file_info.size));
|
||||
JS_FIELD("accessTime", mjs_mk_number(mjs, timestamp));
|
||||
}
|
||||
mjs_return(mjs, ret);
|
||||
}
|
||||
|
||||
static void js_storage_remove(struct mjs* mjs) {
|
||||
JsStorageInst* storage = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 1)) return;
|
||||
|
||||
const char* path;
|
||||
if(!get_path_arg(mjs, &path, 0)) return;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_remove(storage, path)));
|
||||
}
|
||||
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_remove(storage->api, path)));
|
||||
static void js_storage_rmrf(struct mjs* mjs) {
|
||||
const char* path;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_remove_recursive(storage, path)));
|
||||
}
|
||||
|
||||
static void js_storage_rename(struct mjs* mjs) {
|
||||
const char *old, *new;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&old), JS_ARG_STR(&new));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
FS_Error status = storage_common_rename(storage, old, new);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, status == FSE_OK));
|
||||
}
|
||||
|
||||
static void js_storage_copy(struct mjs* mjs) {
|
||||
JsStorageInst* storage = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 2)) return;
|
||||
const char *source, *dest;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&source), JS_ARG_STR(&dest));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
FS_Error status = storage_common_copy(storage, source, dest);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, status == FSE_OK || status == FSE_EXIST));
|
||||
}
|
||||
|
||||
const char* old_path;
|
||||
if(!get_path_arg(mjs, &old_path, 0)) return;
|
||||
|
||||
const char* new_path;
|
||||
if(!get_path_arg(mjs, &new_path, 1)) return;
|
||||
|
||||
FS_Error error = storage_common_copy(storage->api, old_path, new_path);
|
||||
if(error == FSE_OK) {
|
||||
static void js_storage_fs_info(struct mjs* mjs) {
|
||||
const char* fs;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&fs));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
uint64_t total_space, free_space;
|
||||
if(storage_common_fs_info(storage, fs, &total_space, &free_space) != FSE_OK) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
} else {
|
||||
ret_int_err(mjs, storage_error_get_desc(error));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void js_storage_move(struct mjs* mjs) {
|
||||
JsStorageInst* storage = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 2)) return;
|
||||
|
||||
const char* old_path;
|
||||
if(!get_path_arg(mjs, &old_path, 0)) return;
|
||||
|
||||
const char* new_path;
|
||||
if(!get_path_arg(mjs, &new_path, 1)) return;
|
||||
|
||||
FS_Error error = storage_common_rename(storage->api, old_path, new_path);
|
||||
if(error == FSE_OK) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
} else {
|
||||
ret_int_err(mjs, storage_error_get_desc(error));
|
||||
mjs_val_t ret = mjs_mk_object(mjs);
|
||||
JS_ASSIGN_MULTI(mjs, ret) {
|
||||
JS_FIELD("totalSpace", mjs_mk_number(mjs, total_space));
|
||||
JS_FIELD("freeSpace", mjs_mk_number(mjs, free_space));
|
||||
}
|
||||
mjs_return(mjs, ret);
|
||||
}
|
||||
|
||||
static void js_storage_mkdir(struct mjs* mjs) {
|
||||
JsStorageInst* storage = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 1)) return;
|
||||
|
||||
const char* path;
|
||||
if(!get_path_arg(mjs, &path, 0)) return;
|
||||
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_mkdir(storage->api, path)));
|
||||
static void js_storage_next_available_filename(struct mjs* mjs) {
|
||||
const char *dir_path, *file_name, *file_ext;
|
||||
int32_t max_len;
|
||||
JS_FETCH_ARGS_OR_RETURN(
|
||||
mjs,
|
||||
JS_EXACTLY,
|
||||
JS_ARG_STR(&dir_path),
|
||||
JS_ARG_STR(&file_name),
|
||||
JS_ARG_STR(&file_ext),
|
||||
JS_ARG_INT32(&max_len));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
FuriString* next_name = furi_string_alloc();
|
||||
storage_get_next_filename(storage, dir_path, file_name, file_ext, next_name, max_len);
|
||||
mjs_return(mjs, mjs_mk_string(mjs, furi_string_get_cstr(next_name), ~0, true));
|
||||
furi_string_free(next_name);
|
||||
}
|
||||
|
||||
// ---=== path ops ===---
|
||||
|
||||
static void js_storage_are_paths_equal(struct mjs* mjs) {
|
||||
const char *path1, *path2;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path1), JS_ARG_STR(&path2));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_equivalent_path(storage, path1, path2)));
|
||||
}
|
||||
|
||||
static void js_storage_is_subpath_of(struct mjs* mjs) {
|
||||
const char *parent, *child;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&parent), JS_ARG_STR(&child));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_is_subdir(storage, parent, child)));
|
||||
}
|
||||
|
||||
// ---=== virtual mount api ===---
|
||||
|
||||
#define VIRTUAL_PROP_NAME "_v"
|
||||
|
||||
static void js_storage_virtual_init(struct mjs* mjs) {
|
||||
JsStorageInst* storage = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 1)) return;
|
||||
|
||||
const char* path;
|
||||
if(!get_path_arg(mjs, &path, 0)) return;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
|
||||
if(storage->virtual) {
|
||||
ret_int_err(mjs, "Virtual already setup");
|
||||
return;
|
||||
mjs_val_t this = mjs_get_this(mjs);
|
||||
mjs_val_t virtual = mjs_get(mjs, this, VIRTUAL_PROP_NAME, ~0);
|
||||
if(virtual != MJS_UNDEFINED) {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Virtual already setup");
|
||||
}
|
||||
|
||||
storage->virtual = storage_file_alloc(storage->api);
|
||||
if(!storage_file_open(storage->virtual, path, FSAM_READ | FSAM_WRITE, FSOM_OPEN_EXISTING)) {
|
||||
storage_file_free(storage->virtual);
|
||||
storage->virtual = NULL;
|
||||
ret_int_err(mjs, "Open file failed");
|
||||
return;
|
||||
File* file = storage_file_alloc(storage);
|
||||
if(!storage_file_open(file, path, FSAM_READ | FSAM_WRITE, FSOM_OPEN_EXISTING)) {
|
||||
storage_file_free(file);
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Open file failed");
|
||||
}
|
||||
|
||||
bool success = storage_virtual_init(storage->api, storage->virtual) == FSE_OK;
|
||||
bool success = storage_virtual_init(storage, file) == FSE_OK;
|
||||
if(!success) {
|
||||
if(storage_virtual_quit(storage->api) == FSE_OK) {
|
||||
success = storage_virtual_init(storage->api, storage->virtual) == FSE_OK;
|
||||
if(storage_virtual_quit(storage) == FSE_OK) {
|
||||
success = storage_virtual_init(storage, file) == FSE_OK;
|
||||
}
|
||||
}
|
||||
if(!success) {
|
||||
storage_file_free(storage->virtual);
|
||||
storage->virtual = NULL;
|
||||
ret_int_err(mjs, "Virtual init failed");
|
||||
return;
|
||||
storage_file_free(file);
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Virtual init failed");
|
||||
}
|
||||
|
||||
mjs_set(mjs, this, VIRTUAL_PROP_NAME, ~0, mjs_mk_foreign(mjs, file));
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_storage_virtual_mount(struct mjs* mjs) {
|
||||
JsStorageInst* storage = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 0)) return;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
|
||||
if(storage_virtual_mount(storage->api) != FSE_OK) {
|
||||
ret_int_err(mjs, "Virtual mount failed");
|
||||
return;
|
||||
if(storage_virtual_mount(storage) != FSE_OK) {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Virtual mount failed");
|
||||
}
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_storage_virtual_quit(struct mjs* mjs) {
|
||||
JsStorageInst* storage = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 0)) return;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
|
||||
if(storage_virtual_quit(storage->api) != FSE_OK) {
|
||||
ret_int_err(mjs, "Virtual quit failed");
|
||||
return;
|
||||
if(storage_virtual_quit(storage) != FSE_OK) {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Virtual quit failed");
|
||||
}
|
||||
|
||||
if(storage->virtual) {
|
||||
storage_file_free(storage->virtual);
|
||||
storage->virtual = NULL;
|
||||
mjs_val_t this = mjs_get_this(mjs);
|
||||
mjs_val_t virtual = mjs_get(mjs, this, VIRTUAL_PROP_NAME, ~0);
|
||||
if(virtual != MJS_UNDEFINED) {
|
||||
File* file = mjs_get_ptr(mjs, virtual);
|
||||
storage_file_free(file);
|
||||
mjs_del(mjs, this, VIRTUAL_PROP_NAME, ~0);
|
||||
}
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void* js_storage_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
JsStorageInst* storage = malloc(sizeof(JsStorageInst));
|
||||
mjs_val_t storage_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, storage_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, storage));
|
||||
mjs_set(mjs, storage_obj, "read", ~0, MJS_MK_FN(js_storage_read));
|
||||
mjs_set(mjs, storage_obj, "write", ~0, MJS_MK_FN(js_storage_write));
|
||||
mjs_set(mjs, storage_obj, "append", ~0, MJS_MK_FN(js_storage_append));
|
||||
mjs_set(mjs, storage_obj, "exists", ~0, MJS_MK_FN(js_storage_exists));
|
||||
mjs_set(mjs, storage_obj, "remove", ~0, MJS_MK_FN(js_storage_remove));
|
||||
mjs_set(mjs, storage_obj, "copy", ~0, MJS_MK_FN(js_storage_copy));
|
||||
mjs_set(mjs, storage_obj, "move", ~0, MJS_MK_FN(js_storage_move));
|
||||
mjs_set(mjs, storage_obj, "mkdir", ~0, MJS_MK_FN(js_storage_mkdir));
|
||||
mjs_set(mjs, storage_obj, "virtualInit", ~0, MJS_MK_FN(js_storage_virtual_init));
|
||||
mjs_set(mjs, storage_obj, "virtualMount", ~0, MJS_MK_FN(js_storage_virtual_mount));
|
||||
mjs_set(mjs, storage_obj, "virtualQuit", ~0, MJS_MK_FN(js_storage_virtual_quit));
|
||||
storage->api = furi_record_open(RECORD_STORAGE);
|
||||
*object = storage_obj;
|
||||
return storage;
|
||||
// mjs struct not passed in module destroy(), and not using Inst struct
|
||||
// to keep similar code to OFW so we use destructor
|
||||
static void js_storage_destructor(struct mjs* mjs, mjs_val_t obj) {
|
||||
Storage* storage = JS_GET_INST(mjs, obj);
|
||||
mjs_val_t virtual = mjs_get(mjs, obj, VIRTUAL_PROP_NAME, ~0);
|
||||
if(virtual != MJS_UNDEFINED) {
|
||||
File* file = mjs_get_ptr(mjs, virtual);
|
||||
storage_virtual_quit(storage);
|
||||
storage_file_free(file);
|
||||
mjs_del(mjs, obj, VIRTUAL_PROP_NAME, ~0);
|
||||
}
|
||||
}
|
||||
|
||||
static void js_storage_destroy(void* inst) {
|
||||
JsStorageInst* storage = inst;
|
||||
if(storage->virtual) {
|
||||
storage_virtual_quit(storage->api);
|
||||
storage_file_free(storage->virtual);
|
||||
// ---=== module ctor & dtor ===---
|
||||
|
||||
static void* js_storage_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
UNUSED(storage);
|
||||
*object = mjs_mk_object(mjs);
|
||||
JS_ASSIGN_MULTI(mjs, *object) {
|
||||
JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, storage));
|
||||
JS_FIELD(MJS_DESTRUCTOR_PROP_NAME, MJS_MK_FN(js_storage_destructor));
|
||||
|
||||
// top-level file ops
|
||||
JS_FIELD("openFile", MJS_MK_FN(js_storage_open_file));
|
||||
JS_FIELD("fileExists", MJS_MK_FN(js_storage_file_exists));
|
||||
|
||||
// dir ops
|
||||
JS_FIELD("readDirectory", MJS_MK_FN(js_storage_read_directory));
|
||||
JS_FIELD("directoryExists", MJS_MK_FN(js_storage_directory_exists));
|
||||
JS_FIELD("makeDirectory", MJS_MK_FN(js_storage_make_directory));
|
||||
|
||||
// common ops
|
||||
JS_FIELD("fileOrDirExists", MJS_MK_FN(js_storage_file_or_dir_exists));
|
||||
JS_FIELD("stat", MJS_MK_FN(js_storage_stat));
|
||||
JS_FIELD("remove", MJS_MK_FN(js_storage_remove));
|
||||
JS_FIELD("rmrf", MJS_MK_FN(js_storage_rmrf));
|
||||
JS_FIELD("rename", MJS_MK_FN(js_storage_rename));
|
||||
JS_FIELD("copy", MJS_MK_FN(js_storage_copy));
|
||||
JS_FIELD("fsInfo", MJS_MK_FN(js_storage_fs_info));
|
||||
JS_FIELD("nextAvailableFilename", MJS_MK_FN(js_storage_next_available_filename));
|
||||
|
||||
// path ops
|
||||
JS_FIELD("arePathsEqual", MJS_MK_FN(js_storage_are_paths_equal));
|
||||
JS_FIELD("isSubpathOf", MJS_MK_FN(js_storage_is_subpath_of));
|
||||
|
||||
// virtual mount api
|
||||
JS_FIELD("virtualInit", MJS_MK_FN(js_storage_virtual_init));
|
||||
JS_FIELD("virtualMount", MJS_MK_FN(js_storage_virtual_mount));
|
||||
JS_FIELD("virtualQuit", MJS_MK_FN(js_storage_virtual_quit));
|
||||
}
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
free(storage);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void js_storage_destroy(void* data) {
|
||||
UNUSED(data);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
}
|
||||
|
||||
// ---=== boilerplate ===---
|
||||
|
||||
static const JsModuleDescriptor js_storage_desc = {
|
||||
"storage",
|
||||
js_storage_create,
|
||||
js_storage_destroy,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
|
||||
@@ -484,7 +484,8 @@ static void js_subghz_end(struct mjs* mjs) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void* js_subghz_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
static void* js_subghz_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
JsSubghzInst* js_subghz = malloc(sizeof(JsSubghzInst));
|
||||
mjs_val_t subghz_obj = mjs_mk_object(mjs);
|
||||
|
||||
@@ -524,6 +525,7 @@ static const JsModuleDescriptor js_subghz_desc = {
|
||||
"subghz",
|
||||
js_subghz_create,
|
||||
js_subghz_destroy,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
#include <gui/modules/submenu.h>
|
||||
#include <gui/view_holder.h>
|
||||
#include <gui/view.h>
|
||||
#include <toolbox/api_lock.h>
|
||||
#include "../js_modules.h"
|
||||
|
||||
typedef struct {
|
||||
Submenu* submenu;
|
||||
ViewHolder* view_holder;
|
||||
FuriApiLock lock;
|
||||
uint32_t result;
|
||||
bool accepted;
|
||||
} JsSubmenuInst;
|
||||
|
||||
static JsSubmenuInst* get_this_ctx(struct mjs* mjs) {
|
||||
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
|
||||
JsSubmenuInst* submenu = mjs_get_ptr(mjs, obj_inst);
|
||||
furi_assert(submenu);
|
||||
return submenu;
|
||||
}
|
||||
|
||||
static void ret_bad_args(struct mjs* mjs, const char* error) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error);
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static bool check_arg_count(struct mjs* mjs, size_t count) {
|
||||
size_t num_args = mjs_nargs(mjs);
|
||||
if(num_args != count) {
|
||||
ret_bad_args(mjs, "Wrong argument count");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void submenu_callback(void* context, uint32_t id) {
|
||||
JsSubmenuInst* submenu = context;
|
||||
submenu->result = id;
|
||||
submenu->accepted = true;
|
||||
api_lock_unlock(submenu->lock);
|
||||
}
|
||||
|
||||
static void submenu_exit(void* context) {
|
||||
JsSubmenuInst* submenu = context;
|
||||
submenu->result = 0;
|
||||
submenu->accepted = false;
|
||||
api_lock_unlock(submenu->lock);
|
||||
}
|
||||
|
||||
static void js_submenu_add_item(struct mjs* mjs) {
|
||||
JsSubmenuInst* submenu = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 2)) return;
|
||||
|
||||
mjs_val_t label_arg = mjs_arg(mjs, 0);
|
||||
const char* label = mjs_get_string(mjs, &label_arg, NULL);
|
||||
if(!label) {
|
||||
ret_bad_args(mjs, "Label must be a string");
|
||||
return;
|
||||
}
|
||||
|
||||
mjs_val_t id_arg = mjs_arg(mjs, 1);
|
||||
if(!mjs_is_number(id_arg)) {
|
||||
ret_bad_args(mjs, "Id must be a number");
|
||||
return;
|
||||
}
|
||||
int32_t id = mjs_get_int32(mjs, id_arg);
|
||||
|
||||
submenu_add_item(submenu->submenu, label, id, submenu_callback, submenu);
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_submenu_set_header(struct mjs* mjs) {
|
||||
JsSubmenuInst* submenu = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 1)) return;
|
||||
|
||||
mjs_val_t header_arg = mjs_arg(mjs, 0);
|
||||
const char* header = mjs_get_string(mjs, &header_arg, NULL);
|
||||
if(!header) {
|
||||
ret_bad_args(mjs, "Header must be a string");
|
||||
return;
|
||||
}
|
||||
|
||||
submenu_set_header(submenu->submenu, header);
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_submenu_show(struct mjs* mjs) {
|
||||
JsSubmenuInst* submenu = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 0)) return;
|
||||
|
||||
submenu->lock = api_lock_alloc_locked();
|
||||
Gui* gui = furi_record_open(RECORD_GUI);
|
||||
submenu->view_holder = view_holder_alloc();
|
||||
view_holder_attach_to_gui(submenu->view_holder, gui);
|
||||
view_holder_set_back_callback(submenu->view_holder, submenu_exit, submenu);
|
||||
|
||||
view_holder_set_view(submenu->view_holder, submenu_get_view(submenu->submenu));
|
||||
api_lock_wait_unlock(submenu->lock);
|
||||
|
||||
view_holder_set_view(submenu->view_holder, NULL);
|
||||
view_holder_free(submenu->view_holder);
|
||||
furi_record_close(RECORD_GUI);
|
||||
api_lock_free(submenu->lock);
|
||||
|
||||
submenu_reset(submenu->submenu);
|
||||
if(submenu->accepted) {
|
||||
mjs_return(mjs, mjs_mk_number(mjs, submenu->result));
|
||||
} else {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
}
|
||||
|
||||
static void* js_submenu_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
JsSubmenuInst* submenu = malloc(sizeof(JsSubmenuInst));
|
||||
mjs_val_t submenu_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, submenu_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, submenu));
|
||||
mjs_set(mjs, submenu_obj, "addItem", ~0, MJS_MK_FN(js_submenu_add_item));
|
||||
mjs_set(mjs, submenu_obj, "setHeader", ~0, MJS_MK_FN(js_submenu_set_header));
|
||||
mjs_set(mjs, submenu_obj, "show", ~0, MJS_MK_FN(js_submenu_show));
|
||||
submenu->submenu = submenu_alloc();
|
||||
*object = submenu_obj;
|
||||
return submenu;
|
||||
}
|
||||
|
||||
static void js_submenu_destroy(void* inst) {
|
||||
JsSubmenuInst* submenu = inst;
|
||||
submenu_free(submenu->submenu);
|
||||
free(submenu);
|
||||
}
|
||||
|
||||
static const JsModuleDescriptor js_submenu_desc = {
|
||||
"submenu",
|
||||
js_submenu_create,
|
||||
js_submenu_destroy,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor submenu_plugin_descriptor = {
|
||||
.appid = PLUGIN_APP_ID,
|
||||
.ep_api_version = PLUGIN_API_VERSION,
|
||||
.entry_point = &js_submenu_desc,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* js_submenu_ep(void) {
|
||||
return &submenu_plugin_descriptor;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
#include "../js_modules.h" // IWYU pragma: keep
|
||||
#include <core/common_defines.h>
|
||||
#include <furi_hal_version.h>
|
||||
#include <power/power_service/power.h>
|
||||
|
||||
#define TAG "JsTests"
|
||||
|
||||
static void js_tests_fail(struct mjs* mjs) {
|
||||
furi_check(mjs_nargs(mjs) == 1);
|
||||
mjs_val_t message_arg = mjs_arg(mjs, 0);
|
||||
const char* message = mjs_get_string(mjs, &message_arg, NULL);
|
||||
furi_check(message);
|
||||
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "%s", message);
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_tests_assert_eq(struct mjs* mjs) {
|
||||
furi_check(mjs_nargs(mjs) == 2);
|
||||
|
||||
mjs_val_t expected_arg = mjs_arg(mjs, 0);
|
||||
mjs_val_t result_arg = mjs_arg(mjs, 1);
|
||||
|
||||
if(mjs_is_number(expected_arg) && mjs_is_number(result_arg)) {
|
||||
int32_t expected = mjs_get_int32(mjs, expected_arg);
|
||||
int32_t result = mjs_get_int32(mjs, result_arg);
|
||||
if(expected == result) {
|
||||
FURI_LOG_T(TAG, "eq passed (exp=%ld res=%ld)", expected, result);
|
||||
} else {
|
||||
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "expected %d, found %d", expected, result);
|
||||
}
|
||||
} else if(mjs_is_string(expected_arg) && mjs_is_string(result_arg)) {
|
||||
const char* expected = mjs_get_string(mjs, &expected_arg, NULL);
|
||||
const char* result = mjs_get_string(mjs, &result_arg, NULL);
|
||||
if(strcmp(expected, result) == 0) {
|
||||
FURI_LOG_T(TAG, "eq passed (exp=\"%s\" res=\"%s\")", expected, result);
|
||||
} else {
|
||||
mjs_prepend_errorf(
|
||||
mjs, MJS_INTERNAL_ERROR, "expected \"%s\", found \"%s\"", expected, result);
|
||||
}
|
||||
} else if(mjs_is_boolean(expected_arg) && mjs_is_boolean(result_arg)) {
|
||||
bool expected = mjs_get_bool(mjs, expected_arg);
|
||||
bool result = mjs_get_bool(mjs, result_arg);
|
||||
if(expected == result) {
|
||||
FURI_LOG_T(
|
||||
TAG,
|
||||
"eq passed (exp=%s res=%s)",
|
||||
expected ? "true" : "false",
|
||||
result ? "true" : "false");
|
||||
} else {
|
||||
mjs_prepend_errorf(
|
||||
mjs,
|
||||
MJS_INTERNAL_ERROR,
|
||||
"expected %s, found %s",
|
||||
expected ? "true" : "false",
|
||||
result ? "true" : "false");
|
||||
}
|
||||
} else {
|
||||
JS_ERROR_AND_RETURN(
|
||||
mjs,
|
||||
MJS_INTERNAL_ERROR,
|
||||
"type mismatch (expected %s, result %s)",
|
||||
mjs_typeof(expected_arg),
|
||||
mjs_typeof(result_arg));
|
||||
}
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_tests_assert_float_close(struct mjs* mjs) {
|
||||
furi_check(mjs_nargs(mjs) == 3);
|
||||
|
||||
mjs_val_t expected_arg = mjs_arg(mjs, 0);
|
||||
mjs_val_t result_arg = mjs_arg(mjs, 1);
|
||||
mjs_val_t epsilon_arg = mjs_arg(mjs, 2);
|
||||
furi_check(mjs_is_number(expected_arg));
|
||||
furi_check(mjs_is_number(result_arg));
|
||||
furi_check(mjs_is_number(epsilon_arg));
|
||||
double expected = mjs_get_double(mjs, expected_arg);
|
||||
double result = mjs_get_double(mjs, result_arg);
|
||||
double epsilon = mjs_get_double(mjs, epsilon_arg);
|
||||
|
||||
if(ABS(expected - result) > epsilon) {
|
||||
mjs_prepend_errorf(
|
||||
mjs,
|
||||
MJS_INTERNAL_ERROR,
|
||||
"expected %f found %f (tolerance=%f)",
|
||||
expected,
|
||||
result,
|
||||
epsilon);
|
||||
}
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
void* js_tests_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
mjs_val_t tests_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, tests_obj, "fail", ~0, MJS_MK_FN(js_tests_fail));
|
||||
mjs_set(mjs, tests_obj, "assert_eq", ~0, MJS_MK_FN(js_tests_assert_eq));
|
||||
mjs_set(mjs, tests_obj, "assert_float_close", ~0, MJS_MK_FN(js_tests_assert_float_close));
|
||||
*object = tests_obj;
|
||||
|
||||
return (void*)1;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
#include "../js_thread_i.h"
|
||||
#include "../js_modules.h"
|
||||
|
||||
void* js_tests_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules);
|
||||
@@ -1,219 +0,0 @@
|
||||
#include <gui/modules/text_box.h>
|
||||
#include <gui/view_holder.h>
|
||||
#include "../js_modules.h"
|
||||
|
||||
typedef struct {
|
||||
TextBox* text_box;
|
||||
ViewHolder* view_holder;
|
||||
FuriString* text;
|
||||
bool is_shown;
|
||||
} JsTextboxInst;
|
||||
|
||||
static JsTextboxInst* get_this_ctx(struct mjs* mjs) {
|
||||
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
|
||||
JsTextboxInst* textbox = mjs_get_ptr(mjs, obj_inst);
|
||||
furi_assert(textbox);
|
||||
return textbox;
|
||||
}
|
||||
|
||||
static void ret_bad_args(struct mjs* mjs, const char* error) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error);
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static bool check_arg_count(struct mjs* mjs, size_t count) {
|
||||
size_t num_args = mjs_nargs(mjs);
|
||||
if(num_args != count) {
|
||||
ret_bad_args(mjs, "Wrong argument count");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void js_textbox_set_config(struct mjs* mjs) {
|
||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 2)) return;
|
||||
|
||||
TextBoxFocus set_focus = TextBoxFocusStart;
|
||||
mjs_val_t focus_arg = mjs_arg(mjs, 0);
|
||||
const char* focus = mjs_get_string(mjs, &focus_arg, NULL);
|
||||
if(!focus) {
|
||||
ret_bad_args(mjs, "Focus must be a string");
|
||||
return;
|
||||
} else {
|
||||
if(!strncmp(focus, "start", strlen("start"))) {
|
||||
set_focus = TextBoxFocusStart;
|
||||
} else if(!strncmp(focus, "end", strlen("end"))) {
|
||||
set_focus = TextBoxFocusEnd;
|
||||
} else {
|
||||
ret_bad_args(mjs, "Bad focus value");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
TextBoxFont set_font = TextBoxFontText;
|
||||
mjs_val_t font_arg = mjs_arg(mjs, 1);
|
||||
const char* font = mjs_get_string(mjs, &font_arg, NULL);
|
||||
if(!font) {
|
||||
ret_bad_args(mjs, "Font must be a string");
|
||||
return;
|
||||
} else {
|
||||
if(!strncmp(font, "text", strlen("text"))) {
|
||||
set_font = TextBoxFontText;
|
||||
} else if(!strncmp(font, "hex", strlen("hex"))) {
|
||||
set_font = TextBoxFontHex;
|
||||
} else {
|
||||
ret_bad_args(mjs, "Bad font value");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
text_box_set_focus(textbox->text_box, set_focus);
|
||||
text_box_set_font(textbox->text_box, set_font);
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_textbox_add_text(struct mjs* mjs) {
|
||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 1)) return;
|
||||
|
||||
mjs_val_t text_arg = mjs_arg(mjs, 0);
|
||||
size_t text_len = 0;
|
||||
const char* text = mjs_get_string(mjs, &text_arg, &text_len);
|
||||
if(!text) {
|
||||
ret_bad_args(mjs, "Text must be a string");
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid condition race between GUI and JS thread
|
||||
text_box_set_text(textbox->text_box, "");
|
||||
|
||||
size_t new_len = furi_string_size(textbox->text) + text_len;
|
||||
if(new_len >= 4096) {
|
||||
furi_string_right(textbox->text, new_len / 2);
|
||||
}
|
||||
|
||||
furi_string_cat(textbox->text, text);
|
||||
|
||||
text_box_set_text(textbox->text_box, furi_string_get_cstr(textbox->text));
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_textbox_clear_text(struct mjs* mjs) {
|
||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 0)) return;
|
||||
|
||||
// Avoid condition race between GUI and JS thread
|
||||
text_box_set_text(textbox->text_box, "");
|
||||
|
||||
furi_string_reset(textbox->text);
|
||||
|
||||
text_box_set_text(textbox->text_box, furi_string_get_cstr(textbox->text));
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_textbox_is_open(struct mjs* mjs) {
|
||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 0)) return;
|
||||
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, textbox->is_shown));
|
||||
}
|
||||
|
||||
static void textbox_callback(void* context, uint32_t arg) {
|
||||
UNUSED(arg);
|
||||
JsTextboxInst* textbox = context;
|
||||
view_holder_set_view(textbox->view_holder, NULL);
|
||||
textbox->is_shown = false;
|
||||
}
|
||||
|
||||
static void textbox_exit(void* context) {
|
||||
JsTextboxInst* textbox = context;
|
||||
// Using timer to schedule view_holder stop, will not work under high CPU load
|
||||
furi_timer_pending_callback(textbox_callback, textbox, 0);
|
||||
}
|
||||
|
||||
static void js_textbox_show(struct mjs* mjs) {
|
||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 0)) return;
|
||||
|
||||
if(textbox->is_shown) {
|
||||
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Textbox is already shown");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
view_holder_set_view(textbox->view_holder, text_box_get_view(textbox->text_box));
|
||||
textbox->is_shown = true;
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_textbox_close(struct mjs* mjs) {
|
||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 0)) return;
|
||||
|
||||
view_holder_set_view(textbox->view_holder, NULL);
|
||||
textbox->is_shown = false;
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void* js_textbox_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
JsTextboxInst* textbox = malloc(sizeof(JsTextboxInst));
|
||||
|
||||
mjs_val_t textbox_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, textbox_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, textbox));
|
||||
mjs_set(mjs, textbox_obj, "setConfig", ~0, MJS_MK_FN(js_textbox_set_config));
|
||||
mjs_set(mjs, textbox_obj, "addText", ~0, MJS_MK_FN(js_textbox_add_text));
|
||||
mjs_set(mjs, textbox_obj, "clearText", ~0, MJS_MK_FN(js_textbox_clear_text));
|
||||
mjs_set(mjs, textbox_obj, "isOpen", ~0, MJS_MK_FN(js_textbox_is_open));
|
||||
mjs_set(mjs, textbox_obj, "show", ~0, MJS_MK_FN(js_textbox_show));
|
||||
mjs_set(mjs, textbox_obj, "close", ~0, MJS_MK_FN(js_textbox_close));
|
||||
|
||||
textbox->text = furi_string_alloc();
|
||||
textbox->text_box = text_box_alloc();
|
||||
|
||||
Gui* gui = furi_record_open(RECORD_GUI);
|
||||
textbox->view_holder = view_holder_alloc();
|
||||
view_holder_attach_to_gui(textbox->view_holder, gui);
|
||||
view_holder_set_back_callback(textbox->view_holder, textbox_exit, textbox);
|
||||
|
||||
*object = textbox_obj;
|
||||
return textbox;
|
||||
}
|
||||
|
||||
static void js_textbox_destroy(void* inst) {
|
||||
JsTextboxInst* textbox = inst;
|
||||
|
||||
view_holder_set_view(textbox->view_holder, NULL);
|
||||
view_holder_free(textbox->view_holder);
|
||||
textbox->view_holder = NULL;
|
||||
|
||||
furi_record_close(RECORD_GUI);
|
||||
|
||||
text_box_reset(textbox->text_box);
|
||||
furi_string_reset(textbox->text);
|
||||
|
||||
text_box_free(textbox->text_box);
|
||||
furi_string_free(textbox->text);
|
||||
free(textbox);
|
||||
}
|
||||
|
||||
static const JsModuleDescriptor js_textbox_desc = {
|
||||
"textbox",
|
||||
js_textbox_create,
|
||||
js_textbox_destroy,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor textbox_plugin_descriptor = {
|
||||
.appid = PLUGIN_APP_ID,
|
||||
.ep_api_version = PLUGIN_API_VERSION,
|
||||
.entry_point = &js_textbox_desc,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* js_textbox_ep(void) {
|
||||
return &textbox_plugin_descriptor;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user