diff --git a/.drone.yml b/.drone.yml index d14ebd5177..0087130397 100644 --- a/.drone.yml +++ b/.drone.yml @@ -114,7 +114,7 @@ steps: from_secret: fbt_link - name: "Bundle self-update packages" - image: kramos/alpine-zip + image: joshkeegan/zip commands: - cp artifacts-extra-apps/flipper-z-f7-update-${DRONE_TAG}e.tgz . - cp artifacts-rgb-patch/flipper-z-f7-update-${DRONE_TAG}r.tgz . @@ -245,9 +245,11 @@ steps: - wget "https://raw.githubusercontent.com/fieu/discord.sh/2253303efc0e7211ac2777d2535054cbb872f1e0/discord.sh" - chmod +x ./discord.sh - sed -n '/## Main changes/,/## Other changes/p' CHANGELOG.md | sed -e 's/## Main changes//' -e 's/## Other changes//' > changelogcut.txt - - truncate -s -1 changelogcut.txt - - tail -c +2 changelogcut.txt > changelogready.txt + - head -c 1544 changelogcut.txt > changelogcutfin.txt + - truncate -s -1 changelogcutfin.txt + - tail -c +2 changelogcutfin.txt > changelogready.txt - rm -f changelogcut.txt + - rm -f changelogcutfin.txt - echo '' >> changelogready.txt - echo '## [Read full changelog](https://github.com/DarkFlippers/unleashed-firmware/releases/tag/'${DRONE_TAG}')' >> changelogready.txt - sed -i 's/(releasever)/'${DRONE_TAG}'/g' .ci_files/release_msg_discord.txt @@ -417,7 +419,7 @@ steps: from_secret: fbt_link - name: "Bundle self-update packages" - image: kramos/alpine-zip + image: joshkeegan/zip commands: - cp artifacts-extra-apps/flipper-z-f7-update-${DRONE_BUILD_NUMBER}e.tgz . - cp artifacts-rgb-patch/flipper-z-f7-update-${DRONE_BUILD_NUMBER}r.tgz . @@ -529,14 +531,20 @@ steps: commands: - wget "https://raw.githubusercontent.com/fieu/discord.sh/2253303efc0e7211ac2777d2535054cbb872f1e0/discord.sh" - chmod +x ./discord.sh - - sed -n '/## Main changes/,/

/p' CHANGELOG.md | sed -e 's/

//' > changelogcut.txt - - truncate -s -1 changelogcut.txt + - sed -n '/## Main changes/,/## Other changes/p' CHANGELOG.md | sed -e 's/## Main changes//' -e 's/## Other changes//' > changelogcut.txt + - head -c 1544 changelogcut.txt > changelogcutfin.txt + - truncate -s -1 changelogcutfin.txt + - tail -c +2 changelogcutfin.txt > changelogready.txt + - rm -f changelogcut.txt + - rm -f changelogcutfin.txt + - echo '' >> changelogready.txt + - echo '## [Read full changelog](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/CHANGELOG.md)' >> changelogready.txt - sed -i 's/(buildnum)/'${DRONE_BUILD_NUMBER}'/g' .ci_files/devbuild_msg_discord.txt - sed -i 's/(commitsha)/'${DRONE_COMMIT_SHA}'/g' .ci_files/devbuild_msg_discord.txt - sed -i 's/(buildnum)/'${DRONE_BUILD_NUMBER}'/g' .ci_files/devbuild_msg_telegram.txt - sed -i 's/(commitsha)/'${DRONE_COMMIT_SHA}'/g' .ci_files/devbuild_msg_telegram.txt - cp .ci_files/devbuild_msg_telegram.txt tg_dev_message.tpl - - ./discord.sh --title "Changelog" --description "$(jq -Rs . **Breaking API change** + - **Backporting custom features** (read about most of the changes after other changes section) (by @xMasterX and @Willy-JL) + - Add i2c & SPI module (by @jamisonderek) +* OFW: FuriHal, drivers: rework gauge initialization routine -> **Downgrade to older releases may break battery UI percent indicator, upgrade to this or newer version to restore** +* OFW: heap: increased size -> **More free RAM!!** +* OFW: New layout for BadUSB (es-LA) +* OFW: Require PIN on boot * Apps: **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes -* Docs: Improved the description steps to create a new remote BFT Mitto with more detailed and accurate instructions (by @chrostino | PR #805) -* OFW: FuriTimer: Use an event instead of a volatile bool to wait for deletion -* OFW: Threading, Timers improvements -* OFW: Replace all calls to strncpy with strlcpy, use strdup more, expose strlcat -* OFW: feat: add linux/gnome badusb demo resource files -* OFW: Exposed `view_dispatcher_get_event_loop` -* OFW: Infrared button operation fails now shows more informative messages -* OFW: Loader: Warn about missing SD card for main apps -* OFW: Desktop: Sanity check PIN length for good measure -* OFW: DialogEx: Fix NULL ptr crash -* OFW: Debug: use proper hook for handle_exit in flipperapps -* OFW: Clean up of LFS traces -* OFW: Proper integer parsing -* OFW: SubGhz: Fix RPC status for ButtonRelease event -* OFW: CCID: App changes -* OFW: 5V on GPIO control for ext. modules -* OFW: Gui: Add up and down button drawing functions to GUI elements -* OFW: Gui: change dialog_ex text ownership model -* OFW: Publishing T5577 page 1 block count macro +* SubGHz: Freq analyzer - Fix duplicated frequency lists and use user config for nearest frequency selector too +* SubGHz: Code cleanup and fix for rare dupicated (Data) field cases +* OFW: NFC TRT Parser: Additional checks to prevent false positives +* OFW PR 3992: Loader: Fix BusFault in handling of OOM (by @Willy-JL) +* OFW PR 3885: NFC: Add API to enforce ISO15693 mode (by @aaronjamt) +* OFW: NFC: iso14443_4a improvements (by @RebornedBrain) +* OFW: NFC: Plantain parser improvements (by @assasinfil) & fixes (by @mxcdoam) +* OFW: NFC: Moscow social card parser (by @assasinfil) +* OFW: fix: npm deps +* OFW: 目覚め時計 (Added alarm option and clock settings) +* OFW: JS: Backport and more additions & fixes +* OFW: nfc: add Caltrain zones for Clipper +* OFW: Update unit tests docs +* OFW: Fix JS memory corruption (in gpio module) +* OFW: Full-fledged JS SDK + npm packages +* OFW: FurEventLoop: add support for FuriEventFlag, simplify API +* OFW: lib: digital_signal: digital_sequence: add furi_hal.h wrapped in ifdefs +* OFW: Add warning about stealth mode in vibro CLI +* OFW: Small fixes in the wifi devboard docs +* OFW: BadUSB - Improve ChromeOS and GNOME demo scripts +* OFW: Small JS fixes +* OFW: Canvas: extended icon draw. +* OFW: Fixes Mouse Clicker Should have a "0" value setting for "as fast as possible" +* OFW: Wi-Fi Devboard documentation rework +* OFW: Furi: A Lot of Fixes +* OFW PR 3933: furi_hal_random: Wait for ready state and no errors before sampling (by @n1kolasM) +* OFW: nfc/clipper: Update BART station codes +* OFW: FuriThread: Improve state callbacks +* OFW: Documentation: update and cleanup +* OFW: Improve bit_buffer.h docs +* OFW: Prevent idle priority threads from potentially starving the FreeRTOS idle task +* OFW: IR universal remote additions +* OFW: Fix EM4100 T5577 writing block order (was already done in UL) +* OFW: kerel typo +* OFW: Folder rename fails +* OFW: Put errno into TCB +* OFW: Fix USB-UART bridge exit screen stopping the bridge prematurely +**More details on JS changes** (js changelog written by @Willy-JL , thanks!): +- Our custom JS SDK can be found on npm now: https://www.npmjs.com/org/darkflippers +- Non-exhaustive list of changes to help you fix your scripts: + - `badusb`: + - `setup()`: `mfr_name`, `prod_name`, `layout_path` parameters renamed to `mfrName`, `prodName`, `layoutPath` + - effort required to update old scripts using badusb: very minimal + - `dialog`: + - removed, now replaced by `gui/dialog` and `gui/file_picker` (see below) + - `event_loop`: + - new module, allows timer functionality, callbacks and event-driven programming, used heavily alongside gpio and gui modules + - `gpio`: + - fully overhauled, now you `get()` pin instances and perform actions on them like `.init()` + - now supports interrupts, callbacks and more cool things + - effort required to update old scripts using gpio: moderate + - `gui`: + - new module, fully overhauled, replaces dialog, keyboard, submenu, textbox modules + - higher barrier to entry than older modules (requires usage of `event_loop` and `gui.viewDispatcher`), but much more flexible, powerful and easier to extend + - includes all previously available js gui functionality (except `widget`), and also adds `gui/loading` and `gui/empty_screen` views + - currently `gui/file_picker` works different than other new view objects, it is a simple `.pickFile()` synchronous function, but this [may change later](https://github.com/flipperdevices/flipperzero-firmware/pull/3961#discussion_r1805579153) + - effort required to update old scripts using gui: extensive + - `keyboard`: + - removed, now replaced by `gui/text_input` and `gui/byte_input` (see above) + - `math`: + - `is_equal()` renamed to `isEqual()` + - `storage`: + - fully overhauled, now you `openFile()`s and perform actions on them like `.read()` + - now supports many more operations including different open modes, directories and much more + - effort required to update old scripts using storage: moderate + - `submenu`: + - removed, now replaced by `gui/submenu` (see above) + - `textbox`: + - removed, now replace by `gui/text_box` (see above) + - `widget`: + - only gui functionality not ported to new gui module, remains unchanged for now but likely to be ported later on + - globals: + - `__filepath` and `__dirpath` renamed to `__filename` and `__dirname` like in nodejs + - `to_string()` renamed and moved to number class as `n.toString()`, now supports optional base parameter + - `to_hex_string()` removed, now use `n.toString(16)` + - `parse_int()` renamed to `parseInt()`, now supports optional base parameter + - `to_upper_case()` and `to_lower_case()` renamed and moved to string class as `s.toUpperCase()` and `s.toLowerCase()` + - effort required to update old scripts using these: minimal + - Added type definitions (typescript files for type checking in IDE, Flipper does not run typescript) + - Documentation is incomplete and deprecated, from now on you should refer to type definitions (`applications/system/js_app/types`), those will always be correct + - Type definitions for extra modules we have that OFW doesn't will come later

