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 commit 0eaad8bf64.
* 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 commit 0f831412fa.

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:
WillyJL
2024-10-19 01:55:03 +01:00
committed by GitHub
209 changed files with 7591 additions and 2764 deletions
+12 -2
View File
@@ -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
View File
@@ -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)));
@@ -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
+18
View File
@@ -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 |
# +------------------------------------------------------------------------------------------------------------------------+
+4 -5
View File
@@ -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
View File
@@ -1,5 +1,6 @@
#pragma once
#include <cli/cli.h>
#include <cli/cli_ansi.h>
void subghz_on_system_start(void);
+10 -10
View File
@@ -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
View File
@@ -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(
+1 -15
View File
@@ -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"
+76
View File
@@ -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),
};
}
+94
View File
@@ -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
+212 -29
View File
@@ -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);
+5 -4
View File
@@ -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;
+2 -4
View File
@@ -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,
+8 -9
View File
@@ -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);
+15 -7
View File
@@ -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;
+7 -6
View File
@@ -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;
+35 -32
View File
@@ -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) {
+2 -2
View File
@@ -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 -1
View File
@@ -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;
+110 -57
View File
@@ -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 :)")
+1 -1
View File
@@ -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);
+81 -33
View File
@@ -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;
}
+258 -6
View File
@@ -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);
+14 -100
View File
@@ -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);
+8
View File
@@ -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);
+280 -330
View File
@@ -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;
}
+3 -1
View File
@@ -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 = {
+362 -237
View File
@@ -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), &timestamp) ==
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, &timestamp)) != 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