diff --git a/.github/workflow_data/devbuild.py b/.github/workflow_data/devbuild.py index 79cdb2612..4a082d958 100644 --- a/.github/workflow_data/devbuild.py +++ b/.github/workflow_data/devbuild.py @@ -4,8 +4,9 @@ import requests import json import os -artifact_tgz = f"{os.environ['INDEXER_URL']}/firmware/dev/{os.environ['ARTIFACT_TAG']}.tgz" -artifact_sdk = f"{os.environ['INDEXER_URL']}/firmware/dev/{os.environ['ARTIFACT_TAG'].replace('update', 'sdk')}.zip" +base_url = f"{os.environ['INDEXER_URL']}/builds/firmware/dev" +artifact_tgz = f"{base_url}/{os.environ['ARTIFACT_TAG']}.tgz" +artifact_sdk = f"{base_url}/{os.environ['ARTIFACT_TAG'].replace('update', 'sdk')}.zip" artifact_lab = f"https://lab.flipper.net/?url={artifact_tgz}&channel=dev-cfw&version={os.environ['VERSION_TAG']}" @@ -41,28 +42,34 @@ if __name__ == "__main__": "fields": [ { "name": "Code Diff:", - "value": "\n".join([ - f"[From last release ({release} to {after[:8]})]({compare}/{release}...{after})", - f"[From last build ({before[:8]} to {after[:8]})]({compare}/{before}...{after})", - ]) + "value": "\n".join( + [ + f"[From last release ({release} to {after[:8]})]({compare}/{release}...{after})", + f"[From last build ({before[:8]} to {after[:8]})]({compare}/{before}...{after})", + ] + ), }, { "name": "Changelog:", - "value": "\n".join([ - f"[Since last release ({release})]({event['repository']['html_url']}/blob/{after}/CHANGELOG.md)", - ]) + "value": "\n".join( + [ + f"[Since last release ({release})]({event['repository']['html_url']}/blob/{after}/CHANGELOG.md)", + ] + ), }, { "name": "Firmware Artifacts:", - "value": "\n".join([ - f"- [🖥️ Install with Web Updater](https://momentum-fw.dev/update)", - f"- [☁️ Open in Flipper Lab/App]({artifact_lab})", - f"- [🐬 Download Firmware TGZ]({artifact_tgz})", - f"- [🛠️ SDK (for development)]({artifact_sdk})", - ]) - } + "value": "\n".join( + [ + f"- [🖥️ Install with Web Updater](https://momentum-fw.dev/update)", + f"- [☁️ Open in Flipper Lab/App]({artifact_lab})", + f"- [🐬 Download Firmware TGZ]({artifact_tgz})", + f"- [🛠️ SDK (for development)]({artifact_sdk})", + ] + ), + }, ], - "timestamp": dt.datetime.utcnow().isoformat() + "timestamp": dt.datetime.utcnow().isoformat(), } ], }, diff --git a/.github/workflow_data/release.md b/.github/workflow_data/release.md index a050ce9b5..441896d20 100644 --- a/.github/workflow_data/release.md +++ b/.github/workflow_data/release.md @@ -1,7 +1,7 @@ ## ⬇️ Download >### [🖥️ Web Updater (chrome)](https://momentum-fw.dev/update) [recommended] ->### [☁️ Flipper Lab/App (chrome/mobile)](https://lab.flipper.net/?url=https://up.momentum-fw.dev/firmware/{VERSION_TAG}/flipper-z-f7-update-{VERSION_TAG}.tgz&channel=release-cfw&version={VERSION_TAG}) +>### [☁️ Flipper Lab/App (chrome/mobile)](https://lab.flipper.net/?url=https://up.momentum-fw.dev/builds/firmware/{VERSION_TAG}/flipper-z-f7-update-{VERSION_TAG}.tgz&channel=release-cfw&version={VERSION_TAG}) >### [🐬 qFlipper Package (.tgz)](https://github.com/Next-Flip/Momentum-Firmware/releases/download/{VERSION_TAG}/flipper-z-f7-update-{VERSION_TAG}.tgz) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 52eac4c91..d221e9524 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -119,6 +119,20 @@ jobs: tar czpf "artifacts/flipper-z-any-scripts-${SUFFIX}.tgz" scripts cp build/core2_firmware.tgz "artifacts/flipper-z-any-core2_firmware-${SUFFIX}.tgz" + - name: "Calculate DFU sizes" + env: + INDEXER_URL: ${{ secrets.INDEXER_URL }} + if: ${{ env.INDEXER_URL != '' && github.event.pull_request && matrix.target == env.DEFAULT_TARGET }} + run: | + curl -L "${{ secrets.INDEXER_URL }}"/firmware/development/f7/full_dfu -o dev.dfu + dfu_size_new=$(du --apparent-size -B 1 artifacts/flipper-z-${TARGET}-full-*.dfu | cut -f1) + dfu_size_dev=$(du --apparent-size -B 1 dev.dfu | cut -f1) + dfu_size_diff=$((dfu_size_new - dfu_size_dev)) + DFU_SIZE=$(echo ${dfu_size_new}B | sed -r 's/^([0-9]+)([0-9]{2})([0-9])B/\1.\2K/') + DFU_DIFF=$(echo ${dfu_size_diff}B | sed -r 's/^(-?[0-9]+)([0-9]{2})([0-9])B/\1.\2K/' | sed -r 's/^([^-])/+\1/') + echo "DFU_SIZE=$DFU_SIZE" >> $GITHUB_ENV + echo "DFU_DIFF=$DFU_DIFF" >> $GITHUB_ENV + - name: "Upload artifacts to update server" env: INDEXER_URL: ${{ secrets.INDEXER_URL }} @@ -153,8 +167,9 @@ jobs: issue-number: ${{ github.event.pull_request.number }} body: | **Compiled ${{ matrix.target }} firmware for commit `${{steps.names.outputs.commit_sha}}`:** - - [☁️ Flipper Lab/App](https://lab.flipper.net/?url=${{secrets.INDEXER_URL}}/firmware/${{steps.names.outputs.branch_name}}/flipper-z-${{steps.names.outputs.default_target}}-update-${{steps.names.outputs.suffix}}.tgz&channel=mntm-${{steps.names.outputs.branch_name}}&version=${{steps.names.outputs.commit_sha}}) - - [📦 qFlipper Package](${{secrets.INDEXER_URL}}/firmware/${{steps.names.outputs.branch_name}}/flipper-z-${{steps.names.outputs.default_target}}-update-${{steps.names.outputs.suffix}}.tgz) + - [☁️ Flipper Lab/App](https://lab.flipper.net/?url=${{secrets.INDEXER_URL}}/builds/firmware/${{steps.names.outputs.branch_name}}/flipper-z-${{steps.names.outputs.default_target}}-update-${{steps.names.outputs.suffix}}.tgz&channel=mntm-${{steps.names.outputs.branch_name}}&version=${{steps.names.outputs.commit_sha}}) + - [📦 qFlipper Package](${{secrets.INDEXER_URL}}/builds/firmware/${{steps.names.outputs.branch_name}}/flipper-z-${{steps.names.outputs.default_target}}-update-${{steps.names.outputs.suffix}}.tgz) + - DFU Size: `${{ env.DFU_SIZE }}` (`${{ env.DFU_DIFF }}` from dev) edit-mode: replace - name: Send devbuild webhook diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bab70cf3e..03c3b0236 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,9 +25,9 @@ jobs: - name: "Download release assets from tag build" run: | set -e - wget "${{ secrets.INDEXER_URL }}"/firmware/${{ github.event.release.tag_name }}/flipper-z-f7-update-${{ github.event.release.tag_name }}.tgz - wget "${{ secrets.INDEXER_URL }}"/firmware/${{ github.event.release.tag_name }}/flipper-z-f7-update-${{ github.event.release.tag_name }}.zip - wget "${{ secrets.INDEXER_URL }}"/firmware/${{ github.event.release.tag_name }}/flipper-z-f7-sdk-${{ github.event.release.tag_name }}.zip + wget "${{ secrets.INDEXER_URL }}"/builds/firmware/${{ github.event.release.tag_name }}/flipper-z-f7-update-${{ github.event.release.tag_name }}.tgz + wget "${{ secrets.INDEXER_URL }}"/builds/firmware/${{ github.event.release.tag_name }}/flipper-z-f7-update-${{ github.event.release.tag_name }}.zip + wget "${{ secrets.INDEXER_URL }}"/builds/firmware/${{ github.event.release.tag_name }}/flipper-z-f7-sdk-${{ github.event.release.tag_name }}.zip - name: "Update release with assets and notes" uses: "softprops/action-gh-release@v1" diff --git a/.gitignore b/.gitignore index fbda5ad8e..f2884f2b7 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,6 @@ PVS-Studio.log .gdbinit /fbt_options_local.py + +# JS packages +node_modules/ diff --git a/CHANGELOG.md b/CHANGELOG.md index b9cba5555..6507b0bce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,7 +46,7 @@ - `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 + - Documentation is incomplete and deprecated, from now on you should refer to type definitions (`applications/system/js_app/packages/fz-sdk`), those will always be correct - Type definitions for extra modules we have that OFW doesn't will come later - GUI: Refactored TextInput illegal symbols (by @Willy-JL) - If your app used `text_input_add_illegal_symbols(text_input)` it should change to `text_input_show_illegal_symbols(text_input, true)` @@ -64,7 +64,7 @@ - UL: Add GangQi protocol (static 34 bit) with button parsing and add manually (by @xMasterX & @Skorpionm) - UL: Add Hollarm protocol (static 42 bit) with button parsing and add manually (by @xMasterX & @Skorpionm) - UL: Add Hay21 protocol (dynamic 21 bit) with button parsing (by @xMasterX) - - UL: Add Keeloq Monarch full support, with add manually (by @ashphx) + - UL: Add Keeloq Monarch full support, with add manually (by @ashphx & @xMasterX) - UL: Princeton custom buttons support (by @xMasterX) - NFC: - OFW: MIFARE Classic Key Recovery Improvements (PR 3822 by @noproto) @@ -80,9 +80,20 @@ - Option to "Load from Library File" for Universal Remotes (#255 by @zxkmm) - Updater: New Yappy themed icon while updating (#253 by @the1anonlypr3 & @Kuronons & @nescap) - JS: + - OFW: JS: Modules backport & overhaul (by @portasynthinca3) + - See above for list of breaking changes, here are listed strictly new functionalities + - New `event_loop` module for event-driven interactivity + - Interrupt and callback support for `gpio` module + - New `gui` module that allows much more developed interfaces, also new `gui/loading` and `gui/empty_screen` views + - Directory operations and many more file operations for `storage` module + - OFW: Full-fledged JS SDK + npm packages (by @portasynthinca3) + - CFWs can have their own JS SDKs too! Check ours out at [`@next-flip/fz-sdk-mntm`](https://www.npmjs.com/package/@next-flip/fz-sdk-mntm) - New `i2c` module (#259 by @jamisonderek) - New `spi` module (#272 by @jamisonderek) + - Added `illegalSymbols` prop for `gui/text_input` view (#290 by @Willy-JL) + - Added typedocs for all extra JS modules in Momentum (by @Willy-JL) - RPC: Added ASCII event support (#284 by @Willy-JL) +- OFW: Settings: Clock editing & Alarm function (目覚め時計) (by @skotopes) - BadKB: - OFW: Add linux/gnome badusb demo files (by @thomasnemer) - Add older qFlipper install demos for windows and macos (by @DXVVAY & @grugnoymeme) @@ -95,7 +106,10 @@ - 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) +- Furi: + - OFW: Add FuriEventLoop support for FuriEventFlag, simplify API (by @Skorpionm) + - OFW: Put errno into TCB, better integration with libc (by @portasynthinca3) + - OFW: FuriHalRtc Alarm support (目覚め時計) (by @skotopes) ### Updated: - Apps: @@ -111,13 +125,17 @@ - Seader: Enable T=1, show error for timeout, fix wrong LRC logging, fix crash scanning NTAG215 with MFC option (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) + - Flappy Bird: Yappy Bird mode, highscore system (by @jaylikesbunda & @the1anonlypr3) - CLI-GUI Bridge: Add more symbols to keyboard (#222 by @Willy-JL) + - NRF24 Batch: Add Aeropac SN board txt file (by @vad7) - 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) - UL: W5500 Ethernet: Various fixes and improvements (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) +- CLI: + - Print plugin name on load fail (by @Willy-JL) + - Move more commands as plugins on SD, refactor plugin wrapper (#276 by @Willy-JL) - NFC: - NDEF Parser: - Mifare Classic support (#265 by @luu176), protocol-agnostic rewrite and more improvements (#265 by @Willy-JL) @@ -132,6 +150,7 @@ - OFW: Moscow social card parser (by @assasinfil) - OFW: Fixes and improvements to iso14443_4a listener and poller (by @RebornedBrain) - OFW: Update BART station codes in Clipper plugin (by @ted-logan) + - OFW: Add Caltrain zones for Clipper parser (by @tomholford) - Sub-GHz: - UL: Frequency analyzer fixes and improvements (by @xMasterX): - Enforce int module (like in OFW) usage due to lack of required hardware on external boards (PathIsolate (+rf switch for multiple paths)) and incorrect usage and/or understanding the purpose of frequency analyzer app by users, it should be used only to get frequency of the remote placed around 1-10cm around flipper's left corner @@ -162,6 +181,7 @@ - OFW: Update and cleanup (by @rnadyrshin) - OFW: Improve bit_buffer.h docs (by @Astrrra) - OFW: Wi-Fi Devboard documentation rework (by @rnadyrshin) + - OFW: Update unit tests docs (by @portasynthinca3) ### Fixed: - RFID: @@ -173,6 +193,7 @@ - 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) + - UL: Code cleanup and fix for rare dupicated "Data" field cases (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) @@ -181,10 +202,10 @@ - RPC: Fixed apps not updating and staying at 100% (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) + - OFW: Fixed bug with reading pwd locked MFULs (by @mishamyte) - OFW: RPC: Broken file interaction fixes (by @RebornedBrain) - OFW: GPIO: Fix USB-UART bridge exit screen stopping the bridge prematurely (by @portasynthinca3) - OFW: GUI: Fix dialog_ex NULL ptr crash (by @Willy-JL) diff --git a/applications/debug/event_loop_blink_test/event_loop_blink_test.c b/applications/debug/event_loop_blink_test/event_loop_blink_test.c index 7f00e63f2..1cddfa323 100644 --- a/applications/debug/event_loop_blink_test/event_loop_blink_test.c +++ b/applications/debug/event_loop_blink_test/event_loop_blink_test.c @@ -82,7 +82,7 @@ static void view_port_input_callback(InputEvent* input_event, void* context) { furi_message_queue_put(app->input_queue, input_event, 0); } -static bool input_queue_callback(FuriEventLoopObject* object, void* context) { +static void input_queue_callback(FuriEventLoopObject* object, void* context) { FuriMessageQueue* queue = object; EventLoopBlinkTestApp* app = context; @@ -107,8 +107,6 @@ static bool input_queue_callback(FuriEventLoopObject* object, void* context) { furi_event_loop_stop(app->event_loop); } } - - return true; } static void blink_timer_callback(void* context) { diff --git a/applications/debug/unit_tests/resources/unit_tests/js/basic.js b/applications/debug/unit_tests/resources/unit_tests/js/basic.js index 0927595a2..b1e7dcc84 100644 --- a/applications/debug/unit_tests/resources/unit_tests/js/basic.js +++ b/applications/debug/unit_tests/resources/unit_tests/js/basic.js @@ -1,4 +1,15 @@ let tests = require("tests"); +let flipper = require("flipper"); tests.assert_eq(1337, 1337); tests.assert_eq("hello", "hello"); + +tests.assert_eq("compatible", sdkCompatibilityStatus(0, 1)); +tests.assert_eq("firmwareTooOld", sdkCompatibilityStatus(100500, 0)); +tests.assert_eq("firmwareTooNew", sdkCompatibilityStatus(-100500, 0)); +tests.assert_eq(true, doesSdkSupport(["baseline"])); +tests.assert_eq(false, doesSdkSupport(["abobus", "other-nonexistent-feature"])); + +tests.assert_eq("momentum", flipper.firmwareVendor); +tests.assert_eq(0, flipper.jsSdkVersion[0]); +tests.assert_eq(1, flipper.jsSdkVersion[1]); diff --git a/applications/debug/unit_tests/tests/furi/furi_event_loop.c b/applications/debug/unit_tests/tests/furi/furi_event_loop.c deleted file mode 100644 index 291181c77..000000000 --- a/applications/debug/unit_tests/tests/furi/furi_event_loop.c +++ /dev/null @@ -1,205 +0,0 @@ -#include "../test.h" -#include -#include - -#include -#include - -#define TAG "TestFuriEventLoop" - -#define EVENT_LOOP_EVENT_COUNT (256u) - -typedef struct { - FuriMessageQueue* mq; - - FuriEventLoop* producer_event_loop; - uint32_t producer_counter; - - FuriEventLoop* consumer_event_loop; - uint32_t consumer_counter; -} TestFuriData; - -bool test_furi_event_loop_producer_mq_callback(FuriEventLoopObject* object, void* context) { - furi_check(context); - - TestFuriData* data = context; - furi_check(data->mq == object, "Invalid queue"); - - FURI_LOG_I( - TAG, "producer_mq_callback: %lu %lu", data->producer_counter, data->consumer_counter); - - if(data->producer_counter == EVENT_LOOP_EVENT_COUNT / 2) { - furi_event_loop_unsubscribe(data->producer_event_loop, data->mq); - furi_event_loop_subscribe_message_queue( - data->producer_event_loop, - data->mq, - FuriEventLoopEventOut, - test_furi_event_loop_producer_mq_callback, - data); - } - - if(data->producer_counter == EVENT_LOOP_EVENT_COUNT) { - furi_event_loop_stop(data->producer_event_loop); - return false; - } - - data->producer_counter++; - furi_check( - furi_message_queue_put(data->mq, &data->producer_counter, 0) == FuriStatusOk, - "furi_message_queue_put failed"); - furi_delay_us(furi_hal_random_get() % 1000); - - return true; -} - -int32_t test_furi_event_loop_producer(void* p) { - furi_check(p); - - TestFuriData* data = p; - - FURI_LOG_I(TAG, "producer start 1st run"); - - data->producer_event_loop = furi_event_loop_alloc(); - furi_event_loop_subscribe_message_queue( - data->producer_event_loop, - data->mq, - FuriEventLoopEventOut, - test_furi_event_loop_producer_mq_callback, - data); - - furi_event_loop_run(data->producer_event_loop); - - // 2 EventLoop index, 0xFFFFFFFF - all possible flags, emulate uncleared flags - xTaskNotifyIndexed(xTaskGetCurrentTaskHandle(), 2, 0xFFFFFFFF, eSetBits); - - furi_event_loop_unsubscribe(data->producer_event_loop, data->mq); - furi_event_loop_free(data->producer_event_loop); - - FURI_LOG_I(TAG, "producer start 2nd run"); - - data->producer_counter = 0; - data->producer_event_loop = furi_event_loop_alloc(); - - furi_event_loop_subscribe_message_queue( - data->producer_event_loop, - data->mq, - FuriEventLoopEventOut, - test_furi_event_loop_producer_mq_callback, - data); - - furi_event_loop_run(data->producer_event_loop); - - furi_event_loop_unsubscribe(data->producer_event_loop, data->mq); - furi_event_loop_free(data->producer_event_loop); - - FURI_LOG_I(TAG, "producer end"); - - return 0; -} - -bool test_furi_event_loop_consumer_mq_callback(FuriEventLoopObject* object, void* context) { - furi_check(context); - - TestFuriData* data = context; - furi_check(data->mq == object); - - furi_delay_us(furi_hal_random_get() % 1000); - furi_check(furi_message_queue_get(data->mq, &data->consumer_counter, 0) == FuriStatusOk); - - FURI_LOG_I( - TAG, "consumer_mq_callback: %lu %lu", data->producer_counter, data->consumer_counter); - - if(data->consumer_counter == EVENT_LOOP_EVENT_COUNT / 2) { - furi_event_loop_unsubscribe(data->consumer_event_loop, data->mq); - furi_event_loop_subscribe_message_queue( - data->consumer_event_loop, - data->mq, - FuriEventLoopEventIn, - test_furi_event_loop_consumer_mq_callback, - data); - } - - if(data->consumer_counter == EVENT_LOOP_EVENT_COUNT) { - furi_event_loop_stop(data->consumer_event_loop); - return false; - } - - return true; -} - -int32_t test_furi_event_loop_consumer(void* p) { - furi_check(p); - - TestFuriData* data = p; - - FURI_LOG_I(TAG, "consumer start 1st run"); - - data->consumer_event_loop = furi_event_loop_alloc(); - furi_event_loop_subscribe_message_queue( - data->consumer_event_loop, - data->mq, - FuriEventLoopEventIn, - test_furi_event_loop_consumer_mq_callback, - data); - - furi_event_loop_run(data->consumer_event_loop); - - // 2 EventLoop index, 0xFFFFFFFF - all possible flags, emulate uncleared flags - xTaskNotifyIndexed(xTaskGetCurrentTaskHandle(), 2, 0xFFFFFFFF, eSetBits); - - furi_event_loop_unsubscribe(data->consumer_event_loop, data->mq); - furi_event_loop_free(data->consumer_event_loop); - - FURI_LOG_I(TAG, "consumer start 2nd run"); - - data->consumer_counter = 0; - data->consumer_event_loop = furi_event_loop_alloc(); - furi_event_loop_subscribe_message_queue( - data->consumer_event_loop, - data->mq, - FuriEventLoopEventIn, - test_furi_event_loop_consumer_mq_callback, - data); - - furi_event_loop_run(data->consumer_event_loop); - - furi_event_loop_unsubscribe(data->consumer_event_loop, data->mq); - furi_event_loop_free(data->consumer_event_loop); - - FURI_LOG_I(TAG, "consumer end"); - - return 0; -} - -void test_furi_event_loop(void) { - TestFuriData data = {}; - - data.mq = furi_message_queue_alloc(16, sizeof(uint32_t)); - - FuriThread* producer_thread = furi_thread_alloc(); - furi_thread_set_name(producer_thread, "producer_thread"); - furi_thread_set_stack_size(producer_thread, 1 * 1024); - furi_thread_set_callback(producer_thread, test_furi_event_loop_producer); - furi_thread_set_context(producer_thread, &data); - furi_thread_start(producer_thread); - - FuriThread* consumer_thread = furi_thread_alloc(); - furi_thread_set_name(consumer_thread, "consumer_thread"); - furi_thread_set_stack_size(consumer_thread, 1 * 1024); - furi_thread_set_callback(consumer_thread, test_furi_event_loop_consumer); - furi_thread_set_context(consumer_thread, &data); - furi_thread_start(consumer_thread); - - // Wait for thread to complete their tasks - furi_thread_join(producer_thread); - furi_thread_join(consumer_thread); - - // The test itself - mu_assert_int_eq(data.producer_counter, data.consumer_counter); - mu_assert_int_eq(data.producer_counter, EVENT_LOOP_EVENT_COUNT); - - // Release memory - furi_thread_free(consumer_thread); - furi_thread_free(producer_thread); - furi_message_queue_free(data.mq); -} diff --git a/applications/debug/unit_tests/tests/furi/furi_event_loop_test.c b/applications/debug/unit_tests/tests/furi/furi_event_loop_test.c new file mode 100644 index 000000000..73f38ab77 --- /dev/null +++ b/applications/debug/unit_tests/tests/furi/furi_event_loop_test.c @@ -0,0 +1,490 @@ +#include "../test.h" +#include +#include + +#include +#include + +#define TAG "TestFuriEventLoop" + +#define MESSAGE_COUNT (256UL) +#define EVENT_FLAG_COUNT (23UL) +#define PRIMITIVE_COUNT (4UL) +#define RUN_COUNT (2UL) + +typedef struct { + FuriEventLoop* event_loop; + uint32_t message_queue_count; + uint32_t stream_buffer_count; + uint32_t event_flag_count; + uint32_t semaphore_count; + uint32_t primitives_tested; +} TestFuriEventLoopThread; + +typedef struct { + FuriMessageQueue* message_queue; + FuriStreamBuffer* stream_buffer; + FuriEventFlag* event_flag; + FuriSemaphore* semaphore; + + TestFuriEventLoopThread producer; + TestFuriEventLoopThread consumer; +} TestFuriEventLoopData; + +static void test_furi_event_loop_pending_callback(void* context) { + furi_check(context); + + TestFuriEventLoopThread* test_thread = context; + furi_check(test_thread->primitives_tested < PRIMITIVE_COUNT); + + test_thread->primitives_tested++; + FURI_LOG_I(TAG, "primitives tested: %lu", test_thread->primitives_tested); + + if(test_thread->primitives_tested == PRIMITIVE_COUNT) { + furi_event_loop_stop(test_thread->event_loop); + } +} + +static void test_furi_event_loop_thread_init(TestFuriEventLoopThread* test_thread) { + memset(test_thread, 0, sizeof(TestFuriEventLoopThread)); + test_thread->event_loop = furi_event_loop_alloc(); +} + +static void test_furi_event_loop_thread_run_and_cleanup(TestFuriEventLoopThread* test_thread) { + furi_event_loop_run(test_thread->event_loop); + // 2 EventLoop index, 0xFFFFFFFF - all possible flags, emulate uncleared flags + xTaskNotifyIndexed(xTaskGetCurrentTaskHandle(), 2, 0xFFFFFFFF, eSetBits); + furi_event_loop_free(test_thread->event_loop); +} + +static void test_furi_event_loop_producer_message_queue_callback( + FuriEventLoopObject* object, + void* context) { + furi_check(context); + + TestFuriEventLoopData* data = context; + furi_check(data->message_queue == object); + + FURI_LOG_I( + TAG, + "producer MessageQueue: %lu %lu", + data->producer.message_queue_count, + data->consumer.message_queue_count); + + if(data->producer.message_queue_count == MESSAGE_COUNT / 2) { + furi_event_loop_unsubscribe(data->producer.event_loop, data->message_queue); + furi_event_loop_subscribe_message_queue( + data->producer.event_loop, + data->message_queue, + FuriEventLoopEventOut, + test_furi_event_loop_producer_message_queue_callback, + data); + + } else if(data->producer.message_queue_count == MESSAGE_COUNT) { + furi_event_loop_unsubscribe(data->producer.event_loop, data->message_queue); + furi_event_loop_pend_callback( + data->producer.event_loop, test_furi_event_loop_pending_callback, &data->producer); + return; + } + + data->producer.message_queue_count++; + + furi_check( + furi_message_queue_put(data->message_queue, &data->producer.message_queue_count, 0) == + FuriStatusOk); + + furi_delay_us(furi_hal_random_get() % 100); +} + +static void test_furi_event_loop_producer_stream_buffer_callback( + FuriEventLoopObject* object, + void* context) { + furi_check(context); + + TestFuriEventLoopData* data = context; + furi_check(data->stream_buffer == object); + + TestFuriEventLoopThread* producer = &data->producer; + TestFuriEventLoopThread* consumer = &data->consumer; + + FURI_LOG_I( + TAG, + "producer StreamBuffer: %lu %lu", + producer->stream_buffer_count, + consumer->stream_buffer_count); + + if(producer->stream_buffer_count == MESSAGE_COUNT / 2) { + furi_event_loop_unsubscribe(producer->event_loop, data->stream_buffer); + furi_event_loop_subscribe_stream_buffer( + producer->event_loop, + data->stream_buffer, + FuriEventLoopEventOut, + test_furi_event_loop_producer_stream_buffer_callback, + data); + + } else if(producer->stream_buffer_count == MESSAGE_COUNT) { + furi_event_loop_unsubscribe(producer->event_loop, data->stream_buffer); + furi_event_loop_pend_callback( + producer->event_loop, test_furi_event_loop_pending_callback, producer); + return; + } + + producer->stream_buffer_count++; + + furi_check( + furi_stream_buffer_send( + data->stream_buffer, &producer->stream_buffer_count, sizeof(uint32_t), 0) == + sizeof(uint32_t)); + + furi_delay_us(furi_hal_random_get() % 100); +} + +static void + test_furi_event_loop_producer_event_flag_callback(FuriEventLoopObject* object, void* context) { + furi_check(context); + + TestFuriEventLoopData* data = context; + furi_check(data->event_flag == object); + + const uint32_t producer_flags = (1UL << data->producer.event_flag_count); + const uint32_t consumer_flags = (1UL << data->consumer.event_flag_count); + + FURI_LOG_I(TAG, "producer EventFlag: 0x%06lX 0x%06lX", producer_flags, consumer_flags); + + furi_check(furi_event_flag_set(data->event_flag, producer_flags) & producer_flags); + + if(data->producer.event_flag_count == EVENT_FLAG_COUNT / 2) { + furi_event_loop_unsubscribe(data->producer.event_loop, data->event_flag); + furi_event_loop_subscribe_event_flag( + data->producer.event_loop, + data->event_flag, + FuriEventLoopEventOut, + test_furi_event_loop_producer_event_flag_callback, + data); + + } else if(data->producer.event_flag_count == EVENT_FLAG_COUNT) { + furi_event_loop_unsubscribe(data->producer.event_loop, data->event_flag); + furi_event_loop_pend_callback( + data->producer.event_loop, test_furi_event_loop_pending_callback, &data->producer); + return; + } + + data->producer.event_flag_count++; + + furi_delay_us(furi_hal_random_get() % 100); +} + +static void + test_furi_event_loop_producer_semaphore_callback(FuriEventLoopObject* object, void* context) { + furi_check(context); + + TestFuriEventLoopData* data = context; + furi_check(data->semaphore == object); + + TestFuriEventLoopThread* producer = &data->producer; + TestFuriEventLoopThread* consumer = &data->consumer; + + FURI_LOG_I( + TAG, "producer Semaphore: %lu %lu", producer->semaphore_count, consumer->semaphore_count); + furi_check(furi_semaphore_release(data->semaphore) == FuriStatusOk); + + if(producer->semaphore_count == MESSAGE_COUNT / 2) { + furi_event_loop_unsubscribe(producer->event_loop, data->semaphore); + furi_event_loop_subscribe_semaphore( + producer->event_loop, + data->semaphore, + FuriEventLoopEventOut, + test_furi_event_loop_producer_semaphore_callback, + data); + + } else if(producer->semaphore_count == MESSAGE_COUNT) { + furi_event_loop_unsubscribe(producer->event_loop, data->semaphore); + furi_event_loop_pend_callback( + producer->event_loop, test_furi_event_loop_pending_callback, producer); + return; + } + + data->producer.semaphore_count++; + + furi_delay_us(furi_hal_random_get() % 100); +} + +static int32_t test_furi_event_loop_producer(void* p) { + furi_check(p); + + TestFuriEventLoopData* data = p; + TestFuriEventLoopThread* producer = &data->producer; + + for(uint32_t i = 0; i < RUN_COUNT; ++i) { + FURI_LOG_I(TAG, "producer start run %lu", i); + + test_furi_event_loop_thread_init(producer); + + furi_event_loop_subscribe_message_queue( + producer->event_loop, + data->message_queue, + FuriEventLoopEventOut, + test_furi_event_loop_producer_message_queue_callback, + data); + furi_event_loop_subscribe_stream_buffer( + producer->event_loop, + data->stream_buffer, + FuriEventLoopEventOut, + test_furi_event_loop_producer_stream_buffer_callback, + data); + furi_event_loop_subscribe_event_flag( + producer->event_loop, + data->event_flag, + FuriEventLoopEventOut, + test_furi_event_loop_producer_event_flag_callback, + data); + furi_event_loop_subscribe_semaphore( + producer->event_loop, + data->semaphore, + FuriEventLoopEventOut, + test_furi_event_loop_producer_semaphore_callback, + data); + + test_furi_event_loop_thread_run_and_cleanup(producer); + } + + FURI_LOG_I(TAG, "producer end"); + + return 0; +} + +static void test_furi_event_loop_consumer_message_queue_callback( + FuriEventLoopObject* object, + void* context) { + furi_check(context); + + TestFuriEventLoopData* data = context; + furi_check(data->message_queue == object); + + furi_delay_us(furi_hal_random_get() % 100); + + furi_check( + furi_message_queue_get(data->message_queue, &data->consumer.message_queue_count, 0) == + FuriStatusOk); + + FURI_LOG_I( + TAG, + "consumer MessageQueue: %lu %lu", + data->producer.message_queue_count, + data->consumer.message_queue_count); + + if(data->consumer.message_queue_count == MESSAGE_COUNT / 2) { + furi_event_loop_unsubscribe(data->consumer.event_loop, data->message_queue); + furi_event_loop_subscribe_message_queue( + data->consumer.event_loop, + data->message_queue, + FuriEventLoopEventIn, + test_furi_event_loop_consumer_message_queue_callback, + data); + + } else if(data->consumer.message_queue_count == MESSAGE_COUNT) { + furi_event_loop_unsubscribe(data->consumer.event_loop, data->message_queue); + furi_event_loop_pend_callback( + data->consumer.event_loop, test_furi_event_loop_pending_callback, &data->consumer); + } +} + +static void test_furi_event_loop_consumer_stream_buffer_callback( + FuriEventLoopObject* object, + void* context) { + furi_check(context); + + TestFuriEventLoopData* data = context; + furi_check(data->stream_buffer == object); + + TestFuriEventLoopThread* producer = &data->producer; + TestFuriEventLoopThread* consumer = &data->consumer; + + furi_delay_us(furi_hal_random_get() % 100); + + furi_check( + furi_stream_buffer_receive( + data->stream_buffer, &consumer->stream_buffer_count, sizeof(uint32_t), 0) == + sizeof(uint32_t)); + + FURI_LOG_I( + TAG, + "consumer StreamBuffer: %lu %lu", + producer->stream_buffer_count, + consumer->stream_buffer_count); + + if(consumer->stream_buffer_count == MESSAGE_COUNT / 2) { + furi_event_loop_unsubscribe(consumer->event_loop, data->stream_buffer); + furi_event_loop_subscribe_stream_buffer( + consumer->event_loop, + data->stream_buffer, + FuriEventLoopEventIn, + test_furi_event_loop_consumer_stream_buffer_callback, + data); + + } else if(consumer->stream_buffer_count == MESSAGE_COUNT) { + furi_event_loop_unsubscribe(data->consumer.event_loop, data->stream_buffer); + furi_event_loop_pend_callback( + consumer->event_loop, test_furi_event_loop_pending_callback, consumer); + } +} + +static void + test_furi_event_loop_consumer_event_flag_callback(FuriEventLoopObject* object, void* context) { + furi_check(context); + + TestFuriEventLoopData* data = context; + furi_check(data->event_flag == object); + + furi_delay_us(furi_hal_random_get() % 100); + + const uint32_t producer_flags = (1UL << data->producer.event_flag_count); + const uint32_t consumer_flags = (1UL << data->consumer.event_flag_count); + + furi_check( + furi_event_flag_wait(data->event_flag, consumer_flags, FuriFlagWaitAny, 0) & + consumer_flags); + + FURI_LOG_I(TAG, "consumer EventFlag: 0x%06lX 0x%06lX", producer_flags, consumer_flags); + + if(data->consumer.event_flag_count == EVENT_FLAG_COUNT / 2) { + furi_event_loop_unsubscribe(data->consumer.event_loop, data->event_flag); + furi_event_loop_subscribe_event_flag( + data->consumer.event_loop, + data->event_flag, + FuriEventLoopEventIn, + test_furi_event_loop_consumer_event_flag_callback, + data); + + } else if(data->consumer.event_flag_count == EVENT_FLAG_COUNT) { + furi_event_loop_unsubscribe(data->consumer.event_loop, data->event_flag); + furi_event_loop_pend_callback( + data->consumer.event_loop, test_furi_event_loop_pending_callback, &data->consumer); + return; + } + + data->consumer.event_flag_count++; +} + +static void + test_furi_event_loop_consumer_semaphore_callback(FuriEventLoopObject* object, void* context) { + furi_check(context); + + TestFuriEventLoopData* data = context; + furi_check(data->semaphore == object); + + furi_delay_us(furi_hal_random_get() % 100); + + TestFuriEventLoopThread* producer = &data->producer; + TestFuriEventLoopThread* consumer = &data->consumer; + + furi_check(furi_semaphore_acquire(data->semaphore, 0) == FuriStatusOk); + + FURI_LOG_I( + TAG, "consumer Semaphore: %lu %lu", producer->semaphore_count, consumer->semaphore_count); + + if(consumer->semaphore_count == MESSAGE_COUNT / 2) { + furi_event_loop_unsubscribe(consumer->event_loop, data->semaphore); + furi_event_loop_subscribe_semaphore( + consumer->event_loop, + data->semaphore, + FuriEventLoopEventIn, + test_furi_event_loop_consumer_semaphore_callback, + data); + + } else if(consumer->semaphore_count == MESSAGE_COUNT) { + furi_event_loop_unsubscribe(consumer->event_loop, data->semaphore); + furi_event_loop_pend_callback( + consumer->event_loop, test_furi_event_loop_pending_callback, consumer); + return; + } + + data->consumer.semaphore_count++; +} + +static int32_t test_furi_event_loop_consumer(void* p) { + furi_check(p); + + TestFuriEventLoopData* data = p; + TestFuriEventLoopThread* consumer = &data->consumer; + + for(uint32_t i = 0; i < RUN_COUNT; ++i) { + FURI_LOG_I(TAG, "consumer start run %lu", i); + + test_furi_event_loop_thread_init(consumer); + + furi_event_loop_subscribe_message_queue( + consumer->event_loop, + data->message_queue, + FuriEventLoopEventIn, + test_furi_event_loop_consumer_message_queue_callback, + data); + furi_event_loop_subscribe_stream_buffer( + consumer->event_loop, + data->stream_buffer, + FuriEventLoopEventIn, + test_furi_event_loop_consumer_stream_buffer_callback, + data); + furi_event_loop_subscribe_event_flag( + consumer->event_loop, + data->event_flag, + FuriEventLoopEventIn, + test_furi_event_loop_consumer_event_flag_callback, + data); + furi_event_loop_subscribe_semaphore( + consumer->event_loop, + data->semaphore, + FuriEventLoopEventIn, + test_furi_event_loop_consumer_semaphore_callback, + data); + + test_furi_event_loop_thread_run_and_cleanup(consumer); + } + + FURI_LOG_I(TAG, "consumer end"); + + return 0; +} + +void test_furi_event_loop(void) { + TestFuriEventLoopData data = {}; + + data.message_queue = furi_message_queue_alloc(16, sizeof(uint32_t)); + data.stream_buffer = furi_stream_buffer_alloc(16, sizeof(uint32_t)); + data.event_flag = furi_event_flag_alloc(); + data.semaphore = furi_semaphore_alloc(8, 0); + + FuriThread* producer_thread = + furi_thread_alloc_ex("producer_thread", 1 * 1024, test_furi_event_loop_producer, &data); + furi_thread_start(producer_thread); + + FuriThread* consumer_thread = + furi_thread_alloc_ex("consumer_thread", 1 * 1024, test_furi_event_loop_consumer, &data); + furi_thread_start(consumer_thread); + + // Wait for thread to complete their tasks + furi_thread_join(producer_thread); + furi_thread_join(consumer_thread); + + TestFuriEventLoopThread* producer = &data.producer; + TestFuriEventLoopThread* consumer = &data.consumer; + + // The test itself + mu_assert_int_eq(producer->message_queue_count, consumer->message_queue_count); + mu_assert_int_eq(producer->message_queue_count, MESSAGE_COUNT); + mu_assert_int_eq(producer->stream_buffer_count, consumer->stream_buffer_count); + mu_assert_int_eq(producer->stream_buffer_count, MESSAGE_COUNT); + mu_assert_int_eq(producer->event_flag_count, consumer->event_flag_count); + mu_assert_int_eq(producer->event_flag_count, EVENT_FLAG_COUNT); + mu_assert_int_eq(producer->semaphore_count, consumer->semaphore_count); + mu_assert_int_eq(producer->semaphore_count, MESSAGE_COUNT); + + // Release memory + furi_thread_free(consumer_thread); + furi_thread_free(producer_thread); + + furi_message_queue_free(data.message_queue); + furi_stream_buffer_free(data.stream_buffer); + furi_event_flag_free(data.event_flag); + furi_semaphore_free(data.semaphore); +} diff --git a/applications/debug/unit_tests/tests/furi/furi_primitives_test.c b/applications/debug/unit_tests/tests/furi/furi_primitives_test.c new file mode 100644 index 000000000..d9ad03039 --- /dev/null +++ b/applications/debug/unit_tests/tests/furi/furi_primitives_test.c @@ -0,0 +1,103 @@ +#include +#include "../test.h" // IWYU pragma: keep + +#define MESSAGE_QUEUE_CAPACITY (16U) +#define MESSAGE_QUEUE_ELEMENT_SIZE (sizeof(uint32_t)) + +#define STREAM_BUFFER_SIZE (32U) +#define STREAM_BUFFER_TRG_LEVEL (STREAM_BUFFER_SIZE / 2U) + +typedef struct { + FuriMessageQueue* message_queue; + FuriStreamBuffer* stream_buffer; +} TestFuriPrimitivesData; + +static void test_furi_message_queue(TestFuriPrimitivesData* data) { + FuriMessageQueue* message_queue = data->message_queue; + + mu_assert_int_eq(0, furi_message_queue_get_count(message_queue)); + mu_assert_int_eq(MESSAGE_QUEUE_CAPACITY, furi_message_queue_get_space(message_queue)); + mu_assert_int_eq(MESSAGE_QUEUE_CAPACITY, furi_message_queue_get_capacity(message_queue)); + mu_assert_int_eq( + MESSAGE_QUEUE_ELEMENT_SIZE, furi_message_queue_get_message_size(message_queue)); + + for(uint32_t i = 0;; ++i) { + mu_assert_int_eq(MESSAGE_QUEUE_CAPACITY - i, furi_message_queue_get_space(message_queue)); + mu_assert_int_eq(i, furi_message_queue_get_count(message_queue)); + + if(furi_message_queue_put(message_queue, &i, 0) != FuriStatusOk) { + break; + } + } + + mu_assert_int_eq(0, furi_message_queue_get_space(message_queue)); + mu_assert_int_eq(MESSAGE_QUEUE_CAPACITY, furi_message_queue_get_count(message_queue)); + + for(uint32_t i = 0;; ++i) { + mu_assert_int_eq(i, furi_message_queue_get_space(message_queue)); + mu_assert_int_eq(MESSAGE_QUEUE_CAPACITY - i, furi_message_queue_get_count(message_queue)); + + uint32_t value; + if(furi_message_queue_get(message_queue, &value, 0) != FuriStatusOk) { + break; + } + + mu_assert_int_eq(i, value); + } + + mu_assert_int_eq(0, furi_message_queue_get_count(message_queue)); + mu_assert_int_eq(MESSAGE_QUEUE_CAPACITY, furi_message_queue_get_space(message_queue)); +} + +static void test_furi_stream_buffer(TestFuriPrimitivesData* data) { + FuriStreamBuffer* stream_buffer = data->stream_buffer; + + mu_assert(furi_stream_buffer_is_empty(stream_buffer), "Must be empty"); + mu_assert(!furi_stream_buffer_is_full(stream_buffer), "Must be not full"); + mu_assert_int_eq(0, furi_stream_buffer_bytes_available(stream_buffer)); + mu_assert_int_eq(STREAM_BUFFER_SIZE, furi_stream_buffer_spaces_available(stream_buffer)); + + for(uint8_t i = 0;; ++i) { + mu_assert_int_eq(i, furi_stream_buffer_bytes_available(stream_buffer)); + mu_assert_int_eq( + STREAM_BUFFER_SIZE - i, furi_stream_buffer_spaces_available(stream_buffer)); + + if(furi_stream_buffer_send(stream_buffer, &i, sizeof(uint8_t), 0) != sizeof(uint8_t)) { + break; + } + } + + mu_assert(!furi_stream_buffer_is_empty(stream_buffer), "Must be not empty"); + mu_assert(furi_stream_buffer_is_full(stream_buffer), "Must be full"); + mu_assert_int_eq(STREAM_BUFFER_SIZE, furi_stream_buffer_bytes_available(stream_buffer)); + mu_assert_int_eq(0, furi_stream_buffer_spaces_available(stream_buffer)); + + for(uint8_t i = 0;; ++i) { + mu_assert_int_eq( + STREAM_BUFFER_SIZE - i, furi_stream_buffer_bytes_available(stream_buffer)); + mu_assert_int_eq(i, furi_stream_buffer_spaces_available(stream_buffer)); + + uint8_t value; + if(furi_stream_buffer_receive(stream_buffer, &value, sizeof(uint8_t), 0) != + sizeof(uint8_t)) { + break; + } + + mu_assert_int_eq(i, value); + } +} + +// This is a stub that needs expanding +void test_furi_primitives(void) { + TestFuriPrimitivesData data = { + .message_queue = + furi_message_queue_alloc(MESSAGE_QUEUE_CAPACITY, MESSAGE_QUEUE_ELEMENT_SIZE), + .stream_buffer = furi_stream_buffer_alloc(STREAM_BUFFER_SIZE, STREAM_BUFFER_TRG_LEVEL), + }; + + test_furi_message_queue(&data); + test_furi_stream_buffer(&data); + + furi_message_queue_free(data.message_queue); + furi_stream_buffer_free(data.stream_buffer); +} diff --git a/applications/debug/unit_tests/tests/furi/furi_test.c b/applications/debug/unit_tests/tests/furi/furi_test.c index 2a76d5184..193a8124d 100644 --- a/applications/debug/unit_tests/tests/furi/furi_test.c +++ b/applications/debug/unit_tests/tests/furi/furi_test.c @@ -9,6 +9,7 @@ void test_furi_pubsub(void); void test_furi_memmgr(void); void test_furi_event_loop(void); void test_errno_saving(void); +void test_furi_primitives(void); static int foo = 0; @@ -47,6 +48,10 @@ MU_TEST(mu_test_errno_saving) { test_errno_saving(); } +MU_TEST(mu_test_furi_primitives) { + test_furi_primitives(); +} + MU_TEST_SUITE(test_suite) { MU_SUITE_CONFIGURE(&test_setup, &test_teardown); MU_RUN_TEST(test_check); @@ -57,6 +62,7 @@ MU_TEST_SUITE(test_suite) { MU_RUN_TEST(mu_test_furi_memmgr); MU_RUN_TEST(mu_test_furi_event_loop); MU_RUN_TEST(mu_test_errno_saving); + MU_RUN_TEST(mu_test_furi_primitives); } int run_minunit_test_furi(void) { diff --git a/applications/examples/example_event_loop/application.fam b/applications/examples/example_event_loop/application.fam index a37ffb1a0..15a7c8837 100644 --- a/applications/examples/example_event_loop/application.fam +++ b/applications/examples/example_event_loop/application.fam @@ -1,3 +1,12 @@ +App( + appid="example_event_loop_event_flags", + name="Example: Event Loop Event Flags", + apptype=FlipperAppType.EXTERNAL, + sources=["example_event_loop_event_flags.c"], + entry_point="example_event_loop_event_flags_app", + fap_category="Examples", +) + App( appid="example_event_loop_timer", name="Example: Event Loop Timer", diff --git a/applications/examples/example_event_loop/example_event_loop_event_flags.c b/applications/examples/example_event_loop/example_event_loop_event_flags.c new file mode 100644 index 000000000..5d0acf7f1 --- /dev/null +++ b/applications/examples/example_event_loop/example_event_loop_event_flags.c @@ -0,0 +1,173 @@ +/** + * @file example_event_loop_event_flags.c + * @brief Example application demonstrating the use of the FuriEventFlag primitive in FuriEventLoop instances. + * + * This application receives keystrokes from the input service and sets the appropriate flags, + * which are subsequently processed in the event loop + */ + +#include +#include +#include + +#include + +#define TAG "ExampleEventLoopEventFlags" + +typedef struct { + Gui* gui; + ViewPort* view_port; + FuriEventLoop* event_loop; + FuriEventFlag* event_flag; +} EventLoopEventFlagsApp; + +typedef enum { + EventLoopEventFlagsOk = (1 << 0), + EventLoopEventFlagsUp = (1 << 1), + EventLoopEventFlagsDown = (1 << 2), + EventLoopEventFlagsLeft = (1 << 3), + EventLoopEventFlagsRight = (1 << 4), + EventLoopEventFlagsBack = (1 << 5), + EventLoopEventFlagsExit = (1 << 6), +} EventLoopEventFlags; + +#define EVENT_LOOP_EVENT_FLAGS_MASK \ + (EventLoopEventFlagsOk | EventLoopEventFlagsUp | EventLoopEventFlagsDown | \ + EventLoopEventFlagsLeft | EventLoopEventFlagsRight | EventLoopEventFlagsBack | \ + EventLoopEventFlagsExit) + +// This function is executed in the GUI context each time an input event occurs (e.g. the user pressed a key) +static void event_loop_event_flags_app_input_callback(InputEvent* event, void* context) { + furi_assert(context); + EventLoopEventFlagsApp* app = context; + UNUSED(app); + + if(event->type == InputTypePress) { + if(event->key == InputKeyOk) { + furi_event_flag_set(app->event_flag, EventLoopEventFlagsOk); + } else if(event->key == InputKeyUp) { + furi_event_flag_set(app->event_flag, EventLoopEventFlagsUp); + } else if(event->key == InputKeyDown) { + furi_event_flag_set(app->event_flag, EventLoopEventFlagsDown); + } else if(event->key == InputKeyLeft) { + furi_event_flag_set(app->event_flag, EventLoopEventFlagsLeft); + } else if(event->key == InputKeyRight) { + furi_event_flag_set(app->event_flag, EventLoopEventFlagsRight); + } else if(event->key == InputKeyBack) { + furi_event_flag_set(app->event_flag, EventLoopEventFlagsBack); + } + } else if(event->type == InputTypeLong) { + if(event->key == InputKeyBack) { + furi_event_flag_set(app->event_flag, EventLoopEventFlagsExit); + } + } +} + +// This function is executed each time a new event flag is inserted in the input event flag. +static void + event_loop_event_flags_app_event_flags_callback(FuriEventLoopObject* object, void* context) { + furi_assert(context); + EventLoopEventFlagsApp* app = context; + + furi_assert(object == app->event_flag); + + EventLoopEventFlags events = + furi_event_flag_wait(app->event_flag, EVENT_LOOP_EVENT_FLAGS_MASK, FuriFlagWaitAny, 0); + furi_check((events) != 0); + + if(events & EventLoopEventFlagsOk) { + FURI_LOG_I(TAG, "Press \"Ok\""); + } + if(events & EventLoopEventFlagsUp) { + FURI_LOG_I(TAG, "Press \"Up\""); + } + if(events & EventLoopEventFlagsDown) { + FURI_LOG_I(TAG, "Press \"Down\""); + } + if(events & EventLoopEventFlagsLeft) { + FURI_LOG_I(TAG, "Press \"Left\""); + } + if(events & EventLoopEventFlagsRight) { + FURI_LOG_I(TAG, "Press \"Right\""); + } + if(events & EventLoopEventFlagsBack) { + FURI_LOG_I(TAG, "Press \"Back\""); + } + if(events & EventLoopEventFlagsExit) { + FURI_LOG_I(TAG, "Exit App"); + furi_event_loop_stop(app->event_loop); + } +} + +static EventLoopEventFlagsApp* event_loop_event_flags_app_alloc(void) { + EventLoopEventFlagsApp* app = malloc(sizeof(EventLoopEventFlagsApp)); + + // Create event loop instances. + app->event_loop = furi_event_loop_alloc(); + // Create event flag instances. + app->event_flag = furi_event_flag_alloc(); + + // Create GUI instance. + app->gui = furi_record_open(RECORD_GUI); + app->view_port = view_port_alloc(); + // Gain exclusive access to the input events + view_port_input_callback_set(app->view_port, event_loop_event_flags_app_input_callback, app); + gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen); + + // Notify the event loop about incoming messages in the event flag + furi_event_loop_subscribe_event_flag( + app->event_loop, + app->event_flag, + FuriEventLoopEventIn | FuriEventLoopEventFlagEdge, + event_loop_event_flags_app_event_flags_callback, + app); + + return app; +} + +static void event_loop_event_flags_app_free(EventLoopEventFlagsApp* app) { + gui_remove_view_port(app->gui, app->view_port); + + furi_record_close(RECORD_GUI); + app->gui = NULL; + + // Delete all instances + view_port_free(app->view_port); + app->view_port = NULL; + + // IMPORTANT: The user code MUST unsubscribe from all events before deleting the event loop. + // Failure to do so will result in a crash. + furi_event_loop_unsubscribe(app->event_loop, app->event_flag); + + furi_event_flag_free(app->event_flag); + app->event_flag = NULL; + + furi_event_loop_free(app->event_loop); + app->event_loop = NULL; + + free(app); +} + +static void event_loop_event_flags_app_run(EventLoopEventFlagsApp* app) { + FURI_LOG_I(TAG, "Press keys to see them printed here."); + FURI_LOG_I(TAG, "Quickly press different keys to generate events."); + FURI_LOG_I(TAG, "Long press \"Back\" to exit app."); + + // Run the application event loop. This call will block until the application is about to exit. + furi_event_loop_run(app->event_loop); +} + +/******************************************************************* + * vvv START HERE vvv + * + * The application's entry point - referenced in application.fam + *******************************************************************/ +int32_t example_event_loop_event_flags_app(void* arg) { + UNUSED(arg); + + EventLoopEventFlagsApp* app = event_loop_event_flags_app_alloc(); + event_loop_event_flags_app_run(app); + event_loop_event_flags_app_free(app); + + return 0; +} diff --git a/applications/examples/example_event_loop/example_event_loop_multi.c b/applications/examples/example_event_loop/example_event_loop_multi.c index ebfb00911..ae748da55 100644 --- a/applications/examples/example_event_loop/example_event_loop_multi.c +++ b/applications/examples/example_event_loop/example_event_loop_multi.c @@ -52,7 +52,7 @@ typedef struct { */ // This function is executed each time the data is taken out of the stream buffer. It is used to restart the worker timer. -static bool +static void event_loop_multi_app_stream_buffer_worker_callback(FuriEventLoopObject* object, void* context) { furi_assert(context); EventLoopMultiAppWorker* worker = context; @@ -62,8 +62,6 @@ static bool FURI_LOG_I(TAG, "Data was removed from buffer"); // Restart the timer to generate another block of random data. furi_event_loop_timer_start(worker->timer, WORKER_DATA_INTERVAL_MS); - - return true; } // This function is executed when the worker timer expires. The timer will NOT restart automatically @@ -152,7 +150,7 @@ static void event_loop_multi_app_input_callback(InputEvent* event, void* context } // This function is executed each time new data is available in the stream buffer. -static bool +static void event_loop_multi_app_stream_buffer_callback(FuriEventLoopObject* object, void* context) { furi_assert(context); EventLoopMultiApp* app = context; @@ -172,12 +170,10 @@ static bool FURI_LOG_I(TAG, "Received data: %s", furi_string_get_cstr(tmp_str)); furi_string_free(tmp_str); - - return true; } // This function is executed each time a new message is inserted in the input queue. -static bool event_loop_multi_app_input_queue_callback(FuriEventLoopObject* object, void* context) { +static void event_loop_multi_app_input_queue_callback(FuriEventLoopObject* object, void* context) { furi_assert(context); EventLoopMultiApp* app = context; @@ -222,8 +218,6 @@ static bool event_loop_multi_app_input_queue_callback(FuriEventLoopObject* objec // Not a long press, just print the key's name. FURI_LOG_I(TAG, "Short press: %s", input_get_key_name(event.key)); } - - return true; } // This function is executed each time the countdown timer expires. diff --git a/applications/examples/example_event_loop/example_event_loop_mutex.c b/applications/examples/example_event_loop/example_event_loop_mutex.c index d043f3f89..20bf7af4b 100644 --- a/applications/examples/example_event_loop/example_event_loop_mutex.c +++ b/applications/examples/example_event_loop/example_event_loop_mutex.c @@ -59,7 +59,7 @@ static int32_t event_loop_mutex_app_worker_thread(void* context) { } // This function is being run each time when the mutex gets released -static bool event_loop_mutex_app_event_callback(FuriEventLoopObject* object, void* context) { +static void event_loop_mutex_app_event_callback(FuriEventLoopObject* object, void* context) { furi_assert(context); EventLoopMutexApp* app = context; @@ -82,8 +82,6 @@ static bool event_loop_mutex_app_event_callback(FuriEventLoopObject* object, voi MUTEX_EVENT_AND_FLAGS, event_loop_mutex_app_event_callback, app); - - return true; } static EventLoopMutexApp* event_loop_mutex_app_alloc(void) { diff --git a/applications/examples/example_event_loop/example_event_loop_stream_buffer.c b/applications/examples/example_event_loop/example_event_loop_stream_buffer.c index 65dbd83cf..6f7280973 100644 --- a/applications/examples/example_event_loop/example_event_loop_stream_buffer.c +++ b/applications/examples/example_event_loop/example_event_loop_stream_buffer.c @@ -54,7 +54,7 @@ static int32_t event_loop_stream_buffer_app_worker_thread(void* context) { } // This function is being run each time when the number of bytes in the buffer is above its trigger level. -static bool +static void event_loop_stream_buffer_app_event_callback(FuriEventLoopObject* object, void* context) { furi_assert(context); EventLoopStreamBufferApp* app = context; @@ -76,8 +76,6 @@ static bool FURI_LOG_I(TAG, "Received data: %s", furi_string_get_cstr(tmp_str)); furi_string_free(tmp_str); - - return true; } static EventLoopStreamBufferApp* event_loop_stream_buffer_app_alloc(void) { diff --git a/applications/external b/applications/external index d0300c7a5..68bb084ec 160000 --- a/applications/external +++ b/applications/external @@ -1 +1 @@ -Subproject commit d0300c7a5a353ba0c73030c81afc75ec440797e6 +Subproject commit 68bb084ec9476b011465b78f27fdacd560262e2c diff --git a/applications/main/ibutton/application.fam b/applications/main/ibutton/application.fam index d33519580..8fe147968 100644 --- a/applications/main/ibutton/application.fam +++ b/applications/main/ibutton/application.fam @@ -25,6 +25,6 @@ App( apptype=FlipperAppType.STARTUP, targets=["f7"], entry_point="ibutton_on_system_start", - sources=["ibutton_start.c"], + sources=["ibutton_cli.c"], order=60, ) diff --git a/applications/main/ibutton/ibutton_cli.c b/applications/main/ibutton/ibutton_cli.c index dcac8f963..8ca832500 100644 --- a/applications/main/ibutton/ibutton_cli.c +++ b/applications/main/ibutton/ibutton_cli.c @@ -8,6 +8,22 @@ #include #include +static void ibutton_cli(Cli* cli, FuriString* args, void* context); + +#include +CLI_PLUGIN_WRAPPER("ibutton", ibutton_cli) + +// app cli function +void ibutton_on_system_start(void) { +#ifdef SRV_CLI + Cli* cli = furi_record_open(RECORD_CLI); + cli_add_command(cli, "ikey", CliCommandFlagDefault, ibutton_cli_wrapper, cli); + furi_record_close(RECORD_CLI); +#else + UNUSED(ibutton_cli); +#endif +} + static void ibutton_cli_print_usage(void) { printf("Usage:\r\n"); printf("ikey read\r\n"); @@ -239,16 +255,3 @@ void ibutton_cli(Cli* cli, FuriString* args, void* context) { furi_string_free(cmd); } - -#include -#include - -static const FlipperAppPluginDescriptor plugin_descriptor = { - .appid = CLI_PLUGIN_APP_ID, - .ep_api_version = CLI_PLUGIN_API_VERSION, - .entry_point = &ibutton_cli, -}; - -const FlipperAppPluginDescriptor* ibutton_cli_plugin_ep(void) { - return &plugin_descriptor; -} diff --git a/applications/main/ibutton/ibutton_start.c b/applications/main/ibutton/ibutton_start.c deleted file mode 100644 index d252bed7f..000000000 --- a/applications/main/ibutton/ibutton_start.c +++ /dev/null @@ -1,11 +0,0 @@ -#include - -static void ibutton_cli_wrapper(Cli* cli, FuriString* args, void* context) { - cli_plugin_wrapper("ibutton", cli, args, context); -} - -void ibutton_on_system_start(void) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "ikey", CliCommandFlagDefault, ibutton_cli_wrapper, cli); - furi_record_close(RECORD_CLI); -} diff --git a/applications/main/infrared/application.fam b/applications/main/infrared/application.fam index 8ffaafa34..8c86c78f3 100644 --- a/applications/main/infrared/application.fam +++ b/applications/main/infrared/application.fam @@ -17,7 +17,7 @@ App( appid="infrared_cli", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="infrared_cli_plugin_ep", + entry_point="infrared_cli_start_ir_plugin_ep", requires=["cli"], sources=[ "infrared_cli.c", @@ -31,6 +31,6 @@ App( apptype=FlipperAppType.STARTUP, targets=["f7"], entry_point="infrared_on_system_start", - sources=["infrared_start.c"], + sources=["infrared_cli.c"], order=20, ) diff --git a/applications/main/infrared/infrared_cli.c b/applications/main/infrared/infrared_cli.c index d4668d699..3579c683a 100644 --- a/applications/main/infrared/infrared_cli.c +++ b/applications/main/infrared/infrared_cli.c @@ -555,15 +555,15 @@ static void infrared_cli_start_ir(Cli* cli, FuriString* args, void* context) { furi_string_free(command); } -#include #include +CLI_PLUGIN_WRAPPER("infrared", infrared_cli_start_ir) -static const FlipperAppPluginDescriptor plugin_descriptor = { - .appid = CLI_PLUGIN_APP_ID, - .ep_api_version = CLI_PLUGIN_API_VERSION, - .entry_point = &infrared_cli_start_ir, -}; - -const FlipperAppPluginDescriptor* infrared_cli_plugin_ep(void) { - return &plugin_descriptor; +void infrared_on_system_start(void) { +#ifdef SRV_CLI + Cli* cli = (Cli*)furi_record_open(RECORD_CLI); + cli_add_command(cli, "ir", CliCommandFlagDefault, infrared_cli_start_ir_wrapper, NULL); + furi_record_close(RECORD_CLI); +#else + UNUSED(infrared_cli_start_ir); +#endif } diff --git a/applications/main/infrared/infrared_start.c b/applications/main/infrared/infrared_start.c deleted file mode 100644 index 6de11b677..000000000 --- a/applications/main/infrared/infrared_start.c +++ /dev/null @@ -1,11 +0,0 @@ -#include - -static void infrared_cli_start_ir_wrapper(Cli* cli, FuriString* args, void* context) { - cli_plugin_wrapper("infrared", cli, args, context); -} - -void infrared_on_system_start(void) { - Cli* cli = (Cli*)furi_record_open(RECORD_CLI); - cli_add_command(cli, "ir", CliCommandFlagDefault, infrared_cli_start_ir_wrapper, NULL); - furi_record_close(RECORD_CLI); -} diff --git a/applications/main/lfrfid/application.fam b/applications/main/lfrfid/application.fam index 5e97e925d..d2abf300d 100644 --- a/applications/main/lfrfid/application.fam +++ b/applications/main/lfrfid/application.fam @@ -25,6 +25,6 @@ App( targets=["f7"], apptype=FlipperAppType.STARTUP, entry_point="lfrfid_on_system_start", - sources=["lfrfid_start.c"], + sources=["lfrfid_cli.c"], order=50, ) diff --git a/applications/main/lfrfid/lfrfid_cli.c b/applications/main/lfrfid/lfrfid_cli.c index eaafcda92..acee316fe 100644 --- a/applications/main/lfrfid/lfrfid_cli.c +++ b/applications/main/lfrfid/lfrfid_cli.c @@ -14,6 +14,18 @@ #include #include +static void lfrfid_cli(Cli* cli, FuriString* args, void* context); + +#include +CLI_PLUGIN_WRAPPER("lfrfid", lfrfid_cli) + +// app cli function +void lfrfid_on_system_start(void) { + Cli* cli = furi_record_open(RECORD_CLI); + cli_add_command(cli, "rfid", CliCommandFlagDefault, lfrfid_cli_wrapper, NULL); + furi_record_close(RECORD_CLI); +} + static void lfrfid_cli_print_usage(void) { printf("Usage:\r\n"); printf("rfid read - read in ASK/PSK mode\r\n"); @@ -568,16 +580,3 @@ static void lfrfid_cli(Cli* cli, FuriString* args, void* context) { furi_string_free(cmd); } - -#include -#include - -static const FlipperAppPluginDescriptor plugin_descriptor = { - .appid = CLI_PLUGIN_APP_ID, - .ep_api_version = CLI_PLUGIN_API_VERSION, - .entry_point = &lfrfid_cli, -}; - -const FlipperAppPluginDescriptor* lfrfid_cli_plugin_ep(void) { - return &plugin_descriptor; -} diff --git a/applications/main/lfrfid/lfrfid_start.c b/applications/main/lfrfid/lfrfid_start.c deleted file mode 100644 index faf275355..000000000 --- a/applications/main/lfrfid/lfrfid_start.c +++ /dev/null @@ -1,11 +0,0 @@ -#include - -static void lfrfid_cli_wrapper(Cli* cli, FuriString* args, void* context) { - cli_plugin_wrapper("lfrfid", cli, args, context); -} - -void lfrfid_on_system_start(void) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "rfid", CliCommandFlagDefault, lfrfid_cli_wrapper, NULL); - furi_record_close(RECORD_CLI); -} diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index 53c04f609..13c26b7aa 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -352,6 +352,6 @@ App( targets=["f7"], apptype=FlipperAppType.STARTUP, entry_point="nfc_on_system_start", - sources=["nfc_start.c"], + sources=["nfc_cli.c"], order=30, ) diff --git a/applications/main/nfc/nfc_cli.c b/applications/main/nfc/nfc_cli.c index fae8ca933..005d6cce1 100644 --- a/applications/main/nfc/nfc_cli.c +++ b/applications/main/nfc/nfc_cli.c @@ -63,15 +63,15 @@ static void nfc_cli(Cli* cli, FuriString* args, void* context) { furi_string_free(cmd); } -#include #include +CLI_PLUGIN_WRAPPER("nfc", nfc_cli) -static const FlipperAppPluginDescriptor plugin_descriptor = { - .appid = CLI_PLUGIN_APP_ID, - .ep_api_version = CLI_PLUGIN_API_VERSION, - .entry_point = &nfc_cli, -}; - -const FlipperAppPluginDescriptor* nfc_cli_plugin_ep(void) { - return &plugin_descriptor; +void nfc_on_system_start(void) { +#ifdef SRV_CLI + Cli* cli = furi_record_open(RECORD_CLI); + cli_add_command(cli, "nfc", CliCommandFlagDefault, nfc_cli_wrapper, NULL); + furi_record_close(RECORD_CLI); +#else + UNUSED(nfc_cli); +#endif } diff --git a/applications/main/nfc/nfc_start.c b/applications/main/nfc/nfc_start.c deleted file mode 100644 index d38a956c3..000000000 --- a/applications/main/nfc/nfc_start.c +++ /dev/null @@ -1,11 +0,0 @@ -#include - -static void nfc_cli_wrapper(Cli* cli, FuriString* args, void* context) { - cli_plugin_wrapper("nfc", cli, args, context); -} - -void nfc_on_system_start(void) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "nfc", CliCommandFlagDefault, nfc_cli_wrapper, NULL); - furi_record_close(RECORD_CLI); -} diff --git a/applications/main/nfc/plugins/supported_cards/clipper.c b/applications/main/nfc/plugins/supported_cards/clipper.c index 90e8a8919..3c306c9bc 100644 --- a/applications/main/nfc/plugins/supported_cards/clipper.c +++ b/applications/main/nfc/plugins/supported_cards/clipper.c @@ -140,6 +140,19 @@ static const IdMapping actransit_zones[] = { }; static const size_t kNumACTransitZones = COUNT(actransit_zones); +// Instead of persisting individual Station IDs, Caltrain saves Zone numbers. +// https://www.caltrain.com/stations-zones +static const IdMapping caltrain_zones[] = { + {.id = 0x0001, .name = "Zone 1"}, + {.id = 0x0002, .name = "Zone 2"}, + {.id = 0x0003, .name = "Zone 3"}, + {.id = 0x0004, .name = "Zone 4"}, + {.id = 0x0005, .name = "Zone 5"}, + {.id = 0x0006, .name = "Zone 6"}, +}; + +static const size_t kNumCaltrainZones = COUNT(caltrain_zones); + // // Full agency+zone mapping. // @@ -150,6 +163,7 @@ static const struct { } agency_zone_map[] = { {.agency_id = 0x0001, .zone_map = actransit_zones, .zone_count = kNumACTransitZones}, {.agency_id = 0x0004, .zone_map = bart_zones, .zone_count = kNumBARTZones}, + {.agency_id = 0x0006, .zone_map = caltrain_zones, .zone_count = kNumCaltrainZones}, {.agency_id = 0x0012, .zone_map = muni_zones, .zone_count = kNumMUNIZones}}; static const size_t kNumAgencyZoneMaps = COUNT(agency_zone_map); diff --git a/applications/main/nfc/plugins/supported_cards/ndef.c b/applications/main/nfc/plugins/supported_cards/ndef.c index 820324421..fb2c4da48 100644 --- a/applications/main/nfc/plugins/supported_cards/ndef.c +++ b/applications/main/nfc/plugins/supported_cards/ndef.c @@ -582,7 +582,7 @@ bool ndef_parse_record( NdefTnf tnf, const char* type, uint8_t type_len) { - FURI_LOG_D(TAG, "payload type: %.*s len: %d", type_len, type, len); + FURI_LOG_D(TAG, "payload type: %.*s len: %hu", type_len, type, len); if(!len) { furi_string_cat(ndef->output, "Empty\n"); return true; @@ -702,9 +702,9 @@ bool ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num, pos += id_len; if(smart_poster) { - furi_string_cat_printf(ndef->output, "\e*> SP-R%d: ", record_num); + furi_string_cat_printf(ndef->output, "\e*> SP-R%zu: ", record_num); } else { - furi_string_cat_printf(ndef->output, "\e*> M%d-R%d: ", message_num, record_num); + furi_string_cat_printf(ndef->output, "\e*> M%zu-R%zu: ", message_num, record_num); } if(!ndef_parse_record(ndef, pos, payload_len, flags_tnf.type_name_format, type, type_len)) { if(type_was_allocated) free(type); @@ -721,7 +721,7 @@ bool ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num, if(smart_poster) { furi_string_cat(ndef->output, "\e*> SP: Empty\n\n"); } else { - furi_string_cat_printf(ndef->output, "\e*> M%d: Empty\n\n", message_num); + furi_string_cat_printf(ndef->output, "\e*> M%zu: Empty\n\n", message_num); } } @@ -949,7 +949,7 @@ static bool ndef_mfc_parse(const NfcDevice* device, FuriString* parsed_data) { } else { data_block = 93 + (sector - 32) * 15; } - FURI_LOG_D(TAG, "data_block: %d", data_block); + FURI_LOG_D(TAG, "data_block: %zu", data_block); size_t data_start = data_block * MF_CLASSIC_BLOCK_SIZE; size_t parsed = ndef_parse_tlv(&ndef, data_start, data_size - data_start, total_parsed); @@ -982,7 +982,7 @@ static bool ndef_slix_parse(const NfcDevice* device, FuriString* parsed_data) { const uint16_t block_count = iso15693_3_get_block_count(data); const uint8_t* blocks = simple_array_cget_data(data->block_data); - // TODO: Find some way to check for other iso15693 NDEF cards and + // TODO(-nofl): Find some way to check for other iso15693 NDEF cards and // split this to also support non-slix iso15693 NDEF tags // Rest of the code works on iso15693 too, but uses SLIX layout assumptions if(block_size != SLIX_BLOCK_SIZE) { diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_dict_attack.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_dict_attack.c index 84352e3fe..500dd759a 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_classic_dict_attack.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_dict_attack.c @@ -5,8 +5,8 @@ #define TAG "NfcMfClassicDictAttack" -// TODO: Fix lag when leaving the dictionary attack view after Hardnested -// TODO: Re-enters backdoor detection between user and system dictionary if no backdoor is found +// TODO FL-3926: Fix lag when leaving the dictionary attack view after Hardnested +// TODO FL-3926: Re-enters backdoor detection between user and system dictionary if no backdoor is found typedef enum { DictAttackStateCUIDDictInProgress, diff --git a/applications/main/onewire/application.fam b/applications/main/onewire/application.fam index 9fac2ff21..ef756ba56 100644 --- a/applications/main/onewire/application.fam +++ b/applications/main/onewire/application.fam @@ -11,6 +11,6 @@ App( appid="onewire_start", apptype=FlipperAppType.STARTUP, entry_point="onewire_on_system_start", - sources=["onewire_start.c"], + sources=["onewire_cli.c"], order=60, ) diff --git a/applications/main/onewire/onewire_cli.c b/applications/main/onewire/onewire_cli.c index 4ee81a462..1aeb2452f 100644 --- a/applications/main/onewire/onewire_cli.c +++ b/applications/main/onewire/onewire_cli.c @@ -6,6 +6,21 @@ #include +static void onewire_cli(Cli* cli, FuriString* args, void* context); + +#include +CLI_PLUGIN_WRAPPER("onewire", onewire_cli) + +void onewire_on_system_start(void) { +#ifdef SRV_CLI + Cli* cli = furi_record_open(RECORD_CLI); + cli_add_command(cli, "onewire", CliCommandFlagDefault, onewire_cli_wrapper, cli); + furi_record_close(RECORD_CLI); +#else + UNUSED(onewire_cli); +#endif +} + static void onewire_cli_print_usage(void) { printf("Usage:\r\n"); printf("onewire search\r\n"); @@ -58,16 +73,3 @@ void onewire_cli(Cli* cli, FuriString* args, void* context) { furi_string_free(cmd); } - -#include -#include - -static const FlipperAppPluginDescriptor plugin_descriptor = { - .appid = CLI_PLUGIN_APP_ID, - .ep_api_version = CLI_PLUGIN_API_VERSION, - .entry_point = &onewire_cli, -}; - -const FlipperAppPluginDescriptor* onewire_cli_plugin_ep(void) { - return &plugin_descriptor; -} diff --git a/applications/main/onewire/onewire_start.c b/applications/main/onewire/onewire_start.c deleted file mode 100644 index 219335411..000000000 --- a/applications/main/onewire/onewire_start.c +++ /dev/null @@ -1,11 +0,0 @@ -#include - -static void onewire_cli_wrapper(Cli* cli, FuriString* args, void* context) { - cli_plugin_wrapper("onewire", cli, args, context); -} - -void onewire_on_system_start(void) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "onewire", CliCommandFlagDefault, onewire_cli_wrapper, cli); - furi_record_close(RECORD_CLI); -} diff --git a/applications/main/subghz/application.fam b/applications/main/subghz/application.fam index 15831d6e6..3c9625ed3 100644 --- a/applications/main/subghz/application.fam +++ b/applications/main/subghz/application.fam @@ -48,7 +48,7 @@ App( appid="subghz_cli", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="subghz_cli_plugin_ep", + entry_point="subghz_cli_command_plugin_ep", requires=["cli"], sources=["subghz_cli.c", "helpers/subghz_chat.c"], ) @@ -59,7 +59,7 @@ App( targets=["f7"], apptype=FlipperAppType.STARTUP, entry_point="subghz_on_system_start", - # sources=["subghz_start.c"], + # sources=["subghz_cli.c"], order=40, ) diff --git a/applications/main/subghz/subghz_cli.c b/applications/main/subghz/subghz_cli.c index cf7300d59..70ce5b138 100644 --- a/applications/main/subghz/subghz_cli.c +++ b/applications/main/subghz/subghz_cli.c @@ -1185,15 +1185,24 @@ static void subghz_cli_command(Cli* cli, FuriString* args, void* context) { furi_string_free(cmd); } -#include #include +CLI_PLUGIN_WRAPPER("subghz", subghz_cli_command) -static const FlipperAppPluginDescriptor plugin_descriptor = { - .appid = CLI_PLUGIN_APP_ID, - .ep_api_version = CLI_PLUGIN_API_VERSION, - .entry_point = &subghz_cli_command, -}; - -const FlipperAppPluginDescriptor* subghz_cli_plugin_ep(void) { - return &plugin_descriptor; +static void subghz_cli_command_chat_wrapper(Cli* cli, FuriString* args, void* context) { + furi_string_replace_at(args, 0, 0, "chat "); + subghz_cli_command_wrapper(cli, args, context); +} + +void subghz_on_system_start(void) { +#ifdef SRV_CLI + Cli* cli = furi_record_open(RECORD_CLI); + + cli_add_command(cli, "subghz", CliCommandFlagDefault, subghz_cli_command_wrapper, NULL); + cli_add_command(cli, "chat", CliCommandFlagDefault, subghz_cli_command_chat_wrapper, NULL); + + furi_record_close(RECORD_CLI); +#else + UNUSED(subghz_cli_command); + UNUSED(subghz_cli_command_chat_wrapper); +#endif } diff --git a/applications/main/subghz/subghz_start.c b/applications/main/subghz/subghz_start.c deleted file mode 100644 index 13a8e5c55..000000000 --- a/applications/main/subghz/subghz_start.c +++ /dev/null @@ -1,113 +0,0 @@ -#include - -#include -#include -#include - -#define SUBGHZ_REGION_FILENAME "/int/.region_data" - -static void subghz_cli_command_wrapper(Cli* cli, FuriString* args, void* context) { - cli_plugin_wrapper("subghz", cli, args, context); -} - -static void subghz_cli_command_chat_wrapper(Cli* cli, FuriString* args, void* context) { - furi_string_replace_at(args, 0, 0, "chat "); - subghz_cli_command_wrapper(cli, args, context); -} - -static bool - subghz_on_system_start_istream_read(pb_istream_t* istream, pb_byte_t* buf, size_t count) { - File* file = istream->state; - size_t ret = storage_file_read(file, buf, count); - return (count == ret); -} - -static bool subghz_on_system_start_istream_decode_band( - pb_istream_t* stream, - const pb_field_t* field, - void** arg) { - (void)field; - FuriHalRegion* region = *arg; - - PB_Region_Band band = {0}; - if(!pb_decode(stream, PB_Region_Band_fields, &band)) { - FURI_LOG_E("SubGhzOnStart", "PB Region band decode error: %s", PB_GET_ERROR(stream)); - return false; - } - - region->bands_count += 1; - region = realloc( //-V701 - region, - sizeof(FuriHalRegion) + sizeof(FuriHalRegionBand) * region->bands_count); - size_t pos = region->bands_count - 1; - region->bands[pos].start = band.start; - region->bands[pos].end = band.end; - region->bands[pos].power_limit = band.power_limit; - region->bands[pos].duty_cycle = band.duty_cycle; - *arg = region; - - FURI_LOG_I( - "SubGhzOnStart", - "Add allowed band: start %luHz, stop %luHz, power_limit %ddBm, duty_cycle %u%%", - band.start, - band.end, - band.power_limit, - band.duty_cycle); - return true; -} - -void subghz_on_system_start(void) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "subghz", CliCommandFlagDefault, subghz_cli_command_wrapper, NULL); - cli_add_command(cli, "chat", CliCommandFlagDefault, subghz_cli_command_chat_wrapper, NULL); - furi_record_close(RECORD_CLI); - -#ifdef SRV_STORAGE - Storage* storage = furi_record_open(RECORD_STORAGE); - File* file = storage_file_alloc(storage); - FileInfo fileinfo = {0}; - PB_Region pb_region = {0}; - pb_region.bands.funcs.decode = subghz_on_system_start_istream_decode_band; - - do { - if(storage_common_stat(storage, SUBGHZ_REGION_FILENAME, &fileinfo) != FSE_OK || - fileinfo.size == 0) { - FURI_LOG_W("SubGhzOnStart", "Region data is missing or empty"); - break; - } - - if(!storage_file_open(file, SUBGHZ_REGION_FILENAME, FSAM_READ, FSOM_OPEN_EXISTING)) { - FURI_LOG_E("SubGhzOnStart", "Unable to open region data"); - break; - } - - pb_istream_t istream = { - .callback = subghz_on_system_start_istream_read, - .state = file, - .errmsg = NULL, - .bytes_left = fileinfo.size, - }; - - pb_region.bands.arg = malloc(sizeof(FuriHalRegion)); - if(!pb_decode(&istream, PB_Region_fields, &pb_region)) { - FURI_LOG_E("SubGhzOnStart", "Invalid region data"); - free(pb_region.bands.arg); - break; - } - - FuriHalRegion* region = pb_region.bands.arg; - memcpy( - region->country_code, - pb_region.country_code->bytes, - pb_region.country_code->size < 4 ? pb_region.country_code->size : 3); - furi_hal_region_set(region); - } while(0); - - pb_release(PB_Region_fields, &pb_region); - storage_file_free(file); - furi_record_close(RECORD_STORAGE); -#else - UNUSED(subghz_on_system_start_istream_decode_band); - UNUSED(subghz_on_system_start_istream_read); -#endif -} diff --git a/applications/services/bt/application.fam b/applications/services/bt/application.fam index 54e197259..5e77b2a84 100644 --- a/applications/services/bt/application.fam +++ b/applications/services/bt/application.fam @@ -23,7 +23,7 @@ App( apptype=FlipperAppType.PLUGIN, entry_point="bt_cli_plugin_ep", requires=["cli"], - sources=["bt_cli.c"], + sources=["bt_cli.c", "bt_service/bt_settings_api.c"], ) App( diff --git a/applications/services/bt/bt_cli.c b/applications/services/bt/bt_cli.c index 8f94e1963..bd85c8861 100644 --- a/applications/services/bt/bt_cli.c +++ b/applications/services/bt/bt_cli.c @@ -227,15 +227,15 @@ static void bt_cli(Cli* cli, FuriString* args, void* context) { furi_record_close(RECORD_BT); } -#include #include +CLI_PLUGIN_WRAPPER("bt", bt_cli) -static const FlipperAppPluginDescriptor plugin_descriptor = { - .appid = CLI_PLUGIN_APP_ID, - .ep_api_version = CLI_PLUGIN_API_VERSION, - .entry_point = &bt_cli, -}; - -const FlipperAppPluginDescriptor* bt_cli_plugin_ep(void) { - return &plugin_descriptor; +void bt_on_system_start(void) { +#ifdef SRV_CLI + Cli* cli = furi_record_open(RECORD_CLI); + cli_add_command(cli, RECORD_BT, CliCommandFlagDefault, bt_cli_wrapper, NULL); + furi_record_close(RECORD_CLI); +#else + UNUSED(bt_cli); +#endif } diff --git a/applications/services/bt/bt_service/bt_api.c b/applications/services/bt/bt_service/bt_api.c index 39b9a099d..f0e792d42 100644 --- a/applications/services/bt/bt_service/bt_api.c +++ b/applications/services/bt/bt_service/bt_api.c @@ -77,39 +77,3 @@ void bt_keys_storage_set_default_path(Bt* bt) { bt_keys_storage_set_file_path(bt->keys_storage, BT_KEYS_STORAGE_PATH); } - -/* - * Private API for the Settings app - */ - -void bt_get_settings(Bt* bt, BtSettings* settings) { - furi_assert(bt); - furi_assert(settings); - - BtMessage message = { - .lock = api_lock_alloc_locked(), - .type = BtMessageTypeGetSettings, - .data.settings = settings, - }; - - furi_check( - furi_message_queue_put(bt->message_queue, &message, FuriWaitForever) == FuriStatusOk); - - api_lock_wait_unlock_and_free(message.lock); -} - -void bt_set_settings(Bt* bt, const BtSettings* settings) { - furi_assert(bt); - furi_assert(settings); - - BtMessage message = { - .lock = api_lock_alloc_locked(), - .type = BtMessageTypeSetSettings, - .data.csettings = settings, - }; - - furi_check( - furi_message_queue_put(bt->message_queue, &message, FuriWaitForever) == FuriStatusOk); - - api_lock_wait_unlock_and_free(message.lock); -} diff --git a/applications/services/bt/bt_service/bt_settings_api.c b/applications/services/bt/bt_service/bt_settings_api.c new file mode 100644 index 000000000..333b14cd3 --- /dev/null +++ b/applications/services/bt/bt_service/bt_settings_api.c @@ -0,0 +1,37 @@ +#include "bt_i.h" + +/* + * Private API for the Settings app + */ + +void bt_get_settings(Bt* bt, BtSettings* settings) { + furi_assert(bt); + furi_assert(settings); + + BtMessage message = { + .lock = api_lock_alloc_locked(), + .type = BtMessageTypeGetSettings, + .data.settings = settings, + }; + + furi_check( + furi_message_queue_put(bt->message_queue, &message, FuriWaitForever) == FuriStatusOk); + + api_lock_wait_unlock_and_free(message.lock); +} + +void bt_set_settings(Bt* bt, const BtSettings* settings) { + furi_assert(bt); + furi_assert(settings); + + BtMessage message = { + .lock = api_lock_alloc_locked(), + .type = BtMessageTypeSetSettings, + .data.csettings = settings, + }; + + furi_check( + furi_message_queue_put(bt->message_queue, &message, FuriWaitForever) == FuriStatusOk); + + api_lock_wait_unlock_and_free(message.lock); +} diff --git a/applications/services/bt/bt_start.c b/applications/services/bt/bt_start.c deleted file mode 100644 index 124e4f93f..000000000 --- a/applications/services/bt/bt_start.c +++ /dev/null @@ -1,12 +0,0 @@ -#include -#include "bt_service/bt.h" - -static void bt_cli_wrapper(Cli* cli, FuriString* args, void* context) { - cli_plugin_wrapper("bt", cli, args, context); -} - -void bt_on_system_start(void) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, RECORD_BT, CliCommandFlagDefault, bt_cli_wrapper, NULL); - furi_record_close(RECORD_CLI); -} diff --git a/applications/services/cli/application.fam b/applications/services/cli/application.fam index 7a57bb607..b6c72ecd4 100644 --- a/applications/services/cli/application.fam +++ b/applications/services/cli/application.fam @@ -8,3 +8,102 @@ App( order=30, sdk_headers=["cli.h", "cli_vcp.h"], ) + +App( + appid="info_cli", + targets=["f7"], + apptype=FlipperAppType.PLUGIN, + entry_point="cli_command_info_plugin_ep", + requires=["cli"], + sources=["cli_commands.c"], +) + +App( + appid="src_cli", + targets=["f7"], + apptype=FlipperAppType.PLUGIN, + entry_point="cli_command_src_plugin_ep", + requires=["cli"], + sources=["cli_commands.c"], +) + +App( + appid="neofetch_cli", + targets=["f7"], + apptype=FlipperAppType.PLUGIN, + entry_point="cli_command_neofetch_plugin_ep", + requires=["cli"], + sources=["cli_commands.c"], +) + +App( + appid="help_cli", + targets=["f7"], + apptype=FlipperAppType.PLUGIN, + entry_point="cli_command_help_plugin_ep", + requires=["cli"], + sources=["cli_commands.c"], +) + +App( + appid="uptime_cli", + targets=["f7"], + apptype=FlipperAppType.PLUGIN, + entry_point="cli_command_uptime_plugin_ep", + requires=["cli"], + sources=["cli_commands.c"], +) + +App( + appid="date_cli", + targets=["f7"], + apptype=FlipperAppType.PLUGIN, + entry_point="cli_command_date_plugin_ep", + requires=["cli"], + sources=["cli_commands.c"], +) + +App( + appid="sysctl_cli", + targets=["f7"], + apptype=FlipperAppType.PLUGIN, + entry_point="cli_command_sysctl_plugin_ep", + requires=["cli"], + sources=["cli_commands.c"], +) + +App( + appid="vibro_cli", + targets=["f7"], + apptype=FlipperAppType.PLUGIN, + entry_point="cli_command_vibro_plugin_ep", + requires=["cli"], + sources=["cli_commands.c"], +) + +App( + appid="led_cli", + targets=["f7"], + apptype=FlipperAppType.PLUGIN, + entry_point="cli_command_led_plugin_ep", + requires=["cli"], + sources=["cli_commands.c"], +) + +App( + appid="gpio_cli", + targets=["f7"], + apptype=FlipperAppType.PLUGIN, + entry_point="cli_command_gpio_plugin_ep", + requires=["cli"], + sources=["cli_commands.c", "cli_command_gpio.c"], +) + +App( + appid="i2c_cli", + targets=["f7"], + apptype=FlipperAppType.PLUGIN, + entry_point="cli_command_i2c_plugin_ep", + requires=["cli"], + sources=["cli_commands.c"], +) diff --git a/applications/services/cli/cli_commands.c b/applications/services/cli/cli_commands.c index a305ae734..9dd4e4265 100644 --- a/applications/services/cli/cli_commands.c +++ b/applications/services/cli/cli_commands.c @@ -682,33 +682,46 @@ void cli_command_i2c(Cli* cli, FuriString* args, void* context) { furi_hal_i2c_release(&furi_hal_i2c_handle_external); } +CLI_PLUGIN_WRAPPER("info", cli_command_info) +CLI_PLUGIN_WRAPPER("src", cli_command_src) +CLI_PLUGIN_WRAPPER("neofetch", cli_command_neofetch) +CLI_PLUGIN_WRAPPER("help", cli_command_help) +CLI_PLUGIN_WRAPPER("uptime", cli_command_uptime) +CLI_PLUGIN_WRAPPER("date", cli_command_date) +CLI_PLUGIN_WRAPPER("sysctl", cli_command_sysctl) +CLI_PLUGIN_WRAPPER("vibro", cli_command_vibro) +CLI_PLUGIN_WRAPPER("led", cli_command_led) +CLI_PLUGIN_WRAPPER("gpio", cli_command_gpio) +CLI_PLUGIN_WRAPPER("i2c", cli_command_i2c) + 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, "source", CliCommandFlagParallelSafe, cli_command_src, NULL); - cli_add_command(cli, "src", CliCommandFlagParallelSafe, cli_command_src, NULL); + cli_add_command(cli, "!", CliCommandFlagParallelSafe, cli_command_info_wrapper, (void*)true); + cli_add_command(cli, "info", CliCommandFlagParallelSafe, cli_command_info_wrapper, NULL); + cli_add_command( + cli, "device_info", CliCommandFlagParallelSafe, cli_command_info_wrapper, (void*)true); + cli_add_command(cli, "source", CliCommandFlagParallelSafe, cli_command_src_wrapper, NULL); + cli_add_command(cli, "src", CliCommandFlagParallelSafe, cli_command_src_wrapper, NULL); cli_add_command( cli, "neofetch", CliCommandFlagParallelSafe | CliCommandFlagHidden, - cli_command_neofetch, + cli_command_neofetch_wrapper, NULL); - cli_add_command(cli, "?", CliCommandFlagParallelSafe, cli_command_help, NULL); - cli_add_command(cli, "help", CliCommandFlagParallelSafe, cli_command_help, NULL); + cli_add_command(cli, "?", CliCommandFlagParallelSafe, cli_command_help_wrapper, NULL); + cli_add_command(cli, "help", CliCommandFlagParallelSafe, cli_command_help_wrapper, NULL); - cli_add_command(cli, "uptime", CliCommandFlagDefault, cli_command_uptime, NULL); - cli_add_command(cli, "date", CliCommandFlagParallelSafe, cli_command_date, NULL); + cli_add_command(cli, "uptime", CliCommandFlagDefault, cli_command_uptime_wrapper, NULL); + cli_add_command(cli, "date", CliCommandFlagParallelSafe, cli_command_date_wrapper, NULL); cli_add_command(cli, "log", CliCommandFlagParallelSafe, cli_command_log, NULL); cli_add_command(cli, "l", CliCommandFlagParallelSafe, cli_command_log, NULL); - cli_add_command(cli, "sysctl", CliCommandFlagDefault, cli_command_sysctl, NULL); + cli_add_command(cli, "sysctl", CliCommandFlagDefault, cli_command_sysctl_wrapper, NULL); cli_add_command(cli, "top", CliCommandFlagParallelSafe, cli_command_top, NULL); cli_add_command(cli, "free", CliCommandFlagParallelSafe, cli_command_free, NULL); cli_add_command(cli, "free_blocks", CliCommandFlagParallelSafe, cli_command_free_blocks, NULL); - cli_add_command(cli, "vibro", CliCommandFlagDefault, cli_command_vibro, NULL); - cli_add_command(cli, "led", CliCommandFlagDefault, cli_command_led, NULL); - cli_add_command(cli, "gpio", CliCommandFlagDefault, cli_command_gpio, NULL); - cli_add_command(cli, "i2c", CliCommandFlagDefault, cli_command_i2c, NULL); + cli_add_command(cli, "vibro", CliCommandFlagDefault, cli_command_vibro_wrapper, NULL); + cli_add_command(cli, "led", CliCommandFlagDefault, cli_command_led_wrapper, NULL); + cli_add_command(cli, "gpio", CliCommandFlagDefault, cli_command_gpio_wrapper, NULL); + cli_add_command(cli, "i2c", CliCommandFlagDefault, cli_command_i2c_wrapper, NULL); } diff --git a/applications/services/cli/cli_i.h b/applications/services/cli/cli_i.h index ed207b9f9..f6d2bba7e 100644 --- a/applications/services/cli/cli_i.h +++ b/applications/services/cli/cli_i.h @@ -62,12 +62,28 @@ void cli_putc(Cli* cli, char c); void cli_stdout_callback(void* _cookie, const char* data, size_t size); -// Wraps CLI commands to load from plugin file -// Must call from CLI context, like dummy CLI command callback -// You need to setup the plugin to compile correctly separately +// CLI command wrapping to load from plugin file on SD card +// Just need to: +// - Use CLI_PLUGIN_WRAPPER("name", cmd_callback) +// - Replace callback usages with cmd_callback_wrapper +// - Add "name_cli" entry in app manifest to build as plugin +void cli_plugin_wrapper(const char* name, Cli* cli, FuriString* args, void* context); +#include #define CLI_PLUGIN_APP_ID "cli" #define CLI_PLUGIN_API_VERSION 1 -void cli_plugin_wrapper(const char* name, Cli* cli, FuriString* args, void* context); +#define CLI_PLUGIN_WRAPPER(plugin_name_without_cli_suffix, cli_command_callback) \ + void cli_command_callback##_wrapper(Cli* cli, FuriString* args, void* context) { \ + cli_plugin_wrapper(plugin_name_without_cli_suffix, cli, args, context); \ + } \ + static const FlipperAppPluginDescriptor cli_command_callback##_plugin_descriptor = { \ + .appid = CLI_PLUGIN_APP_ID, \ + .ep_api_version = CLI_PLUGIN_API_VERSION, \ + .entry_point = &cli_command_callback, \ + }; \ + const FlipperAppPluginDescriptor* cli_command_callback##_plugin_ep(void) { \ + UNUSED(cli_command_callback##_wrapper); \ + return &cli_command_callback##_plugin_descriptor; \ + } #ifdef __cplusplus } diff --git a/applications/services/crypto/application.fam b/applications/services/crypto/application.fam index 9a45c27db..e42f4e0f7 100644 --- a/applications/services/crypto/application.fam +++ b/applications/services/crypto/application.fam @@ -11,6 +11,6 @@ App( appid="crypto_start", apptype=FlipperAppType.STARTUP, entry_point="crypto_on_system_start", - sources=["crypto_start.c"], + sources=["crypto_cli.c"], order=10, ) diff --git a/applications/services/crypto/crypto_cli.c b/applications/services/crypto/crypto_cli.c index 5ef704b8b..e3637b275 100644 --- a/applications/services/crypto/crypto_cli.c +++ b/applications/services/crypto/crypto_cli.c @@ -317,15 +317,15 @@ static void crypto_cli(Cli* cli, FuriString* args, void* context) { furi_string_free(cmd); } -#include #include +CLI_PLUGIN_WRAPPER("crypto", crypto_cli) -static const FlipperAppPluginDescriptor plugin_descriptor = { - .appid = CLI_PLUGIN_APP_ID, - .ep_api_version = CLI_PLUGIN_API_VERSION, - .entry_point = &crypto_cli, -}; - -const FlipperAppPluginDescriptor* crypto_cli_plugin_ep(void) { - return &plugin_descriptor; +void crypto_on_system_start(void) { +#ifdef SRV_CLI + Cli* cli = furi_record_open(RECORD_CLI); + cli_add_command(cli, "crypto", CliCommandFlagDefault, crypto_cli_wrapper, NULL); + furi_record_close(RECORD_CLI); +#else + UNUSED(crypto_cli); +#endif } diff --git a/applications/services/crypto/crypto_start.c b/applications/services/crypto/crypto_start.c deleted file mode 100644 index 0ec3940f9..000000000 --- a/applications/services/crypto/crypto_start.c +++ /dev/null @@ -1,11 +0,0 @@ -#include - -static void crypto_cli_wrapper(Cli* cli, FuriString* args, void* context) { - cli_plugin_wrapper("crypto", cli, args, context); -} - -void crypto_on_system_start(void) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "crypto", CliCommandFlagDefault, crypto_cli_wrapper, NULL); - furi_record_close(RECORD_CLI); -} diff --git a/applications/services/dolphin/dolphin.c b/applications/services/dolphin/dolphin.c index d9c6b50ae..ff1bbe383 100644 --- a/applications/services/dolphin/dolphin.c +++ b/applications/services/dolphin/dolphin.c @@ -231,7 +231,7 @@ static void dolphin_reset_butthurt_timer(Dolphin* dolphin) { } } -static bool dolphin_process_event(FuriEventLoopObject* object, void* context) { +static void dolphin_process_event(FuriEventLoopObject* object, void* context) { UNUSED(object); Dolphin* dolphin = context; @@ -284,8 +284,6 @@ static bool dolphin_process_event(FuriEventLoopObject* object, void* context) { } dolphin_event_release(&event); - - return true; } static void dolphin_storage_callback(const void* message, void* context) { diff --git a/applications/services/gui/view_dispatcher.c b/applications/services/gui/view_dispatcher.c index fb3cdd4ef..80be9c153 100644 --- a/applications/services/gui/view_dispatcher.c +++ b/applications/services/gui/view_dispatcher.c @@ -442,7 +442,7 @@ void view_dispatcher_update(View* view, void* context) { } } -bool view_dispatcher_run_event_callback(FuriEventLoopObject* object, void* context) { +void view_dispatcher_run_event_callback(FuriEventLoopObject* object, void* context) { furi_assert(context); ViewDispatcher* instance = context; furi_assert(instance->event_queue == object); @@ -450,11 +450,9 @@ bool view_dispatcher_run_event_callback(FuriEventLoopObject* object, void* conte uint32_t event; furi_check(furi_message_queue_get(instance->event_queue, &event, 0) == FuriStatusOk); view_dispatcher_handle_custom_event(instance, event); - - return true; } -bool view_dispatcher_run_input_callback(FuriEventLoopObject* object, void* context) { +void view_dispatcher_run_input_callback(FuriEventLoopObject* object, void* context) { furi_assert(context); ViewDispatcher* instance = context; furi_assert(instance->input_queue == object); @@ -462,11 +460,9 @@ bool view_dispatcher_run_input_callback(FuriEventLoopObject* object, void* conte InputEvent input; furi_check(furi_message_queue_get(instance->input_queue, &input, 0) == FuriStatusOk); view_dispatcher_handle_input(instance, &input); - - return true; } -bool view_dispatcher_run_ascii_callback(FuriEventLoopObject* object, void* context) { +void view_dispatcher_run_ascii_callback(FuriEventLoopObject* object, void* context) { furi_assert(context); ViewDispatcher* instance = context; furi_assert(instance->ascii_queue == object); @@ -474,6 +470,4 @@ bool view_dispatcher_run_ascii_callback(FuriEventLoopObject* object, void* conte AsciiEvent ascii; furi_check(furi_message_queue_get(instance->ascii_queue, &ascii, 0) == FuriStatusOk); view_dispatcher_handle_ascii(instance, &ascii); - - return true; } diff --git a/applications/services/gui/view_dispatcher_i.h b/applications/services/gui/view_dispatcher_i.h index 80d4d2f70..866f189e1 100644 --- a/applications/services/gui/view_dispatcher_i.h +++ b/applications/services/gui/view_dispatcher_i.h @@ -65,10 +65,10 @@ void view_dispatcher_set_current_view(ViewDispatcher* view_dispatcher, View* vie void view_dispatcher_update(View* view, void* context); /** ViewDispatcher run event loop event callback */ -bool view_dispatcher_run_event_callback(FuriEventLoopObject* object, void* context); +void view_dispatcher_run_event_callback(FuriEventLoopObject* object, void* context); /** ViewDispatcher run event loop input callback */ -bool view_dispatcher_run_input_callback(FuriEventLoopObject* object, void* context); +void view_dispatcher_run_input_callback(FuriEventLoopObject* object, void* context); /** ViewDispatcher run event loop ascii callback */ -bool view_dispatcher_run_ascii_callback(FuriEventLoopObject* object, void* context); +void view_dispatcher_run_ascii_callback(FuriEventLoopObject* object, void* context); diff --git a/applications/services/input/input.c b/applications/services/input/input.c index a298001e1..78b565aa7 100644 --- a/applications/services/input/input.c +++ b/applications/services/input/input.c @@ -25,7 +25,7 @@ typedef struct { } InputPinState; /** Input CLI command handler */ -void input_cli(Cli* cli, FuriString* args, void* context); +void input_cli_wrapper(Cli* cli, FuriString* args, void* context); // #define INPUT_DEBUG @@ -79,12 +79,6 @@ const char* input_get_type_name(InputType type) { } } -#include - -static void input_cli_wrapper(Cli* cli, FuriString* args, void* context) { - cli_plugin_wrapper("input", cli, args, context); -} - int32_t input_srv(void* p) { UNUSED(p); @@ -102,8 +96,6 @@ int32_t input_srv(void* p) { #ifdef SRV_CLI Cli* cli = furi_record_open(RECORD_CLI); cli_add_command(cli, "input", CliCommandFlagParallelSafe, input_cli_wrapper, event_pubsub); -#else - UNUSED(input_cli_wrapper); #endif InputPinState pin_states[input_pins_count]; diff --git a/applications/services/input/input_cli.c b/applications/services/input/input_cli.c index 10a09cd42..27f7dae72 100644 --- a/applications/services/input/input_cli.c +++ b/applications/services/input/input_cli.c @@ -226,15 +226,5 @@ void input_cli(Cli* cli, FuriString* args, void* context) { furi_string_free(cmd); } -#include #include - -static const FlipperAppPluginDescriptor plugin_descriptor = { - .appid = CLI_PLUGIN_APP_ID, - .ep_api_version = CLI_PLUGIN_API_VERSION, - .entry_point = &input_cli, -}; - -const FlipperAppPluginDescriptor* input_cli_plugin_ep(void) { - return &plugin_descriptor; -} +CLI_PLUGIN_WRAPPER("input", input_cli) diff --git a/applications/services/loader/loader_cli.c b/applications/services/loader/loader_cli.c index 45acefa68..15b4c914d 100644 --- a/applications/services/loader/loader_cli.c +++ b/applications/services/loader/loader_cli.c @@ -141,15 +141,15 @@ static void loader_cli(Cli* cli, FuriString* args, void* context) { furi_record_close(RECORD_LOADER); } -#include #include +CLI_PLUGIN_WRAPPER("loader", loader_cli) -static const FlipperAppPluginDescriptor plugin_descriptor = { - .appid = CLI_PLUGIN_APP_ID, - .ep_api_version = CLI_PLUGIN_API_VERSION, - .entry_point = &loader_cli, -}; - -const FlipperAppPluginDescriptor* loader_cli_plugin_ep(void) { - return &plugin_descriptor; +void loader_on_system_start(void) { +#ifdef SRV_CLI + Cli* cli = furi_record_open(RECORD_CLI); + cli_add_command(cli, RECORD_LOADER, CliCommandFlagParallelSafe, loader_cli_wrapper, NULL); + furi_record_close(RECORD_CLI); +#else + UNUSED(loader_cli); +#endif } diff --git a/applications/services/loader/loader_start.c b/applications/services/loader/loader_start.c deleted file mode 100644 index 23122062f..000000000 --- a/applications/services/loader/loader_start.c +++ /dev/null @@ -1,12 +0,0 @@ -#include -#include "loader.h" - -static void loader_cli_wrapper(Cli* cli, FuriString* args, void* context) { - cli_plugin_wrapper("loader", cli, args, context); -} - -void loader_on_system_start(void) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, RECORD_LOADER, CliCommandFlagParallelSafe, loader_cli_wrapper, NULL); - furi_record_close(RECORD_CLI); -} diff --git a/applications/services/notification/notification_messages.c b/applications/services/notification/notification_messages.c index 8b7916226..3dc154654 100644 --- a/applications/services/notification/notification_messages.c +++ b/applications/services/notification/notification_messages.c @@ -593,3 +593,7 @@ const NotificationSequence sequence_lcd_contrast_update = { &message_lcd_contrast_update, NULL, }; + +const NotificationSequence sequence_empty = { + NULL, +}; diff --git a/applications/services/notification/notification_messages.h b/applications/services/notification/notification_messages.h index 873bb37a8..3960d93b7 100644 --- a/applications/services/notification/notification_messages.h +++ b/applications/services/notification/notification_messages.h @@ -145,6 +145,9 @@ extern const NotificationSequence sequence_audiovisual_alert; // LCD extern const NotificationSequence sequence_lcd_contrast_update; +// Wait for notification queue become empty +extern const NotificationSequence sequence_empty; + #ifdef __cplusplus } #endif diff --git a/applications/services/power/power_cli.c b/applications/services/power/power_cli.c index 0b2a9851b..929a4fa78 100644 --- a/applications/services/power/power_cli.c +++ b/applications/services/power/power_cli.c @@ -108,15 +108,17 @@ void power_cli(Cli* cli, FuriString* args, void* context) { furi_string_free(cmd); } -#include #include +CLI_PLUGIN_WRAPPER("power", power_cli) -static const FlipperAppPluginDescriptor plugin_descriptor = { - .appid = CLI_PLUGIN_APP_ID, - .ep_api_version = CLI_PLUGIN_API_VERSION, - .entry_point = &power_cli, -}; +void power_on_system_start(void) { +#ifdef SRV_CLI + Cli* cli = furi_record_open(RECORD_CLI); -const FlipperAppPluginDescriptor* power_cli_plugin_ep(void) { - return &plugin_descriptor; + cli_add_command(cli, "power", CliCommandFlagParallelSafe, power_cli_wrapper, NULL); + + furi_record_close(RECORD_CLI); +#else + UNUSED(power_cli); +#endif } diff --git a/applications/services/power/power_service/power.c b/applications/services/power/power_service/power.c index ba8e6cd6a..b2798e338 100644 --- a/applications/services/power/power_service/power.c +++ b/applications/services/power/power_service/power.c @@ -499,7 +499,7 @@ static void power_handle_reboot(PowerBootMode mode) { furi_hal_power_reset(); } -static bool power_message_callback(FuriEventLoopObject* object, void* context) { +static void power_message_callback(FuriEventLoopObject* object, void* context) { furi_assert(context); Power* power = context; @@ -545,8 +545,6 @@ static bool power_message_callback(FuriEventLoopObject* object, void* context) { if(msg.lock) { api_lock_unlock(msg.lock); } - - return true; } static void power_tick_callback(void* context) { diff --git a/applications/services/power/power_start.c b/applications/services/power/power_start.c deleted file mode 100644 index 33e6901f1..000000000 --- a/applications/services/power/power_start.c +++ /dev/null @@ -1,11 +0,0 @@ -#include - -static void power_cli_wrapper(Cli* cli, FuriString* args, void* context) { - cli_plugin_wrapper("power", cli, args, context); -} - -void power_on_system_start(void) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "power", CliCommandFlagParallelSafe, power_cli_wrapper, NULL); - furi_record_close(RECORD_CLI); -} diff --git a/applications/settings/application.fam b/applications/settings/application.fam index cc4b9703d..1d6db35a7 100644 --- a/applications/settings/application.fam +++ b/applications/settings/application.fam @@ -5,6 +5,7 @@ App( provides=[ "passport", "system_settings", + "clock_settings", "about", ], ) diff --git a/applications/settings/bt_settings_app/mock_imports/mock_bt_api.c b/applications/settings/bt_settings_app/mock_imports/mock_bt_settings_api.c similarity index 85% rename from applications/settings/bt_settings_app/mock_imports/mock_bt_api.c rename to applications/settings/bt_settings_app/mock_imports/mock_bt_settings_api.c index c7a2036f1..0f28d559f 100644 --- a/applications/settings/bt_settings_app/mock_imports/mock_bt_api.c +++ b/applications/settings/bt_settings_app/mock_imports/mock_bt_settings_api.c @@ -4,4 +4,4 @@ // Then, we still use the Header from original code as if nothing happened // bt_get_settings(), bt_set_settings() -#include +#include diff --git a/applications/settings/clock_settings/application.fam b/applications/settings/clock_settings/application.fam new file mode 100644 index 000000000..0ae42a4af --- /dev/null +++ b/applications/settings/clock_settings/application.fam @@ -0,0 +1,18 @@ +App( + appid="clock_settings", + name="Clock & Alarm", + apptype=FlipperAppType.SETTINGS, + entry_point="clock_settings", + requires=["gui"], + provides=["clock_settings_start"], + stack_size=1 * 1024, + order=90, + fap_category="assets", +) + +App( + appid="clock_settings_start", + apptype=FlipperAppType.STARTUP, + entry_point="clock_settings_start", + order=1000, +) diff --git a/applications/settings/clock_settings/clock_settings.c b/applications/settings/clock_settings/clock_settings.c new file mode 100644 index 000000000..455f1deea --- /dev/null +++ b/applications/settings/clock_settings/clock_settings.c @@ -0,0 +1,71 @@ +#include "clock_settings.h" + +#include +#include + +static bool clock_settings_custom_event_callback(void* context, uint32_t event) { + furi_assert(context); + ClockSettings* app = context; + return scene_manager_handle_custom_event(app->scene_manager, event); +} + +static bool clock_settings_back_event_callback(void* context) { + furi_assert(context); + ClockSettings* app = context; + return scene_manager_handle_back_event(app->scene_manager); +} + +ClockSettings* clock_settings_alloc() { + ClockSettings* app = malloc(sizeof(ClockSettings)); + + app->gui = furi_record_open(RECORD_GUI); + + app->view_dispatcher = view_dispatcher_alloc(); + app->scene_manager = scene_manager_alloc(&clock_settings_scene_handlers, app); + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + + view_dispatcher_set_custom_event_callback( + app->view_dispatcher, clock_settings_custom_event_callback); + view_dispatcher_set_navigation_event_callback( + app->view_dispatcher, clock_settings_back_event_callback); + + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); + + app->pwm_view = + clock_settings_module_alloc(view_dispatcher_get_event_loop(app->view_dispatcher)); + view_dispatcher_add_view( + app->view_dispatcher, ClockSettingsViewPwm, clock_settings_module_get_view(app->pwm_view)); + + scene_manager_next_scene(app->scene_manager, ClockSettingsSceneStart); + + return app; +} + +void clock_settings_free(ClockSettings* app) { + furi_assert(app); + + // Views + view_dispatcher_remove_view(app->view_dispatcher, ClockSettingsViewPwm); + + clock_settings_module_free(app->pwm_view); + + // View dispatcher + view_dispatcher_free(app->view_dispatcher); + scene_manager_free(app->scene_manager); + + // Close records + furi_record_close(RECORD_GUI); + + free(app); +} + +int32_t clock_settings(void* p) { + UNUSED(p); + ClockSettings* clock_settings = clock_settings_alloc(); + + view_dispatcher_run(clock_settings->view_dispatcher); + + clock_settings_free(clock_settings); + + return 0; +} diff --git a/applications/settings/clock_settings/clock_settings.h b/applications/settings/clock_settings/clock_settings.h new file mode 100644 index 000000000..db404ec8e --- /dev/null +++ b/applications/settings/clock_settings/clock_settings.h @@ -0,0 +1,31 @@ +#pragma once + +#include "scenes/clock_settings_scene.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include "views/clock_settings_module.h" + +typedef struct ClockSettings ClockSettings; + +struct ClockSettings { + Gui* gui; + ViewDispatcher* view_dispatcher; + SceneManager* scene_manager; + ClockSettingsModule* pwm_view; +}; + +typedef enum { + ClockSettingsViewPwm, +} ClockSettingsView; + +typedef enum { + ClockSettingsCustomEventNone, +} ClockSettingsCustomEvent; diff --git a/applications/settings/clock_settings/clock_settings_alarm.c b/applications/settings/clock_settings/clock_settings_alarm.c new file mode 100644 index 000000000..7b096ef70 --- /dev/null +++ b/applications/settings/clock_settings/clock_settings_alarm.c @@ -0,0 +1,177 @@ +#include +#include + +#include +#include + +#include +#include + +#include + +#define TAG "ClockSettingsAlarm" + +typedef struct { + DateTime now; + IconAnimation* icon; +} ClockSettingsAlramModel; + +const NotificationSequence sequence_alarm = { + &message_force_speaker_volume_setting_1f, + &message_force_vibro_setting_on, + &message_force_display_brightness_setting_1f, + &message_vibro_on, + + &message_display_backlight_on, + &message_note_c7, + &message_delay_250, + + &message_display_backlight_off, + &message_note_c4, + &message_delay_250, + + &message_display_backlight_on, + &message_note_c7, + &message_delay_250, + + &message_display_backlight_off, + &message_note_c4, + &message_delay_250, + + &message_sound_off, + &message_vibro_off, + NULL, +}; + +static void clock_settings_alarm_draw_callback(Canvas* canvas, void* ctx) { + ClockSettingsAlramModel* model = ctx; + char buffer[64] = {}; + + canvas_draw_icon_animation(canvas, 5, 6, model->icon); + + canvas_set_font(canvas, FontBigNumbers); + snprintf(buffer, sizeof(buffer), "%02u:%02u", model->now.hour, model->now.minute); + canvas_draw_str(canvas, 58, 32, buffer); + + canvas_set_font(canvas, FontPrimary); + snprintf( + buffer, + sizeof(buffer), + "%02u.%02u.%04u", + model->now.day, + model->now.month, + model->now.year); + canvas_draw_str(canvas, 60, 44, buffer); +} + +static void clock_settings_alarm_input_callback(InputEvent* input_event, void* ctx) { + furi_assert(ctx); + FuriMessageQueue* event_queue = ctx; + furi_message_queue_put(event_queue, input_event, FuriWaitForever); +} + +void clock_settings_alarm_animation_callback(IconAnimation* instance, void* context) { + UNUSED(instance); + ViewPort* view_port = context; + view_port_update(view_port); +} + +int32_t clock_settings_alarm(void* p) { + UNUSED(p); + + // View Model + ClockSettingsAlramModel model; + + furi_hal_rtc_get_datetime(&model.now); + model.icon = icon_animation_alloc(&A_Alarm_47x39); + + // Alloc message queue + FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); + + // Configure view port + ViewPort* view_port = view_port_alloc(); + view_port_draw_callback_set(view_port, clock_settings_alarm_draw_callback, &model); + view_port_input_callback_set(view_port, clock_settings_alarm_input_callback, event_queue); + + // Register view port in GUI + Gui* gui = furi_record_open(RECORD_GUI); + gui_add_view_port(gui, view_port, GuiLayerFullscreen); + + NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION); + notification_message(notification, &sequence_alarm); + + icon_animation_set_update_callback( + model.icon, clock_settings_alarm_animation_callback, view_port); + icon_animation_start(model.icon); + + // Process events + InputEvent event; + bool running = true; + while(running) { + if(furi_message_queue_get(event_queue, &event, 2000) == FuriStatusOk) { + if(event.type == InputTypePress) { + running = false; + } + } else { + notification_message(notification, &sequence_alarm); + furi_hal_rtc_get_datetime(&model.now); + view_port_update(view_port); + } + } + + icon_animation_stop(model.icon); + + notification_message_block(notification, &sequence_empty); + furi_record_close(RECORD_NOTIFICATION); + + view_port_enabled_set(view_port, false); + gui_remove_view_port(gui, view_port); + view_port_free(view_port); + furi_message_queue_free(event_queue); + furi_record_close(RECORD_GUI); + + icon_animation_free(model.icon); + + return 0; +} + +FuriThread* clock_settings_alarm_thread = NULL; + +static void clock_settings_alarm_thread_state_callback( + FuriThread* thread, + FuriThreadState state, + void* context) { + furi_assert(clock_settings_alarm_thread == thread); + UNUSED(context); + + if(state == FuriThreadStateStopped) { + furi_thread_free(thread); + clock_settings_alarm_thread = NULL; + } +} + +static void clock_settings_alarm_start(void* context, uint32_t arg) { + UNUSED(context); + UNUSED(arg); + + FURI_LOG_I(TAG, "spawning alarm thread"); + + if(clock_settings_alarm_thread) return; + + clock_settings_alarm_thread = + furi_thread_alloc_ex("ClockAlarm", 1024, clock_settings_alarm, NULL); + furi_thread_set_state_callback( + clock_settings_alarm_thread, clock_settings_alarm_thread_state_callback); + furi_thread_start(clock_settings_alarm_thread); +} + +static void clock_settings_alarm_isr(void* context) { + UNUSED(context); + furi_timer_pending_callback(clock_settings_alarm_start, NULL, 0); +} + +void clock_settings_start(void) { +#ifndef FURI_RAM_EXEC + furi_hal_rtc_set_alarm_callback(clock_settings_alarm_isr, NULL); +#endif +} diff --git a/applications/settings/clock_settings/scenes/clock_settings_scene.c b/applications/settings/clock_settings/scenes/clock_settings_scene.c new file mode 100644 index 000000000..13a1c3395 --- /dev/null +++ b/applications/settings/clock_settings/scenes/clock_settings_scene.c @@ -0,0 +1,30 @@ +#include "../clock_settings.h" + +// Generate scene on_enter handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter, +void (*const clock_settings_scene_on_enter_handlers[])(void*) = { +#include "clock_settings_scene_config.h" +}; +#undef ADD_SCENE + +// Generate scene on_event handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event, +bool (*const clock_settings_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = { +#include "clock_settings_scene_config.h" +}; +#undef ADD_SCENE + +// Generate scene on_exit handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit, +void (*const clock_settings_scene_on_exit_handlers[])(void* context) = { +#include "clock_settings_scene_config.h" +}; +#undef ADD_SCENE + +// Initialize scene handlers configuration structure +const SceneManagerHandlers clock_settings_scene_handlers = { + .on_enter_handlers = clock_settings_scene_on_enter_handlers, + .on_event_handlers = clock_settings_scene_on_event_handlers, + .on_exit_handlers = clock_settings_scene_on_exit_handlers, + .scene_num = ClockSettingsSceneNum, +}; diff --git a/applications/settings/clock_settings/scenes/clock_settings_scene.h b/applications/settings/clock_settings/scenes/clock_settings_scene.h new file mode 100644 index 000000000..d0582252c --- /dev/null +++ b/applications/settings/clock_settings/scenes/clock_settings_scene.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +// Generate scene id and total number +#define ADD_SCENE(prefix, name, id) ClockSettingsScene##id, +typedef enum { +#include "clock_settings_scene_config.h" + ClockSettingsSceneNum, +} ClockSettingsScene; +#undef ADD_SCENE + +extern const SceneManagerHandlers clock_settings_scene_handlers; + +// Generate scene on_enter handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*); +#include "clock_settings_scene_config.h" +#undef ADD_SCENE + +// Generate scene on_event handlers declaration +#define ADD_SCENE(prefix, name, id) \ + bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event); +#include "clock_settings_scene_config.h" +#undef ADD_SCENE + +// Generate scene on_exit handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context); +#include "clock_settings_scene_config.h" +#undef ADD_SCENE diff --git a/applications/settings/clock_settings/scenes/clock_settings_scene_config.h b/applications/settings/clock_settings/scenes/clock_settings_scene_config.h new file mode 100644 index 000000000..496b3f7af --- /dev/null +++ b/applications/settings/clock_settings/scenes/clock_settings_scene_config.h @@ -0,0 +1 @@ +ADD_SCENE(clock_settings, start, Start) diff --git a/applications/settings/clock_settings/scenes/clock_settings_scene_start.c b/applications/settings/clock_settings/scenes/clock_settings_scene_start.c new file mode 100644 index 000000000..81cf58a74 --- /dev/null +++ b/applications/settings/clock_settings/scenes/clock_settings_scene_start.c @@ -0,0 +1,32 @@ +#include "../clock_settings.h" +#include + +#define TAG "SceneStart" + +typedef enum { + SubmenuIndexPwm, + SubmenuIndexClockOutput, +} SubmenuIndex; + +void clock_settings_scene_start_submenu_callback(void* context, uint32_t index) { + ClockSettings* app = context; + + view_dispatcher_send_custom_event(app->view_dispatcher, index); +} + +void clock_settings_scene_start_on_enter(void* context) { + ClockSettings* app = context; + + view_dispatcher_switch_to_view(app->view_dispatcher, ClockSettingsViewPwm); +} + +bool clock_settings_scene_start_on_event(void* context, SceneManagerEvent event) { + UNUSED(context); + UNUSED(event); + + return false; +} + +void clock_settings_scene_start_on_exit(void* context) { + UNUSED(context); +} diff --git a/applications/settings/clock_settings/views/clock_settings_module.c b/applications/settings/clock_settings/views/clock_settings_module.c new file mode 100644 index 000000000..9ab5773a5 --- /dev/null +++ b/applications/settings/clock_settings/views/clock_settings_module.c @@ -0,0 +1,438 @@ +#include "clock_settings_module.h" + +#include +#include +#include + +#define TAG "ClockSettingsModule" + +struct ClockSettingsModule { + FuriEventLoopTimer* timer; + View* view; +}; + +typedef struct { + DateTime current; + DateTime alarm; + bool alarm_enabled; + bool editing; + + uint8_t row; + uint8_t column; +} ClockSettingsModuleViewModel; + +typedef enum { + EditStateNone, + EditStateActive, + EditStateActiveEditing, +} EditState; + +#define get_state(m, r, c) \ + ((m)->row == (r) && (m)->column == (c) ? \ + ((m)->editing ? EditStateActiveEditing : EditStateActive) : \ + EditStateNone) + +#define ROW_0_Y (4) +#define ROW_0_H (20) + +#define ROW_1_Y (30) +#define ROW_1_H (12) + +#define ROW_2_Y (48) +#define ROW_2_H (12) + +#define ROW_COUNT 3 +#define COLUMN_COUNT 3 + +static inline void clock_settings_module_cleanup_date(DateTime* dt) { + uint8_t day_per_month = + datetime_get_days_per_month(datetime_is_leap_year(dt->year), dt->month); + if(dt->day > day_per_month) { + dt->day = day_per_month; + } +} + +static inline void clock_settings_module_draw_block( + Canvas* canvas, + int32_t x, + int32_t y, + size_t w, + size_t h, + Font font, + EditState state, + const char* text) { + canvas_set_color(canvas, ColorBlack); + if(state != EditStateNone) { + if(state == EditStateActiveEditing) { + canvas_draw_icon(canvas, x + w / 2 - 2, y - 1 - 3, &I_SmallArrowUp_3x5); + canvas_draw_icon(canvas, x + w / 2 - 2, y + h + 1, &I_SmallArrowDown_3x5); + } + canvas_draw_rbox(canvas, x, y, w, h, 1); + canvas_set_color(canvas, ColorWhite); + } else { + canvas_draw_rframe(canvas, x, y, w, h, 1); + } + + canvas_set_font(canvas, font); + canvas_draw_str_aligned(canvas, x + w / 2, y + h / 2, AlignCenter, AlignCenter, text); + if(state != EditStateNone) { + canvas_set_color(canvas, ColorBlack); + } +} + +static void + clock_settings_module_draw_time_callback(Canvas* canvas, ClockSettingsModuleViewModel* model) { + char buffer[64]; + + canvas_set_font(canvas, FontPrimary); + canvas_draw_str(canvas, 0, ROW_0_Y + 15, "Time"); + + snprintf(buffer, sizeof(buffer), "%02u", model->current.hour); + clock_settings_module_draw_block( + canvas, 32, ROW_0_Y, 28, ROW_0_H, FontBigNumbers, get_state(model, 0, 0), buffer); + canvas_draw_box(canvas, 62, ROW_0_Y + ROW_0_H - 7, 2, 2); + canvas_draw_box(canvas, 62, ROW_0_Y + ROW_0_H - 7 - 6, 2, 2); + + snprintf(buffer, sizeof(buffer), "%02u", model->current.minute); + clock_settings_module_draw_block( + canvas, 66, ROW_0_Y, 28, ROW_0_H, FontBigNumbers, get_state(model, 0, 1), buffer); + canvas_draw_box(canvas, 96, ROW_0_Y + ROW_0_H - 7, 2, 2); + canvas_draw_box(canvas, 96, ROW_0_Y + ROW_0_H - 7 - 6, 2, 2); + + snprintf(buffer, sizeof(buffer), "%02u", model->current.second); + clock_settings_module_draw_block( + canvas, 100, ROW_0_Y, 28, ROW_0_H, FontBigNumbers, get_state(model, 0, 2), buffer); +} + +static void + clock_settings_module_draw_date_callback(Canvas* canvas, ClockSettingsModuleViewModel* model) { + char buffer[64]; + + canvas_set_font(canvas, FontPrimary); + canvas_draw_str(canvas, 0, ROW_1_Y + 9, "Date"); + // Day + snprintf(buffer, sizeof(buffer), "%02u", model->current.day); + clock_settings_module_draw_block( + canvas, 44, ROW_1_Y, 17, ROW_1_H, FontPrimary, get_state(model, 1, 0), buffer); + canvas_draw_box(canvas, 71 - 6, ROW_1_Y + ROW_1_H - 4, 2, 2); + // Month + snprintf(buffer, sizeof(buffer), "%02u", model->current.month); + clock_settings_module_draw_block( + canvas, 71, ROW_1_Y, 17, ROW_1_H, FontPrimary, get_state(model, 1, 1), buffer); + canvas_draw_box(canvas, 98 - 6, ROW_1_Y + ROW_1_H - 4, 2, 2); + // Year + snprintf(buffer, sizeof(buffer), "%04u", model->current.year); + clock_settings_module_draw_block( + canvas, 98, ROW_1_Y, 30, ROW_1_H, FontPrimary, get_state(model, 1, 2), buffer); +} + +static void + clock_settings_module_draw_alarm_callback(Canvas* canvas, ClockSettingsModuleViewModel* model) { + char buffer[64]; + + canvas_set_font(canvas, FontPrimary); + canvas_draw_str(canvas, 0, ROW_2_Y + 9, "Alarm"); + + snprintf(buffer, sizeof(buffer), "%02u", model->alarm.hour); + clock_settings_module_draw_block( + canvas, 58, ROW_2_Y, 17, ROW_2_H, FontPrimary, get_state(model, 2, 0), buffer); + canvas_draw_box(canvas, 81 - 4, ROW_2_Y + ROW_2_H - 4, 2, 2); + canvas_draw_box(canvas, 81 - 4, ROW_2_Y + ROW_2_H - 4 - 4, 2, 2); + + snprintf(buffer, sizeof(buffer), "%02u", model->alarm.minute); + clock_settings_module_draw_block( + canvas, 81, ROW_2_Y, 17, ROW_2_H, FontPrimary, get_state(model, 2, 1), buffer); + + clock_settings_module_draw_block( + canvas, + 106, + ROW_2_Y, + 22, + ROW_2_H, + FontPrimary, + get_state(model, 2, 2), + model->alarm_enabled ? "On" : "Off"); +} + +static void clock_settings_module_draw_callback(Canvas* canvas, void* _model) { + ClockSettingsModuleViewModel* model = _model; + clock_settings_module_draw_time_callback(canvas, model); + clock_settings_module_draw_date_callback(canvas, model); + clock_settings_module_draw_alarm_callback(canvas, model); +} + +static bool clock_settings_module_input_navigation_callback( + InputEvent* event, + ClockSettingsModuleViewModel* model) { + if(event->key == InputKeyUp) { + if(model->row > 0) model->row--; + } else if(event->key == InputKeyDown) { + if(model->row < ROW_COUNT - 1) model->row++; + } else if(event->key == InputKeyOk) { + model->editing = !model->editing; + } else if(event->key == InputKeyRight) { + if(model->column < COLUMN_COUNT - 1) model->column++; + } else if(event->key == InputKeyLeft) { + if(model->column > 0) model->column--; + } else if(event->key == InputKeyBack && model->editing) { + model->editing = false; + } else { + return false; + } + + return true; +} + +static bool clock_settings_module_input_time_callback( + InputEvent* event, + ClockSettingsModuleViewModel* model) { + if(event->key == InputKeyUp) { + if(model->column == 0) { + model->current.hour++; + model->current.hour = model->current.hour % 24; + } else if(model->column == 1) { + model->current.minute++; + model->current.minute = model->current.minute % 60; + } else if(model->column == 2) { + model->current.second++; + model->current.second = model->current.second % 60; + } else { + furi_crash(); + } + } else if(event->key == InputKeyDown) { + if(model->column == 0) { + if(model->current.hour > 0) { + model->current.hour--; + } else { + model->current.hour = 23; + } + model->current.hour = model->current.hour % 24; + } else if(model->column == 1) { + if(model->current.minute > 0) { + model->current.minute--; + } else { + model->current.minute = 59; + } + model->current.minute = model->current.minute % 60; + } else if(model->column == 2) { + if(model->current.second > 0) { + model->current.second--; + } else { + model->current.second = 59; + } + model->current.second = model->current.second % 60; + } else { + furi_crash(); + } + } else { + return clock_settings_module_input_navigation_callback(event, model); + } + + return true; +} + +static bool clock_settings_module_input_date_callback( + InputEvent* event, + ClockSettingsModuleViewModel* model) { + if(event->key == InputKeyUp) { + if(model->column == 0) { + if(model->current.day < 31) model->current.day++; + } else if(model->column == 1) { + if(model->current.month < 12) { + model->current.month++; + } + } else if(model->column == 2) { + if(model->current.year < 2099) { + model->current.year++; + } + } else { + furi_crash(); + } + } else if(event->key == InputKeyDown) { + if(model->column == 0) { + if(model->current.day > 1) { + model->current.day--; + } + } else if(model->column == 1) { + if(model->current.month > 1) { + model->current.month--; + } + } else if(model->column == 2) { + if(model->current.year > 2000) { + model->current.year--; + } + } else { + furi_crash(); + } + } else { + return clock_settings_module_input_navigation_callback(event, model); + } + + clock_settings_module_cleanup_date(&model->current); + + return true; +} + +static bool clock_settings_module_input_alarm_callback( + InputEvent* event, + ClockSettingsModuleViewModel* model) { + if(event->key == InputKeyUp) { + if(model->column == 0) { + model->alarm.hour++; + model->alarm.hour = model->alarm.hour % 24; + } else if(model->column == 1) { + model->alarm.minute++; + model->alarm.minute = model->alarm.minute % 60; + } else if(model->column == 2) { + model->alarm_enabled = !model->alarm_enabled; + } else { + furi_crash(); + } + } else if(event->key == InputKeyDown) { + if(model->column == 0) { + if(model->alarm.hour > 0) { + model->alarm.hour--; + } else { + model->alarm.hour = 23; + } + model->alarm.hour = model->alarm.hour % 24; + } else if(model->column == 1) { + if(model->alarm.minute > 0) { + model->alarm.minute--; + } else { + model->alarm.minute = 59; + } + model->alarm.minute = model->alarm.minute % 60; + } else if(model->column == 2) { + model->alarm_enabled = !model->alarm_enabled; + } else { + furi_crash(); + } + } else { + return clock_settings_module_input_navigation_callback(event, model); + } + + return true; +} + +static bool clock_settings_module_input_callback(InputEvent* event, void* context) { + furi_assert(context); + + ClockSettingsModule* instance = context; + bool consumed = false; + + with_view_model( + instance->view, + ClockSettingsModuleViewModel * model, + { + if(event->type == InputTypeShort || event->type == InputTypeRepeat) { + bool previous_editing = model->editing; + if(model->editing) { + if(model->row == 0) { + consumed = clock_settings_module_input_time_callback(event, model); + } else if(model->row == 1) { + consumed = clock_settings_module_input_date_callback(event, model); + } else if(model->row == 2) { + consumed = clock_settings_module_input_alarm_callback(event, model); + } else { + furi_crash(); + } + } else { + consumed = clock_settings_module_input_navigation_callback(event, model); + } + + // Switching between navigate/edit + if(model->editing != previous_editing) { + if(model->row == 2) { + if(!model->editing) { + // Disable alarm + furi_hal_rtc_set_alarm(NULL, false); + // Set new alarm + furi_hal_rtc_set_alarm(&model->alarm, model->alarm_enabled); + // Confirm + model->alarm_enabled = furi_hal_rtc_get_alarm(&model->alarm); + } + } else { + if(model->editing) { + // stop timer to prevent mess with current date time + furi_event_loop_timer_stop(instance->timer); + } else { + // save date time and restart timer + furi_hal_rtc_set_datetime(&model->current); + furi_event_loop_timer_start(instance->timer, 1000); + } + } + } + } + }, + true); + + return consumed; +} + +static void clock_settings_module_timer_callback(void* context) { + furi_assert(context); + ClockSettingsModule* instance = context; + + DateTime dt; + furi_hal_rtc_get_datetime(&dt); + with_view_model( + instance->view, ClockSettingsModuleViewModel * model, { model->current = dt; }, true); +} + +static void clock_settings_module_view_enter_callback(void* context) { + furi_assert(context); + ClockSettingsModule* instance = context; + + clock_settings_module_timer_callback(context); + + DateTime alarm; + bool enabled = furi_hal_rtc_get_alarm(&alarm); + + with_view_model( + instance->view, + ClockSettingsModuleViewModel * model, + { + model->alarm = alarm; + model->alarm_enabled = enabled; + }, + true); + + furi_event_loop_timer_start(instance->timer, 1000); +} + +static void clock_settings_module_view_exit_callback(void* context) { + furi_assert(context); + ClockSettingsModule* instance = context; + furi_event_loop_timer_stop(instance->timer); +} + +ClockSettingsModule* clock_settings_module_alloc(FuriEventLoop* event_loop) { + ClockSettingsModule* instance = malloc(sizeof(ClockSettingsModule)); + + instance->timer = furi_event_loop_timer_alloc( + event_loop, clock_settings_module_timer_callback, FuriEventLoopTimerTypePeriodic, instance); + instance->view = view_alloc(); + view_set_enter_callback(instance->view, clock_settings_module_view_enter_callback); + view_set_exit_callback(instance->view, clock_settings_module_view_exit_callback); + view_allocate_model( + instance->view, ViewModelTypeLocking, sizeof(ClockSettingsModuleViewModel)); + with_view_model( + instance->view, ClockSettingsModuleViewModel * model, { model->row = 0; }, false); + view_set_context(instance->view, instance); + view_set_draw_callback(instance->view, clock_settings_module_draw_callback); + view_set_input_callback(instance->view, clock_settings_module_input_callback); + + return instance; +} + +void clock_settings_module_free(ClockSettingsModule* instance) { + furi_assert(instance); + view_free(instance->view); + free(instance); +} + +View* clock_settings_module_get_view(ClockSettingsModule* instance) { + furi_assert(instance); + return instance->view; +} diff --git a/applications/settings/clock_settings/views/clock_settings_module.h b/applications/settings/clock_settings/views/clock_settings_module.h new file mode 100644 index 000000000..1ae1dee0e --- /dev/null +++ b/applications/settings/clock_settings/views/clock_settings_module.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +typedef struct ClockSettingsModule ClockSettingsModule; +typedef void (*ClockSettingsModuleViewCallback)( + uint8_t channel_id, + uint32_t freq, + uint8_t duty, + void* context); + +ClockSettingsModule* clock_settings_module_alloc(FuriEventLoop* event_loop); + +void clock_settings_module_free(ClockSettingsModule* instance); + +View* clock_settings_module_get_view(ClockSettingsModule* instance); + +void clock_settings_module_set( + ClockSettingsModule* instance, + const DateTime* datetime, + bool enabled); + +bool clock_settings_module_get(ClockSettingsModule* instance, DateTime* datetime); diff --git a/applications/system/application.fam b/applications/system/application.fam index 9b3bbcd84..e17bf3b7d 100644 --- a/applications/system/application.fam +++ b/applications/system/application.fam @@ -5,7 +5,6 @@ App( provides=[ "updater_app", "js_app", - "js_app_start", "findmy_startup", # "archive", ], diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index 38278212e..f7db213be 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -2,31 +2,38 @@ App( appid="js_app", name="JS Runner", apptype=FlipperAppType.SYSTEM, - entry_point="js_app", # cdefines=["JS_RUNNER_FAP"], - # Sources separation breaks linking when internal, comment as needed - # sources=[ - # "*.c*", - # "!modules", - # "modules/js_flipper.c", - # ], + fap_icon="icon.png", + fap_category="assets", + entry_point="js_app", stack_size=2 * 1024, resources="examples", order=0, - fap_icon="icon.png", - fap_category="assets", + provides=["js_app_start"], + sources=[ + "js_app.c", + "js_modules.c", + "js_thread.c", + "plugin_api/app_api_table.cpp", + "views/console_view.c", + "modules/js_flipper.c", + "modules/js_tests.c", + ], ) App( appid="js_cli", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="js_cli_plugin_ep", + entry_point="js_cli_execute_plugin_ep", requires=["cli"], sources=[ - "*.c*", - "!modules", + "js_app.c", + "js_modules.c", + "js_thread.c", + "plugin_api/app_api_table.cpp", "modules/js_flipper.c", + "modules/js_tests.c", ], ) @@ -34,8 +41,8 @@ App( appid="js_app_start", apptype=FlipperAppType.STARTUP, entry_point="js_app_on_system_start", - # sources=["js_start.c"], order=160, + sources=["js_app.c"], ) App( @@ -119,6 +126,7 @@ App( entry_point="js_gui_file_picker_ep", requires=["js_app"], sources=["modules/js_gui/file_picker.c"], + fap_libs=[], ) App( diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/blebeacon.js b/applications/system/js_app/examples/apps/Scripts/Examples/blebeacon.js index 1187f1c20..c1a6c1d16 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/blebeacon.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/blebeacon.js @@ -1,3 +1,6 @@ +// Script cannot work without blebeacon module so check before +checkSdkFeatures(["blebeacon"]); + let blebeacon = require("blebeacon"); // Stop if previous background beacon is active diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/gpio.js b/applications/system/js_app/examples/apps/Scripts/Examples/gpio.js index f3b4bc121..24d0f0286 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/gpio.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/gpio.js @@ -19,7 +19,7 @@ eventLoop.subscribe(eventLoop.timer("periodic", 1000), function (_, _item, led, // 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"); + print("PC0 is at", pot.readAnalog(), "mV"); }, pot); // the program will just exit unless this is here diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/gui.js b/applications/system/js_app/examples/apps/Scripts/Examples/gui.js index a1e023853..110b388b8 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/gui.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/gui.js @@ -45,6 +45,12 @@ let views = { }), }; +// Enable illegal filename symbols since we're not choosing filenames, gives more flexibility +// Not available in all firmwares, good idea to check if it is supported +if (doesSdkSupport(["gui-textinput-illegalsymbols"])) { + views.keyboard.set("illegalSymbols", true); +} + // demo selector eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, views) { if (index === 0) { diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/i2c.js b/applications/system/js_app/examples/apps/Scripts/Examples/i2c.js index 456b44ccd..5c71cfe4e 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/i2c.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/i2c.js @@ -1,3 +1,6 @@ +// Script cannot work without i2c module so check before +checkSdkFeatures(["i2c"]); + // Connect an 24C32N EEPROM to the I2C bus of the board. SDA=pin 15, SCL=pin 16, VCC=pin 9, GND=pin 8. let i2c = require("i2c"); diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/spi.js b/applications/system/js_app/examples/apps/Scripts/Examples/spi.js index 810637b3b..225b7b139 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/spi.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/spi.js @@ -1,3 +1,6 @@ +// Script cannot work without spi module so check before +checkSdkFeatures(["spi"]); + // Connect a w25q32 SPI device to the Flipper Zero. // D1=pin 2 (MOSI), SLK=pin 5 (SCK), GND=pin 8 (GND), D0=pin 3 (MISO), CS=pin 4 (CS), VCC=pin 9 (3V3) let spi = require("spi"); @@ -5,8 +8,9 @@ let spi = require("spi"); // Display textbox so user can scroll to see all output. let eventLoop = require("event_loop"); let gui = require("gui"); +let textBoxView = require("gui/text_box"); let text = "SPI demo\n"; -let textBox = require("gui/text_box").makeWith({ +let textBox = textBoxView.makeWith({ focus: "end", font: "text", text: text, diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/storage.js b/applications/system/js_app/examples/apps/Scripts/Examples/storage.js index c0ec8bfa4..c846d673c 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/storage.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/storage.js @@ -26,4 +26,16 @@ storage.remove(path); print("Done") // 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 \ No newline at end of file +// There's also many more functions and options, check type definitions in firmware repo + +// There is also virtual API, which is not available in all firmwares +// Allows you to mount disk images (like mass storage) and read/modify their contents +// If you want to use this as an optional feature, not essential to your script: +// if (doesSdkSupport(["storage-virtual"])) { +// storage.virtualInit("/ext/disk_image.img"); +// storage.virtualMount(); +// // Read/modify data from /mnt filesystem +// storage.virtualQuit(); +// } +// If instead virtual API is essential to your script, put this near beginning of script: +// checkSdkFeatures(["storage-virtual"]); \ No newline at end of file diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/subghz.js b/applications/system/js_app/examples/apps/Scripts/Examples/subghz.js index b5e54bac6..d53635b50 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/subghz.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/subghz.js @@ -1,3 +1,6 @@ +// Script cannot work without subghz module so check before +checkSdkFeatures(["subghz"]); + let subghz = require("subghz"); subghz.setup(); diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/usbdisk.js b/applications/system/js_app/examples/apps/Scripts/Examples/usbdisk.js index 87f3224dc..8bf78dde6 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/usbdisk.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/usbdisk.js @@ -1,12 +1,39 @@ +// Script cannot work without usbdisk module so check before +checkSdkFeatures(["usbdisk"]); + let usbdisk = require("usbdisk"); -// print("Creating image..."); -// usbdisk.createImage("/ext/apps_data/mass_storage/128MB.img", 128 * 1024 * 1024); +let storage = require("storage"); + +let imagePath = "/ext/apps_data/mass_storage/128MB.img"; +let imageSize = 128 * 1024 * 1024; + +let imageExisted = storage.fileExists(imagePath); +if (imageExisted) { + print("Disk image '128MB' already exists"); +} else { + // CreateImage isn't necessary to overall function, check when its used not at script start + if (doesSdkSupport(["usbdisk-createimage"])) { + print("Creating disk image '128MB'..."); + usbdisk.createImage(imagePath, imageSize); + } else { + die("Disk image '128MB' not present, can't auto-create"); + } +} + print("Starting UsbDisk..."); usbdisk.start("/ext/apps_data/mass_storage/128MB.img"); + print("Started, waiting until ejected..."); while (!usbdisk.wasEjected()) { delay(1000); } + print("Ejected, stopping UsbDisk..."); usbdisk.stop(); + +if (!imageExisted) { + print("Removing disk image..."); + storage.remove(imagePath); +} + print("Done"); \ No newline at end of file diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/widget.js b/applications/system/js_app/examples/apps/Scripts/Examples/widget.js index 8091f356e..be094d80e 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/widget.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/widget.js @@ -1,17 +1,20 @@ +// Script cannot work without widget module so check before +checkSdkFeatures(["widget"]); + let widget = require("widget"); let demo_seconds = 30; -print("Loading file", __filepath); -print("From directory", __dirpath); +print("Loading file", __filename); +print("From directory", __dirname); // addText supports "Primary" and "Secondary" font sizes. widget.addText(10, 10, "Primary", "Example JS widget"); widget.addText(10, 20, "Secondary", "Example widget from JS!"); // load a Xbm file from the same directory as this script. -widget.addText(0, 30, "Secondary", __filepath); -let logo = widget.loadImageXbm(__dirpath + "/widget-js.fxbm"); +widget.addText(0, 30, "Secondary", __filename); +let logo = widget.loadImageXbm(__dirname + "/widget-js.fxbm"); // add a line (x1, y1, x2, y2) widget.addLine(10, 35, 120, 35); @@ -34,8 +37,12 @@ widget.addDot(102, 44); widget.addDot(104, 43); // add an icon (x, y, icon) -widget.addIcon(100, 50, "ButtonUp_7x4"); -widget.addIcon(100, 55, "ButtonDown_7x4"); +// not available in all firmwares, but not essential for this script's +// functionality, so we just check at runtime and use it if it is available +if (doesSdkSupport(["widget-addicon"])) { + widget.addIcon(100, 50, "ButtonUp_7x4"); + widget.addIcon(100, 55, "ButtonDown_7x4"); +} // add a glyph (x, y, glyph) widget.addGlyph(115, 50, "#".charCodeAt(0)); diff --git a/applications/system/js_app/examples/apps/Scripts/interactive.js b/applications/system/js_app/examples/apps/Scripts/interactive.js index 40ca98c30..7cb0965bd 100644 --- a/applications/system/js_app/examples/apps/Scripts/interactive.js +++ b/applications/system/js_app/examples/apps/Scripts/interactive.js @@ -32,6 +32,12 @@ let views = { loading: loading.make(), }; +// Enable illegal filename symbols since we're not choosing filenames, gives more flexibility +// Not available in all firmwares, good idea to check if it is supported +if (doesSdkSupport(["gui-textinput-illegalsymbols"])) { + views.textInput.set("illegalSymbols", true); +} + eventLoop.subscribe(views.dialog.input, function (_sub, button, gui, views) { if (button === "center") { gui.viewDispatcher.switchTo(views.textInput); diff --git a/applications/system/js_app/js_app.c b/applications/system/js_app/js_app.c index 27084b3b5..572c32262 100644 --- a/applications/system/js_app/js_app.c +++ b/applications/system/js_app/js_app.c @@ -204,15 +204,13 @@ void js_cli_execute(Cli* cli, FuriString* args, void* context) { furi_record_close(RECORD_STORAGE); } -#include #include +CLI_PLUGIN_WRAPPER("js", js_cli_execute) -static const FlipperAppPluginDescriptor plugin_descriptor = { - .appid = CLI_PLUGIN_APP_ID, - .ep_api_version = CLI_PLUGIN_API_VERSION, - .entry_point = &js_cli_execute, -}; - -const FlipperAppPluginDescriptor* js_cli_plugin_ep(void) { - return &plugin_descriptor; +void js_app_on_system_start(void) { +#ifdef SRV_CLI + Cli* cli = furi_record_open(RECORD_CLI); + cli_add_command(cli, "js", CliCommandFlagDefault, js_cli_execute_wrapper, NULL); + furi_record_close(RECORD_CLI); +#endif } diff --git a/applications/system/js_app/js_modules.c b/applications/system/js_app/js_modules.c index 38ff46f75..6918bfaba 100644 --- a/applications/system/js_app/js_modules.c +++ b/applications/system/js_app/js_modules.c @@ -1,6 +1,8 @@ #include #include "js_modules.h" #include +#include +#include #include "modules/js_flipper.h" #ifdef FW_CFG_unit_tests @@ -76,6 +78,12 @@ JsModuleData* js_find_loaded_module(JsModules* instance, const char* name) { } mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_len) { + // Ignore the initial part of the module name + const char* optional_module_prefix = "@" JS_SDK_VENDOR "/fz-sdk/"; + if(strncmp(name, optional_module_prefix, strlen(optional_module_prefix)) == 0) { + name += strlen(optional_module_prefix); + } + // Check if module is already installed JsModuleData* module_inst = js_find_loaded_module(modules, name); if(module_inst) { //-V547 @@ -175,3 +183,148 @@ void* js_module_get(JsModules* modules, const char* name) { furi_string_free(module_name); return module_inst ? module_inst->context : NULL; } + +typedef enum { + JsSdkCompatStatusCompatible, + JsSdkCompatStatusFirmwareTooOld, + JsSdkCompatStatusFirmwareTooNew, +} JsSdkCompatStatus; + +/** + * @brief Checks compatibility between the firmware and the JS SDK version + * expected by the script + */ +static JsSdkCompatStatus + js_internal_sdk_compatibility_status(int32_t exp_major, int32_t exp_minor) { + if(exp_major < JS_SDK_MAJOR) return JsSdkCompatStatusFirmwareTooNew; + if(exp_major > JS_SDK_MAJOR || exp_minor > JS_SDK_MINOR) + return JsSdkCompatStatusFirmwareTooOld; + return JsSdkCompatStatusCompatible; +} + +#define JS_SDK_COMPAT_ARGS \ + int32_t major, minor; \ + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&major), JS_ARG_INT32(&minor)); + +void js_sdk_compatibility_status(struct mjs* mjs) { + JS_SDK_COMPAT_ARGS; + JsSdkCompatStatus status = js_internal_sdk_compatibility_status(major, minor); + switch(status) { + case JsSdkCompatStatusCompatible: + mjs_return(mjs, mjs_mk_string(mjs, "compatible", ~0, 0)); + return; + case JsSdkCompatStatusFirmwareTooOld: + mjs_return(mjs, mjs_mk_string(mjs, "firmwareTooOld", ~0, 0)); + return; + case JsSdkCompatStatusFirmwareTooNew: + mjs_return(mjs, mjs_mk_string(mjs, "firmwareTooNew", ~0, 0)); + return; + } +} + +void js_is_sdk_compatible(struct mjs* mjs) { + JS_SDK_COMPAT_ARGS; + JsSdkCompatStatus status = js_internal_sdk_compatibility_status(major, minor); + mjs_return(mjs, mjs_mk_boolean(mjs, status == JsSdkCompatStatusCompatible)); +} + +/** + * @brief Asks the user whether to continue executing an incompatible script + */ +static bool js_internal_compat_ask_user(const char* message) { + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + DialogMessage* dialog = dialog_message_alloc(); + dialog_message_set_header(dialog, message, 64, 0, AlignCenter, AlignTop); + dialog_message_set_text( + dialog, "This script may not\nwork as expected", 79, 32, AlignCenter, AlignCenter); + dialog_message_set_icon(dialog, &I_Warning_30x23, 0, 18); + dialog_message_set_buttons(dialog, "Go back", NULL, "Run anyway"); + DialogMessageButton choice = dialog_message_show(dialogs, dialog); + dialog_message_free(dialog); + furi_record_close(RECORD_DIALOGS); + return choice == DialogMessageButtonRight; +} + +void js_check_sdk_compatibility(struct mjs* mjs) { + JS_SDK_COMPAT_ARGS; + JsSdkCompatStatus status = js_internal_sdk_compatibility_status(major, minor); + if(status != JsSdkCompatStatusCompatible) { + FURI_LOG_E( + TAG, + "Script requests JS SDK %ld.%ld, firmware provides JS SDK %d.%d", + major, + minor, + JS_SDK_MAJOR, + JS_SDK_MINOR); + + const char* message = (status == JsSdkCompatStatusFirmwareTooOld) ? "Outdated Firmware" : + "Outdated Script"; + if(!js_internal_compat_ask_user(message)) { + JS_ERROR_AND_RETURN(mjs, MJS_NOT_IMPLEMENTED_ERROR, "Incompatible script"); + } + } +} + +static const char* extra_features[] = { + "baseline", // dummy "feature" + + // extra modules + "blebeacon", + "i2c", + "spi", + "subghz", + "usbdisk", + "vgm", + "widget", + + // extra features + "gui-textinput-illegalsymbols", + "storage-virtual", + "usbdisk-createimage", + "widget-addicon", +}; + +/** + * @brief Determines whether a feature is supported + */ +static bool js_internal_supports(const char* feature) { + for(size_t i = 0; i < COUNT_OF(extra_features); i++) { // -V1008 + if(strcmp(feature, extra_features[i]) == 0) return true; + } + return false; +} + +/** + * @brief Determines whether all of the requested features are supported + */ +static bool js_internal_supports_all_of(struct mjs* mjs, mjs_val_t feature_arr) { + furi_assert(mjs_is_array(feature_arr)); + + for(size_t i = 0; i < mjs_array_length(mjs, feature_arr); i++) { + mjs_val_t feature = mjs_array_get(mjs, feature_arr, i); + const char* feature_str = mjs_get_string(mjs, &feature, NULL); + if(!feature_str) return false; + + if(!js_internal_supports(feature_str)) return false; + } + + return true; +} + +void js_does_sdk_support(struct mjs* mjs) { + mjs_val_t features; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ARR(&features)); + mjs_return(mjs, mjs_mk_boolean(mjs, js_internal_supports_all_of(mjs, features))); +} + +void js_check_sdk_features(struct mjs* mjs) { + mjs_val_t features; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ARR(&features)); + if(!js_internal_supports_all_of(mjs, features)) { + FURI_LOG_E(TAG, "Script requests unsupported features"); + + if(!js_internal_compat_ask_user("Unsupported Feature")) { + JS_ERROR_AND_RETURN(mjs, MJS_NOT_IMPLEMENTED_ERROR, "Incompatible script"); + } + } +} diff --git a/applications/system/js_app/js_modules.h b/applications/system/js_app/js_modules.h index 788715872..95cfb913b 100644 --- a/applications/system/js_app/js_modules.h +++ b/applications/system/js_app/js_modules.h @@ -9,6 +9,11 @@ #define PLUGIN_APP_ID "js" #define PLUGIN_API_VERSION 1 +#define JS_SDK_VENDOR_FIRMWARE "momentum" +#define JS_SDK_VENDOR "flipperdevices" +#define JS_SDK_MAJOR 0 +#define JS_SDK_MINOR 1 + /** * @brief Returns the foreign pointer in `obj["_"]` */ @@ -275,3 +280,28 @@ mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_le * @returns Pointer to module context, NULL if the module is not instantiated */ void* js_module_get(JsModules* modules, const char* name); + +/** + * @brief `sdkCompatibilityStatus` function + */ +void js_sdk_compatibility_status(struct mjs* mjs); + +/** + * @brief `isSdkCompatible` function + */ +void js_is_sdk_compatible(struct mjs* mjs); + +/** + * @brief `checkSdkCompatibility` function + */ +void js_check_sdk_compatibility(struct mjs* mjs); + +/** + * @brief `doesSdkSupport` function + */ +void js_does_sdk_support(struct mjs* mjs); + +/** + * @brief `checkSdkFeatures` function + */ +void js_check_sdk_features(struct mjs* mjs); diff --git a/applications/system/js_app/js_start.c b/applications/system/js_app/js_start.c deleted file mode 100644 index 2328601c5..000000000 --- a/applications/system/js_app/js_start.c +++ /dev/null @@ -1,11 +0,0 @@ -#include - -static void js_cli_execute_wrapper(Cli* cli, FuriString* args, void* context) { - cli_plugin_wrapper("js", cli, args, context); -} - -void js_app_on_system_start(void) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "js", CliCommandFlagDefault, js_cli_execute_wrapper, NULL); - furi_record_close(RECORD_CLI); -} diff --git a/applications/system/js_app/js_thread.c b/applications/system/js_app/js_thread.c index 83f9e604c..600c2676e 100644 --- a/applications/system/js_app/js_thread.c +++ b/applications/system/js_app/js_thread.c @@ -244,6 +244,8 @@ static int32_t js_thread(void* arg) { struct mjs* mjs = mjs_create(worker); worker->modules = js_modules_create(mjs, worker->resolver); mjs_val_t global = mjs_get_global(mjs); + mjs_val_t console_obj = mjs_mk_object(mjs); + if(worker->path) { FuriString* dirpath = furi_string_alloc(); path_extract_dirname(furi_string_get_cstr(worker->path), dirpath); @@ -262,18 +264,28 @@ static int32_t js_thread(void* arg) { 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, "ffi_address", ~0, MJS_MK_FN(js_ffi_address)); - mjs_set(mjs, global, "require", ~0, MJS_MK_FN(js_require)); - 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)); - mjs_set(mjs, console_obj, "warn", ~0, MJS_MK_FN(js_console_warn)); - mjs_set(mjs, console_obj, "error", ~0, MJS_MK_FN(js_console_error)); - mjs_set(mjs, console_obj, "debug", ~0, MJS_MK_FN(js_console_debug)); - mjs_set(mjs, global, "console", ~0, console_obj); + JS_ASSIGN_MULTI(mjs, global) { + JS_FIELD("print", MJS_MK_FN(js_print)); + JS_FIELD("delay", MJS_MK_FN(js_delay)); + JS_FIELD("parseInt", MJS_MK_FN(js_parse_int)); + JS_FIELD("ffi_address", MJS_MK_FN(js_ffi_address)); + JS_FIELD("require", MJS_MK_FN(js_require)); + JS_FIELD("console", console_obj); + + JS_FIELD("sdkCompatibilityStatus", MJS_MK_FN(js_sdk_compatibility_status)); + JS_FIELD("isSdkCompatible", MJS_MK_FN(js_is_sdk_compatible)); + JS_FIELD("checkSdkCompatibility", MJS_MK_FN(js_check_sdk_compatibility)); + JS_FIELD("doesSdkSupport", MJS_MK_FN(js_does_sdk_support)); + JS_FIELD("checkSdkFeatures", MJS_MK_FN(js_check_sdk_features)); + } + + JS_ASSIGN_MULTI(mjs, console_obj) { + JS_FIELD("log", MJS_MK_FN(js_console_log)); + JS_FIELD("warn", MJS_MK_FN(js_console_warn)); + JS_FIELD("error", MJS_MK_FN(js_console_error)); + JS_FIELD("debug", MJS_MK_FN(js_console_debug)); + } mjs_set_ffi_resolver(mjs, js_dlsym, worker->resolver); diff --git a/applications/system/js_app/modules/js_event_loop/js_event_loop.c b/applications/system/js_app/modules/js_event_loop/js_event_loop.c index c4f0d1bee..7f45c1a0f 100644 --- a/applications/system/js_app/modules/js_event_loop/js_event_loop.c +++ b/applications/system/js_app/modules/js_event_loop/js_event_loop.c @@ -80,7 +80,7 @@ static void js_event_loop_callback_generic(void* param) { /** * @brief Handles non-timer events */ -static bool js_event_loop_callback(void* object, void* param) { +static void js_event_loop_callback(void* object, void* param) { JsEventLoopCallbackContext* context = param; if(context->transformer) { @@ -102,8 +102,6 @@ static bool js_event_loop_callback(void* object, void* param) { } js_event_loop_callback_generic(param); - - return true; } /** diff --git a/applications/system/js_app/modules/js_flipper.c b/applications/system/js_app/modules/js_flipper.c index 43c675e10..228cd0661 100644 --- a/applications/system/js_app/modules/js_flipper.c +++ b/applications/system/js_app/modules/js_flipper.c @@ -27,11 +27,19 @@ static void js_flipper_get_battery(struct mjs* mjs) { void* js_flipper_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { UNUSED(modules); + mjs_val_t sdk_vsn = mjs_mk_array(mjs); + mjs_array_push(mjs, sdk_vsn, mjs_mk_number(mjs, JS_SDK_MAJOR)); + mjs_array_push(mjs, sdk_vsn, mjs_mk_number(mjs, JS_SDK_MINOR)); + 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)); - mjs_set(mjs, flipper_obj, "getBatteryCharge", ~0, MJS_MK_FN(js_flipper_get_battery)); *object = flipper_obj; + JS_ASSIGN_MULTI(mjs, flipper_obj) { + JS_FIELD("getModel", MJS_MK_FN(js_flipper_get_model)); + JS_FIELD("getName", MJS_MK_FN(js_flipper_get_name)); + JS_FIELD("getBatteryCharge", MJS_MK_FN(js_flipper_get_battery)); + JS_FIELD("firmwareVendor", mjs_mk_string(mjs, JS_SDK_VENDOR_FIRMWARE, ~0, false)); + JS_FIELD("jsSdkVersion", sdk_vsn); + } return (void*)1; } diff --git a/applications/system/js_app/modules/js_gpio.c b/applications/system/js_app/modules/js_gpio.c index d2d65da4d..ae3fefd71 100644 --- a/applications/system/js_app/modules/js_gpio.c +++ b/applications/system/js_app/modules/js_gpio.c @@ -220,7 +220,7 @@ static void js_gpio_interrupt(struct mjs* mjs) { * let gpio = require("gpio"); * let pot = gpio.get("pc0"); * pot.init({ direction: "in", inMode: "analog" }); - * print("voltage:" pot.read_analog(), "mV"); + * print("voltage:" pot.readAnalog(), "mV"); * ``` */ static void js_gpio_read_analog(struct mjs* mjs) { @@ -273,7 +273,7 @@ static void js_gpio_get(struct mjs* mjs) { 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, "readAnalog", ~0, MJS_MK_FN(js_gpio_read_analog)); mjs_set(mjs, manager, "interrupt", ~0, MJS_MK_FN(js_gpio_interrupt)); mjs_return(mjs, manager); diff --git a/applications/system/js_app/modules/js_gui/text_input.c b/applications/system/js_app/modules/js_gui/text_input.c index 38dabb488..926f7574c 100644 --- a/applications/system/js_app/modules/js_gui/text_input.c +++ b/applications/system/js_app/modules/js_gui/text_input.c @@ -85,7 +85,7 @@ static bool default_text_assign( context->buffer = realloc(context->buffer, context->buffer_size); //-V701 } // Also trim excess previous data with strlcpy() - strlcpy(context->buffer, value.string, context->buffer_size); + strlcpy(context->buffer, value.string, context->buffer_size); //-V575 text_input_set_result_callback( input, (TextInputCallback)input_callback, @@ -115,6 +115,18 @@ static bool default_text_clear_assign( return true; } +static bool illegal_symbols_assign( + struct mjs* mjs, + TextInput* input, + JsViewPropValue value, + JsKbdContext* context) { + UNUSED(mjs); + UNUSED(context); + + text_input_show_illegal_symbols(input, value.boolean); + return true; +} + static JsKbdContext* ctx_make(struct mjs* mjs, TextInput* input, mjs_val_t view_obj) { JsKbdContext* context = malloc(sizeof(JsKbdContext)); *context = (JsKbdContext){ @@ -135,7 +147,6 @@ static JsKbdContext* ctx_make(struct mjs* mjs, TextInput* input, mjs_val_t view_ .transformer_context = context, }, }; - text_input_show_illegal_symbols(input, true); // Temporary until prop is available text_input_set_result_callback( input, (TextInputCallback)input_callback, @@ -162,7 +173,7 @@ static const JsViewDescriptor view_descriptor = { .get_view = (JsViewGetView)text_input_get_view, .custom_make = (JsViewCustomMake)ctx_make, .custom_destroy = (JsViewCustomDestroy)ctx_destroy, - .prop_cnt = 5, + .prop_cnt = 6, .props = { (JsViewPropDescriptor){ .name = "header", @@ -184,6 +195,10 @@ static const JsViewDescriptor view_descriptor = { .name = "defaultTextClear", .type = JsViewPropTypeBool, .assign = (JsViewPropAssign)default_text_clear_assign}, + (JsViewPropDescriptor){ + .name = "illegalSymbols", + .type = JsViewPropTypeBool, + .assign = (JsViewPropAssign)illegal_symbols_assign}, }}; JS_GUI_VIEW_DEF(text_input, &view_descriptor); diff --git a/applications/system/js_app/modules/js_subghz/js_subghz.c b/applications/system/js_app/modules/js_subghz/js_subghz.c index 76c678a7c..0e6accfa0 100644 --- a/applications/system/js_app/modules/js_subghz/js_subghz.c +++ b/applications/system/js_app/modules/js_subghz/js_subghz.c @@ -223,7 +223,7 @@ static void js_subghz_transmit_file(struct mjs* mjs) { // - "repeat" as variable and loop in this code applies to RAW files only // parsed files handle repeat in protocol layer instead // We keep 0 as default, or literal value if specified by user - // If user did not specify, -1 is detected below, and we use: + // If user did not specify, 0 is detected below, and we use: // - 1 repeat for RAW // - 10 repeats for parsed, which is passed to protocol, and we loop once here uint32_t repeat = 0; diff --git a/applications/system/js_app/packages/create-fz-app/README.md b/applications/system/js_app/packages/create-fz-app/README.md new file mode 100644 index 000000000..bdc30d5a8 --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/README.md @@ -0,0 +1,24 @@ +# Momentum FW JavaScript SDK Wizard +This package contains an interactive wizard that lets you scaffold a JavaScript +application for Flipper Zero using the Momentum Firmware JS SDK. + +This is a fork of the [Official Flipper Zero JS SDK Wizard](https://www.npmjs.com/package/@flipperdevices/create-fz-app), +configured to use the [Momentum JavaScript SDK]((https://www.npmjs.com/package/@next-flip/fz-sdk-mntm)) instead. +No other changes are included. + +## Getting started +Create your application using the interactive wizard: +```shell +npx @next-flip/create-fz-app-mntm@latest +``` + +Then, enter the directory with your application and launch it: +```shell +cd my-flip-app +npm start +``` + +You are free to use `pnpm` or `yarn` instead of `npm`. + +## Documentation +Check out the [JavaScript section in the Developer Documentation](https://developer.flipper.net/flipperzero/doxygen/js.html) diff --git a/applications/system/js_app/packages/create-fz-app/index.js b/applications/system/js_app/packages/create-fz-app/index.js new file mode 100755 index 000000000..0bfe9376e --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/index.js @@ -0,0 +1,68 @@ +#!/usr/bin/env node +import prompts from "prompts"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "url"; +import { spawnSync } from "node:child_process"; +import { replaceInFileSync } from "replace-in-file"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +(async () => { + const { name, pkgManager, confirm } = await prompts([ + { + type: "text", + name: "name", + message: "What is the name of your project?", + initial: "my-flip-app" + }, + { + type: "select", + name: "pkgManager", + message: "What package manager should your project use?", + choices: [ + { title: "npm", value: "npm" }, + { title: "pnpm", value: "pnpm" }, + { title: "yarn", value: "yarn" }, + ], + }, + { + type: "confirm", + name: "confirm", + message: "Create project?", + initial: true, + }, + ]); + + if (!confirm) + return; + + if (fs.existsSync(name)) { + const { replace } = await prompts([ + { + type: "confirm", + name: "replace", + message: `File or directory \`${name}\` already exists. Continue anyway?`, + initial: false, + }, + ]); + if (!replace) + return; + } + + fs.rmSync(name, { recursive: true, force: true }); + + console.log("Copying files..."); + fs.cpSync(path.resolve(__dirname, "template"), name, { recursive: true }); + replaceInFileSync({ files: `${name}/**/*`, from: //g, to: name }); + + console.log("Installing packages..."); + spawnSync("bash", ["-c", `cd ${name} && ${pkgManager} install`], { + cwd: process.cwd(), + detached: true, + stdio: "inherit", + }); + + console.log(`Done! Created ${name}. Run \`cd ${name} && ${pkgManager} start\` to run it on your Flipper.`); +})(); diff --git a/applications/system/js_app/packages/create-fz-app/package.json b/applications/system/js_app/packages/create-fz-app/package.json new file mode 100644 index 000000000..6ceeb9339 --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/package.json @@ -0,0 +1,25 @@ +{ + "name": "@next-flip/create-fz-app-mntm", + "version": "0.1.1", + "description": "Template package for JS apps for Flipper Zero using Momentum Custom Firmware JS SDK", + "bin": "index.js", + "type": "module", + "keywords": [ + "momentum", + "momentum firmware", + "next-flip", + "flipper", + "flipper zero" + ], + "author": "Flipper Devices", + "license": "GPL-3.0-only", + "repository": { + "type": "git", + "url": "git+https://github.com/Next-Flip/Momentum-Firmware.git", + "directory": "applications/system/js_app/packages/create-fz-app" + }, + "dependencies": { + "prompts": "^2.4.2", + "replace-in-file": "^8.2.0" + } +} \ No newline at end of file diff --git a/applications/system/js_app/packages/create-fz-app/pnpm-lock.yaml b/applications/system/js_app/packages/create-fz-app/pnpm-lock.yaml new file mode 100644 index 000000000..58f20a385 --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/pnpm-lock.yaml @@ -0,0 +1,373 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + prompts: + specifier: ^2.4.2 + version: 2.4.2 + replace-in-file: + specifier: ^8.2.0 + version: 8.2.0 + +packages: + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + replace-in-file@8.2.0: + resolution: {integrity: sha512-hMsQtdYHwWviQT5ZbNsgfu0WuCiNlcUSnnD+aHAL081kbU9dPkPocDaHlDvAHKydTWWpx1apfcEcmvIyQk3CpQ==} + engines: {node: '>=18'} + hasBin: true + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + +snapshots: + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@pkgjs/parseargs@0.11.0': + optional: true + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + balanced-match@1.0.2: {} + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + chalk@5.3.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + eastasianwidth@0.2.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + escalade@3.2.0: {} + + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + + get-caller-file@2.0.5: {} + + glob@10.4.5: + dependencies: + foreground-child: 3.3.0 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + is-fullwidth-code-point@3.0.0: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + kleur@3.0.3: {} + + lru-cache@10.4.3: {} + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minipass@7.1.2: {} + + package-json-from-dist@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + replace-in-file@8.2.0: + dependencies: + chalk: 5.3.0 + glob: 10.4.5 + yargs: 17.7.2 + + require-directory@2.1.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + sisteransi@1.0.5: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 diff --git a/applications/system/js_app/packages/create-fz-app/template/.gitignore b/applications/system/js_app/packages/create-fz-app/template/.gitignore new file mode 100644 index 000000000..aa57f8d03 --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/template/.gitignore @@ -0,0 +1,2 @@ +/dist +node_modules/ diff --git a/applications/system/js_app/packages/create-fz-app/template/fz-sdk.config.json5 b/applications/system/js_app/packages/create-fz-app/template/fz-sdk.config.json5 new file mode 100644 index 000000000..e545841c5 --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/template/fz-sdk.config.json5 @@ -0,0 +1,23 @@ +{ + build: { + // Where to put the compiled file + output: "dist/.js", + + // Whether to reduce the final file size at the cost of readability and + // clarity of error messages + minify: false, + + // Set this to `false` if you've thoroughly read the documentation and + // are sure that you can use manual version checks to your advantage + enforceSdkVersion: true, + }, + + upload: { + // Where to grab the file from. If you're not doing any extra processing + // after the SDK, this should match `build.output` + input: "dist/.js", + + // Where to put the file on the device + output: "/ext/apps/Scripts/.js", + }, +} diff --git a/applications/system/js_app/packages/create-fz-app/template/index.ts b/applications/system/js_app/packages/create-fz-app/template/index.ts new file mode 100644 index 000000000..424141145 --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/template/index.ts @@ -0,0 +1,30 @@ +// import modules +// caution: `eventLoop` HAS to be imported before `gui`, and `gui` HAS to be +// imported before any `gui` submodules. +import * as eventLoop from "@next-flip/fz-sdk-mntm/event_loop"; +import * as gui from "@next-flip/fz-sdk-mntm/gui"; +import * as dialog from "@next-flip/fz-sdk-mntm/gui/dialog"; + +// a common pattern is to declare all the views that your app uses on one object +const views = { + dialog: dialog.makeWith({ + header: "Hello from ", + text: "Check out index.ts and\nchange something :)", + center: "Gonna do that!", + }), +}; + +// stop app on center button press +eventLoop.subscribe(views.dialog.input, (_sub, button, eventLoop) => { + if (button === "center") + eventLoop.stop(); +}, eventLoop); + +// stop app on back button press +eventLoop.subscribe(gui.viewDispatcher.navigation, (_sub, _item, eventLoop) => { + eventLoop.stop(); +}, eventLoop); + +// run app +gui.viewDispatcher.switchTo(views.dialog); +eventLoop.run(); diff --git a/applications/system/js_app/packages/create-fz-app/template/package.json b/applications/system/js_app/packages/create-fz-app/template/package.json new file mode 100644 index 000000000..ee23f1c1f --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/template/package.json @@ -0,0 +1,12 @@ +{ + "name": "", + "version": "1.0.0", + "scripts": { + "build": "tsc && node node_modules/@next-flip/fz-sdk-mntm/sdk.js build", + "start": "npm run build && node node_modules/@next-flip/fz-sdk-mntm/sdk.js upload" + }, + "devDependencies": { + "@next-flip/fz-sdk-mntm": "^0.1", + "typescript": "^5.6.3" + } +} \ No newline at end of file diff --git a/applications/system/js_app/packages/create-fz-app/template/tsconfig.json b/applications/system/js_app/packages/create-fz-app/template/tsconfig.json new file mode 100644 index 000000000..1efa63d97 --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/template/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "outDir": "dist", + "checkJs": true, + "module": "CommonJS", + "noLib": true, + "target": "ES2015", + "types": [], + }, + "files": [ + "./node_modules/@next-flip/fz-sdk-mntm/global.d.ts", + ], + "include": [ + "./**/*.ts", + "./**/*.js", + ], + "exclude": [ + "./node_modules/**/*", + "dist/**/*", + ], +} \ No newline at end of file diff --git a/applications/system/js_app/packages/fz-sdk/.gitignore b/applications/system/js_app/packages/fz-sdk/.gitignore new file mode 100644 index 000000000..77f12ae2e --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/.gitignore @@ -0,0 +1 @@ +docs/ diff --git a/applications/system/js_app/packages/fz-sdk/README.md b/applications/system/js_app/packages/fz-sdk/README.md new file mode 100644 index 000000000..3ebadb335 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/README.md @@ -0,0 +1,42 @@ +# Momentum FW JavaScript SDK +This package contains tooling and typings for developing Flipper Zero +applications in JavaScript for Momentum Custom Firmware. + +This is a fork of the [Official Flipper Zero JS SDK](https://www.npmjs.com/package/@flipperdevices/fz-sdk), +with added types for the extra features provided by the Momentum JavaScript API. + +Scripts made for Official Flipper Zero JS SDK will work on Momentum Firmware too. +If you use extra features provided by Momentum, you are encouraged to use syntax like +`if (doesSdkSupport(["feature-name"])) { ... }` so that your JS app can work on Official +Firmware too, aswell as all other compliant Custom Firmwares. If some of those extra +features are essential to the functionality of your app, you can use `checkSdkFeatures(["feature1", "feature2"])` +near the beginning of your script, which will show a warning to the user that these features +are not available in their firmware distribution. + +## Getting started +Create your application using the interactive wizard: +```shell +npx @next-flip/create-fz-app-mntm@latest +``` + +Then, enter the directory with your application and launch it: +```shell +cd my-flip-app +npm start +``` + +You are free to use `pnpm` or `yarn` instead of `npm`. + +## Versioning +For each version of this package, the major and minor components match those of +the Flipper Zero JS SDK version that that package version targets. This version +follows semver. For example, apps compiled with SDK version `0.1.0` will be +compatible with SDK versions `0.1`...`1.0` (not including `1.0`). + +Every API has a version history reflected in its JSDoc comment. It is heavily +recommended to check SDK compatibility using a combination of +`sdkCompatibilityStatus`, `isSdkCompatible`, `assertSdkCompatibility` depending +on your use case. + +## Documentation +Check out the [JavaScript section in the Developer Documentation](https://developer.flipper.net/flipperzero/doxygen/js.html) diff --git a/applications/system/js_app/types/badusb/index.d.ts b/applications/system/js_app/packages/fz-sdk/badusb/index.d.ts similarity index 90% rename from applications/system/js_app/types/badusb/index.d.ts rename to applications/system/js_app/packages/fz-sdk/badusb/index.d.ts index 57c2662cd..3eca288c2 100644 --- a/applications/system/js_app/types/badusb/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/badusb/index.d.ts @@ -1,8 +1,10 @@ /** * @brief Special key codes that this module recognizes + * @version Added in JS SDK 0.1 */ export type ModifierKey = "CTRL" | "SHIFT" | "ALT" | "GUI"; +/** @version Added in JS SDK 0.1 */ export type MainKey = "DOWN" | "LEFT" | "RIGHT" | "UP" | @@ -31,6 +33,7 @@ export type MainKey = "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z"; +/** @version Added in JS SDK 0.1 */ export type KeyCode = MainKey | ModifierKey | number; /** @@ -39,11 +42,13 @@ export type KeyCode = MainKey | ModifierKey | number; * Automatically unlocks USB profile, so qFlipper connection will be interrupted. * * @param settings USB device settings. Omit to select default parameters + * @version Added in JS SDK 0.1 */ export declare function setup(settings?: { vid: number, pid: number, mfrName?: string, prodName?: string, layoutPath?: string }): void; /** * @brief Tells whether the virtual USB HID device has successfully connected + * @version Added in JS SDK 0.1 */ export declare function isConnected(): boolean; @@ -52,6 +57,7 @@ export declare function isConnected(): boolean; * @param keys The arguments represent a set of keys to. Out of that set, only * one of the keys may represent a "main key" (see `MainKey`), with * the rest being modifier keys (see `ModifierKey`). + * @version Added in JS SDK 0.1 */ export declare function press(...keys: KeyCode[]): void; @@ -60,6 +66,7 @@ export declare function press(...keys: KeyCode[]): void; * @param keys The arguments represent a set of keys to. Out of that set, only * one of the keys may represent a "main key" (see `MainKey`), with * the rest being modifier keys (see `ModifierKey`). + * @version Added in JS SDK 0.1 */ export declare function hold(...keys: KeyCode[]): void; @@ -68,6 +75,7 @@ export declare function hold(...keys: KeyCode[]): void; * @param keys The arguments represent a set of keys to. Out of that set, only * one of the keys may represent a "main key" (see `MainKey`), with * the rest being modifier keys (see `ModifierKey`). + * @version Added in JS SDK 0.1 */ export declare function release(...keys: KeyCode[]): void; @@ -75,6 +83,7 @@ export declare function release(...keys: KeyCode[]): void; * @brief Prints a string by repeatedly pressing and releasing keys * @param string The string to print * @param delay How many milliseconds to wait between key presses + * @version Added in JS SDK 0.1 */ export declare function print(string: string, delay?: number): void; @@ -83,6 +92,7 @@ export declare function print(string: string, delay?: number): void; * "Enter" after printing the string * @param string The string to print * @param delay How many milliseconds to wait between key presses + * @version Added in JS SDK 0.1 */ export declare function println(string: string, delay?: number): void; @@ -90,6 +100,7 @@ export declare function println(string: string, delay?: number): void; * @brief Prints a string by Alt+Numpad method - works only on Windows! * @param string The string to print * @param delay How many milliseconds to wait between key presses + * @version Added in JS SDK 0.1 */ export declare function altPrint(string: string, delay?: number): void; @@ -98,10 +109,12 @@ export declare function altPrint(string: string, delay?: number): void; * Presses "Enter" after printing the string * @param string The string to print * @param delay How many milliseconds to wait between key presses + * @version Added in JS SDK 0.1 */ export declare function altPrintln(string: string, delay?: number): void; /** * @brief Releases usb, optional, but allows to switch usb profile + * @version Added in JS SDK 0.1 */ export declare function quit(): void; diff --git a/applications/system/js_app/packages/fz-sdk/blebeacon/index.d.ts b/applications/system/js_app/packages/fz-sdk/blebeacon/index.d.ts new file mode 100644 index 000000000..b2b527a40 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/blebeacon/index.d.ts @@ -0,0 +1,41 @@ +/** + * Module for using the BLE extra beacon + * @version Available with JS feature `blebeacon` + * @module + */ + +/** + * @brief Check if the BLE beacon is active + */ +export declare function isActive(): boolean; + +/** + * @brief Set BLE beacon configuration + * @param mac The MAC address to use + * @param power The power level to use, in GapAdvPowerLevel scale: 0x00 (-40dBm) to 0x1F (+6dBm) + * @param minInterval Minimum advertisement interval + * @param maxInterval Maximum advertisement interval + */ +export declare function setConfig(mac: Uint8Array, power?: number, minInterval?: number, maxInterval?: number): void; + +/** + * @brief Set BLE beacon advertisement data + * @param data The advertisement data to use + */ +export declare function setData(data: Uint8Array): void; + +/** + * @brief Start BLE beacon + */ +export declare function start(): void; + +/** + * @brief Stop BLE beacon + */ +export declare function stop(): void; + +/** + * @brief Set whether the BLE beacon will remain active after the script exits + * @param keep True if BLE beacon should remain active after script exit + */ +export declare function keepAlive(keep: boolean): void; diff --git a/applications/system/js_app/packages/fz-sdk/docs_readme.md b/applications/system/js_app/packages/fz-sdk/docs_readme.md new file mode 100644 index 000000000..f82f58ec7 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/docs_readme.md @@ -0,0 +1 @@ +# Welcome diff --git a/applications/system/js_app/packages/fz-sdk/event_loop/index.d.ts b/applications/system/js_app/packages/fz-sdk/event_loop/index.d.ts new file mode 100644 index 000000000..001518f87 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/event_loop/index.d.ts @@ -0,0 +1,182 @@ +/** + * Module for dealing with events + * + * ```js + * let eventLoop = require("event_loop"); + * ``` + * + * The event loop is central to event-based programming in many frameworks, and + * our JS subsystem is no exception. It is a good idea to familiarize yourself + * with the event loop first before using any of the advanced modules (e.g. GPIO + * and GUI). + * + * # Conceptualizing the event loop + * If you ever wrote JavaScript before, you have definitely seen callbacks. It's + * when a function accepts another function (usually an anonymous one) as one of + * the arguments, which it will call later on, e.g. when an event happens or + * when data becomes ready: + * ```js + * setTimeout(function() { console.log("Hello, World!") }, 1000); + * ``` + * + * Many JavaScript engines employ a queue that the runtime fetches events from + * as they occur, subsequently calling the corresponding callbacks. This is done + * in a long-running loop, hence the name "event loop". Here's the pseudocode + * for a typical event loop: + * ```js + * while(loop_is_running()) { + * if(event_available_in_queue()) { + * let event = fetch_event_from_queue(); + * let callback = get_callback_associated_with(event); + * if(callback) + * callback(get_extra_data_for(event)); + * } else { + * // avoid wasting CPU time + * sleep_until_any_event_becomes_available(); + * } + * } + * ``` + * + * Most JS runtimes enclose the event loop within themselves, so that most JS + * programmers does not even need to be aware of its existence. This is not the + * case with our JS subsystem. + * + * # Example + * This is how one would write something similar to the `setTimeout` example + * above: + * ```js + * // import module + * let eventLoop = require("event_loop"); + * + * // create an event source that will fire once 1 second after it has been created + * let timer = eventLoop.timer("oneshot", 1000); + * + * // subscribe a callback to the event source + * eventLoop.subscribe(timer, function(_subscription, _item, eventLoop) { + * print("Hello, World!"); + * eventLoop.stop(); + * }, eventLoop); // notice this extra argument. we'll come back to this later + * + * // run the loop until it is stopped + * eventLoop.run(); + * + * // the previous line will only finish executing once `.stop()` is called, hence + * // the following line will execute only after "Hello, World!" is printed + * print("Stopped"); + * ``` + * + * I promised you that we'll come back to the extra argument after the callback + * function. Our JavaScript engine does not support closures (anonymous + * functions that access values outside of their arguments), so we ask + * `subscribe` to pass an outside value (namely, `eventLoop`) as an argument to + * the callback so that we can access it. We can modify this extra state: + * ```js + * // this timer will fire every second + * let timer = eventLoop.timer("periodic", 1000); + * eventLoop.subscribe(timer, function(_subscription, _item, counter, eventLoop) { + * print("Counter is at:", counter); + * if(counter === 10) + * eventLoop.stop(); + * // modify the extra arguments that will be passed to us the next time + * return [counter + 1, eventLoop]; + * }, 0, eventLoop); + * ``` + * + * Because we have two extra arguments, if we return anything other than an + * array of length 2, the arguments will be kept as-is for the next call. + * + * The first two arguments that get passed to our callback are: + * - The subscription manager that lets us `.cancel()` our subscription + * - The event item, used for events that have extra data. Timer events do + * not, they just produce `undefined`. + * + * @version Added in JS SDK 0.1 + * @module + */ + +/** + * @ignore + */ +type Lit = undefined | null | {}; + +/** + * Subscription control interface + * @version Added in JS SDK 0.1 + */ +export interface Subscription { + /** + * Cancels the subscription, preventing any future events managed by the + * subscription from firing + * @version Added in JS SDK 0.1 + */ + cancel(): void; +} + +/** + * Opaque event source identifier + * @version Added in JS SDK 0.1 + */ +export type Contract = symbol & { "__tag__": "contract" }; +// introducing a nominal type in a hacky way; the `__tag__` property doesn't really exist. + +/** + * A callback can be assigned to an event loop to listen to an event. It may + * return an array with values that will be passed to it as arguments the next + * time that it is called. The first argument is always the subscription + * manager, and the second argument is always the item that trigged the event. + * The type of the item is defined by the event source. + * @version Added in JS SDK 0.1 + */ +export type Callback = (subscription: Subscription, item: Item, ...args: Args) => Args | undefined | void; + +/** + * Subscribes a callback to an event + * @param contract Event identifier + * @param callback Function to call when the event is triggered + * @param args Initial arguments passed to the callback + * @version Added in JS SDK 0.1 + */ +export function subscribe(contract: Contract, callback: Callback, ...args: Args): Subscription; +/** + * Runs the event loop until it is stopped (potentially never) + * @version Added in JS SDK 0.1 + */ +export function run(): void | never; +/** + * Stops the event loop + * @version Added in JS SDK 0.1 + */ +export function stop(): void; + +/** + * Creates a timer event that can be subscribed to just like any other event + * @param mode Either `"oneshot"` or `"periodic"` + * @param interval Timer interval in milliseconds + * @version Added in JS SDK 0.1 + */ +export function timer(mode: "oneshot" | "periodic", interval: number): Contract; + +/** + * Message queue + * @version Added in JS SDK 0.1 + */ +export declare class Queue { + /** + * Message event + * @version Added in JS SDK 0.1 + */ + input: Contract; + /** + * Sends a message to the queue + * @param message message to send + * @version Added in JS SDK 0.1 + */ + send(message: T): void; +} + +/** + * Creates a message queue + * @param length maximum queue capacity + * @version Added in JS SDK 0.1 + */ +export function queue(length: number): Queue; diff --git a/applications/system/js_app/packages/fz-sdk/flipper/index.d.ts b/applications/system/js_app/packages/fz-sdk/flipper/index.d.ts new file mode 100644 index 000000000..1c154165b --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/flipper/index.d.ts @@ -0,0 +1,41 @@ +/** + * Module for querying device properties + * @version Added in JS SDK 0.1 + * @module + */ + +/** + * @brief Returns the device model + * @version Added in JS SDK 0.1 + */ +export declare function getModel(): string; + +/** + * @brief Returns the name of the virtual dolphin + * @version Added in JS SDK 0.1 + */ +export declare function getName(): string; + +/** + * @brief Returns the battery charge percentage + * @version Added in JS SDK 0.1 + */ +export declare function getBatteryCharge(): number; + +/** + * @warning Do **NOT** use this to check the presence or absence of features. If + * you do, I'm gonna be sad :( Instead, refer to `checkSdkFeatures` and + * other similar mechanisms. + * @note Momentum firmware reports `"momentum"`. + * @version Added in JS SDK 0.1 + */ +export declare const firmwareVendor: string; + +/** + * @warning Do **NOT** use this to check the presence or absence of features. If + * you do, I'm gonna be sad :( Instead, refer to + * `checkSdkCompatibility` and other similar mechanisms. + * @note You're looking at JS SDK 0.1 + * @version Added in JS SDK 0.1 + */ +export declare const jsSdkVersion: [number, number]; diff --git a/applications/system/js_app/packages/fz-sdk/global.d.ts b/applications/system/js_app/packages/fz-sdk/global.d.ts new file mode 100644 index 000000000..ba6996f27 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/global.d.ts @@ -0,0 +1,409 @@ +/** + * Things from this module are automatically available to you without having to + * explicitly import anything. + * + * # SDK versioning and features + * + * ## Motivation + * It is very important that you check that features are implemented before you + * use them. By adding the necessary checks, you ensure that your users get a + * clear warning instead of a cryptic error message when running the script. + * + * This system has been designed in collaboration with our community in order to + * make things better for everybody involved. You can find out more in this + * discussion: https://github.com/flipperdevices/flipperzero-firmware/pull/3961 + * + * ## Community agreement + * Each interpreter implementation (aka "JS SDK", aka "JS API"), including + * those found in third-party firmware distributions, defines two markers for + * signaling what it supports: the **SDK version** and the + * **extra feature set**. + * + * The **SDK version** consists of two semver-like integer components: the major + * version and the minor version. Like semver, the major version is bumped when + * a breaking change is introduced (i.e. one that would require correction of + * apps by their developers), and the minor version is bumped when a new + * non-breaking feature is introduced. Because we have adopted TypeScript, + * the https://www.semver-ts.org/ standard is used to determine whether a change + * is breaking or not. The basis of `semver-ts` is the "no new red squiggles" + * rule. + * + * Every major version is associated with a set of **extra features** that are + * present in some firmware distributions but not others. Distributions may + * cross-port features between each other, until at some point they get ported + * into the upstream firmware distribution. With the next major release of the + * JS SDK, all extra features present in the upstream distribution are now + * declared **baseline features**, and thus no longer recognized as "extra + * features". + * + * Before using a feature, you must check that the interpreter that you're + * running on actually supports it. If you don't, the portability of your + * application will suffer. + * + * ## Implementation + * Use the following functions to check version compatibility: + * - `checkSdkCompatibility` when your script absolutely cannot function on an + * incompatible interpreter + * - `isSdkCompatible` when your script can leverage multiple interpreter + * editions to its advantage + * - `sdkCompatibilityStatus` when you need a detailed status on compatibility + * + * Use the following functions to check feature compatibility: + * - `checkSdkFeatures` when your script absolutely cannot function on an + * incompatible interpreter + * - `doesSdkSupport` when your script can leverage multiple interpreter + * editions to its advantage + * + * ## Automatic version enforcement + * The SDK will automatically insert a call to `checkSdkCompatibility` in the + * beginning of the resulting script. If you would like to disable this check + * and instead use other manual compatibility checking facilities, edit your + * `fz-sdk.config.json5`. + * + * # Standard library + * Standard library features are mostly unimplemented. This module defines, + * among other things, the features that _are_ implemented. + * + * @version Added in JS SDK 0.1 + * @module + */ + +/** + * @brief Checks compatibility between the script and the JS SDK that the + * firmware provides + * + * @note You're looking at JS SDK v0.1 + * + * @param expectedMajor JS SDK major version expected by the script + * @param expectedMinor JS SDK minor version expected by the script + * @returns Compatibility status: + * - `"compatible"` if the script and the JS SDK are compatible + * - `"firmwareTooOld"` if the expected major version is larger than the + * version of the firmware, or if the expected minor version is larger than + * the version of the firmware + * - `"firmwareTooNew"` if the expected major version is lower than the + * version of the firmware + * @version Added in JS SDK 0.1 + */ +declare function sdkCompatibilityStatus(expectedMajor: number, expectedMinor: number): + "compatible" | "firmwareTooOld" | "firmwareTooNew"; + +/** + * @brief Checks compatibility between the script and the JS SDK that the + * firmware provides in a boolean fashion + * + * @note You're looking at JS SDK v0.1 + * + * @param expectedMajor JS SDK major version expected by the script + * @param expectedMinor JS SDK minor version expected by the script + * @returns `true` if the two are compatible, `false` otherwise + * @version Added in JS SDK 0.1 + */ +declare function isSdkCompatible(expectedMajor: number, expectedMinor: number): boolean; + +/** + * @brief Asks the user whether to continue executing the script if the versions + * are not compatible. Does nothing if they are. + * + * @note You're looking at JS SDK v0.1 + * + * @param expectedMajor JS SDK major version expected by the script + * @param expectedMinor JS SDK minor version expected by the script + * @version Added in JS SDK 0.1 + */ +declare function checkSdkCompatibility(expectedMajor: number, expectedMinor: number): void | never; + +/** + * @brief Checks whether all of the specified extra features are supported by + * the interpreter. + * @warning This function will return `false` if a queried feature is now + * recognized as a baseline feature. For more info, consult the module + * documentation. + * @param features Array of named features to query + * @version Added in JS SDK 0.1 + */ +declare function doesSdkSupport(features: string[]): boolean; + +/** + * @brief Checks whether all of the specified extra features are supported by + * the interpreter, asking the user if they want to continue running the + * script if they're not. + * @warning This function will act as if the feature is not implemented for + * features that are now recognized as baseline features. For more + * info, consult the module documentation. + * @param features Array of named features to query + * @version Added in JS SDK 0.1 + */ +declare function checkSdkFeatures(features: string[]): void | never; + +/** + * @brief Pauses JavaScript execution for a while + * @param ms How many milliseconds to pause the execution for + * @version Added in JS SDK 0.1 + */ +declare function delay(ms: number): void; + +/** + * @brief Prints to the GUI console view + * @param args The arguments are converted to strings, concatenated without any + * spaces in between and printed to the console view + * @version Added in JS SDK 0.1 + */ +declare function print(...args: any[]): void; + +/** + * @brief Converts a string to a number + * @param text The string to convert to a number + * @param base Integer base (`2`...`16`), default: 10 + * @version Added in JS SDK 0.1 + */ +declare function parseInt(text: string, base?: number): number; + +/** + * @brief Path to the directory containing the current script + * @version Added in JS SDK 0.1 + */ +declare const __dirname: string; + +/** + * @brief Path to the current script file + * @version Added in JS SDK 0.1 + */ +declare const __filename: string; + +/** + * @brief Runs a JS file and returns value from it + * + * Reads a file at the specified path and runs it as JS, returning the last evaluated value. + * + * The result is cached and this filepath will not re-evaluated on future + * load() calls for this session. + * + * @param path The path to the file + * @param scope An object to use as global scope while running this file + * @version Added in JS SDK 0.1 + */ +declare function load(path: string, scope?: object): any; + +/** + * @brief Return 1-byte string whose ASCII code is the integer `n` + * + * If `n` is not numeric or outside of `0-255` range, `null` is returned + * + * @param n The ASCII code to convert to string + * @version Added in JS SDK 0.1 + */ +declare function chr(n: number): string | null; + +/** + * @brief Loads a natively implemented module + * @param module The name of the module to load + * @version Added in JS SDK 0.1 + */ +declare function require(module: string): any; + +/** + * @brief Exit JavaScript with given message + * @param message The error message to show to user + * @version Added in JS SDK 0.1 + */ +declare function die(message: string): never; + +/** + * @brief mJS Foreign Pointer type + * + * JavaScript code cannot do anything with values of `RawPointer` type except + * acquire them from native code and pass them right back to other parts of + * native code. These values cannot be turned into something meaningful, nor can + * be they modified. + * + * @version Added in JS SDK 0.1 + */ +declare type RawPointer = symbol & { "__tag__": "raw_ptr" }; +// introducing a nominal type in a hacky way; the `__tag__` property doesn't really exist. + +/** + * @brief Holds raw bytes + * @version Added in JS SDK 0.1 + */ +declare class ArrayBuffer { + /** + * @brief The pointer to the byte buffer + * @note Like other `RawPointer` values, this value is essentially useless + * to JS code. + * @version Added in JS SDK 0.1 + */ + getPtr: RawPointer; + /** + * @brief The length of the buffer in bytes + * @version Added in JS SDK 0.1 + */ + byteLength: number; + /** + * @brief Creates an `ArrayBuffer` that contains a sub-part of the buffer + * @param start The index of the byte in the source buffer to be used as the + * start for the new buffer + * @param end The index of the byte in the source buffer that follows the + * byte to be used as the last byte for the new buffer + * @version Added in JS SDK 0.1 + */ + slice(start: number, end?: number): ArrayBuffer; +} + +declare function ArrayBuffer(): ArrayBuffer; + +declare type ElementType = "u8" | "i8" | "u16" | "i16" | "u32" | "i32"; + +declare class TypedArray { + /** + * @brief The length of the buffer in bytes + * @version Added in JS SDK 0.1 + */ + byteLength: number; + /** + * @brief The length of the buffer in typed elements + * @version Added in JS SDK 0.1 + */ + length: number; + /** + * @brief The underlying `ArrayBuffer` + * @version Added in JS SDK 0.1 + */ + buffer: ArrayBuffer; +} + +declare class Uint8Array extends TypedArray<"u8"> { } +declare class Int8Array extends TypedArray<"i8"> { } +declare class Uint16Array extends TypedArray<"u16"> { } +declare class Int16Array extends TypedArray<"i16"> { } +declare class Uint32Array extends TypedArray<"u32"> { } +declare class Int32Array extends TypedArray<"i32"> { } + +declare function Uint8Array(data: ArrayBuffer | number | number[]): Uint8Array; +declare function Int8Array(data: ArrayBuffer | number | number[]): Int8Array; +declare function Uint16Array(data: ArrayBuffer | number | number[]): Uint16Array; +declare function Int16Array(data: ArrayBuffer | number | number[]): Int16Array; +declare function Uint32Array(data: ArrayBuffer | number | number[]): Uint32Array; +declare function Int32Array(data: ArrayBuffer | number | number[]): Int32Array; + +declare const console: { + /** + * @brief Prints to the UART logs at the `[I]` level + * @param args The arguments are converted to strings, concatenated without any + * spaces in between and printed to the logs + * @version Added in JS SDK 0.1 + */ + log(...args: any[]): void; + /** + * @brief Prints to the UART logs at the `[D]` level + * @param args The arguments are converted to strings, concatenated without any + * spaces in between and printed to the logs + * @version Added in JS SDK 0.1 + */ + debug(...args: any[]): void; + /** + * @brief Prints to the UART logs at the `[W]` level + * @param args The arguments are converted to strings, concatenated without any + * spaces in between and printed to the logs + * @version Added in JS SDK 0.1 + */ + warn(...args: any[]): void; + /** + * @brief Prints to the UART logs at the `[E]` level + * @param args The arguments are converted to strings, concatenated without any + * spaces in between and printed to the logs + * @version Added in JS SDK 0.1 + */ + error(...args: any[]): void; +}; + +declare class Array { + /** + * @brief Takes items out of the array + * + * Removes elements from the array and returns them in a new array + * + * @param start The index to start taking elements from + * @param deleteCount How many elements to take + * @returns The elements that were taken out of the original array as a new + * array + * @version Added in JS SDK 0.1 + */ + splice(start: number, deleteCount: number): T[]; + /** + * @brief Adds a value to the end of the array + * @param value The value to add + * @returns New length of the array + * @version Added in JS SDK 0.1 + */ + push(value: T): number; + /** + * @brief How many elements there are in the array + * @version Added in JS SDK 0.1 + */ + length: number; +} + +declare class String { + /** + * @brief How many characters there are in the string + * @version Added in JS SDK 0.1 + */ + length: number; + /** + * @brief Returns the character code at an index in the string + * @param index The index to consult + * @version Added in JS SDK 0.1 + */ + charCodeAt(index: number): number; + /** + * See `charCodeAt` + * @version Added in JS SDK 0.1 + */ + at(index: number): number; + /** + * @brief Return index of first occurrence of substr within the string or `-1` if not found + * @param substr The string to search for + * @param fromIndex The index to start searching from + * @version Added in JS SDK 0.1 + */ + indexOf(substr: string, fromIndex?: number): number; + /** + * @brief Return a substring between two indices + * @param start The index to start substring at + * @param end The index to end substring at + * @version Added in JS SDK 0.1 + */ + slice(start: number, end?: number): string; + /** + * @brief Return this string transformed to upper case + * @version Added in JS SDK 0.1 + */ + toUpperCase(): string; + /** + * @brief Return this string transformed to lower case + * @version Added in JS SDK 0.1 + */ + toLowerCase(): string; +} + +declare class Boolean { } + +declare class Function { } + +declare class Number { + /** + * @brief Converts this number to a string + * @param base Integer base (`2`...`16`), default: 10 + * @version Added in JS SDK 0.1 + */ + toString(base?: number): string; +} + +declare class Object { } + +declare class RegExp { } + +declare interface IArguments { } + +declare type Partial = { [K in keyof O]?: O[K] }; diff --git a/applications/system/js_app/types/gpio/index.d.ts b/applications/system/js_app/packages/fz-sdk/gpio/index.d.ts similarity index 59% rename from applications/system/js_app/types/gpio/index.d.ts rename to applications/system/js_app/packages/fz-sdk/gpio/index.d.ts index 18705f898..b484ebbf6 100644 --- a/applications/system/js_app/types/gpio/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/gpio/index.d.ts @@ -1,5 +1,37 @@ +/** + * Module for accessing the GPIO (General Purpose Input/Output) ports + * + * ```js + * let eventLoop = require("event_loop"); + * let gpio = require("gpio"); + * ``` + * + * This module depends on the `event_loop` module, so it _must_ only be imported + * after `event_loop` is imported. + * + * # Example + * ```js + * let eventLoop = require("event_loop"); + * let gpio = require("gpio"); + * + * let led = gpio.get("pc3"); + * led.init({ direction: "out", outMode: "push_pull" }); + * + * led.write(true); + * delay(1000); + * led.write(false); + * delay(1000); + * ``` + * + * @version Added in JS SDK 0.1 + * @module + */ + import type { Contract } from "../event_loop"; +/** + * @version Added in JS SDK 0.1 + */ export interface Mode { direction: "in" | "out"; outMode?: "push_pull" | "open_drain"; @@ -8,31 +40,39 @@ export interface Mode { pull?: "up" | "down"; } +/** + * @version Added in JS SDK 0.1 + */ export interface Pin { /** * Configures a pin. This may be done several times. * @param mode Pin configuration object + * @version Added in JS SDK 0.1 */ init(mode: Mode): void; /** * Sets the output value of a pin if it's been configured with * `direction: "out"`. * @param value Logic value to output + * @version Added in JS SDK 0.1 */ write(value: boolean): void; /** * Gets the input value of a pin if it's been configured with * `direction: "in"`, but not `inMode: "analog"`. + * @version Added in JS SDK 0.1 */ read(): boolean; /** * Gets the input voltage of a pin in millivolts if it's been configured * with `direction: "in"` and `inMode: "analog"` + * @version Added in JS SDK 0.1 */ - read_analog(): number; + readAnalog(): number; /** * Returns an `event_loop` event that can be used to listen to interrupts, * as configured by `init` + * @version Added in JS SDK 0.1 */ interrupt(): Contract; } @@ -41,5 +81,6 @@ export interface Pin { * Returns an object that can be used to manage a GPIO pin. For the list of * available pins, see https://docs.flipper.net/gpio-and-modules#miFsS * @param pin Pin name (e.g. `"PC3"`) or number (e.g. `7`) + * @version Added in JS SDK 0.1 */ export function get(pin: string | number): Pin; diff --git a/applications/system/js_app/packages/fz-sdk/gui/byte_input.d.ts b/applications/system/js_app/packages/fz-sdk/gui/byte_input.d.ts new file mode 100644 index 000000000..5556e7fbb --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/byte_input.d.ts @@ -0,0 +1,41 @@ +/** + * Displays a byte input keyboard. + * + * Sample screenshot of the view + * + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * let byteInputView = require("gui/byte_input"); + * ``` + * + * This module depends on the `gui` module, which in turn depends on the + * `event_loop` module, so they _must_ be imported in this order. It is also + * recommended to conceptualize these modules first before using this one. + * + * # Example + * For an example refer to the `gui.js` example script. + * + * # View props + * - `header`: Text displayed at the top of the screen + * - `length`: Length of data to edit + * - `defaultData`: Data to show by default + * + * @version Added in JS SDK 0.1 + * @module + */ + +import type { View, ViewFactory } from "."; +import type { Contract } from "../event_loop"; + +type Props = { + header: string, + length: number, + defaultData: Uint8Array | ArrayBuffer, +} +declare class ByteInput extends View { + input: Contract; +} +declare class ByteInputFactory extends ViewFactory { } +declare const factory: ByteInputFactory; +export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/gui/dialog.d.ts b/applications/system/js_app/packages/fz-sdk/gui/dialog.d.ts new file mode 100644 index 000000000..9bd0c3966 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/dialog.d.ts @@ -0,0 +1,45 @@ +/** + * Displays a dialog with up to three options. + * + * Sample screenshot of the view + * + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * let dialogView = require("gui/dialog"); + * ``` + * + * This module depends on the `gui` module, which in turn depends on the + * `event_loop` module, so they _must_ be imported in this order. It is also + * recommended to conceptualize these modules first before using this one. + * + * # Example + * For an example refer to the `gui.js` example script. + * + * # View props + * - `header`: Text displayed in bold at the top of the screen + * - `text`: Text displayed in the middle of the string + * - `left`: Text for the left button + * - `center`: Text for the center button + * - `right`: Text for the right button + * + * @version Added in JS SDK 0.1 + * @module + */ + +import type { View, ViewFactory } from "."; +import type { Contract } from "../event_loop"; + +type Props = { + header: string, + text: string, + left: string, + center: string, + right: string, +} +declare class Dialog extends View { + input: Contract<"left" | "center" | "right">; +} +declare class DialogFactory extends ViewFactory { } +declare const factory: DialogFactory; +export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/gui/empty_screen.d.ts b/applications/system/js_app/packages/fz-sdk/gui/empty_screen.d.ts new file mode 100644 index 000000000..49e591426 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/empty_screen.d.ts @@ -0,0 +1,32 @@ +/** + * Displays nothing. + * + * Sample screenshot of the view + * + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * let emptyView = require("gui/empty_screen"); + * ``` + * + * This module depends on the `gui` module, which in turn depends on the + * `event_loop` module, so they _must_ be imported in this order. It is also + * recommended to conceptualize these modules first before using this one. + * + * # Example + * For an example refer to the GUI example. + * + * # View props + * This view does not have any props. + * + * @version Added in JS SDK 0.1 + * @module + */ + +import type { View, ViewFactory } from "."; + +type Props = {}; +declare class EmptyScreen extends View { } +declare class EmptyScreenFactory extends ViewFactory { } +declare const factory: EmptyScreenFactory; +export = factory; diff --git a/applications/system/js_app/types/gui/file_picker.d.ts b/applications/system/js_app/packages/fz-sdk/gui/file_picker.d.ts similarity index 90% rename from applications/system/js_app/types/gui/file_picker.d.ts rename to applications/system/js_app/packages/fz-sdk/gui/file_picker.d.ts index 2051e1432..9447f89a9 100644 --- a/applications/system/js_app/types/gui/file_picker.d.ts +++ b/applications/system/js_app/packages/fz-sdk/gui/file_picker.d.ts @@ -2,5 +2,6 @@ * @brief Displays a file picker and returns the selected file, or undefined if cancelled * @param basePath The path to start at * @param extension The file extension(s) to show (like `.sub`, `.iso|.img`, `*`) + * @version Added in JS SDK 0.1 */ export declare function pickFile(basePath: string, extension: string): string | undefined; diff --git a/applications/system/js_app/packages/fz-sdk/gui/index.d.ts b/applications/system/js_app/packages/fz-sdk/gui/index.d.ts new file mode 100644 index 000000000..93a6846c2 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/index.d.ts @@ -0,0 +1,191 @@ +/** + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * ``` + * + * This module depends on the `event_loop` module, so it _must_ only be imported + * after `event_loop` is imported. + * + * ## Conceptualizing GUI + * ### Event loop + * It is highly recommended to familiarize yourself with the event loop first + * before doing GUI-related things. + * + * ### Canvas + * The canvas is just a drawing area with no abstractions over it. Drawing on + * the canvas directly (i.e. not through a viewport) is useful in case you want + * to implement a custom design element, but this is rather uncommon. + * + * ### Viewport + * A viewport is a window into a rectangular portion of the canvas. Applications + * always access the canvas through a viewport. + * + * ### View + * In Flipper's terminology, a "View" is a fullscreen design element that + * assumes control over the entire viewport and all input events. Different + * types of views are available (not all of which are unfortunately currently + * implemented in JS): + * | View | Has JS adapter? | + * |----------------------|------------------| + * | `button_menu` | ❌ | + * | `button_panel` | ❌ | + * | `byte_input` | ✅ | + * | `dialog_ex` | ✅ (as `dialog`) | + * | `empty_screen` | ✅ | + * | `file_browser` | ❌ | + * | `loading` | ✅ | + * | `menu` | ❌ | + * | `number_input` | ❌ | + * | `popup` | ❌ | + * | `submenu` | ✅ | + * | `text_box` | ✅ | + * | `text_input` | ✅ | + * | `variable_item_list` | ❌ | + * | `widget` | ❌ | + * + * In JS, each view has its own set of properties (or just "props"). The + * programmer can manipulate these properties in two ways: + * - Instantiate a `View` using the `makeWith(props)` method, passing an + * object with the initial properties + * - Call `set(name, value)` to modify a property of an existing `View` + * + * ### View Dispatcher + * The view dispatcher holds references to all the views that an application + * needs and switches between them as the application makes requests to do so. + * + * ### Scene Manager + * The scene manager is an optional add-on to the view dispatcher that makes + * managing applications with complex navigation flows easier. It is currently + * inaccessible from JS. + * + * ### Approaches + * In total, there are three different approaches that you may take when writing + * a GUI application: + * | Approach | Use cases | Available from JS | + * |----------------|------------------------------------------------------------------------------|-------------------| + * | ViewPort only | Accessing the graphics API directly, without any of the nice UI abstractions | ❌ | + * | ViewDispatcher | Common UI elements that fit with the overall look of the system | ✅ | + * | SceneManager | Additional navigation flow management for complex applications | ❌ | + * + * # Example + * An example with three different views using the ViewDispatcher approach: + * ```js + * 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"); + * + * // Common pattern: declare all the views in an object. This is absolutely not + * // required, but adds clarity to the script. + * let views = { + * // the view dispatcher auto-✨magically✨ remembers views as they are created + * loading: loadingView.make(), + * empty: emptyView.make(), + * demos: submenuView.makeWith({ + * items: [ + * "Hourglass screen", + * "Empty screen", + * "Exit app", + * ], + * }), + * }; + * + * // go to different screens depending on what was selected + * eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, views) { + * if (index === 0) { + * gui.viewDispatcher.switchTo(views.loading); + * } else if (index === 1) { + * gui.viewDispatcher.switchTo(views.empty); + * } else if (index === 2) { + * eventLoop.stop(); + * } + * }, gui, eventLoop, views); + * + * // go to the demo chooser screen when the back key is pressed + * eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, gui, views) { + * gui.viewDispatcher.switchTo(views.demos); + * }, gui, views); + * + * // run UI + * gui.viewDispatcher.switchTo(views.demos); + * eventLoop.run(); + * ``` + * + * @version Added in JS SDK 0.1 + * @module + */ + +import type { Contract } from "../event_loop"; + +type Properties = { [K: string]: any }; + +export declare class View { + /** + * Assign value to property by name + * @param property Name of the property + * @param value Value to assign + * @version Added in JS SDK 0.1 + */ + set

(property: P, value: Props[P]): void; +} + +export declare class ViewFactory> { + /** + * Create view instance with default values, can be changed later with set() + * @version Added in JS SDK 0.1 + */ + make(): V; + /** + * Create view instance with custom values, can be changed later with set() + * @param initial Dictionary of property names to values + * @version Added in JS SDK 0.1 + */ + makeWith(initial: Partial): V; +} + +/** + * @version Added in JS SDK 0.1 + */ +declare class ViewDispatcher { + /** + * Event source for `sendCustom` events + * @version Added in JS SDK 0.1 + */ + custom: Contract; + /** + * Event source for navigation events (back key presses) + * @version Added in JS SDK 0.1 + */ + navigation: Contract; + /** + * View object currently shown + * @version Added in JS SDK 0.1 + */ + currentView: View; + /** + * Sends a number to the custom event handler + * @param event number to send + * @version Added in JS SDK 0.1 + */ + sendCustom(event: number): void; + /** + * Switches to a view + * @param assoc View-ViewDispatcher association as returned by `add` + * @version Added in JS SDK 0.1 + */ + switchTo(assoc: View): void; + /** + * Sends this ViewDispatcher to the front or back, above or below all other + * GUI viewports + * @param direction Either `"front"` or `"back"` + * @version Added in JS SDK 0.1 + */ + sendTo(direction: "front" | "back"): void; +} + +/** + * @version Added in JS SDK 0.1 + */ +export const viewDispatcher: ViewDispatcher; diff --git a/applications/system/js_app/packages/fz-sdk/gui/loading.d.ts b/applications/system/js_app/packages/fz-sdk/gui/loading.d.ts new file mode 100644 index 000000000..b8b10c43a --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/loading.d.ts @@ -0,0 +1,33 @@ +/** + * Displays an animated hourglass icon. Suppresses all `navigation` events, + * making it impossible for the user to exit the view by pressing the back key. + * + * Sample screenshot of the view + * + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * let loadingView = require("gui/loading"); + * ``` + * + * This module depends on the `gui` module, which in turn depends on the + * `event_loop` module, so they _must_ be imported in this order. It is also + * recommended to conceptualize these modules first before using this one. + * + * # Example + * For an example refer to the GUI example. + * + * # View props + * This view does not have any props. + * + * @version Added in JS SDK 0.1 + * @module + */ + +import type { View, ViewFactory } from "."; + +type Props = {}; +declare class Loading extends View { } +declare class LoadingFactory extends ViewFactory { } +declare const factory: LoadingFactory; +export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/gui/submenu.d.ts b/applications/system/js_app/packages/fz-sdk/gui/submenu.d.ts new file mode 100644 index 000000000..31e08aab8 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/submenu.d.ts @@ -0,0 +1,39 @@ +/** + * Displays a scrollable list of clickable textual entries. + * + * Sample screenshot of the view + * + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * let submenuView = require("gui/submenu"); + * ``` + * + * This module depends on the `gui` module, which in turn depends on the + * `event_loop` module, so they _must_ be imported in this order. It is also + * recommended to conceptualize these modules first before using this one. + * + * # Example + * For an example refer to the GUI example. + * + * # View props + * - `header`: Text displayed at the top of the screen in bold + * - `items`: Array of selectable textual items + * + * @version Added in JS SDK 0.1 + * @module + */ + +import type { View, ViewFactory } from "."; +import type { Contract } from "../event_loop"; + +type Props = { + header: string, + items: string[], +}; +declare class Submenu extends View { + chosen: Contract; +} +declare class SubmenuFactory extends ViewFactory { } +declare const factory: SubmenuFactory; +export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/gui/text_box.d.ts b/applications/system/js_app/packages/fz-sdk/gui/text_box.d.ts new file mode 100644 index 000000000..a46ec73fa --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/text_box.d.ts @@ -0,0 +1,41 @@ +/** + * Displays a scrollable read-only text field. + * + * Sample screenshot of the view + * + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * let textBoxView = require("gui/text_box"); + * ``` + * + * This module depends on the `gui` module, which in turn depends on the + * `event_loop` module, so they _must_ be imported in this order. It is also + * recommended to conceptualize these modules first before using this one. + * + * # Example + * For an example refer to the `gui.js` example script. + * + * # View props + * - `text`: Text in the text box + * - `font`: The font to display the text in (`"text"` or `"hex"`) + * - `focus`: The initial focus of the text box (`"start"` or `"end"`) + * + * @version Added in JS SDK 0.1 + * @module + */ + +import type { View, ViewFactory } from "."; +import type { Contract } from "../event_loop"; + +type Props = { + text: string, + font: "text" | "hex", + focus: "start" | "end", +} +declare class TextBox extends View { + chosen: Contract; +} +declare class TextBoxFactory extends ViewFactory { } +declare const factory: TextBoxFactory; +export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/gui/text_input.d.ts b/applications/system/js_app/packages/fz-sdk/gui/text_input.d.ts new file mode 100644 index 000000000..6542ff01d --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/text_input.d.ts @@ -0,0 +1,47 @@ +/** + * Displays a text input keyboard. + * + * Sample screenshot of the view + * + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * let textInputView = require("gui/text_input"); + * ``` + * + * This module depends on the `gui` module, which in turn depends on the + * `event_loop` module, so they _must_ be imported in this order. It is also + * recommended to conceptualize these modules first before using this one. + * + * # Example + * For an example refer to the `gui.js` example script. + * + * # View props + * - `header`: Text displayed at the top of the screen + * - `minLength`: Minimum allowed text length + * - `maxLength`: Maximum allowed text length + * - `defaultText`: Text to show by default + * - `defaultTextClear`: Whether to clear the default text on next character typed + * - `illegalSymbols`: Whether to show illegal filename symbols, available with JS feature `gui-textinput-illegalsymbols` + * + * @version Added in JS SDK 0.1 + * @module + */ + +import type { View, ViewFactory } from "."; +import type { Contract } from "../event_loop"; + +type Props = { + header: string, + minLength: number, + maxLength: number, + defaultText: string, + defaultTextClear: boolean, + illegalSymbols: boolean, +} +declare class TextInput extends View { + input: Contract; +} +declare class TextInputFactory extends ViewFactory { } +declare const factory: TextInputFactory; +export = factory; diff --git a/applications/system/js_app/types/i2c/index.d.ts b/applications/system/js_app/packages/fz-sdk/i2c/index.d.ts similarity index 93% rename from applications/system/js_app/types/i2c/index.d.ts rename to applications/system/js_app/packages/fz-sdk/i2c/index.d.ts index ea0a640cb..7ce118fe5 100644 --- a/applications/system/js_app/types/i2c/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/i2c/index.d.ts @@ -1,3 +1,9 @@ +/** + * I2C bus communication + * @version Available with JS feature `i2c` + * @module + */ + /** * @brief Check if there is an I2C device ready on the bus * @param address The device address to check diff --git a/applications/system/js_app/types/math/index.d.ts b/applications/system/js_app/packages/fz-sdk/math/index.d.ts similarity index 52% rename from applications/system/js_app/types/math/index.d.ts rename to applications/system/js_app/packages/fz-sdk/math/index.d.ts index 4924eea7e..67c805db5 100644 --- a/applications/system/js_app/types/math/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/math/index.d.ts @@ -1,27 +1,60 @@ +/** + * Math operations + * @version Added in JS SDK 0.1 + * @module + */ + +/** @version Added in JS SDK 0.1 */ export function isEqual(a: number, b: number, tolerance: number): boolean; +/** @version Added in JS SDK 0.1 */ export function abs(n: number): number; +/** @version Added in JS SDK 0.1 */ export function acos(n: number): number; +/** @version Added in JS SDK 0.1 */ export function acosh(n: number): number; +/** @version Added in JS SDK 0.1 */ export function asin(n: number): number; +/** @version Added in JS SDK 0.1 */ export function asinh(n: number): number; +/** @version Added in JS SDK 0.1 */ export function atan(n: number): number; +/** @version Added in JS SDK 0.1 */ export function atan2(a: number, b: number): number; +/** @version Added in JS SDK 0.1 */ export function atanh(n: number): number; +/** @version Added in JS SDK 0.1 */ export function cbrt(n: number): number; +/** @version Added in JS SDK 0.1 */ export function ceil(n: number): number; +/** @version Added in JS SDK 0.1 */ export function clz32(n: number): number; +/** @version Added in JS SDK 0.1 */ export function cos(n: number): number; +/** @version Added in JS SDK 0.1 */ export function exp(n: number): number; +/** @version Added in JS SDK 0.1 */ export function floor(n: number): number; +/** @version Added in JS SDK 0.1 */ export function log(n: number): number; +/** @version Added in JS SDK 0.1 */ export function max(n: number, m: number): number; +/** @version Added in JS SDK 0.1 */ export function min(n: number, m: number): number; +/** @version Added in JS SDK 0.1 */ export function pow(n: number, m: number): number; +/** @version Added in JS SDK 0.1 */ export function random(): number; +/** @version Added in JS SDK 0.1 */ export function sign(n: number): number; +/** @version Added in JS SDK 0.1 */ export function sin(n: number): number; +/** @version Added in JS SDK 0.1 */ export function sqrt(n: number): number; +/** @version Added in JS SDK 0.1 */ export function trunc(n: number): number; +/** @version Added in JS SDK 0.1 */ declare const PI: number; +/** @version Added in JS SDK 0.1 */ declare const E: number; +/** @version Added in JS SDK 0.1 */ declare const EPSILON: number; diff --git a/applications/system/js_app/types/notification/index.d.ts b/applications/system/js_app/packages/fz-sdk/notification/index.d.ts similarity index 72% rename from applications/system/js_app/types/notification/index.d.ts rename to applications/system/js_app/packages/fz-sdk/notification/index.d.ts index 947daba21..499994d3a 100644 --- a/applications/system/js_app/types/notification/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/notification/index.d.ts @@ -1,20 +1,32 @@ +/** + * Module for using the color LED and vibration motor + * @version Added in JS SDK 0.1 + * @module + */ + /** * @brief Signals success to the user via the color LED, speaker and vibration * motor + * @version Added in JS SDK 0.1 */ export declare function success(): void; /** * @brief Signals failure to the user via the color LED, speaker and vibration * motor + * @version Added in JS SDK 0.1 */ export declare function error(): void; +/** + * @version Added in JS SDK 0.1 + */ export type Color = "red" | "green" | "blue" | "yellow" | "cyan" | "magenta"; /** * @brief Displays a basic color on the color LED * @param color The color to display, see `Color` * @param duration The duration, either `"short"` (10ms) or `"long"` (100ms) + * @version Added in JS SDK 0.1 */ export declare function blink(color: Color, duration: "short" | "long"): void; diff --git a/applications/system/js_app/packages/fz-sdk/package.json b/applications/system/js_app/packages/fz-sdk/package.json new file mode 100644 index 000000000..c39c5b9f8 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/package.json @@ -0,0 +1,30 @@ +{ + "name": "@next-flip/fz-sdk-mntm", + "version": "0.1.3", + "description": "Type declarations and documentation for native JS modules available on Momentum Custom Firmware for Flipper Zero", + "keywords": [ + "momentum", + "momentum firmware", + "next-flip", + "flipper", + "flipper zero", + "framework" + ], + "author": "Flipper Devices", + "license": "GPL-3.0-only", + "repository": { + "type": "git", + "url": "git+https://github.com/Next-Flip/Momentum-Firmware.git", + "directory": "applications/system/js_app/packages/fz-sdk" + }, + "type": "module", + "dependencies": { + "esbuild": "^0.24.0", + "esbuild-plugin-tsc": "^0.4.0", + "json5": "^2.2.3", + "typedoc": "^0.26.10", + "typedoc-material-theme": "^1.1.0", + "prompts": "^2.4.2", + "serialport": "^12.0.0" + } +} \ No newline at end of file diff --git a/applications/system/js_app/packages/fz-sdk/pnpm-lock.yaml b/applications/system/js_app/packages/fz-sdk/pnpm-lock.yaml new file mode 100644 index 000000000..45944a854 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/pnpm-lock.yaml @@ -0,0 +1,896 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + prompts: + specifier: ^2.4.2 + version: 2.4.2 + serialport: + specifier: ^12.0.0 + version: 12.0.0 + devDependencies: + esbuild: + specifier: ^0.24.0 + version: 0.24.0 + esbuild-plugin-tsc: + specifier: ^0.4.0 + version: 0.4.0(typescript@5.6.3) + json5: + specifier: ^2.2.3 + version: 2.2.3 + typedoc: + specifier: ^0.26.10 + version: 0.26.10(typescript@5.6.3) + typedoc-material-theme: + specifier: ^1.1.0 + version: 1.1.0(typedoc@0.26.10(typescript@5.6.3)) + +packages: + + '@esbuild/aix-ppc64@0.24.0': + resolution: {integrity: sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.24.0': + resolution: {integrity: sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.24.0': + resolution: {integrity: sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.24.0': + resolution: {integrity: sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.24.0': + resolution: {integrity: sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.24.0': + resolution: {integrity: sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.24.0': + resolution: {integrity: sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.24.0': + resolution: {integrity: sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.24.0': + resolution: {integrity: sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.24.0': + resolution: {integrity: sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.24.0': + resolution: {integrity: sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.24.0': + resolution: {integrity: sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.24.0': + resolution: {integrity: sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.24.0': + resolution: {integrity: sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.24.0': + resolution: {integrity: sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.24.0': + resolution: {integrity: sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.24.0': + resolution: {integrity: sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.24.0': + resolution: {integrity: sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.24.0': + resolution: {integrity: sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.24.0': + resolution: {integrity: sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.24.0': + resolution: {integrity: sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.24.0': + resolution: {integrity: sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.24.0': + resolution: {integrity: sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.24.0': + resolution: {integrity: sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@material/material-color-utilities@0.2.7': + resolution: {integrity: sha512-0FCeqG6WvK4/Cc06F/xXMd/pv4FeisI0c1tUpBbfhA2n9Y8eZEv4Karjbmf2ZqQCPUWMrGp8A571tCjizxoTiQ==} + + '@serialport/binding-mock@10.2.2': + resolution: {integrity: sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==} + engines: {node: '>=12.0.0'} + + '@serialport/bindings-cpp@12.0.1': + resolution: {integrity: sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==} + engines: {node: '>=16.0.0'} + + '@serialport/bindings-interface@1.2.2': + resolution: {integrity: sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==} + engines: {node: ^12.22 || ^14.13 || >=16} + + '@serialport/parser-byte-length@12.0.0': + resolution: {integrity: sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-cctalk@12.0.0': + resolution: {integrity: sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-delimiter@11.0.0': + resolution: {integrity: sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-delimiter@12.0.0': + resolution: {integrity: sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-inter-byte-timeout@12.0.0': + resolution: {integrity: sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-packet-length@12.0.0': + resolution: {integrity: sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==} + engines: {node: '>=8.6.0'} + + '@serialport/parser-readline@11.0.0': + resolution: {integrity: sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-readline@12.0.0': + resolution: {integrity: sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-ready@12.0.0': + resolution: {integrity: sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-regex@12.0.0': + resolution: {integrity: sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-slip-encoder@12.0.0': + resolution: {integrity: sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-spacepacket@12.0.0': + resolution: {integrity: sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==} + engines: {node: '>=12.0.0'} + + '@serialport/stream@12.0.0': + resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==} + engines: {node: '>=12.0.0'} + + '@shikijs/core@1.22.0': + resolution: {integrity: sha512-S8sMe4q71TJAW+qG93s5VaiihujRK6rqDFqBnxqvga/3LvqHEnxqBIOPkt//IdXVtHkQWKu4nOQNk0uBGicU7Q==} + + '@shikijs/engine-javascript@1.22.0': + resolution: {integrity: sha512-AeEtF4Gcck2dwBqCFUKYfsCq0s+eEbCEbkUuFou53NZ0sTGnJnJ/05KHQFZxpii5HMXbocV9URYVowOP2wH5kw==} + + '@shikijs/engine-oniguruma@1.22.0': + resolution: {integrity: sha512-5iBVjhu/DYs1HB0BKsRRFipRrD7rqjxlWTj4F2Pf+nQSPqc3kcyqFFeZXnBMzDf0HdqaFVvhDRAGiYNvyLP+Mw==} + + '@shikijs/types@1.22.0': + resolution: {integrity: sha512-Fw/Nr7FGFhlQqHfxzZY8Cwtwk5E9nKDUgeLjZgt3UuhcM3yJR9xj3ZGNravZZok8XmEZMiYkSMTPlPkULB8nww==} + + '@shikijs/vscode-textmate@9.3.0': + resolution: {integrity: sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@ungap/structured-clone@1.2.0': + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + esbuild-plugin-tsc@0.4.0: + resolution: {integrity: sha512-q9gWIovt1nkwchMLc2zhyksaiHOv3kDK4b0AUol8lkMCRhJ1zavgfb2fad6BKp7FT9rh/OHmEBXVjczLoi/0yw==} + peerDependencies: + typescript: ^4.0.0 || ^5.0.0 + + esbuild@0.24.0: + resolution: {integrity: sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==} + engines: {node: '>=18'} + hasBin: true + + hast-util-to-html@9.0.3: + resolution: {integrity: sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + lunr@2.3.9: + resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + micromark-util-character@2.1.0: + resolution: {integrity: sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==} + + micromark-util-encode@2.0.0: + resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==} + + micromark-util-sanitize-uri@2.0.0: + resolution: {integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==} + + micromark-util-symbol@2.0.0: + resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==} + + micromark-util-types@2.0.0: + resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + node-addon-api@7.0.0: + resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==} + + node-gyp-build@4.6.0: + resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} + hasBin: true + + oniguruma-to-js@0.4.3: + resolution: {integrity: sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + regex@4.3.3: + resolution: {integrity: sha512-r/AadFO7owAq1QJVeZ/nq9jNS1vyZt+6t1p/E59B56Rn2GCya+gr1KSyOzNL/er+r+B7phv5jG2xU2Nz1YkmJg==} + + serialport@12.0.0: + resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==} + engines: {node: '>=16.0.0'} + + shiki@1.22.0: + resolution: {integrity: sha512-/t5LlhNs+UOKQCYBtl5ZsH/Vclz73GIqT2yQsCBygr8L/ppTdmpL4w3kPLoZJbMKVWtoG77Ue1feOjZfDxvMkw==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-comments@2.0.1: + resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} + engines: {node: '>=10'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + typedoc-material-theme@1.1.0: + resolution: {integrity: sha512-LLWGVb8w+i+QGnsu/a0JKjcuzndFQt/UeGVOQz0HFFGGocROEHv5QYudIACrj+phL2LDwH05tJx0Ob3pYYH2UA==} + engines: {node: '>=18.0.0', npm: '>=8.6.0'} + peerDependencies: + typedoc: ^0.25.13 || ^0.26.3 + + typedoc@0.26.10: + resolution: {integrity: sha512-xLmVKJ8S21t+JeuQLNueebEuTVphx6IrP06CdV7+0WVflUSW3SPmR+h1fnWVdAR/FQePEgsSWCUHXqKKjzuUAw==} + engines: {node: '>= 18'} + hasBin: true + peerDependencies: + typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + yaml@2.6.0: + resolution: {integrity: sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==} + engines: {node: '>= 14'} + hasBin: true + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@esbuild/aix-ppc64@0.24.0': + optional: true + + '@esbuild/android-arm64@0.24.0': + optional: true + + '@esbuild/android-arm@0.24.0': + optional: true + + '@esbuild/android-x64@0.24.0': + optional: true + + '@esbuild/darwin-arm64@0.24.0': + optional: true + + '@esbuild/darwin-x64@0.24.0': + optional: true + + '@esbuild/freebsd-arm64@0.24.0': + optional: true + + '@esbuild/freebsd-x64@0.24.0': + optional: true + + '@esbuild/linux-arm64@0.24.0': + optional: true + + '@esbuild/linux-arm@0.24.0': + optional: true + + '@esbuild/linux-ia32@0.24.0': + optional: true + + '@esbuild/linux-loong64@0.24.0': + optional: true + + '@esbuild/linux-mips64el@0.24.0': + optional: true + + '@esbuild/linux-ppc64@0.24.0': + optional: true + + '@esbuild/linux-riscv64@0.24.0': + optional: true + + '@esbuild/linux-s390x@0.24.0': + optional: true + + '@esbuild/linux-x64@0.24.0': + optional: true + + '@esbuild/netbsd-x64@0.24.0': + optional: true + + '@esbuild/openbsd-arm64@0.24.0': + optional: true + + '@esbuild/openbsd-x64@0.24.0': + optional: true + + '@esbuild/sunos-x64@0.24.0': + optional: true + + '@esbuild/win32-arm64@0.24.0': + optional: true + + '@esbuild/win32-ia32@0.24.0': + optional: true + + '@esbuild/win32-x64@0.24.0': + optional: true + + '@material/material-color-utilities@0.2.7': {} + + '@serialport/binding-mock@10.2.2': + dependencies: + '@serialport/bindings-interface': 1.2.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + '@serialport/bindings-cpp@12.0.1': + dependencies: + '@serialport/bindings-interface': 1.2.2 + '@serialport/parser-readline': 11.0.0 + debug: 4.3.4 + node-addon-api: 7.0.0 + node-gyp-build: 4.6.0 + transitivePeerDependencies: + - supports-color + + '@serialport/bindings-interface@1.2.2': {} + + '@serialport/parser-byte-length@12.0.0': {} + + '@serialport/parser-cctalk@12.0.0': {} + + '@serialport/parser-delimiter@11.0.0': {} + + '@serialport/parser-delimiter@12.0.0': {} + + '@serialport/parser-inter-byte-timeout@12.0.0': {} + + '@serialport/parser-packet-length@12.0.0': {} + + '@serialport/parser-readline@11.0.0': + dependencies: + '@serialport/parser-delimiter': 11.0.0 + + '@serialport/parser-readline@12.0.0': + dependencies: + '@serialport/parser-delimiter': 12.0.0 + + '@serialport/parser-ready@12.0.0': {} + + '@serialport/parser-regex@12.0.0': {} + + '@serialport/parser-slip-encoder@12.0.0': {} + + '@serialport/parser-spacepacket@12.0.0': {} + + '@serialport/stream@12.0.0': + dependencies: + '@serialport/bindings-interface': 1.2.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + '@shikijs/core@1.22.0': + dependencies: + '@shikijs/engine-javascript': 1.22.0 + '@shikijs/engine-oniguruma': 1.22.0 + '@shikijs/types': 1.22.0 + '@shikijs/vscode-textmate': 9.3.0 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.3 + + '@shikijs/engine-javascript@1.22.0': + dependencies: + '@shikijs/types': 1.22.0 + '@shikijs/vscode-textmate': 9.3.0 + oniguruma-to-js: 0.4.3 + + '@shikijs/engine-oniguruma@1.22.0': + dependencies: + '@shikijs/types': 1.22.0 + '@shikijs/vscode-textmate': 9.3.0 + + '@shikijs/types@1.22.0': + dependencies: + '@shikijs/vscode-textmate': 9.3.0 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@9.3.0': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/unist@3.0.3': {} + + '@ungap/structured-clone@1.2.0': {} + + argparse@2.0.1: {} + + balanced-match@1.0.2: {} + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + ccount@2.0.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + comma-separated-tokens@2.0.3: {} + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + dequal@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + entities@4.5.0: {} + + esbuild-plugin-tsc@0.4.0(typescript@5.6.3): + dependencies: + strip-comments: 2.0.1 + typescript: 5.6.3 + + esbuild@0.24.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.24.0 + '@esbuild/android-arm': 0.24.0 + '@esbuild/android-arm64': 0.24.0 + '@esbuild/android-x64': 0.24.0 + '@esbuild/darwin-arm64': 0.24.0 + '@esbuild/darwin-x64': 0.24.0 + '@esbuild/freebsd-arm64': 0.24.0 + '@esbuild/freebsd-x64': 0.24.0 + '@esbuild/linux-arm': 0.24.0 + '@esbuild/linux-arm64': 0.24.0 + '@esbuild/linux-ia32': 0.24.0 + '@esbuild/linux-loong64': 0.24.0 + '@esbuild/linux-mips64el': 0.24.0 + '@esbuild/linux-ppc64': 0.24.0 + '@esbuild/linux-riscv64': 0.24.0 + '@esbuild/linux-s390x': 0.24.0 + '@esbuild/linux-x64': 0.24.0 + '@esbuild/netbsd-x64': 0.24.0 + '@esbuild/openbsd-arm64': 0.24.0 + '@esbuild/openbsd-x64': 0.24.0 + '@esbuild/sunos-x64': 0.24.0 + '@esbuild/win32-arm64': 0.24.0 + '@esbuild/win32-ia32': 0.24.0 + '@esbuild/win32-x64': 0.24.0 + + hast-util-to-html@9.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + html-void-elements@3.0.0: {} + + json5@2.2.3: {} + + kleur@3.0.3: {} + + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + lunr@2.3.9: {} + + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.2.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.0 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdurl@2.0.0: {} + + micromark-util-character@2.1.0: + dependencies: + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-encode@2.0.0: {} + + micromark-util-sanitize-uri@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-encode: 2.0.0 + micromark-util-symbol: 2.0.0 + + micromark-util-symbol@2.0.0: {} + + micromark-util-types@2.0.0: {} + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + ms@2.1.2: {} + + node-addon-api@7.0.0: {} + + node-gyp-build@4.6.0: {} + + oniguruma-to-js@0.4.3: + dependencies: + regex: 4.3.3 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + property-information@6.5.0: {} + + punycode.js@2.3.1: {} + + regex@4.3.3: {} + + serialport@12.0.0: + dependencies: + '@serialport/binding-mock': 10.2.2 + '@serialport/bindings-cpp': 12.0.1 + '@serialport/parser-byte-length': 12.0.0 + '@serialport/parser-cctalk': 12.0.0 + '@serialport/parser-delimiter': 12.0.0 + '@serialport/parser-inter-byte-timeout': 12.0.0 + '@serialport/parser-packet-length': 12.0.0 + '@serialport/parser-readline': 12.0.0 + '@serialport/parser-ready': 12.0.0 + '@serialport/parser-regex': 12.0.0 + '@serialport/parser-slip-encoder': 12.0.0 + '@serialport/parser-spacepacket': 12.0.0 + '@serialport/stream': 12.0.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + shiki@1.22.0: + dependencies: + '@shikijs/core': 1.22.0 + '@shikijs/engine-javascript': 1.22.0 + '@shikijs/engine-oniguruma': 1.22.0 + '@shikijs/types': 1.22.0 + '@shikijs/vscode-textmate': 9.3.0 + '@types/hast': 3.0.4 + + sisteransi@1.0.5: {} + + space-separated-tokens@2.0.2: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-comments@2.0.1: {} + + trim-lines@3.0.1: {} + + typedoc-material-theme@1.1.0(typedoc@0.26.10(typescript@5.6.3)): + dependencies: + '@material/material-color-utilities': 0.2.7 + typedoc: 0.26.10(typescript@5.6.3) + + typedoc@0.26.10(typescript@5.6.3): + dependencies: + lunr: 2.3.9 + markdown-it: 14.1.0 + minimatch: 9.0.5 + shiki: 1.22.0 + typescript: 5.6.3 + yaml: 2.6.0 + + typescript@5.6.3: {} + + uc.micro@2.1.0: {} + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.2 + + yaml@2.6.0: {} + + zwitch@2.0.4: {} diff --git a/applications/system/js_app/packages/fz-sdk/sdk.js b/applications/system/js_app/packages/fz-sdk/sdk.js new file mode 100644 index 000000000..55c3c2bb0 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/sdk.js @@ -0,0 +1,182 @@ +#!/usr/bin/env node +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { SerialPort } from "serialport"; +import prompts from "prompts"; +import esbuild from "esbuild"; +import json5 from "json5"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const cfwSdkName = "@next-flip/fz-sdk-mntm/"; +const ofwSdkName = "@flipperdevices/fz-sdk/"; + +async function build(config) { + await esbuild.build({ + entryPoints: ["./dist/index.js"], + outfile: config.output, + tsconfig: "./tsconfig.json", + format: "cjs", + bundle: true, + minify: config.minify, + external: [ + "@next-flip/fz-sdk-mntm/*" + ], + supported: { + "array-spread": false, + "arrow": false, + "async-await": false, + "async-generator": false, + "bigint": false, + "class": false, + "const-and-let": true, + "decorators": false, + "default-argument": false, + "destructuring": false, + "dynamic-import": false, + "exponent-operator": false, + "export-star-as": false, + "for-await": false, + "for-of": false, + "function-name-configurable": false, + "function-or-class-property-access": false, + "generator": false, + "hashbang": false, + "import-assertions": false, + "import-meta": false, + "inline-script": false, + "logical-assignment": false, + "nested-rest-binding": false, + "new-target": false, + "node-colon-prefix-import": false, + "node-colon-prefix-require": false, + "nullish-coalescing": false, + "object-accessors": false, + "object-extensions": false, + "object-rest-spread": false, + "optional-catch-binding": false, + "optional-chain": false, + "regexp-dot-all-flag": false, + "regexp-lookbehind-assertions": false, + "regexp-match-indices": false, + "regexp-named-capture-groups": false, + "regexp-set-notation": false, + "regexp-sticky-and-unicode-flags": false, + "regexp-unicode-property-escapes": false, + "rest-argument": false, + "template-literal": false, + "top-level-await": false, + "typeof-exotic-object-is-object": false, + "unicode-escapes": false, + "using": false, + }, + }); + + let outContents = fs.readFileSync(config.output, "utf8"); + outContents = "let exports = {};\n" + outContents; + + // Transform CFW SDK name to OFW SDK name so all firmwares understand it + outContents = outContents.replaceAll(`require("${cfwSdkName}`, `require("${ofwSdkName}`); + + if (config.enforceSdkVersion) { + const version = json5.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8")).version; + let [major, minor, _] = version.split("."); + outContents = `checkSdkCompatibility(${major}, ${minor});\n${outContents}`; + } + + fs.writeFileSync(config.output, outContents); +} + +async function upload(config) { + const appFile = fs.readFileSync(config.input, "utf8"); + const flippers = (await SerialPort.list()).filter(x => x.serialNumber?.startsWith("flip_")); + + if (!flippers) { + console.error("No Flippers found"); + process.exit(1); + } + + let portPath = flippers[0].path; + if (flippers.length > 1) { + port = (await prompts([{ + type: "select", + name: "port", + message: "Select Flipper to run the app on", + choices: flippers.map(x => ({ title: x.serialNumber.replace("flip_", ""), value: x.path })), + }])).port; + } + + console.log(`Connecting to Flipper at ${portPath}`); + let port = new SerialPort({ path: portPath, baudRate: 230400 }); + let received = ""; + let lastMatch = 0; + async function waitFor(string, timeoutMs) { + return new Promise((resolve, _reject) => { + let timeout = undefined; + if (timeoutMs) { + timeout = setTimeout(() => { + console.error("Error: timeout"); + process.exit(1); + }, timeoutMs); + } + setInterval(() => { + let idx = received.indexOf(string, lastMatch); + if (idx !== -1) { + lastMatch = idx; + if (timeoutMs) + clearTimeout(timeout); + resolve(); + } + }, 50); + }); + } + port.on("data", (data) => { + received += data.toString(); + }); + + await waitFor(">: ", 1000); + console.log("Uploading application file"); + port.write(`storage remove ${config.output}\x0d`); + port.drain(); + await waitFor(">: ", 1000); + port.write(`storage write_chunk ${config.output} ${appFile.length}\x0d`); + await waitFor("Ready", 1000); + port.write(appFile); + port.drain(); + await waitFor(">: ", 1000); + + console.log("Launching application"); + port.write(`js ${config.output}\x0d`); + port.drain(); + + await waitFor("Running", 1000); + process.stdout.write(received.slice(lastMatch)); + port.on("data", (data) => { + process.stdout.write(data.toString()); + }); + process.on("exit", () => { + port.write("\x03"); + }); + + await waitFor("Script done!", 0); + process.exit(0); +} + +(async () => { + const commands = { + "build": build, + "upload": upload, + }; + + const config = json5.parse(fs.readFileSync("./fz-sdk.config.json5", "utf8")); + const command = process.argv[2]; + + if (!Object.keys(commands).includes(command)) { + console.error(`Unknown command ${command}. Supported: ${Object.keys(commands).join(", ")}`); + process.exit(1); + } + + await commands[command](config[command]); +})(); diff --git a/applications/system/js_app/types/serial/index.d.ts b/applications/system/js_app/packages/fz-sdk/serial/index.d.ts similarity index 92% rename from applications/system/js_app/types/serial/index.d.ts rename to applications/system/js_app/packages/fz-sdk/serial/index.d.ts index 680f9bd6e..3c249352e 100644 --- a/applications/system/js_app/types/serial/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/serial/index.d.ts @@ -1,3 +1,9 @@ +/** + * Module for accessing the serial port + * @version Added in JS SDK 0.1 + * @module + */ + /** * @brief Initializes the serial port * @@ -5,6 +11,7 @@ * * @param port The port to initialize (`"lpuart"` or `"start"`) * @param baudRate + * @version Added in JS SDK 0.1 */ export declare function setup(port: "lpuart" | "usart", baudRate: number): void; @@ -16,6 +23,7 @@ export declare function setup(port: "lpuart" | "usart", baudRate: number): void; * - Arrays of numbers will get sent as a sequence of bytes. * - `ArrayBuffer`s and `TypedArray`s will be sent as a sequence * of bytes. + * @version Added in JS SDK 0.1 */ export declare function write(value: string | number | number[] | ArrayBuffer | TypedArray): void; @@ -27,6 +35,7 @@ export declare function write(value: string | number | nu * unset, the function will wait forever. * @returns The received data interpreted as ASCII, or `undefined` if 0 bytes * were read. + * @version Added in JS SDK 0.1 */ export declare function read(length: number, timeout?: number): string | undefined; @@ -42,6 +51,7 @@ export declare function read(length: number, timeout?: number): string | undefin * applies to characters, not entire strings. * @returns The received data interpreted as ASCII, or `undefined` if 0 bytes * were read. + * @version Added in JS SDK 0.1 */ export declare function readln(timeout?: number): string; @@ -55,6 +65,7 @@ export declare function readln(timeout?: number): string; * wait forever. * @returns The received data interpreted as ASCII, or `undefined` if 0 bytes * were read. + * @version Added in JS SDK 0.1 */ export declare function readAny(timeout?: number): string | undefined; @@ -66,6 +77,7 @@ export declare function readAny(timeout?: number): string | undefined; * unset, the function will wait forever. * @returns The received data as an ArrayBuffer, or `undefined` if 0 bytes were * read. + * @version Added in JS SDK 0.1 */ export declare function readBytes(length: number, timeout?: number): ArrayBuffer; @@ -89,10 +101,12 @@ export declare function readBytes(length: number, timeout?: number): ArrayBuffer * @returns The index of the matched pattern if multiple were provided, or 0 if * only one was provided and it matched, or `undefined` if none of the * patterns matched. + * @version Added in JS SDK 0.1 */ export declare function expect(patterns: string | number[] | string[] | number[][], timeout?: number): number | undefined; /** * @brief Deinitializes the serial port, allowing multiple initializations per script run + * @version Added in JS SDK 0.1 */ export declare function end(): void; diff --git a/applications/system/js_app/types/spi/index.d.ts b/applications/system/js_app/packages/fz-sdk/spi/index.d.ts similarity index 91% rename from applications/system/js_app/types/spi/index.d.ts rename to applications/system/js_app/packages/fz-sdk/spi/index.d.ts index 8d72bc29c..e4bb7774b 100644 --- a/applications/system/js_app/types/spi/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/spi/index.d.ts @@ -1,3 +1,9 @@ +/** + * SPI bus communication + * @version Available with JS feature `spi` + * @module + */ + /** * @brief Acquire SPI bus */ diff --git a/applications/system/js_app/types/storage/index.d.ts b/applications/system/js_app/packages/fz-sdk/storage/index.d.ts similarity index 82% rename from applications/system/js_app/types/storage/index.d.ts rename to applications/system/js_app/packages/fz-sdk/storage/index.d.ts index 7e12627e9..588df2cf0 100644 --- a/applications/system/js_app/types/storage/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/storage/index.d.ts @@ -1,8 +1,15 @@ +/** + * Module for accessing the filesystem + * @version Added in JS SDK 0.1 + * @module + */ + /** * File readability mode: * - `"r"`: read-only * - `"w"`: write-only * - `"rw"`: read-write + * @version Added in JS SDK 0.1 */ export type AccessMode = "r" | "w" | "rw"; @@ -13,53 +20,78 @@ export type AccessMode = "r" | "w" | "rw"; * - `"open_append"`: open file and set r/w pointer to EOF, or create a new one if it doesn't exist * - `"create_new"`: create new file or fail if it exists * - `"create_always"`: truncate and open file, or create a new empty one if it doesn't exist + * @version Added in JS SDK 0.1 */ export type OpenMode = "open_existing" | "open_always" | "open_append" | "create_new" | "create_always"; -/** Standard UNIX timestamp */ +/** + * Standard UNIX timestamp + * @version Added in JS SDK 0.1 + */ export type Timestamp = number; -/** File information structure */ +/** + * File information structure + * @version Added in JS SDK 0.1 + */ export declare class FileInfo { /** * Full path (e.g. "/ext/test", returned by `stat`) or file name * (e.g. "test", returned by `readDirectory`) + * @version Added in JS SDK 0.1 */ path: string; /** * Is the file a directory? + * @version Added in JS SDK 0.1 */ isDirectory: boolean; /** * File size in bytes, or 0 in the case of directories + * @version Added in JS SDK 0.1 */ size: number; /** * Time of last access as a UNIX timestamp + * @version Added in JS SDK 0.1 */ accessTime: Timestamp; } -/** Filesystem information structure */ +/** + * Filesystem information structure + * @version Added in JS SDK 0.1 + */ export declare class FsInfo { - /** Total size of the filesystem, in bytes */ + /** + * Total size of the filesystem, in bytes + * @version Added in JS SDK 0.1 + */ totalSpace: number; - /** Free space in the filesystem, in bytes */ + /** + * Free space in the filesystem, in bytes + * @version Added in JS SDK 0.1 + */ freeSpace: number; } // file operations -/** File class */ +/** + * File class + * @version Added in JS SDK 0.1 + */ export declare class File { /** * Closes the file. After this method is called, all other operations * related to this file become unavailable. * @returns `true` on success, `false` on failure + * @version Added in JS SDK 0.1 */ close(): boolean; /** * Is the file currently open? + * @version Added in JS SDK 0.1 */ isOpen(): boolean; /** @@ -70,6 +102,7 @@ export declare class File { * @returns an `ArrayBuf` if the mode is `"binary"`, a `string` if the mode * is `ascii`. The number of bytes that was actually read may be * fewer than requested. + * @version Added in JS SDK 0.1 */ read(mode: T extends ArrayBuffer ? "binary" : "ascii", bytes: number): T; /** @@ -77,36 +110,43 @@ export declare class File { * @param data The data to write: a string that will be ASCII-encoded, or an * ArrayBuf * @returns the amount of bytes that was actually written + * @version Added in JS SDK 0.1 */ write(data: ArrayBuffer | string): number; /** * Moves the R/W pointer forward * @param bytes How many bytes to move the pointer forward by * @returns `true` on success, `false` on failure + * @version Added in JS SDK 0.1 */ seekRelative(bytes: number): boolean; /** * Moves the R/W pointer to an absolute position inside the file * @param bytes The position inside the file * @returns `true` on success, `false` on failure + * @version Added in JS SDK 0.1 */ seekAbsolute(bytes: number): boolean; /** * Gets the absolute position of the R/W pointer in bytes + * @version Added in JS SDK 0.1 */ tell(): number; /** * Discards the data after the current position of the R/W pointer in a file * opened in either write-only or read-write mode. * @returns `true` on success, `false` on failure + * @version Added in JS SDK 0.1 */ truncate(): boolean; /** * Reads the total size of the file in bytes + * @version Added in JS SDK 0.1 */ size(): number; /** * Detects whether the R/W pointer has reached the end of the file + * @version Added in JS SDK 0.1 */ eof(): boolean; /** @@ -115,6 +155,7 @@ export declare class File { * @param dest The file to copy the bytes into * @param bytes The number of bytes to copy * @returns `true` on success, `false` on failure + * @version Added in JS SDK 0.1 */ copyTo(dest: File, bytes: number): boolean; } @@ -126,12 +167,14 @@ export declare class File { * @param openMode `"open_existing"`, `"open_always"`, `"open_append"`, * `"create_new"` or `"create_always"`; see `OpenMode` * @returns a `File` on success, or `undefined` on failure + * @version Added in JS SDK 0.1 */ export declare function openFile(path: string, accessMode: AccessMode, openMode: OpenMode): File | undefined; /** * Detects whether a file exists * @param path The path to the file * @returns `true` on success, `false` on failure + * @version Added in JS SDK 0.1 */ export declare function fileExists(path: string): boolean; @@ -142,17 +185,20 @@ export declare function fileExists(path: string): boolean; * @param path The path to the directory * @returns Array of `FileInfo` structures with directory entries, * or `undefined` on failure + * @version Added in JS SDK 0.1 */ export declare function readDirectory(path: string): FileInfo[] | undefined; /** * Detects whether a directory exists * @param path The path to the directory + * @version Added in JS SDK 0.1 */ export declare function directoryExists(path: string): boolean; /** * Creates an empty directory * @param path The path to the new directory * @returns `true` on success, `false` on failure + * @version Added in JS SDK 0.1 */ export declare function makeDirectory(path: string): boolean; @@ -161,24 +207,28 @@ export declare function makeDirectory(path: string): boolean; /** * Detects whether a file or a directory exists * @param path The path to the file or directory + * @version Added in JS SDK 0.1 */ export declare function fileOrDirExists(path: string): boolean; /** * Acquires metadata about a file or directory * @param path The path to the file or directory * @returns A `FileInfo` structure or `undefined` on failure + * @version Added in JS SDK 0.1 */ export declare function stat(path: string): FileInfo | undefined; /** * Removes a file or an empty directory * @param path The path to the file or directory * @returns `true` on success, `false` on failure + * @version Added in JS SDK 0.1 */ export declare function remove(path: string): boolean; /** * Removes a file or recursively removes a possibly non-empty directory * @param path The path to the file or directory * @returns `true` on success, `false` on failure + * @version Added in JS SDK 0.1 */ export declare function rmrf(path: string): boolean; /** @@ -187,6 +237,7 @@ export declare function rmrf(path: string): boolean; * @param newPath The new path that the file or directory will become accessible * under * @returns `true` on success, `false` on failure + * @version Added in JS SDK 0.1 */ export declare function rename(oldPath: string, newPath: string): boolean; /** @@ -194,11 +245,13 @@ export declare function rename(oldPath: string, newPath: string): boolean; * @param oldPath The original path to the file or directory * @param newPath The new path that the copy of the file or directory will be * accessible under + * @version Added in JS SDK 0.1 */ export declare function copy(oldPath: string, newPath: string): boolean; /** * Fetches generic information about a filesystem * @param filesystem The path to the filesystem (e.g. `"/ext"` or `"/int"`) + * @version Added in JS SDK 0.1 */ export declare function fsInfo(filesystem: string): FsInfo | undefined; /** @@ -218,6 +271,7 @@ export declare function fsInfo(filesystem: string): FsInfo | undefined; * @param maxLen The maximum length of the filename with the numeric suffix * @returns The base of the filename with the next available numeric suffix, * without the extension or the base directory. + * @version Added in JS SDK 0.1 */ export declare function nextAvailableFilename(dirPath: string, fileName: string, fileExt: string, maxLen: number): string; @@ -226,6 +280,7 @@ export declare function nextAvailableFilename(dirPath: string, fileName: string, /** * Determines whether the two paths are equivalent. Respects filesystem-defined * path equivalence rules. + * @version Added in JS SDK 0.1 */ export declare function arePathsEqual(path1: string, path2: string): boolean; /** @@ -233,6 +288,7 @@ export declare function arePathsEqual(path1: string, path2: string): boolean; * filesystem-defined path equivalence rules. * @param parentPath The parent path * @param childPath The child path + * @version Added in JS SDK 0.1 */ export declare function isSubpathOf(parentPath: string, childPath: string): boolean; @@ -242,15 +298,18 @@ export declare function isSubpathOf(parentPath: string, childPath: string): bool * Initialize virtual mount api with disk image at given path. * Errors on failure. * @param parentPath Path to disk image file + * @version Available with JS feature `storage-virtual` */ export declare function virtualInit(path: string): void; /** * Mount selected disk image at /mnt path. * Errors on failure. + * @version Available with JS feature `storage-virtual` */ export declare function virtualMount(): void; /** * Deinitialize virtual mount api. * Errors on failure. + * @version Available with JS feature `storage-virtual` */ export declare function virtualQuit(): void; diff --git a/applications/system/js_app/packages/fz-sdk/subghz/index.d.ts b/applications/system/js_app/packages/fz-sdk/subghz/index.d.ts new file mode 100644 index 000000000..9af9e67a9 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/subghz/index.d.ts @@ -0,0 +1,64 @@ +/** + * Module for using Sub-GHz transciever + * @version Available with JS feature `subghz` + * @module + */ + +/** + * @brief Initialize Sub-GHz module + */ +export declare function setup(): void; + +/** + * @brief Deinitialize Sub-GHz module + */ +export declare function end(): void; + +/** + * @brief Set radio to receive mode + */ +export declare function setRx(): void; + +/** + * @brief Set radio to idle mode + */ +export declare function setIdle(): void; + +/** + * @brief Return current RSSI on current frequency, or undefined if radio is not in receive mode + */ +export declare function getRssi(): number | undefined; + +type RadioState = "RX" | "TX" | "IDLE" | ""; + +/** + * @brief Get current radio mode/state + */ +export declare function getState(): RadioState; + +/** + * @brief Get currently selected frequency + */ +export declare function getFrequency(): number; + +/** + * @brief Change current frequency, radio must be in idle mode + * + * Returns the effective frequency, since radio module cant use all precise + * values and instead chooses closest one available + * + * @param frequency What frequency to use + */ +export declare function setFrequency(frequency: number): number; + +/** + * @brief Check whether the radio module in use is internal or external + */ +export declare function isExternal(): boolean; + +/** + * @brief Transmit a .sub file, return true on success or error on failure + * @param path What .sub file to transmit + * @param repeat How many times to repeat the signal + */ +export declare function transmitFile(path: string, repeat?: number): true; diff --git a/applications/system/js_app/types/tests/index.d.ts b/applications/system/js_app/packages/fz-sdk/tests/index.d.ts similarity index 88% rename from applications/system/js_app/types/tests/index.d.ts rename to applications/system/js_app/packages/fz-sdk/tests/index.d.ts index 8aaeec5e5..031588d4a 100644 --- a/applications/system/js_app/types/tests/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/tests/index.d.ts @@ -1,6 +1,8 @@ /** * Unit test module. Only available if the firmware has been configured with * `FIRMWARE_APP_SET=unit_tests`. + * @version Added in JS SDK 0.1 + * @module */ export function fail(message: string): never; diff --git a/applications/system/js_app/packages/fz-sdk/tsconfig.json b/applications/system/js_app/packages/fz-sdk/tsconfig.json new file mode 100644 index 000000000..cfb792e3f --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "checkJs": true, + "module": "CommonJS", + "noLib": true, + }, + "include": [ + "./**/*.d.ts" + ], + "exclude": [ + "node_modules", + ] +} \ No newline at end of file diff --git a/applications/system/js_app/packages/fz-sdk/typedoc.json b/applications/system/js_app/packages/fz-sdk/typedoc.json new file mode 100644 index 000000000..8b3befa6d --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/typedoc.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "name": "Flipper Zero JS API", + "excludePrivate": true, + "entryPointStrategy": "expand", + "entryPoints": [ + ".", + ], + "exclude": [ + "node_modules" + ], + "cleanOutputDir": true, + "out": "./docs", + "plugin": [ + "typedoc-material-theme", + ], + "readme": "./docs_readme.md", + "themeColor": "#ff8200", +} \ No newline at end of file diff --git a/applications/system/js_app/packages/fz-sdk/usbdisk/index.d.ts b/applications/system/js_app/packages/fz-sdk/usbdisk/index.d.ts new file mode 100644 index 000000000..c6c8a5309 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/usbdisk/index.d.ts @@ -0,0 +1,32 @@ +/** + * Module for USB mass storage emulation + * @version Available with JS feature `usbdisk` + * @module + */ + +/** + * @brief Create a disk image and format it as FatFs, error on failure + * @param path Where to create the disk image + * @param size How big the disk image should be + * @version Available with JS feature `usbdisk-createimage` + */ +export declare function createImage(path: string, size: number): void; + +/** + * @brief Start emulating mass storage device + * @param path The disk image to emulate + */ +export declare function start(path: string): void; + +/** + * @brief Stop emulating mass storage device + */ +export declare function stop(): void; + +/** + * @brief Check if the mass storage device was exected + * + * Useful as a loop condition with a delay, so UsbDisk keeps running until ejected + * + */ +export declare function wasEjected(): boolean; diff --git a/applications/system/js_app/packages/fz-sdk/vgm/index.d.ts b/applications/system/js_app/packages/fz-sdk/vgm/index.d.ts new file mode 100644 index 000000000..a6bef6f16 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/vgm/index.d.ts @@ -0,0 +1,32 @@ +/** + * Module for interactive with Flipper Video Game Module (VGM) + * @version Available with JS feature `vgm` + * @module + */ + +/** + * @brief Get current VGM pitch, or undefined if VGM not present + */ +export declare function getPitch(): number | undefined; + +/** + * @brief Get current VGM roll, or undefined if VGM not present + */ +export declare function getRoll(): number | undefined; + +/** + * @brief Get current VGM yaw, or undefined if VGM not present + */ +export declare function getYaw(): number | undefined; + +/** + * @brief Wait until yaw changed by specified amount + * + * Returns how much the yaw changed from initial value if it exceeded the + * specified yaw angle, or returns 0 if it was not exceeded within the timeout + * Or returns undefined if VGM is not present + * + * @param angle How much the yaw needs to change + * @param timeout Maximum time in milliseconds to wait for specified yaw change, default 3000ms + */ +export declare function deltaYaw(angle: number, timeout?: number): number | undefined; diff --git a/applications/system/js_app/packages/fz-sdk/widget/index.d.ts b/applications/system/js_app/packages/fz-sdk/widget/index.d.ts new file mode 100644 index 000000000..bbbee7f44 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/widget/index.d.ts @@ -0,0 +1,140 @@ +/** + * Displays a customizable Widget on screen + * @version Available with JS feature `widget` + * @module + */ + +type ComponentId = number; + +/** + * @brief Add a box component + * @param x Horizontal position + * @param y Vertical position + * @param w Width + * @param h Height + */ +export declare function addBox(x: number, y: number, w: number, h: number): ComponentId; + +/** + * @brief Add a circle component + * @param x Horizontal position + * @param y Vertical position + * @param r Radius + */ +export declare function addCircle(x: number, y: number, r: number): ComponentId; + +/** + * @brief Add a disc component + * @param x Horizontal position + * @param y Vertical position + * @param r Radius + */ +export declare function addDisc(x: number, y: number, r: number): ComponentId; + +/** + * @brief Add a dot component + * @param x Horizontal position + * @param y Vertical position + */ +export declare function addDot(x: number, y: number): ComponentId; + +/** + * @brief Add a frame component + * @param x Horizontal position + * @param y Vertical position + * @param w Width + * @param h Height + */ +export declare function addFrame(x: number, y: number, w: number, h: number): ComponentId; + +/** + * @brief Add a glyph component + * @param x Horizontal position + * @param y Vertical position + * @param ch ASCII character code (eg. `"C".charCodeAt(0)`) + */ +export declare function addGlyph(x: number, y: number, ch: number): ComponentId; + +/** + * @brief Add an icon component + * @param x Horizontal position + * @param y Vertical position + * @param icon Name of the icon (eg. `"ButtonUp_7x4"`) + * @version Available with JS feature `widget-addicon` + */ +export declare function addIcon(x: number, y: number, icon: string): ComponentId; + +/** + * @brief Add a line component + * @param x1 Horizontal position 1 + * @param y1 Vertical position 1 + * @param x2 Horizontal position 2 + * @param y2 Vertical position 2 + */ +export declare function addLine(x1: number, y1: number, x2: number, y2: number): ComponentId; + +/** + * @brief Add a rounded box component + * @param x Horizontal position + * @param y Vertical position + * @param w Width + * @param h Height + * @param r Radius + */ +export declare function addRbox(x: number, y: number, w: number, h: number, r: number): ComponentId; + +/** + * @brief Add a rounded frame component + * @param x Horizontal position + * @param y Vertical position + * @param w Width + * @param h Height + * @param r Radius + */ +export declare function addRframe(x: number, y: number, w: number, h: number, r: number): ComponentId; + +/** + * @brief Add a text component + * @param x Horizontal position + * @param y Vertical position + * @param font What font to use, Primary or Secondary + * @param text Text to display + */ +export declare function addText(x: number, y: number, font: "Primary" | "Secondary", text: string): ComponentId; + +type XbmId = number; + +/** + * @brief Add an xbm image component + * @param x Horizontal position + * @param y Vertical position + * @param index Loaded xbm id to use + */ +export declare function addXbm(x: number, y: number, index: XbmId): ComponentId; + +/** + * @brief Load an xbm image sprite + * @param path Xbm file to load + */ +export declare function loadImageXbm(path: string): XbmId; + +/** + * @brief Remove a component + * @param id Component id to remove + */ +export declare function remove(id: ComponentId): boolean; + +/** + * @brief Check if the widget view is shown + */ +export declare function isOpen(): boolean; + +/** + * @brief Show the widget view + */ +export declare function show(): void; + +/** + * @brief Close the widget view + */ +export declare function close(): void; diff --git a/applications/system/js_app/types/event_loop/index.d.ts b/applications/system/js_app/types/event_loop/index.d.ts deleted file mode 100644 index 49237782c..000000000 --- a/applications/system/js_app/types/event_loop/index.d.ts +++ /dev/null @@ -1,70 +0,0 @@ -type Lit = undefined | null | {}; - -/** - * Subscription control interface - */ -export interface Subscription { - /** - * Cancels the subscription, preventing any future events managed by the - * subscription from firing - */ - cancel(): void; -} - -/** - * Opaque event source identifier - */ -export type Contract = symbol; - -/** - * A callback can be assigned to an event loop to listen to an event. It may - * return an array with values that will be passed to it as arguments the next - * time that it is called. The first argument is always the subscription - * manager, and the second argument is always the item that trigged the event. - * The type of the item is defined by the event source. - */ -export type Callback = (subscription: Subscription, item: Item, ...args: Args) => Args | undefined | void; - -/** - * Subscribes a callback to an event - * @param contract Event identifier - * @param callback Function to call when the event is triggered - * @param args Initial arguments passed to the callback - */ -export function subscribe(contract: Contract, callback: Callback, ...args: Args): Subscription; -/** - * Runs the event loop until it is stopped (potentially never) - */ -export function run(): void | never; -/** - * Stops the event loop - */ -export function stop(): void; - -/** - * Creates a timer event that can be subscribed to just like any other event - * @param mode Either `"oneshot"` or `"periodic"` - * @param interval Timer interval in milliseconds - */ -export function timer(mode: "oneshot" | "periodic", interval: number): Contract; - -/** - * Message queue - */ -export interface Queue { - /** - * Message event - */ - input: Contract; - /** - * Sends a message to the queue - * @param message message to send - */ - send(message: T): void; -} - -/** - * Creates a message queue - * @param length maximum queue capacity - */ -export function queue(length: number): Queue; diff --git a/applications/system/js_app/types/flipper/index.d.ts b/applications/system/js_app/types/flipper/index.d.ts deleted file mode 100644 index b1b1d474b..000000000 --- a/applications/system/js_app/types/flipper/index.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * @brief Returns the device model - */ -export declare function getModel(): string; - -/** - * @brief Returns the name of the virtual dolphin - */ -export declare function getName(): string; - -/** - * @brief Returns the battery charge percentage - */ -export declare function getBatteryCharge(): number; diff --git a/applications/system/js_app/types/global.d.ts b/applications/system/js_app/types/global.d.ts deleted file mode 100644 index a55dae7d9..000000000 --- a/applications/system/js_app/types/global.d.ts +++ /dev/null @@ -1,226 +0,0 @@ -/** - * @brief Pauses JavaScript execution for a while - * @param ms How many milliseconds to pause the execution for - */ -declare function delay(ms: number): void; - -/** - * @brief Prints to the GUI console view - * @param args The arguments are converted to strings, concatenated without any - * spaces in between and printed to the console view - */ -declare function print(...args: any[]): void; - -/** - * @brief Converts a string to a number - * @param text The string to convert to a number - * @param base Integer base (`2`...`16`), default: 10 - */ -declare function parseInt(text: string, base?: number): number; - -/** - * @brief Path to the directory containing the current script - */ -declare const __dirname: string; - -/** - * @brief Path to the current script file - */ -declare const __filename: string; - -/** - * @brief Reads a JS value from a file - * - * Reads a file at the specified path and runs it as JS, returning the last evaluated value. - * - * The result is cached and this filepath will not re-evaluated on future - * load() calls for this session. - * - * @param path The path to the file - * @param scope An object to use as global scope while running this file - */ -declare function load(path: string, scope?: object): any; - -/** - * @brief Return 1-byte string whose ASCII code is the integer `n` - * - * If `n` is not numeric or outside of `0-255` range, `null` is returned - * - * @param n The ASCII code to convert to string - */ -declare function chr(n: number): string | null; - -/** - * @brief mJS Foreign Pointer type - * - * JavaScript code cannot do anything with values of `RawPointer` type except - * acquire them from native code and pass them right back to other parts of - * native code. These values cannot be turned into something meaningful, nor can - * be they modified. - */ -declare type RawPointer = symbol & { "__tag__": "raw_ptr" }; -// introducing a nominal type in a hacky way; the `__tag__` property doesn't really exist. - -/** - * @brief Holds raw bytes - */ -declare class ArrayBuffer { - /** - * @brief The pointer to the byte buffer - * @note Like other `RawPointer` values, this value is essentially useless - * to JS code. - */ - getPtr: RawPointer; - /** - * @brief The length of the buffer in bytes - */ - byteLength: number; - /** - * @brief Creates an `ArrayBuffer` that contains a sub-part of the buffer - * @param start The index of the byte in the source buffer to be used as the - * start for the new buffer - * @param end The index of the byte in the source buffer that follows the - * byte to be used as the last byte for the new buffer - */ - slice(start: number, end?: number): ArrayBuffer; -} - -declare function ArrayBuffer(): ArrayBuffer; - -declare type ElementType = "u8" | "i8" | "u16" | "i16" | "u32" | "i32"; - -declare class TypedArray { - /** - * @brief The length of the buffer in bytes - */ - byteLength: number; - /** - * @brief The length of the buffer in typed elements - */ - length: number; - /** - * @brief The underlying `ArrayBuffer` - */ - buffer: ArrayBuffer; -} - -declare class Uint8Array extends TypedArray<"u8"> { } -declare class Int8Array extends TypedArray<"i8"> { } -declare class Uint16Array extends TypedArray<"u16"> { } -declare class Int16Array extends TypedArray<"i16"> { } -declare class Uint32Array extends TypedArray<"u32"> { } -declare class Int32Array extends TypedArray<"i32"> { } - -declare function Uint8Array(data: ArrayBuffer | number | number[]): Uint8Array; -declare function Int8Array(data: ArrayBuffer | number | number[]): Int8Array; -declare function Uint16Array(data: ArrayBuffer | number | number[]): Uint16Array; -declare function Int16Array(data: ArrayBuffer | number | number[]): Int16Array; -declare function Uint32Array(data: ArrayBuffer | number | number[]): Uint32Array; -declare function Int32Array(data: ArrayBuffer | number | number[]): Int32Array; - -declare const console: { - /** - * @brief Prints to the UART logs at the `[I]` level - * @param args The arguments are converted to strings, concatenated without any - * spaces in between and printed to the logs - */ - log(...args: any[]): void; - /** - * @brief Prints to the UART logs at the `[D]` level - * @param args The arguments are converted to strings, concatenated without any - * spaces in between and printed to the logs - */ - debug(...args: any[]): void; - /** - * @brief Prints to the UART logs at the `[W]` level - * @param args The arguments are converted to strings, concatenated without any - * spaces in between and printed to the logs - */ - warn(...args: any[]): void; - /** - * @brief Prints to the UART logs at the `[E]` level - * @param args The arguments are converted to strings, concatenated without any - * spaces in between and printed to the logs - */ - error(...args: any[]): void; -}; - -declare class Array { - /** - * @brief Takes items out of the array - * - * Removes elements from the array and returns them in a new array - * - * @param start The index to start taking elements from - * @param deleteCount How many elements to take - * @returns The elements that were taken out of the original array as a new - * array - */ - splice(start: number, deleteCount: number): T[]; - /** - * @brief Adds a value to the end of the array - * @param value The value to add - * @returns New length of the array - */ - push(value: T): number; - /** - * @brief How many elements there are in the array - */ - length: number; -} - -declare class String { - /** - * @brief How many characters there are in the string - */ - length: number; - /** - * @brief Returns the character code at an index in the string - * @param index The index to consult - */ - charCodeAt(index: number): number; - /** - * See `charCodeAt` - */ - at(index: number): number; - /** - * @brief Return index of first occurence of substr within the string or `-1` if not found - * @param substr The string to search for - * @param fromIndex The index to start searching from - */ - indexOf(substr: string, fromIndex?: number): number; - /** - * @brief Return a substring between two indices - * @param start The index to start substring at - * @param end The index to end substring at - */ - slice(start: number, end?: number): string; - /** - * @brief Return this string transformed to upper case - */ - toUpperCase(): string; - /** - * @brief Return this string transformed to lower case - */ - toLowerCase(): string; -} - -declare class Boolean { } - -declare class Function { } - -declare class Number { - /** - * @brief Converts this number to a string - * @param base Integer base (`2`...`16`), default: 10 - */ - toString(base?: number): string; -} - -declare class Object { } - -declare class RegExp { } - -declare interface IArguments { } - -declare type Partial = { [K in keyof O]?: O[K] }; diff --git a/applications/system/js_app/types/gui/byte_input.d.ts b/applications/system/js_app/types/gui/byte_input.d.ts deleted file mode 100644 index 92bd4c5a8..000000000 --- a/applications/system/js_app/types/gui/byte_input.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { View, ViewFactory } from "."; -import type { Contract } from "../event_loop"; - -type Props = { - header: string, - length: number, - defaultData: Uint8Array | ArrayBuffer, -} -declare class ByteInput extends View { - input: Contract; -} -declare class ByteInputFactory extends ViewFactory { } -declare const factory: ByteInputFactory; -export = factory; diff --git a/applications/system/js_app/types/gui/dialog.d.ts b/applications/system/js_app/types/gui/dialog.d.ts deleted file mode 100644 index 6d9c8d43b..000000000 --- a/applications/system/js_app/types/gui/dialog.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { View, ViewFactory } from "."; -import type { Contract } from "../event_loop"; - -type Props = { - header: string, - text: string, - left: string, - center: string, - right: string, -} -declare class Dialog extends View { - input: Contract<"left" | "center" | "right">; -} -declare class DialogFactory extends ViewFactory { } -declare const factory: DialogFactory; -export = factory; diff --git a/applications/system/js_app/types/gui/empty_screen.d.ts b/applications/system/js_app/types/gui/empty_screen.d.ts deleted file mode 100644 index c71e93b32..000000000 --- a/applications/system/js_app/types/gui/empty_screen.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { View, ViewFactory } from "."; - -type Props = {}; -declare class EmptyScreen extends View { } -declare class EmptyScreenFactory extends ViewFactory { } -declare const factory: EmptyScreenFactory; -export = factory; diff --git a/applications/system/js_app/types/gui/index.d.ts b/applications/system/js_app/types/gui/index.d.ts deleted file mode 100644 index d4cdc6bc9..000000000 --- a/applications/system/js_app/types/gui/index.d.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { Contract } from "../event_loop"; - -type Properties = { [K: string]: any }; - -export declare class View { - /** - * Assign value to property by name - * @param property Name of the property - * @param value Value to assign - */ - set

(property: P, value: Props[P]): void; -} - -export declare class ViewFactory> { - /** - * Create view instance with default values, can be changed later with set() - */ - make(): V; - /** - * Create view instance with custom values, can be changed later with set() - * @param initial Dictionary of property names to values - */ - makeWith(initial: Partial): V; -} - -declare class ViewDispatcher { - /** - * Event source for `sendCustom` events - */ - custom: Contract; - /** - * Event source for navigation events (back key presses) - */ - navigation: Contract; - /** - * View object currently shown - */ - currentView: View; - /** - * Sends a number to the custom event handler - * @param event number to send - */ - sendCustom(event: number): void; - /** - * Switches to a view - * @param assoc View-ViewDispatcher association as returned by `add` - */ - switchTo(assoc: View): void; - /** - * Sends this ViewDispatcher to the front or back, above or below all other - * GUI viewports - * @param direction Either `"front"` or `"back"` - */ - sendTo(direction: "front" | "back"): void; -} - -export const viewDispatcher: ViewDispatcher; diff --git a/applications/system/js_app/types/gui/loading.d.ts b/applications/system/js_app/types/gui/loading.d.ts deleted file mode 100644 index 73a963349..000000000 --- a/applications/system/js_app/types/gui/loading.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { View, ViewFactory } from "."; - -type Props = {}; -declare class Loading extends View { } -declare class LoadingFactory extends ViewFactory { } -declare const factory: LoadingFactory; -export = factory; diff --git a/applications/system/js_app/types/gui/submenu.d.ts b/applications/system/js_app/types/gui/submenu.d.ts deleted file mode 100644 index 59d535864..000000000 --- a/applications/system/js_app/types/gui/submenu.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { View, ViewFactory } from "."; -import type { Contract } from "../event_loop"; - -type Props = { - header: string, - items: string[], -}; -declare class Submenu extends View { - chosen: Contract; -} -declare class SubmenuFactory extends ViewFactory { } -declare const factory: SubmenuFactory; -export = factory; diff --git a/applications/system/js_app/types/gui/text_box.d.ts b/applications/system/js_app/types/gui/text_box.d.ts deleted file mode 100644 index 3dbbac571..000000000 --- a/applications/system/js_app/types/gui/text_box.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { View, ViewFactory } from "."; -import type { Contract } from "../event_loop"; - -type Props = { - text: string, - font: "text" | "hex", - focus: "start" | "end", -} -declare class TextBox extends View { - chosen: Contract; -} -declare class TextBoxFactory extends ViewFactory { } -declare const factory: TextBoxFactory; -export = factory; diff --git a/applications/system/js_app/types/gui/text_input.d.ts b/applications/system/js_app/types/gui/text_input.d.ts deleted file mode 100644 index bdb5b4d4c..000000000 --- a/applications/system/js_app/types/gui/text_input.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { View, ViewFactory } from "."; -import type { Contract } from "../event_loop"; - -type Props = { - header: string, - minLength: number, - maxLength: number, - defaultText: string, - defaultTextClear: boolean, -} -declare class TextInput extends View { - input: Contract; -} -declare class TextInputFactory extends ViewFactory { } -declare const factory: TextInputFactory; -export = factory; diff --git a/assets/icons/Settings/Alarm_47x39/frame_0.png b/assets/icons/Settings/Alarm_47x39/frame_0.png new file mode 100644 index 000000000..564116036 Binary files /dev/null and b/assets/icons/Settings/Alarm_47x39/frame_0.png differ diff --git a/assets/icons/Settings/Alarm_47x39/frame_1.png b/assets/icons/Settings/Alarm_47x39/frame_1.png new file mode 100644 index 000000000..c5c58ea91 Binary files /dev/null and b/assets/icons/Settings/Alarm_47x39/frame_1.png differ diff --git a/assets/icons/Settings/Alarm_47x39/frame_2.png b/assets/icons/Settings/Alarm_47x39/frame_2.png new file mode 100644 index 000000000..b7d338f7d Binary files /dev/null and b/assets/icons/Settings/Alarm_47x39/frame_2.png differ diff --git a/assets/icons/Settings/Alarm_47x39/frame_3.png b/assets/icons/Settings/Alarm_47x39/frame_3.png new file mode 100644 index 000000000..067d55ddd Binary files /dev/null and b/assets/icons/Settings/Alarm_47x39/frame_3.png differ diff --git a/assets/icons/Settings/Alarm_47x39/frame_4.png b/assets/icons/Settings/Alarm_47x39/frame_4.png new file mode 100644 index 000000000..564116036 Binary files /dev/null and b/assets/icons/Settings/Alarm_47x39/frame_4.png differ diff --git a/assets/icons/Settings/Alarm_47x39/frame_rate b/assets/icons/Settings/Alarm_47x39/frame_rate new file mode 100644 index 000000000..0cfbf0888 --- /dev/null +++ b/assets/icons/Settings/Alarm_47x39/frame_rate @@ -0,0 +1 @@ +2 diff --git a/documentation/JavaScript.md b/documentation/JavaScript.md index 3aabfc125..cb3e13e9b 100644 --- a/documentation/JavaScript.md +++ b/documentation/JavaScript.md @@ -2,7 +2,7 @@ # JavaScript scripting API (WIP) > [!IMPORTANT] -> This documentation is deprecated! You should now look at type info in `applications/system/js_app/types` and examples in `applications/system/js_app/examples/apps/Scripts`. +> This documentation is deprecated! You should now look at type info in `applications/system/js_app/packages/fz-sdk` and examples in `applications/system/js_app/examples/apps/Scripts`. > Human readable documentation will eventually be derived from this type information. > For now, old documentation is still available here and in `documentation/js`, but it may be inaccurate or missing. diff --git a/documentation/UnitTests.md b/documentation/UnitTests.md index 5c80e763d..9711c6ae1 100644 --- a/documentation/UnitTests.md +++ b/documentation/UnitTests.md @@ -20,7 +20,7 @@ To run the unit tests, follow these steps: 3. Launch the CLI session and run the `unit_tests` command. **NOTE:** To run a particular test (and skip all others), specify its name as the command argument. -See [test_index.c](https://github.com/flipperdevices/flipperzero-firmware/blob/dev/applications/debug/unit_tests/test_index.c) for the complete list of test names. +Test names match application names defined [here](https://github.com/flipperdevices/flipperzero-firmware/blob/dev/applications/debug/unit_tests/application.fam). ## Adding unit tests @@ -28,7 +28,7 @@ See [test_index.c](https://github.com/flipperdevices/flipperzero-firmware/blob/d #### Entry point -The common entry point for all tests is the [unit_tests](https://github.com/flipperdevices/flipperzero-firmware/tree/dev/applications/debug/unit_tests) app. Test-specific code is placed into an arbitrarily named subdirectory and is then called from the [test_index.c](https://github.com/flipperdevices/flipperzero-firmware/tree/dev/applications/debug/unit_tests/test_index.c) source file. +The common entry point for all tests is the [unit_tests](https://github.com/flipperdevices/flipperzero-firmware/tree/dev/applications/debug/unit_tests) app. Test-specific code is packaged as a `PLUGIN` app placed in a subdirectory of `tests` in the `unit_tests` mother-app and referenced in the common `application.fam`. Look at other tests for an example. #### Test assets diff --git a/documentation/js/js_builtin.md b/documentation/js/js_builtin.md index da7d1b252..05c8c80fb 100644 --- a/documentation/js/js_builtin.md +++ b/documentation/js/js_builtin.md @@ -73,18 +73,18 @@ Transforms a string to lower case. toLowerCase("Example") // "example" ``` -## __dirpath +## __dirname Path to the directory containing the current script. ### Examples: ```js -print(__dirpath); // /ext/apps/Scripts/Examples +print(__dirname); // /ext/apps/Scripts/Examples ``` -## __filepath +## __filename Path to the current script file. ### Examples: ```js -print(__filepath); // /ext/apps/Scripts/Examples/path.js +print(__filename); // /ext/apps/Scripts/Examples/path.js ``` diff --git a/documentation/js/js_gpio.md b/documentation/js/js_gpio.md index 9791fb4eb..aa444bacd 100644 --- a/documentation/js/js_gpio.md +++ b/documentation/js/js_gpio.md @@ -61,7 +61,7 @@ Reads a digital value from a pin configured with `direction: "in"` and any #### Returns Boolean logic level -### `Pin.read_analog()` +### `Pin.readAnalog()` Reads an analog voltage level in millivolts from a pin configured with `direction: "in"` and `inMode: "analog"` diff --git a/furi/core/event_flag.c b/furi/core/event_flag.c index 19b28a500..721f4c2fa 100644 --- a/furi/core/event_flag.c +++ b/furi/core/event_flag.c @@ -5,11 +5,15 @@ #include #include +#include "event_loop_link_i.h" + #define FURI_EVENT_FLAG_MAX_BITS_EVENT_GROUPS 24U -#define FURI_EVENT_FLAG_INVALID_BITS (~((1UL << FURI_EVENT_FLAG_MAX_BITS_EVENT_GROUPS) - 1U)) +#define FURI_EVENT_FLAG_VALID_BITS ((1UL << FURI_EVENT_FLAG_MAX_BITS_EVENT_GROUPS) - 1U) +#define FURI_EVENT_FLAG_INVALID_BITS (~(FURI_EVENT_FLAG_VALID_BITS)) struct FuriEventFlag { StaticEventGroup_t container; + FuriEventLoopLink event_loop_link; }; // IMPORTANT: container MUST be the FIRST struct member @@ -27,6 +31,11 @@ FuriEventFlag* furi_event_flag_alloc(void) { void furi_event_flag_free(FuriEventFlag* instance) { furi_check(!FURI_IS_IRQ_MODE()); + + // Event Loop must be disconnected + furi_check(!instance->event_loop_link.item_in); + furi_check(!instance->event_loop_link.item_out); + vEventGroupDelete((EventGroupHandle_t)instance); free(instance); } @@ -39,6 +48,8 @@ uint32_t furi_event_flag_set(FuriEventFlag* instance, uint32_t flags) { uint32_t rflags; BaseType_t yield; + FURI_CRITICAL_ENTER(); + if(FURI_IS_IRQ_MODE()) { yield = pdFALSE; if(xEventGroupSetBitsFromISR(hEventGroup, (EventBits_t)flags, &yield) == pdFAIL) { @@ -48,11 +59,15 @@ uint32_t furi_event_flag_set(FuriEventFlag* instance, uint32_t flags) { portYIELD_FROM_ISR(yield); } } else { - vTaskSuspendAll(); rflags = xEventGroupSetBits(hEventGroup, (EventBits_t)flags); - (void)xTaskResumeAll(); } + if(rflags & flags) { + furi_event_loop_link_notify(&instance->event_loop_link, FuriEventLoopEventIn); + } + + FURI_CRITICAL_EXIT(); + /* Return event flags after setting */ return rflags; } @@ -64,6 +79,7 @@ uint32_t furi_event_flag_clear(FuriEventFlag* instance, uint32_t flags) { EventGroupHandle_t hEventGroup = (EventGroupHandle_t)instance; uint32_t rflags; + FURI_CRITICAL_ENTER(); if(FURI_IS_IRQ_MODE()) { rflags = xEventGroupGetBitsFromISR(hEventGroup); @@ -79,6 +95,11 @@ uint32_t furi_event_flag_clear(FuriEventFlag* instance, uint32_t flags) { rflags = xEventGroupClearBits(hEventGroup, (EventBits_t)flags); } + if(rflags & flags) { + furi_event_loop_link_notify(&instance->event_loop_link, FuriEventLoopEventOut); + } + FURI_CRITICAL_EXIT(); + /* Return event flags before clearing */ return rflags; } @@ -146,6 +167,36 @@ uint32_t furi_event_flag_wait( } } + if((rflags & FuriFlagError) == 0U) { + furi_event_loop_link_notify(&instance->event_loop_link, FuriEventLoopEventOut); + } + /* Return event flags before clearing */ return rflags; } + +static FuriEventLoopLink* furi_event_flag_event_loop_get_link(FuriEventLoopObject* object) { + FuriEventFlag* instance = object; + furi_assert(instance); + return &instance->event_loop_link; +} + +static bool + furi_event_flag_event_loop_get_level(FuriEventLoopObject* object, FuriEventLoopEvent event) { + FuriEventFlag* instance = object; + furi_assert(instance); + + if(event == FuriEventLoopEventIn) { + return (furi_event_flag_get(instance) & FURI_EVENT_FLAG_VALID_BITS); + } else if(event == FuriEventLoopEventOut) { + return (furi_event_flag_get(instance) & FURI_EVENT_FLAG_VALID_BITS) != + FURI_EVENT_FLAG_VALID_BITS; + } else { + furi_crash(); + } +} + +const FuriEventLoopContract furi_event_flag_event_loop_contract = { + .get_link = furi_event_flag_event_loop_get_link, + .get_level = furi_event_flag_event_loop_get_level, +}; diff --git a/furi/core/event_loop.c b/furi/core/event_loop.c index b622aa7a1..c0998ea90 100644 --- a/furi/core/event_loop.c +++ b/furi/core/event_loop.c @@ -101,36 +101,39 @@ void furi_event_loop_free(FuriEventLoop* instance) { } static inline FuriEventLoopProcessStatus - furi_event_loop_poll_process_level_event(FuriEventLoopItem* item) { - if(!item->contract->get_level(item->object, item->event)) { - return FuriEventLoopProcessStatusComplete; - } else if(item->callback(item->object, item->callback_context)) { - return FuriEventLoopProcessStatusIncomplete; - } else { - return FuriEventLoopProcessStatusAgain; - } + furi_event_loop_process_edge_event(FuriEventLoopItem* item) { + FuriEventLoopProcessStatus status = FuriEventLoopProcessStatusComplete; + item->callback(item->object, item->callback_context); + + return status; } static inline FuriEventLoopProcessStatus - furi_event_loop_poll_process_edge_event(FuriEventLoopItem* item) { - if(item->callback(item->object, item->callback_context)) { - return FuriEventLoopProcessStatusComplete; - } else { - return FuriEventLoopProcessStatusAgain; + furi_event_loop_process_level_event(FuriEventLoopItem* item) { + FuriEventLoopProcessStatus status = FuriEventLoopProcessStatusComplete; + if(item->contract->get_level(item->object, item->event)) { + item->callback(item->object, item->callback_context); + + if(item->contract->get_level(item->object, item->event)) { + status = FuriEventLoopProcessStatusIncomplete; + } } + + return status; } static inline FuriEventLoopProcessStatus - furi_event_loop_poll_process_event(FuriEventLoop* instance, FuriEventLoopItem* item) { + furi_event_loop_process_event(FuriEventLoop* instance, FuriEventLoopItem* item) { FuriEventLoopProcessStatus status; + if(item->event & FuriEventLoopEventFlagOnce) { furi_event_loop_unsubscribe(instance, item->object); } if(item->event & FuriEventLoopEventFlagEdge) { - status = furi_event_loop_poll_process_edge_event(item); + status = furi_event_loop_process_edge_event(item); } else { - status = furi_event_loop_poll_process_level_event(item); + status = furi_event_loop_process_level_event(item); } if(item->owner == NULL) { @@ -140,7 +143,7 @@ static inline FuriEventLoopProcessStatus return status; } -static void furi_event_loop_process_waiting_list(FuriEventLoop* instance) { +static inline FuriEventLoopItem* furi_event_loop_get_waiting_item(FuriEventLoop* instance) { FuriEventLoopItem* item = NULL; FURI_CRITICAL_ENTER(); @@ -152,27 +155,42 @@ static void furi_event_loop_process_waiting_list(FuriEventLoop* instance) { FURI_CRITICAL_EXIT(); + return item; +} + +static inline void furi_event_loop_sync_flags(FuriEventLoop* instance) { + FURI_CRITICAL_ENTER(); + + if(!WaitingList_empty_p(instance->waiting_list)) { + xTaskNotifyIndexed( + (TaskHandle_t)instance->thread_id, + FURI_EVENT_LOOP_FLAG_NOTIFY_INDEX, + FuriEventLoopFlagEvent, + eSetBits); + } + + FURI_CRITICAL_EXIT(); +} + +static void furi_event_loop_process_waiting_list(FuriEventLoop* instance) { + FuriEventLoopItem* item = furi_event_loop_get_waiting_item(instance); if(!item) return; - while(true) { - FuriEventLoopProcessStatus ret = furi_event_loop_poll_process_event(instance, item); + FuriEventLoopProcessStatus status = furi_event_loop_process_event(instance, item); - if(ret == FuriEventLoopProcessStatusComplete) { - // Event processing complete, break from loop - break; - } else if(ret == FuriEventLoopProcessStatusIncomplete) { - // Event processing incomplete more processing needed - } else if(ret == FuriEventLoopProcessStatusAgain) { //-V547 - furi_event_loop_item_notify(item); - break; - // Unsubscribed from inside the callback, delete item - } else if(ret == FuriEventLoopProcessStatusFreeLater) { //-V547 - furi_event_loop_item_free(item); - break; - } else { - furi_crash(); - } + if(status == FuriEventLoopProcessStatusComplete) { + // Event processing complete, do nothing + } else if(status == FuriEventLoopProcessStatusIncomplete) { + // Event processing incomplete, put item back in waiting list + furi_event_loop_item_notify(item); + } else if(status == FuriEventLoopProcessStatusFreeLater) { //-V547 + // Unsubscribed from inside the callback, delete item + furi_event_loop_item_free(item); + } else { + furi_crash(); } + + furi_event_loop_sync_flags(instance); } static void furi_event_loop_restore_flags(FuriEventLoop* instance, uint32_t flags) { @@ -239,14 +257,28 @@ void furi_event_loop_run(FuriEventLoop* instance) { } } +static void furi_event_loop_notify(FuriEventLoop* instance, FuriEventLoopFlag flag) { + if(FURI_IS_IRQ_MODE()) { + BaseType_t yield = pdFALSE; + + (void)xTaskNotifyIndexedFromISR( + (TaskHandle_t)instance->thread_id, + FURI_EVENT_LOOP_FLAG_NOTIFY_INDEX, + flag, + eSetBits, + &yield); + + portYIELD_FROM_ISR(yield); + + } else { + (void)xTaskNotifyIndexed( + (TaskHandle_t)instance->thread_id, FURI_EVENT_LOOP_FLAG_NOTIFY_INDEX, flag, eSetBits); + } +} + void furi_event_loop_stop(FuriEventLoop* instance) { furi_check(instance); - - xTaskNotifyIndexed( - (TaskHandle_t)instance->thread_id, - FURI_EVENT_LOOP_FLAG_NOTIFY_INDEX, - FuriEventLoopFlagStop, - eSetBits); + furi_event_loop_notify(instance, FuriEventLoopFlagStop); } /* @@ -268,11 +300,7 @@ void furi_event_loop_pend_callback( PendingQueue_push_front(instance->pending_queue, item); - xTaskNotifyIndexed( - (TaskHandle_t)instance->thread_id, - FURI_EVENT_LOOP_FLAG_NOTIFY_INDEX, - FuriEventLoopFlagPending, - eSetBits); + furi_event_loop_notify(instance, FuriEventLoopFlagPending); } /* @@ -328,6 +356,17 @@ static void furi_event_loop_object_subscribe( * Public specialized subscription API */ +void furi_event_loop_subscribe_event_flag( + FuriEventLoop* instance, + FuriEventFlag* event_flag, + FuriEventLoopEvent event, + FuriEventLoopEventCallback callback, + void* context) { + extern const FuriEventLoopContract furi_event_flag_event_loop_contract; + furi_event_loop_object_subscribe( + instance, event_flag, &furi_event_flag_event_loop_contract, event, callback, context); +} + void furi_event_loop_subscribe_message_queue( FuriEventLoop* instance, FuriMessageQueue* message_queue, @@ -491,11 +530,7 @@ static void furi_event_loop_item_notify(FuriEventLoopItem* instance) { FURI_CRITICAL_EXIT(); - xTaskNotifyIndexed( - (TaskHandle_t)owner->thread_id, - FURI_EVENT_LOOP_FLAG_NOTIFY_INDEX, - FuriEventLoopFlagEvent, - eSetBits); + furi_event_loop_notify(owner, FuriEventLoopFlagEvent); } static bool furi_event_loop_item_is_waiting(FuriEventLoopItem* instance) { diff --git a/furi/core/event_loop.h b/furi/core/event_loop.h index 6c5ba432c..d5e8710a6 100644 --- a/furi/core/event_loop.h +++ b/furi/core/event_loop.h @@ -11,6 +11,9 @@ * provide any compatibility with other event driven APIs. But * programming concepts are the same, except some runtime * limitations from our side. + * + * @warning Only ONE instance of FuriEventLoop per thread is possible. ALL FuriEventLoop + * funcitons MUST be called from the same thread that the instance was created in. */ #pragma once @@ -197,10 +200,29 @@ typedef void FuriEventLoopObject; * * @param object The object that triggered the event * @param context The context that was provided upon subscription - * - * @return true if event was processed, false if we need to delay processing */ -typedef bool (*FuriEventLoopEventCallback)(FuriEventLoopObject* object, void* context); +typedef void (*FuriEventLoopEventCallback)(FuriEventLoopObject* object, void* context); + +/** Opaque event flag type */ +typedef struct FuriEventFlag FuriEventFlag; + +/** Subscribe to event flag events + * + * @warning you can only have one subscription for one event type. + * + * @param instance The Event Loop instance + * @param event_flag The event flag to add + * @param[in] event The Event Loop event to trigger on + * @param[in] callback The callback to call on event + * @param context The context for callback + */ + +void furi_event_loop_subscribe_event_flag( + FuriEventLoop* instance, + FuriEventFlag* event_flag, + FuriEventLoopEvent event, + FuriEventLoopEventCallback callback, + void* context); /** Opaque message queue type */ typedef struct FuriMessageQueue FuriMessageQueue; diff --git a/furi/core/event_loop_i.h b/furi/core/event_loop_i.h index 15efa8f86..7016e1e1b 100644 --- a/furi/core/event_loop_i.h +++ b/furi/core/event_loop_i.h @@ -59,7 +59,6 @@ typedef enum { typedef enum { FuriEventLoopProcessStatusComplete, FuriEventLoopProcessStatusIncomplete, - FuriEventLoopProcessStatusAgain, FuriEventLoopProcessStatusFreeLater, } FuriEventLoopProcessStatus; diff --git a/furi/core/event_loop_link_i.h b/furi/core/event_loop_link_i.h index 992ca6555..4b993390f 100644 --- a/furi/core/event_loop_link_i.h +++ b/furi/core/event_loop_link_i.h @@ -21,7 +21,7 @@ void furi_event_loop_link_notify(FuriEventLoopLink* instance, FuriEventLoopEvent typedef FuriEventLoopLink* (*FuriEventLoopContractGetLink)(FuriEventLoopObject* object); -typedef uint32_t ( +typedef bool ( *FuriEventLoopContractGetLevel)(FuriEventLoopObject* object, FuriEventLoopEvent event); typedef struct { diff --git a/furi/core/event_loop_timer.h b/furi/core/event_loop_timer.h index 9034043fa..50fb57389 100644 --- a/furi/core/event_loop_timer.h +++ b/furi/core/event_loop_timer.h @@ -1,6 +1,9 @@ /** * @file event_loop_timer.h * @brief Software timer functionality for FuriEventLoop. + * + * @warning ALL FuriEventLoopTimer functions MUST be called from the + * same thread that the owner FuriEventLoop instance was created in. */ #pragma once diff --git a/furi/core/message_queue.c b/furi/core/message_queue.c index bd0cec021..b0862e501 100644 --- a/furi/core/message_queue.c +++ b/furi/core/message_queue.c @@ -213,7 +213,7 @@ static FuriEventLoopLink* furi_message_queue_event_loop_get_link(FuriEventLoopOb return &instance->event_loop_link; } -static uint32_t +static bool furi_message_queue_event_loop_get_level(FuriEventLoopObject* object, FuriEventLoopEvent event) { FuriMessageQueue* instance = object; furi_assert(instance); diff --git a/furi/core/mutex.c b/furi/core/mutex.c index f9848e1ba..edaba9e00 100644 --- a/furi/core/mutex.c +++ b/furi/core/mutex.c @@ -144,13 +144,13 @@ static FuriEventLoopLink* furi_mutex_event_loop_get_link(FuriEventLoopObject* ob return &instance->event_loop_link; } -static uint32_t +static bool furi_mutex_event_loop_get_level(FuriEventLoopObject* object, FuriEventLoopEvent event) { FuriMutex* instance = object; furi_assert(instance); if(event == FuriEventLoopEventIn || event == FuriEventLoopEventOut) { - return furi_mutex_get_owner(instance) ? 0 : 1; + return !furi_mutex_get_owner(instance); } else { furi_crash(); } diff --git a/furi/core/semaphore.c b/furi/core/semaphore.c index 850169ad6..d05b9bf09 100644 --- a/furi/core/semaphore.c +++ b/furi/core/semaphore.c @@ -165,7 +165,7 @@ static FuriEventLoopLink* furi_semaphore_event_loop_get_link(FuriEventLoopObject return &instance->event_loop_link; } -static uint32_t +static bool furi_semaphore_event_loop_get_level(FuriEventLoopObject* object, FuriEventLoopEvent event) { FuriSemaphore* instance = object; furi_assert(instance); diff --git a/furi/core/stream_buffer.c b/furi/core/stream_buffer.c index f35abec64..783b2d741 100644 --- a/furi/core/stream_buffer.c +++ b/furi/core/stream_buffer.c @@ -157,7 +157,7 @@ static FuriEventLoopLink* furi_stream_buffer_event_loop_get_link(FuriEventLoopOb return &stream_buffer->event_loop_link; } -static uint32_t +static bool furi_stream_buffer_event_loop_get_level(FuriEventLoopObject* object, FuriEventLoopEvent event) { FuriStreamBuffer* stream_buffer = object; furi_assert(stream_buffer); diff --git a/lib/flipper_application/flipper_application.c b/lib/flipper_application/flipper_application.c index f4479302f..697053463 100644 --- a/lib/flipper_application/flipper_application.c +++ b/lib/flipper_application/flipper_application.c @@ -243,9 +243,6 @@ static int32_t flipper_application_thread(void* context) { // wait until all notifications from RAM are completed NotificationApp* notifications = furi_record_open(RECORD_NOTIFICATION); - const NotificationSequence sequence_empty = { - NULL, - }; notification_message_block(notifications, &sequence_empty); furi_record_close(RECORD_NOTIFICATION); diff --git a/lib/mjs/mjs_builtin.c b/lib/mjs/mjs_builtin.c index afcf9ce6f..7a1d74ff1 100644 --- a/lib/mjs/mjs_builtin.c +++ b/lib/mjs/mjs_builtin.c @@ -146,10 +146,17 @@ void mjs_init_builtin(struct mjs* mjs, mjs_val_t obj) { // mjs_set(mjs, obj, "JSON", ~0, v); /* - * Populate Object.create() + * Populate Object */ v = mjs_mk_object(mjs); mjs_set(mjs, v, "create", ~0, mjs_mk_foreign_func(mjs, (mjs_func_ptr_t)mjs_op_create_object)); + mjs_set( + mjs, + v, + "defineProperty", + ~0, + mjs_mk_foreign_func( + mjs, (mjs_func_ptr_t)mjs_op_object_define_property)); // stub, do not use mjs_set(mjs, obj, "Object", ~0, v); /* diff --git a/lib/mjs/mjs_object.c b/lib/mjs/mjs_object.c index 60bacf514..cf14a499f 100644 --- a/lib/mjs/mjs_object.c +++ b/lib/mjs/mjs_object.c @@ -294,6 +294,11 @@ clean: mjs_return(mjs, ret); } +MJS_PRIVATE void mjs_op_object_define_property(struct mjs* mjs) { + // stub, do not use + mjs_return(mjs, MJS_UNDEFINED); +} + mjs_val_t mjs_struct_to_obj(struct mjs* mjs, const void* base, const struct mjs_c_struct_member* defs) { mjs_val_t obj; diff --git a/lib/mjs/mjs_object.h b/lib/mjs/mjs_object.h index 870486d06..101272e29 100644 --- a/lib/mjs/mjs_object.h +++ b/lib/mjs/mjs_object.h @@ -50,6 +50,11 @@ MJS_PRIVATE mjs_err_t mjs_set_internal( */ MJS_PRIVATE void mjs_op_create_object(struct mjs* mjs); +/* + * Stub of `Object.defineProperty()` + */ +MJS_PRIVATE void mjs_op_object_define_property(struct mjs* mjs); + /* * Cell destructor for object arena */ diff --git a/lib/mjs/mjs_parser.c b/lib/mjs/mjs_parser.c index 503b16942..212804a86 100644 --- a/lib/mjs/mjs_parser.c +++ b/lib/mjs/mjs_parser.c @@ -76,7 +76,8 @@ static int s_assign_ops[] = { static int findtok(int* toks, int tok) { int i = 0; - while(tok != toks[i] && toks[i] != TOK_EOF) i++; + while(tok != toks[i] && toks[i] != TOK_EOF) + i++; return toks[i]; } @@ -87,7 +88,7 @@ static void emit_op(struct pstate* pstate, int tok) { } #define BINOP_STACK_FRAME_SIZE 16 -#define STACK_LIMIT 8192 +#define STACK_LIMIT 8192 // Intentionally left as macro rather than a function, to let the // compiler to inline calls and mimimize runtime stack usage. @@ -166,7 +167,8 @@ static mjs_err_t parse_statement_list(struct pstate* p, int et) { if(drop) emit_byte(p, OP_DROP); res = parse_statement(p); drop = 1; - while(p->tok.tok == TOK_SEMICOLON) pnext1(p); + while(p->tok.tok == TOK_SEMICOLON) + pnext1(p); } /* @@ -523,7 +525,11 @@ static mjs_err_t parse_expr(struct pstate* p) { static mjs_err_t parse_let(struct pstate* p) { mjs_err_t res = MJS_OK; LOG(LL_VERBOSE_DEBUG, ("[%.*s]", 10, p->tok.ptr)); - EXPECT(p, TOK_KEYWORD_LET); + if((p)->tok.tok != TOK_KEYWORD_VAR && (p)->tok.tok != TOK_KEYWORD_LET && + (p)->tok.tok != TOK_KEYWORD_CONST) + SYNTAX_ERROR(p); + else + pnext1(p); for(;;) { struct tok tmp = p->tok; EXPECT(p, TOK_IDENT); @@ -910,6 +916,8 @@ static mjs_err_t parse_statement(struct pstate* p) { pnext1(p); return MJS_OK; case TOK_KEYWORD_LET: + case TOK_KEYWORD_VAR: + case TOK_KEYWORD_CONST: return parse_let(p); case TOK_OPEN_CURLY: return parse_block(p, 1); @@ -939,7 +947,6 @@ static mjs_err_t parse_statement(struct pstate* p) { case TOK_KEYWORD_SWITCH: case TOK_KEYWORD_THROW: case TOK_KEYWORD_TRY: - case TOK_KEYWORD_VAR: case TOK_KEYWORD_VOID: case TOK_KEYWORD_WITH: mjs_set_errorf( diff --git a/lib/mjs/mjs_tok.c b/lib/mjs/mjs_tok.c index bdff5a86a..f89606d23 100644 --- a/lib/mjs/mjs_tok.c +++ b/lib/mjs/mjs_tok.c @@ -80,12 +80,13 @@ static int getnum(struct pstate* p) { } static int is_reserved_word_token(const char* s, int len) { - const char* reserved[] = {"break", "case", "catch", "continue", "debugger", "default", - "delete", "do", "else", "false", "finally", "for", - "function", "if", "in", "instanceof", "new", "null", - "return", "switch", "this", "throw", "true", "try", - "typeof", "var", "void", "while", "with", "let", - "undefined", NULL}; + const char* reserved[] = {"break", "case", "catch", "continue", "debugger", + "default", "delete", "do", "else", "false", + "finally", "for", "function", "if", "in", + "instanceof", "new", "null", "return", "switch", + "this", "throw", "true", "try", "typeof", + "var", "void", "while", "with", "let", + "const", "undefined", NULL}; int i; if(!mjs_is_alpha(s[0])) return 0; for(i = 0; reserved[i] != NULL; i++) { @@ -95,7 +96,8 @@ static int is_reserved_word_token(const char* s, int len) { } static int getident(struct pstate* p) { - while(mjs_is_ident(p->pos[0]) || mjs_is_digit(p->pos[0])) p->pos++; + while(mjs_is_ident(p->pos[0]) || mjs_is_digit(p->pos[0])) + p->pos++; p->tok.len = p->pos - p->tok.ptr; p->pos--; return TOK_IDENT; @@ -125,7 +127,8 @@ static void skip_spaces_and_comments(struct pstate* p) { p->pos++; } if(p->pos[0] == '/' && p->pos[1] == '/') { - while(p->pos[0] != '\0' && p->pos[0] != '\n') p->pos++; + while(p->pos[0] != '\0' && p->pos[0] != '\n') + p->pos++; } if(p->pos[0] == '/' && p->pos[1] == '*') { p->pos += 2; @@ -142,8 +145,8 @@ static void skip_spaces_and_comments(struct pstate* p) { } static int ptranslate(int tok) { -#define DT(a, b) ((a) << 8 | (b)) -#define TT(a, b, c) ((a) << 16 | (b) << 8 | (c)) +#define DT(a, b) ((a) << 8 | (b)) +#define TT(a, b, c) ((a) << 16 | (b) << 8 | (c)) #define QT(a, b, c, d) ((a) << 24 | (b) << 16 | (c) << 8 | (d)) /* Map token ID produced by mjs_tok.c to token ID produced by lemon */ /* clang-format off */ diff --git a/lib/mjs/mjs_tok.h b/lib/mjs/mjs_tok.h index 03d8fe6fa..5ff079492 100644 --- a/lib/mjs/mjs_tok.h +++ b/lib/mjs/mjs_tok.h @@ -125,6 +125,7 @@ enum { TOK_KEYWORD_WHILE, TOK_KEYWORD_WITH, TOK_KEYWORD_LET, + TOK_KEYWORD_CONST, TOK_KEYWORD_UNDEFINED, TOK_MAX }; diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller.c b/lib/nfc/protocols/mf_classic/mf_classic_poller.c index edfdb2a95..ec37c8015 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller.c +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller.c @@ -6,11 +6,11 @@ #define TAG "MfClassicPoller" -// TODO: Buffer writes for Hardnested, set state to Log when finished and sum property matches -// TODO: Store target key in CUID dictionary -// TODO: Dead code for malloc returning NULL? -// TODO: Auth1 static encrypted exists (rare) -// TODO: Use keys found by NFC plugins, cached keys +// TODO FL-3926: Buffer writes for Hardnested, set state to Log when finished and sum property matches +// TODO FL-3926: Store target key in CUID dictionary +// TODO FL-3926: Dead code for malloc returning NULL? +// TODO FL-3926: Auth1 static encrypted exists (rare) +// TODO FL-3926: Use keys found by NFC plugins, cached keys #define MF_CLASSIC_MAX_BUFF_SIZE (64) @@ -606,7 +606,7 @@ NfcCommand mf_classic_poller_handler_analyze_backdoor(MfClassicPoller* instance) } NfcCommand mf_classic_poller_handler_backdoor_read_sector(MfClassicPoller* instance) { - // TODO: Reauth not needed + // TODO FL-3926: Reauth not needed NfcCommand command = NfcCommandContinue; MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; MfClassicError error = MfClassicErrorNone; @@ -1289,8 +1289,6 @@ NfcCommand mf_classic_poller_handler_nested_calibrate(MfClassicPoller* instance) float std_dev = sqrtf(variance); // Filter out values over 3 standard deviations away from the median - dict_attack_ctx->d_min = UINT16_MAX; - dict_attack_ctx->d_max = 0; for(uint8_t i = 0; i < valid_distances; i++) { if(fabsf((float)distances[i] - median) <= 3 * std_dev) { if(distances[i] < dict_attack_ctx->d_min) dict_attack_ctx->d_min = distances[i]; @@ -1328,8 +1326,8 @@ static inline bool is_byte_found(uint8_t* found, uint8_t byte) { } NfcCommand mf_classic_poller_handler_nested_collect_nt_enc(MfClassicPoller* instance) { - // TODO: Handle when nonce is not collected (retry counter? Do not increment nested_target_key) - // TODO: Look into using MfClassicNt more + // TODO FL-3926: Handle when nonce is not collected (retry counter? Do not increment nested_target_key) + // TODO FL-3926: Look into using MfClassicNt more NfcCommand command = NfcCommandContinue; MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; @@ -1563,8 +1561,8 @@ static MfClassicKey* search_dicts_for_nonce_key( } NfcCommand mf_classic_poller_handler_nested_dict_attack(MfClassicPoller* instance) { - // TODO: Handle when nonce is not collected (retry counter? Do not increment nested_target_key) - // TODO: Look into using MfClassicNt more + // TODO FL-3926: Handle when nonce is not collected (retry counter? Do not increment nested_target_key) + // TODO FL-3926: Look into using MfClassicNt more NfcCommand command = NfcCommandContinue; MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; @@ -1706,7 +1704,7 @@ NfcCommand mf_classic_poller_handler_nested_log(MfClassicPoller* instance) { if(weak_prng && (!(static_encrypted)) && (dict_attack_ctx->nested_nonce.count != 2)) { FURI_LOG_E( TAG, - "MfClassicPollerStateNestedLog expected 2 nonces, received %u", + "MfClassicPollerStateNestedLog expected 2 nonces, received %zu", dict_attack_ctx->nested_nonce.count); break; } @@ -1722,7 +1720,7 @@ NfcCommand mf_classic_poller_handler_nested_log(MfClassicPoller* instance) { bool params_write_success = true; for(size_t i = 0; i < nonce_pair_count; i++) { MfClassicNestedNonce* nonce = &dict_attack_ctx->nested_nonce.nonces[i]; - // TODO: Avoid repeating logic here + // TODO FL-3926: Avoid repeating logic here uint8_t nonce_sector = nonce->key_idx / (weak_prng ? 4 : 2); MfClassicKeyType nonce_key_type = (nonce->key_idx % (weak_prng ? 4 : 2) < (weak_prng ? 2 : 1)) ? MfClassicKeyTypeA : @@ -1857,7 +1855,7 @@ NfcCommand mf_classic_poller_handler_nested_controller(MfClassicPoller* instance } else if(dict_attack_ctx->prng_type == MfClassicPrngTypeNoTag) { FURI_LOG_E(TAG, "No tag detected"); // Free nonce array - // TODO: Consider using .count here + // TODO FL-3926: Consider using .count here if(dict_attack_ctx->nested_nonce.nonces) { free(dict_attack_ctx->nested_nonce.nonces); dict_attack_ctx->nested_nonce.nonces = NULL; @@ -1868,7 +1866,7 @@ NfcCommand mf_classic_poller_handler_nested_controller(MfClassicPoller* instance } if(dict_attack_ctx->nested_nonce.nonces) { // Free nonce array - // TODO: Consider using .count here + // TODO FL-3926: Consider using .count here free(dict_attack_ctx->nested_nonce.nonces); dict_attack_ctx->nested_nonce.nonces = NULL; dict_attack_ctx->nested_nonce.count = 0; @@ -1935,7 +1933,7 @@ NfcCommand mf_classic_poller_handler_nested_controller(MfClassicPoller* instance } if(!(dict_attack_ctx->auth_passed)) { dict_attack_ctx->attempt_count++; - } else if(dict_attack_ctx->auth_passed && !(initial_dict_attack_iter)) { + } else if(!(initial_dict_attack_iter)) { dict_attack_ctx->nested_target_key++; dict_attack_ctx->attempt_count = 0; } diff --git a/lib/subghz/protocols/alutech_at_4n.c b/lib/subghz/protocols/alutech_at_4n.c index b6fb8d290..091a726ed 100644 --- a/lib/subghz/protocols/alutech_at_4n.c +++ b/lib/subghz/protocols/alutech_at_4n.c @@ -308,16 +308,22 @@ bool subghz_protocol_alutech_at_4n_create_data( instance->generic.serial = serial; instance->generic.cnt = cnt; instance->generic.data_count_bit = 72; - bool res = subghz_protocol_alutech_at_4n_gen_data(instance, btn); - if(res) { + if(subghz_protocol_alutech_at_4n_gen_data(instance, btn)) { if((subghz_block_generic_serialize(&instance->generic, flipper_format, preset) != - SubGhzProtocolStatusOk) || - !flipper_format_write_uint32(flipper_format, "CRC", &instance->crc, 1)) { + SubGhzProtocolStatusOk)) { + FURI_LOG_E(TAG, "Serialize error"); + return false; + } + if(!flipper_format_rewind(flipper_format)) { + FURI_LOG_E(TAG, "Rewind error"); + return false; + } + if(!flipper_format_insert_or_update_uint32(flipper_format, "CRC", &instance->crc, 1)) { FURI_LOG_E(TAG, "Unable to add CRC"); - res = false; + return false; } } - return res; + return true; } /** @@ -667,8 +673,12 @@ SubGhzProtocolStatus subghz_protocol_decoder_alutech_at_4n_serialize( SubGhzProtocolDecoderAlutech_at_4n* instance = context; SubGhzProtocolStatus res = subghz_block_generic_serialize(&instance->generic, flipper_format, preset); + if(!flipper_format_rewind(flipper_format)) { + FURI_LOG_E(TAG, "Rewind error"); + res = SubGhzProtocolStatusErrorParserOthers; + } if((res == SubGhzProtocolStatusOk) && - !flipper_format_write_uint32(flipper_format, "CRC", &instance->crc, 1)) { + !flipper_format_insert_or_update_uint32(flipper_format, "CRC", &instance->crc, 1)) { FURI_LOG_E(TAG, "Unable to add CRC"); res = SubGhzProtocolStatusErrorParserOthers; } diff --git a/lib/subghz/protocols/keeloq.c b/lib/subghz/protocols/keeloq.c index b1dbb6f86..4d458bc10 100644 --- a/lib/subghz/protocols/keeloq.c +++ b/lib/subghz/protocols/keeloq.c @@ -357,12 +357,12 @@ bool subghz_protocol_keeloq_create_data( instance->generic.cnt = cnt; instance->manufacture_name = manufacture_name; instance->generic.data_count_bit = 64; - bool res = subghz_protocol_keeloq_gen_data(instance, btn, false); - if(res) { - return SubGhzProtocolStatusOk == - subghz_block_generic_serialize(&instance->generic, flipper_format, preset); + if(subghz_protocol_keeloq_gen_data(instance, btn, false)) { + return ( + subghz_block_generic_serialize(&instance->generic, flipper_format, preset) == + SubGhzProtocolStatusOk); } - return res; + return false; } bool subghz_protocol_keeloq_bft_create_data( @@ -382,13 +382,13 @@ bool subghz_protocol_keeloq_bft_create_data( instance->generic.seed = seed; instance->manufacture_name = manufacture_name; instance->generic.data_count_bit = 64; - // roguuemaster don't steal.!!!! - bool res = subghz_protocol_keeloq_gen_data(instance, btn, false); - if(res) { - return SubGhzProtocolStatusOk == - subghz_block_generic_serialize(&instance->generic, flipper_format, preset); + // hehehehe + if(subghz_protocol_keeloq_gen_data(instance, btn, false)) { + return ( + subghz_block_generic_serialize(&instance->generic, flipper_format, preset) == + SubGhzProtocolStatusOk); } - return res; + return false; } /** diff --git a/lib/subghz/protocols/kinggates_stylo_4k.c b/lib/subghz/protocols/kinggates_stylo_4k.c index fc7ca01fd..7e7488f00 100644 --- a/lib/subghz/protocols/kinggates_stylo_4k.c +++ b/lib/subghz/protocols/kinggates_stylo_4k.c @@ -309,7 +309,7 @@ SubGhzProtocolStatus subghz_protocol_encoder_kinggates_stylo_4k_deserialize( key_data[sizeof(uint64_t) - i - 1] = (instance->generic.data_2 >> i * 8) & 0xFF; } if(!flipper_format_update_hex(flipper_format, "Data", key_data, sizeof(uint64_t))) { - FURI_LOG_E(TAG, "Unable to add Key"); + FURI_LOG_E(TAG, "Unable to update Data"); break; } @@ -524,8 +524,13 @@ SubGhzProtocolStatus subghz_protocol_decoder_kinggates_stylo_4k_serialize( key_data[sizeof(uint64_t) - i - 1] = (instance->generic.data_2 >> (i * 8)) & 0xFF; } + if(!flipper_format_rewind(flipper_format)) { + FURI_LOG_E(TAG, "Rewind error"); + ret = SubGhzProtocolStatusErrorParserOthers; + } + if((ret == SubGhzProtocolStatusOk) && - !flipper_format_write_hex(flipper_format, "Data", key_data, sizeof(uint64_t))) { + !flipper_format_insert_or_update_hex(flipper_format, "Data", key_data, sizeof(uint64_t))) { FURI_LOG_E(TAG, "Unable to add Data"); ret = SubGhzProtocolStatusErrorParserOthers; } diff --git a/lib/subghz/protocols/nice_flor_s.c b/lib/subghz/protocols/nice_flor_s.c index 19273e18e..1e5c14c6e 100644 --- a/lib/subghz/protocols/nice_flor_s.c +++ b/lib/subghz/protocols/nice_flor_s.c @@ -271,7 +271,7 @@ SubGhzProtocolStatus key_data[sizeof(uint64_t) - i - 1] = (instance->generic.data >> i * 8) & 0xFF; } if(!flipper_format_update_hex(flipper_format, "Key", key_data, sizeof(uint64_t))) { - FURI_LOG_E(TAG, "Unable to add Key"); + FURI_LOG_E(TAG, "Unable to update Key"); break; } @@ -282,7 +282,7 @@ SubGhzProtocolStatus } uint32_t temp = (instance->generic.data_2 >> 4) & 0xFFFFF; if(!flipper_format_update_uint32(flipper_format, "Data", &temp, 1)) { - FURI_LOG_E(TAG, "Unable to add Data"); + FURI_LOG_E(TAG, "Unable to update Data"); } } @@ -711,8 +711,13 @@ SubGhzProtocolStatus subghz_protocol_decoder_nice_flor_s_serialize( SubGhzProtocolStatus ret = subghz_block_generic_serialize(&instance->generic, flipper_format, preset); if(instance->generic.data_count_bit == NICE_ONE_COUNT_BIT) { + if(!flipper_format_rewind(flipper_format)) { + FURI_LOG_E(TAG, "Rewind error"); + ret = SubGhzProtocolStatusErrorParserOthers; + } if((ret == SubGhzProtocolStatusOk) && - !flipper_format_write_uint32(flipper_format, "Data", (uint32_t*)&instance->data, 1)) { + !flipper_format_insert_or_update_uint32( + flipper_format, "Data", (uint32_t*)&instance->data, 1)) { FURI_LOG_E(TAG, "Unable to add Data"); ret = SubGhzProtocolStatusErrorParserOthers; } diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 7943c4cfc..b5d51a0dd 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,77.2,, +Version,+,78.1,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, @@ -1120,6 +1120,7 @@ Function,+,furi_event_loop_is_subscribed,_Bool,"FuriEventLoop*, FuriEventLoopObj Function,+,furi_event_loop_pend_callback,void,"FuriEventLoop*, FuriEventLoopPendingCallback, void*" Function,+,furi_event_loop_run,void,FuriEventLoop* Function,+,furi_event_loop_stop,void,FuriEventLoop* +Function,+,furi_event_loop_subscribe_event_flag,void,"FuriEventLoop*, FuriEventFlag*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" Function,+,furi_event_loop_subscribe_message_queue,void,"FuriEventLoop*, FuriMessageQueue*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" Function,+,furi_event_loop_subscribe_mutex,void,"FuriEventLoop*, FuriMutex*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" Function,+,furi_event_loop_subscribe_semaphore,void,"FuriEventLoop*, FuriSemaphore*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" @@ -1376,6 +1377,7 @@ Function,-,furi_hal_resources_init_early,void, Function,+,furi_hal_resources_pin_by_name,const GpioPinRecord*,const char* Function,+,furi_hal_resources_pin_by_number,const GpioPinRecord*,uint8_t Function,-,furi_hal_rtc_deinit_early,void, +Function,-,furi_hal_rtc_get_alarm,_Bool,DateTime* Function,+,furi_hal_rtc_get_boot_mode,FuriHalRtcBootMode, Function,+,furi_hal_rtc_get_datetime,void,DateTime* Function,+,furi_hal_rtc_get_fault_data,uint32_t, @@ -1393,8 +1395,11 @@ Function,+,furi_hal_rtc_get_timestamp,uint32_t, Function,-,furi_hal_rtc_init,void, Function,-,furi_hal_rtc_init_early,void, Function,+,furi_hal_rtc_is_flag_set,_Bool,FuriHalRtcFlag +Function,-,furi_hal_rtc_prepare_for_shutdown,void, Function,+,furi_hal_rtc_reset_flag,void,FuriHalRtcFlag Function,+,furi_hal_rtc_reset_registers,void, +Function,-,furi_hal_rtc_set_alarm,void,"const DateTime*, _Bool" +Function,-,furi_hal_rtc_set_alarm_callback,void,"FuriHalRtcAlarmCallback, void*" Function,+,furi_hal_rtc_set_boot_mode,void,FuriHalRtcBootMode Function,+,furi_hal_rtc_set_datetime,void,DateTime* Function,+,furi_hal_rtc_set_fault_data,void,uint32_t @@ -3114,6 +3119,7 @@ Variable,+,sequence_display_backlight_off,const NotificationSequence, Variable,+,sequence_display_backlight_off_delay_1000,const NotificationSequence, Variable,+,sequence_display_backlight_on,const NotificationSequence, Variable,+,sequence_double_vibro,const NotificationSequence, +Variable,+,sequence_empty,const NotificationSequence, Variable,+,sequence_error,const NotificationSequence, Variable,+,sequence_lcd_contrast_update,const NotificationSequence, Variable,+,sequence_not_charging,const NotificationSequence, diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 55c66b02d..af8ab3f60 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,77.2,, +Version,+,78.1,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/main/archive/helpers/archive_helpers_ext.h,, Header,+,applications/main/subghz/subghz_fap.h,, @@ -1280,6 +1280,7 @@ Function,+,furi_event_loop_is_subscribed,_Bool,"FuriEventLoop*, FuriEventLoopObj Function,+,furi_event_loop_pend_callback,void,"FuriEventLoop*, FuriEventLoopPendingCallback, void*" Function,+,furi_event_loop_run,void,FuriEventLoop* Function,+,furi_event_loop_stop,void,FuriEventLoop* +Function,+,furi_event_loop_subscribe_event_flag,void,"FuriEventLoop*, FuriEventFlag*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" Function,+,furi_event_loop_subscribe_message_queue,void,"FuriEventLoop*, FuriMessageQueue*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" Function,+,furi_event_loop_subscribe_mutex,void,"FuriEventLoop*, FuriMutex*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" Function,+,furi_event_loop_subscribe_semaphore,void,"FuriEventLoop*, FuriSemaphore*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" @@ -1622,6 +1623,7 @@ Function,+,furi_hal_rfid_tim_read_pause,void, Function,+,furi_hal_rfid_tim_read_start,void,"float, float" Function,+,furi_hal_rfid_tim_read_stop,void, Function,-,furi_hal_rtc_deinit_early,void, +Function,+,furi_hal_rtc_get_alarm,_Bool,DateTime* Function,+,furi_hal_rtc_get_boot_mode,FuriHalRtcBootMode, Function,+,furi_hal_rtc_get_datetime,void,DateTime* Function,+,furi_hal_rtc_get_fault_data,uint32_t, @@ -1639,8 +1641,11 @@ Function,+,furi_hal_rtc_get_timestamp,uint32_t, Function,-,furi_hal_rtc_init,void, Function,-,furi_hal_rtc_init_early,void, Function,+,furi_hal_rtc_is_flag_set,_Bool,FuriHalRtcFlag +Function,-,furi_hal_rtc_prepare_for_shutdown,void, Function,+,furi_hal_rtc_reset_flag,void,FuriHalRtcFlag Function,+,furi_hal_rtc_reset_registers,void, +Function,+,furi_hal_rtc_set_alarm,void,"const DateTime*, _Bool" +Function,-,furi_hal_rtc_set_alarm_callback,void,"FuriHalRtcAlarmCallback, void*" Function,+,furi_hal_rtc_set_boot_mode,void,FuriHalRtcBootMode Function,+,furi_hal_rtc_set_datetime,void,DateTime* Function,+,furi_hal_rtc_set_fault_data,void,uint32_t @@ -3850,6 +3855,7 @@ Function,-,ynf,float,"int, float" Variable,-,AHBPrescTable,const uint32_t[16], Variable,-,APBPrescTable,const uint32_t[8], Variable,+,A_125khz_14,const Icon, +Variable,+,A_Alarm_47x39,const Icon, Variable,+,A_BadKb_14,const Icon, Variable,+,A_GPIO_14,const Icon, Variable,+,A_Infrared_14,const Icon, @@ -4432,6 +4438,7 @@ Variable,+,sequence_display_backlight_off,const NotificationSequence, Variable,+,sequence_display_backlight_off_delay_1000,const NotificationSequence, Variable,+,sequence_display_backlight_on,const NotificationSequence, Variable,+,sequence_double_vibro,const NotificationSequence, +Variable,+,sequence_empty,const NotificationSequence, Variable,+,sequence_error,const NotificationSequence, Variable,+,sequence_lcd_contrast_update,const NotificationSequence, Variable,+,sequence_not_charging,const NotificationSequence, diff --git a/targets/f7/furi_hal/furi_hal_interrupt.c b/targets/f7/furi_hal/furi_hal_interrupt.c index 5012ce88f..f93248eb8 100644 --- a/targets/f7/furi_hal/furi_hal_interrupt.c +++ b/targets/f7/furi_hal/furi_hal_interrupt.c @@ -68,6 +68,9 @@ const IRQn_Type furi_hal_interrupt_irqn[FuriHalInterruptIdMax] = { // COMP [FuriHalInterruptIdCOMP] = COMP_IRQn, + // RTC + [FuriHalInterruptIdRtcAlarm] = RTC_Alarm_IRQn, + // HSEM [FuriHalInterruptIdHsem] = HSEM_IRQn, @@ -256,6 +259,10 @@ void DMA2_Channel7_IRQHandler(void) { furi_hal_interrupt_call(FuriHalInterruptIdDma2Ch7); } +void RTC_Alarm_IRQHandler(void) { + furi_hal_interrupt_call(FuriHalInterruptIdRtcAlarm); +} + void HSEM_IRQHandler(void) { furi_hal_interrupt_call(FuriHalInterruptIdHsem); } diff --git a/targets/f7/furi_hal/furi_hal_interrupt.h b/targets/f7/furi_hal/furi_hal_interrupt.h index 2326d3c0a..5ea830ced 100644 --- a/targets/f7/furi_hal/furi_hal_interrupt.h +++ b/targets/f7/furi_hal/furi_hal_interrupt.h @@ -42,6 +42,9 @@ typedef enum { // Comp FuriHalInterruptIdCOMP, + // RTC + FuriHalInterruptIdRtcAlarm, + // HSEM FuriHalInterruptIdHsem, diff --git a/targets/f7/furi_hal/furi_hal_power.c b/targets/f7/furi_hal/furi_hal_power.c index 37c6a8b1b..fe5c0cf17 100644 --- a/targets/f7/furi_hal/furi_hal_power.c +++ b/targets/f7/furi_hal/furi_hal_power.c @@ -326,6 +326,7 @@ void furi_hal_power_shutdown(void) { void furi_hal_power_off(void) { // Crutch: shutting down with ext 3V3 off is causing LSE to stop + furi_hal_rtc_prepare_for_shutdown(); furi_hal_power_enable_external_3_3v(); furi_hal_vibro_on(true); furi_delay_us(50000); diff --git a/targets/f7/furi_hal/furi_hal_rtc.c b/targets/f7/furi_hal/furi_hal_rtc.c index d5cda7476..ab592fb78 100644 --- a/targets/f7/furi_hal/furi_hal_rtc.c +++ b/targets/f7/furi_hal/furi_hal_rtc.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -42,6 +43,13 @@ typedef struct { _Static_assert(sizeof(SystemReg) == 4, "SystemReg size mismatch"); +typedef struct { + FuriHalRtcAlarmCallback alarm_callback; + void* alarm_callback_context; +} FuriHalRtc; + +static FuriHalRtc furi_hal_rtc = {}; + static const FuriHalSerialId furi_hal_rtc_log_devices[] = { [FuriHalRtcLogDeviceUsart] = FuriHalSerialIdUsart, [FuriHalRtcLogDeviceLpuart] = FuriHalSerialIdLpuart, @@ -60,6 +68,17 @@ static const uint32_t furi_hal_rtc_log_baud_rates[] = { [FuriHalRtcLogBaudRate1843200] = 1843200, }; +static void furi_hal_rtc_enter_init_mode(void) { + LL_RTC_EnableInitMode(RTC); + while(LL_RTC_IsActiveFlag_INIT(RTC) != 1) + ; +} + +static void furi_hal_rtc_exit_init_mode(void) { + LL_RTC_DisableInitMode(RTC); + furi_hal_rtc_sync_shadow(); +} + static void furi_hal_rtc_reset(void) { LL_RCC_ForceBackupDomainReset(); LL_RCC_ReleaseBackupDomainReset(); @@ -127,6 +146,36 @@ static void furi_hal_rtc_recover(void) { } } +static void furi_hal_rtc_alarm_handler(void* context) { + UNUSED(context); + + if(LL_RTC_IsActiveFlag_ALRA(RTC) != 0) { + /* Clear the Alarm interrupt pending bit */ + LL_RTC_ClearFlag_ALRA(RTC); + + /* Alarm callback */ + furi_check(furi_hal_rtc.alarm_callback); + furi_hal_rtc.alarm_callback(furi_hal_rtc.alarm_callback_context); + } + LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_17); +} + +static void furi_hal_rtc_set_alarm_out(bool enable) { + FURI_CRITICAL_ENTER(); + LL_RTC_DisableWriteProtection(RTC); + if(enable) { + LL_RTC_SetAlarmOutEvent(RTC, LL_RTC_ALARMOUT_ALMA); + LL_RTC_SetOutputPolarity(RTC, LL_RTC_OUTPUTPOLARITY_PIN_LOW); + LL_RTC_SetAlarmOutputType(RTC, LL_RTC_ALARM_OUTPUTTYPE_OPENDRAIN); + } else { + LL_RTC_SetAlarmOutEvent(RTC, LL_RTC_ALARMOUT_DISABLE); + LL_RTC_SetOutputPolarity(RTC, LL_RTC_OUTPUTPOLARITY_PIN_LOW); + LL_RTC_SetAlarmOutputType(RTC, LL_RTC_ALARM_OUTPUTTYPE_OPENDRAIN); + } + LL_RTC_EnableWriteProtection(RTC); + FURI_CRITICAL_EXIT(); +} + void furi_hal_rtc_init_early(void) { // Enable RTCAPB clock LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_RTCAPB); @@ -167,6 +216,11 @@ void furi_hal_rtc_init(void) { furi_hal_rtc_log_baud_rates[furi_hal_rtc_get_log_baud_rate()]); FURI_LOG_I(TAG, "Init OK"); + furi_hal_rtc_set_alarm_out(false); +} + +void furi_hal_rtc_prepare_for_shutdown(void) { + furi_hal_rtc_set_alarm_out(true); } void furi_hal_rtc_sync_shadow(void) { @@ -347,9 +401,7 @@ void furi_hal_rtc_set_datetime(DateTime* datetime) { LL_RTC_DisableWriteProtection(RTC); /* Enter Initialization mode and wait for INIT flag to be set */ - LL_RTC_EnableInitMode(RTC); - while(!LL_RTC_IsActiveFlag_INIT(RTC)) { - } + furi_hal_rtc_enter_init_mode(); /* Set time */ LL_RTC_TIME_Config( @@ -368,9 +420,7 @@ void furi_hal_rtc_set_datetime(DateTime* datetime) { __LL_RTC_CONVERT_BIN2BCD(datetime->year - 2000)); /* Exit Initialization mode */ - LL_RTC_DisableInitMode(RTC); - - furi_hal_rtc_sync_shadow(); + furi_hal_rtc_exit_init_mode(); /* Enable write protection */ LL_RTC_EnableWriteProtection(RTC); @@ -395,6 +445,82 @@ void furi_hal_rtc_get_datetime(DateTime* datetime) { datetime->weekday = __LL_RTC_CONVERT_BCD2BIN((date >> 24) & 0xFF); } +void furi_hal_rtc_set_alarm(const DateTime* datetime, bool enabled) { + furi_check(!FURI_IS_IRQ_MODE()); + + FURI_CRITICAL_ENTER(); + LL_RTC_DisableWriteProtection(RTC); + + if(datetime) { + LL_RTC_ALMA_ConfigTime( + RTC, + LL_RTC_ALMA_TIME_FORMAT_AM, + __LL_RTC_CONVERT_BIN2BCD(datetime->hour), + __LL_RTC_CONVERT_BIN2BCD(datetime->minute), + __LL_RTC_CONVERT_BIN2BCD(datetime->second)); + LL_RTC_ALMA_SetMask(RTC, LL_RTC_ALMA_MASK_DATEWEEKDAY); + } + + if(enabled) { + LL_RTC_ClearFlag_ALRA(RTC); + LL_RTC_ALMA_Enable(RTC); + } else { + LL_RTC_ALMA_Disable(RTC); + LL_RTC_ClearFlag_ALRA(RTC); + } + + LL_RTC_EnableWriteProtection(RTC); + FURI_CRITICAL_EXIT(); +} + +bool furi_hal_rtc_get_alarm(DateTime* datetime) { + furi_check(datetime); + + memset(datetime, 0, sizeof(DateTime)); + + datetime->hour = __LL_RTC_CONVERT_BCD2BIN(LL_RTC_ALMA_GetHour(RTC)); + datetime->minute = __LL_RTC_CONVERT_BCD2BIN(LL_RTC_ALMA_GetMinute(RTC)); + datetime->second = __LL_RTC_CONVERT_BCD2BIN(LL_RTC_ALMA_GetSecond(RTC)); + + return READ_BIT(RTC->CR, RTC_CR_ALRAE); +} + +void furi_hal_rtc_set_alarm_callback(FuriHalRtcAlarmCallback callback, void* context) { + FURI_CRITICAL_ENTER(); + LL_RTC_DisableWriteProtection(RTC); + if(callback) { + furi_check(!furi_hal_rtc.alarm_callback); + // Set our callbacks + furi_hal_rtc.alarm_callback = callback; + furi_hal_rtc.alarm_callback_context = context; + // Enable RTC ISR + furi_hal_interrupt_set_isr(FuriHalInterruptIdRtcAlarm, furi_hal_rtc_alarm_handler, NULL); + // Hello EXTI my old friend + // Chain: RTC->LINE-17->EXTI->NVIC->FuriHalInterruptIdRtcAlarm + LL_EXTI_EnableRisingTrig_0_31(LL_EXTI_LINE_17); + LL_EXTI_EnableIT_0_31(LL_EXTI_LINE_17); + // Enable alarm interrupt + LL_RTC_EnableIT_ALRA(RTC); + // Force trigger + furi_hal_rtc_alarm_handler(NULL); + } else { + furi_check(furi_hal_rtc.alarm_callback); + // Cleanup EXTI flags and config + LL_EXTI_DisableIT_0_31(LL_EXTI_LINE_17); + LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_17); + LL_EXTI_DisableRisingTrig_0_31(LL_EXTI_LINE_17); + // Cleanup NVIC flags and config + furi_hal_interrupt_set_isr(FuriHalInterruptIdRtcAlarm, NULL, NULL); + // Disable alarm interrupt + LL_RTC_DisableIT_ALRA(RTC); + + furi_hal_rtc.alarm_callback = NULL; + furi_hal_rtc.alarm_callback_context = NULL; + } + LL_RTC_EnableWriteProtection(RTC); + FURI_CRITICAL_EXIT(); +} + void furi_hal_rtc_set_fault_data(uint32_t value) { furi_hal_rtc_set_register(FuriHalRtcRegisterFaultData, value); } diff --git a/targets/f7/furi_hal/furi_hal_rtc.h b/targets/f7/furi_hal/furi_hal_rtc.h index 9b7022eab..49d35eeb9 100644 --- a/targets/f7/furi_hal/furi_hal_rtc.h +++ b/targets/f7/furi_hal/furi_hal_rtc.h @@ -98,6 +98,14 @@ void furi_hal_rtc_deinit_early(void); /** Initialize RTC subsystem */ void furi_hal_rtc_init(void); +/** Prepare system for shutdown + * + * This function must be called before system sent to transport mode(power off). + * FlipperZero implementation configures and enables ALARM output on pin PC13 + * (Back button). This allows the system to wake-up charger from transport mode. + */ +void furi_hal_rtc_prepare_for_shutdown(void); + /** Force sync shadow registers */ void furi_hal_rtc_sync_shadow(void); @@ -247,6 +255,38 @@ void furi_hal_rtc_set_datetime(DateTime* datetime); */ void furi_hal_rtc_get_datetime(DateTime* datetime); +/** Set alarm + * + * @param[in] datetime The date time to set or NULL if time change is not needed + * @param[in] enabled Indicates if alarm must be enabled or disabled + */ +void furi_hal_rtc_set_alarm(const DateTime* datetime, bool enabled); + +/** Get alarm + * + * @param datetime Pointer to DateTime object + * + * @return true if alarm was set, false otherwise + */ +bool furi_hal_rtc_get_alarm(DateTime* datetime); + +/** Furi HAL RTC alarm callback signature */ +typedef void (*FuriHalRtcAlarmCallback)(void* context); + +/** Set alarm callback + * + * Use it to subscribe to alarm trigger event. Setting alarm callback is + * independent from setting alarm. + * + * @warning Normally this callback will be delivered from the ISR, however we may + * deliver it while this function is called. This happens when + * the alarm has already triggered, but there was no ISR set. + * + * @param[in] callback The callback + * @param context The context + */ +void furi_hal_rtc_set_alarm_callback(FuriHalRtcAlarmCallback callback, void* context); + /** Set RTC Fault Data * * @param[in] value The value diff --git a/targets/furi_hal_include/furi_hal.h b/targets/furi_hal_include/furi_hal.h index 00997de5d..697a315ba 100644 --- a/targets/furi_hal_include/furi_hal.h +++ b/targets/furi_hal_include/furi_hal.h @@ -52,13 +52,24 @@ void furi_hal_set_is_normal_boot(bool value); /** True if booting normally with all subsystems */ bool furi_hal_is_normal_boot(void); -/** Early FuriHal init, only essential subsystems */ +/** Early FuriHal init + * + * Init essential subsystems used in pre-DFU stage. + * This state can be undone with `furi_hal_deinit_early`. + * + */ void furi_hal_init_early(void); -/** Early FuriHal deinit */ +/** Early FuriHal deinit + * + * Undo `furi_hal_init_early`, prepare system for switch to another firmware/bootloader. + */ void furi_hal_deinit_early(void); -/** Init FuriHal */ +/** Init FuriHal + * + * Initialize the rest of the HAL, must be used after `furi_hal_init_early`. + */ void furi_hal_init(void); /** Jump to the void* diff --git a/tsconfig.json b/tsconfig.json index 2655a8b97..53f0a3625 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,13 +3,13 @@ "checkJs": true, "module": "CommonJS", "typeRoots": [ - "./applications/system/js_app/types" + "./applications/system/js_app/packages/fz-sdk/" ], "noLib": true, }, "include": [ "./applications/system/js_app/examples/apps/Scripts", "./applications/debug/unit_tests/resources/unit_tests/js", - "./applications/system/js_app/types/global.d.ts", + "./applications/system/js_app/packages/fz-sdk/global.d.ts", ] } \ No newline at end of file