#### Known NFC post-refactor regressions list: - Mifare Mini clones reading is broken (original mini working fine) (OFW) @@ -60,7 +133,7 @@ |cloudtips|only RU payments accepted|
QR image
|https://pay.cloudtips.ru/p/7b3e9d65| |YooMoney|only RU payments accepted|
QR image
|https://yoomoney.ru/fundraise/XA49mgQLPA0.221209| |USDT|(TRC20)|
QR image
|`TSXcitMSnWXUFqiUfEXrTVpVewXy2cYhrs`| -|ETH|(BSC/ERC20-Tokens)|
QR image
|`darkflippers.eth` (or `0xFebF1bBc8229418FF2408C07AF6Afa49152fEc6a`)| +|ETH|(BSC/ERC20-Tokens)|
QR image
|`0xFebF1bBc8229418FF2408C07AF6Afa49152fEc6a`| |BTC||
QR image
|`bc1q0np836jk9jwr4dd7p6qv66d04vamtqkxrecck9`| |SOL|(Solana/Tokens)|
QR image
|`DSgwouAEgu8iP5yr7EHHDqMNYWZxAqXWsTEeqCAXGLj8`| |DOGE||
QR image
|`D6R6gYgBn5LwTNmPyvAQR6bZ9EtGgFCpvv`| diff --git a/ReadMe.md b/ReadMe.md index 4c3807688a..f01dd48987 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -36,9 +36,14 @@ ## FAQ (frequently asked questions) [Follow this link to find answers to most asked questions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/FAQ.md) +## Our official domains +- https://flipperunleashed.com/ -> our main web page +- https://unleashedflip.com/ -> update server, direct .tgz update links for web updater or direct download + ## Dev builds (unstable) (built automatically from dev branch) - https://dev.unleashedflip.com/ - https://t.me/kotnehleb + ## Releases in Telegram - https://t.me/unleashed_fw @@ -112,7 +117,7 @@ Decoders/Encoders or emulation (+ programming mode) support made by @xMasterX: - Hay21 (dynamic 21 bit) with button parsing - Nero Radio 57bit (+ 56bit support) - CAME 12bit/24bit encoder fixes (Fixes are now merged in OFW) -- Keeloq: Dea Mio, Genius Bravo, GSN, HCS101, AN-Motors, JCM Tech, MHouse, Nice Smilo, DTM Neo, FAAC RC,XT, Mutancode, Normstahl, Beninca + Allmatic, Stilmatic, CAME Space, Aprimatic (model TR and similar), Centurion Nova (thanks Carlos !), Hormann EcoStar, Novoferm, Sommer +- Keeloq: Dea Mio, Genius Bravo, GSN, HCS101, AN-Motors, JCM Tech, MHouse, Nice Smilo, DTM Neo, FAAC RC,XT, Mutancode, Normstahl, Beninca + Allmatic, Stilmatic, CAME Space, Aprimatic (model TR and similar), Centurion Nova (thanks Carlos !), Hormann EcoStar, Novoferm, Sommer, Monarch (thanks @ashphx !) Protocols support made by Skorp (original implementation) and @xMasterX (current version): - CAME Atomo -> Update! check out new [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) @@ -150,7 +155,7 @@ You can support us by using links or addresses below: |cloudtips|only RU payments accepted|
QR image
|https://pay.cloudtips.ru/p/7b3e9d65| |YooMoney|only RU payments accepted|
QR image
|https://yoomoney.ru/fundraise/XA49mgQLPA0.221209| |USDT|(TRC20)|
QR image
|`TSXcitMSnWXUFqiUfEXrTVpVewXy2cYhrs`| -|ETH|(BSC/ERC20-Tokens)|
QR image
|`darkflippers.eth` (or `0xFebF1bBc8229418FF2408C07AF6Afa49152fEc6a`)| +|ETH|(BSC/ERC20-Tokens)|
QR image
|`0xFebF1bBc8229418FF2408C07AF6Afa49152fEc6a`| |BTC||
QR image
|`bc1q0np836jk9jwr4dd7p6qv66d04vamtqkxrecck9`| |SOL|(Solana/Tokens)|
QR image
|`DSgwouAEgu8iP5yr7EHHDqMNYWZxAqXWsTEeqCAXGLj8`| |DOGE||
QR image
|`D6R6gYgBn5LwTNmPyvAQR6bZ9EtGgFCpvv`| diff --git a/applications/debug/direct_draw/direct_draw.c b/applications/debug/direct_draw/direct_draw.c index bc1e554597..1e45a6d949 100644 --- a/applications/debug/direct_draw/direct_draw.c +++ b/applications/debug/direct_draw/direct_draw.c @@ -1,6 +1,5 @@ #include #include -#include #include #define BUFFER_SIZE (32U) @@ -42,10 +41,11 @@ static DirectDraw* direct_draw_alloc(void) { static void direct_draw_free(DirectDraw* instance) { furi_pubsub_unsubscribe(instance->input, instance->input_subscription); - instance->canvas = NULL; gui_direct_draw_release(instance->gui); furi_record_close(RECORD_GUI); furi_record_close(RECORD_INPUT_EVENTS); + + free(instance); } static void direct_draw_block(Canvas* canvas, uint32_t size, uint32_t counter) { 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 7f00e63f2e..1cddfa323d 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/application.fam b/applications/debug/unit_tests/application.fam index c87305847a..dec3283e4e 100644 --- a/applications/debug/unit_tests/application.fam +++ b/applications/debug/unit_tests/application.fam @@ -221,6 +221,14 @@ App( requires=["unit_tests"], ) +App( + appid="test_js", + sources=["tests/common/*.c", "tests/js/*.c"], + apptype=FlipperAppType.PLUGIN, + entry_point="get_api", + requires=["unit_tests", "js_app"], +) + App( appid="test_strint", sources=["tests/common/*.c", "tests/strint/*.c"], diff --git a/applications/debug/unit_tests/resources/unit_tests/js/basic.js b/applications/debug/unit_tests/resources/unit_tests/js/basic.js new file mode 100644 index 0000000000..a08041e9fd --- /dev/null +++ b/applications/debug/unit_tests/resources/unit_tests/js/basic.js @@ -0,0 +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("flipperdevices", flipper.firmwareVendor); +tests.assert_eq(0, flipper.jsSdkVersion[0]); +tests.assert_eq(1, flipper.jsSdkVersion[1]); diff --git a/applications/debug/unit_tests/resources/unit_tests/js/event_loop.js b/applications/debug/unit_tests/resources/unit_tests/js/event_loop.js new file mode 100644 index 0000000000..0437b82932 --- /dev/null +++ b/applications/debug/unit_tests/resources/unit_tests/js/event_loop.js @@ -0,0 +1,30 @@ +let tests = require("tests"); +let event_loop = require("event_loop"); + +let ext = { + i: 0, + received: false, +}; + +let queue = event_loop.queue(16); + +event_loop.subscribe(queue.input, function (_, item, tests, ext) { + tests.assert_eq(123, item); + ext.received = true; +}, tests, ext); + +event_loop.subscribe(event_loop.timer("periodic", 1), function (_, _item, queue, counter, ext) { + ext.i++; + queue.send(123); + if (counter === 10) + event_loop.stop(); + return [queue, counter + 1, ext]; +}, queue, 1, ext); + +event_loop.subscribe(event_loop.timer("oneshot", 1000), function (_, _item, tests) { + tests.fail("event loop was not stopped"); +}, tests); + +event_loop.run(); +tests.assert_eq(10, ext.i); +tests.assert_eq(true, ext.received); diff --git a/applications/debug/unit_tests/resources/unit_tests/js/math.js b/applications/debug/unit_tests/resources/unit_tests/js/math.js new file mode 100644 index 0000000000..ea8d80f914 --- /dev/null +++ b/applications/debug/unit_tests/resources/unit_tests/js/math.js @@ -0,0 +1,34 @@ +let tests = require("tests"); +let math = require("math"); + +// math.EPSILON on Flipper Zero is 2.22044604925031308085e-16 + +// basics +tests.assert_float_close(5, math.abs(-5), math.EPSILON); +tests.assert_float_close(0.5, math.abs(-0.5), math.EPSILON); +tests.assert_float_close(5, math.abs(5), math.EPSILON); +tests.assert_float_close(0.5, math.abs(0.5), math.EPSILON); +tests.assert_float_close(3, math.cbrt(27), math.EPSILON); +tests.assert_float_close(6, math.ceil(5.3), math.EPSILON); +tests.assert_float_close(31, math.clz32(1), math.EPSILON); +tests.assert_float_close(5, math.floor(5.7), math.EPSILON); +tests.assert_float_close(5, math.max(3, 5), math.EPSILON); +tests.assert_float_close(3, math.min(3, 5), math.EPSILON); +tests.assert_float_close(-1, math.sign(-5), math.EPSILON); +tests.assert_float_close(5, math.trunc(5.7), math.EPSILON); + +// trig +tests.assert_float_close(1.0471975511965976, math.acos(0.5), math.EPSILON); +tests.assert_float_close(1.3169578969248166, math.acosh(2), math.EPSILON); +tests.assert_float_close(0.5235987755982988, math.asin(0.5), math.EPSILON); +tests.assert_float_close(1.4436354751788103, math.asinh(2), math.EPSILON); +tests.assert_float_close(0.7853981633974483, math.atan(1), math.EPSILON); +tests.assert_float_close(0.7853981633974483, math.atan2(1, 1), math.EPSILON); +tests.assert_float_close(0.5493061443340549, math.atanh(0.5), math.EPSILON); +tests.assert_float_close(-1, math.cos(math.PI), math.EPSILON * 18); // Error 3.77475828372553223744e-15 +tests.assert_float_close(1, math.sin(math.PI / 2), math.EPSILON * 4.5); // Error 9.99200722162640886381e-16 + +// powers +tests.assert_float_close(5, math.sqrt(25), math.EPSILON); +tests.assert_float_close(8, math.pow(2, 3), math.EPSILON); +tests.assert_float_close(2.718281828459045, math.exp(1), math.EPSILON * 2); // Error 4.44089209850062616169e-16 diff --git a/applications/debug/unit_tests/resources/unit_tests/js/storage.js b/applications/debug/unit_tests/resources/unit_tests/js/storage.js new file mode 100644 index 0000000000..872b29cfbc --- /dev/null +++ b/applications/debug/unit_tests/resources/unit_tests/js/storage.js @@ -0,0 +1,136 @@ +let storage = require("storage"); +let tests = require("tests"); + +let baseDir = "/ext/.tmp/unit_tests"; + +tests.assert_eq(true, storage.rmrf(baseDir)); +tests.assert_eq(true, storage.makeDirectory(baseDir)); + +// write +let file = storage.openFile(baseDir + "/helloworld", "w", "create_always"); +tests.assert_eq(true, !!file); +tests.assert_eq(true, file.isOpen()); +tests.assert_eq(13, file.write("Hello, World!")); +tests.assert_eq(true, file.close()); +tests.assert_eq(false, file.isOpen()); + +// read +file = storage.openFile(baseDir + "/helloworld", "r", "open_existing"); +tests.assert_eq(true, !!file); +tests.assert_eq(true, file.isOpen()); +tests.assert_eq(13, file.size()); +tests.assert_eq("Hello, World!", file.read("ascii", 128)); +tests.assert_eq(true, file.close()); +tests.assert_eq(false, file.isOpen()); + +// seek +file = storage.openFile(baseDir + "/helloworld", "r", "open_existing"); +tests.assert_eq(true, !!file); +tests.assert_eq(true, file.isOpen()); +tests.assert_eq(13, file.size()); +tests.assert_eq("Hello, World!", file.read("ascii", 128)); +tests.assert_eq(true, file.seekAbsolute(1)); +tests.assert_eq(true, file.seekRelative(2)); +tests.assert_eq(3, file.tell()); +tests.assert_eq(false, file.eof()); +tests.assert_eq("lo, World!", file.read("ascii", 128)); +tests.assert_eq(true, file.eof()); +tests.assert_eq(true, file.close()); +tests.assert_eq(false, file.isOpen()); + +// byte-level copy +let src = storage.openFile(baseDir + "/helloworld", "r", "open_existing"); +let dst = storage.openFile(baseDir + "/helloworld2", "rw", "create_always"); +tests.assert_eq(true, !!src); +tests.assert_eq(true, src.isOpen()); +tests.assert_eq(true, !!dst); +tests.assert_eq(true, dst.isOpen()); +tests.assert_eq(true, src.copyTo(dst, 10)); +tests.assert_eq(true, dst.seekAbsolute(0)); +tests.assert_eq("Hello, Wor", dst.read("ascii", 128)); +tests.assert_eq(true, src.copyTo(dst, 3)); +tests.assert_eq(true, dst.seekAbsolute(0)); +tests.assert_eq("Hello, World!", dst.read("ascii", 128)); +tests.assert_eq(true, src.eof()); +tests.assert_eq(true, src.close()); +tests.assert_eq(false, src.isOpen()); +tests.assert_eq(true, dst.eof()); +tests.assert_eq(true, dst.close()); +tests.assert_eq(false, dst.isOpen()); + +// truncate +tests.assert_eq(true, storage.copy(baseDir + "/helloworld", baseDir + "/helloworld2")); +file = storage.openFile(baseDir + "/helloworld2", "w", "open_existing"); +tests.assert_eq(true, !!file); +tests.assert_eq(true, file.seekAbsolute(5)); +tests.assert_eq(true, file.truncate()); +tests.assert_eq(true, file.close()); +file = storage.openFile(baseDir + "/helloworld2", "r", "open_existing"); +tests.assert_eq(true, !!file); +tests.assert_eq("Hello", file.read("ascii", 128)); +tests.assert_eq(true, file.close()); + +// existence +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld")); +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld2")); +tests.assert_eq(false, storage.fileExists(baseDir + "/sus_amogus_123")); +tests.assert_eq(false, storage.directoryExists(baseDir + "/helloworld")); +tests.assert_eq(false, storage.fileExists(baseDir)); +tests.assert_eq(true, storage.directoryExists(baseDir)); +tests.assert_eq(true, storage.fileOrDirExists(baseDir)); +tests.assert_eq(true, storage.remove(baseDir + "/helloworld2")); +tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld2")); + +// stat +let stat = storage.stat(baseDir + "/helloworld"); +tests.assert_eq(true, !!stat); +tests.assert_eq(baseDir + "/helloworld", stat.path); +tests.assert_eq(false, stat.isDirectory); +tests.assert_eq(13, stat.size); + +// rename +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld")); +tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld123")); +tests.assert_eq(true, storage.rename(baseDir + "/helloworld", baseDir + "/helloworld123")); +tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld")); +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld123")); +tests.assert_eq(true, storage.rename(baseDir + "/helloworld123", baseDir + "/helloworld")); +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld")); +tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld123")); + +// copy +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld")); +tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld123")); +tests.assert_eq(true, storage.copy(baseDir + "/helloworld", baseDir + "/helloworld123")); +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld")); +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld123")); + +// next avail +tests.assert_eq("helloworld1", storage.nextAvailableFilename(baseDir, "helloworld", "", 20)); + +// fs info +let fsInfo = storage.fsInfo("/ext"); +tests.assert_eq(true, !!fsInfo); +tests.assert_eq(true, fsInfo.freeSpace < fsInfo.totalSpace); // idk \(-_-)/ +fsInfo = storage.fsInfo("/int"); +tests.assert_eq(true, !!fsInfo); +tests.assert_eq(true, fsInfo.freeSpace < fsInfo.totalSpace); + +// path operations +tests.assert_eq(true, storage.arePathsEqual("/ext/test", "/ext/Test")); +tests.assert_eq(false, storage.arePathsEqual("/ext/test", "/ext/Testttt")); +tests.assert_eq(true, storage.isSubpathOf("/ext/test", "/ext/test/sub")); +tests.assert_eq(false, storage.isSubpathOf("/ext/test/sub", "/ext/test")); + +// dir +let entries = storage.readDirectory(baseDir); +tests.assert_eq(true, !!entries); +// FIXME: (-nofl) this test suite assumes that files are listed by +// `readDirectory` in the exact order that they were created, which is not +// something that is actually guaranteed. +// Possible solution: sort and compare the array. +tests.assert_eq("helloworld", entries[0].path); +tests.assert_eq("helloworld123", entries[1].path); + +tests.assert_eq(true, storage.rmrf(baseDir)); +tests.assert_eq(true, storage.makeDirectory(baseDir)); diff --git a/applications/debug/unit_tests/tests/furi/furi_errno_test.c b/applications/debug/unit_tests/tests/furi/furi_errno_test.c new file mode 100644 index 0000000000..b42e7c0828 --- /dev/null +++ b/applications/debug/unit_tests/tests/furi/furi_errno_test.c @@ -0,0 +1,51 @@ +#include +#include +#include "../test.h" // IWYU pragma: keep + +#define TAG "ErrnoTest" +#define THREAD_CNT 16 +#define ITER_CNT 1000 + +static int32_t errno_fuzzer(void* context) { + int start_value = (int)context; + int32_t fails = 0; + + for(int i = start_value; i < start_value + ITER_CNT; i++) { + errno = i; + furi_thread_yield(); + if(errno != i) fails++; + } + + for(int i = 0; i < ITER_CNT; i++) { + errno = 0; + furi_thread_yield(); + UNUSED(strtol("123456", NULL, 10)); // -V530 + furi_thread_yield(); + if(errno != 0) fails++; + + errno = 0; + furi_thread_yield(); + UNUSED(strtol("123456123456123456123456123456123456123456123456", NULL, 10)); // -V530 + furi_thread_yield(); + if(errno != ERANGE) fails++; + } + + return fails; +} + +void test_errno_saving(void) { + FuriThread* threads[THREAD_CNT]; + + for(int i = 0; i < THREAD_CNT; i++) { + int start_value = i * ITER_CNT; + threads[i] = furi_thread_alloc_ex("ErrnoFuzzer", 1024, errno_fuzzer, (void*)start_value); + furi_thread_set_priority(threads[i], FuriThreadPriorityNormal); + furi_thread_start(threads[i]); + } + + for(int i = 0; i < THREAD_CNT; i++) { + furi_thread_join(threads[i]); + mu_assert_int_eq(0, furi_thread_get_return_code(threads[i])); + furi_thread_free(threads[i]); + } +} 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 291181c77f..0000000000 --- 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 0000000000..73f38ab77f --- /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 0000000000..d9ad030395 --- /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 be579d2b8c..193a8124d9 100644 --- a/applications/debug/unit_tests/tests/furi/furi_test.c +++ b/applications/debug/unit_tests/tests/furi/furi_test.c @@ -8,6 +8,8 @@ void test_furi_concurrent_access(void); 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; @@ -42,6 +44,14 @@ MU_TEST(mu_test_furi_event_loop) { test_furi_event_loop(); } +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); @@ -51,6 +61,8 @@ MU_TEST_SUITE(test_suite) { MU_RUN_TEST(mu_test_furi_pubsub); 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/debug/unit_tests/tests/js/js_test.c b/applications/debug/unit_tests/tests/js/js_test.c new file mode 100644 index 0000000000..af590e8995 --- /dev/null +++ b/applications/debug/unit_tests/tests/js/js_test.c @@ -0,0 +1,88 @@ +#include "../test.h" // IWYU pragma: keep + +#include +#include +#include + +#include +#include + +#include + +#define JS_SCRIPT_PATH(name) EXT_PATH("unit_tests/js/" name ".js") + +typedef enum { + JsTestsFinished = 1, + JsTestsError = 2, +} JsTestFlag; + +typedef struct { + FuriEventFlag* event_flags; + FuriString* error_string; +} JsTestCallbackContext; + +static void js_test_callback(JsThreadEvent event, const char* msg, void* param) { + JsTestCallbackContext* context = param; + if(event == JsThreadEventPrint) { + FURI_LOG_I("js_test", "%s", msg); + } else if(event == JsThreadEventError || event == JsThreadEventErrorTrace) { + context->error_string = furi_string_alloc_set_str(msg); + furi_event_flag_set(context->event_flags, JsTestsFinished | JsTestsError); + } else if(event == JsThreadEventDone) { + furi_event_flag_set(context->event_flags, JsTestsFinished); + } +} + +static void js_test_run(const char* script_path) { + JsTestCallbackContext* context = malloc(sizeof(JsTestCallbackContext)); + context->event_flags = furi_event_flag_alloc(); + + JsThread* thread = js_thread_run(script_path, js_test_callback, context); + uint32_t flags = furi_event_flag_wait( + context->event_flags, JsTestsFinished, FuriFlagWaitAny, FuriWaitForever); + if(flags & FuriFlagError) { + // getting the flags themselves should not fail + furi_crash(); + } + + FuriString* error_string = context->error_string; + + js_thread_stop(thread); + furi_event_flag_free(context->event_flags); + free(context); + + if(flags & JsTestsError) { + // memory leak: not freeing the FuriString if the tests fail, + // because mu_fail executes a return + // + // who cares tho? + mu_fail(furi_string_get_cstr(error_string)); + } +} + +MU_TEST(js_test_basic) { + js_test_run(JS_SCRIPT_PATH("basic")); +} +MU_TEST(js_test_math) { + js_test_run(JS_SCRIPT_PATH("math")); +} +MU_TEST(js_test_event_loop) { + js_test_run(JS_SCRIPT_PATH("event_loop")); +} +MU_TEST(js_test_storage) { + js_test_run(JS_SCRIPT_PATH("storage")); +} + +MU_TEST_SUITE(test_js) { + MU_RUN_TEST(js_test_basic); + MU_RUN_TEST(js_test_math); + MU_RUN_TEST(js_test_event_loop); + MU_RUN_TEST(js_test_storage); +} + +int run_minunit_test_js(void) { + MU_RUN_SUITE(test_js); + return MU_EXIT_CODE; +} + +TEST_API_DEFINE(run_minunit_test_js) diff --git a/applications/debug/unit_tests/tests/minunit.h b/applications/debug/unit_tests/tests/minunit.h index 9310cfc9c9..9ca3bb403d 100644 --- a/applications/debug/unit_tests/tests/minunit.h +++ b/applications/debug/unit_tests/tests/minunit.h @@ -31,7 +31,7 @@ extern "C" { #include #if defined(_MSC_VER) && _MSC_VER < 1900 #define snprintf _snprintf -#define __func__ __FUNCTION__ +#define __func__ __FUNCTION__ //-V1059 #endif #elif defined(__unix__) || defined(__unix) || defined(unix) || \ @@ -56,7 +56,7 @@ extern "C" { #endif #if __GNUC__ >= 5 && !defined(__STDC_VERSION__) -#define __func__ __extension__ __FUNCTION__ +#define __func__ __extension__ __FUNCTION__ //-V1059 #endif #else @@ -102,6 +102,7 @@ void minunit_printf_warning(const char* format, ...); MU__SAFE_BLOCK(minunit_setup = setup_fun; minunit_teardown = teardown_fun;) /* Test runner */ +//-V:MU_RUN_TEST:550 #define MU_RUN_TEST(test) \ MU__SAFE_BLOCK( \ if(minunit_real_timer == 0 && minunit_proc_timer == 0) { \ diff --git a/applications/debug/unit_tests/tests/nfc/nfc_test.c b/applications/debug/unit_tests/tests/nfc/nfc_test.c index 0898ac8eda..4ba934b6d5 100644 --- a/applications/debug/unit_tests/tests/nfc/nfc_test.c +++ b/applications/debug/unit_tests/tests/nfc/nfc_test.c @@ -496,7 +496,7 @@ NfcCommand mf_classic_poller_send_frame_callback(NfcGenericEventEx event, void* MfClassicKey key = { .data = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, }; - error = mf_classic_poller_auth(instance, 0, &key, MfClassicKeyTypeA, NULL); + error = mf_classic_poller_auth(instance, 0, &key, MfClassicKeyTypeA, NULL, false); frame_test->state = (error == MfClassicErrorNone) ? NfcTestMfClassicSendFrameTestStateReadBlock : NfcTestMfClassicSendFrameTestStateFail; diff --git a/applications/debug/unit_tests/tests/storage/storage_test.c b/applications/debug/unit_tests/tests/storage/storage_test.c index f317fbf680..75c52ef9a7 100644 --- a/applications/debug/unit_tests/tests/storage/storage_test.c +++ b/applications/debug/unit_tests/tests/storage/storage_test.c @@ -6,9 +6,10 @@ // This is a hack to access internal storage functions and definitions #include -#define UNIT_TESTS_PATH(path) EXT_PATH("unit_tests/" path) +#define UNIT_TESTS_RESOURCES_PATH(path) EXT_PATH("unit_tests/" path) +#define UNIT_TESTS_PATH(path) EXT_PATH(".tmp/unit_tests/" path) -#define STORAGE_LOCKED_FILE EXT_PATH("locked_file.test") +#define STORAGE_LOCKED_FILE UNIT_TESTS_PATH("locked_file.test") #define STORAGE_LOCKED_DIR STORAGE_INT_PATH_PREFIX #define STORAGE_TEST_DIR UNIT_TESTS_PATH("test_dir") @@ -369,33 +370,78 @@ MU_TEST(storage_file_rename) { Storage* storage = furi_record_open(RECORD_STORAGE); File* file = storage_file_alloc(storage); - mu_check(write_file_13DA(storage, EXT_PATH("file.old"))); - mu_check(check_file_13DA(storage, EXT_PATH("file.old"))); + mu_check(write_file_13DA(storage, UNIT_TESTS_PATH("file.old"))); + mu_check(check_file_13DA(storage, UNIT_TESTS_PATH("file.old"))); mu_assert_int_eq( - FSE_OK, storage_common_rename(storage, EXT_PATH("file.old"), EXT_PATH("file.new"))); - mu_assert_int_eq(FSE_NOT_EXIST, storage_common_stat(storage, EXT_PATH("file.old"), NULL)); - mu_assert_int_eq(FSE_OK, storage_common_stat(storage, EXT_PATH("file.new"), NULL)); - mu_check(check_file_13DA(storage, EXT_PATH("file.new"))); - mu_assert_int_eq(FSE_OK, storage_common_remove(storage, EXT_PATH("file.new"))); + FSE_OK, + storage_common_rename(storage, UNIT_TESTS_PATH("file.old"), UNIT_TESTS_PATH("file.new"))); + mu_assert_int_eq( + FSE_NOT_EXIST, storage_common_stat(storage, UNIT_TESTS_PATH("file.old"), NULL)); + mu_assert_int_eq(FSE_OK, storage_common_stat(storage, UNIT_TESTS_PATH("file.new"), NULL)); + mu_check(check_file_13DA(storage, UNIT_TESTS_PATH("file.new"))); + mu_assert_int_eq(FSE_OK, storage_common_remove(storage, UNIT_TESTS_PATH("file.new"))); storage_file_free(file); furi_record_close(RECORD_STORAGE); } +static const char* dir_rename_tests[][2] = { + {UNIT_TESTS_PATH("dir.old"), UNIT_TESTS_PATH("dir.new")}, + {UNIT_TESTS_PATH("test_dir"), UNIT_TESTS_PATH("test_dir-new")}, + {UNIT_TESTS_PATH("test"), UNIT_TESTS_PATH("test-test")}, +}; + MU_TEST(storage_dir_rename) { Storage* storage = furi_record_open(RECORD_STORAGE); - storage_dir_create(storage, EXT_PATH("dir.old")); + for(size_t i = 0; i < COUNT_OF(dir_rename_tests); i++) { + const char* old_path = dir_rename_tests[i][0]; + const char* new_path = dir_rename_tests[i][1]; + + storage_dir_create(storage, old_path); + mu_check(storage_dir_rename_check(storage, old_path)); + + mu_assert_int_eq(FSE_OK, storage_common_rename(storage, old_path, new_path)); + mu_assert_int_eq(FSE_NOT_EXIST, storage_common_stat(storage, old_path, NULL)); + mu_check(storage_dir_rename_check(storage, new_path)); + + storage_dir_remove(storage, new_path); + mu_assert_int_eq(FSE_NOT_EXIST, storage_common_stat(storage, new_path, NULL)); + } + + furi_record_close(RECORD_STORAGE); +} - mu_check(storage_dir_rename_check(storage, EXT_PATH("dir.old"))); +MU_TEST(storage_equiv_and_subdir) { + Storage* storage = furi_record_open(RECORD_STORAGE); mu_assert_int_eq( - FSE_OK, storage_common_rename(storage, EXT_PATH("dir.old"), EXT_PATH("dir.new"))); - mu_assert_int_eq(FSE_NOT_EXIST, storage_common_stat(storage, EXT_PATH("dir.old"), NULL)); - mu_check(storage_dir_rename_check(storage, EXT_PATH("dir.new"))); + true, + storage_common_equivalent_path(storage, UNIT_TESTS_PATH("blah"), UNIT_TESTS_PATH("blah"))); + mu_assert_int_eq( + true, + storage_common_equivalent_path( + storage, UNIT_TESTS_PATH("blah/"), UNIT_TESTS_PATH("blah/"))); + mu_assert_int_eq( + false, + storage_common_equivalent_path( + storage, UNIT_TESTS_PATH("blah"), UNIT_TESTS_PATH("blah-blah"))); + mu_assert_int_eq( + false, + storage_common_equivalent_path( + storage, UNIT_TESTS_PATH("blah/"), UNIT_TESTS_PATH("blah-blah/"))); - storage_dir_remove(storage, EXT_PATH("dir.new")); - mu_assert_int_eq(FSE_NOT_EXIST, storage_common_stat(storage, EXT_PATH("dir.new"), NULL)); + mu_assert_int_eq( + true, storage_common_is_subdir(storage, UNIT_TESTS_PATH("blah"), UNIT_TESTS_PATH("blah"))); + mu_assert_int_eq( + true, + storage_common_is_subdir(storage, UNIT_TESTS_PATH("blah"), UNIT_TESTS_PATH("blah/blah"))); + mu_assert_int_eq( + false, + storage_common_is_subdir(storage, UNIT_TESTS_PATH("blah/blah"), UNIT_TESTS_PATH("blah"))); + mu_assert_int_eq( + false, + storage_common_is_subdir(storage, UNIT_TESTS_PATH("blah"), UNIT_TESTS_PATH("blah-blah"))); furi_record_close(RECORD_STORAGE); } @@ -403,10 +449,13 @@ MU_TEST(storage_dir_rename) { MU_TEST_SUITE(storage_rename) { MU_RUN_TEST(storage_file_rename); MU_RUN_TEST(storage_dir_rename); + MU_RUN_TEST(storage_equiv_and_subdir); Storage* storage = furi_record_open(RECORD_STORAGE); - storage_dir_remove(storage, EXT_PATH("dir.old")); - storage_dir_remove(storage, EXT_PATH("dir.new")); + for(size_t i = 0; i < COUNT_OF(dir_rename_tests); i++) { + storage_dir_remove(storage, dir_rename_tests[i][0]); + storage_dir_remove(storage, dir_rename_tests[i][1]); + } furi_record_close(RECORD_STORAGE); } @@ -653,7 +702,7 @@ MU_TEST(test_md5_calc) { Storage* storage = furi_record_open(RECORD_STORAGE); File* file = storage_file_alloc(storage); - const char* path = UNIT_TESTS_PATH("storage/md5.txt"); + const char* path = UNIT_TESTS_RESOURCES_PATH("storage/md5.txt"); const char* md5_cstr = "2a456fa43e75088fdde41c93159d62a2"; const uint8_t md5[MD5_HASH_SIZE] = { 0x2a, diff --git a/applications/debug/unit_tests/unit_test_api_table_i.h b/applications/debug/unit_tests/unit_test_api_table_i.h index 50524e5b7d..10b0890225 100644 --- a/applications/debug/unit_tests/unit_test_api_table_i.h +++ b/applications/debug/unit_tests/unit_test_api_table_i.h @@ -7,7 +7,7 @@ #include #include -#include +#include static constexpr auto unit_tests_api_table = sort(create_array_t( API_METHOD(resource_manifest_reader_alloc, ResourceManifestReader*, (Storage*)), @@ -33,13 +33,9 @@ static constexpr auto unit_tests_api_table = sort(create_array_t( xQueueGenericSend, BaseType_t, (QueueHandle_t, const void* const, TickType_t, const BaseType_t)), - API_METHOD(furi_event_loop_alloc, FuriEventLoop*, (void)), - API_METHOD(furi_event_loop_free, void, (FuriEventLoop*)), API_METHOD( - furi_event_loop_subscribe_message_queue, - void, - (FuriEventLoop*, FuriMessageQueue*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*)), - API_METHOD(furi_event_loop_unsubscribe, void, (FuriEventLoop*, FuriEventLoopObject*)), - API_METHOD(furi_event_loop_run, void, (FuriEventLoop*)), - API_METHOD(furi_event_loop_stop, void, (FuriEventLoop*)), + js_thread_run, + JsThread*, + (const char* script_path, JsThreadCallback callback, void* context)), + API_METHOD(js_thread_stop, void, (JsThread * worker)), API_VARIABLE(PB_Main_msg, PB_Main_msg_t))); diff --git a/applications/drivers/subghz/cc1101_ext/cc1101_ext.c b/applications/drivers/subghz/cc1101_ext/cc1101_ext.c index ac72ab5c1a..27af3e5572 100644 --- a/applications/drivers/subghz/cc1101_ext/cc1101_ext.c +++ b/applications/drivers/subghz/cc1101_ext/cc1101_ext.c @@ -92,7 +92,7 @@ typedef struct { const GpioPin* g0_pin; SubGhzDeviceCC1101ExtAsyncTx async_tx; SubGhzDeviceCC1101ExtAsyncRx async_rx; - bool power_amp; + bool amp_and_leds; bool extended_range; } SubGhzDeviceCC1101Ext; @@ -219,11 +219,11 @@ bool subghz_device_cc1101_ext_alloc(SubGhzDeviceConf* conf) { subghz_device_cc1101_ext->async_mirror_pin = NULL; subghz_device_cc1101_ext->spi_bus_handle = &furi_hal_spi_bus_handle_external; subghz_device_cc1101_ext->g0_pin = SUBGHZ_DEVICE_CC1101_EXT_TX_GPIO; - subghz_device_cc1101_ext->power_amp = false; + subghz_device_cc1101_ext->amp_and_leds = false; subghz_device_cc1101_ext->extended_range = false; if(conf) { if(conf->ver == SUBGHZ_DEVICE_CC1101_CONFIG_VER) { - subghz_device_cc1101_ext->power_amp = conf->power_amp; + subghz_device_cc1101_ext->amp_and_leds = conf->amp_and_leds; subghz_device_cc1101_ext->extended_range = conf->extended_range; } else { FURI_LOG_E(TAG, "Config version mismatch"); @@ -233,7 +233,7 @@ bool subghz_device_cc1101_ext_alloc(SubGhzDeviceConf* conf) { subghz_device_cc1101_ext->async_rx.capture_delta_duration = 0; furi_hal_spi_bus_handle_init(subghz_device_cc1101_ext->spi_bus_handle); - if(subghz_device_cc1101_ext->power_amp) { + if(subghz_device_cc1101_ext->amp_and_leds) { furi_hal_gpio_init_simple(SUBGHZ_DEVICE_CC1101_EXT_E07_AMP_GPIO, GpioModeOutputPushPull); } @@ -244,7 +244,7 @@ void subghz_device_cc1101_ext_free(void) { furi_assert(subghz_device_cc1101_ext != NULL); furi_hal_spi_bus_handle_deinit(subghz_device_cc1101_ext->spi_bus_handle); - if(subghz_device_cc1101_ext->power_amp) { + if(subghz_device_cc1101_ext->amp_and_leds) { furi_hal_gpio_init_simple(SUBGHZ_DEVICE_CC1101_EXT_E07_AMP_GPIO, GpioModeAnalog); } @@ -421,6 +421,7 @@ void subghz_device_cc1101_ext_reset(void) { // Reset GDO2 (!TX/RX) to floating state cc1101_write_reg( subghz_device_cc1101_ext->spi_bus_handle, CC1101_IOCFG2, CC1101IocfgHighImpedance); + furi_hal_spi_release(subghz_device_cc1101_ext->spi_bus_handle); } @@ -430,13 +431,13 @@ void subghz_device_cc1101_ext_idle(void) { //waiting for the chip to switch to IDLE mode furi_check(cc1101_wait_status_state( subghz_device_cc1101_ext->spi_bus_handle, CC1101StateIDLE, 10000)); + + furi_hal_gpio_write(SUBGHZ_DEVICE_CC1101_EXT_E07_AMP_GPIO, 0); // Reset GDO2 (!TX/RX) to floating state cc1101_write_reg( subghz_device_cc1101_ext->spi_bus_handle, CC1101_IOCFG2, CC1101IocfgHighImpedance); + furi_hal_spi_release(subghz_device_cc1101_ext->spi_bus_handle); - if(subghz_device_cc1101_ext->power_amp) { - furi_hal_gpio_write(SUBGHZ_DEVICE_CC1101_EXT_E07_AMP_GPIO, 0); - } } void subghz_device_cc1101_ext_rx(void) { @@ -445,14 +446,17 @@ void subghz_device_cc1101_ext_rx(void) { //waiting for the chip to switch to Rx mode furi_check( cc1101_wait_status_state(subghz_device_cc1101_ext->spi_bus_handle, CC1101StateRX, 10000)); - // Go GDO2 (!TX/RX) to high (RX state) - cc1101_write_reg( - subghz_device_cc1101_ext->spi_bus_handle, CC1101_IOCFG2, CC1101IocfgHW | CC1101_IOCFG_INV); - furi_hal_spi_release(subghz_device_cc1101_ext->spi_bus_handle); - if(subghz_device_cc1101_ext->power_amp) { + if(subghz_device_cc1101_ext->amp_and_leds) { furi_hal_gpio_write(SUBGHZ_DEVICE_CC1101_EXT_E07_AMP_GPIO, 0); + // Go GDO2 (!TX/RX) to high (RX state) + cc1101_write_reg( + subghz_device_cc1101_ext->spi_bus_handle, + CC1101_IOCFG2, + CC1101IocfgHW | CC1101_IOCFG_INV); } + + furi_hal_spi_release(subghz_device_cc1101_ext->spi_bus_handle); } bool subghz_device_cc1101_ext_tx(void) { @@ -462,12 +466,14 @@ bool subghz_device_cc1101_ext_tx(void) { //waiting for the chip to switch to Tx mode furi_check( cc1101_wait_status_state(subghz_device_cc1101_ext->spi_bus_handle, CC1101StateTX, 10000)); - // Go GDO2 (!TX/RX) to low (TX state) - cc1101_write_reg(subghz_device_cc1101_ext->spi_bus_handle, CC1101_IOCFG2, CC1101IocfgHW); - furi_hal_spi_release(subghz_device_cc1101_ext->spi_bus_handle); - if(subghz_device_cc1101_ext->power_amp) { + + if(subghz_device_cc1101_ext->amp_and_leds) { furi_hal_gpio_write(SUBGHZ_DEVICE_CC1101_EXT_E07_AMP_GPIO, 1); + // Go GDO2 (!TX/RX) to low (TX state) + cc1101_write_reg(subghz_device_cc1101_ext->spi_bus_handle, CC1101_IOCFG2, CC1101IocfgHW); } + + furi_hal_spi_release(subghz_device_cc1101_ext->spi_bus_handle); return true; } diff --git a/applications/examples/example_event_loop/application.fam b/applications/examples/example_event_loop/application.fam index a37ffb1a04..15a7c8837f 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 0000000000..5d0acf7f10 --- /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 ebfb009118..ae748da55f 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 d043f3f899..20bf7af4b0 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 65dbd83cf5..6f72809739 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/examples/example_number_input/ReadMe.md b/applications/examples/example_number_input/ReadMe.md index 9d5a0a9e5e..8a221ba087 100644 --- a/applications/examples/example_number_input/ReadMe.md +++ b/applications/examples/example_number_input/ReadMe.md @@ -1,7 +1,13 @@ -# Number Input +# Number Input {#example_number_input} -Simple keyboard that limits user inputs to a full number (integer). Useful to enforce correct entries without the need of intense validations after a user input. +Simple keyboard that limits user inputs to a full number (integer). Useful to enforce correct entries without the need for intense validations after a user input. -Definition of min/max values is required. Numbers are of type int32_t. If negative numbers are allowed withing min - max, an additional button is displayed to switch the sign between + and -. +## Source code -It is also possible to define a header text, shown in this example app with the 3 different input options. \ No newline at end of file +Source code for this example can be found [here](https://github.com/flipperdevices/flipperzero-firmware/tree/dev/applications/examples/example_number_input). + +## General principle + +Definition of min/max values is required. Numbers are of type int32_t. If negative numbers are allowed within min - max, an additional button is displayed to switch the sign between + and -. + +It is also possible to define a header text, as shown in this example app with the 3 different input options. \ No newline at end of file diff --git a/applications/main/bad_usb/bad_usb_app.c b/applications/main/bad_usb/bad_usb_app.c index 2d2d4be86c..eda702cf4d 100644 --- a/applications/main/bad_usb/bad_usb_app.c +++ b/applications/main/bad_usb/bad_usb_app.c @@ -94,6 +94,11 @@ static void bad_usb_save_settings(BadUsbApp* app) { furi_record_close(RECORD_STORAGE); } +void bad_usb_set_interface(BadUsbApp* app, BadUsbHidInterface interface) { + app->interface = interface; + bad_usb_view_set_interface(app->bad_usb_view, interface); +} + BadUsbApp* bad_usb_app_alloc(char* arg) { BadUsbApp* app = malloc(sizeof(BadUsbApp)); @@ -125,7 +130,11 @@ BadUsbApp* bad_usb_app_alloc(char* arg) { // Custom Widget app->widget = widget_alloc(); view_dispatcher_add_view( - app->view_dispatcher, BadUsbAppViewError, widget_get_view(app->widget)); + app->view_dispatcher, BadUsbAppViewWidget, widget_get_view(app->widget)); + + // Popup + app->popup = popup_alloc(); + view_dispatcher_add_view(app->view_dispatcher, BadUsbAppViewPopup, popup_get_view(app->popup)); app->var_item_list = variable_item_list_alloc(); view_dispatcher_add_view( @@ -171,9 +180,13 @@ void bad_usb_app_free(BadUsbApp* app) { bad_usb_view_free(app->bad_usb_view); // Custom Widget - view_dispatcher_remove_view(app->view_dispatcher, BadUsbAppViewError); + view_dispatcher_remove_view(app->view_dispatcher, BadUsbAppViewWidget); widget_free(app->widget); + // Popup + view_dispatcher_remove_view(app->view_dispatcher, BadUsbAppViewPopup); + popup_free(app->popup); + // Config menu view_dispatcher_remove_view(app->view_dispatcher, BadUsbAppViewConfig); variable_item_list_free(app->var_item_list); diff --git a/applications/main/bad_usb/bad_usb_app_i.h b/applications/main/bad_usb/bad_usb_app_i.h index a4dd57d8b9..b34bd5de69 100644 --- a/applications/main/bad_usb/bad_usb_app_i.h +++ b/applications/main/bad_usb/bad_usb_app_i.h @@ -13,6 +13,7 @@ #include #include #include +#include #include "views/bad_usb_view.h" #include @@ -33,6 +34,7 @@ struct BadUsbApp { NotificationApp* notifications; DialogsApp* dialogs; Widget* widget; + Popup* popup; VariableItemList* var_item_list; BadUsbAppError error; @@ -46,7 +48,10 @@ struct BadUsbApp { }; typedef enum { - BadUsbAppViewError, + BadUsbAppViewWidget, + BadUsbAppViewPopup, BadUsbAppViewWork, BadUsbAppViewConfig, } BadUsbAppView; + +void bad_usb_set_interface(BadUsbApp* app, BadUsbHidInterface interface); diff --git a/applications/main/bad_usb/resources/badusb/assets/layouts/es-LA.kl b/applications/main/bad_usb/resources/badusb/assets/layouts/es-LA.kl new file mode 100644 index 0000000000..2a6c78adcb Binary files /dev/null and b/applications/main/bad_usb/resources/badusb/assets/layouts/es-LA.kl differ diff --git a/applications/main/bad_usb/resources/badusb/demo_chromeos.txt b/applications/main/bad_usb/resources/badusb/demo_chromeos.txt index c5f675fb33..7f42574ce1 100644 --- a/applications/main/bad_usb/resources/badusb/demo_chromeos.txt +++ b/applications/main/bad_usb/resources/badusb/demo_chromeos.txt @@ -1,12 +1,17 @@ -REM This is BadUSB demo script for ChromeOS by kowalski7cc +REM This is BadUSB demo script for Chrome and ChromeOS by kowalski7cc +REM Exit from Overview +ESC REM Open a new tab CTRL t REM wait for some slower chromebooks DELAY 1000 +REM Make sure we have omnibox focus +CTRL l +DELAY 200 REM Open an empty editable page DEFAULT_DELAY 50 -STRING data:text/html,