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| |https://pay.cloudtips.ru/p/7b3e9d65|
|YooMoney|only RU payments accepted| |https://yoomoney.ru/fundraise/XA49mgQLPA0.221209|
|USDT|(TRC20)| |`TSXcitMSnWXUFqiUfEXrTVpVewXy2cYhrs`|
-|ETH|(BSC/ERC20-Tokens)| |`darkflippers.eth` (or `0xFebF1bBc8229418FF2408C07AF6Afa49152fEc6a`)|
+|ETH|(BSC/ERC20-Tokens)| |`0xFebF1bBc8229418FF2408C07AF6Afa49152fEc6a`|
|BTC|| |`bc1q0np836jk9jwr4dd7p6qv66d04vamtqkxrecck9`|
|SOL|(Solana/Tokens)| |`DSgwouAEgu8iP5yr7EHHDqMNYWZxAqXWsTEeqCAXGLj8`|
|DOGE|| |`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| |https://pay.cloudtips.ru/p/7b3e9d65|
|YooMoney|only RU payments accepted| |https://yoomoney.ru/fundraise/XA49mgQLPA0.221209|
|USDT|(TRC20)| |`TSXcitMSnWXUFqiUfEXrTVpVewXy2cYhrs`|
-|ETH|(BSC/ERC20-Tokens)| |`darkflippers.eth` (or `0xFebF1bBc8229418FF2408C07AF6Afa49152fEc6a`)|
+|ETH|(BSC/ERC20-Tokens)| |`0xFebF1bBc8229418FF2408C07AF6Afa49152fEc6a`|
|BTC|| |`bc1q0np836jk9jwr4dd7p6qv66d04vamtqkxrecck9`|
|SOL|(Solana/Tokens)| |`DSgwouAEgu8iP5yr7EHHDqMNYWZxAqXWsTEeqCAXGLj8`|
|DOGE|| |`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,