mirror of
https://github.com/Next-Flip/Momentum-Firmware.git
synced 2026-05-05 05:09:09 -07:00
Merge remote-tracking branch 'pr3822/nestednonces' into dev
This commit is contained in:
@@ -221,6 +221,14 @@ App(
|
||||
requires=["unit_tests"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="test_js",
|
||||
sources=["tests/common/*.c", "tests/js/*.c"],
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="get_api",
|
||||
requires=["unit_tests", "js_app"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="test_strint",
|
||||
sources=["tests/common/*.c", "tests/strint/*.c"],
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
let tests = require("tests");
|
||||
|
||||
tests.assert_eq(1337, 1337);
|
||||
tests.assert_eq("hello", "hello");
|
||||
@@ -0,0 +1,30 @@
|
||||
let tests = require("tests");
|
||||
let event_loop = require("event_loop");
|
||||
|
||||
let ext = {
|
||||
i: 0,
|
||||
received: false,
|
||||
};
|
||||
|
||||
let queue = event_loop.queue(16);
|
||||
|
||||
event_loop.subscribe(queue.input, function (_, item, tests, ext) {
|
||||
tests.assert_eq(123, item);
|
||||
ext.received = true;
|
||||
}, tests, ext);
|
||||
|
||||
event_loop.subscribe(event_loop.timer("periodic", 1), function (_, _item, queue, counter, ext) {
|
||||
ext.i++;
|
||||
queue.send(123);
|
||||
if (counter === 10)
|
||||
event_loop.stop();
|
||||
return [queue, counter + 1, ext];
|
||||
}, queue, 1, ext);
|
||||
|
||||
event_loop.subscribe(event_loop.timer("oneshot", 1000), function (_, _item, tests) {
|
||||
tests.fail("event loop was not stopped");
|
||||
}, tests);
|
||||
|
||||
event_loop.run();
|
||||
tests.assert_eq(10, ext.i);
|
||||
tests.assert_eq(true, ext.received);
|
||||
@@ -0,0 +1,34 @@
|
||||
let tests = require("tests");
|
||||
let math = require("math");
|
||||
|
||||
// math.EPSILON on Flipper Zero is 2.22044604925031308085e-16
|
||||
|
||||
// basics
|
||||
tests.assert_float_close(5, math.abs(-5), math.EPSILON);
|
||||
tests.assert_float_close(0.5, math.abs(-0.5), math.EPSILON);
|
||||
tests.assert_float_close(5, math.abs(5), math.EPSILON);
|
||||
tests.assert_float_close(0.5, math.abs(0.5), math.EPSILON);
|
||||
tests.assert_float_close(3, math.cbrt(27), math.EPSILON);
|
||||
tests.assert_float_close(6, math.ceil(5.3), math.EPSILON);
|
||||
tests.assert_float_close(31, math.clz32(1), math.EPSILON);
|
||||
tests.assert_float_close(5, math.floor(5.7), math.EPSILON);
|
||||
tests.assert_float_close(5, math.max(3, 5), math.EPSILON);
|
||||
tests.assert_float_close(3, math.min(3, 5), math.EPSILON);
|
||||
tests.assert_float_close(-1, math.sign(-5), math.EPSILON);
|
||||
tests.assert_float_close(5, math.trunc(5.7), math.EPSILON);
|
||||
|
||||
// trig
|
||||
tests.assert_float_close(1.0471975511965976, math.acos(0.5), math.EPSILON);
|
||||
tests.assert_float_close(1.3169578969248166, math.acosh(2), math.EPSILON);
|
||||
tests.assert_float_close(0.5235987755982988, math.asin(0.5), math.EPSILON);
|
||||
tests.assert_float_close(1.4436354751788103, math.asinh(2), math.EPSILON);
|
||||
tests.assert_float_close(0.7853981633974483, math.atan(1), math.EPSILON);
|
||||
tests.assert_float_close(0.7853981633974483, math.atan2(1, 1), math.EPSILON);
|
||||
tests.assert_float_close(0.5493061443340549, math.atanh(0.5), math.EPSILON);
|
||||
tests.assert_float_close(-1, math.cos(math.PI), math.EPSILON * 18); // Error 3.77475828372553223744e-15
|
||||
tests.assert_float_close(1, math.sin(math.PI / 2), math.EPSILON * 4.5); // Error 9.99200722162640886381e-16
|
||||
|
||||
// powers
|
||||
tests.assert_float_close(5, math.sqrt(25), math.EPSILON);
|
||||
tests.assert_float_close(8, math.pow(2, 3), math.EPSILON);
|
||||
tests.assert_float_close(2.718281828459045, math.exp(1), math.EPSILON * 2); // Error 4.44089209850062616169e-16
|
||||
136
applications/debug/unit_tests/resources/unit_tests/js/storage.js
Normal file
136
applications/debug/unit_tests/resources/unit_tests/js/storage.js
Normal file
@@ -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));
|
||||
88
applications/debug/unit_tests/tests/js/js_test.c
Normal file
88
applications/debug/unit_tests/tests/js/js_test.c
Normal file
@@ -0,0 +1,88 @@
|
||||
#include "../test.h" // IWYU pragma: keep
|
||||
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
#include <furi_hal_random.h>
|
||||
|
||||
#include <storage/storage.h>
|
||||
#include <applications/system/js_app/js_thread.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define JS_SCRIPT_PATH(name) EXT_PATH("unit_tests/js/" name ".js")
|
||||
|
||||
typedef enum {
|
||||
JsTestsFinished = 1,
|
||||
JsTestsError = 2,
|
||||
} JsTestFlag;
|
||||
|
||||
typedef struct {
|
||||
FuriEventFlag* event_flags;
|
||||
FuriString* error_string;
|
||||
} JsTestCallbackContext;
|
||||
|
||||
static void js_test_callback(JsThreadEvent event, const char* msg, void* param) {
|
||||
JsTestCallbackContext* context = param;
|
||||
if(event == JsThreadEventPrint) {
|
||||
FURI_LOG_I("js_test", "%s", msg);
|
||||
} else if(event == JsThreadEventError || event == JsThreadEventErrorTrace) {
|
||||
context->error_string = furi_string_alloc_set_str(msg);
|
||||
furi_event_flag_set(context->event_flags, JsTestsFinished | JsTestsError);
|
||||
} else if(event == JsThreadEventDone) {
|
||||
furi_event_flag_set(context->event_flags, JsTestsFinished);
|
||||
}
|
||||
}
|
||||
|
||||
static void js_test_run(const char* script_path) {
|
||||
JsTestCallbackContext* context = malloc(sizeof(JsTestCallbackContext));
|
||||
context->event_flags = furi_event_flag_alloc();
|
||||
|
||||
JsThread* thread = js_thread_run(script_path, js_test_callback, context);
|
||||
uint32_t flags = furi_event_flag_wait(
|
||||
context->event_flags, JsTestsFinished, FuriFlagWaitAny, FuriWaitForever);
|
||||
if(flags & FuriFlagError) {
|
||||
// getting the flags themselves should not fail
|
||||
furi_crash();
|
||||
}
|
||||
|
||||
FuriString* error_string = context->error_string;
|
||||
|
||||
js_thread_stop(thread);
|
||||
furi_event_flag_free(context->event_flags);
|
||||
free(context);
|
||||
|
||||
if(flags & JsTestsError) {
|
||||
// memory leak: not freeing the FuriString if the tests fail,
|
||||
// because mu_fail executes a return
|
||||
//
|
||||
// who cares tho?
|
||||
mu_fail(furi_string_get_cstr(error_string));
|
||||
}
|
||||
}
|
||||
|
||||
MU_TEST(js_test_basic) {
|
||||
js_test_run(JS_SCRIPT_PATH("basic"));
|
||||
}
|
||||
MU_TEST(js_test_math) {
|
||||
js_test_run(JS_SCRIPT_PATH("math"));
|
||||
}
|
||||
MU_TEST(js_test_event_loop) {
|
||||
js_test_run(JS_SCRIPT_PATH("event_loop"));
|
||||
}
|
||||
MU_TEST(js_test_storage) {
|
||||
js_test_run(JS_SCRIPT_PATH("storage"));
|
||||
}
|
||||
|
||||
MU_TEST_SUITE(test_js) {
|
||||
MU_RUN_TEST(js_test_basic);
|
||||
MU_RUN_TEST(js_test_math);
|
||||
MU_RUN_TEST(js_test_event_loop);
|
||||
MU_RUN_TEST(js_test_storage);
|
||||
}
|
||||
|
||||
int run_minunit_test_js(void) {
|
||||
MU_RUN_SUITE(test_js);
|
||||
return MU_EXIT_CODE;
|
||||
}
|
||||
|
||||
TEST_API_DEFINE(run_minunit_test_js)
|
||||
@@ -31,7 +31,7 @@ extern "C" {
|
||||
#include <Windows.h>
|
||||
#if defined(_MSC_VER) && _MSC_VER < 1900
|
||||
#define snprintf _snprintf
|
||||
#define __func__ __FUNCTION__
|
||||
#define __func__ __FUNCTION__ //-V1059
|
||||
#endif
|
||||
|
||||
#elif defined(__unix__) || defined(__unix) || defined(unix) || \
|
||||
@@ -56,7 +56,7 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
#if __GNUC__ >= 5 && !defined(__STDC_VERSION__)
|
||||
#define __func__ __extension__ __FUNCTION__
|
||||
#define __func__ __extension__ __FUNCTION__ //-V1059
|
||||
#endif
|
||||
|
||||
#else
|
||||
@@ -102,6 +102,7 @@ void minunit_printf_warning(const char* format, ...);
|
||||
MU__SAFE_BLOCK(minunit_setup = setup_fun; minunit_teardown = teardown_fun;)
|
||||
|
||||
/* Test runner */
|
||||
//-V:MU_RUN_TEST:550
|
||||
#define MU_RUN_TEST(test) \
|
||||
MU__SAFE_BLOCK( \
|
||||
if(minunit_real_timer == 0 && minunit_proc_timer == 0) { \
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#include <rpc/rpc_i.h>
|
||||
#include <flipper.pb.h>
|
||||
#include <core/event_loop.h>
|
||||
#include <applications/system/js_app/js_thread.h>
|
||||
|
||||
static constexpr auto unit_tests_api_table = sort(create_array_t<sym_entry>(
|
||||
API_METHOD(resource_manifest_reader_alloc, ResourceManifestReader*, (Storage*)),
|
||||
@@ -33,13 +33,9 @@ static constexpr auto unit_tests_api_table = sort(create_array_t<sym_entry>(
|
||||
xQueueGenericSend,
|
||||
BaseType_t,
|
||||
(QueueHandle_t, const void* const, TickType_t, const BaseType_t)),
|
||||
API_METHOD(furi_event_loop_alloc, FuriEventLoop*, (void)),
|
||||
API_METHOD(furi_event_loop_free, void, (FuriEventLoop*)),
|
||||
API_METHOD(
|
||||
furi_event_loop_subscribe_message_queue,
|
||||
void,
|
||||
(FuriEventLoop*, FuriMessageQueue*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*)),
|
||||
API_METHOD(furi_event_loop_unsubscribe, void, (FuriEventLoop*, FuriEventLoopObject*)),
|
||||
API_METHOD(furi_event_loop_run, void, (FuriEventLoop*)),
|
||||
API_METHOD(furi_event_loop_stop, void, (FuriEventLoop*)),
|
||||
js_thread_run,
|
||||
JsThread*,
|
||||
(const char* script_path, JsThreadCallback callback, void* context)),
|
||||
API_METHOD(js_thread_stop, void, (JsThread * worker)),
|
||||
API_VARIABLE(PB_Main_msg, PB_Main_msg_t)));
|
||||
|
||||
@@ -200,6 +200,15 @@ App(
|
||||
sources=["plugins/supported_cards/skylanders.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="hworld_parser",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="hworld_plugin_ep",
|
||||
targets=["f7"],
|
||||
requires=["nfc"],
|
||||
sources=["plugins/supported_cards/hworld.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="nfc_start",
|
||||
targets=["f7"],
|
||||
|
||||
243
applications/main/nfc/plugins/supported_cards/hworld.c
Normal file
243
applications/main/nfc/plugins/supported_cards/hworld.c
Normal file
@@ -0,0 +1,243 @@
|
||||
// Flipper Zero parser for H World Hotel Key Cards
|
||||
// H World operates around 10,000 hotels, most of which in mainland China
|
||||
// Reverse engineering and parser written by @Torron (Github: @zinongli)
|
||||
#include "nfc_supported_card_plugin.h"
|
||||
#include <flipper_application.h>
|
||||
#include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
|
||||
#include <bit_lib.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define TAG "H World"
|
||||
#define ROOM_SECTOR 1
|
||||
#define VIP_SECTOR 5
|
||||
#define ROOM_SECTOR_KEY_BLOCK 7
|
||||
#define VIP_SECTOR_KEY_BLOCK 23
|
||||
#define ACCESS_INFO_BLOCK 5
|
||||
#define ROOM_NUM_DECIMAL_BLOCK 6
|
||||
#define H_WORLD_YEAR_OFFSET 2000
|
||||
|
||||
typedef struct {
|
||||
uint64_t a;
|
||||
uint64_t b;
|
||||
} MfClassicKeyPair;
|
||||
|
||||
static MfClassicKeyPair hworld_standard_keys[] = {
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 000
|
||||
{.a = 0x543071543071, .b = 0x5F01015F0101}, // 001
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 002
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 003
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 004
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 005
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 006
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 007
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 008
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 009
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 010
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 011
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 012
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 013
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 014
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 015
|
||||
};
|
||||
|
||||
static MfClassicKeyPair hworld_vip_keys[] = {
|
||||
{.a = 0x000000000000, .b = 0xFFFFFFFFFFFF}, // 000
|
||||
{.a = 0x543071543071, .b = 0x5F01015F0101}, // 001
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 002
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 003
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 004
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 005
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 006
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 007
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 008
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 009
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 010
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 011
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 012
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 013
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 014
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 015
|
||||
};
|
||||
|
||||
static bool hworld_verify(Nfc* nfc) {
|
||||
bool verified = false;
|
||||
|
||||
do {
|
||||
const uint8_t block_num = mf_classic_get_first_block_num_of_sector(ROOM_SECTOR);
|
||||
|
||||
MfClassicKey standard_key = {0};
|
||||
bit_lib_num_to_bytes_be(
|
||||
hworld_standard_keys[ROOM_SECTOR].a, COUNT_OF(standard_key.data), standard_key.data);
|
||||
|
||||
MfClassicAuthContext auth_context;
|
||||
MfClassicError standard_error = mf_classic_poller_sync_auth(
|
||||
nfc, block_num, &standard_key, MfClassicKeyTypeA, &auth_context);
|
||||
|
||||
if(standard_error != MfClassicErrorNone) {
|
||||
FURI_LOG_D(TAG, "Failed static key check for block %u", block_num);
|
||||
break;
|
||||
}
|
||||
|
||||
MfClassicKey vip_key = {0};
|
||||
bit_lib_num_to_bytes_be(
|
||||
hworld_vip_keys[VIP_SECTOR].b, COUNT_OF(vip_key.data), vip_key.data);
|
||||
|
||||
MfClassicError vip_error = mf_classic_poller_sync_auth(
|
||||
nfc, block_num, &vip_key, MfClassicKeyTypeB, &auth_context);
|
||||
|
||||
if(vip_error == MfClassicErrorNone) {
|
||||
FURI_LOG_D(TAG, "VIP card detected");
|
||||
} else {
|
||||
FURI_LOG_D(TAG, "Standard card detected");
|
||||
}
|
||||
|
||||
verified = true;
|
||||
} while(false);
|
||||
|
||||
return verified;
|
||||
}
|
||||
|
||||
static bool hworld_read(Nfc* nfc, NfcDevice* device) {
|
||||
furi_assert(nfc);
|
||||
furi_assert(device);
|
||||
|
||||
bool is_read = false;
|
||||
|
||||
MfClassicData* data = mf_classic_alloc();
|
||||
nfc_device_copy_data(device, NfcProtocolMfClassic, data);
|
||||
|
||||
do {
|
||||
MfClassicType type = MfClassicType1k;
|
||||
MfClassicError standard_error = mf_classic_poller_sync_detect_type(nfc, &type);
|
||||
MfClassicError vip_error = MfClassicErrorNotPresent;
|
||||
if(standard_error != MfClassicErrorNone) break;
|
||||
data->type = type;
|
||||
|
||||
MfClassicDeviceKeys standard_keys = {};
|
||||
for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
|
||||
bit_lib_num_to_bytes_be(
|
||||
hworld_standard_keys[i].a, sizeof(MfClassicKey), standard_keys.key_a[i].data);
|
||||
FURI_BIT_SET(standard_keys.key_a_mask, i);
|
||||
bit_lib_num_to_bytes_be(
|
||||
hworld_standard_keys[i].b, sizeof(MfClassicKey), standard_keys.key_b[i].data);
|
||||
FURI_BIT_SET(standard_keys.key_b_mask, i);
|
||||
}
|
||||
|
||||
standard_error = mf_classic_poller_sync_read(nfc, &standard_keys, data);
|
||||
if(standard_error == MfClassicErrorNone) {
|
||||
FURI_LOG_I(TAG, "Standard card successfully read");
|
||||
} else {
|
||||
MfClassicDeviceKeys vip_keys = {};
|
||||
for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
|
||||
bit_lib_num_to_bytes_be(
|
||||
hworld_vip_keys[i].a, sizeof(MfClassicKey), vip_keys.key_a[i].data);
|
||||
FURI_BIT_SET(vip_keys.key_a_mask, i);
|
||||
bit_lib_num_to_bytes_be(
|
||||
hworld_vip_keys[i].b, sizeof(MfClassicKey), vip_keys.key_b[i].data);
|
||||
FURI_BIT_SET(vip_keys.key_b_mask, i);
|
||||
}
|
||||
|
||||
vip_error = mf_classic_poller_sync_read(nfc, &vip_keys, data);
|
||||
|
||||
if(vip_error == MfClassicErrorNone) {
|
||||
FURI_LOG_I(TAG, "VIP card successfully read");
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
nfc_device_set_data(device, NfcProtocolMfClassic, data);
|
||||
|
||||
is_read = (standard_error == MfClassicErrorNone) | (vip_error == MfClassicErrorNone);
|
||||
} while(false);
|
||||
|
||||
mf_classic_free(data);
|
||||
|
||||
return is_read;
|
||||
}
|
||||
|
||||
bool hworld_parse(const NfcDevice* device, FuriString* parsed_data) {
|
||||
furi_assert(device);
|
||||
const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
|
||||
bool parsed = false;
|
||||
|
||||
do {
|
||||
// Check card type
|
||||
if(data->type != MfClassicType1k) break;
|
||||
|
||||
// Check static key for verificaiton
|
||||
const uint8_t* data_room_sec_key_a_ptr = &data->block[ROOM_SECTOR_KEY_BLOCK].data[0];
|
||||
const uint8_t* data_room_sec_key_b_ptr = &data->block[ROOM_SECTOR_KEY_BLOCK].data[10];
|
||||
uint64_t data_room_sec_key_a = bit_lib_get_bits_64(data_room_sec_key_a_ptr, 0, 48);
|
||||
uint64_t data_room_sec_key_b = bit_lib_get_bits_64(data_room_sec_key_b_ptr, 0, 48);
|
||||
if((data_room_sec_key_a != hworld_standard_keys[ROOM_SECTOR].a) |
|
||||
(data_room_sec_key_b != hworld_standard_keys[ROOM_SECTOR].b))
|
||||
break;
|
||||
|
||||
// Check whether this card is VIP
|
||||
const uint8_t* data_vip_sec_key_b_ptr = &data->block[VIP_SECTOR_KEY_BLOCK].data[10];
|
||||
uint64_t data_vip_sec_key_b = bit_lib_get_bits_64(data_vip_sec_key_b_ptr, 0, 48);
|
||||
bool is_hworld_vip = (data_vip_sec_key_b == hworld_vip_keys[VIP_SECTOR].b);
|
||||
uint8_t room_floor = data->block[ACCESS_INFO_BLOCK].data[13];
|
||||
uint8_t room_num = data->block[ACCESS_INFO_BLOCK].data[14];
|
||||
|
||||
// Check in date & time
|
||||
uint16_t check_in_year = data->block[ACCESS_INFO_BLOCK].data[2] + H_WORLD_YEAR_OFFSET;
|
||||
uint8_t check_in_month = data->block[ACCESS_INFO_BLOCK].data[3];
|
||||
uint8_t check_in_day = data->block[ACCESS_INFO_BLOCK].data[4];
|
||||
uint8_t check_in_hour = data->block[ACCESS_INFO_BLOCK].data[5];
|
||||
uint8_t check_in_minute = data->block[ACCESS_INFO_BLOCK].data[6];
|
||||
|
||||
// Expire date & time
|
||||
uint16_t expire_year = data->block[ACCESS_INFO_BLOCK].data[7] + H_WORLD_YEAR_OFFSET;
|
||||
uint8_t expire_month = data->block[ACCESS_INFO_BLOCK].data[8];
|
||||
uint8_t expire_day = data->block[ACCESS_INFO_BLOCK].data[9];
|
||||
uint8_t expire_hour = data->block[ACCESS_INFO_BLOCK].data[10];
|
||||
uint8_t expire_minute = data->block[ACCESS_INFO_BLOCK].data[11];
|
||||
|
||||
furi_string_cat_printf(parsed_data, "\e#H World Card\n");
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "%s\n", is_hworld_vip ? "VIP card" : "Standard room key");
|
||||
furi_string_cat_printf(parsed_data, "Room Num: %u%02u\n", room_floor, room_num);
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Check-in Date: \n%04u-%02d-%02d\n%02d:%02d:00\n",
|
||||
check_in_year,
|
||||
check_in_month,
|
||||
check_in_day,
|
||||
check_in_hour,
|
||||
check_in_minute);
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Expiration Date: \n%04u-%02d-%02d\n%02d:%02d:00",
|
||||
expire_year,
|
||||
expire_month,
|
||||
expire_day,
|
||||
expire_hour,
|
||||
expire_minute);
|
||||
parsed = true;
|
||||
} while(false);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/* Actual implementation of app<>plugin interface */
|
||||
static const NfcSupportedCardsPlugin hworld_plugin = {
|
||||
.protocol = NfcProtocolMfClassic,
|
||||
.verify = hworld_verify,
|
||||
.read = hworld_read,
|
||||
.parse = hworld_parse,
|
||||
};
|
||||
|
||||
/* Plugin descriptor to comply with basic plugin specification */
|
||||
static const FlipperAppPluginDescriptor hworld_plugin_descriptor = {
|
||||
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
|
||||
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
|
||||
.entry_point = &hworld_plugin,
|
||||
};
|
||||
|
||||
/* Plugin entry point - must return a pointer to const descriptor */
|
||||
const FlipperAppPluginDescriptor* hworld_plugin_ep(void) {
|
||||
return &hworld_plugin_descriptor;
|
||||
}
|
||||
@@ -999,13 +999,12 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) {
|
||||
chat_event = subghz_chat_worker_get_event_chat(subghz_chat);
|
||||
switch(chat_event.event) {
|
||||
case SubGhzChatEventInputData:
|
||||
if(chat_event.c == CliSymbolAsciiETX) {
|
||||
if(chat_event.c == CliKeyETX) {
|
||||
printf("\r\n");
|
||||
chat_event.event = SubGhzChatEventUserExit;
|
||||
subghz_chat_worker_put_event_chat(subghz_chat, &chat_event);
|
||||
break;
|
||||
} else if(
|
||||
(chat_event.c == CliSymbolAsciiBackspace) || (chat_event.c == CliSymbolAsciiDel)) {
|
||||
} else if((chat_event.c == CliKeyBackspace) || (chat_event.c == CliKeyDEL)) {
|
||||
size_t len = furi_string_utf8_length(input);
|
||||
if(len > furi_string_utf8_length(name)) {
|
||||
printf("%s", "\e[D\e[1P");
|
||||
@@ -1027,7 +1026,7 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) {
|
||||
}
|
||||
furi_string_set(input, sysmsg);
|
||||
}
|
||||
} else if(chat_event.c == CliSymbolAsciiCR) {
|
||||
} else if(chat_event.c == CliKeyCR) {
|
||||
printf("\r\n");
|
||||
furi_string_push_back(input, '\r');
|
||||
furi_string_push_back(input, '\n');
|
||||
@@ -1041,7 +1040,7 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) {
|
||||
furi_string_printf(input, "%s", furi_string_get_cstr(name));
|
||||
printf("%s", furi_string_get_cstr(input));
|
||||
fflush(stdout);
|
||||
} else if(chat_event.c == CliSymbolAsciiLF) {
|
||||
} else if(chat_event.c == CliKeyLF) {
|
||||
//cut out the symbol \n
|
||||
} else {
|
||||
putc(chat_event.c, stdout);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <cli/cli.h>
|
||||
#include <cli/cli_ansi.h>
|
||||
|
||||
void subghz_on_system_start(void);
|
||||
|
||||
@@ -430,13 +430,11 @@ static void bt_change_profile(Bt* bt, BtMessage* message) {
|
||||
*message->profile_instance = NULL;
|
||||
}
|
||||
}
|
||||
if(message->lock) api_lock_unlock(message->lock);
|
||||
}
|
||||
|
||||
static void bt_close_connection(Bt* bt, BtMessage* message) {
|
||||
static void bt_close_connection(Bt* bt) {
|
||||
bt_close_rpc_connection(bt);
|
||||
furi_hal_bt_stop_advertising();
|
||||
if(message->lock) api_lock_unlock(message->lock);
|
||||
}
|
||||
|
||||
static void bt_apply_settings(Bt* bt) {
|
||||
@@ -484,19 +482,13 @@ static void bt_load_settings(Bt* bt) {
|
||||
}
|
||||
|
||||
static void bt_handle_get_settings(Bt* bt, BtMessage* message) {
|
||||
furi_assert(message->lock);
|
||||
*message->data.settings = bt->bt_settings;
|
||||
api_lock_unlock(message->lock);
|
||||
}
|
||||
|
||||
static void bt_handle_set_settings(Bt* bt, BtMessage* message) {
|
||||
furi_assert(message->lock);
|
||||
bt->bt_settings = *message->data.csettings;
|
||||
|
||||
bt_apply_settings(bt);
|
||||
bt_settings_save(&bt->bt_settings);
|
||||
|
||||
api_lock_unlock(message->lock);
|
||||
}
|
||||
|
||||
static void bt_handle_reload_keys_settings(Bt* bt) {
|
||||
@@ -548,6 +540,12 @@ int32_t bt_srv(void* p) {
|
||||
while(1) {
|
||||
furi_check(
|
||||
furi_message_queue_get(bt->message_queue, &message, FuriWaitForever) == FuriStatusOk);
|
||||
FURI_LOG_D(
|
||||
TAG,
|
||||
"call %d, lock 0x%p, result 0x%p",
|
||||
message.type,
|
||||
(void*)message.lock,
|
||||
(void*)message.result);
|
||||
if(message.type == BtMessageTypeUpdateStatus) {
|
||||
// Update view ports
|
||||
bt_statusbar_update(bt);
|
||||
@@ -571,7 +569,7 @@ int32_t bt_srv(void* p) {
|
||||
} else if(message.type == BtMessageTypeSetProfile) {
|
||||
bt_change_profile(bt, &message);
|
||||
} else if(message.type == BtMessageTypeDisconnect) {
|
||||
bt_close_connection(bt, &message);
|
||||
bt_close_connection(bt);
|
||||
} else if(message.type == BtMessageTypeForgetBondedDevices) {
|
||||
bt_keys_storage_delete(bt->keys_storage);
|
||||
} else if(message.type == BtMessageTypeGetSettings) {
|
||||
@@ -581,6 +579,8 @@ int32_t bt_srv(void* p) {
|
||||
} else if(message.type == BtMessageTypeReloadKeysSettings) {
|
||||
bt_handle_reload_keys_settings(bt);
|
||||
}
|
||||
|
||||
if(message.lock) api_lock_unlock(message.lock);
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
#include "cli_i.h"
|
||||
#include "cli_commands.h"
|
||||
#include "cli_vcp.h"
|
||||
#include "cli_ansi.h"
|
||||
#include <furi_hal_version.h>
|
||||
#include <loader/loader.h>
|
||||
|
||||
#define TAG "CliSrv"
|
||||
|
||||
#define CLI_INPUT_LEN_LIMIT 256
|
||||
#define CLI_PROMPT ">: " // qFlipper does not recognize us if we use escape sequences :(
|
||||
#define CLI_PROMPT_LENGTH 3 // printable characters
|
||||
|
||||
Cli* cli_alloc(void) {
|
||||
Cli* cli = malloc(sizeof(Cli));
|
||||
@@ -85,7 +88,7 @@ bool cli_cmd_interrupt_received(Cli* cli) {
|
||||
char c = '\0';
|
||||
if(cli_is_connected(cli)) {
|
||||
if(cli->session->rx((uint8_t*)&c, 1, 0) == 1) {
|
||||
return c == CliSymbolAsciiETX;
|
||||
return c == CliKeyETX;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
@@ -102,7 +105,8 @@ void cli_print_usage(const char* cmd, const char* usage, const char* arg) {
|
||||
}
|
||||
|
||||
void cli_motd(void) {
|
||||
printf("\r\n"
|
||||
printf(ANSI_FLIPPER_BRAND_ORANGE
|
||||
"\r\n"
|
||||
" _.-------.._ -,\r\n"
|
||||
" .-\"```\"--..,,_/ /`-, -, \\ \r\n"
|
||||
" .:\" /:/ /'\\ \\ ,_..., `. | |\r\n"
|
||||
@@ -116,12 +120,11 @@ void cli_motd(void) {
|
||||
" _L_ _ ___ ___ ___ ___ ____--\"`___ _ ___\r\n"
|
||||
"| __|| | |_ _|| _ \\| _ \\| __|| _ \\ / __|| | |_ _|\r\n"
|
||||
"| _| | |__ | | | _/| _/| _| | / | (__ | |__ | |\r\n"
|
||||
"|_| |____||___||_| |_| |___||_|_\\ \\___||____||___|\r\n"
|
||||
"\r\n"
|
||||
"Welcome to Flipper Zero Command Line Interface!\r\n"
|
||||
"|_| |____||___||_| |_| |___||_|_\\ \\___||____||___|\r\n" ANSI_RESET
|
||||
"\r\n" ANSI_FG_BR_WHITE "Welcome to " ANSI_FLIPPER_BRAND_ORANGE
|
||||
"Flipper Zero" ANSI_FG_BR_WHITE " Command Line Interface!\r\n"
|
||||
"Read the manual: https://docs.flipper.net/development/cli\r\n"
|
||||
"Run `help` or `?` to list available commands\r\n"
|
||||
"\r\n");
|
||||
"Run `help` or `?` to list available commands\r\n" ANSI_RESET "\r\n");
|
||||
|
||||
const Version* firmware_version = furi_hal_version_get_firmware_version();
|
||||
if(firmware_version) {
|
||||
@@ -142,7 +145,7 @@ void cli_nl(Cli* cli) {
|
||||
|
||||
void cli_prompt(Cli* cli) {
|
||||
UNUSED(cli);
|
||||
printf("\r\n>: %s", furi_string_get_cstr(cli->line));
|
||||
printf("\r\n" CLI_PROMPT "%s", furi_string_get_cstr(cli->line));
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
@@ -165,7 +168,7 @@ static void cli_handle_backspace(Cli* cli) {
|
||||
|
||||
cli->cursor_position--;
|
||||
} else {
|
||||
cli_putc(cli, CliSymbolAsciiBell);
|
||||
cli_putc(cli, CliKeyBell);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +244,7 @@ static void cli_handle_enter(Cli* cli) {
|
||||
printf(
|
||||
"`%s` command not found, use `help` or `?` to list all available commands",
|
||||
furi_string_get_cstr(command));
|
||||
cli_putc(cli, CliSymbolAsciiBell);
|
||||
cli_putc(cli, CliKeyBell);
|
||||
}
|
||||
|
||||
cli_reset(cli);
|
||||
@@ -305,8 +308,85 @@ static void cli_handle_autocomplete(Cli* cli) {
|
||||
cli_prompt(cli);
|
||||
}
|
||||
|
||||
static void cli_handle_escape(Cli* cli, char c) {
|
||||
if(c == 'A') {
|
||||
typedef enum {
|
||||
CliCharClassWord,
|
||||
CliCharClassSpace,
|
||||
CliCharClassOther,
|
||||
} CliCharClass;
|
||||
|
||||
/**
|
||||
* @brief Determines the class that a character belongs to
|
||||
*
|
||||
* The return value of this function should not be used on its own; it should
|
||||
* only be used for comparing it with other values returned by this function.
|
||||
* This function is used internally in `cli_skip_run`.
|
||||
*/
|
||||
static CliCharClass cli_char_class(char c) {
|
||||
if((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') {
|
||||
return CliCharClassWord;
|
||||
} else if(c == ' ') {
|
||||
return CliCharClassSpace;
|
||||
} else {
|
||||
return CliCharClassOther;
|
||||
}
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
CliSkipDirectionLeft,
|
||||
CliSkipDirectionRight,
|
||||
} CliSkipDirection;
|
||||
|
||||
/**
|
||||
* @brief Skips a run of a class of characters
|
||||
*
|
||||
* @param string Input string
|
||||
* @param original_pos Position to start the search at
|
||||
* @param direction Direction in which to perform the search
|
||||
* @returns The position at which the run ends
|
||||
*/
|
||||
static size_t cli_skip_run(FuriString* string, size_t original_pos, CliSkipDirection direction) {
|
||||
if(furi_string_size(string) == 0) return original_pos;
|
||||
if(direction == CliSkipDirectionLeft && original_pos == 0) return original_pos;
|
||||
if(direction == CliSkipDirectionRight && original_pos == furi_string_size(string))
|
||||
return original_pos;
|
||||
|
||||
int8_t look_offset = (direction == CliSkipDirectionLeft) ? -1 : 0;
|
||||
int8_t increment = (direction == CliSkipDirectionLeft) ? -1 : 1;
|
||||
int32_t position = original_pos;
|
||||
CliCharClass start_class =
|
||||
cli_char_class(furi_string_get_char(string, position + look_offset));
|
||||
|
||||
while(true) {
|
||||
position += increment;
|
||||
if(position < 0) break;
|
||||
if(position >= (int32_t)furi_string_size(string)) break;
|
||||
if(cli_char_class(furi_string_get_char(string, position + look_offset)) != start_class)
|
||||
break;
|
||||
}
|
||||
|
||||
return MAX(0, position);
|
||||
}
|
||||
|
||||
void cli_process_input(Cli* cli) {
|
||||
CliKeyCombo combo = cli_read_ansi_key_combo(cli);
|
||||
FURI_LOG_T(TAG, "code=0x%02x, mod=0x%x\r\n", combo.key, combo.modifiers);
|
||||
|
||||
if(combo.key == CliKeyTab) {
|
||||
cli_handle_autocomplete(cli);
|
||||
|
||||
} else if(combo.key == CliKeySOH) {
|
||||
furi_delay_ms(33); // We are too fast, Minicom is not ready yet
|
||||
cli_motd();
|
||||
cli_prompt(cli);
|
||||
|
||||
} else if(combo.key == CliKeyETX) {
|
||||
cli_reset(cli);
|
||||
cli_prompt(cli);
|
||||
|
||||
} else if(combo.key == CliKeyEOT) {
|
||||
cli_reset(cli);
|
||||
|
||||
} else if(combo.key == CliKeyUp && combo.modifiers == CliModKeyNo) {
|
||||
// Use previous command if line buffer is empty
|
||||
if(furi_string_size(cli->line) == 0 && furi_string_cmp(cli->line, cli->last_line) != 0) {
|
||||
// Set line buffer and cursor position
|
||||
@@ -315,67 +395,85 @@ static void cli_handle_escape(Cli* cli, char c) {
|
||||
// Show new line to user
|
||||
printf("%s", furi_string_get_cstr(cli->line));
|
||||
}
|
||||
} else if(c == 'B') {
|
||||
} else if(c == 'C') {
|
||||
|
||||
} else if(combo.key == CliKeyDown && combo.modifiers == CliModKeyNo) {
|
||||
// Clear input buffer
|
||||
furi_string_reset(cli->line);
|
||||
cli->cursor_position = 0;
|
||||
printf("\r" CLI_PROMPT "\e[0K");
|
||||
|
||||
} else if(combo.key == CliKeyRight && combo.modifiers == CliModKeyNo) {
|
||||
// Move right
|
||||
if(cli->cursor_position < furi_string_size(cli->line)) {
|
||||
cli->cursor_position++;
|
||||
printf("\e[C");
|
||||
}
|
||||
} else if(c == 'D') {
|
||||
|
||||
} else if(combo.key == CliKeyLeft && combo.modifiers == CliModKeyNo) {
|
||||
// Move left
|
||||
if(cli->cursor_position > 0) {
|
||||
cli->cursor_position--;
|
||||
printf("\e[D");
|
||||
}
|
||||
}
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
void cli_process_input(Cli* cli) {
|
||||
char in_chr = cli_getc(cli);
|
||||
size_t rx_len;
|
||||
} else if(combo.key == CliKeyHome && combo.modifiers == CliModKeyNo) {
|
||||
// Move to beginning of line
|
||||
cli->cursor_position = 0;
|
||||
printf("\e[%uG", CLI_PROMPT_LENGTH + 1); // columns start at 1 \(-_-)/
|
||||
|
||||
} else if(combo.key == CliKeyEnd && combo.modifiers == CliModKeyNo) {
|
||||
// Move to end of line
|
||||
cli->cursor_position = furi_string_size(cli->line);
|
||||
printf("\e[%zuG", CLI_PROMPT_LENGTH + cli->cursor_position + 1);
|
||||
|
||||
if(in_chr == CliSymbolAsciiTab) {
|
||||
cli_handle_autocomplete(cli);
|
||||
} else if(in_chr == CliSymbolAsciiSOH) {
|
||||
furi_delay_ms(33); // We are too fast, Minicom is not ready yet
|
||||
cli_motd();
|
||||
cli_prompt(cli);
|
||||
} else if(in_chr == CliSymbolAsciiETX) {
|
||||
cli_reset(cli);
|
||||
cli_prompt(cli);
|
||||
} else if(in_chr == CliSymbolAsciiEOT) {
|
||||
cli_reset(cli);
|
||||
} else if(in_chr == CliSymbolAsciiEsc) {
|
||||
rx_len = cli_read(cli, (uint8_t*)&in_chr, 1);
|
||||
if((rx_len > 0) && (in_chr == '[')) {
|
||||
cli_read(cli, (uint8_t*)&in_chr, 1);
|
||||
cli_handle_escape(cli, in_chr);
|
||||
} else {
|
||||
cli_putc(cli, CliSymbolAsciiBell);
|
||||
}
|
||||
} else if(in_chr == CliSymbolAsciiBackspace || in_chr == CliSymbolAsciiDel) {
|
||||
cli_handle_backspace(cli);
|
||||
} else if(in_chr == CliSymbolAsciiCR) {
|
||||
cli_handle_enter(cli);
|
||||
} else if(
|
||||
(in_chr >= 0x20 && in_chr < 0x7F) && //-V560
|
||||
combo.modifiers == CliModKeyCtrl &&
|
||||
(combo.key == CliKeyLeft || combo.key == CliKeyRight)) {
|
||||
// Skip run of similar chars to the left or right
|
||||
CliSkipDirection direction = (combo.key == CliKeyLeft) ? CliSkipDirectionLeft :
|
||||
CliSkipDirectionRight;
|
||||
cli->cursor_position = cli_skip_run(cli->line, cli->cursor_position, direction);
|
||||
printf("\e[%zuG", CLI_PROMPT_LENGTH + cli->cursor_position + 1);
|
||||
|
||||
} else if(combo.key == CliKeyBackspace || combo.key == CliKeyDEL) {
|
||||
cli_handle_backspace(cli);
|
||||
|
||||
} else if(combo.key == CliKeyETB) { // Ctrl + Backspace
|
||||
// Delete run of similar chars to the left
|
||||
size_t run_start = cli_skip_run(cli->line, cli->cursor_position, CliSkipDirectionLeft);
|
||||
furi_string_replace_at(cli->line, run_start, cli->cursor_position - run_start, "");
|
||||
cli->cursor_position = run_start;
|
||||
printf(
|
||||
"\e[%zuG%s\e[0K\e[%zuG", // move cursor, print second half of line, erase remains, move cursor again
|
||||
CLI_PROMPT_LENGTH + cli->cursor_position + 1,
|
||||
furi_string_get_cstr(cli->line) + run_start,
|
||||
CLI_PROMPT_LENGTH + run_start + 1);
|
||||
|
||||
} else if(combo.key == CliKeyCR) {
|
||||
cli_handle_enter(cli);
|
||||
|
||||
} else if(
|
||||
(combo.key >= 0x20 && combo.key < 0x7F) && //-V560
|
||||
(furi_string_size(cli->line) < CLI_INPUT_LEN_LIMIT)) {
|
||||
if(cli->cursor_position == furi_string_size(cli->line)) {
|
||||
furi_string_push_back(cli->line, in_chr);
|
||||
cli_putc(cli, in_chr);
|
||||
furi_string_push_back(cli->line, combo.key);
|
||||
cli_putc(cli, combo.key);
|
||||
} else {
|
||||
// Insert character to line buffer
|
||||
const char in_str[2] = {in_chr, 0};
|
||||
const char in_str[2] = {combo.key, 0};
|
||||
furi_string_replace_at(cli->line, cli->cursor_position, 0, in_str);
|
||||
|
||||
// Print character in replace mode
|
||||
printf("\e[4h%c\e[4l", in_chr);
|
||||
printf("\e[4h%c\e[4l", combo.key);
|
||||
fflush(stdout);
|
||||
}
|
||||
cli->cursor_position++;
|
||||
|
||||
} else {
|
||||
cli_putc(cli, CliSymbolAsciiBell);
|
||||
cli_putc(cli, CliKeyBell);
|
||||
}
|
||||
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
void cli_add_command(
|
||||
|
||||
@@ -10,26 +10,12 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
CliSymbolAsciiSOH = 0x01,
|
||||
CliSymbolAsciiETX = 0x03,
|
||||
CliSymbolAsciiEOT = 0x04,
|
||||
CliSymbolAsciiBell = 0x07,
|
||||
CliSymbolAsciiBackspace = 0x08,
|
||||
CliSymbolAsciiTab = 0x09,
|
||||
CliSymbolAsciiLF = 0x0A,
|
||||
CliSymbolAsciiCR = 0x0D,
|
||||
CliSymbolAsciiEsc = 0x1B,
|
||||
CliSymbolAsciiUS = 0x1F,
|
||||
CliSymbolAsciiSpace = 0x20,
|
||||
CliSymbolAsciiDel = 0x7F,
|
||||
} CliSymbols;
|
||||
|
||||
typedef enum {
|
||||
CliCommandFlagDefault = 0, /**< Default, loader lock is used */
|
||||
CliCommandFlagParallelSafe =
|
||||
(1 << 0), /**< Safe to run in parallel with other apps, loader lock is not used */
|
||||
CliCommandFlagInsomniaSafe = (1 << 1), /**< Safe to run with insomnia mode on */
|
||||
CliCommandFlagHidden = (1 << 2), /**< Not shown in `help` */
|
||||
} CliCommandFlag;
|
||||
|
||||
#define RECORD_CLI "cli"
|
||||
|
||||
76
applications/services/cli/cli_ansi.c
Normal file
76
applications/services/cli/cli_ansi.c
Normal file
@@ -0,0 +1,76 @@
|
||||
#include "cli_ansi.h"
|
||||
|
||||
/**
|
||||
* @brief Converts a single character representing a special key into the enum
|
||||
* representation
|
||||
*/
|
||||
static CliKey cli_ansi_key_from_mnemonic(char c) {
|
||||
switch(c) {
|
||||
case 'A':
|
||||
return CliKeyUp;
|
||||
case 'B':
|
||||
return CliKeyDown;
|
||||
case 'C':
|
||||
return CliKeyRight;
|
||||
case 'D':
|
||||
return CliKeyLeft;
|
||||
case 'F':
|
||||
return CliKeyEnd;
|
||||
case 'H':
|
||||
return CliKeyHome;
|
||||
default:
|
||||
return CliKeyUnrecognized;
|
||||
}
|
||||
}
|
||||
|
||||
CliKeyCombo cli_read_ansi_key_combo(Cli* cli) {
|
||||
char ch = cli_getc(cli);
|
||||
|
||||
if(ch != CliKeyEsc)
|
||||
return (CliKeyCombo){
|
||||
.modifiers = CliModKeyNo,
|
||||
.key = ch,
|
||||
};
|
||||
|
||||
ch = cli_getc(cli);
|
||||
|
||||
// ESC ESC -> ESC
|
||||
if(ch == '\e')
|
||||
return (CliKeyCombo){
|
||||
.modifiers = CliModKeyNo,
|
||||
.key = '\e',
|
||||
};
|
||||
|
||||
// ESC <char> -> Alt + <char>
|
||||
if(ch != '[')
|
||||
return (CliKeyCombo){
|
||||
.modifiers = CliModKeyAlt,
|
||||
.key = cli_getc(cli),
|
||||
};
|
||||
|
||||
ch = cli_getc(cli);
|
||||
|
||||
// ESC [ 1
|
||||
if(ch == '1') {
|
||||
// ESC [ 1 ; <modifier bitfield> <key mnemonic>
|
||||
if(cli_getc(cli) == ';') {
|
||||
CliModKey modifiers = (cli_getc(cli) - '0'); // convert following digit to a number
|
||||
modifiers &= ~1;
|
||||
return (CliKeyCombo){
|
||||
.modifiers = modifiers,
|
||||
.key = cli_ansi_key_from_mnemonic(cli_getc(cli)),
|
||||
};
|
||||
}
|
||||
|
||||
return (CliKeyCombo){
|
||||
.modifiers = CliModKeyNo,
|
||||
.key = CliKeyUnrecognized,
|
||||
};
|
||||
}
|
||||
|
||||
// ESC [ <key mnemonic>
|
||||
return (CliKeyCombo){
|
||||
.modifiers = CliModKeyNo,
|
||||
.key = cli_ansi_key_from_mnemonic(ch),
|
||||
};
|
||||
}
|
||||
94
applications/services/cli/cli_ansi.h
Normal file
94
applications/services/cli/cli_ansi.h
Normal file
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#include "cli.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define ANSI_RESET "\e[0m"
|
||||
#define ANSI_BOLD "\e[1m"
|
||||
#define ANSI_FAINT "\e[2m"
|
||||
|
||||
#define ANSI_FG_BLACK "\e[30m"
|
||||
#define ANSI_FG_RED "\e[31m"
|
||||
#define ANSI_FG_GREEN "\e[32m"
|
||||
#define ANSI_FG_YELLOW "\e[33m"
|
||||
#define ANSI_FG_BLUE "\e[34m"
|
||||
#define ANSI_FG_MAGENTA "\e[35m"
|
||||
#define ANSI_FG_CYAN "\e[36m"
|
||||
#define ANSI_FG_WHITE "\e[37m"
|
||||
#define ANSI_FG_BR_BLACK "\e[90m"
|
||||
#define ANSI_FG_BR_RED "\e[91m"
|
||||
#define ANSI_FG_BR_GREEN "\e[92m"
|
||||
#define ANSI_FG_BR_YELLOW "\e[93m"
|
||||
#define ANSI_FG_BR_BLUE "\e[94m"
|
||||
#define ANSI_FG_BR_MAGENTA "\e[95m"
|
||||
#define ANSI_FG_BR_CYAN "\e[96m"
|
||||
#define ANSI_FG_BR_WHITE "\e[97m"
|
||||
|
||||
#define ANSI_BG_BLACK "\e[40m"
|
||||
#define ANSI_BG_RED "\e[41m"
|
||||
#define ANSI_BG_GREEN "\e[42m"
|
||||
#define ANSI_BG_YELLOW "\e[43m"
|
||||
#define ANSI_BG_BLUE "\e[44m"
|
||||
#define ANSI_BG_MAGENTA "\e[45m"
|
||||
#define ANSI_BG_CYAN "\e[46m"
|
||||
#define ANSI_BG_WHITE "\e[47m"
|
||||
#define ANSI_BG_BR_BLACK "\e[100m"
|
||||
#define ANSI_BG_BR_RED "\e[101m"
|
||||
#define ANSI_BG_BR_GREEN "\e[102m"
|
||||
#define ANSI_BG_BR_YELLOW "\e[103m"
|
||||
#define ANSI_BG_BR_BLUE "\e[104m"
|
||||
#define ANSI_BG_BR_MAGENTA "\e[105m"
|
||||
#define ANSI_BG_BR_CYAN "\e[106m"
|
||||
#define ANSI_BG_BR_WHITE "\e[107m"
|
||||
|
||||
#define ANSI_FLIPPER_BRAND_ORANGE "\e[38;2;255;130;0m"
|
||||
|
||||
typedef enum {
|
||||
CliKeyUnrecognized = 0,
|
||||
|
||||
CliKeySOH = 0x01,
|
||||
CliKeyETX = 0x03,
|
||||
CliKeyEOT = 0x04,
|
||||
CliKeyBell = 0x07,
|
||||
CliKeyBackspace = 0x08,
|
||||
CliKeyTab = 0x09,
|
||||
CliKeyLF = 0x0A,
|
||||
CliKeyCR = 0x0D,
|
||||
CliKeyETB = 0x17,
|
||||
CliKeyEsc = 0x1B,
|
||||
CliKeyUS = 0x1F,
|
||||
CliKeySpace = 0x20,
|
||||
CliKeyDEL = 0x7F,
|
||||
|
||||
CliKeySpecial = 0x80,
|
||||
CliKeyLeft,
|
||||
CliKeyRight,
|
||||
CliKeyUp,
|
||||
CliKeyDown,
|
||||
CliKeyHome,
|
||||
CliKeyEnd,
|
||||
} CliKey;
|
||||
|
||||
typedef enum {
|
||||
CliModKeyNo = 0,
|
||||
CliModKeyAlt = 2,
|
||||
CliModKeyCtrl = 4,
|
||||
CliModKeyMeta = 8,
|
||||
} CliModKey;
|
||||
|
||||
typedef struct {
|
||||
CliModKey modifiers;
|
||||
CliKey key;
|
||||
} CliKeyCombo;
|
||||
|
||||
/**
|
||||
* @brief Reads a key or key combination
|
||||
*/
|
||||
CliKeyCombo cli_read_ansi_key_combo(Cli* cli);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "cli_commands.h"
|
||||
#include "cli_command_gpio.h"
|
||||
#include "cli_ansi.h"
|
||||
|
||||
#include <core/thread.h>
|
||||
#include <furi_hal.h>
|
||||
@@ -10,6 +11,7 @@
|
||||
#include <loader/loader.h>
|
||||
#include <lib/toolbox/args.h>
|
||||
#include <lib/toolbox/strint.h>
|
||||
#include <storage/storage.h>
|
||||
|
||||
// Close to ISO, `date +'%Y-%m-%d %H:%M:%S %u'`
|
||||
#define CLI_DATE_FORMAT "%.4d-%.2d-%.2d %.2d:%.2d:%.2d %d"
|
||||
@@ -52,37 +54,196 @@ void cli_command_info(Cli* cli, FuriString* args, void* context) {
|
||||
}
|
||||
}
|
||||
|
||||
void cli_command_help(Cli* cli, FuriString* args, void* context) {
|
||||
// Lil Easter egg :>
|
||||
void cli_command_neofetch(Cli* cli, FuriString* args, void* context) {
|
||||
UNUSED(cli);
|
||||
UNUSED(args);
|
||||
UNUSED(context);
|
||||
|
||||
static const char* const neofetch_logo[] = {
|
||||
" _.-------.._ -,",
|
||||
" .-\"```\"--..,,_/ /`-, -, \\ ",
|
||||
" .:\" /:/ /'\\ \\ ,_..., `. | |",
|
||||
" / ,----/:/ /`\\ _\\~`_-\"` _;",
|
||||
" ' / /`\"\"\"'\\ \\ \\.~`_-' ,-\"'/ ",
|
||||
" | | | 0 | | .-' ,/` /",
|
||||
" | ,..\\ \\ ,.-\"` ,/` /",
|
||||
"; : `/`\"\"\\` ,/--==,/-----,",
|
||||
"| `-...| -.___-Z:_______J...---;",
|
||||
": ` _-'",
|
||||
};
|
||||
#define NEOFETCH_COLOR ANSI_FLIPPER_BRAND_ORANGE
|
||||
|
||||
// Determine logo parameters
|
||||
size_t logo_height = COUNT_OF(neofetch_logo), logo_width = 0;
|
||||
for(size_t i = 0; i < logo_height; i++)
|
||||
logo_width = MAX(logo_width, strlen(neofetch_logo[i]));
|
||||
logo_width += 4; // space between logo and info
|
||||
|
||||
// Format hostname delimiter
|
||||
const size_t size_of_hostname = 4 + strlen(furi_hal_version_get_name_ptr());
|
||||
char delimiter[64];
|
||||
memset(delimiter, '-', size_of_hostname);
|
||||
delimiter[size_of_hostname] = '\0';
|
||||
|
||||
// Get heap info
|
||||
size_t heap_total = memmgr_get_total_heap();
|
||||
size_t heap_used = heap_total - memmgr_get_free_heap();
|
||||
uint16_t heap_percent = (100 * heap_used) / heap_total;
|
||||
|
||||
// Get storage info
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
uint64_t ext_total, ext_free, ext_used, ext_percent;
|
||||
storage_common_fs_info(storage, "/ext", &ext_total, &ext_free);
|
||||
ext_used = ext_total - ext_free;
|
||||
ext_percent = (100 * ext_used) / ext_total;
|
||||
ext_used /= 1024 * 1024;
|
||||
ext_total /= 1024 * 1024;
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
// Get battery info
|
||||
uint16_t charge_percent = furi_hal_power_get_pct();
|
||||
const char* charge_state;
|
||||
if(furi_hal_power_is_charging()) {
|
||||
if((charge_percent < 100) && (!furi_hal_power_is_charging_done())) {
|
||||
charge_state = "charging";
|
||||
} else {
|
||||
charge_state = "charged";
|
||||
}
|
||||
} else {
|
||||
charge_state = "discharging";
|
||||
}
|
||||
|
||||
// Get misc info
|
||||
uint32_t uptime = furi_get_tick() / furi_kernel_get_tick_frequency();
|
||||
const Version* version = version_get();
|
||||
uint16_t major, minor;
|
||||
furi_hal_info_get_api_version(&major, &minor);
|
||||
|
||||
// Print ASCII art with info
|
||||
const size_t info_height = 16;
|
||||
for(size_t i = 0; i < MAX(logo_height, info_height); i++) {
|
||||
printf(NEOFETCH_COLOR "%-*s", logo_width, (i < logo_height) ? neofetch_logo[i] : "");
|
||||
switch(i) {
|
||||
case 0: // you@<hostname>
|
||||
printf("you" ANSI_RESET "@" NEOFETCH_COLOR "%s", furi_hal_version_get_name_ptr());
|
||||
break;
|
||||
case 1: // delimiter
|
||||
printf(ANSI_RESET "%s", delimiter);
|
||||
break;
|
||||
case 2: // OS: FURI <edition> <branch> <version> <commit> (SDK <maj>.<min>)
|
||||
printf(
|
||||
"OS" ANSI_RESET ": FURI %s %s %s %s (SDK %hu.%hu)",
|
||||
version_get_version(version),
|
||||
version_get_gitbranch(version),
|
||||
version_get_version(version),
|
||||
version_get_githash(version),
|
||||
major,
|
||||
minor);
|
||||
break;
|
||||
case 3: // Host: <model> <hostname>
|
||||
printf(
|
||||
"Host" ANSI_RESET ": %s %s",
|
||||
furi_hal_version_get_model_code(),
|
||||
furi_hal_version_get_device_name_ptr());
|
||||
break;
|
||||
case 4: // Kernel: FreeRTOS <maj>.<min>.<build>
|
||||
printf(
|
||||
"Kernel" ANSI_RESET ": FreeRTOS %d.%d.%d",
|
||||
tskKERNEL_VERSION_MAJOR,
|
||||
tskKERNEL_VERSION_MINOR,
|
||||
tskKERNEL_VERSION_BUILD);
|
||||
break;
|
||||
case 5: // Uptime: ?h?m?s
|
||||
printf(
|
||||
"Uptime" ANSI_RESET ": %luh%lum%lus",
|
||||
uptime / 60 / 60,
|
||||
uptime / 60 % 60,
|
||||
uptime % 60);
|
||||
break;
|
||||
case 6: // ST7567 128x64 @ 1 bpp in 1.4"
|
||||
printf("Display" ANSI_RESET ": ST7567 128x64 @ 1 bpp in 1.4\"");
|
||||
break;
|
||||
case 7: // DE: GuiSrv
|
||||
printf("DE" ANSI_RESET ": GuiSrv");
|
||||
break;
|
||||
case 8: // Shell: CliSrv
|
||||
printf("Shell" ANSI_RESET ": CliSrv");
|
||||
break;
|
||||
case 9: // CPU: STM32WB55RG @ 64 MHz
|
||||
printf("CPU" ANSI_RESET ": STM32WB55RG @ 64 MHz");
|
||||
break;
|
||||
case 10: // Memory: <used> / <total> B (??%)
|
||||
printf(
|
||||
"Memory" ANSI_RESET ": %zu / %zu B (%hu%%)", heap_used, heap_total, heap_percent);
|
||||
break;
|
||||
case 11: // Disk (/ext): <used> / <total> MiB (??%)
|
||||
printf(
|
||||
"Disk (/ext)" ANSI_RESET ": %llu / %llu MiB (%llu%%)",
|
||||
ext_used,
|
||||
ext_total,
|
||||
ext_percent);
|
||||
break;
|
||||
case 12: // Battery: ??% (<state>)
|
||||
printf("Battery" ANSI_RESET ": %hu%% (%s)" ANSI_RESET, charge_percent, charge_state);
|
||||
break;
|
||||
case 13: // empty space
|
||||
break;
|
||||
case 14: // Colors (line 1)
|
||||
for(size_t j = 30; j <= 37; j++)
|
||||
printf("\e[%dm███", j);
|
||||
break;
|
||||
case 15: // Colors (line 2)
|
||||
for(size_t j = 90; j <= 97; j++)
|
||||
printf("\e[%dm███", j);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
printf("\r\n");
|
||||
}
|
||||
printf(ANSI_RESET);
|
||||
#undef NEOFETCH_COLOR
|
||||
}
|
||||
|
||||
void cli_command_help(Cli* cli, FuriString* args, void* context) {
|
||||
UNUSED(context);
|
||||
printf("Commands available:");
|
||||
|
||||
// Command count
|
||||
const size_t commands_count = CliCommandTree_size(cli->commands);
|
||||
const size_t commands_count_mid = commands_count / 2 + commands_count % 2;
|
||||
// Count non-hidden commands
|
||||
CliCommandTree_it_t it_count;
|
||||
CliCommandTree_it(it_count, cli->commands);
|
||||
size_t commands_count = 0;
|
||||
while(!CliCommandTree_end_p(it_count)) {
|
||||
if(!(CliCommandTree_cref(it_count)->value_ptr->flags & CliCommandFlagHidden))
|
||||
commands_count++;
|
||||
CliCommandTree_next(it_count);
|
||||
}
|
||||
|
||||
// Use 2 iterators from start and middle to show 2 columns
|
||||
CliCommandTree_it_t it_left;
|
||||
CliCommandTree_it(it_left, cli->commands);
|
||||
CliCommandTree_it_t it_right;
|
||||
CliCommandTree_it(it_right, cli->commands);
|
||||
for(size_t i = 0; i < commands_count_mid; i++)
|
||||
CliCommandTree_next(it_right);
|
||||
// Create iterators starting at different positions
|
||||
const size_t columns = 3;
|
||||
const size_t commands_per_column = (commands_count / columns) + (commands_count % columns);
|
||||
CliCommandTree_it_t iterators[columns];
|
||||
for(size_t c = 0; c < columns; c++) {
|
||||
CliCommandTree_it(iterators[c], cli->commands);
|
||||
for(size_t i = 0; i < c * commands_per_column; i++)
|
||||
CliCommandTree_next(iterators[c]);
|
||||
}
|
||||
|
||||
// Iterate throw tree
|
||||
for(size_t i = 0; i < commands_count_mid; i++) {
|
||||
// Print commands
|
||||
for(size_t r = 0; r < commands_per_column; r++) {
|
||||
printf("\r\n");
|
||||
// Left Column
|
||||
if(!CliCommandTree_end_p(it_left)) {
|
||||
printf("%-30s", furi_string_get_cstr(*CliCommandTree_ref(it_left)->key_ptr));
|
||||
CliCommandTree_next(it_left);
|
||||
|
||||
for(size_t c = 0; c < columns; c++) {
|
||||
if(!CliCommandTree_end_p(iterators[c])) {
|
||||
const CliCommandTree_itref_t* item = CliCommandTree_cref(iterators[c]);
|
||||
if(!(item->value_ptr->flags & CliCommandFlagHidden)) {
|
||||
printf("%-30s", furi_string_get_cstr(*item->key_ptr));
|
||||
}
|
||||
CliCommandTree_next(iterators[c]);
|
||||
}
|
||||
}
|
||||
// Right Column
|
||||
if(!CliCommandTree_end_p(it_right)) {
|
||||
printf("%s", furi_string_get_cstr(*CliCommandTree_ref(it_right)->key_ptr));
|
||||
CliCommandTree_next(it_right);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if(furi_string_size(args) > 0) {
|
||||
cli_nl(cli);
|
||||
@@ -391,16 +552,18 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) {
|
||||
int interval = 1000;
|
||||
args_read_int_and_trim(args, &interval);
|
||||
|
||||
if(interval) printf("\e[2J\e[?25l"); // Clear display, hide cursor
|
||||
|
||||
FuriThreadList* thread_list = furi_thread_list_alloc();
|
||||
while(!cli_cmd_interrupt_received(cli)) {
|
||||
uint32_t tick = furi_get_tick();
|
||||
furi_thread_enumerate(thread_list);
|
||||
|
||||
if(interval) printf("\e[2J\e[0;0f"); // Clear display and return to 0
|
||||
if(interval) printf("\e[0;0f"); // Return to 0,0
|
||||
|
||||
uint32_t uptime = tick / furi_kernel_get_tick_frequency();
|
||||
printf(
|
||||
"Threads: %zu, ISR Time: %0.2f%%, Uptime: %luh%lum%lus\r\n",
|
||||
"\rThreads: %zu, ISR Time: %0.2f%%, Uptime: %luh%lum%lus\e[0K\r\n",
|
||||
furi_thread_list_size(thread_list),
|
||||
(double)furi_thread_list_get_isr_time(thread_list),
|
||||
uptime / 60 / 60,
|
||||
@@ -408,14 +571,14 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) {
|
||||
uptime % 60);
|
||||
|
||||
printf(
|
||||
"Heap: total %zu, free %zu, minimum %zu, max block %zu\r\n\r\n",
|
||||
"\rHeap: total %zu, free %zu, minimum %zu, max block %zu\e[0K\r\n\r\n",
|
||||
memmgr_get_total_heap(),
|
||||
memmgr_get_free_heap(),
|
||||
memmgr_get_minimum_free_heap(),
|
||||
memmgr_heap_get_max_free_block());
|
||||
|
||||
printf(
|
||||
"%-17s %-20s %-10s %5s %12s %6s %10s %7s %5s\r\n",
|
||||
"\r%-17s %-20s %-10s %5s %12s %6s %10s %7s %5s\e[0K\r\n",
|
||||
"AppID",
|
||||
"Name",
|
||||
"State",
|
||||
@@ -429,7 +592,7 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) {
|
||||
for(size_t i = 0; i < furi_thread_list_size(thread_list); i++) {
|
||||
const FuriThreadListItem* item = furi_thread_list_get_at(thread_list, i);
|
||||
printf(
|
||||
"%-17s %-20s %-10s %5d 0x%08lx %6lu %10lu %7zu %5.1f\r\n",
|
||||
"\r%-17s %-20s %-10s %5d 0x%08lx %6lu %10lu %7zu %5.1f\e[0K\r\n",
|
||||
item->app_id,
|
||||
item->name,
|
||||
item->state,
|
||||
@@ -448,6 +611,8 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) {
|
||||
}
|
||||
}
|
||||
furi_thread_list_free(thread_list);
|
||||
|
||||
if(interval) printf("\e[?25h"); // Show cursor
|
||||
}
|
||||
|
||||
void cli_command_free(Cli* cli, FuriString* args, void* context) {
|
||||
@@ -499,6 +664,12 @@ void cli_commands_init(Cli* cli) {
|
||||
cli_add_command(cli, "!", CliCommandFlagParallelSafe, cli_command_info, (void*)true);
|
||||
cli_add_command(cli, "info", CliCommandFlagParallelSafe, cli_command_info, NULL);
|
||||
cli_add_command(cli, "device_info", CliCommandFlagParallelSafe, cli_command_info, (void*)true);
|
||||
cli_add_command(
|
||||
cli,
|
||||
"neofetch",
|
||||
CliCommandFlagParallelSafe | CliCommandFlagHidden,
|
||||
cli_command_neofetch,
|
||||
NULL);
|
||||
|
||||
cli_add_command(cli, "?", CliCommandFlagParallelSafe, cli_command_help, NULL);
|
||||
cli_add_command(cli, "help", CliCommandFlagParallelSafe, cli_command_help, NULL);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <lib/toolbox/args.h>
|
||||
#include <cli/cli.h>
|
||||
#include <cli/cli_ansi.h>
|
||||
|
||||
void crypto_cli_print_usage(void) {
|
||||
printf("Usage:\r\n");
|
||||
@@ -45,14 +46,14 @@ void crypto_cli_encrypt(Cli* cli, FuriString* args) {
|
||||
input = furi_string_alloc();
|
||||
char c;
|
||||
while(cli_read(cli, (uint8_t*)&c, 1) == 1) {
|
||||
if(c == CliSymbolAsciiETX) {
|
||||
if(c == CliKeyETX) {
|
||||
printf("\r\n");
|
||||
break;
|
||||
} else if(c >= 0x20 && c < 0x7F) {
|
||||
putc(c, stdout);
|
||||
fflush(stdout);
|
||||
furi_string_push_back(input, c);
|
||||
} else if(c == CliSymbolAsciiCR) {
|
||||
} else if(c == CliKeyCR) {
|
||||
printf("\r\n");
|
||||
furi_string_cat(input, "\r\n");
|
||||
}
|
||||
@@ -120,14 +121,14 @@ void crypto_cli_decrypt(Cli* cli, FuriString* args) {
|
||||
hex_input = furi_string_alloc();
|
||||
char c;
|
||||
while(cli_read(cli, (uint8_t*)&c, 1) == 1) {
|
||||
if(c == CliSymbolAsciiETX) {
|
||||
if(c == CliKeyETX) {
|
||||
printf("\r\n");
|
||||
break;
|
||||
} else if(c >= 0x20 && c < 0x7F) {
|
||||
putc(c, stdout);
|
||||
fflush(stdout);
|
||||
furi_string_push_back(hex_input, c);
|
||||
} else if(c == CliSymbolAsciiCR) {
|
||||
} else if(c == CliKeyCR) {
|
||||
printf("\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,11 +54,14 @@ bool dialogs_app_process_module_file_browser(const DialogsAppMessageDataFileBrow
|
||||
ret = file_browser_context->result;
|
||||
|
||||
view_holder_set_view(view_holder, NULL);
|
||||
view_holder_free(view_holder);
|
||||
file_browser_stop(file_browser);
|
||||
|
||||
file_browser_free(file_browser);
|
||||
view_holder_free(view_holder);
|
||||
|
||||
api_lock_free(file_browser_context->lock);
|
||||
free(file_browser_context);
|
||||
|
||||
furi_record_close(RECORD_GUI);
|
||||
|
||||
return ret;
|
||||
|
||||
@@ -512,9 +512,21 @@ void canvas_draw_xbm(
|
||||
size_t height,
|
||||
const uint8_t* bitmap) {
|
||||
furi_check(canvas);
|
||||
canvas_draw_xbm_ex(canvas, x, y, width, height, IconRotation0, bitmap);
|
||||
}
|
||||
|
||||
void canvas_draw_xbm_ex(
|
||||
Canvas* canvas,
|
||||
int32_t x,
|
||||
int32_t y,
|
||||
size_t width,
|
||||
size_t height,
|
||||
IconRotation rotation,
|
||||
const uint8_t* bitmap_data) {
|
||||
furi_check(canvas);
|
||||
x += canvas->offset_x;
|
||||
y += canvas->offset_y;
|
||||
canvas_draw_u8g2_bitmap(&canvas->fb, x, y, width, height, bitmap, IconRotation0);
|
||||
canvas_draw_u8g2_bitmap(&canvas->fb, x, y, width, height, bitmap_data, rotation);
|
||||
}
|
||||
|
||||
void canvas_draw_glyph(Canvas* canvas, int32_t x, int32_t y, uint16_t ch) {
|
||||
|
||||
@@ -287,6 +287,25 @@ void canvas_draw_xbm(
|
||||
size_t height,
|
||||
const uint8_t* bitmap);
|
||||
|
||||
/** Draw rotated XBM bitmap
|
||||
*
|
||||
* @param canvas Canvas instance
|
||||
* @param x x coordinate
|
||||
* @param y y coordinate
|
||||
* @param[in] width bitmap width
|
||||
* @param[in] height bitmap height
|
||||
* @param[in] rotation bitmap rotation
|
||||
* @param bitmap_data pointer to XBM bitmap data
|
||||
*/
|
||||
void canvas_draw_xbm_ex(
|
||||
Canvas* canvas,
|
||||
int32_t x,
|
||||
int32_t y,
|
||||
size_t width,
|
||||
size_t height,
|
||||
IconRotation rotation,
|
||||
const uint8_t* bitmap_data);
|
||||
|
||||
/** Draw dot at x,y
|
||||
*
|
||||
* @param canvas Canvas instance
|
||||
|
||||
@@ -18,6 +18,7 @@ typedef struct {
|
||||
const char* header;
|
||||
char* text_buffer;
|
||||
size_t text_buffer_size;
|
||||
size_t minimum_length;
|
||||
bool clear_default_text;
|
||||
|
||||
TextInputCallback callback;
|
||||
@@ -321,7 +322,7 @@ static void text_input_handle_ok(TextInput* text_input, TextInputModel* model, b
|
||||
model->text_buffer, model->validator_text, model->validator_callback_context))) {
|
||||
model->validator_message_visible = true;
|
||||
furi_timer_start(text_input->timer, furi_kernel_get_tick_frequency() * 4);
|
||||
} else if(model->callback != 0 && text_length > 0) {
|
||||
} else if(model->callback != 0 && text_length >= model->minimum_length) {
|
||||
model->callback(model->callback_context);
|
||||
}
|
||||
} else if(selected == BACKSPACE_KEY) {
|
||||
@@ -487,6 +488,7 @@ void text_input_reset(TextInput* text_input) {
|
||||
model->header = "";
|
||||
model->selected_row = 0;
|
||||
model->selected_column = 0;
|
||||
model->minimum_length = 1;
|
||||
model->clear_default_text = false;
|
||||
model->text_buffer = NULL;
|
||||
model->text_buffer_size = 0;
|
||||
@@ -531,6 +533,14 @@ void text_input_set_result_callback(
|
||||
true);
|
||||
}
|
||||
|
||||
void text_input_set_minimum_length(TextInput* text_input, size_t minimum_length) {
|
||||
with_view_model(
|
||||
text_input->view,
|
||||
TextInputModel * model,
|
||||
{ model->minimum_length = minimum_length; },
|
||||
true);
|
||||
}
|
||||
|
||||
void text_input_set_validator(
|
||||
TextInput* text_input,
|
||||
TextInputValidatorCallback callback,
|
||||
|
||||
@@ -65,6 +65,13 @@ void text_input_set_result_callback(
|
||||
size_t text_buffer_size,
|
||||
bool clear_default_text);
|
||||
|
||||
/**
|
||||
* @brief Sets the minimum length of a TextInput
|
||||
* @param [in] text_input TextInput
|
||||
* @param [in] minimum_length Minimum input length
|
||||
*/
|
||||
void text_input_set_minimum_length(TextInput* text_input, size_t minimum_length);
|
||||
|
||||
void text_input_set_validator(
|
||||
TextInput* text_input,
|
||||
TextInputValidatorCallback callback,
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
#define VIEW_DISPATCHER_QUEUE_LEN (16U)
|
||||
|
||||
ViewDispatcher* view_dispatcher_alloc(void) {
|
||||
ViewDispatcher* dispatcher = view_dispatcher_alloc_ex(furi_event_loop_alloc());
|
||||
dispatcher->is_event_loop_owned = true;
|
||||
return dispatcher;
|
||||
}
|
||||
|
||||
ViewDispatcher* view_dispatcher_alloc_ex(FuriEventLoop* loop) {
|
||||
ViewDispatcher* view_dispatcher = malloc(sizeof(ViewDispatcher));
|
||||
|
||||
view_dispatcher->view_port = view_port_alloc();
|
||||
@@ -16,7 +22,7 @@ ViewDispatcher* view_dispatcher_alloc(void) {
|
||||
|
||||
ViewDict_init(view_dispatcher->views);
|
||||
|
||||
view_dispatcher->event_loop = furi_event_loop_alloc();
|
||||
view_dispatcher->event_loop = loop;
|
||||
|
||||
view_dispatcher->input_queue =
|
||||
furi_message_queue_alloc(VIEW_DISPATCHER_QUEUE_LEN, sizeof(InputEvent));
|
||||
@@ -57,7 +63,7 @@ void view_dispatcher_free(ViewDispatcher* view_dispatcher) {
|
||||
furi_message_queue_free(view_dispatcher->input_queue);
|
||||
furi_message_queue_free(view_dispatcher->event_queue);
|
||||
|
||||
furi_event_loop_free(view_dispatcher->event_loop);
|
||||
if(view_dispatcher->is_event_loop_owned) furi_event_loop_free(view_dispatcher->event_loop);
|
||||
// Free dispatcher
|
||||
free(view_dispatcher);
|
||||
}
|
||||
@@ -85,6 +91,7 @@ void view_dispatcher_set_tick_event_callback(
|
||||
ViewDispatcherTickEventCallback callback,
|
||||
uint32_t tick_period) {
|
||||
furi_check(view_dispatcher);
|
||||
furi_check(view_dispatcher->is_event_loop_owned);
|
||||
view_dispatcher->tick_event_callback = callback;
|
||||
view_dispatcher->tick_period = tick_period;
|
||||
}
|
||||
@@ -106,11 +113,12 @@ void view_dispatcher_run(ViewDispatcher* view_dispatcher) {
|
||||
uint32_t tick_period = view_dispatcher->tick_period == 0 ? FuriWaitForever :
|
||||
view_dispatcher->tick_period;
|
||||
|
||||
furi_event_loop_tick_set(
|
||||
view_dispatcher->event_loop,
|
||||
tick_period,
|
||||
view_dispatcher_handle_tick_event,
|
||||
view_dispatcher);
|
||||
if(view_dispatcher->is_event_loop_owned)
|
||||
furi_event_loop_tick_set(
|
||||
view_dispatcher->event_loop,
|
||||
tick_period,
|
||||
view_dispatcher_handle_tick_event,
|
||||
view_dispatcher);
|
||||
|
||||
furi_event_loop_run(view_dispatcher->event_loop);
|
||||
|
||||
|
||||
@@ -47,6 +47,15 @@ typedef void (*ViewDispatcherTickEventCallback)(void* context);
|
||||
*/
|
||||
ViewDispatcher* view_dispatcher_alloc(void);
|
||||
|
||||
/** Allocate ViewDispatcher instance with an externally owned event loop. If
|
||||
* this constructor is used instead of `view_dispatcher_alloc`, the burden of
|
||||
* freeing the event loop is placed on the caller.
|
||||
*
|
||||
* @param loop pointer to FuriEventLoop instance
|
||||
* @return pointer to ViewDispatcher instance
|
||||
*/
|
||||
ViewDispatcher* view_dispatcher_alloc_ex(FuriEventLoop* loop);
|
||||
|
||||
/** Free ViewDispatcher instance
|
||||
*
|
||||
* @warning All added views MUST be removed using view_dispatcher_remove_view()
|
||||
@@ -97,6 +106,10 @@ void view_dispatcher_set_navigation_event_callback(
|
||||
|
||||
/** Set tick event handler
|
||||
*
|
||||
* @warning Requires the event loop to be owned by the view dispatcher, i.e.
|
||||
* it should have been instantiated with `view_dispatcher_alloc`, not
|
||||
* `view_dispatcher_alloc_ex`.
|
||||
*
|
||||
* @param view_dispatcher ViewDispatcher instance
|
||||
* @param callback ViewDispatcherTickEventCallback
|
||||
* @param tick_period callback call period
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
DICT_DEF2(ViewDict, uint32_t, M_DEFAULT_OPLIST, View*, M_PTR_OPLIST) // NOLINT
|
||||
|
||||
struct ViewDispatcher {
|
||||
bool is_event_loop_owned;
|
||||
FuriEventLoop* event_loop;
|
||||
FuriMessageQueue* input_queue;
|
||||
FuriMessageQueue* event_queue;
|
||||
|
||||
@@ -67,7 +67,7 @@ static RpcSystemCallbacks rpc_systems[] = {
|
||||
struct RpcSession {
|
||||
Rpc* rpc;
|
||||
|
||||
FuriThreadId thread_id;
|
||||
FuriThread* thread;
|
||||
|
||||
RpcHandlerDict_t handlers;
|
||||
FuriStreamBuffer* stream;
|
||||
@@ -172,7 +172,7 @@ size_t rpc_session_feed(
|
||||
|
||||
size_t bytes_sent = furi_stream_buffer_send(session->stream, encoded_bytes, size, timeout);
|
||||
|
||||
furi_thread_flags_set(session->thread_id, RpcEvtNewData);
|
||||
furi_thread_flags_set(furi_thread_get_id(session->thread), RpcEvtNewData);
|
||||
|
||||
return bytes_sent;
|
||||
}
|
||||
@@ -220,7 +220,7 @@ bool rpc_pb_stream_read(pb_istream_t* istream, pb_byte_t* buf, size_t count) {
|
||||
break;
|
||||
} else {
|
||||
/* Save disconnect flag and continue reading buffer */
|
||||
furi_thread_flags_set(session->thread_id, RpcEvtDisconnect);
|
||||
furi_thread_flags_set(furi_thread_get_id(session->thread), RpcEvtDisconnect);
|
||||
}
|
||||
} else if(flags & RpcEvtNewData) {
|
||||
// Just wake thread up
|
||||
@@ -347,32 +347,37 @@ static int32_t rpc_session_worker(void* context) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void rpc_session_thread_release_callback(
|
||||
FuriThread* thread,
|
||||
FuriThreadState thread_state,
|
||||
void* context) {
|
||||
if(thread_state == FuriThreadStateStopped) {
|
||||
RpcSession* session = (RpcSession*)context;
|
||||
static void rpc_session_thread_pending_callback(void* context, uint32_t arg) {
|
||||
UNUSED(arg);
|
||||
RpcSession* session = (RpcSession*)context;
|
||||
|
||||
for(size_t i = 0; i < COUNT_OF(rpc_systems); ++i) {
|
||||
if(rpc_systems[i].free) {
|
||||
(rpc_systems[i].free)(session->system_contexts[i]);
|
||||
}
|
||||
for(size_t i = 0; i < COUNT_OF(rpc_systems); ++i) {
|
||||
if(rpc_systems[i].free) {
|
||||
(rpc_systems[i].free)(session->system_contexts[i]);
|
||||
}
|
||||
free(session->system_contexts);
|
||||
free(session->decoded_message);
|
||||
RpcHandlerDict_clear(session->handlers);
|
||||
furi_stream_buffer_free(session->stream);
|
||||
}
|
||||
free(session->system_contexts);
|
||||
free(session->decoded_message);
|
||||
RpcHandlerDict_clear(session->handlers);
|
||||
furi_stream_buffer_free(session->stream);
|
||||
|
||||
furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever);
|
||||
if(session->terminated_callback) {
|
||||
session->terminated_callback(session->context);
|
||||
}
|
||||
furi_mutex_release(session->callbacks_mutex);
|
||||
furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever);
|
||||
if(session->terminated_callback) {
|
||||
session->terminated_callback(session->context);
|
||||
}
|
||||
furi_mutex_release(session->callbacks_mutex);
|
||||
|
||||
furi_mutex_free(session->callbacks_mutex);
|
||||
furi_thread_free(thread);
|
||||
free(session);
|
||||
furi_mutex_free(session->callbacks_mutex);
|
||||
furi_thread_join(session->thread);
|
||||
furi_thread_free(session->thread);
|
||||
free(session);
|
||||
}
|
||||
|
||||
static void
|
||||
rpc_session_thread_state_callback(FuriThread* thread, FuriThreadState state, void* context) {
|
||||
UNUSED(thread);
|
||||
if(state == FuriThreadStateStopped) {
|
||||
furi_timer_pending_callback(rpc_session_thread_pending_callback, context, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,14 +409,12 @@ RpcSession* rpc_session_open(Rpc* rpc, RpcOwner owner) {
|
||||
};
|
||||
rpc_add_handler(session, PB_Main_stop_session_tag, &rpc_handler);
|
||||
|
||||
FuriThread* thread =
|
||||
furi_thread_alloc_ex("RpcSessionWorker", 3072, rpc_session_worker, session);
|
||||
session->thread_id = furi_thread_get_id(thread);
|
||||
session->thread = furi_thread_alloc_ex("RpcSessionWorker", 3072, rpc_session_worker, session);
|
||||
|
||||
furi_thread_set_state_context(thread, session);
|
||||
furi_thread_set_state_callback(thread, rpc_session_thread_release_callback);
|
||||
furi_thread_set_state_context(session->thread, session);
|
||||
furi_thread_set_state_callback(session->thread, rpc_session_thread_state_callback);
|
||||
|
||||
furi_thread_start(thread);
|
||||
furi_thread_start(session->thread);
|
||||
|
||||
return session;
|
||||
}
|
||||
@@ -423,7 +426,7 @@ void rpc_session_close(RpcSession* session) {
|
||||
rpc_session_set_send_bytes_callback(session, NULL);
|
||||
rpc_session_set_close_callback(session, NULL);
|
||||
rpc_session_set_buffer_is_empty_callback(session, NULL);
|
||||
furi_thread_flags_set(session->thread_id, RpcEvtDisconnect);
|
||||
furi_thread_flags_set(furi_thread_get_id(session->thread), RpcEvtDisconnect);
|
||||
}
|
||||
|
||||
void rpc_on_system_start(void* p) {
|
||||
|
||||
@@ -377,7 +377,7 @@ void storage_common_resolve_path_and_ensure_app_directory(Storage* storage, Furi
|
||||
* @param storage pointer to a storage API instance.
|
||||
* @param source pointer to a zero-terminated string containing the source path.
|
||||
* @param dest pointer to a zero-terminated string containing the destination path.
|
||||
* @return FSE_OK if the migration was successfull completed, any other error code on failure.
|
||||
* @return FSE_OK if the migration was successfully completed, any other error code on failure.
|
||||
*/
|
||||
FS_Error storage_common_migrate(Storage* storage, const char* source, const char* dest);
|
||||
|
||||
@@ -425,7 +425,7 @@ bool storage_common_is_subdir(Storage* storage, const char* parent, const char*
|
||||
/******************* Error Functions *******************/
|
||||
|
||||
/**
|
||||
* @brief Get the textual description of a numeric error identifer.
|
||||
* @brief Get the textual description of a numeric error identifier.
|
||||
*
|
||||
* @param error_id numeric identifier of the error in question.
|
||||
* @return pointer to a statically allocated zero-terminated string containing the respective error text.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <furi_hal.h>
|
||||
|
||||
#include <cli/cli.h>
|
||||
#include <cli/cli_ansi.h>
|
||||
#include <lib/toolbox/args.h>
|
||||
#include <lib/toolbox/dir_walk.h>
|
||||
#include <lib/toolbox/md5_calc.h>
|
||||
@@ -224,7 +225,7 @@ static void storage_cli_write(Cli* cli, FuriString* path, FuriString* args) {
|
||||
while(true) {
|
||||
uint8_t symbol = cli_getc(cli);
|
||||
|
||||
if(symbol == CliSymbolAsciiETX) {
|
||||
if(symbol == CliKeyETX) {
|
||||
size_t write_size = read_index % buffer_size;
|
||||
|
||||
if(write_size > 0) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#define TAG "HidMouseClicker"
|
||||
|
||||
#define DEFAULT_CLICK_RATE 1
|
||||
#define MAXIMUM_CLICK_RATE 60
|
||||
#define MAXIMUM_CLICK_RATE 100
|
||||
|
||||
struct HidMouseClicker {
|
||||
View* view;
|
||||
@@ -34,7 +34,9 @@ static void hid_mouse_clicker_start_or_restart_timer(void* context) {
|
||||
HidMouseClickerModel * model,
|
||||
{
|
||||
furi_timer_start(
|
||||
hid_mouse_clicker->timer, furi_kernel_get_tick_frequency() / model->rate);
|
||||
hid_mouse_clicker->timer,
|
||||
furi_kernel_get_tick_frequency() /
|
||||
((model->rate) ? model->rate : MAXIMUM_CLICK_RATE));
|
||||
},
|
||||
true);
|
||||
}
|
||||
@@ -75,7 +77,11 @@ static void hid_mouse_clicker_draw_callback(Canvas* canvas, void* context) {
|
||||
|
||||
// Clicks/s
|
||||
char label[20];
|
||||
snprintf(label, sizeof(label), "%d clicks/s", model->rate);
|
||||
if(model->rate) {
|
||||
snprintf(label, sizeof(label), "%d clicks/s", model->rate);
|
||||
} else {
|
||||
snprintf(label, sizeof(label), "max clicks/s");
|
||||
}
|
||||
elements_multiline_text_aligned(canvas, 28, 37, AlignCenter, AlignBottom, label);
|
||||
|
||||
canvas_draw_icon(canvas, 25, 20, &I_ButtonUp_7x4);
|
||||
@@ -139,7 +145,7 @@ static bool hid_mouse_clicker_input_callback(InputEvent* event, void* context) {
|
||||
consumed = true;
|
||||
break;
|
||||
case InputKeyDown:
|
||||
if(model->rate > 1) {
|
||||
if(model->rate > 0) {
|
||||
model->rate--;
|
||||
}
|
||||
rate_changed = true;
|
||||
|
||||
@@ -16,11 +16,70 @@ App(
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_dialog",
|
||||
appid="js_event_loop",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_dialog_ep",
|
||||
entry_point="js_event_loop_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_dialog.c"],
|
||||
sources=[
|
||||
"modules/js_event_loop/js_event_loop.c",
|
||||
"modules/js_event_loop/js_event_loop_api_table.cpp",
|
||||
],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_gui",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_gui_ep",
|
||||
requires=["js_app", "js_event_loop"],
|
||||
sources=["modules/js_gui/js_gui.c", "modules/js_gui/js_gui_api_table.cpp"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_gui__loading",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_view_loading_ep",
|
||||
requires=["js_app", "js_gui", "js_event_loop"],
|
||||
sources=["modules/js_gui/loading.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_gui__empty_screen",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_view_empty_screen_ep",
|
||||
requires=["js_app", "js_gui", "js_event_loop"],
|
||||
sources=["modules/js_gui/empty_screen.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_gui__submenu",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_view_submenu_ep",
|
||||
requires=["js_app", "js_gui"],
|
||||
sources=["modules/js_gui/submenu.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_gui__text_input",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_view_text_input_ep",
|
||||
requires=["js_app", "js_gui", "js_event_loop"],
|
||||
sources=["modules/js_gui/text_input.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_gui__text_box",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_view_text_box_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_gui/text_box.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_gui__dialog",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_view_dialog_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_gui/dialog.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
@@ -48,11 +107,11 @@ App(
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_submenu",
|
||||
appid="js_gpio",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_submenu_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_submenu.c"],
|
||||
entry_point="js_gpio_ep",
|
||||
requires=["js_app", "js_event_loop"],
|
||||
sources=["modules/js_gpio.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
@@ -64,9 +123,9 @@ App(
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_textbox",
|
||||
appid="js_storage",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_textbox_ep",
|
||||
entry_point="js_storage_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_textbox.c"],
|
||||
sources=["modules/js_storage.c"],
|
||||
)
|
||||
|
||||
@@ -1,33 +1,58 @@
|
||||
let badusb = require("badusb");
|
||||
let notify = require("notification");
|
||||
let flipper = require("flipper");
|
||||
let dialog = require("dialog");
|
||||
let eventLoop = require("event_loop");
|
||||
let gui = require("gui");
|
||||
let dialog = require("gui/dialog");
|
||||
|
||||
badusb.setup({ vid: 0xAAAA, pid: 0xBBBB, mfr_name: "Flipper", prod_name: "Zero" });
|
||||
dialog.message("BadUSB demo", "Press OK to start");
|
||||
let views = {
|
||||
dialog: dialog.makeWith({
|
||||
header: "BadUSB demo",
|
||||
text: "Press OK to start",
|
||||
center: "Start",
|
||||
}),
|
||||
};
|
||||
|
||||
if (badusb.isConnected()) {
|
||||
notify.blink("green", "short");
|
||||
print("USB is connected");
|
||||
badusb.setup({ vid: 0xAAAA, pid: 0xBBBB, mfrName: "Flipper", prodName: "Zero" });
|
||||
|
||||
badusb.println("Hello, world!");
|
||||
eventLoop.subscribe(views.dialog.input, function (_sub, button, eventLoop, gui) {
|
||||
if (button !== "center")
|
||||
return;
|
||||
|
||||
badusb.press("CTRL", "a");
|
||||
badusb.press("CTRL", "c");
|
||||
badusb.press("DOWN");
|
||||
delay(1000);
|
||||
badusb.press("CTRL", "v");
|
||||
delay(1000);
|
||||
badusb.press("CTRL", "v");
|
||||
gui.viewDispatcher.sendTo("back");
|
||||
|
||||
badusb.println("1234", 200);
|
||||
if (badusb.isConnected()) {
|
||||
notify.blink("green", "short");
|
||||
print("USB is connected");
|
||||
|
||||
badusb.println("Flipper Model: " + flipper.getModel());
|
||||
badusb.println("Flipper Name: " + flipper.getName());
|
||||
badusb.println("Battery level: " + to_string(flipper.getBatteryCharge()) + "%");
|
||||
badusb.println("Hello, world!");
|
||||
|
||||
notify.success();
|
||||
} else {
|
||||
print("USB not connected");
|
||||
notify.error();
|
||||
}
|
||||
badusb.press("CTRL", "a");
|
||||
badusb.press("CTRL", "c");
|
||||
badusb.press("DOWN");
|
||||
delay(1000);
|
||||
badusb.press("CTRL", "v");
|
||||
delay(1000);
|
||||
badusb.press("CTRL", "v");
|
||||
|
||||
badusb.println("1234", 200);
|
||||
|
||||
badusb.println("Flipper Model: " + flipper.getModel());
|
||||
badusb.println("Flipper Name: " + flipper.getName());
|
||||
badusb.println("Battery level: " + toString(flipper.getBatteryCharge()) + "%");
|
||||
|
||||
notify.success();
|
||||
} else {
|
||||
print("USB not connected");
|
||||
notify.error();
|
||||
}
|
||||
|
||||
eventLoop.stop();
|
||||
}, eventLoop, gui);
|
||||
|
||||
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _item, eventLoop) {
|
||||
eventLoop.stop();
|
||||
}, eventLoop);
|
||||
|
||||
gui.viewDispatcher.switchTo(views.dialog);
|
||||
eventLoop.run();
|
||||
|
||||
@@ -6,4 +6,4 @@ print("2");
|
||||
delay(1000)
|
||||
print("3");
|
||||
delay(1000)
|
||||
print("end");
|
||||
print("end");
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
let dialog = require("dialog");
|
||||
|
||||
let result1 = dialog.message("Dialog demo", "Press OK to start");
|
||||
print(result1);
|
||||
|
||||
let dialog_params = ({
|
||||
header: "Test_header",
|
||||
text: "Test_text",
|
||||
button_left: "Left",
|
||||
button_right: "Right",
|
||||
button_center: "OK"
|
||||
});
|
||||
|
||||
let result2 = dialog.custom(dialog_params);
|
||||
if (result2 === "") {
|
||||
print("Back is pressed");
|
||||
} else {
|
||||
print(result2, "is pressed");
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
let eventLoop = require("event_loop");
|
||||
|
||||
// print a string after 1337 milliseconds
|
||||
eventLoop.subscribe(eventLoop.timer("oneshot", 1337), function (_subscription, _item) {
|
||||
print("Hi after 1337 ms");
|
||||
});
|
||||
|
||||
// count up to 5 with a delay of 100ms between increments
|
||||
eventLoop.subscribe(eventLoop.timer("periodic", 100), function (subscription, _item, counter) {
|
||||
print("Counter two:", counter);
|
||||
if (counter === 5)
|
||||
subscription.cancel();
|
||||
return [counter + 1];
|
||||
}, 0);
|
||||
|
||||
// count up to 15 with a delay of 100ms between increments
|
||||
// and stop the program when the count reaches 15
|
||||
eventLoop.subscribe(eventLoop.timer("periodic", 100), function (subscription, _item, event_loop, counter) {
|
||||
print("Counter one:", counter);
|
||||
if (counter === 15)
|
||||
event_loop.stop();
|
||||
return [event_loop, counter + 1];
|
||||
}, eventLoop, 0);
|
||||
|
||||
eventLoop.run();
|
||||
57
applications/system/js_app/examples/apps/Scripts/gpio.js
Normal file
57
applications/system/js_app/examples/apps/Scripts/gpio.js
Normal file
@@ -0,0 +1,57 @@
|
||||
let eventLoop = require("event_loop");
|
||||
let gpio = require("gpio");
|
||||
|
||||
// initialize pins
|
||||
let led = gpio.get("pc3"); // same as `gpio.get(7)`
|
||||
let pot = gpio.get("pc0"); // same as `gpio.get(16)`
|
||||
let button = gpio.get("pc1"); // same as `gpio.get(15)`
|
||||
led.init({ direction: "out", outMode: "push_pull" });
|
||||
pot.init({ direction: "in", inMode: "analog" });
|
||||
button.init({ direction: "in", pull: "up", inMode: "interrupt", edge: "falling" });
|
||||
|
||||
// blink led
|
||||
print("Commencing blinking (PC3)");
|
||||
eventLoop.subscribe(eventLoop.timer("periodic", 1000), function (_, _item, led, state) {
|
||||
led.write(state);
|
||||
return [led, !state];
|
||||
}, led, true);
|
||||
|
||||
// read potentiometer when button is pressed
|
||||
print("Press the button (PC1)");
|
||||
eventLoop.subscribe(button.interrupt(), function (_, _item, pot) {
|
||||
print("PC0 is at", pot.read_analog(), "mV");
|
||||
}, pot);
|
||||
|
||||
// the program will just exit unless this is here
|
||||
eventLoop.run();
|
||||
|
||||
// possible pins https://docs.flipper.net/gpio-and-modules#miFsS
|
||||
// "PA7" aka 2
|
||||
// "PA6" aka 3
|
||||
// "PA4" aka 4
|
||||
// "PB3" aka 5
|
||||
// "PB2" aka 6
|
||||
// "PC3" aka 7
|
||||
// "PA14" aka 10
|
||||
// "PA13" aka 12
|
||||
// "PB6" aka 13
|
||||
// "PB7" aka 14
|
||||
// "PC1" aka 15
|
||||
// "PC0" aka 16
|
||||
// "PB14" aka 17
|
||||
|
||||
// possible modes
|
||||
// { direction: "out", outMode: "push_pull" }
|
||||
// { direction: "out", outMode: "open_drain" }
|
||||
// { direction: "out", outMode: "push_pull", altFn: true }
|
||||
// { direction: "out", outMode: "open_drain", altFn: true }
|
||||
// { direction: "in", inMode: "analog" }
|
||||
// { direction: "in", inMode: "plain_digital" }
|
||||
// { direction: "in", inMode: "interrupt", edge: "rising" }
|
||||
// { direction: "in", inMode: "interrupt", edge: "falling" }
|
||||
// { direction: "in", inMode: "interrupt", edge: "both" }
|
||||
// { direction: "in", inMode: "event", edge: "rising" }
|
||||
// { direction: "in", inMode: "event", edge: "falling" }
|
||||
// { direction: "in", inMode: "event", edge: "both" }
|
||||
// all variants support an optional `pull` field which can either be undefined,
|
||||
// "up" or "down"
|
||||
77
applications/system/js_app/examples/apps/Scripts/gui.js
Normal file
77
applications/system/js_app/examples/apps/Scripts/gui.js
Normal file
@@ -0,0 +1,77 @@
|
||||
// import modules
|
||||
let eventLoop = require("event_loop");
|
||||
let gui = require("gui");
|
||||
let loadingView = require("gui/loading");
|
||||
let submenuView = require("gui/submenu");
|
||||
let emptyView = require("gui/empty_screen");
|
||||
let textInputView = require("gui/text_input");
|
||||
let textBoxView = require("gui/text_box");
|
||||
let dialogView = require("gui/dialog");
|
||||
|
||||
// declare view instances
|
||||
let views = {
|
||||
loading: loadingView.make(),
|
||||
empty: emptyView.make(),
|
||||
keyboard: textInputView.makeWith({
|
||||
header: "Enter your name",
|
||||
minLength: 0,
|
||||
maxLength: 32,
|
||||
}),
|
||||
helloDialog: dialogView.makeWith({
|
||||
center: "Hi Flipper! :)",
|
||||
}),
|
||||
longText: textBoxView.makeWith({
|
||||
text: "This is a very long string that demonstrates the TextBox view. Use the D-Pad to scroll backwards and forwards.\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rhoncus est malesuada quam egestas ultrices. Maecenas non eros a nulla eleifend vulputate et ut risus. Quisque in mauris mattis, venenatis risus eget, aliquam diam. Fusce pretium feugiat mauris, ut faucibus ex volutpat in. Phasellus volutpat ex sed gravida consectetur. Aliquam sed lectus feugiat, tristique lectus et, bibendum lacus. Ut sit amet augue eu sapien elementum aliquam quis vitae tortor. Vestibulum quis commodo odio. In elementum fermentum massa, eu pellentesque nibh cursus at. Integer eleifend lacus nec purus elementum sodales. Nulla elementum neque urna, non vulputate massa semper sed. Fusce ut nisi vitae dui blandit congue pretium vitae turpis.",
|
||||
}),
|
||||
demos: submenuView.makeWith({
|
||||
header: "Choose a demo",
|
||||
items: [
|
||||
"Hourglass screen",
|
||||
"Empty screen",
|
||||
"Text input & Dialog",
|
||||
"Text box",
|
||||
"Exit app",
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
// demo selector
|
||||
eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, views) {
|
||||
if (index === 0) {
|
||||
gui.viewDispatcher.switchTo(views.loading);
|
||||
// the loading view captures all back events, preventing our navigation callback from firing
|
||||
// switch to the demo chooser after a second
|
||||
eventLoop.subscribe(eventLoop.timer("oneshot", 1000), function (_sub, _, gui, views) {
|
||||
gui.viewDispatcher.switchTo(views.demos);
|
||||
}, gui, views);
|
||||
} else if (index === 1) {
|
||||
gui.viewDispatcher.switchTo(views.empty);
|
||||
} else if (index === 2) {
|
||||
gui.viewDispatcher.switchTo(views.keyboard);
|
||||
} else if (index === 3) {
|
||||
gui.viewDispatcher.switchTo(views.longText);
|
||||
} else if (index === 4) {
|
||||
eventLoop.stop();
|
||||
}
|
||||
}, gui, eventLoop, views);
|
||||
|
||||
// say hi after keyboard input
|
||||
eventLoop.subscribe(views.keyboard.input, function (_sub, name, gui, views) {
|
||||
views.helloDialog.set("text", "Hi " + name + "! :)");
|
||||
gui.viewDispatcher.switchTo(views.helloDialog);
|
||||
}, gui, views);
|
||||
|
||||
// go back after the greeting dialog
|
||||
eventLoop.subscribe(views.helloDialog.input, function (_sub, button, gui, views) {
|
||||
if (button === "center")
|
||||
gui.viewDispatcher.switchTo(views.demos);
|
||||
}, gui, views);
|
||||
|
||||
// go to the demo chooser screen when the back key is pressed
|
||||
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, gui, views) {
|
||||
gui.viewDispatcher.switchTo(views.demos);
|
||||
}, gui, views);
|
||||
|
||||
// run UI
|
||||
gui.viewDispatcher.switchTo(views.demos);
|
||||
eventLoop.run();
|
||||
@@ -1,3 +1,3 @@
|
||||
let math = load("/ext/apps/Scripts/load_api.js");
|
||||
let result = math.add(5, 10);
|
||||
print(result);
|
||||
print(result);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
({
|
||||
add: function (a, b) { return a + b; },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -22,48 +22,3 @@ print("math.sign(-5):", math.sign(-5));
|
||||
print("math.sin(math.PI/2):", math.sin(math.PI / 2));
|
||||
print("math.sqrt(25):", math.sqrt(25));
|
||||
print("math.trunc(5.7):", math.trunc(5.7));
|
||||
|
||||
// Unit tests. Please add more if you have time and knowledge.
|
||||
// math.EPSILON on Flipper Zero is 2.22044604925031308085e-16
|
||||
|
||||
let succeeded = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(text, result, expected, epsilon) {
|
||||
let is_equal = math.is_equal(result, expected, epsilon);
|
||||
if (is_equal) {
|
||||
succeeded += 1;
|
||||
} else {
|
||||
failed += 1;
|
||||
print(text, "expected", expected, "got", result);
|
||||
}
|
||||
}
|
||||
|
||||
test("math.abs(5)", math.abs(-5), 5, math.EPSILON);
|
||||
test("math.abs(0.5)", math.abs(-0.5), 0.5, math.EPSILON);
|
||||
test("math.abs(5)", math.abs(5), 5, math.EPSILON);
|
||||
test("math.abs(-0.5)", math.abs(0.5), 0.5, math.EPSILON);
|
||||
test("math.acos(0.5)", math.acos(0.5), 1.0471975511965976, math.EPSILON);
|
||||
test("math.acosh(2)", math.acosh(2), 1.3169578969248166, math.EPSILON);
|
||||
test("math.asin(0.5)", math.asin(0.5), 0.5235987755982988, math.EPSILON);
|
||||
test("math.asinh(2)", math.asinh(2), 1.4436354751788103, math.EPSILON);
|
||||
test("math.atan(1)", math.atan(1), 0.7853981633974483, math.EPSILON);
|
||||
test("math.atan2(1, 1)", math.atan2(1, 1), 0.7853981633974483, math.EPSILON);
|
||||
test("math.atanh(0.5)", math.atanh(0.5), 0.5493061443340549, math.EPSILON);
|
||||
test("math.cbrt(27)", math.cbrt(27), 3, math.EPSILON);
|
||||
test("math.ceil(5.3)", math.ceil(5.3), 6, math.EPSILON);
|
||||
test("math.clz32(1)", math.clz32(1), 31, math.EPSILON);
|
||||
test("math.floor(5.7)", math.floor(5.7), 5, math.EPSILON);
|
||||
test("math.max(3, 5)", math.max(3, 5), 5, math.EPSILON);
|
||||
test("math.min(3, 5)", math.min(3, 5), 3, math.EPSILON);
|
||||
test("math.pow(2, 3)", math.pow(2, 3), 8, math.EPSILON);
|
||||
test("math.sign(-5)", math.sign(-5), -1, math.EPSILON);
|
||||
test("math.sqrt(25)", math.sqrt(25), 5, math.EPSILON);
|
||||
test("math.trunc(5.7)", math.trunc(5.7), 5, math.EPSILON);
|
||||
test("math.cos(math.PI)", math.cos(math.PI), -1, math.EPSILON * 18); // Error 3.77475828372553223744e-15
|
||||
test("math.exp(1)", math.exp(1), 2.718281828459045, math.EPSILON * 2); // Error 4.44089209850062616169e-16
|
||||
test("math.sin(math.PI / 2)", math.sin(math.PI / 2), 1, math.EPSILON * 4.5); // Error 9.99200722162640886381e-16
|
||||
|
||||
if (failed > 0) {
|
||||
print("!!!", failed, "Unit tests failed !!!");
|
||||
}
|
||||
@@ -6,4 +6,4 @@ delay(1000);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
notify.blink("red", "short");
|
||||
delay(500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
let submenu = require("submenu");
|
||||
|
||||
submenu.addItem("Item 1", 0);
|
||||
submenu.addItem("Item 2", 1);
|
||||
submenu.addItem("Item 3", 2);
|
||||
|
||||
submenu.setHeader("Select an option:");
|
||||
|
||||
let result = submenu.show();
|
||||
// Returns undefined when pressing back
|
||||
print("Result:", result);
|
||||
@@ -1,30 +0,0 @@
|
||||
let textbox = require("textbox");
|
||||
|
||||
// You should set config before adding text
|
||||
// Focus (start / end), Font (text / hex)
|
||||
textbox.setConfig("end", "text");
|
||||
|
||||
// Can make sure it's cleared before showing, in case of reusing in same script
|
||||
// (Closing textbox already clears the text, but maybe you added more in a loop for example)
|
||||
textbox.clearText();
|
||||
|
||||
// Add default text
|
||||
textbox.addText("Example dynamic updating textbox\n");
|
||||
|
||||
// Non-blocking, can keep updating text after, can close in JS or in GUI
|
||||
textbox.show();
|
||||
|
||||
let i = 0;
|
||||
while (textbox.isOpen() && i < 20) {
|
||||
print("console", i++);
|
||||
|
||||
// Add text to textbox buffer
|
||||
textbox.addText("textbox " + to_string(i) + "\n");
|
||||
|
||||
delay(500);
|
||||
}
|
||||
|
||||
// If not closed by user (instead i < 20 is false above), close forcefully
|
||||
if (textbox.isOpen()) {
|
||||
textbox.close();
|
||||
}
|
||||
@@ -6,6 +6,6 @@ while (1) {
|
||||
if (rx_data !== undefined) {
|
||||
serial.write(rx_data);
|
||||
let data_view = Uint8Array(rx_data);
|
||||
print("0x" + to_hex_string(data_view[0]));
|
||||
print("0x" + toString(data_view[0], 16));
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,7 @@ int32_t js_app(void* arg) {
|
||||
FuriString* start_text =
|
||||
furi_string_alloc_printf("Running %s", furi_string_get_cstr(name));
|
||||
console_view_print(app->console_view, furi_string_get_cstr(start_text));
|
||||
console_view_print(app->console_view, "------------");
|
||||
console_view_print(app->console_view, "-------------");
|
||||
furi_string_free(name);
|
||||
furi_string_free(start_text);
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
#include <core/common_defines.h>
|
||||
#include "js_modules.h"
|
||||
#include <m-dict.h>
|
||||
#include <m-array.h>
|
||||
|
||||
#include "modules/js_flipper.h"
|
||||
#ifdef FW_CFG_unit_tests
|
||||
#include "modules/js_tests.h"
|
||||
#endif
|
||||
|
||||
#define TAG "JS modules"
|
||||
|
||||
@@ -9,54 +13,72 @@
|
||||
#define MODULES_PATH "/ext/apps_data/js_app/plugins"
|
||||
|
||||
typedef struct {
|
||||
JsModeConstructor create;
|
||||
JsModeDestructor destroy;
|
||||
FuriString* name;
|
||||
const JsModuleConstructor create;
|
||||
const JsModuleDestructor destroy;
|
||||
void* context;
|
||||
} JsModuleData;
|
||||
|
||||
DICT_DEF2(JsModuleDict, FuriString*, FURI_STRING_OPLIST, JsModuleData, M_POD_OPLIST);
|
||||
// not using:
|
||||
// - a dict because ordering is required
|
||||
// - a bptree because it forces a sorted ordering
|
||||
// - an rbtree because i deemed it more tedious to implement, and with the
|
||||
// amount of modules in use (under 10 in the overwhelming majority of cases)
|
||||
// i bet it's going to be slower than a plain array
|
||||
ARRAY_DEF(JsModuleArray, JsModuleData, M_POD_OPLIST);
|
||||
#define M_OPL_JsModuleArray_t() ARRAY_OPLIST(JsModuleArray)
|
||||
|
||||
static const JsModuleDescriptor modules_builtin[] = {
|
||||
{"flipper", js_flipper_create, NULL},
|
||||
{"flipper", js_flipper_create, NULL, NULL},
|
||||
#ifdef FW_CFG_unit_tests
|
||||
{"tests", js_tests_create, NULL, NULL},
|
||||
#endif
|
||||
};
|
||||
|
||||
struct JsModules {
|
||||
struct mjs* mjs;
|
||||
JsModuleDict_t module_dict;
|
||||
JsModuleArray_t modules;
|
||||
PluginManager* plugin_manager;
|
||||
CompositeApiResolver* resolver;
|
||||
};
|
||||
|
||||
JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver) {
|
||||
JsModules* modules = malloc(sizeof(JsModules));
|
||||
modules->mjs = mjs;
|
||||
JsModuleDict_init(modules->module_dict);
|
||||
JsModuleArray_init(modules->modules);
|
||||
|
||||
modules->plugin_manager = plugin_manager_alloc(
|
||||
PLUGIN_APP_ID, PLUGIN_API_VERSION, composite_api_resolver_get(resolver));
|
||||
|
||||
modules->resolver = resolver;
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
void js_modules_destroy(JsModules* modules) {
|
||||
JsModuleDict_it_t it;
|
||||
for(JsModuleDict_it(it, modules->module_dict); !JsModuleDict_end_p(it);
|
||||
JsModuleDict_next(it)) {
|
||||
const JsModuleDict_itref_t* module_itref = JsModuleDict_cref(it);
|
||||
if(module_itref->value.destroy) {
|
||||
module_itref->value.destroy(module_itref->value.context);
|
||||
void js_modules_destroy(JsModules* instance) {
|
||||
for
|
||||
M_EACH(module, instance->modules, JsModuleArray_t) {
|
||||
FURI_LOG_T(TAG, "Tearing down %s", furi_string_get_cstr(module->name));
|
||||
if(module->destroy) module->destroy(module->context);
|
||||
furi_string_free(module->name);
|
||||
}
|
||||
}
|
||||
plugin_manager_free(modules->plugin_manager);
|
||||
JsModuleDict_clear(modules->module_dict);
|
||||
free(modules);
|
||||
plugin_manager_free(instance->plugin_manager);
|
||||
JsModuleArray_clear(instance->modules);
|
||||
free(instance);
|
||||
}
|
||||
|
||||
JsModuleData* js_find_loaded_module(JsModules* instance, const char* name) {
|
||||
for
|
||||
M_EACH(module, instance->modules, JsModuleArray_t) {
|
||||
if(furi_string_cmp_str(module->name, name) == 0) return module;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_len) {
|
||||
FuriString* module_name = furi_string_alloc_set_str(name);
|
||||
// Check if module is already installed
|
||||
JsModuleData* module_inst = JsModuleDict_get(modules->module_dict, module_name);
|
||||
JsModuleData* module_inst = js_find_loaded_module(modules, name);
|
||||
if(module_inst) { //-V547
|
||||
furi_string_free(module_name);
|
||||
mjs_prepend_errorf(
|
||||
modules->mjs, MJS_BAD_ARGS_ERROR, "\"%s\" module is already installed", name);
|
||||
return MJS_UNDEFINED;
|
||||
@@ -73,8 +95,11 @@ mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_le
|
||||
|
||||
if(strncmp(name, modules_builtin[i].name, name_compare_len) == 0) {
|
||||
JsModuleData module = {
|
||||
.create = modules_builtin[i].create, .destroy = modules_builtin[i].destroy};
|
||||
JsModuleDict_set_at(modules->module_dict, module_name, module);
|
||||
.create = modules_builtin[i].create,
|
||||
.destroy = modules_builtin[i].destroy,
|
||||
.name = furi_string_alloc_set_str(name),
|
||||
};
|
||||
JsModuleArray_push_at(modules->modules, 0, module);
|
||||
module_found = true;
|
||||
FURI_LOG_I(TAG, "Using built-in module %s", name);
|
||||
break;
|
||||
@@ -83,39 +108,57 @@ mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_le
|
||||
|
||||
// External module load
|
||||
if(!module_found) {
|
||||
FuriString* deslashed_name = furi_string_alloc_set_str(name);
|
||||
furi_string_replace_all_str(deslashed_name, "/", "__");
|
||||
FuriString* module_path = furi_string_alloc();
|
||||
furi_string_printf(module_path, "%s/js_%s.fal", MODULES_PATH, name);
|
||||
FURI_LOG_I(TAG, "Loading external module %s", furi_string_get_cstr(module_path));
|
||||
furi_string_printf(
|
||||
module_path, "%s/js_%s.fal", MODULES_PATH, furi_string_get_cstr(deslashed_name));
|
||||
FURI_LOG_I(
|
||||
TAG, "Loading external module %s from %s", name, furi_string_get_cstr(module_path));
|
||||
do {
|
||||
uint32_t plugin_cnt_last = plugin_manager_get_count(modules->plugin_manager);
|
||||
PluginManagerError load_error = plugin_manager_load_single(
|
||||
modules->plugin_manager, furi_string_get_cstr(module_path));
|
||||
if(load_error != PluginManagerErrorNone) {
|
||||
FURI_LOG_E(
|
||||
TAG,
|
||||
"Module %s load error. It may depend on other modules that are not yet loaded.",
|
||||
name);
|
||||
break;
|
||||
}
|
||||
const JsModuleDescriptor* plugin =
|
||||
plugin_manager_get_ep(modules->plugin_manager, plugin_cnt_last);
|
||||
furi_assert(plugin);
|
||||
|
||||
if(strncmp(name, plugin->name, name_len) != 0) {
|
||||
FURI_LOG_E(TAG, "Module name missmatch %s", plugin->name);
|
||||
if(furi_string_cmp_str(deslashed_name, plugin->name) != 0) {
|
||||
FURI_LOG_E(TAG, "Module name mismatch %s", plugin->name);
|
||||
break;
|
||||
}
|
||||
JsModuleData module = {.create = plugin->create, .destroy = plugin->destroy};
|
||||
JsModuleDict_set_at(modules->module_dict, module_name, module);
|
||||
JsModuleData module = {
|
||||
.create = plugin->create,
|
||||
.destroy = plugin->destroy,
|
||||
.name = furi_string_alloc_set_str(name),
|
||||
};
|
||||
JsModuleArray_push_at(modules->modules, 0, module);
|
||||
|
||||
if(plugin->api_interface) {
|
||||
FURI_LOG_I(TAG, "Added module API to composite resolver: %s", plugin->name);
|
||||
composite_api_resolver_add(modules->resolver, plugin->api_interface);
|
||||
}
|
||||
|
||||
module_found = true;
|
||||
} while(0);
|
||||
furi_string_free(module_path);
|
||||
furi_string_free(deslashed_name);
|
||||
}
|
||||
|
||||
// Run module constructor
|
||||
mjs_val_t module_object = MJS_UNDEFINED;
|
||||
if(module_found) {
|
||||
module_inst = JsModuleDict_get(modules->module_dict, module_name);
|
||||
module_inst = js_find_loaded_module(modules, name);
|
||||
furi_assert(module_inst);
|
||||
if(module_inst->create) { //-V779
|
||||
module_inst->context = module_inst->create(modules->mjs, &module_object);
|
||||
module_inst->context = module_inst->create(modules->mjs, &module_object, modules);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +166,12 @@ mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_le
|
||||
mjs_prepend_errorf(modules->mjs, MJS_BAD_ARGS_ERROR, "\"%s\" module load fail", name);
|
||||
}
|
||||
|
||||
furi_string_free(module_name);
|
||||
|
||||
return module_object;
|
||||
}
|
||||
|
||||
void* js_module_get(JsModules* modules, const char* name) {
|
||||
FuriString* module_name = furi_string_alloc_set_str(name);
|
||||
JsModuleData* module_inst = js_find_loaded_module(modules, name);
|
||||
furi_string_free(module_name);
|
||||
return module_inst ? module_inst->context : NULL;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "js_thread_i.h"
|
||||
#include <flipper_application/flipper_application.h>
|
||||
#include <flipper_application/plugins/plugin_manager.h>
|
||||
@@ -7,19 +9,269 @@
|
||||
#define PLUGIN_APP_ID "js"
|
||||
#define PLUGIN_API_VERSION 1
|
||||
|
||||
typedef void* (*JsModeConstructor)(struct mjs* mjs, mjs_val_t* object);
|
||||
typedef void (*JsModeDestructor)(void* inst);
|
||||
/**
|
||||
* @brief Returns the foreign pointer in `obj["_"]`
|
||||
*/
|
||||
#define JS_GET_INST(mjs, obj) mjs_get_ptr(mjs, mjs_get(mjs, obj, INST_PROP_NAME, ~0))
|
||||
/**
|
||||
* @brief Returns the foreign pointer in `this["_"]`
|
||||
*/
|
||||
#define JS_GET_CONTEXT(mjs) JS_GET_INST(mjs, mjs_get_this(mjs))
|
||||
|
||||
/**
|
||||
* @brief Syntax sugar for constructing an object
|
||||
*
|
||||
* @example
|
||||
* ```c
|
||||
* mjs_val_t my_obj = mjs_mk_object(mjs);
|
||||
* JS_ASSIGN_MULTI(mjs, my_obj) {
|
||||
* JS_FIELD("method1", MJS_MK_FN(js_storage_file_is_open));
|
||||
* JS_FIELD("method2", MJS_MK_FN(js_storage_file_is_open));
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
#define JS_ASSIGN_MULTI(mjs, object) \
|
||||
for(struct { \
|
||||
struct mjs* mjs; \
|
||||
mjs_val_t val; \
|
||||
int i; \
|
||||
} _ass_multi = {mjs, object, 0}; \
|
||||
_ass_multi.i == 0; \
|
||||
_ass_multi.i++)
|
||||
#define JS_FIELD(name, value) mjs_set(_ass_multi.mjs, _ass_multi.val, name, ~0, value)
|
||||
|
||||
/**
|
||||
* @brief The first word of structures that foreign pointer JS values point to
|
||||
*
|
||||
* This is used to detect situations where JS code mistakenly passes an opaque
|
||||
* foreign pointer of one type as an argument to a native function which expects
|
||||
* a struct of another type.
|
||||
*
|
||||
* It is recommended to use this functionality in conjunction with the following
|
||||
* convenience verification macros:
|
||||
* - `JS_ARG_STRUCT()`
|
||||
* - `JS_ARG_OBJ_WITH_STRUCT()`
|
||||
*
|
||||
* @warning In order for the mechanism to work properly, your struct must store
|
||||
* the magic value in the first word.
|
||||
*/
|
||||
typedef enum {
|
||||
JsForeignMagicStart = 0x15BAD000,
|
||||
JsForeignMagic_JsEventLoopContract,
|
||||
} JsForeignMagic;
|
||||
|
||||
// Are you tired of your silly little JS+C glue code functions being 75%
|
||||
// argument validation code and 25% actual logic? Introducing: ASS (Argument
|
||||
// Schema for Scripts)! ASS is a set of macros that reduce the typical
|
||||
// boilerplate code of "check argument count, get arguments, validate arguments,
|
||||
// extract C values from arguments" down to just one line!
|
||||
|
||||
/**
|
||||
* When passed as the second argument to `JS_FETCH_ARGS_OR_RETURN`, signifies
|
||||
* that the function requires exactly as many arguments as were specified.
|
||||
*/
|
||||
#define JS_EXACTLY ==
|
||||
/**
|
||||
* When passed as the second argument to `JS_FETCH_ARGS_OR_RETURN`, signifies
|
||||
* that the function requires at least as many arguments as were specified.
|
||||
*/
|
||||
#define JS_AT_LEAST >=
|
||||
|
||||
#define JS_ENUM_MAP(var_name, ...) \
|
||||
static const JsEnumMapping var_name##_mapping[] = { \
|
||||
{NULL, sizeof(var_name)}, \
|
||||
__VA_ARGS__, \
|
||||
{NULL, 0}, \
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
const char* name;
|
||||
size_t value;
|
||||
} JsEnumMapping;
|
||||
|
||||
typedef struct {
|
||||
void* out;
|
||||
int (*validator)(mjs_val_t);
|
||||
void (*converter)(struct mjs*, mjs_val_t*, void* out, const void* extra);
|
||||
const char* expected_type;
|
||||
bool (*extended_validator)(struct mjs*, mjs_val_t, const void* extra);
|
||||
const void* extra_data;
|
||||
} _js_arg_decl;
|
||||
|
||||
static inline void _js_to_int32(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) {
|
||||
UNUSED(extra);
|
||||
*(int32_t*)out = mjs_get_int32(mjs, *in);
|
||||
}
|
||||
#define JS_ARG_INT32(out) ((_js_arg_decl){out, mjs_is_number, _js_to_int32, "number", NULL, NULL})
|
||||
|
||||
static inline void _js_to_ptr(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) {
|
||||
UNUSED(extra);
|
||||
*(void**)out = mjs_get_ptr(mjs, *in);
|
||||
}
|
||||
#define JS_ARG_PTR(out) \
|
||||
((_js_arg_decl){out, mjs_is_foreign, _js_to_ptr, "opaque pointer", NULL, NULL})
|
||||
|
||||
static inline void _js_to_string(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) {
|
||||
UNUSED(extra);
|
||||
*(const char**)out = mjs_get_string(mjs, in, NULL);
|
||||
}
|
||||
#define JS_ARG_STR(out) ((_js_arg_decl){out, mjs_is_string, _js_to_string, "string", NULL, NULL})
|
||||
|
||||
static inline void _js_to_bool(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) {
|
||||
UNUSED(extra);
|
||||
*(bool*)out = !!mjs_get_bool(mjs, *in);
|
||||
}
|
||||
#define JS_ARG_BOOL(out) ((_js_arg_decl){out, mjs_is_boolean, _js_to_bool, "boolean", NULL, NULL})
|
||||
|
||||
static inline void _js_passthrough(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) {
|
||||
UNUSED(extra);
|
||||
UNUSED(mjs);
|
||||
*(mjs_val_t*)out = *in;
|
||||
}
|
||||
#define JS_ARG_ANY(out) ((_js_arg_decl){out, NULL, _js_passthrough, "any", NULL, NULL})
|
||||
#define JS_ARG_OBJ(out) ((_js_arg_decl){out, mjs_is_object, _js_passthrough, "any", NULL, NULL})
|
||||
#define JS_ARG_FN(out) \
|
||||
((_js_arg_decl){out, mjs_is_function, _js_passthrough, "function", NULL, NULL})
|
||||
#define JS_ARG_ARR(out) ((_js_arg_decl){out, mjs_is_array, _js_passthrough, "array", NULL, NULL})
|
||||
|
||||
static inline bool _js_validate_struct(struct mjs* mjs, mjs_val_t val, const void* extra) {
|
||||
JsForeignMagic expected_magic = (JsForeignMagic)(size_t)extra;
|
||||
JsForeignMagic struct_magic = *(JsForeignMagic*)mjs_get_ptr(mjs, val);
|
||||
return struct_magic == expected_magic;
|
||||
}
|
||||
#define JS_ARG_STRUCT(type, out) \
|
||||
((_js_arg_decl){ \
|
||||
out, \
|
||||
mjs_is_foreign, \
|
||||
_js_to_ptr, \
|
||||
#type, \
|
||||
_js_validate_struct, \
|
||||
(void*)JsForeignMagic##_##type})
|
||||
|
||||
static inline bool _js_validate_obj_w_struct(struct mjs* mjs, mjs_val_t val, const void* extra) {
|
||||
JsForeignMagic expected_magic = (JsForeignMagic)(size_t)extra;
|
||||
JsForeignMagic struct_magic = *(JsForeignMagic*)JS_GET_INST(mjs, val);
|
||||
return struct_magic == expected_magic;
|
||||
}
|
||||
#define JS_ARG_OBJ_WITH_STRUCT(type, out) \
|
||||
((_js_arg_decl){ \
|
||||
out, \
|
||||
mjs_is_object, \
|
||||
_js_passthrough, \
|
||||
#type, \
|
||||
_js_validate_obj_w_struct, \
|
||||
(void*)JsForeignMagic##_##type})
|
||||
|
||||
static inline bool _js_validate_enum(struct mjs* mjs, mjs_val_t val, const void* extra) {
|
||||
for(const JsEnumMapping* mapping = (JsEnumMapping*)extra + 1; mapping->name; mapping++)
|
||||
if(strcmp(mapping->name, mjs_get_string(mjs, &val, NULL)) == 0) return true;
|
||||
return false;
|
||||
}
|
||||
static inline void
|
||||
_js_convert_enum(struct mjs* mjs, mjs_val_t* val, void* out, const void* extra) {
|
||||
const JsEnumMapping* mapping = (JsEnumMapping*)extra;
|
||||
size_t size = mapping->value; // get enum size from first entry
|
||||
for(mapping++; mapping->name; mapping++) {
|
||||
if(strcmp(mapping->name, mjs_get_string(mjs, val, NULL)) == 0) {
|
||||
if(size == 1)
|
||||
*(uint8_t*)out = mapping->value;
|
||||
else if(size == 2)
|
||||
*(uint16_t*)out = mapping->value;
|
||||
else if(size == 4)
|
||||
*(uint32_t*)out = mapping->value;
|
||||
else if(size == 8)
|
||||
*(uint64_t*)out = mapping->value;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// unreachable, thanks to _js_validate_enum
|
||||
}
|
||||
#define JS_ARG_ENUM(var_name, name) \
|
||||
((_js_arg_decl){ \
|
||||
&var_name, \
|
||||
mjs_is_string, \
|
||||
_js_convert_enum, \
|
||||
name " enum", \
|
||||
_js_validate_enum, \
|
||||
var_name##_mapping})
|
||||
|
||||
//-V:JS_FETCH_ARGS_OR_RETURN:1008
|
||||
/**
|
||||
* @brief Fetches and validates the arguments passed to a JS function
|
||||
*
|
||||
* Example: `int32_t my_arg; JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&my_arg));`
|
||||
*
|
||||
* @warning This macro executes `return;` by design in case of an argument count
|
||||
* mismatch or a validation failure
|
||||
*/
|
||||
#define JS_FETCH_ARGS_OR_RETURN(mjs, arg_operator, ...) \
|
||||
_js_arg_decl _js_args[] = {__VA_ARGS__}; \
|
||||
int _js_arg_cnt = COUNT_OF(_js_args); \
|
||||
mjs_val_t _js_arg_vals[_js_arg_cnt]; \
|
||||
if(!(mjs_nargs(mjs) arg_operator _js_arg_cnt)) \
|
||||
JS_ERROR_AND_RETURN( \
|
||||
mjs, \
|
||||
MJS_BAD_ARGS_ERROR, \
|
||||
"expected %s%d arguments, got %d", \
|
||||
#arg_operator, \
|
||||
_js_arg_cnt, \
|
||||
mjs_nargs(mjs)); \
|
||||
for(int _i = 0; _i < _js_arg_cnt; _i++) { \
|
||||
_js_arg_vals[_i] = mjs_arg(mjs, _i); \
|
||||
if(_js_args[_i].validator) \
|
||||
if(!_js_args[_i].validator(_js_arg_vals[_i])) \
|
||||
JS_ERROR_AND_RETURN( \
|
||||
mjs, \
|
||||
MJS_BAD_ARGS_ERROR, \
|
||||
"argument %d: expected %s", \
|
||||
_i, \
|
||||
_js_args[_i].expected_type); \
|
||||
if(_js_args[_i].extended_validator) \
|
||||
if(!_js_args[_i].extended_validator(mjs, _js_arg_vals[_i], _js_args[_i].extra_data)) \
|
||||
JS_ERROR_AND_RETURN( \
|
||||
mjs, \
|
||||
MJS_BAD_ARGS_ERROR, \
|
||||
"argument %d: expected %s", \
|
||||
_i, \
|
||||
_js_args[_i].expected_type); \
|
||||
_js_args[_i].converter( \
|
||||
mjs, &_js_arg_vals[_i], _js_args[_i].out, _js_args[_i].extra_data); \
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Prepends an error, sets the JS return value to `undefined` and returns
|
||||
* from the C function
|
||||
* @warning This macro executes `return;` by design
|
||||
*/
|
||||
#define JS_ERROR_AND_RETURN(mjs, error_code, ...) \
|
||||
do { \
|
||||
mjs_prepend_errorf(mjs, error_code, __VA_ARGS__); \
|
||||
mjs_return(mjs, MJS_UNDEFINED); \
|
||||
return; \
|
||||
} while(0)
|
||||
|
||||
typedef struct JsModules JsModules;
|
||||
|
||||
typedef void* (*JsModuleConstructor)(struct mjs* mjs, mjs_val_t* object, JsModules* modules);
|
||||
typedef void (*JsModuleDestructor)(void* inst);
|
||||
|
||||
typedef struct {
|
||||
char* name;
|
||||
JsModeConstructor create;
|
||||
JsModeDestructor destroy;
|
||||
JsModuleConstructor create;
|
||||
JsModuleDestructor destroy;
|
||||
const ElfApiInterface* api_interface;
|
||||
} JsModuleDescriptor;
|
||||
|
||||
typedef struct JsModules JsModules;
|
||||
|
||||
JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver);
|
||||
|
||||
void js_modules_destroy(JsModules* modules);
|
||||
|
||||
mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_len);
|
||||
|
||||
/**
|
||||
* @brief Gets a module instance by its name
|
||||
* This is useful when a module wants to access a stateful API of another
|
||||
* module.
|
||||
* @returns Pointer to module context, NULL if the module is not instantiated
|
||||
*/
|
||||
void* js_module_get(JsModules* modules, const char* name);
|
||||
|
||||
@@ -195,17 +195,11 @@ static void js_require(struct mjs* mjs) {
|
||||
}
|
||||
|
||||
static void js_global_to_string(struct mjs* mjs) {
|
||||
int base = 10;
|
||||
if(mjs_nargs(mjs) > 1) base = mjs_get_int(mjs, mjs_arg(mjs, 1));
|
||||
double num = mjs_get_int(mjs, mjs_arg(mjs, 0));
|
||||
char tmp_str[] = "-2147483648";
|
||||
itoa(num, tmp_str, 10);
|
||||
mjs_val_t ret = mjs_mk_string(mjs, tmp_str, ~0, true);
|
||||
mjs_return(mjs, ret);
|
||||
}
|
||||
|
||||
static void js_global_to_hex_string(struct mjs* mjs) {
|
||||
double num = mjs_get_int(mjs, mjs_arg(mjs, 0));
|
||||
char tmp_str[] = "-FFFFFFFF";
|
||||
itoa(num, tmp_str, 16);
|
||||
itoa(num, tmp_str, base);
|
||||
mjs_val_t ret = mjs_mk_string(mjs, tmp_str, ~0, true);
|
||||
mjs_return(mjs, ret);
|
||||
}
|
||||
@@ -239,8 +233,7 @@ static int32_t js_thread(void* arg) {
|
||||
mjs_val_t global = mjs_get_global(mjs);
|
||||
mjs_set(mjs, global, "print", ~0, MJS_MK_FN(js_print));
|
||||
mjs_set(mjs, global, "delay", ~0, MJS_MK_FN(js_delay));
|
||||
mjs_set(mjs, global, "to_string", ~0, MJS_MK_FN(js_global_to_string));
|
||||
mjs_set(mjs, global, "to_hex_string", ~0, MJS_MK_FN(js_global_to_hex_string));
|
||||
mjs_set(mjs, global, "toString", ~0, MJS_MK_FN(js_global_to_string));
|
||||
mjs_set(mjs, global, "ffi_address", ~0, MJS_MK_FN(js_ffi_address));
|
||||
mjs_set(mjs, global, "require", ~0, MJS_MK_FN(js_require));
|
||||
|
||||
@@ -296,8 +289,8 @@ static int32_t js_thread(void* arg) {
|
||||
}
|
||||
}
|
||||
|
||||
js_modules_destroy(worker->modules);
|
||||
mjs_destroy(mjs);
|
||||
js_modules_destroy(worker->modules);
|
||||
|
||||
composite_api_resolver_free(worker->resolver);
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct JsThread JsThread;
|
||||
|
||||
typedef enum {
|
||||
@@ -14,3 +18,7 @@ typedef void (*JsThreadCallback)(JsThreadEvent event, const char* msg, void* con
|
||||
JsThread* js_thread_run(const char* script_path, JsThreadCallback callback, void* context);
|
||||
|
||||
void js_thread_stop(JsThread* worker);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -72,8 +72,8 @@ static bool setup_parse_params(struct mjs* mjs, mjs_val_t arg, FuriHalUsbHidConf
|
||||
}
|
||||
mjs_val_t vid_obj = mjs_get(mjs, arg, "vid", ~0);
|
||||
mjs_val_t pid_obj = mjs_get(mjs, arg, "pid", ~0);
|
||||
mjs_val_t mfr_obj = mjs_get(mjs, arg, "mfr_name", ~0);
|
||||
mjs_val_t prod_obj = mjs_get(mjs, arg, "prod_name", ~0);
|
||||
mjs_val_t mfr_obj = mjs_get(mjs, arg, "mfrName", ~0);
|
||||
mjs_val_t prod_obj = mjs_get(mjs, arg, "prodName", ~0);
|
||||
|
||||
if(mjs_is_number(vid_obj) && mjs_is_number(pid_obj)) {
|
||||
hid_cfg->vid = mjs_get_int32(mjs, vid_obj);
|
||||
@@ -378,7 +378,8 @@ static void js_badusb_println(struct mjs* mjs) {
|
||||
badusb_print(mjs, true);
|
||||
}
|
||||
|
||||
static void* js_badusb_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
static void* js_badusb_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
JsBadusbInst* badusb = malloc(sizeof(JsBadusbInst));
|
||||
mjs_val_t badusb_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, badusb_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, badusb));
|
||||
@@ -409,6 +410,7 @@ static const JsModuleDescriptor js_badusb_desc = {
|
||||
"badusb",
|
||||
js_badusb_create,
|
||||
js_badusb_destroy,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
#include <core/common_defines.h>
|
||||
#include "../js_modules.h"
|
||||
#include <dialogs/dialogs.h>
|
||||
|
||||
static bool js_dialog_msg_parse_params(struct mjs* mjs, const char** hdr, const char** msg) {
|
||||
size_t num_args = mjs_nargs(mjs);
|
||||
if(num_args != 2) {
|
||||
return false;
|
||||
}
|
||||
mjs_val_t header_obj = mjs_arg(mjs, 0);
|
||||
mjs_val_t msg_obj = mjs_arg(mjs, 1);
|
||||
if((!mjs_is_string(header_obj)) || (!mjs_is_string(msg_obj))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t arg_len = 0;
|
||||
*hdr = mjs_get_string(mjs, &header_obj, &arg_len);
|
||||
if(arg_len == 0) {
|
||||
*hdr = NULL;
|
||||
}
|
||||
|
||||
*msg = mjs_get_string(mjs, &msg_obj, &arg_len);
|
||||
if(arg_len == 0) {
|
||||
*msg = NULL;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void js_dialog_message(struct mjs* mjs) {
|
||||
const char* dialog_header = NULL;
|
||||
const char* dialog_msg = NULL;
|
||||
if(!js_dialog_msg_parse_params(mjs, &dialog_header, &dialog_msg)) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
|
||||
DialogMessage* message = dialog_message_alloc();
|
||||
dialog_message_set_buttons(message, NULL, "OK", NULL);
|
||||
if(dialog_header) {
|
||||
dialog_message_set_header(message, dialog_header, 64, 3, AlignCenter, AlignTop);
|
||||
}
|
||||
if(dialog_msg) {
|
||||
dialog_message_set_text(message, dialog_msg, 64, 26, AlignCenter, AlignTop);
|
||||
}
|
||||
DialogMessageButton result = dialog_message_show(dialogs, message);
|
||||
dialog_message_free(message);
|
||||
furi_record_close(RECORD_DIALOGS);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, result == DialogMessageButtonCenter));
|
||||
}
|
||||
|
||||
static void js_dialog_custom(struct mjs* mjs) {
|
||||
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
|
||||
DialogMessage* message = dialog_message_alloc();
|
||||
|
||||
bool params_correct = false;
|
||||
|
||||
do {
|
||||
if(mjs_nargs(mjs) != 1) {
|
||||
break;
|
||||
}
|
||||
mjs_val_t params_obj = mjs_arg(mjs, 0);
|
||||
if(!mjs_is_object(params_obj)) {
|
||||
break;
|
||||
}
|
||||
|
||||
mjs_val_t text_obj = mjs_get(mjs, params_obj, "header", ~0);
|
||||
size_t arg_len = 0;
|
||||
const char* text_str = mjs_get_string(mjs, &text_obj, &arg_len);
|
||||
if(arg_len == 0) {
|
||||
text_str = NULL;
|
||||
}
|
||||
if(text_str) {
|
||||
dialog_message_set_header(message, text_str, 64, 3, AlignCenter, AlignTop);
|
||||
}
|
||||
|
||||
text_obj = mjs_get(mjs, params_obj, "text", ~0);
|
||||
text_str = mjs_get_string(mjs, &text_obj, &arg_len);
|
||||
if(arg_len == 0) {
|
||||
text_str = NULL;
|
||||
}
|
||||
if(text_str) {
|
||||
dialog_message_set_text(message, text_str, 64, 26, AlignCenter, AlignTop);
|
||||
}
|
||||
|
||||
mjs_val_t btn_obj[3] = {
|
||||
mjs_get(mjs, params_obj, "button_left", ~0),
|
||||
mjs_get(mjs, params_obj, "button_center", ~0),
|
||||
mjs_get(mjs, params_obj, "button_right", ~0),
|
||||
};
|
||||
const char* btn_text[3] = {NULL, NULL, NULL};
|
||||
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
if(!mjs_is_string(btn_obj[i])) {
|
||||
continue;
|
||||
}
|
||||
btn_text[i] = mjs_get_string(mjs, &btn_obj[i], &arg_len);
|
||||
if(arg_len == 0) {
|
||||
btn_text[i] = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
dialog_message_set_buttons(message, btn_text[0], btn_text[1], btn_text[2]);
|
||||
|
||||
DialogMessageButton result = dialog_message_show(dialogs, message);
|
||||
mjs_val_t return_obj = MJS_UNDEFINED;
|
||||
if(result == DialogMessageButtonLeft) {
|
||||
return_obj = mjs_mk_string(mjs, btn_text[0], ~0, true);
|
||||
} else if(result == DialogMessageButtonCenter) {
|
||||
return_obj = mjs_mk_string(mjs, btn_text[1], ~0, true);
|
||||
} else if(result == DialogMessageButtonRight) {
|
||||
return_obj = mjs_mk_string(mjs, btn_text[2], ~0, true);
|
||||
} else {
|
||||
return_obj = mjs_mk_string(mjs, "", ~0, true);
|
||||
}
|
||||
|
||||
mjs_return(mjs, return_obj);
|
||||
params_correct = true;
|
||||
} while(0);
|
||||
|
||||
dialog_message_free(message);
|
||||
furi_record_close(RECORD_DIALOGS);
|
||||
|
||||
if(!params_correct) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
}
|
||||
|
||||
static void* js_dialog_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
mjs_val_t dialog_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, dialog_obj, "message", ~0, MJS_MK_FN(js_dialog_message));
|
||||
mjs_set(mjs, dialog_obj, "custom", ~0, MJS_MK_FN(js_dialog_custom));
|
||||
*object = dialog_obj;
|
||||
|
||||
return (void*)1;
|
||||
}
|
||||
|
||||
static const JsModuleDescriptor js_dialog_desc = {
|
||||
"dialog",
|
||||
js_dialog_create,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
.appid = PLUGIN_APP_ID,
|
||||
.ep_api_version = PLUGIN_API_VERSION,
|
||||
.entry_point = &js_dialog_desc,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* js_dialog_ep(void) {
|
||||
return &plugin_descriptor;
|
||||
}
|
||||
451
applications/system/js_app/modules/js_event_loop/js_event_loop.c
Normal file
451
applications/system/js_app/modules/js_event_loop/js_event_loop.c
Normal file
@@ -0,0 +1,451 @@
|
||||
#include "js_event_loop.h"
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include <expansion/expansion.h>
|
||||
#include <mlib/m-array.h>
|
||||
|
||||
/**
|
||||
* @brief Number of arguments that callbacks receive from this module that they can't modify
|
||||
*/
|
||||
#define SYSTEM_ARGS 2
|
||||
|
||||
/**
|
||||
* @brief Context passed to the generic event callback
|
||||
*/
|
||||
typedef struct {
|
||||
JsEventLoopObjectType object_type;
|
||||
|
||||
struct mjs* mjs;
|
||||
mjs_val_t callback;
|
||||
// NOTE: not using an mlib array because resizing is not needed.
|
||||
mjs_val_t* arguments;
|
||||
size_t arity;
|
||||
|
||||
JsEventLoopTransformer transformer;
|
||||
void* transformer_context;
|
||||
} JsEventLoopCallbackContext;
|
||||
|
||||
/**
|
||||
* @brief Contains data needed to cancel a subscription
|
||||
*/
|
||||
typedef struct {
|
||||
FuriEventLoop* loop;
|
||||
JsEventLoopObjectType object_type;
|
||||
FuriEventLoopObject* object;
|
||||
JsEventLoopCallbackContext* context;
|
||||
JsEventLoopContract* contract;
|
||||
void* subscriptions; // SubscriptionArray_t, which we can't reference in this definition
|
||||
} JsEventLoopSubscription;
|
||||
|
||||
typedef struct {
|
||||
FuriEventLoop* loop;
|
||||
struct mjs* mjs;
|
||||
} JsEventLoopTickContext;
|
||||
|
||||
ARRAY_DEF(SubscriptionArray, JsEventLoopSubscription*, M_PTR_OPLIST); //-V575
|
||||
ARRAY_DEF(ContractArray, JsEventLoopContract*, M_PTR_OPLIST); //-V575
|
||||
|
||||
/**
|
||||
* @brief Per-module instance control structure
|
||||
*/
|
||||
struct JsEventLoop {
|
||||
FuriEventLoop* loop;
|
||||
SubscriptionArray_t subscriptions;
|
||||
ContractArray_t owned_contracts; //<! Contracts that were produced by this module
|
||||
JsEventLoopTickContext* tick_context;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Generic event callback, handles all events by calling the JS callbacks
|
||||
*/
|
||||
static void js_event_loop_callback_generic(void* param) {
|
||||
JsEventLoopCallbackContext* context = param;
|
||||
mjs_val_t result;
|
||||
mjs_apply(
|
||||
context->mjs,
|
||||
&result,
|
||||
context->callback,
|
||||
MJS_UNDEFINED,
|
||||
context->arity,
|
||||
context->arguments);
|
||||
|
||||
// save returned args for next call
|
||||
if(mjs_array_length(context->mjs, result) != context->arity - SYSTEM_ARGS) return;
|
||||
for(size_t i = 0; i < context->arity - SYSTEM_ARGS; i++) {
|
||||
mjs_disown(context->mjs, &context->arguments[i + SYSTEM_ARGS]);
|
||||
context->arguments[i + SYSTEM_ARGS] = mjs_array_get(context->mjs, result, i);
|
||||
mjs_own(context->mjs, &context->arguments[i + SYSTEM_ARGS]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handles non-timer events
|
||||
*/
|
||||
static bool js_event_loop_callback(void* object, void* param) {
|
||||
JsEventLoopCallbackContext* context = param;
|
||||
|
||||
if(context->transformer) {
|
||||
mjs_disown(context->mjs, &context->arguments[1]);
|
||||
context->arguments[1] =
|
||||
context->transformer(context->mjs, object, context->transformer_context);
|
||||
mjs_own(context->mjs, &context->arguments[1]);
|
||||
} else {
|
||||
// default behavior: take semaphores and mutexes
|
||||
switch(context->object_type) {
|
||||
case JsEventLoopObjectTypeSemaphore: {
|
||||
FuriSemaphore* semaphore = object;
|
||||
furi_check(furi_semaphore_acquire(semaphore, 0) == FuriStatusOk);
|
||||
} break;
|
||||
default:
|
||||
// the corresponding check has been performed when we were given the contract
|
||||
furi_crash();
|
||||
}
|
||||
}
|
||||
|
||||
js_event_loop_callback_generic(param);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cancels an event subscription
|
||||
*/
|
||||
static void js_event_loop_subscription_cancel(struct mjs* mjs) {
|
||||
JsEventLoopSubscription* subscription = JS_GET_CONTEXT(mjs);
|
||||
|
||||
if(subscription->object_type == JsEventLoopObjectTypeTimer) {
|
||||
furi_event_loop_timer_stop(subscription->object);
|
||||
} else {
|
||||
furi_event_loop_unsubscribe(subscription->loop, subscription->object);
|
||||
}
|
||||
|
||||
free(subscription->context->arguments);
|
||||
free(subscription->context);
|
||||
|
||||
// find and remove ourselves from the array
|
||||
SubscriptionArray_it_t iterator;
|
||||
for(SubscriptionArray_it(iterator, subscription->subscriptions);
|
||||
!SubscriptionArray_end_p(iterator);
|
||||
SubscriptionArray_next(iterator)) {
|
||||
JsEventLoopSubscription* item = *SubscriptionArray_cref(iterator);
|
||||
if(item == subscription) break;
|
||||
}
|
||||
SubscriptionArray_remove(subscription->subscriptions, iterator);
|
||||
free(subscription);
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Subscribes a JavaScript function to an event
|
||||
*/
|
||||
static void js_event_loop_subscribe(struct mjs* mjs) {
|
||||
JsEventLoop* module = JS_GET_CONTEXT(mjs);
|
||||
|
||||
// get arguments
|
||||
JsEventLoopContract* contract;
|
||||
mjs_val_t callback;
|
||||
JS_FETCH_ARGS_OR_RETURN(
|
||||
mjs, JS_AT_LEAST, JS_ARG_STRUCT(JsEventLoopContract, &contract), JS_ARG_FN(&callback));
|
||||
|
||||
// create subscription object
|
||||
JsEventLoopSubscription* subscription = malloc(sizeof(JsEventLoopSubscription));
|
||||
JsEventLoopCallbackContext* context = malloc(sizeof(JsEventLoopCallbackContext));
|
||||
subscription->loop = module->loop;
|
||||
subscription->object_type = contract->object_type;
|
||||
subscription->context = context;
|
||||
subscription->subscriptions = module->subscriptions;
|
||||
if(contract->object_type == JsEventLoopObjectTypeTimer) subscription->contract = contract;
|
||||
mjs_val_t subscription_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, subscription_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, subscription));
|
||||
mjs_set(mjs, subscription_obj, "cancel", ~0, MJS_MK_FN(js_event_loop_subscription_cancel));
|
||||
|
||||
// create callback context
|
||||
context->object_type = contract->object_type;
|
||||
context->arity = mjs_nargs(mjs) - SYSTEM_ARGS + 2;
|
||||
context->arguments = calloc(context->arity, sizeof(mjs_val_t));
|
||||
context->arguments[0] = subscription_obj;
|
||||
context->arguments[1] = MJS_UNDEFINED;
|
||||
for(size_t i = SYSTEM_ARGS; i < context->arity; i++) {
|
||||
mjs_val_t arg = mjs_arg(mjs, i - SYSTEM_ARGS + 2);
|
||||
context->arguments[i] = arg;
|
||||
mjs_own(mjs, &context->arguments[i]);
|
||||
}
|
||||
context->mjs = mjs;
|
||||
context->callback = callback;
|
||||
mjs_own(mjs, &context->callback);
|
||||
mjs_own(mjs, &context->arguments[0]);
|
||||
mjs_own(mjs, &context->arguments[1]);
|
||||
|
||||
// queue and stream contracts must have a transform callback, others are allowed to delegate
|
||||
// the obvious default behavior to this module
|
||||
if(contract->object_type == JsEventLoopObjectTypeQueue ||
|
||||
contract->object_type == JsEventLoopObjectTypeStream) {
|
||||
furi_check(contract->non_timer.transformer);
|
||||
}
|
||||
context->transformer = contract->non_timer.transformer;
|
||||
context->transformer_context = contract->non_timer.transformer_context;
|
||||
|
||||
// subscribe
|
||||
switch(contract->object_type) {
|
||||
case JsEventLoopObjectTypeTimer: {
|
||||
FuriEventLoopTimer* timer = furi_event_loop_timer_alloc(
|
||||
module->loop, js_event_loop_callback_generic, contract->timer.type, context);
|
||||
furi_event_loop_timer_start(timer, contract->timer.interval_ticks);
|
||||
contract->object = timer;
|
||||
} break;
|
||||
case JsEventLoopObjectTypeSemaphore:
|
||||
furi_event_loop_subscribe_semaphore(
|
||||
module->loop,
|
||||
contract->object,
|
||||
contract->non_timer.event,
|
||||
js_event_loop_callback,
|
||||
context);
|
||||
break;
|
||||
case JsEventLoopObjectTypeQueue:
|
||||
furi_event_loop_subscribe_message_queue(
|
||||
module->loop,
|
||||
contract->object,
|
||||
contract->non_timer.event,
|
||||
js_event_loop_callback,
|
||||
context);
|
||||
break;
|
||||
default:
|
||||
furi_crash("unimplemented");
|
||||
}
|
||||
|
||||
subscription->object = contract->object;
|
||||
SubscriptionArray_push_back(module->subscriptions, subscription);
|
||||
mjs_return(mjs, subscription_obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Runs the event loop until it is stopped
|
||||
*/
|
||||
static void js_event_loop_run(struct mjs* mjs) {
|
||||
JsEventLoop* module = JS_GET_CONTEXT(mjs);
|
||||
furi_event_loop_run(module->loop);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stops a running event loop
|
||||
*/
|
||||
static void js_event_loop_stop(struct mjs* mjs) {
|
||||
JsEventLoop* module = JS_GET_CONTEXT(mjs);
|
||||
furi_event_loop_stop(module->loop);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Creates a timer event that can be subscribed to just like any other
|
||||
* event
|
||||
*/
|
||||
static void js_event_loop_timer(struct mjs* mjs) {
|
||||
// get arguments
|
||||
const char* mode_str;
|
||||
int32_t interval;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&mode_str), JS_ARG_INT32(&interval));
|
||||
JsEventLoop* module = JS_GET_CONTEXT(mjs);
|
||||
|
||||
FuriEventLoopTimerType mode;
|
||||
if(strcasecmp(mode_str, "periodic") == 0) {
|
||||
mode = FuriEventLoopTimerTypePeriodic;
|
||||
} else if(strcasecmp(mode_str, "oneshot") == 0) {
|
||||
mode = FuriEventLoopTimerTypeOnce;
|
||||
} else {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "argument 0: unknown mode");
|
||||
}
|
||||
|
||||
// make timer contract
|
||||
JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract));
|
||||
*contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object_type = JsEventLoopObjectTypeTimer,
|
||||
.object = NULL,
|
||||
.timer =
|
||||
{
|
||||
.interval_ticks = furi_ms_to_ticks((uint32_t)interval),
|
||||
.type = mode,
|
||||
},
|
||||
};
|
||||
ContractArray_push_back(module->owned_contracts, contract);
|
||||
mjs_return(mjs, mjs_mk_foreign(mjs, contract));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Queue transformer. Takes `mjs_val_t` pointers out of a queue and
|
||||
* returns their dereferenced value
|
||||
*/
|
||||
static mjs_val_t
|
||||
js_event_loop_queue_transformer(struct mjs* mjs, FuriEventLoopObject* object, void* context) {
|
||||
UNUSED(context);
|
||||
mjs_val_t* message_ptr;
|
||||
furi_check(furi_message_queue_get(object, &message_ptr, 0) == FuriStatusOk);
|
||||
mjs_val_t message = *message_ptr;
|
||||
mjs_disown(mjs, message_ptr);
|
||||
free(message_ptr);
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sends a message to a queue
|
||||
*/
|
||||
static void js_event_loop_queue_send(struct mjs* mjs) {
|
||||
// get arguments
|
||||
mjs_val_t message;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&message));
|
||||
JsEventLoopContract* contract = JS_GET_CONTEXT(mjs);
|
||||
|
||||
// send message
|
||||
mjs_val_t* message_ptr = malloc(sizeof(mjs_val_t));
|
||||
*message_ptr = message;
|
||||
mjs_own(mjs, message_ptr);
|
||||
furi_message_queue_put(contract->object, &message_ptr, 0);
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Creates a queue
|
||||
*/
|
||||
static void js_event_loop_queue(struct mjs* mjs) {
|
||||
// get arguments
|
||||
int32_t length;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&length));
|
||||
JsEventLoop* module = JS_GET_CONTEXT(mjs);
|
||||
|
||||
// make queue contract
|
||||
JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract));
|
||||
*contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object_type = JsEventLoopObjectTypeQueue,
|
||||
// we could store `mjs_val_t`s in the queue directly if not for mJS' requirement to have consistent pointers to owned values
|
||||
.object = furi_message_queue_alloc((size_t)length, sizeof(mjs_val_t*)),
|
||||
.non_timer =
|
||||
{
|
||||
.event = FuriEventLoopEventIn,
|
||||
.transformer = js_event_loop_queue_transformer,
|
||||
},
|
||||
};
|
||||
ContractArray_push_back(module->owned_contracts, contract);
|
||||
|
||||
// return object with control methods
|
||||
mjs_val_t queue = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, queue, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, contract));
|
||||
mjs_set(mjs, queue, "input", ~0, mjs_mk_foreign(mjs, contract));
|
||||
mjs_set(mjs, queue, "send", ~0, MJS_MK_FN(js_event_loop_queue_send));
|
||||
mjs_return(mjs, queue);
|
||||
}
|
||||
|
||||
static void js_event_loop_tick(void* param) {
|
||||
JsEventLoopTickContext* context = param;
|
||||
uint32_t flags = furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny | FuriFlagNoClear, 0);
|
||||
if(flags & FuriFlagError) {
|
||||
return;
|
||||
}
|
||||
if(flags & ThreadEventStop) {
|
||||
furi_event_loop_stop(context->loop);
|
||||
mjs_exit(context->mjs);
|
||||
}
|
||||
}
|
||||
|
||||
static void* js_event_loop_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
mjs_val_t event_loop_obj = mjs_mk_object(mjs);
|
||||
JsEventLoop* module = malloc(sizeof(JsEventLoop));
|
||||
JsEventLoopTickContext* tick_ctx = malloc(sizeof(JsEventLoopTickContext));
|
||||
module->loop = furi_event_loop_alloc();
|
||||
tick_ctx->loop = module->loop;
|
||||
tick_ctx->mjs = mjs;
|
||||
module->tick_context = tick_ctx;
|
||||
furi_event_loop_tick_set(module->loop, 10, js_event_loop_tick, tick_ctx);
|
||||
SubscriptionArray_init(module->subscriptions);
|
||||
ContractArray_init(module->owned_contracts);
|
||||
|
||||
mjs_set(mjs, event_loop_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, module));
|
||||
mjs_set(mjs, event_loop_obj, "subscribe", ~0, MJS_MK_FN(js_event_loop_subscribe));
|
||||
mjs_set(mjs, event_loop_obj, "run", ~0, MJS_MK_FN(js_event_loop_run));
|
||||
mjs_set(mjs, event_loop_obj, "stop", ~0, MJS_MK_FN(js_event_loop_stop));
|
||||
mjs_set(mjs, event_loop_obj, "timer", ~0, MJS_MK_FN(js_event_loop_timer));
|
||||
mjs_set(mjs, event_loop_obj, "queue", ~0, MJS_MK_FN(js_event_loop_queue));
|
||||
|
||||
*object = event_loop_obj;
|
||||
return module;
|
||||
}
|
||||
|
||||
static void js_event_loop_destroy(void* inst) {
|
||||
if(inst) {
|
||||
JsEventLoop* module = inst;
|
||||
furi_event_loop_stop(module->loop);
|
||||
|
||||
// free subscriptions
|
||||
SubscriptionArray_it_t sub_iterator;
|
||||
for(SubscriptionArray_it(sub_iterator, module->subscriptions);
|
||||
!SubscriptionArray_end_p(sub_iterator);
|
||||
SubscriptionArray_next(sub_iterator)) {
|
||||
JsEventLoopSubscription* const* sub = SubscriptionArray_cref(sub_iterator);
|
||||
free((*sub)->context->arguments);
|
||||
free((*sub)->context);
|
||||
free(*sub);
|
||||
}
|
||||
SubscriptionArray_clear(module->subscriptions);
|
||||
|
||||
// free owned contracts
|
||||
ContractArray_it_t iterator;
|
||||
for(ContractArray_it(iterator, module->owned_contracts); !ContractArray_end_p(iterator);
|
||||
ContractArray_next(iterator)) {
|
||||
// unsubscribe object
|
||||
JsEventLoopContract* contract = *ContractArray_cref(iterator);
|
||||
if(contract->object_type == JsEventLoopObjectTypeTimer) {
|
||||
furi_event_loop_timer_stop(contract->object);
|
||||
} else {
|
||||
furi_event_loop_unsubscribe(module->loop, contract->object);
|
||||
}
|
||||
|
||||
// free object
|
||||
switch(contract->object_type) {
|
||||
case JsEventLoopObjectTypeTimer:
|
||||
furi_event_loop_timer_free(contract->object);
|
||||
break;
|
||||
case JsEventLoopObjectTypeSemaphore:
|
||||
furi_semaphore_free(contract->object);
|
||||
break;
|
||||
case JsEventLoopObjectTypeQueue:
|
||||
furi_message_queue_free(contract->object);
|
||||
break;
|
||||
default:
|
||||
furi_crash("unimplemented");
|
||||
}
|
||||
|
||||
free(contract);
|
||||
}
|
||||
ContractArray_clear(module->owned_contracts);
|
||||
|
||||
furi_event_loop_free(module->loop);
|
||||
free(module->tick_context);
|
||||
free(module);
|
||||
}
|
||||
}
|
||||
|
||||
extern const ElfApiInterface js_event_loop_hashtable_api_interface;
|
||||
|
||||
static const JsModuleDescriptor js_event_loop_desc = {
|
||||
"event_loop",
|
||||
js_event_loop_create,
|
||||
js_event_loop_destroy,
|
||||
&js_event_loop_hashtable_api_interface,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
.appid = PLUGIN_APP_ID,
|
||||
.ep_api_version = PLUGIN_API_VERSION,
|
||||
.entry_point = &js_event_loop_desc,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* js_event_loop_ep(void) {
|
||||
return &plugin_descriptor;
|
||||
}
|
||||
|
||||
FuriEventLoop* js_event_loop_get_loop(JsEventLoop* loop) {
|
||||
// porta: not the proudest function that i ever wrote
|
||||
furi_check(loop);
|
||||
return loop->loop;
|
||||
}
|
||||
104
applications/system/js_app/modules/js_event_loop/js_event_loop.h
Normal file
104
applications/system/js_app/modules/js_event_loop/js_event_loop.h
Normal file
@@ -0,0 +1,104 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include <furi/core/event_loop.h>
|
||||
#include <furi/core/event_loop_timer.h>
|
||||
|
||||
/**
|
||||
* @file js_event_loop.h
|
||||
*
|
||||
* In JS interpreter code, `js_event_loop` always creates and maintains the
|
||||
* event loop. There are two ways in which other modules can integrate with this
|
||||
* loop:
|
||||
* - Via contracts: The user of your module would have to acquire an opaque
|
||||
* JS value from you and pass it to `js_event_loop`. This is useful for
|
||||
* events that they user may be interested in. For more info, look at
|
||||
* `JsEventLoopContract`. Also look at `js_event_loop_get_loop`, which
|
||||
* you will need to unsubscribe the event loop from your object.
|
||||
* - Directly: When your module is created, you can acquire an instance of
|
||||
* `JsEventLoop` which you can use to acquire an instance of
|
||||
* `FuriEventLoop` that you can manipulate directly, without the JS
|
||||
* programmer having to pass contracts around. This is useful for
|
||||
* "behind-the-scenes" events that the user does not need to know about. For
|
||||
* more info, look at `js_event_loop_get_loop`.
|
||||
*
|
||||
* In both cases, your module is responsible for both instantiating,
|
||||
* unsubscribing and freeing the object that the event loop subscribes to.
|
||||
*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct JsEventLoop JsEventLoop;
|
||||
|
||||
typedef enum {
|
||||
JsEventLoopObjectTypeTimer,
|
||||
JsEventLoopObjectTypeQueue,
|
||||
JsEventLoopObjectTypeMutex,
|
||||
JsEventLoopObjectTypeSemaphore,
|
||||
JsEventLoopObjectTypeStream,
|
||||
} JsEventLoopObjectType;
|
||||
|
||||
typedef mjs_val_t (
|
||||
*JsEventLoopTransformer)(struct mjs* mjs, FuriEventLoopObject* object, void* context);
|
||||
|
||||
typedef struct {
|
||||
FuriEventLoopEvent event;
|
||||
JsEventLoopTransformer transformer;
|
||||
void* transformer_context;
|
||||
} JsEventLoopNonTimerContract;
|
||||
|
||||
typedef struct {
|
||||
FuriEventLoopTimerType type;
|
||||
uint32_t interval_ticks;
|
||||
} JsEventLoopTimerContract;
|
||||
|
||||
/**
|
||||
* @brief Adapter for other JS modules that wish to integrate with the event
|
||||
* loop JS module
|
||||
*
|
||||
* If another module wishes to integrate with `js_event_loop`, it needs to
|
||||
* implement a function callable from JS that returns an mJS foreign pointer to
|
||||
* an instance of this structure. This value is then read by `event_loop`'s
|
||||
* `subscribe` function.
|
||||
*
|
||||
* There are two fundamental variants of this structure:
|
||||
* - `object_type` is `JsEventLoopObjectTypeTimer`: the `timer` field is
|
||||
* valid, and the `non_timer` field is invalid.
|
||||
* - `object_type` is something else: the `timer` field is invalid, and the
|
||||
* `non_timer` field is valid. `non_timer.event` will be passed to
|
||||
* `furi_event_loop_subscribe`. `non_timer.transformer` will be called to
|
||||
* transform an object into a JS value (called an item) that's passed to the
|
||||
* JS callback. This is useful for example to take an item out of a message
|
||||
* queue and pass it to JS code in a convenient format. If
|
||||
* `non_timer.transformer` is NULL, the event loop will take semaphores and
|
||||
* mutexes on its own.
|
||||
*
|
||||
* The producer of the contract is responsible for freeing both the contract and
|
||||
* the object that it points to when the interpreter is torn down.
|
||||
*/
|
||||
typedef struct {
|
||||
JsForeignMagic magic; // <! `JsForeignMagic_JsEventLoopContract`
|
||||
JsEventLoopObjectType object_type;
|
||||
FuriEventLoopObject* object;
|
||||
union {
|
||||
JsEventLoopNonTimerContract non_timer;
|
||||
JsEventLoopTimerContract timer;
|
||||
};
|
||||
} JsEventLoopContract;
|
||||
|
||||
static_assert(offsetof(JsEventLoopContract, magic) == 0);
|
||||
|
||||
/**
|
||||
* @brief Gets the FuriEventLoop owned by a JsEventLoop
|
||||
*
|
||||
* This function is useful in case your JS module wishes to integrate with
|
||||
* the event loop without passing contracts through JS code. Your module will be
|
||||
* dynamically linked to this one if you use this function, but only if JS code
|
||||
* imports `event_loop` _before_ your module. An instance of `JsEventLoop` may
|
||||
* be obtained via `js_module_get`.
|
||||
*/
|
||||
FuriEventLoop* js_event_loop_get_loop(JsEventLoop* loop);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,16 @@
|
||||
#include <flipper_application/api_hashtable/api_hashtable.h>
|
||||
#include <flipper_application/api_hashtable/compilesort.hpp>
|
||||
|
||||
#include "js_event_loop_api_table_i.h"
|
||||
|
||||
static_assert(!has_hash_collisions(js_event_loop_api_table), "Detected API method hash collision!");
|
||||
|
||||
extern "C" constexpr HashtableApiInterface js_event_loop_hashtable_api_interface{
|
||||
{
|
||||
.api_version_major = 0,
|
||||
.api_version_minor = 0,
|
||||
.resolver_callback = &elf_resolve_from_hashtable,
|
||||
},
|
||||
js_event_loop_api_table.cbegin(),
|
||||
js_event_loop_api_table.cend(),
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
#include "js_event_loop.h"
|
||||
|
||||
static constexpr auto js_event_loop_api_table = sort(
|
||||
create_array_t<sym_entry>(API_METHOD(js_event_loop_get_loop, FuriEventLoop*, (JsEventLoop*))));
|
||||
@@ -25,7 +25,8 @@ static void js_flipper_get_battery(struct mjs* mjs) {
|
||||
mjs_return(mjs, mjs_mk_number(mjs, info.charge));
|
||||
}
|
||||
|
||||
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
mjs_val_t flipper_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, flipper_obj, "getModel", ~0, MJS_MK_FN(js_flipper_get_model));
|
||||
mjs_set(mjs, flipper_obj, "getName", ~0, MJS_MK_FN(js_flipper_get_name));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma once
|
||||
#include "../js_thread_i.h"
|
||||
|
||||
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object);
|
||||
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules);
|
||||
|
||||
345
applications/system/js_app/modules/js_gpio.c
Normal file
345
applications/system/js_app/modules/js_gpio.c
Normal file
@@ -0,0 +1,345 @@
|
||||
#include "../js_modules.h" // IWYU pragma: keep
|
||||
#include "./js_event_loop/js_event_loop.h"
|
||||
#include <furi_hal_gpio.h>
|
||||
#include <furi_hal_resources.h>
|
||||
#include <expansion/expansion.h>
|
||||
#include <limits.h>
|
||||
#include <mlib/m-array.h>
|
||||
|
||||
#define INTERRUPT_QUEUE_LEN 16
|
||||
|
||||
/**
|
||||
* Per-pin control structure
|
||||
*/
|
||||
typedef struct {
|
||||
const GpioPin* pin;
|
||||
bool had_interrupt;
|
||||
FuriSemaphore* interrupt_semaphore;
|
||||
JsEventLoopContract* interrupt_contract;
|
||||
FuriHalAdcChannel adc_channel;
|
||||
FuriHalAdcHandle* adc_handle;
|
||||
} JsGpioPinInst;
|
||||
|
||||
ARRAY_DEF(ManagedPinsArray, JsGpioPinInst*, M_PTR_OPLIST); //-V575
|
||||
|
||||
/**
|
||||
* Per-module instance control structure
|
||||
*/
|
||||
typedef struct {
|
||||
FuriEventLoop* loop;
|
||||
ManagedPinsArray_t managed_pins;
|
||||
FuriHalAdcHandle* adc_handle;
|
||||
} JsGpioInst;
|
||||
|
||||
/**
|
||||
* @brief Interrupt callback
|
||||
*/
|
||||
static void js_gpio_int_cb(void* arg) {
|
||||
furi_assert(arg);
|
||||
FuriSemaphore* semaphore = arg;
|
||||
furi_semaphore_release(semaphore);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initializes a GPIO pin according to the provided mode object
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```js
|
||||
* let gpio = require("gpio");
|
||||
* let led = gpio.get("pc3");
|
||||
* led.init({ direction: "out", outMode: "push_pull" });
|
||||
* ```
|
||||
*/
|
||||
static void js_gpio_init(struct mjs* mjs) {
|
||||
// deconstruct mode object
|
||||
mjs_val_t mode_arg;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&mode_arg));
|
||||
mjs_val_t direction_arg = mjs_get(mjs, mode_arg, "direction", ~0);
|
||||
mjs_val_t out_mode_arg = mjs_get(mjs, mode_arg, "outMode", ~0);
|
||||
mjs_val_t in_mode_arg = mjs_get(mjs, mode_arg, "inMode", ~0);
|
||||
mjs_val_t edge_arg = mjs_get(mjs, mode_arg, "edge", ~0);
|
||||
mjs_val_t pull_arg = mjs_get(mjs, mode_arg, "pull", ~0);
|
||||
|
||||
// get strings
|
||||
const char* direction = mjs_get_string(mjs, &direction_arg, NULL);
|
||||
const char* out_mode = mjs_get_string(mjs, &out_mode_arg, NULL);
|
||||
const char* in_mode = mjs_get_string(mjs, &in_mode_arg, NULL);
|
||||
const char* edge = mjs_get_string(mjs, &edge_arg, NULL);
|
||||
const char* pull = mjs_get_string(mjs, &pull_arg, NULL);
|
||||
if(!direction)
|
||||
JS_ERROR_AND_RETURN(
|
||||
mjs, MJS_BAD_ARGS_ERROR, "Expected string in \"direction\" field of mode object");
|
||||
if(!out_mode) out_mode = "open_drain";
|
||||
if(!in_mode) in_mode = "plain_digital";
|
||||
if(!edge) edge = "rising";
|
||||
|
||||
// convert strings to mode
|
||||
GpioMode mode;
|
||||
if(strcmp(direction, "out") == 0) {
|
||||
if(strcmp(out_mode, "push_pull") == 0)
|
||||
mode = GpioModeOutputPushPull;
|
||||
else if(strcmp(out_mode, "open_drain") == 0)
|
||||
mode = GpioModeOutputOpenDrain;
|
||||
else
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid outMode");
|
||||
} else if(strcmp(direction, "in") == 0) {
|
||||
if(strcmp(in_mode, "analog") == 0) {
|
||||
mode = GpioModeAnalog;
|
||||
} else if(strcmp(in_mode, "plain_digital") == 0) {
|
||||
mode = GpioModeInput;
|
||||
} else if(strcmp(in_mode, "interrupt") == 0) {
|
||||
if(strcmp(edge, "rising") == 0)
|
||||
mode = GpioModeInterruptRise;
|
||||
else if(strcmp(edge, "falling") == 0)
|
||||
mode = GpioModeInterruptFall;
|
||||
else if(strcmp(edge, "both") == 0)
|
||||
mode = GpioModeInterruptRiseFall;
|
||||
else
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid edge");
|
||||
} else if(strcmp(in_mode, "event") == 0) {
|
||||
if(strcmp(edge, "rising") == 0)
|
||||
mode = GpioModeEventRise;
|
||||
else if(strcmp(edge, "falling") == 0)
|
||||
mode = GpioModeEventFall;
|
||||
else if(strcmp(edge, "both") == 0)
|
||||
mode = GpioModeEventRiseFall;
|
||||
else
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid edge");
|
||||
} else {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid inMode");
|
||||
}
|
||||
} else {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid direction");
|
||||
}
|
||||
|
||||
// convert pull
|
||||
GpioPull pull_mode;
|
||||
if(!pull) {
|
||||
pull_mode = GpioPullNo;
|
||||
} else if(strcmp(pull, "up") == 0) {
|
||||
pull_mode = GpioPullUp;
|
||||
} else if(strcmp(pull, "down") == 0) {
|
||||
pull_mode = GpioPullDown;
|
||||
} else {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid pull");
|
||||
}
|
||||
|
||||
// init GPIO
|
||||
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||
furi_hal_gpio_init(manager_data->pin, mode, pull_mode, GpioSpeedVeryHigh);
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Writes a logic value to a GPIO pin
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```js
|
||||
* let gpio = require("gpio");
|
||||
* let led = gpio.get("pc3");
|
||||
* led.init({ direction: "out", outMode: "push_pull" });
|
||||
* led.write(true);
|
||||
* ```
|
||||
*/
|
||||
static void js_gpio_write(struct mjs* mjs) {
|
||||
bool level;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_BOOL(&level));
|
||||
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||
furi_hal_gpio_write(manager_data->pin, level);
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reads a logic value from a GPIO pin
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```js
|
||||
* let gpio = require("gpio");
|
||||
* let button = gpio.get("pc1");
|
||||
* button.init({ direction: "in" });
|
||||
* if(button.read())
|
||||
* print("hi button!!!!!");
|
||||
* ```
|
||||
*/
|
||||
static void js_gpio_read(struct mjs* mjs) {
|
||||
// get level
|
||||
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||
bool value = furi_hal_gpio_read(manager_data->pin);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a event loop contract that can be used to listen to interrupts
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```js
|
||||
* let gpio = require("gpio");
|
||||
* let button = gpio.get("pc1");
|
||||
* let event_loop = require("event_loop");
|
||||
* button.init({ direction: "in", pull: "up", inMode: "interrupt", edge: "falling" });
|
||||
* event_loop.subscribe(button.interrupt(), function (_) { print("Hi!"); });
|
||||
* event_loop.run();
|
||||
* ```
|
||||
*/
|
||||
static void js_gpio_interrupt(struct mjs* mjs) {
|
||||
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||
|
||||
// interrupt handling
|
||||
if(!manager_data->had_interrupt) {
|
||||
furi_hal_gpio_add_int_callback(
|
||||
manager_data->pin, js_gpio_int_cb, manager_data->interrupt_semaphore);
|
||||
furi_hal_gpio_enable_int_callback(manager_data->pin);
|
||||
manager_data->had_interrupt = true;
|
||||
}
|
||||
|
||||
// make contract
|
||||
JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract));
|
||||
*contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object_type = JsEventLoopObjectTypeSemaphore,
|
||||
.object = manager_data->interrupt_semaphore,
|
||||
.non_timer =
|
||||
{
|
||||
.event = FuriEventLoopEventIn,
|
||||
},
|
||||
};
|
||||
manager_data->interrupt_contract = contract;
|
||||
mjs_return(mjs, mjs_mk_foreign(mjs, contract));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reads a voltage from a GPIO pin in analog mode
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```js
|
||||
* let gpio = require("gpio");
|
||||
* let pot = gpio.get("pc0");
|
||||
* pot.init({ direction: "in", inMode: "analog" });
|
||||
* print("voltage:" pot.read_analog(), "mV");
|
||||
* ```
|
||||
*/
|
||||
static void js_gpio_read_analog(struct mjs* mjs) {
|
||||
// get mV (ADC is configured for 12 bits and 2048 mV max)
|
||||
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||
uint16_t millivolts =
|
||||
furi_hal_adc_read(manager_data->adc_handle, manager_data->adc_channel) / 2;
|
||||
mjs_return(mjs, mjs_mk_number(mjs, (double)millivolts));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns an object that manages a specified pin.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```js
|
||||
* let gpio = require("gpio");
|
||||
* let led = gpio.get("pc3");
|
||||
* ```
|
||||
*/
|
||||
static void js_gpio_get(struct mjs* mjs) {
|
||||
mjs_val_t name_arg;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&name_arg));
|
||||
const char* name_string = mjs_get_string(mjs, &name_arg, NULL);
|
||||
const GpioPinRecord* pin_record = NULL;
|
||||
|
||||
// parse input argument to a pin pointer
|
||||
if(name_string) {
|
||||
pin_record = furi_hal_resources_pin_by_name(name_string);
|
||||
} else if(mjs_is_number(name_arg)) {
|
||||
int name_int = mjs_get_int(mjs, name_arg);
|
||||
pin_record = furi_hal_resources_pin_by_number(name_int);
|
||||
} else {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Must be either a string or a number");
|
||||
}
|
||||
|
||||
if(!pin_record) JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Pin not found on device");
|
||||
if(pin_record->debug)
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Pin is used for debugging");
|
||||
|
||||
// return pin manager object
|
||||
JsGpioInst* module = JS_GET_CONTEXT(mjs);
|
||||
mjs_val_t manager = mjs_mk_object(mjs);
|
||||
JsGpioPinInst* manager_data = malloc(sizeof(JsGpioPinInst));
|
||||
manager_data->pin = pin_record->pin;
|
||||
manager_data->interrupt_semaphore = furi_semaphore_alloc(UINT32_MAX, 0);
|
||||
manager_data->adc_handle = module->adc_handle;
|
||||
manager_data->adc_channel = pin_record->channel;
|
||||
mjs_own(mjs, &manager);
|
||||
mjs_set(mjs, manager, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, manager_data));
|
||||
mjs_set(mjs, manager, "init", ~0, MJS_MK_FN(js_gpio_init));
|
||||
mjs_set(mjs, manager, "write", ~0, MJS_MK_FN(js_gpio_write));
|
||||
mjs_set(mjs, manager, "read", ~0, MJS_MK_FN(js_gpio_read));
|
||||
mjs_set(mjs, manager, "read_analog", ~0, MJS_MK_FN(js_gpio_read_analog));
|
||||
mjs_set(mjs, manager, "interrupt", ~0, MJS_MK_FN(js_gpio_interrupt));
|
||||
mjs_return(mjs, manager);
|
||||
|
||||
// remember pin
|
||||
ManagedPinsArray_push_back(module->managed_pins, manager_data);
|
||||
}
|
||||
|
||||
static void* js_gpio_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
JsEventLoop* js_loop = js_module_get(modules, "event_loop");
|
||||
if(M_UNLIKELY(!js_loop)) return NULL;
|
||||
FuriEventLoop* loop = js_event_loop_get_loop(js_loop);
|
||||
|
||||
JsGpioInst* module = malloc(sizeof(JsGpioInst));
|
||||
ManagedPinsArray_init(module->managed_pins);
|
||||
module->adc_handle = furi_hal_adc_acquire();
|
||||
module->loop = loop;
|
||||
furi_hal_adc_configure(module->adc_handle);
|
||||
|
||||
mjs_val_t gpio_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, gpio_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, module));
|
||||
mjs_set(mjs, gpio_obj, "get", ~0, MJS_MK_FN(js_gpio_get));
|
||||
*object = gpio_obj;
|
||||
|
||||
return (void*)module;
|
||||
}
|
||||
|
||||
static void js_gpio_destroy(void* inst) {
|
||||
furi_assert(inst);
|
||||
JsGpioInst* module = (JsGpioInst*)inst;
|
||||
|
||||
// reset pins
|
||||
ManagedPinsArray_it_t iterator;
|
||||
for(ManagedPinsArray_it(iterator, module->managed_pins); !ManagedPinsArray_end_p(iterator);
|
||||
ManagedPinsArray_next(iterator)) {
|
||||
JsGpioPinInst* manager_data = *ManagedPinsArray_cref(iterator);
|
||||
if(manager_data->had_interrupt) {
|
||||
furi_hal_gpio_disable_int_callback(manager_data->pin);
|
||||
furi_hal_gpio_remove_int_callback(manager_data->pin);
|
||||
}
|
||||
furi_hal_gpio_init(manager_data->pin, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
|
||||
furi_event_loop_maybe_unsubscribe(module->loop, manager_data->interrupt_semaphore);
|
||||
furi_semaphore_free(manager_data->interrupt_semaphore);
|
||||
free(manager_data->interrupt_contract);
|
||||
free(manager_data);
|
||||
}
|
||||
|
||||
// free buffers
|
||||
furi_hal_adc_release(module->adc_handle);
|
||||
ManagedPinsArray_clear(module->managed_pins);
|
||||
free(module);
|
||||
}
|
||||
|
||||
static const JsModuleDescriptor js_gpio_desc = {
|
||||
"gpio",
|
||||
js_gpio_create,
|
||||
js_gpio_destroy,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
.appid = PLUGIN_APP_ID,
|
||||
.ep_api_version = PLUGIN_API_VERSION,
|
||||
.entry_point = &js_gpio_desc,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* js_gpio_ep(void) {
|
||||
return &plugin_descriptor;
|
||||
}
|
||||
129
applications/system/js_app/modules/js_gui/dialog.c
Normal file
129
applications/system/js_app/modules/js_gui/dialog.c
Normal file
@@ -0,0 +1,129 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "js_gui.h"
|
||||
#include "../js_event_loop/js_event_loop.h"
|
||||
#include <gui/modules/dialog_ex.h>
|
||||
|
||||
#define QUEUE_LEN 2
|
||||
|
||||
typedef struct {
|
||||
FuriMessageQueue* queue;
|
||||
JsEventLoopContract contract;
|
||||
} JsDialogCtx;
|
||||
|
||||
static mjs_val_t
|
||||
input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsDialogCtx* context) {
|
||||
UNUSED(context);
|
||||
DialogExResult result;
|
||||
furi_check(furi_message_queue_get(queue, &result, 0) == FuriStatusOk);
|
||||
const char* string;
|
||||
if(result == DialogExResultLeft) {
|
||||
string = "left";
|
||||
} else if(result == DialogExResultCenter) {
|
||||
string = "center";
|
||||
} else if(result == DialogExResultRight) {
|
||||
string = "right";
|
||||
} else {
|
||||
furi_crash();
|
||||
}
|
||||
return mjs_mk_string(mjs, string, ~0, false);
|
||||
}
|
||||
|
||||
static void input_callback(DialogExResult result, JsDialogCtx* context) {
|
||||
furi_check(furi_message_queue_put(context->queue, &result, 0) == FuriStatusOk);
|
||||
}
|
||||
|
||||
static bool
|
||||
header_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(context);
|
||||
dialog_ex_set_header(dialog, value.string, 64, 0, AlignCenter, AlignTop);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
text_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(context);
|
||||
dialog_ex_set_text(dialog, value.string, 64, 32, AlignCenter, AlignCenter);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
left_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(context);
|
||||
dialog_ex_set_left_button_text(dialog, value.string);
|
||||
return true;
|
||||
}
|
||||
static bool
|
||||
center_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(context);
|
||||
dialog_ex_set_center_button_text(dialog, value.string);
|
||||
return true;
|
||||
}
|
||||
static bool
|
||||
right_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(context);
|
||||
dialog_ex_set_right_button_text(dialog, value.string);
|
||||
return true;
|
||||
}
|
||||
|
||||
static JsDialogCtx* ctx_make(struct mjs* mjs, DialogEx* dialog, mjs_val_t view_obj) {
|
||||
JsDialogCtx* context = malloc(sizeof(JsDialogCtx));
|
||||
context->queue = furi_message_queue_alloc(QUEUE_LEN, sizeof(DialogExResult));
|
||||
context->contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object_type = JsEventLoopObjectTypeQueue,
|
||||
.object = context->queue,
|
||||
.non_timer =
|
||||
{
|
||||
.event = FuriEventLoopEventIn,
|
||||
.transformer = (JsEventLoopTransformer)input_transformer,
|
||||
},
|
||||
};
|
||||
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
|
||||
dialog_ex_set_result_callback(dialog, (DialogExResultCallback)input_callback);
|
||||
dialog_ex_set_context(dialog, context);
|
||||
return context;
|
||||
}
|
||||
|
||||
static void ctx_destroy(DialogEx* input, JsDialogCtx* context, FuriEventLoop* loop) {
|
||||
UNUSED(input);
|
||||
furi_event_loop_maybe_unsubscribe(loop, context->queue);
|
||||
furi_message_queue_free(context->queue);
|
||||
free(context);
|
||||
}
|
||||
|
||||
static const JsViewDescriptor view_descriptor = {
|
||||
.alloc = (JsViewAlloc)dialog_ex_alloc,
|
||||
.free = (JsViewFree)dialog_ex_free,
|
||||
.get_view = (JsViewGetView)dialog_ex_get_view,
|
||||
.custom_make = (JsViewCustomMake)ctx_make,
|
||||
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
|
||||
.prop_cnt = 5,
|
||||
.props = {
|
||||
(JsViewPropDescriptor){
|
||||
.name = "header",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)header_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "text",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)text_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "left",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)left_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "center",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)center_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "right",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)right_assign},
|
||||
}};
|
||||
|
||||
JS_GUI_VIEW_DEF(dialog, &view_descriptor);
|
||||
12
applications/system/js_app/modules/js_gui/empty_screen.c
Normal file
12
applications/system/js_app/modules/js_gui/empty_screen.c
Normal file
@@ -0,0 +1,12 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "js_gui.h"
|
||||
#include <gui/modules/empty_screen.h>
|
||||
|
||||
static const JsViewDescriptor view_descriptor = {
|
||||
.alloc = (JsViewAlloc)empty_screen_alloc,
|
||||
.free = (JsViewFree)empty_screen_free,
|
||||
.get_view = (JsViewGetView)empty_screen_get_view,
|
||||
.prop_cnt = 0,
|
||||
.props = {},
|
||||
};
|
||||
JS_GUI_VIEW_DEF(empty_screen, &view_descriptor);
|
||||
348
applications/system/js_app/modules/js_gui/js_gui.c
Normal file
348
applications/system/js_app/modules/js_gui/js_gui.c
Normal file
@@ -0,0 +1,348 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "./js_gui.h"
|
||||
#include <furi.h>
|
||||
#include <mlib/m-array.h>
|
||||
#include <gui/view_dispatcher.h>
|
||||
#include "../js_event_loop/js_event_loop.h"
|
||||
#include <m-array.h>
|
||||
|
||||
#define EVENT_QUEUE_SIZE 16
|
||||
|
||||
typedef struct {
|
||||
uint32_t next_view_id;
|
||||
FuriEventLoop* loop;
|
||||
Gui* gui;
|
||||
ViewDispatcher* dispatcher;
|
||||
// event stuff
|
||||
JsEventLoopContract custom_contract;
|
||||
FuriMessageQueue* custom;
|
||||
JsEventLoopContract navigation_contract;
|
||||
FuriSemaphore*
|
||||
navigation; // FIXME: (-nofl) convert into callback once FuriEventLoop starts supporting this
|
||||
} JsGui;
|
||||
|
||||
// Useful for factories
|
||||
static JsGui* js_gui;
|
||||
|
||||
typedef struct {
|
||||
uint32_t id;
|
||||
const JsViewDescriptor* descriptor;
|
||||
void* specific_view;
|
||||
void* custom_data;
|
||||
} JsGuiViewData;
|
||||
|
||||
/**
|
||||
* @brief Transformer for custom events
|
||||
*/
|
||||
static mjs_val_t
|
||||
js_gui_vd_custom_transformer(struct mjs* mjs, FuriEventLoopObject* object, void* context) {
|
||||
UNUSED(context);
|
||||
furi_check(object);
|
||||
FuriMessageQueue* queue = object;
|
||||
uint32_t event;
|
||||
furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk);
|
||||
return mjs_mk_number(mjs, (double)event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ViewDispatcher custom event callback
|
||||
*/
|
||||
static bool js_gui_vd_custom_callback(void* context, uint32_t event) {
|
||||
furi_check(context);
|
||||
JsGui* module = context;
|
||||
furi_check(furi_message_queue_put(module->custom, &event, 0) == FuriStatusOk);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ViewDispatcher navigation event callback
|
||||
*/
|
||||
static bool js_gui_vd_nav_callback(void* context) {
|
||||
furi_check(context);
|
||||
JsGui* module = context;
|
||||
furi_semaphore_release(module->navigation);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `viewDispatcher.sendCustom`
|
||||
*/
|
||||
static void js_gui_vd_send_custom(struct mjs* mjs) {
|
||||
int32_t event;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&event));
|
||||
|
||||
JsGui* module = JS_GET_CONTEXT(mjs);
|
||||
view_dispatcher_send_custom_event(module->dispatcher, (uint32_t)event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `viewDispatcher.sendTo`
|
||||
*/
|
||||
static void js_gui_vd_send_to(struct mjs* mjs) {
|
||||
enum {
|
||||
SendDirToFront,
|
||||
SendDirToBack,
|
||||
} send_direction;
|
||||
JS_ENUM_MAP(send_direction, {"front", SendDirToFront}, {"back", SendDirToBack});
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ENUM(send_direction, "SendDirection"));
|
||||
|
||||
JsGui* module = JS_GET_CONTEXT(mjs);
|
||||
if(send_direction == SendDirToBack) {
|
||||
view_dispatcher_send_to_back(module->dispatcher);
|
||||
} else {
|
||||
view_dispatcher_send_to_front(module->dispatcher);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `viewDispatcher.switchTo`
|
||||
*/
|
||||
static void js_gui_vd_switch_to(struct mjs* mjs) {
|
||||
mjs_val_t view;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&view));
|
||||
JsGuiViewData* view_data = JS_GET_INST(mjs, view);
|
||||
JsGui* module = JS_GET_CONTEXT(mjs);
|
||||
view_dispatcher_switch_to_view(module->dispatcher, (uint32_t)view_data->id);
|
||||
}
|
||||
|
||||
static void* js_gui_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
// get event loop
|
||||
JsEventLoop* js_loop = js_module_get(modules, "event_loop");
|
||||
if(M_UNLIKELY(!js_loop)) return NULL;
|
||||
FuriEventLoop* loop = js_event_loop_get_loop(js_loop);
|
||||
|
||||
// create C object
|
||||
JsGui* module = malloc(sizeof(JsGui));
|
||||
module->loop = loop;
|
||||
module->gui = furi_record_open(RECORD_GUI);
|
||||
module->dispatcher = view_dispatcher_alloc_ex(loop);
|
||||
module->custom = furi_message_queue_alloc(EVENT_QUEUE_SIZE, sizeof(uint32_t));
|
||||
module->navigation = furi_semaphore_alloc(EVENT_QUEUE_SIZE, 0);
|
||||
view_dispatcher_attach_to_gui(module->dispatcher, module->gui, ViewDispatcherTypeFullscreen);
|
||||
view_dispatcher_send_to_front(module->dispatcher);
|
||||
|
||||
// subscribe to events and create contracts
|
||||
view_dispatcher_set_event_callback_context(module->dispatcher, module);
|
||||
view_dispatcher_set_custom_event_callback(module->dispatcher, js_gui_vd_custom_callback);
|
||||
view_dispatcher_set_navigation_event_callback(module->dispatcher, js_gui_vd_nav_callback);
|
||||
module->custom_contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object = module->custom,
|
||||
.object_type = JsEventLoopObjectTypeQueue,
|
||||
.non_timer =
|
||||
{
|
||||
.event = FuriEventLoopEventIn,
|
||||
.transformer = js_gui_vd_custom_transformer,
|
||||
},
|
||||
};
|
||||
module->navigation_contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object = module->navigation,
|
||||
.object_type = JsEventLoopObjectTypeSemaphore,
|
||||
.non_timer =
|
||||
{
|
||||
.event = FuriEventLoopEventIn,
|
||||
},
|
||||
};
|
||||
|
||||
// create viewDispatcher object
|
||||
mjs_val_t view_dispatcher = mjs_mk_object(mjs);
|
||||
JS_ASSIGN_MULTI(mjs, view_dispatcher) {
|
||||
JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, module));
|
||||
JS_FIELD("sendCustom", MJS_MK_FN(js_gui_vd_send_custom));
|
||||
JS_FIELD("sendTo", MJS_MK_FN(js_gui_vd_send_to));
|
||||
JS_FIELD("switchTo", MJS_MK_FN(js_gui_vd_switch_to));
|
||||
JS_FIELD("custom", mjs_mk_foreign(mjs, &module->custom_contract));
|
||||
JS_FIELD("navigation", mjs_mk_foreign(mjs, &module->navigation_contract));
|
||||
}
|
||||
|
||||
// create API object
|
||||
mjs_val_t api = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, api, "viewDispatcher", ~0, view_dispatcher);
|
||||
|
||||
*object = api;
|
||||
js_gui = module;
|
||||
return module;
|
||||
}
|
||||
|
||||
static void js_gui_destroy(void* inst) {
|
||||
furi_assert(inst);
|
||||
JsGui* module = inst;
|
||||
|
||||
view_dispatcher_free(module->dispatcher);
|
||||
furi_event_loop_maybe_unsubscribe(module->loop, module->custom);
|
||||
furi_event_loop_maybe_unsubscribe(module->loop, module->navigation);
|
||||
furi_message_queue_free(module->custom);
|
||||
furi_semaphore_free(module->navigation);
|
||||
|
||||
furi_record_close(RECORD_GUI);
|
||||
free(module);
|
||||
js_gui = NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Assigns a `View` property. Not available from JS.
|
||||
*/
|
||||
static bool
|
||||
js_gui_view_assign(struct mjs* mjs, const char* name, mjs_val_t value, JsGuiViewData* data) {
|
||||
const JsViewDescriptor* descriptor = data->descriptor;
|
||||
for(size_t i = 0; i < descriptor->prop_cnt; i++) {
|
||||
JsViewPropDescriptor prop = descriptor->props[i];
|
||||
if(strcmp(prop.name, name) != 0) continue;
|
||||
|
||||
// convert JS value to C
|
||||
JsViewPropValue c_value;
|
||||
const char* expected_type = NULL;
|
||||
switch(prop.type) {
|
||||
case JsViewPropTypeNumber: {
|
||||
if(!mjs_is_number(value)) {
|
||||
expected_type = "number";
|
||||
break;
|
||||
}
|
||||
c_value = (JsViewPropValue){.number = mjs_get_int32(mjs, value)};
|
||||
} break;
|
||||
case JsViewPropTypeString: {
|
||||
if(!mjs_is_string(value)) {
|
||||
expected_type = "string";
|
||||
break;
|
||||
}
|
||||
c_value = (JsViewPropValue){.string = mjs_get_string(mjs, &value, NULL)};
|
||||
} break;
|
||||
case JsViewPropTypeArr: {
|
||||
if(!mjs_is_array(value)) {
|
||||
expected_type = "array";
|
||||
break;
|
||||
}
|
||||
c_value = (JsViewPropValue){.array = value};
|
||||
} break;
|
||||
}
|
||||
|
||||
if(expected_type) {
|
||||
mjs_prepend_errorf(
|
||||
mjs, MJS_BAD_ARGS_ERROR, "view prop \"%s\" requires %s value", name, expected_type);
|
||||
return false;
|
||||
} else {
|
||||
return prop.assign(mjs, data->specific_view, c_value, data->custom_data);
|
||||
}
|
||||
}
|
||||
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "view has no prop named \"%s\"", name);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `View.set`
|
||||
*/
|
||||
static void js_gui_view_set(struct mjs* mjs) {
|
||||
const char* name;
|
||||
mjs_val_t value;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&name), JS_ARG_ANY(&value));
|
||||
JsGuiViewData* data = JS_GET_CONTEXT(mjs);
|
||||
bool success = js_gui_view_assign(mjs, name, value, data);
|
||||
UNUSED(success);
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `View` destructor
|
||||
*/
|
||||
static void js_gui_view_destructor(struct mjs* mjs, mjs_val_t obj) {
|
||||
JsGuiViewData* data = JS_GET_INST(mjs, obj);
|
||||
view_dispatcher_remove_view(js_gui->dispatcher, data->id);
|
||||
if(data->descriptor->custom_destroy)
|
||||
data->descriptor->custom_destroy(data->specific_view, data->custom_data, js_gui->loop);
|
||||
data->descriptor->free(data->specific_view);
|
||||
free(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Creates a `View` object from a descriptor. Not available from JS.
|
||||
*/
|
||||
static mjs_val_t js_gui_make_view(struct mjs* mjs, const JsViewDescriptor* descriptor) {
|
||||
void* specific_view = descriptor->alloc();
|
||||
View* view = descriptor->get_view(specific_view);
|
||||
uint32_t view_id = js_gui->next_view_id++;
|
||||
view_dispatcher_add_view(js_gui->dispatcher, view_id, view);
|
||||
|
||||
// generic view API
|
||||
mjs_val_t view_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, view_obj, "set", ~0, MJS_MK_FN(js_gui_view_set));
|
||||
|
||||
// object data
|
||||
JsGuiViewData* data = malloc(sizeof(JsGuiViewData));
|
||||
*data = (JsGuiViewData){
|
||||
.descriptor = descriptor,
|
||||
.id = view_id,
|
||||
.specific_view = specific_view,
|
||||
.custom_data =
|
||||
descriptor->custom_make ? descriptor->custom_make(mjs, specific_view, view_obj) : NULL,
|
||||
};
|
||||
mjs_set(mjs, view_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, data));
|
||||
mjs_set(mjs, view_obj, MJS_DESTRUCTOR_PROP_NAME, ~0, MJS_MK_FN(js_gui_view_destructor));
|
||||
|
||||
return view_obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `ViewFactory.make`
|
||||
*/
|
||||
static void js_gui_vf_make(struct mjs* mjs) {
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||
const JsViewDescriptor* descriptor = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, js_gui_make_view(mjs, descriptor));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `ViewFactory.makeWith`
|
||||
*/
|
||||
static void js_gui_vf_make_with(struct mjs* mjs) {
|
||||
mjs_val_t props;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&props));
|
||||
const JsViewDescriptor* descriptor = JS_GET_CONTEXT(mjs);
|
||||
|
||||
// make the object like normal
|
||||
mjs_val_t view_obj = js_gui_make_view(mjs, descriptor);
|
||||
JsGuiViewData* data = JS_GET_INST(mjs, view_obj);
|
||||
|
||||
// assign properties one by one
|
||||
mjs_val_t key, iter = MJS_UNDEFINED;
|
||||
while((key = mjs_next(mjs, props, &iter)) != MJS_UNDEFINED) {
|
||||
furi_check(mjs_is_string(key));
|
||||
const char* name = mjs_get_string(mjs, &key, NULL);
|
||||
mjs_val_t value = mjs_get(mjs, props, name, ~0);
|
||||
|
||||
if(!js_gui_view_assign(mjs, name, value, data)) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
mjs_return(mjs, view_obj);
|
||||
}
|
||||
|
||||
mjs_val_t js_gui_make_view_factory(struct mjs* mjs, const JsViewDescriptor* view_descriptor) {
|
||||
mjs_val_t factory = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, factory, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, (void*)view_descriptor));
|
||||
mjs_set(mjs, factory, "make", ~0, MJS_MK_FN(js_gui_vf_make));
|
||||
mjs_set(mjs, factory, "makeWith", ~0, MJS_MK_FN(js_gui_vf_make_with));
|
||||
return factory;
|
||||
}
|
||||
|
||||
extern const ElfApiInterface js_gui_hashtable_api_interface;
|
||||
|
||||
static const JsModuleDescriptor js_gui_desc = {
|
||||
"gui",
|
||||
js_gui_create,
|
||||
js_gui_destroy,
|
||||
&js_gui_hashtable_api_interface,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
.appid = PLUGIN_APP_ID,
|
||||
.ep_api_version = PLUGIN_API_VERSION,
|
||||
.entry_point = &js_gui_desc,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* js_gui_ep(void) {
|
||||
return &plugin_descriptor;
|
||||
}
|
||||
116
applications/system/js_app/modules/js_gui/js_gui.h
Normal file
116
applications/system/js_app/modules/js_gui/js_gui.h
Normal file
@@ -0,0 +1,116 @@
|
||||
#include "../../js_modules.h"
|
||||
#include <gui/view.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
JsViewPropTypeString,
|
||||
JsViewPropTypeNumber,
|
||||
JsViewPropTypeArr,
|
||||
} JsViewPropType;
|
||||
|
||||
typedef union {
|
||||
const char* string;
|
||||
int32_t number;
|
||||
mjs_val_t array;
|
||||
} JsViewPropValue;
|
||||
|
||||
/**
|
||||
* @brief Assigns a value to a view property
|
||||
*
|
||||
* The name and the type are implicit and defined in the property descriptor
|
||||
*/
|
||||
typedef bool (
|
||||
*JsViewPropAssign)(struct mjs* mjs, void* specific_view, JsViewPropValue value, void* context);
|
||||
|
||||
/** @brief Property descriptor */
|
||||
typedef struct {
|
||||
const char* name; //<! Property name, as visible from JS
|
||||
JsViewPropType type; // <! Property type, ensured by the GUI module
|
||||
JsViewPropAssign assign; // <! Property assignment callback
|
||||
} JsViewPropDescriptor;
|
||||
|
||||
// View method signatures
|
||||
|
||||
/** @brief View's `_alloc` method */
|
||||
typedef void* (*JsViewAlloc)(void);
|
||||
/** @brief View's `_get_view` method */
|
||||
typedef View* (*JsViewGetView)(void* specific_view);
|
||||
/** @brief View's `_free` method */
|
||||
typedef void (*JsViewFree)(void* specific_view);
|
||||
|
||||
// Glue code method signatures
|
||||
|
||||
/** @brief Context instantiation for glue code */
|
||||
typedef void* (*JsViewCustomMake)(struct mjs* mjs, void* specific_view, mjs_val_t view_obj);
|
||||
/** @brief Context destruction for glue code */
|
||||
typedef void (*JsViewCustomDestroy)(void* specific_view, void* custom_state, FuriEventLoop* loop);
|
||||
|
||||
/**
|
||||
* @brief Descriptor for a JS view
|
||||
*
|
||||
* Contains:
|
||||
* - Pointers to generic view methods (`alloc`, `get_view` and `free`)
|
||||
* - Pointers to glue code context ctor/dtor methods (`custom_make`,
|
||||
* `custom_destroy`)
|
||||
* - Descriptors of properties visible from JS (`prop_cnt`, `props`)
|
||||
*
|
||||
* `js_gui` uses this descriptor to produce view factories and views.
|
||||
*/
|
||||
typedef struct {
|
||||
JsViewAlloc alloc;
|
||||
JsViewGetView get_view;
|
||||
JsViewFree free;
|
||||
JsViewCustomMake custom_make; // <! May be NULL
|
||||
JsViewCustomDestroy custom_destroy; // <! May be NULL
|
||||
size_t prop_cnt; //<! Number of properties visible from JS
|
||||
JsViewPropDescriptor props[]; // <! Descriptors of properties visible from JS
|
||||
} JsViewDescriptor;
|
||||
|
||||
// Callback ordering:
|
||||
// alloc -> get_view -> [custom_make (if set)] -> props[i].assign -> [custom_destroy (if_set)] -> free
|
||||
// \_______________ creation ________________/ \___ usage ___/ \_________ destruction _________/
|
||||
|
||||
/**
|
||||
* @brief Creates a JS `ViewFactory` object
|
||||
*
|
||||
* This function is intended to be used by individual view adapter modules that
|
||||
* wish to create a unified JS API interface in a declarative way. Usually this
|
||||
* is done via the `JS_GUI_VIEW_DEF` macro which hides all the boilerplate.
|
||||
*
|
||||
* The `ViewFactory` object exposes two methods, `make` and `makeWith`, each
|
||||
* returning a `View` object. These objects fully comply with the expectations
|
||||
* of the `ViewDispatcher`, TS type definitions and the proposed Flipper JS
|
||||
* coding style.
|
||||
*/
|
||||
mjs_val_t js_gui_make_view_factory(struct mjs* mjs, const JsViewDescriptor* view_descriptor);
|
||||
|
||||
/**
|
||||
* @brief Defines a module implementing `View` glue code
|
||||
*/
|
||||
#define JS_GUI_VIEW_DEF(name, descriptor) \
|
||||
static void* view_mod_ctor(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { \
|
||||
UNUSED(modules); \
|
||||
*object = js_gui_make_view_factory(mjs, descriptor); \
|
||||
return NULL; \
|
||||
} \
|
||||
static const JsModuleDescriptor js_mod_desc = { \
|
||||
"gui__" #name, \
|
||||
view_mod_ctor, \
|
||||
NULL, \
|
||||
NULL, \
|
||||
}; \
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = { \
|
||||
.appid = PLUGIN_APP_ID, \
|
||||
.ep_api_version = PLUGIN_API_VERSION, \
|
||||
.entry_point = &js_mod_desc, \
|
||||
}; \
|
||||
const FlipperAppPluginDescriptor* js_view_##name##_ep(void) { \
|
||||
return &plugin_descriptor; \
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,16 @@
|
||||
#include <flipper_application/api_hashtable/api_hashtable.h>
|
||||
#include <flipper_application/api_hashtable/compilesort.hpp>
|
||||
|
||||
#include "js_gui_api_table_i.h"
|
||||
|
||||
static_assert(!has_hash_collisions(js_gui_api_table), "Detected API method hash collision!");
|
||||
|
||||
extern "C" constexpr HashtableApiInterface js_gui_hashtable_api_interface{
|
||||
{
|
||||
.api_version_major = 0,
|
||||
.api_version_minor = 0,
|
||||
.resolver_callback = &elf_resolve_from_hashtable,
|
||||
},
|
||||
js_gui_api_table.cbegin(),
|
||||
js_gui_api_table.cend(),
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
#include "js_gui.h"
|
||||
|
||||
static constexpr auto js_gui_api_table = sort(create_array_t<sym_entry>(
|
||||
API_METHOD(js_gui_make_view_factory, mjs_val_t, (struct mjs*, const JsViewDescriptor*))));
|
||||
12
applications/system/js_app/modules/js_gui/loading.c
Normal file
12
applications/system/js_app/modules/js_gui/loading.c
Normal file
@@ -0,0 +1,12 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "js_gui.h"
|
||||
#include <gui/modules/loading.h>
|
||||
|
||||
static const JsViewDescriptor view_descriptor = {
|
||||
.alloc = (JsViewAlloc)loading_alloc,
|
||||
.free = (JsViewFree)loading_free,
|
||||
.get_view = (JsViewGetView)loading_get_view,
|
||||
.prop_cnt = 0,
|
||||
.props = {},
|
||||
};
|
||||
JS_GUI_VIEW_DEF(loading, &view_descriptor);
|
||||
87
applications/system/js_app/modules/js_gui/submenu.c
Normal file
87
applications/system/js_app/modules/js_gui/submenu.c
Normal file
@@ -0,0 +1,87 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "js_gui.h"
|
||||
#include "../js_event_loop/js_event_loop.h"
|
||||
#include <gui/modules/submenu.h>
|
||||
|
||||
#define QUEUE_LEN 2
|
||||
|
||||
typedef struct {
|
||||
FuriMessageQueue* queue;
|
||||
JsEventLoopContract contract;
|
||||
} JsSubmenuCtx;
|
||||
|
||||
static mjs_val_t choose_transformer(struct mjs* mjs, FuriMessageQueue* queue, void* context) {
|
||||
UNUSED(context);
|
||||
uint32_t index;
|
||||
furi_check(furi_message_queue_get(queue, &index, 0) == FuriStatusOk);
|
||||
return mjs_mk_number(mjs, (double)index);
|
||||
}
|
||||
|
||||
void choose_callback(void* context, uint32_t index) {
|
||||
JsSubmenuCtx* ctx = context;
|
||||
furi_check(furi_message_queue_put(ctx->queue, &index, 0) == FuriStatusOk);
|
||||
}
|
||||
|
||||
static bool
|
||||
header_assign(struct mjs* mjs, Submenu* submenu, JsViewPropValue value, void* context) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(context);
|
||||
submenu_set_header(submenu, value.string);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool items_assign(struct mjs* mjs, Submenu* submenu, JsViewPropValue value, void* context) {
|
||||
UNUSED(mjs);
|
||||
submenu_reset(submenu);
|
||||
size_t len = mjs_array_length(mjs, value.array);
|
||||
for(size_t i = 0; i < len; i++) {
|
||||
mjs_val_t item = mjs_array_get(mjs, value.array, i);
|
||||
if(!mjs_is_string(item)) return false;
|
||||
submenu_add_item(submenu, mjs_get_string(mjs, &item, NULL), i, choose_callback, context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static JsSubmenuCtx* ctx_make(struct mjs* mjs, Submenu* input, mjs_val_t view_obj) {
|
||||
UNUSED(input);
|
||||
JsSubmenuCtx* context = malloc(sizeof(JsSubmenuCtx));
|
||||
context->queue = furi_message_queue_alloc(QUEUE_LEN, sizeof(uint32_t));
|
||||
context->contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object_type = JsEventLoopObjectTypeQueue,
|
||||
.object = context->queue,
|
||||
.non_timer =
|
||||
{
|
||||
.event = FuriEventLoopEventIn,
|
||||
.transformer = (JsEventLoopTransformer)choose_transformer,
|
||||
},
|
||||
};
|
||||
mjs_set(mjs, view_obj, "chosen", ~0, mjs_mk_foreign(mjs, &context->contract));
|
||||
return context;
|
||||
}
|
||||
|
||||
static void ctx_destroy(Submenu* input, JsSubmenuCtx* context, FuriEventLoop* loop) {
|
||||
UNUSED(input);
|
||||
furi_event_loop_maybe_unsubscribe(loop, context->queue);
|
||||
furi_message_queue_free(context->queue);
|
||||
free(context);
|
||||
}
|
||||
|
||||
static const JsViewDescriptor view_descriptor = {
|
||||
.alloc = (JsViewAlloc)submenu_alloc,
|
||||
.free = (JsViewFree)submenu_free,
|
||||
.get_view = (JsViewGetView)submenu_get_view,
|
||||
.custom_make = (JsViewCustomMake)ctx_make,
|
||||
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
|
||||
.prop_cnt = 2,
|
||||
.props = {
|
||||
(JsViewPropDescriptor){
|
||||
.name = "header",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)header_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "items",
|
||||
.type = JsViewPropTypeArr,
|
||||
.assign = (JsViewPropAssign)items_assign},
|
||||
}};
|
||||
JS_GUI_VIEW_DEF(submenu, &view_descriptor);
|
||||
78
applications/system/js_app/modules/js_gui/text_box.c
Normal file
78
applications/system/js_app/modules/js_gui/text_box.c
Normal file
@@ -0,0 +1,78 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "js_gui.h"
|
||||
#include <gui/modules/text_box.h>
|
||||
|
||||
static bool
|
||||
text_assign(struct mjs* mjs, TextBox* text_box, JsViewPropValue value, FuriString* context) {
|
||||
UNUSED(mjs);
|
||||
furi_string_set(context, value.string);
|
||||
text_box_set_text(text_box, furi_string_get_cstr(context));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool font_assign(struct mjs* mjs, TextBox* text_box, JsViewPropValue value, void* context) {
|
||||
UNUSED(context);
|
||||
TextBoxFont font;
|
||||
if(strcasecmp(value.string, "hex") == 0) {
|
||||
font = TextBoxFontHex;
|
||||
} else if(strcasecmp(value.string, "text") == 0) {
|
||||
font = TextBoxFontText;
|
||||
} else {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "must be one of: \"text\", \"hex\"");
|
||||
return false;
|
||||
}
|
||||
text_box_set_font(text_box, font);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
focus_assign(struct mjs* mjs, TextBox* text_box, JsViewPropValue value, void* context) {
|
||||
UNUSED(context);
|
||||
TextBoxFocus focus;
|
||||
if(strcasecmp(value.string, "start") == 0) {
|
||||
focus = TextBoxFocusStart;
|
||||
} else if(strcasecmp(value.string, "end") == 0) {
|
||||
focus = TextBoxFocusEnd;
|
||||
} else {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "must be one of: \"start\", \"end\"");
|
||||
return false;
|
||||
}
|
||||
text_box_set_focus(text_box, focus);
|
||||
return true;
|
||||
}
|
||||
|
||||
FuriString* ctx_make(struct mjs* mjs, TextBox* specific_view, mjs_val_t view_obj) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(specific_view);
|
||||
UNUSED(view_obj);
|
||||
return furi_string_alloc();
|
||||
}
|
||||
|
||||
void ctx_destroy(TextBox* specific_view, FuriString* context, FuriEventLoop* loop) {
|
||||
UNUSED(specific_view);
|
||||
UNUSED(loop);
|
||||
furi_string_free(context);
|
||||
}
|
||||
|
||||
static const JsViewDescriptor view_descriptor = {
|
||||
.alloc = (JsViewAlloc)text_box_alloc,
|
||||
.free = (JsViewFree)text_box_free,
|
||||
.get_view = (JsViewGetView)text_box_get_view,
|
||||
.custom_make = (JsViewCustomMake)ctx_make,
|
||||
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
|
||||
.prop_cnt = 3,
|
||||
.props = {
|
||||
(JsViewPropDescriptor){
|
||||
.name = "text",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)text_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "font",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)font_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "focus",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)focus_assign},
|
||||
}};
|
||||
JS_GUI_VIEW_DEF(text_box, &view_descriptor);
|
||||
120
applications/system/js_app/modules/js_gui/text_input.c
Normal file
120
applications/system/js_app/modules/js_gui/text_input.c
Normal file
@@ -0,0 +1,120 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "js_gui.h"
|
||||
#include "../js_event_loop/js_event_loop.h"
|
||||
#include <gui/modules/text_input.h>
|
||||
|
||||
#define DEFAULT_BUF_SZ 33
|
||||
|
||||
typedef struct {
|
||||
char* buffer;
|
||||
size_t buffer_size;
|
||||
FuriString* header;
|
||||
FuriSemaphore* input_semaphore;
|
||||
JsEventLoopContract contract;
|
||||
} JsKbdContext;
|
||||
|
||||
static mjs_val_t
|
||||
input_transformer(struct mjs* mjs, FuriSemaphore* semaphore, JsKbdContext* context) {
|
||||
furi_check(furi_semaphore_acquire(semaphore, 0) == FuriStatusOk);
|
||||
return mjs_mk_string(mjs, context->buffer, ~0, true);
|
||||
}
|
||||
|
||||
static void input_callback(JsKbdContext* context) {
|
||||
furi_semaphore_release(context->input_semaphore);
|
||||
}
|
||||
|
||||
static bool
|
||||
header_assign(struct mjs* mjs, TextInput* input, JsViewPropValue value, JsKbdContext* context) {
|
||||
UNUSED(mjs);
|
||||
furi_string_set(context->header, value.string);
|
||||
text_input_set_header_text(input, furi_string_get_cstr(context->header));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool min_len_assign(
|
||||
struct mjs* mjs,
|
||||
TextInput* input,
|
||||
JsViewPropValue value,
|
||||
JsKbdContext* context) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(context);
|
||||
text_input_set_minimum_length(input, (size_t)value.number);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool max_len_assign(
|
||||
struct mjs* mjs,
|
||||
TextInput* input,
|
||||
JsViewPropValue value,
|
||||
JsKbdContext* context) {
|
||||
UNUSED(mjs);
|
||||
context->buffer_size = (size_t)(value.number + 1);
|
||||
context->buffer = realloc(context->buffer, context->buffer_size); //-V701
|
||||
text_input_set_result_callback(
|
||||
input,
|
||||
(TextInputCallback)input_callback,
|
||||
context,
|
||||
context->buffer,
|
||||
context->buffer_size,
|
||||
true);
|
||||
return true;
|
||||
}
|
||||
|
||||
static JsKbdContext* ctx_make(struct mjs* mjs, TextInput* input, mjs_val_t view_obj) {
|
||||
UNUSED(input);
|
||||
JsKbdContext* context = malloc(sizeof(JsKbdContext));
|
||||
*context = (JsKbdContext){
|
||||
.buffer_size = DEFAULT_BUF_SZ,
|
||||
.buffer = malloc(DEFAULT_BUF_SZ),
|
||||
.header = furi_string_alloc(),
|
||||
.input_semaphore = furi_semaphore_alloc(1, 0),
|
||||
};
|
||||
context->contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object_type = JsEventLoopObjectTypeSemaphore,
|
||||
.object = context->input_semaphore,
|
||||
.non_timer =
|
||||
{
|
||||
.event = FuriEventLoopEventIn,
|
||||
.transformer = (JsEventLoopTransformer)input_transformer,
|
||||
.transformer_context = context,
|
||||
},
|
||||
};
|
||||
UNUSED(mjs);
|
||||
UNUSED(view_obj);
|
||||
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
|
||||
return context;
|
||||
}
|
||||
|
||||
static void ctx_destroy(TextInput* input, JsKbdContext* context, FuriEventLoop* loop) {
|
||||
UNUSED(input);
|
||||
furi_event_loop_maybe_unsubscribe(loop, context->input_semaphore);
|
||||
furi_semaphore_free(context->input_semaphore);
|
||||
furi_string_free(context->header);
|
||||
free(context->buffer);
|
||||
free(context);
|
||||
}
|
||||
|
||||
static const JsViewDescriptor view_descriptor = {
|
||||
.alloc = (JsViewAlloc)text_input_alloc,
|
||||
.free = (JsViewFree)text_input_free,
|
||||
.get_view = (JsViewGetView)text_input_get_view,
|
||||
.custom_make = (JsViewCustomMake)ctx_make,
|
||||
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
|
||||
.prop_cnt = 3,
|
||||
.props = {
|
||||
(JsViewPropDescriptor){
|
||||
.name = "header",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)header_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "minLength",
|
||||
.type = JsViewPropTypeNumber,
|
||||
.assign = (JsViewPropAssign)min_len_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "maxLength",
|
||||
.type = JsViewPropTypeNumber,
|
||||
.assign = (JsViewPropAssign)max_len_assign},
|
||||
}};
|
||||
|
||||
JS_GUI_VIEW_DEF(text_input, &view_descriptor);
|
||||
@@ -305,7 +305,8 @@ void js_math_trunc(struct mjs* mjs) {
|
||||
mjs_return(mjs, mjs_mk_number(mjs, x < (double)0. ? ceil(x) : floor(x)));
|
||||
}
|
||||
|
||||
static void* js_math_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
static void* js_math_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
mjs_val_t math_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, math_obj, "is_equal", ~0, MJS_MK_FN(js_math_is_equal));
|
||||
mjs_set(mjs, math_obj, "abs", ~0, MJS_MK_FN(js_math_abs));
|
||||
@@ -342,6 +343,7 @@ static const JsModuleDescriptor js_math_desc = {
|
||||
"math",
|
||||
js_math_create,
|
||||
NULL,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
|
||||
@@ -75,7 +75,8 @@ static void js_notify_blink(struct mjs* mjs) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void* js_notification_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
static void* js_notification_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
|
||||
mjs_val_t notify_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, notify_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, notification));
|
||||
@@ -96,6 +97,7 @@ static const JsModuleDescriptor js_notification_desc = {
|
||||
"notification",
|
||||
js_notification_create,
|
||||
js_notification_destroy,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
|
||||
@@ -573,7 +573,8 @@ static void js_serial_expect(struct mjs* mjs) {
|
||||
}
|
||||
}
|
||||
|
||||
static void* js_serial_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
static void* js_serial_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
JsSerialInst* js_serial = malloc(sizeof(JsSerialInst));
|
||||
js_serial->mjs = mjs;
|
||||
mjs_val_t serial_obj = mjs_mk_object(mjs);
|
||||
@@ -606,6 +607,7 @@ static const JsModuleDescriptor js_serial_desc = {
|
||||
"serial",
|
||||
js_serial_create,
|
||||
js_serial_destroy,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
|
||||
383
applications/system/js_app/modules/js_storage.c
Normal file
383
applications/system/js_app/modules/js_storage.c
Normal file
@@ -0,0 +1,383 @@
|
||||
#include "../js_modules.h" // IWYU pragma: keep
|
||||
#include <path.h>
|
||||
|
||||
// ---=== file ops ===---
|
||||
|
||||
static void js_storage_file_close(struct mjs* mjs) {
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_close(file)));
|
||||
}
|
||||
|
||||
static void js_storage_file_is_open(struct mjs* mjs) {
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_is_open(file)));
|
||||
}
|
||||
|
||||
static void js_storage_file_read(struct mjs* mjs) {
|
||||
enum {
|
||||
ReadModeAscii,
|
||||
ReadModeBinary,
|
||||
} read_mode;
|
||||
JS_ENUM_MAP(read_mode, {"ascii", ReadModeAscii}, {"binary", ReadModeBinary});
|
||||
int32_t length;
|
||||
JS_FETCH_ARGS_OR_RETURN(
|
||||
mjs, JS_EXACTLY, JS_ARG_ENUM(read_mode, "ReadMode"), JS_ARG_INT32(&length));
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
char buffer[length];
|
||||
size_t actually_read = storage_file_read(file, buffer, length);
|
||||
if(read_mode == ReadModeAscii) {
|
||||
mjs_return(mjs, mjs_mk_string(mjs, buffer, actually_read, true));
|
||||
} else if(read_mode == ReadModeBinary) {
|
||||
mjs_return(mjs, mjs_mk_array_buf(mjs, buffer, actually_read));
|
||||
}
|
||||
}
|
||||
|
||||
static void js_storage_file_write(struct mjs* mjs) {
|
||||
mjs_val_t data;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&data));
|
||||
const void* buf;
|
||||
size_t len;
|
||||
if(mjs_is_string(data)) {
|
||||
buf = mjs_get_string(mjs, &data, &len);
|
||||
} else if(mjs_is_array_buf(data)) {
|
||||
buf = mjs_array_buf_get_ptr(mjs, data, &len);
|
||||
} else {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "argument 0: expected string or ArrayBuffer");
|
||||
}
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_number(mjs, storage_file_write(file, buf, len)));
|
||||
}
|
||||
|
||||
static void js_storage_file_seek_relative(struct mjs* mjs) {
|
||||
int32_t offset;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&offset));
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_seek(file, offset, false)));
|
||||
}
|
||||
|
||||
static void js_storage_file_seek_absolute(struct mjs* mjs) {
|
||||
int32_t offset;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&offset));
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_seek(file, offset, true)));
|
||||
}
|
||||
|
||||
static void js_storage_file_tell(struct mjs* mjs) {
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_number(mjs, storage_file_tell(file)));
|
||||
}
|
||||
|
||||
static void js_storage_file_truncate(struct mjs* mjs) {
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_truncate(file)));
|
||||
}
|
||||
|
||||
static void js_storage_file_size(struct mjs* mjs) {
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_number(mjs, storage_file_size(file)));
|
||||
}
|
||||
|
||||
static void js_storage_file_eof(struct mjs* mjs) {
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_eof(file)));
|
||||
}
|
||||
|
||||
static void js_storage_file_copy_to(struct mjs* mjs) {
|
||||
File* source = JS_GET_CONTEXT(mjs);
|
||||
mjs_val_t dest_obj;
|
||||
int32_t bytes;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&dest_obj), JS_ARG_INT32(&bytes));
|
||||
File* destination = JS_GET_INST(mjs, dest_obj);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_copy_to_file(source, destination, bytes)));
|
||||
}
|
||||
|
||||
// ---=== top-level file ops ===---
|
||||
|
||||
// common destructor for file and dir objects
|
||||
static void js_storage_file_destructor(struct mjs* mjs, mjs_val_t obj) {
|
||||
File* file = JS_GET_INST(mjs, obj);
|
||||
storage_file_free(file);
|
||||
}
|
||||
|
||||
static void js_storage_open_file(struct mjs* mjs) {
|
||||
const char* path;
|
||||
FS_AccessMode access_mode;
|
||||
FS_OpenMode open_mode;
|
||||
JS_ENUM_MAP(access_mode, {"r", FSAM_READ}, {"w", FSAM_WRITE}, {"rw", FSAM_READ_WRITE});
|
||||
JS_ENUM_MAP(
|
||||
open_mode,
|
||||
{"open_existing", FSOM_OPEN_EXISTING},
|
||||
{"open_always", FSOM_OPEN_ALWAYS},
|
||||
{"open_append", FSOM_OPEN_APPEND},
|
||||
{"create_new", FSOM_CREATE_NEW},
|
||||
{"create_always", FSOM_CREATE_ALWAYS});
|
||||
JS_FETCH_ARGS_OR_RETURN(
|
||||
mjs,
|
||||
JS_EXACTLY,
|
||||
JS_ARG_STR(&path),
|
||||
JS_ARG_ENUM(access_mode, "AccessMode"),
|
||||
JS_ARG_ENUM(open_mode, "OpenMode"));
|
||||
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
File* file = storage_file_alloc(storage);
|
||||
if(!storage_file_open(file, path, access_mode, open_mode)) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
mjs_val_t file_obj = mjs_mk_object(mjs);
|
||||
JS_ASSIGN_MULTI(mjs, file_obj) {
|
||||
JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, file));
|
||||
JS_FIELD(MJS_DESTRUCTOR_PROP_NAME, MJS_MK_FN(js_storage_file_destructor));
|
||||
JS_FIELD("close", MJS_MK_FN(js_storage_file_close));
|
||||
JS_FIELD("isOpen", MJS_MK_FN(js_storage_file_is_open));
|
||||
JS_FIELD("read", MJS_MK_FN(js_storage_file_read));
|
||||
JS_FIELD("write", MJS_MK_FN(js_storage_file_write));
|
||||
JS_FIELD("seekRelative", MJS_MK_FN(js_storage_file_seek_relative));
|
||||
JS_FIELD("seekAbsolute", MJS_MK_FN(js_storage_file_seek_absolute));
|
||||
JS_FIELD("tell", MJS_MK_FN(js_storage_file_tell));
|
||||
JS_FIELD("truncate", MJS_MK_FN(js_storage_file_truncate));
|
||||
JS_FIELD("size", MJS_MK_FN(js_storage_file_size));
|
||||
JS_FIELD("eof", MJS_MK_FN(js_storage_file_eof));
|
||||
JS_FIELD("copyTo", MJS_MK_FN(js_storage_file_copy_to));
|
||||
}
|
||||
mjs_return(mjs, file_obj);
|
||||
}
|
||||
|
||||
static void js_storage_file_exists(struct mjs* mjs) {
|
||||
const char* path;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_exists(storage, path)));
|
||||
}
|
||||
|
||||
// ---=== dir ops ===---
|
||||
|
||||
static void js_storage_read_directory(struct mjs* mjs) {
|
||||
const char* path;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
File* dir = storage_file_alloc(storage);
|
||||
if(!storage_dir_open(dir, path)) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
FileInfo file_info;
|
||||
char name[128];
|
||||
FuriString* file_path = furi_string_alloc_set_str(path);
|
||||
size_t path_size = furi_string_size(file_path);
|
||||
uint32_t timestamp;
|
||||
|
||||
mjs_val_t ret = mjs_mk_array(mjs);
|
||||
while(storage_dir_read(dir, &file_info, name, sizeof(name))) {
|
||||
furi_string_left(file_path, path_size);
|
||||
path_append(file_path, name);
|
||||
furi_check(
|
||||
storage_common_timestamp(storage, furi_string_get_cstr(file_path), ×tamp) ==
|
||||
FSE_OK);
|
||||
mjs_val_t obj = mjs_mk_object(mjs);
|
||||
JS_ASSIGN_MULTI(mjs, obj) {
|
||||
JS_FIELD("path", mjs_mk_string(mjs, name, ~0, true));
|
||||
JS_FIELD("isDirectory", mjs_mk_boolean(mjs, file_info_is_dir(&file_info)));
|
||||
JS_FIELD("size", mjs_mk_number(mjs, file_info.size));
|
||||
JS_FIELD("timestamp", mjs_mk_number(mjs, timestamp));
|
||||
}
|
||||
mjs_array_push(mjs, ret, obj);
|
||||
}
|
||||
|
||||
storage_file_free(dir);
|
||||
furi_string_free(file_path);
|
||||
mjs_return(mjs, ret);
|
||||
}
|
||||
|
||||
static void js_storage_directory_exists(struct mjs* mjs) {
|
||||
const char* path;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_dir_exists(storage, path)));
|
||||
}
|
||||
|
||||
static void js_storage_make_directory(struct mjs* mjs) {
|
||||
const char* path;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_mkdir(storage, path)));
|
||||
}
|
||||
|
||||
// ---=== common ops ===---
|
||||
|
||||
static void js_storage_file_or_dir_exists(struct mjs* mjs) {
|
||||
const char* path;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_exists(storage, path)));
|
||||
}
|
||||
|
||||
static void js_storage_stat(struct mjs* mjs) {
|
||||
const char* path;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
FileInfo file_info;
|
||||
uint32_t timestamp;
|
||||
if((storage_common_stat(storage, path, &file_info) |
|
||||
storage_common_timestamp(storage, path, ×tamp)) != FSE_OK) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
mjs_val_t ret = mjs_mk_object(mjs);
|
||||
JS_ASSIGN_MULTI(mjs, ret) {
|
||||
JS_FIELD("path", mjs_mk_string(mjs, path, ~0, 1));
|
||||
JS_FIELD("isDirectory", mjs_mk_boolean(mjs, file_info_is_dir(&file_info)));
|
||||
JS_FIELD("size", mjs_mk_number(mjs, file_info.size));
|
||||
JS_FIELD("accessTime", mjs_mk_number(mjs, timestamp));
|
||||
}
|
||||
mjs_return(mjs, ret);
|
||||
}
|
||||
|
||||
static void js_storage_remove(struct mjs* mjs) {
|
||||
const char* path;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_remove(storage, path)));
|
||||
}
|
||||
|
||||
static void js_storage_rmrf(struct mjs* mjs) {
|
||||
const char* path;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_remove_recursive(storage, path)));
|
||||
}
|
||||
|
||||
static void js_storage_rename(struct mjs* mjs) {
|
||||
const char *old, *new;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&old), JS_ARG_STR(&new));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
FS_Error status = storage_common_rename(storage, old, new);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, status == FSE_OK));
|
||||
}
|
||||
|
||||
static void js_storage_copy(struct mjs* mjs) {
|
||||
const char *source, *dest;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&source), JS_ARG_STR(&dest));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
FS_Error status = storage_common_copy(storage, source, dest);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, status == FSE_OK || status == FSE_EXIST));
|
||||
}
|
||||
|
||||
static void js_storage_fs_info(struct mjs* mjs) {
|
||||
const char* fs;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&fs));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
uint64_t total_space, free_space;
|
||||
if(storage_common_fs_info(storage, fs, &total_space, &free_space) != FSE_OK) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
mjs_val_t ret = mjs_mk_object(mjs);
|
||||
JS_ASSIGN_MULTI(mjs, ret) {
|
||||
JS_FIELD("totalSpace", mjs_mk_number(mjs, total_space));
|
||||
JS_FIELD("freeSpace", mjs_mk_number(mjs, free_space));
|
||||
}
|
||||
mjs_return(mjs, ret);
|
||||
}
|
||||
|
||||
static void js_storage_next_available_filename(struct mjs* mjs) {
|
||||
const char *dir_path, *file_name, *file_ext;
|
||||
int32_t max_len;
|
||||
JS_FETCH_ARGS_OR_RETURN(
|
||||
mjs,
|
||||
JS_EXACTLY,
|
||||
JS_ARG_STR(&dir_path),
|
||||
JS_ARG_STR(&file_name),
|
||||
JS_ARG_STR(&file_ext),
|
||||
JS_ARG_INT32(&max_len));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
FuriString* next_name = furi_string_alloc();
|
||||
storage_get_next_filename(storage, dir_path, file_name, file_ext, next_name, max_len);
|
||||
mjs_return(mjs, mjs_mk_string(mjs, furi_string_get_cstr(next_name), ~0, true));
|
||||
furi_string_free(next_name);
|
||||
}
|
||||
|
||||
// ---=== path ops ===---
|
||||
|
||||
static void js_storage_are_paths_equal(struct mjs* mjs) {
|
||||
const char *path1, *path2;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path1), JS_ARG_STR(&path2));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_equivalent_path(storage, path1, path2)));
|
||||
}
|
||||
|
||||
static void js_storage_is_subpath_of(struct mjs* mjs) {
|
||||
const char *parent, *child;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&parent), JS_ARG_STR(&child));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_is_subdir(storage, parent, child)));
|
||||
}
|
||||
|
||||
// ---=== module ctor & dtor ===---
|
||||
|
||||
static void* js_storage_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
UNUSED(storage);
|
||||
*object = mjs_mk_object(mjs);
|
||||
JS_ASSIGN_MULTI(mjs, *object) {
|
||||
JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, storage));
|
||||
|
||||
// top-level file ops
|
||||
JS_FIELD("openFile", MJS_MK_FN(js_storage_open_file));
|
||||
JS_FIELD("fileExists", MJS_MK_FN(js_storage_file_exists));
|
||||
|
||||
// dir ops
|
||||
JS_FIELD("readDirectory", MJS_MK_FN(js_storage_read_directory));
|
||||
JS_FIELD("directoryExists", MJS_MK_FN(js_storage_directory_exists));
|
||||
JS_FIELD("makeDirectory", MJS_MK_FN(js_storage_make_directory));
|
||||
|
||||
// common ops
|
||||
JS_FIELD("fileOrDirExists", MJS_MK_FN(js_storage_file_or_dir_exists));
|
||||
JS_FIELD("stat", MJS_MK_FN(js_storage_stat));
|
||||
JS_FIELD("remove", MJS_MK_FN(js_storage_remove));
|
||||
JS_FIELD("rmrf", MJS_MK_FN(js_storage_rmrf));
|
||||
JS_FIELD("rename", MJS_MK_FN(js_storage_rename));
|
||||
JS_FIELD("copy", MJS_MK_FN(js_storage_copy));
|
||||
JS_FIELD("fsInfo", MJS_MK_FN(js_storage_fs_info));
|
||||
JS_FIELD("nextAvailableFilename", MJS_MK_FN(js_storage_next_available_filename));
|
||||
|
||||
// path ops
|
||||
JS_FIELD("arePathsEqual", MJS_MK_FN(js_storage_are_paths_equal));
|
||||
JS_FIELD("isSubpathOf", MJS_MK_FN(js_storage_is_subpath_of));
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void js_storage_destroy(void* data) {
|
||||
UNUSED(data);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
}
|
||||
|
||||
// ---=== boilerplate ===---
|
||||
|
||||
static const JsModuleDescriptor js_storage_desc = {
|
||||
"storage",
|
||||
js_storage_create,
|
||||
js_storage_destroy,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
.appid = PLUGIN_APP_ID,
|
||||
.ep_api_version = PLUGIN_API_VERSION,
|
||||
.entry_point = &js_storage_desc,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* js_storage_ep(void) {
|
||||
return &plugin_descriptor;
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
#include <gui/modules/submenu.h>
|
||||
#include <gui/view_holder.h>
|
||||
#include <gui/view.h>
|
||||
#include <toolbox/api_lock.h>
|
||||
#include "../js_modules.h"
|
||||
|
||||
typedef struct {
|
||||
Submenu* submenu;
|
||||
ViewHolder* view_holder;
|
||||
FuriApiLock lock;
|
||||
uint32_t result;
|
||||
bool accepted;
|
||||
} JsSubmenuInst;
|
||||
|
||||
static JsSubmenuInst* get_this_ctx(struct mjs* mjs) {
|
||||
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
|
||||
JsSubmenuInst* submenu = mjs_get_ptr(mjs, obj_inst);
|
||||
furi_assert(submenu);
|
||||
return submenu;
|
||||
}
|
||||
|
||||
static void ret_bad_args(struct mjs* mjs, const char* error) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error);
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static bool check_arg_count(struct mjs* mjs, size_t count) {
|
||||
size_t num_args = mjs_nargs(mjs);
|
||||
if(num_args != count) {
|
||||
ret_bad_args(mjs, "Wrong argument count");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void submenu_callback(void* context, uint32_t id) {
|
||||
JsSubmenuInst* submenu = context;
|
||||
submenu->result = id;
|
||||
submenu->accepted = true;
|
||||
api_lock_unlock(submenu->lock);
|
||||
}
|
||||
|
||||
static void submenu_exit(void* context) {
|
||||
JsSubmenuInst* submenu = context;
|
||||
submenu->result = 0;
|
||||
submenu->accepted = false;
|
||||
api_lock_unlock(submenu->lock);
|
||||
}
|
||||
|
||||
static void js_submenu_add_item(struct mjs* mjs) {
|
||||
JsSubmenuInst* submenu = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 2)) return;
|
||||
|
||||
mjs_val_t label_arg = mjs_arg(mjs, 0);
|
||||
const char* label = mjs_get_string(mjs, &label_arg, NULL);
|
||||
if(!label) {
|
||||
ret_bad_args(mjs, "Label must be a string");
|
||||
return;
|
||||
}
|
||||
|
||||
mjs_val_t id_arg = mjs_arg(mjs, 1);
|
||||
if(!mjs_is_number(id_arg)) {
|
||||
ret_bad_args(mjs, "Id must be a number");
|
||||
return;
|
||||
}
|
||||
int32_t id = mjs_get_int32(mjs, id_arg);
|
||||
|
||||
submenu_add_item(submenu->submenu, label, id, submenu_callback, submenu);
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_submenu_set_header(struct mjs* mjs) {
|
||||
JsSubmenuInst* submenu = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 1)) return;
|
||||
|
||||
mjs_val_t header_arg = mjs_arg(mjs, 0);
|
||||
const char* header = mjs_get_string(mjs, &header_arg, NULL);
|
||||
if(!header) {
|
||||
ret_bad_args(mjs, "Header must be a string");
|
||||
return;
|
||||
}
|
||||
|
||||
submenu_set_header(submenu->submenu, header);
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_submenu_show(struct mjs* mjs) {
|
||||
JsSubmenuInst* submenu = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 0)) return;
|
||||
|
||||
submenu->lock = api_lock_alloc_locked();
|
||||
Gui* gui = furi_record_open(RECORD_GUI);
|
||||
submenu->view_holder = view_holder_alloc();
|
||||
view_holder_attach_to_gui(submenu->view_holder, gui);
|
||||
view_holder_set_back_callback(submenu->view_holder, submenu_exit, submenu);
|
||||
|
||||
view_holder_set_view(submenu->view_holder, submenu_get_view(submenu->submenu));
|
||||
api_lock_wait_unlock(submenu->lock);
|
||||
|
||||
view_holder_set_view(submenu->view_holder, NULL);
|
||||
view_holder_free(submenu->view_holder);
|
||||
furi_record_close(RECORD_GUI);
|
||||
api_lock_free(submenu->lock);
|
||||
|
||||
submenu_reset(submenu->submenu);
|
||||
if(submenu->accepted) {
|
||||
mjs_return(mjs, mjs_mk_number(mjs, submenu->result));
|
||||
} else {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
}
|
||||
|
||||
static void* js_submenu_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
JsSubmenuInst* submenu = malloc(sizeof(JsSubmenuInst));
|
||||
mjs_val_t submenu_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, submenu_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, submenu));
|
||||
mjs_set(mjs, submenu_obj, "addItem", ~0, MJS_MK_FN(js_submenu_add_item));
|
||||
mjs_set(mjs, submenu_obj, "setHeader", ~0, MJS_MK_FN(js_submenu_set_header));
|
||||
mjs_set(mjs, submenu_obj, "show", ~0, MJS_MK_FN(js_submenu_show));
|
||||
submenu->submenu = submenu_alloc();
|
||||
*object = submenu_obj;
|
||||
return submenu;
|
||||
}
|
||||
|
||||
static void js_submenu_destroy(void* inst) {
|
||||
JsSubmenuInst* submenu = inst;
|
||||
submenu_free(submenu->submenu);
|
||||
free(submenu);
|
||||
}
|
||||
|
||||
static const JsModuleDescriptor js_submenu_desc = {
|
||||
"submenu",
|
||||
js_submenu_create,
|
||||
js_submenu_destroy,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor submenu_plugin_descriptor = {
|
||||
.appid = PLUGIN_APP_ID,
|
||||
.ep_api_version = PLUGIN_API_VERSION,
|
||||
.entry_point = &js_submenu_desc,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* js_submenu_ep(void) {
|
||||
return &submenu_plugin_descriptor;
|
||||
}
|
||||
104
applications/system/js_app/modules/js_tests.c
Normal file
104
applications/system/js_app/modules/js_tests.c
Normal file
@@ -0,0 +1,104 @@
|
||||
#include "../js_modules.h" // IWYU pragma: keep
|
||||
#include <core/common_defines.h>
|
||||
#include <furi_hal_version.h>
|
||||
#include <power/power_service/power.h>
|
||||
|
||||
#define TAG "JsTests"
|
||||
|
||||
static void js_tests_fail(struct mjs* mjs) {
|
||||
furi_check(mjs_nargs(mjs) == 1);
|
||||
mjs_val_t message_arg = mjs_arg(mjs, 0);
|
||||
const char* message = mjs_get_string(mjs, &message_arg, NULL);
|
||||
furi_check(message);
|
||||
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "%s", message);
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_tests_assert_eq(struct mjs* mjs) {
|
||||
furi_check(mjs_nargs(mjs) == 2);
|
||||
|
||||
mjs_val_t expected_arg = mjs_arg(mjs, 0);
|
||||
mjs_val_t result_arg = mjs_arg(mjs, 1);
|
||||
|
||||
if(mjs_is_number(expected_arg) && mjs_is_number(result_arg)) {
|
||||
int32_t expected = mjs_get_int32(mjs, expected_arg);
|
||||
int32_t result = mjs_get_int32(mjs, result_arg);
|
||||
if(expected == result) {
|
||||
FURI_LOG_T(TAG, "eq passed (exp=%ld res=%ld)", expected, result);
|
||||
} else {
|
||||
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "expected %d, found %d", expected, result);
|
||||
}
|
||||
} else if(mjs_is_string(expected_arg) && mjs_is_string(result_arg)) {
|
||||
const char* expected = mjs_get_string(mjs, &expected_arg, NULL);
|
||||
const char* result = mjs_get_string(mjs, &result_arg, NULL);
|
||||
if(strcmp(expected, result) == 0) {
|
||||
FURI_LOG_T(TAG, "eq passed (exp=\"%s\" res=\"%s\")", expected, result);
|
||||
} else {
|
||||
mjs_prepend_errorf(
|
||||
mjs, MJS_INTERNAL_ERROR, "expected \"%s\", found \"%s\"", expected, result);
|
||||
}
|
||||
} else if(mjs_is_boolean(expected_arg) && mjs_is_boolean(result_arg)) {
|
||||
bool expected = mjs_get_bool(mjs, expected_arg);
|
||||
bool result = mjs_get_bool(mjs, result_arg);
|
||||
if(expected == result) {
|
||||
FURI_LOG_T(
|
||||
TAG,
|
||||
"eq passed (exp=%s res=%s)",
|
||||
expected ? "true" : "false",
|
||||
result ? "true" : "false");
|
||||
} else {
|
||||
mjs_prepend_errorf(
|
||||
mjs,
|
||||
MJS_INTERNAL_ERROR,
|
||||
"expected %s, found %s",
|
||||
expected ? "true" : "false",
|
||||
result ? "true" : "false");
|
||||
}
|
||||
} else {
|
||||
JS_ERROR_AND_RETURN(
|
||||
mjs,
|
||||
MJS_INTERNAL_ERROR,
|
||||
"type mismatch (expected %s, result %s)",
|
||||
mjs_typeof(expected_arg),
|
||||
mjs_typeof(result_arg));
|
||||
}
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_tests_assert_float_close(struct mjs* mjs) {
|
||||
furi_check(mjs_nargs(mjs) == 3);
|
||||
|
||||
mjs_val_t expected_arg = mjs_arg(mjs, 0);
|
||||
mjs_val_t result_arg = mjs_arg(mjs, 1);
|
||||
mjs_val_t epsilon_arg = mjs_arg(mjs, 2);
|
||||
furi_check(mjs_is_number(expected_arg));
|
||||
furi_check(mjs_is_number(result_arg));
|
||||
furi_check(mjs_is_number(epsilon_arg));
|
||||
double expected = mjs_get_double(mjs, expected_arg);
|
||||
double result = mjs_get_double(mjs, result_arg);
|
||||
double epsilon = mjs_get_double(mjs, epsilon_arg);
|
||||
|
||||
if(ABS(expected - result) > epsilon) {
|
||||
mjs_prepend_errorf(
|
||||
mjs,
|
||||
MJS_INTERNAL_ERROR,
|
||||
"expected %f found %f (tolerance=%f)",
|
||||
expected,
|
||||
result,
|
||||
epsilon);
|
||||
}
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
void* js_tests_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
mjs_val_t tests_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, tests_obj, "fail", ~0, MJS_MK_FN(js_tests_fail));
|
||||
mjs_set(mjs, tests_obj, "assert_eq", ~0, MJS_MK_FN(js_tests_assert_eq));
|
||||
mjs_set(mjs, tests_obj, "assert_float_close", ~0, MJS_MK_FN(js_tests_assert_float_close));
|
||||
*object = tests_obj;
|
||||
|
||||
return (void*)1;
|
||||
}
|
||||
5
applications/system/js_app/modules/js_tests.h
Normal file
5
applications/system/js_app/modules/js_tests.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
#include "../js_thread_i.h"
|
||||
#include "../js_modules.h"
|
||||
|
||||
void* js_tests_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules);
|
||||
@@ -1,219 +0,0 @@
|
||||
#include <gui/modules/text_box.h>
|
||||
#include <gui/view_holder.h>
|
||||
#include "../js_modules.h"
|
||||
|
||||
typedef struct {
|
||||
TextBox* text_box;
|
||||
ViewHolder* view_holder;
|
||||
FuriString* text;
|
||||
bool is_shown;
|
||||
} JsTextboxInst;
|
||||
|
||||
static JsTextboxInst* get_this_ctx(struct mjs* mjs) {
|
||||
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
|
||||
JsTextboxInst* textbox = mjs_get_ptr(mjs, obj_inst);
|
||||
furi_assert(textbox);
|
||||
return textbox;
|
||||
}
|
||||
|
||||
static void ret_bad_args(struct mjs* mjs, const char* error) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error);
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static bool check_arg_count(struct mjs* mjs, size_t count) {
|
||||
size_t num_args = mjs_nargs(mjs);
|
||||
if(num_args != count) {
|
||||
ret_bad_args(mjs, "Wrong argument count");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void js_textbox_set_config(struct mjs* mjs) {
|
||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 2)) return;
|
||||
|
||||
TextBoxFocus set_focus = TextBoxFocusStart;
|
||||
mjs_val_t focus_arg = mjs_arg(mjs, 0);
|
||||
const char* focus = mjs_get_string(mjs, &focus_arg, NULL);
|
||||
if(!focus) {
|
||||
ret_bad_args(mjs, "Focus must be a string");
|
||||
return;
|
||||
} else {
|
||||
if(!strncmp(focus, "start", strlen("start"))) {
|
||||
set_focus = TextBoxFocusStart;
|
||||
} else if(!strncmp(focus, "end", strlen("end"))) {
|
||||
set_focus = TextBoxFocusEnd;
|
||||
} else {
|
||||
ret_bad_args(mjs, "Bad focus value");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
TextBoxFont set_font = TextBoxFontText;
|
||||
mjs_val_t font_arg = mjs_arg(mjs, 1);
|
||||
const char* font = mjs_get_string(mjs, &font_arg, NULL);
|
||||
if(!font) {
|
||||
ret_bad_args(mjs, "Font must be a string");
|
||||
return;
|
||||
} else {
|
||||
if(!strncmp(font, "text", strlen("text"))) {
|
||||
set_font = TextBoxFontText;
|
||||
} else if(!strncmp(font, "hex", strlen("hex"))) {
|
||||
set_font = TextBoxFontHex;
|
||||
} else {
|
||||
ret_bad_args(mjs, "Bad font value");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
text_box_set_focus(textbox->text_box, set_focus);
|
||||
text_box_set_font(textbox->text_box, set_font);
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_textbox_add_text(struct mjs* mjs) {
|
||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 1)) return;
|
||||
|
||||
mjs_val_t text_arg = mjs_arg(mjs, 0);
|
||||
size_t text_len = 0;
|
||||
const char* text = mjs_get_string(mjs, &text_arg, &text_len);
|
||||
if(!text) {
|
||||
ret_bad_args(mjs, "Text must be a string");
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid condition race between GUI and JS thread
|
||||
text_box_set_text(textbox->text_box, "");
|
||||
|
||||
size_t new_len = furi_string_size(textbox->text) + text_len;
|
||||
if(new_len >= 4096) {
|
||||
furi_string_right(textbox->text, new_len / 2);
|
||||
}
|
||||
|
||||
furi_string_cat(textbox->text, text);
|
||||
|
||||
text_box_set_text(textbox->text_box, furi_string_get_cstr(textbox->text));
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_textbox_clear_text(struct mjs* mjs) {
|
||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 0)) return;
|
||||
|
||||
// Avoid condition race between GUI and JS thread
|
||||
text_box_set_text(textbox->text_box, "");
|
||||
|
||||
furi_string_reset(textbox->text);
|
||||
|
||||
text_box_set_text(textbox->text_box, furi_string_get_cstr(textbox->text));
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_textbox_is_open(struct mjs* mjs) {
|
||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 0)) return;
|
||||
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, textbox->is_shown));
|
||||
}
|
||||
|
||||
static void textbox_callback(void* context, uint32_t arg) {
|
||||
UNUSED(arg);
|
||||
JsTextboxInst* textbox = context;
|
||||
view_holder_set_view(textbox->view_holder, NULL);
|
||||
textbox->is_shown = false;
|
||||
}
|
||||
|
||||
static void textbox_exit(void* context) {
|
||||
JsTextboxInst* textbox = context;
|
||||
// Using timer to schedule view_holder stop, will not work under high CPU load
|
||||
furi_timer_pending_callback(textbox_callback, textbox, 0);
|
||||
}
|
||||
|
||||
static void js_textbox_show(struct mjs* mjs) {
|
||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 0)) return;
|
||||
|
||||
if(textbox->is_shown) {
|
||||
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Textbox is already shown");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
view_holder_set_view(textbox->view_holder, text_box_get_view(textbox->text_box));
|
||||
textbox->is_shown = true;
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_textbox_close(struct mjs* mjs) {
|
||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 0)) return;
|
||||
|
||||
view_holder_set_view(textbox->view_holder, NULL);
|
||||
textbox->is_shown = false;
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void* js_textbox_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
JsTextboxInst* textbox = malloc(sizeof(JsTextboxInst));
|
||||
|
||||
mjs_val_t textbox_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, textbox_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, textbox));
|
||||
mjs_set(mjs, textbox_obj, "setConfig", ~0, MJS_MK_FN(js_textbox_set_config));
|
||||
mjs_set(mjs, textbox_obj, "addText", ~0, MJS_MK_FN(js_textbox_add_text));
|
||||
mjs_set(mjs, textbox_obj, "clearText", ~0, MJS_MK_FN(js_textbox_clear_text));
|
||||
mjs_set(mjs, textbox_obj, "isOpen", ~0, MJS_MK_FN(js_textbox_is_open));
|
||||
mjs_set(mjs, textbox_obj, "show", ~0, MJS_MK_FN(js_textbox_show));
|
||||
mjs_set(mjs, textbox_obj, "close", ~0, MJS_MK_FN(js_textbox_close));
|
||||
|
||||
textbox->text = furi_string_alloc();
|
||||
textbox->text_box = text_box_alloc();
|
||||
|
||||
Gui* gui = furi_record_open(RECORD_GUI);
|
||||
textbox->view_holder = view_holder_alloc();
|
||||
view_holder_attach_to_gui(textbox->view_holder, gui);
|
||||
view_holder_set_back_callback(textbox->view_holder, textbox_exit, textbox);
|
||||
|
||||
*object = textbox_obj;
|
||||
return textbox;
|
||||
}
|
||||
|
||||
static void js_textbox_destroy(void* inst) {
|
||||
JsTextboxInst* textbox = inst;
|
||||
|
||||
view_holder_set_view(textbox->view_holder, NULL);
|
||||
view_holder_free(textbox->view_holder);
|
||||
textbox->view_holder = NULL;
|
||||
|
||||
furi_record_close(RECORD_GUI);
|
||||
|
||||
text_box_reset(textbox->text_box);
|
||||
furi_string_reset(textbox->text);
|
||||
|
||||
text_box_free(textbox->text_box);
|
||||
furi_string_free(textbox->text);
|
||||
free(textbox);
|
||||
}
|
||||
|
||||
static const JsModuleDescriptor js_textbox_desc = {
|
||||
"textbox",
|
||||
js_textbox_create,
|
||||
js_textbox_destroy,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor textbox_plugin_descriptor = {
|
||||
.appid = PLUGIN_APP_ID,
|
||||
.ep_api_version = PLUGIN_API_VERSION,
|
||||
.entry_point = &js_textbox_desc,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* js_textbox_ep(void) {
|
||||
return &textbox_plugin_descriptor;
|
||||
}
|
||||
@@ -7,4 +7,5 @@
|
||||
static constexpr auto app_api_table = sort(create_array_t<sym_entry>(
|
||||
API_METHOD(js_delay_with_flags, bool, (struct mjs*, uint32_t)),
|
||||
API_METHOD(js_flags_set, void, (struct mjs*, uint32_t)),
|
||||
API_METHOD(js_flags_wait, uint32_t, (struct mjs*, uint32_t, uint32_t))));
|
||||
API_METHOD(js_flags_wait, uint32_t, (struct mjs*, uint32_t, uint32_t)),
|
||||
API_METHOD(js_module_get, void*, (JsModules*, const char*))));
|
||||
|
||||
@@ -7,12 +7,16 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void JsModules;
|
||||
|
||||
bool js_delay_with_flags(struct mjs* mjs, uint32_t time);
|
||||
|
||||
void js_flags_set(struct mjs* mjs, uint32_t flags);
|
||||
|
||||
uint32_t js_flags_wait(struct mjs* mjs, uint32_t flags, uint32_t timeout);
|
||||
|
||||
void* js_module_get(JsModules* modules, const char* name);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
81
applications/system/js_app/types/badusb/index.d.ts
vendored
Normal file
81
applications/system/js_app/types/badusb/index.d.ts
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @brief Special key codes that this module recognizes
|
||||
*/
|
||||
export type ModifierKey = "CTRL" | "SHIFT" | "ALT" | "GUI";
|
||||
|
||||
export type MainKey =
|
||||
"DOWN" | "LEFT" | "RIGHT" | "UP" |
|
||||
|
||||
"ENTER" | "PAUSE" | "CAPSLOCK" | "DELETE" | "BACKSPACE" | "END" | "ESC" |
|
||||
"HOME" | "INSERT" | "NUMLOCK" | "PAGEUP" | "PAGEDOWN" | "PRINTSCREEN" |
|
||||
"SCROLLLOCK" | "SPACE" | "TAB" | "MENU" |
|
||||
|
||||
"F1" | "F2" | "F3" | "F4" | "F5" | "F6" | "F7" | "F8" | "F9" | "F10" |
|
||||
"F11" | "F12" | "F13" | "F14" | "F15" | "F16" | "F17" | "F18" | "F19" |
|
||||
"F20" | "F21" | "F22" | "F23" | "F24" |
|
||||
|
||||
"\n" | " " | "!" | "\"" | "#" | "$" | "%" | "&" | "'" | "(" | ")" | "*" |
|
||||
"+" | "," | "-" | "." | "/" | ":" | ";" | "<" | ">" | "=" | "?" | "@" | "[" |
|
||||
"]" | "\\" | "^" | "_" | "`" | "{" | "}" | "|" | "~" |
|
||||
|
||||
"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" |
|
||||
|
||||
"A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" |
|
||||
"M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" |
|
||||
"Y" | "Z" |
|
||||
|
||||
"a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" |
|
||||
"m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" |
|
||||
"y" | "z";
|
||||
|
||||
export type KeyCode = MainKey | ModifierKey | number;
|
||||
|
||||
/**
|
||||
* @brief Initializes the module
|
||||
* @param settings USB device settings. Omit to select default parameters
|
||||
*/
|
||||
export declare function setup(settings?: { vid: number, pid: number, mfrName?: string, prodName?: string }): void;
|
||||
|
||||
/**
|
||||
* @brief Tells whether the virtual USB HID device has successfully connected
|
||||
*/
|
||||
export declare function isConnected(): boolean;
|
||||
|
||||
/**
|
||||
* @brief Presses one or multiple keys at once, then releases them
|
||||
* @param keys The arguments represent a set of keys to. Out of that set, only
|
||||
* one of the keys may represent a "main key" (see `MainKey`), with
|
||||
* the rest being modifier keys (see `ModifierKey`).
|
||||
*/
|
||||
export declare function press(...keys: KeyCode[]): void;
|
||||
|
||||
/**
|
||||
* @brief Presses one or multiple keys at once without releasing them
|
||||
* @param keys The arguments represent a set of keys to. Out of that set, only
|
||||
* one of the keys may represent a "main key" (see `MainKey`), with
|
||||
* the rest being modifier keys (see `ModifierKey`).
|
||||
*/
|
||||
export declare function hold(...keys: KeyCode[]): void;
|
||||
|
||||
/**
|
||||
* @brief Releases one or multiple keys at once
|
||||
* @param keys The arguments represent a set of keys to. Out of that set, only
|
||||
* one of the keys may represent a "main key" (see `MainKey`), with
|
||||
* the rest being modifier keys (see `ModifierKey`).
|
||||
*/
|
||||
export declare function release(...keys: KeyCode[]): void;
|
||||
|
||||
/**
|
||||
* @brief Prints a string by repeatedly pressing and releasing keys
|
||||
* @param string The string to print
|
||||
* @param delay How many milliseconds to wait between key presses
|
||||
*/
|
||||
export declare function print(string: string, delay?: number): void;
|
||||
|
||||
/**
|
||||
* @brief Prints a string by repeatedly pressing and releasing keys. Presses
|
||||
* "Enter" after printing the string
|
||||
* @param string The string to print
|
||||
* @param delay How many milliseconds to wait between key presses
|
||||
*/
|
||||
export declare function println(): void;
|
||||
70
applications/system/js_app/types/event_loop/index.d.ts
vendored
Normal file
70
applications/system/js_app/types/event_loop/index.d.ts
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
type Lit = undefined | null | {};
|
||||
|
||||
/**
|
||||
* Subscription control interface
|
||||
*/
|
||||
export interface Subscription {
|
||||
/**
|
||||
* Cancels the subscription, preventing any future events managed by the
|
||||
* subscription from firing
|
||||
*/
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opaque event source identifier
|
||||
*/
|
||||
export type Contract<Item = undefined> = symbol;
|
||||
|
||||
/**
|
||||
* A callback can be assigned to an event loop to listen to an event. It may
|
||||
* return an array with values that will be passed to it as arguments the next
|
||||
* time that it is called. The first argument is always the subscription
|
||||
* manager, and the second argument is always the item that trigged the event.
|
||||
* The type of the item is defined by the event source.
|
||||
*/
|
||||
export type Callback<Item, Args extends Lit[]> = (subscription: Subscription, item: Item, ...args: Args) => Args | undefined | void;
|
||||
|
||||
/**
|
||||
* Subscribes a callback to an event
|
||||
* @param contract Event identifier
|
||||
* @param callback Function to call when the event is triggered
|
||||
* @param args Initial arguments passed to the callback
|
||||
*/
|
||||
export function subscribe<Item, Args extends Lit[]>(contract: Contract<Item>, callback: Callback<Item, Args>, ...args: Args): Subscription;
|
||||
/**
|
||||
* Runs the event loop until it is stopped (potentially never)
|
||||
*/
|
||||
export function run(): void | never;
|
||||
/**
|
||||
* Stops the event loop
|
||||
*/
|
||||
export function stop(): void;
|
||||
|
||||
/**
|
||||
* Creates a timer event that can be subscribed to just like any other event
|
||||
* @param mode Either `"oneshot"` or `"periodic"`
|
||||
* @param interval Timer interval in milliseconds
|
||||
*/
|
||||
export function timer(mode: "oneshot" | "periodic", interval: number): Contract;
|
||||
|
||||
/**
|
||||
* Message queue
|
||||
*/
|
||||
export interface Queue<T> {
|
||||
/**
|
||||
* Message event
|
||||
*/
|
||||
input: Contract<T>;
|
||||
/**
|
||||
* Sends a message to the queue
|
||||
* @param message message to send
|
||||
*/
|
||||
send(message: T): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a message queue
|
||||
* @param length maximum queue capacity
|
||||
*/
|
||||
export function queue<T>(length: number): Queue<T>;
|
||||
14
applications/system/js_app/types/flipper/index.d.ts
vendored
Normal file
14
applications/system/js_app/types/flipper/index.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @brief Returns the device model
|
||||
*/
|
||||
export declare function getModel(): string;
|
||||
|
||||
/**
|
||||
* @brief Returns the name of the virtual dolphin
|
||||
*/
|
||||
export declare function getName(): string;
|
||||
|
||||
/**
|
||||
* @brief Returns the battery charge percentage
|
||||
*/
|
||||
export declare function getBatteryCharge(): number;
|
||||
178
applications/system/js_app/types/global.d.ts
vendored
Normal file
178
applications/system/js_app/types/global.d.ts
vendored
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* @brief Pauses JavaScript execution for a while
|
||||
* @param ms How many milliseconds to pause the execution for
|
||||
*/
|
||||
declare function delay(ms: number): void;
|
||||
|
||||
/**
|
||||
* @brief Prints to the GUI console view
|
||||
* @param args The arguments are converted to strings, concatenated without any
|
||||
* spaces in between and printed to the console view
|
||||
*/
|
||||
declare function print(...args: any[]): void;
|
||||
|
||||
/**
|
||||
* @brief Converts a number to a string
|
||||
* @param value The number to convert to a string
|
||||
* @param base Integer base (`2`...`16`), default: 16
|
||||
*/
|
||||
declare function toString(value: number, base?: number): string;
|
||||
|
||||
/**
|
||||
* @brief Reads a JS value from a file
|
||||
*
|
||||
* Reads a file at the specified path, interprets it as a JS value and returns
|
||||
* said value.
|
||||
*
|
||||
* @param path The path to the file
|
||||
*/
|
||||
declare function load(path: string): any;
|
||||
|
||||
/**
|
||||
* @brief mJS Foreign Pointer type
|
||||
*
|
||||
* JavaScript code cannot do anything with values of `RawPointer` type except
|
||||
* acquire them from native code and pass them right back to other parts of
|
||||
* native code. These values cannot be turned into something meaningful, nor can
|
||||
* be they modified.
|
||||
*/
|
||||
declare type RawPointer = symbol & { "__tag__": "raw_ptr" };
|
||||
// introducing a nominal type in a hacky way; the `__tag__` property doesn't really exist.
|
||||
|
||||
/**
|
||||
* @brief Holds raw bytes
|
||||
*/
|
||||
declare class ArrayBuffer {
|
||||
/**
|
||||
* @brief The pointer to the byte buffer
|
||||
* @note Like other `RawPointer` values, this value is essentially useless
|
||||
* to JS code.
|
||||
*/
|
||||
getPtr: RawPointer;
|
||||
/**
|
||||
* @brief The length of the buffer in bytes
|
||||
*/
|
||||
byteLength: number;
|
||||
/**
|
||||
* @brief Creates an `ArrayBuffer` that contains a sub-part of the buffer
|
||||
* @param start The index of the byte in the source buffer to be used as the
|
||||
* start for the new buffer
|
||||
* @param end The index of the byte in the source buffer that follows the
|
||||
* byte to be used as the last byte for the new buffer
|
||||
*/
|
||||
slice(start: number, end?: number): ArrayBuffer;
|
||||
}
|
||||
|
||||
declare function ArrayBuffer(): ArrayBuffer;
|
||||
|
||||
declare type ElementType = "u8" | "i8" | "u16" | "i16" | "u32" | "i32";
|
||||
|
||||
declare class TypedArray<E extends ElementType> {
|
||||
/**
|
||||
* @brief The length of the buffer in bytes
|
||||
*/
|
||||
byteLength: number;
|
||||
/**
|
||||
* @brief The length of the buffer in typed elements
|
||||
*/
|
||||
length: number;
|
||||
/**
|
||||
* @brief The underlying `ArrayBuffer`
|
||||
*/
|
||||
buffer: ArrayBuffer;
|
||||
}
|
||||
|
||||
declare class Uint8Array extends TypedArray<"u8"> { }
|
||||
declare class Int8Array extends TypedArray<"i8"> { }
|
||||
declare class Uint16Array extends TypedArray<"u16"> { }
|
||||
declare class Int16Array extends TypedArray<"i16"> { }
|
||||
declare class Uint32Array extends TypedArray<"u32"> { }
|
||||
declare class Int32Array extends TypedArray<"i32"> { }
|
||||
|
||||
declare function Uint8Array(data: ArrayBuffer | number | number[]): Uint8Array;
|
||||
declare function Int8Array(data: ArrayBuffer | number | number[]): Int8Array;
|
||||
declare function Uint16Array(data: ArrayBuffer | number | number[]): Uint16Array;
|
||||
declare function Int16Array(data: ArrayBuffer | number | number[]): Int16Array;
|
||||
declare function Uint32Array(data: ArrayBuffer | number | number[]): Uint32Array;
|
||||
declare function Int32Array(data: ArrayBuffer | number | number[]): Int32Array;
|
||||
|
||||
declare const console: {
|
||||
/**
|
||||
* @brief Prints to the UART logs at the `[I]` level
|
||||
* @param args The arguments are converted to strings, concatenated without any
|
||||
* spaces in between and printed to the logs
|
||||
*/
|
||||
log(...args: any[]): void;
|
||||
/**
|
||||
* @brief Prints to the UART logs at the `[D]` level
|
||||
* @param args The arguments are converted to strings, concatenated without any
|
||||
* spaces in between and printed to the logs
|
||||
*/
|
||||
debug(...args: any[]): void;
|
||||
/**
|
||||
* @brief Prints to the UART logs at the `[W]` level
|
||||
* @param args The arguments are converted to strings, concatenated without any
|
||||
* spaces in between and printed to the logs
|
||||
*/
|
||||
warn(...args: any[]): void;
|
||||
/**
|
||||
* @brief Prints to the UART logs at the `[E]` level
|
||||
* @param args The arguments are converted to strings, concatenated without any
|
||||
* spaces in between and printed to the logs
|
||||
*/
|
||||
error(...args: any[]): void;
|
||||
};
|
||||
|
||||
declare class Array<T> {
|
||||
/**
|
||||
* @brief Takes items out of the array
|
||||
*
|
||||
* Removes elements from the array and returns them in a new array
|
||||
*
|
||||
* @param start The index to start taking elements from
|
||||
* @param deleteCount How many elements to take
|
||||
* @returns The elements that were taken out of the original array as a new
|
||||
* array
|
||||
*/
|
||||
splice(start: number, deleteCount: number): T[];
|
||||
/**
|
||||
* @brief Adds a value to the end of the array
|
||||
* @param value The value to add
|
||||
* @returns New length of the array
|
||||
*/
|
||||
push(value: T): number;
|
||||
/**
|
||||
* @brief How many elements there are in the array
|
||||
*/
|
||||
length: number;
|
||||
}
|
||||
|
||||
declare class String {
|
||||
/**
|
||||
* @brief How many characters there are in the string
|
||||
*/
|
||||
length: number;
|
||||
/**
|
||||
* @brief Returns the character code at an index in the string
|
||||
* @param index The index to consult
|
||||
*/
|
||||
charCodeAt(index: number): number;
|
||||
/**
|
||||
* See `charCodeAt`
|
||||
*/
|
||||
at(index: number): number;
|
||||
}
|
||||
|
||||
declare class Boolean { }
|
||||
|
||||
declare class Function { }
|
||||
|
||||
declare class Number { }
|
||||
|
||||
declare class Object { }
|
||||
|
||||
declare class RegExp { }
|
||||
|
||||
declare interface IArguments { }
|
||||
|
||||
declare type Partial<O extends object> = { [K in keyof O]?: O[K] };
|
||||
45
applications/system/js_app/types/gpio/index.d.ts
vendored
Normal file
45
applications/system/js_app/types/gpio/index.d.ts
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { Contract } from "../event_loop";
|
||||
|
||||
export interface Mode {
|
||||
direction: "in" | "out";
|
||||
outMode?: "push_pull" | "open_drain";
|
||||
inMode?: "analog" | "plain_digital" | "interrupt" | "event";
|
||||
edge?: "rising" | "falling" | "both";
|
||||
pull?: "up" | "down";
|
||||
}
|
||||
|
||||
export interface Pin {
|
||||
/**
|
||||
* Configures a pin. This may be done several times.
|
||||
* @param mode Pin configuration object
|
||||
*/
|
||||
init(mode: Mode): void;
|
||||
/**
|
||||
* Sets the output value of a pin if it's been configured with
|
||||
* `direction: "out"`.
|
||||
* @param value Logic value to output
|
||||
*/
|
||||
write(value: boolean): void;
|
||||
/**
|
||||
* Gets the input value of a pin if it's been configured with
|
||||
* `direction: "in"`, but not `inMode: "analog"`.
|
||||
*/
|
||||
read(): boolean;
|
||||
/**
|
||||
* Gets the input voltage of a pin in millivolts if it's been configured
|
||||
* with `direction: "in"` and `inMode: "analog"`
|
||||
*/
|
||||
read_analog(): number;
|
||||
/**
|
||||
* Returns an `event_loop` event that can be used to listen to interrupts,
|
||||
* as configured by `init`
|
||||
*/
|
||||
interrupt(): Contract;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object that can be used to manage a GPIO pin. For the list of
|
||||
* available pins, see https://docs.flipper.net/gpio-and-modules#miFsS
|
||||
* @param pin Pin name (e.g. `"PC3"`) or number (e.g. `7`)
|
||||
*/
|
||||
export function get(pin: string | number): Pin;
|
||||
16
applications/system/js_app/types/gui/dialog.d.ts
vendored
Normal file
16
applications/system/js_app/types/gui/dialog.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { View, ViewFactory } from ".";
|
||||
import type { Contract } from "../event_loop";
|
||||
|
||||
type Props = {
|
||||
header: string,
|
||||
text: string,
|
||||
left: string,
|
||||
center: string,
|
||||
right: string,
|
||||
}
|
||||
declare class Dialog extends View<Props> {
|
||||
input: Contract<"left" | "center" | "right">;
|
||||
}
|
||||
declare class DialogFactory extends ViewFactory<Props, Dialog> { }
|
||||
declare const factory: DialogFactory;
|
||||
export = factory;
|
||||
7
applications/system/js_app/types/gui/empty_screen.d.ts
vendored
Normal file
7
applications/system/js_app/types/gui/empty_screen.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { View, ViewFactory } from ".";
|
||||
|
||||
type Props = {};
|
||||
declare class EmptyScreen extends View<Props> { }
|
||||
declare class EmptyScreenFactory extends ViewFactory<Props, EmptyScreen> { }
|
||||
declare const factory: EmptyScreenFactory;
|
||||
export = factory;
|
||||
41
applications/system/js_app/types/gui/index.d.ts
vendored
Normal file
41
applications/system/js_app/types/gui/index.d.ts
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { Contract } from "../event_loop";
|
||||
|
||||
type Properties = { [K: string]: any };
|
||||
|
||||
export declare class View<Props extends Properties> {
|
||||
set<P extends keyof Props>(property: P, value: Props[P]): void;
|
||||
}
|
||||
|
||||
export declare class ViewFactory<Props extends Properties, V extends View<Props>> {
|
||||
make(): V;
|
||||
makeWith(initial: Partial<Props>): V;
|
||||
}
|
||||
|
||||
declare class ViewDispatcher {
|
||||
/**
|
||||
* Event source for `sendCustom` events
|
||||
*/
|
||||
custom: Contract<number>;
|
||||
/**
|
||||
* Event source for navigation events (back key presses)
|
||||
*/
|
||||
navigation: Contract;
|
||||
/**
|
||||
* Sends a number to the custom event handler
|
||||
* @param event number to send
|
||||
*/
|
||||
sendCustom(event: number): void;
|
||||
/**
|
||||
* Switches to a view
|
||||
* @param assoc View-ViewDispatcher association as returned by `add`
|
||||
*/
|
||||
switchTo(assoc: View<any>): void;
|
||||
/**
|
||||
* Sends this ViewDispatcher to the front or back, above or below all other
|
||||
* GUI viewports
|
||||
* @param direction Either `"front"` or `"back"`
|
||||
*/
|
||||
sendTo(direction: "front" | "back"): void;
|
||||
}
|
||||
|
||||
export const viewDispatcher: ViewDispatcher;
|
||||
7
applications/system/js_app/types/gui/loading.d.ts
vendored
Normal file
7
applications/system/js_app/types/gui/loading.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { View, ViewFactory } from ".";
|
||||
|
||||
type Props = {};
|
||||
declare class Loading extends View<Props> { }
|
||||
declare class LoadingFactory extends ViewFactory<Props, Loading> { }
|
||||
declare const factory: LoadingFactory;
|
||||
export = factory;
|
||||
13
applications/system/js_app/types/gui/submenu.d.ts
vendored
Normal file
13
applications/system/js_app/types/gui/submenu.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { View, ViewFactory } from ".";
|
||||
import type { Contract } from "../event_loop";
|
||||
|
||||
type Props = {
|
||||
header: string,
|
||||
items: string[],
|
||||
};
|
||||
declare class Submenu extends View<Props> {
|
||||
chosen: Contract<number>;
|
||||
}
|
||||
declare class SubmenuFactory extends ViewFactory<Props, Submenu> { }
|
||||
declare const factory: SubmenuFactory;
|
||||
export = factory;
|
||||
14
applications/system/js_app/types/gui/text_box.d.ts
vendored
Normal file
14
applications/system/js_app/types/gui/text_box.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { View, ViewFactory } from ".";
|
||||
import type { Contract } from "../event_loop";
|
||||
|
||||
type Props = {
|
||||
text: string,
|
||||
font: "text" | "hex",
|
||||
focus: "start" | "end",
|
||||
}
|
||||
declare class TextBox extends View<Props> {
|
||||
chosen: Contract<number>;
|
||||
}
|
||||
declare class TextBoxFactory extends ViewFactory<Props, TextBox> { }
|
||||
declare const factory: TextBoxFactory;
|
||||
export = factory;
|
||||
14
applications/system/js_app/types/gui/text_input.d.ts
vendored
Normal file
14
applications/system/js_app/types/gui/text_input.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { View, ViewFactory } from ".";
|
||||
import type { Contract } from "../event_loop";
|
||||
|
||||
type Props = {
|
||||
header: string,
|
||||
minLength: number,
|
||||
maxLength: number,
|
||||
}
|
||||
declare class TextInput extends View<Props> {
|
||||
input: Contract<string>;
|
||||
}
|
||||
declare class TextInputFactory extends ViewFactory<Props, TextInput> { }
|
||||
declare const factory: TextInputFactory;
|
||||
export = factory;
|
||||
24
applications/system/js_app/types/math/index.d.ts
vendored
Normal file
24
applications/system/js_app/types/math/index.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
export function abs(n: number): number;
|
||||
export function acos(n: number): number;
|
||||
export function acosh(n: number): number;
|
||||
export function asin(n: number): number;
|
||||
export function asinh(n: number): number;
|
||||
export function atan(n: number): number;
|
||||
export function atan2(a: number, b: number): number;
|
||||
export function atanh(n: number): number;
|
||||
export function cbrt(n: number): number;
|
||||
export function ceil(n: number): number;
|
||||
export function clz32(n: number): number;
|
||||
export function cos(n: number): number;
|
||||
export function exp(n: number): number;
|
||||
export function floor(n: number): number;
|
||||
export function max(n: number, m: number): number;
|
||||
export function min(n: number, m: number): number;
|
||||
export function pow(n: number, m: number): number;
|
||||
export function random(): number;
|
||||
export function sign(n: number): number;
|
||||
export function sin(n: number): number;
|
||||
export function sqrt(n: number): number;
|
||||
export function trunc(n: number): number;
|
||||
declare const PI: number;
|
||||
declare const EPSILON: number;
|
||||
20
applications/system/js_app/types/notification/index.d.ts
vendored
Normal file
20
applications/system/js_app/types/notification/index.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @brief Signals success to the user via the color LED, speaker and vibration
|
||||
* motor
|
||||
*/
|
||||
export declare function success(): void;
|
||||
|
||||
/**
|
||||
* @brief Signals failure to the user via the color LED, speaker and vibration
|
||||
* motor
|
||||
*/
|
||||
export declare function error(): void;
|
||||
|
||||
export type Color = "red" | "green" | "blue" | "yellow" | "cyan" | "magenta";
|
||||
|
||||
/**
|
||||
* @brief Displays a basic color on the color LED
|
||||
* @param color The color to display, see `Color`
|
||||
* @param duration The duration, either `"short"` (10ms) or `"long"` (100ms)
|
||||
*/
|
||||
export declare function blink(color: Color, duration: "short" | "long"): void;
|
||||
77
applications/system/js_app/types/serial/index.d.ts
vendored
Normal file
77
applications/system/js_app/types/serial/index.d.ts
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @brief Initializes the serial port
|
||||
* @param port The port to initialize (`"lpuart"` or `"start"`)
|
||||
* @param baudRate
|
||||
*/
|
||||
export declare function setup(port: "lpuart" | "usart", baudRate: number): void;
|
||||
|
||||
/**
|
||||
* @brief Writes data to the serial port
|
||||
* @param value The data to write:
|
||||
* - Strings will get sent as ASCII.
|
||||
* - Numbers will get sent as a single byte.
|
||||
* - Arrays of numbers will get sent as a sequence of bytes.
|
||||
* - `ArrayBuffer`s and `TypedArray`s will be sent as a sequence
|
||||
* of bytes.
|
||||
*/
|
||||
export declare function write<E extends ElementType>(value: string | number | number[] | ArrayBuffer | TypedArray<E>): void;
|
||||
|
||||
/**
|
||||
* @brief Reads data from the serial port
|
||||
* @param length The number of bytes to read
|
||||
* @param timeout The number of time, in milliseconds, after which this function
|
||||
* will give up and return what it read up to that point. If
|
||||
* unset, the function will wait forever.
|
||||
* @returns The received data interpreted as ASCII, or `undefined` if 0 bytes
|
||||
* were read.
|
||||
*/
|
||||
export declare function read(length: number, timeout?: number): string | undefined;
|
||||
|
||||
/**
|
||||
* @brief Reads data from the serial port
|
||||
*
|
||||
* Data is read one character after another until either a `\r` or `\n`
|
||||
* character is received, neither of which is included in the result.
|
||||
*
|
||||
* @param timeout The number of time, in milliseconds, after which this function
|
||||
* will give up and return what it read up to that point. If
|
||||
* unset, the function will wait forever. The timeout only
|
||||
* applies to characters, not entire strings.
|
||||
* @returns The received data interpreted as ASCII, or `undefined` if 0 bytes
|
||||
* were read.
|
||||
*/
|
||||
export declare function readln(timeout?: number): string;
|
||||
|
||||
/**
|
||||
* @brief Reads data from the serial port
|
||||
* @param length The number of bytes to read
|
||||
* @param timeout The number of time, in milliseconds, after which this function
|
||||
* will give up and return what it read up to that point. If
|
||||
* unset, the function will wait forever.
|
||||
* @returns The received data as an ArrayBuffer, or `undefined` if 0 bytes were
|
||||
* read.
|
||||
*/
|
||||
export declare function readBytes(length: number, timeout?: number): ArrayBuffer;
|
||||
|
||||
/**
|
||||
* @brief Reads data from the serial port, trying to match it to a pattern
|
||||
* @param patterns A single pattern or an array of patterns:
|
||||
* - If the argument is a single `string`, this function will
|
||||
* match against the given string.
|
||||
* - If the argument is an array of `number`s, this function
|
||||
* will match against the given sequence of bytes,
|
||||
* - If the argument is an array of `string`s, this function
|
||||
* will match against any string out of the ones that were
|
||||
* provided.
|
||||
* - If the argument is an array of arrays of `number`s, this
|
||||
* function will match against any sequence of bytes out of
|
||||
* the ones that were provided.
|
||||
* @param timeout The number of time, in milliseconds, after which this function
|
||||
* will give up and return what it read up to that point. If
|
||||
* unset, the function will wait forever. The timeout only
|
||||
* applies to characters, not entire strings.
|
||||
* @returns The index of the matched pattern if multiple were provided, or 0 if
|
||||
* only one was provided and it matched, or `undefined` if none of the
|
||||
* patterns matched.
|
||||
*/
|
||||
export declare function expect(patterns: string | number[] | string[] | number[][], timeout?: number): number | undefined;
|
||||
237
applications/system/js_app/types/storage/index.d.ts
vendored
Normal file
237
applications/system/js_app/types/storage/index.d.ts
vendored
Normal file
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* File readability mode:
|
||||
* - `"r"`: read-only
|
||||
* - `"w"`: write-only
|
||||
* - `"rw"`: read-write
|
||||
*/
|
||||
export type AccessMode = "r" | "w" | "rw";
|
||||
|
||||
/**
|
||||
* File creation mode:
|
||||
* - `"open_existing"`: open file or fail if it doesn't exist
|
||||
* - `"open_always"`: open file or create a new empty one if it doesn't exist
|
||||
* - `"open_append"`: open file and set r/w pointer to EOF, or create a new one if it doesn't exist
|
||||
* - `"create_new"`: create new file or fail if it exists
|
||||
* - `"create_always"`: truncate and open file, or create a new empty one if it doesn't exist
|
||||
*/
|
||||
export type OpenMode = "open_existing" | "open_always" | "open_append" | "create_new" | "create_always";
|
||||
|
||||
/** Standard UNIX timestamp */
|
||||
export type Timestamp = number;
|
||||
|
||||
/** File information structure */
|
||||
export declare class FileInfo {
|
||||
/**
|
||||
* Full path (e.g. "/ext/test", returned by `stat`) or file name
|
||||
* (e.g. "test", returned by `readDirectory`)
|
||||
*/
|
||||
path: string;
|
||||
/**
|
||||
* Is the file a directory?
|
||||
*/
|
||||
isDirectory: boolean;
|
||||
/**
|
||||
* File size in bytes, or 0 in the case of directories
|
||||
*/
|
||||
size: number;
|
||||
/**
|
||||
* Time of last access as a UNIX timestamp
|
||||
*/
|
||||
accessTime: Timestamp;
|
||||
}
|
||||
|
||||
/** Filesystem information structure */
|
||||
export declare class FsInfo {
|
||||
/** Total size of the filesystem, in bytes */
|
||||
totalSpace: number;
|
||||
/** Free space in the filesystem, in bytes */
|
||||
freeSpace: number;
|
||||
}
|
||||
|
||||
// file operations
|
||||
|
||||
/** File class */
|
||||
export declare class File {
|
||||
/**
|
||||
* Closes the file. After this method is called, all other operations
|
||||
* related to this file become unavailable.
|
||||
* @returns `true` on success, `false` on failure
|
||||
*/
|
||||
close(): boolean;
|
||||
/**
|
||||
* Is the file currently open?
|
||||
*/
|
||||
isOpen(): boolean;
|
||||
/**
|
||||
* Reads bytes from a file opened in read-only or read-write mode
|
||||
* @param mode The data type to interpret the bytes as: a `string` decoded
|
||||
* from ASCII data (`"ascii"`), or an `ArrayBuf` (`"binary"`)
|
||||
* @param bytes How many bytes to read from the file
|
||||
* @returns an `ArrayBuf` if the mode is `"binary"`, a `string` if the mode
|
||||
* is `ascii`. The number of bytes that was actually read may be
|
||||
* fewer than requested.
|
||||
*/
|
||||
read<T extends ArrayBuffer | string>(mode: T extends ArrayBuffer ? "binary" : "ascii", bytes: number): T;
|
||||
/**
|
||||
* Writes bytes to a file opened in write-only or read-write mode
|
||||
* @param data The data to write: a string that will be ASCII-encoded, or an
|
||||
* ArrayBuf
|
||||
* @returns the amount of bytes that was actually written
|
||||
*/
|
||||
write(data: ArrayBuffer | string): number;
|
||||
/**
|
||||
* Moves the R/W pointer forward
|
||||
* @param bytes How many bytes to move the pointer forward by
|
||||
* @returns `true` on success, `false` on failure
|
||||
*/
|
||||
seekRelative(bytes: number): boolean;
|
||||
/**
|
||||
* Moves the R/W pointer to an absolute position inside the file
|
||||
* @param bytes The position inside the file
|
||||
* @returns `true` on success, `false` on failure
|
||||
*/
|
||||
seekAbsolute(bytes: number): boolean;
|
||||
/**
|
||||
* Gets the absolute position of the R/W pointer in bytes
|
||||
*/
|
||||
tell(): number;
|
||||
/**
|
||||
* Discards the data after the current position of the R/W pointer in a file
|
||||
* opened in either write-only or read-write mode.
|
||||
* @returns `true` on success, `false` on failure
|
||||
*/
|
||||
truncate(): boolean;
|
||||
/**
|
||||
* Reads the total size of the file in bytes
|
||||
*/
|
||||
size(): number;
|
||||
/**
|
||||
* Detects whether the R/W pointer has reached the end of the file
|
||||
*/
|
||||
eof(): boolean;
|
||||
/**
|
||||
* Copies bytes from the R/W pointer in the current file to the R/W pointer
|
||||
* in another file
|
||||
* @param dest The file to copy the bytes into
|
||||
* @param bytes The number of bytes to copy
|
||||
* @returns `true` on success, `false` on failure
|
||||
*/
|
||||
copyTo(dest: File, bytes: number): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a file
|
||||
* @param path The path to the file
|
||||
* @param accessMode `"r"`, `"w"` or `"rw"`; see `AccessMode`
|
||||
* @param openMode `"open_existing"`, `"open_always"`, `"open_append"`,
|
||||
* `"create_new"` or `"create_always"`; see `OpenMode`
|
||||
* @returns a `File` on success, or `undefined` on failure
|
||||
*/
|
||||
export declare function openFile(path: string, accessMode: AccessMode, openMode: OpenMode): File | undefined;
|
||||
/**
|
||||
* Detects whether a file exists
|
||||
* @param path The path to the file
|
||||
* @returns `true` on success, `false` on failure
|
||||
*/
|
||||
export declare function fileExists(path: string): boolean;
|
||||
|
||||
// directory operations
|
||||
|
||||
/**
|
||||
* Reads the list of files in a directory
|
||||
* @param path The path to the directory
|
||||
* @returns Array of `FileInfo` structures with directory entries,
|
||||
* or `undefined` on failure
|
||||
*/
|
||||
export declare function readDirectory(path: string): FileInfo[] | undefined;
|
||||
/**
|
||||
* Detects whether a directory exists
|
||||
* @param path The path to the directory
|
||||
*/
|
||||
export declare function directoryExists(path: string): boolean;
|
||||
/**
|
||||
* Creates an empty directory
|
||||
* @param path The path to the new directory
|
||||
* @returns `true` on success, `false` on failure
|
||||
*/
|
||||
export declare function makeDirectory(path: string): boolean;
|
||||
|
||||
// common (file/dir) operations
|
||||
|
||||
/**
|
||||
* Detects whether a file or a directory exists
|
||||
* @param path The path to the file or directory
|
||||
*/
|
||||
export declare function fileOrDirExists(path: string): boolean;
|
||||
/**
|
||||
* Acquires metadata about a file or directory
|
||||
* @param path The path to the file or directory
|
||||
* @returns A `FileInfo` structure or `undefined` on failure
|
||||
*/
|
||||
export declare function stat(path: string): FileInfo | undefined;
|
||||
/**
|
||||
* Removes a file or an empty directory
|
||||
* @param path The path to the file or directory
|
||||
* @returns `true` on success, `false` on failure
|
||||
*/
|
||||
export declare function remove(path: string): boolean;
|
||||
/**
|
||||
* Removes a file or recursively removes a possibly non-empty directory
|
||||
* @param path The path to the file or directory
|
||||
* @returns `true` on success, `false` on failure
|
||||
*/
|
||||
export declare function rmrf(path: string): boolean;
|
||||
/**
|
||||
* Renames or moves a file or directory
|
||||
* @param oldPath The old path to the file or directory
|
||||
* @param newPath The new path that the file or directory will become accessible
|
||||
* under
|
||||
* @returns `true` on success, `false` on failure
|
||||
*/
|
||||
export declare function rename(oldPath: string, newPath: string): boolean;
|
||||
/**
|
||||
* Copies a file or recursively copies a possibly non-empty directory
|
||||
* @param oldPath The original path to the file or directory
|
||||
* @param newPath The new path that the copy of the file or directory will be
|
||||
* accessible under
|
||||
*/
|
||||
export declare function copy(oldPath: string, newPath: string): boolean;
|
||||
/**
|
||||
* Fetches generic information about a filesystem
|
||||
* @param filesystem The path to the filesystem (e.g. `"/ext"` or `"/int"`)
|
||||
*/
|
||||
export declare function fsInfo(filesystem: string): FsInfo | undefined;
|
||||
/**
|
||||
* Chooses the next available filename with a numeric suffix in a directory
|
||||
*
|
||||
* ```
|
||||
* "/ext/example_dir/example_file123.txt"
|
||||
* \______________/ \__________/\_/\__/
|
||||
* dirPath fileName | |
|
||||
* | +---- fileExt
|
||||
* +------- selected by this function
|
||||
* ```
|
||||
*
|
||||
* @param dirPath The directory to look in
|
||||
* @param fileName The base of the filename (the part before the numeric suffix)
|
||||
* @param fileExt The extension of the filename (the part after the numeric suffix)
|
||||
* @param maxLen The maximum length of the filename with the numeric suffix
|
||||
* @returns The base of the filename with the next available numeric suffix,
|
||||
* without the extension or the base directory.
|
||||
*/
|
||||
export declare function nextAvailableFilename(dirPath: string, fileName: string, fileExt: string, maxLen: number): string;
|
||||
|
||||
// path operations that do not access the filesystem
|
||||
|
||||
/**
|
||||
* Determines whether the two paths are equivalent. Respects filesystem-defined
|
||||
* path equivalence rules.
|
||||
*/
|
||||
export declare function arePathsEqual(path1: string, path2: string): boolean;
|
||||
/**
|
||||
* Determines whether a path is a subpath of another path. Respects
|
||||
* filesystem-defined path equivalence rules.
|
||||
* @param parentPath The parent path
|
||||
* @param childPath The child path
|
||||
*/
|
||||
export declare function isSubpathOf(parentPath: string, childPath: string): boolean;
|
||||
8
applications/system/js_app/types/tests/index.d.ts
vendored
Normal file
8
applications/system/js_app/types/tests/index.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Unit test module. Only available if the firmware has been configured with
|
||||
* `FIRMWARE_APP_SET=unit_tests`.
|
||||
*/
|
||||
|
||||
export function fail(message: string): never;
|
||||
export function assert_eq<T>(expected: T, result: T): void | never;
|
||||
export function assert_float_close(expected: number, result: number, epsilon: number): void | never;
|
||||
88
documentation/devboard/Debugging via the Devboard.md
Normal file
88
documentation/devboard/Debugging via the Devboard.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Debugging via the Devboard {#dev_board_debugging_guide}
|
||||
|
||||
On this page, you'll learn about how debugging via the Wi-Fi Developer Board works. To illustrate this process, we'll start a debug session for Flipper Zero's firmware in VS Code using the native Flipper Build Tool.
|
||||
|
||||
***
|
||||
|
||||
## Overview
|
||||
|
||||
The Developer Board acts as the debug probe, which provides a bridge between the IDE (integrated development environment) with a debugger running on a host computer and the target microcontroller (in your Flipper Zero). The user controls the debugging process on the computer connected to the Developer Board via [Wi-Fi](#dev_board_wifi_connection) or [USB cable](#dev_board_usb_connection).
|
||||
|
||||
\image html https://cdn.flipperzero.one/Flipper_Zero_WiFi_hardware_CDN.jpg width=700
|
||||
|
||||
Data exchange between the Wi-Fi Developer Board and your Flipper Zero is conducted via the Serial Wire Debug interface. The following GPIO pins serve this purpose:
|
||||
|
||||
- **Pin 10:** Serial Wire Clock (SWCLK)
|
||||
|
||||
- **Pin 12:** Serial Wire Debug Data I/O (SWDIO)
|
||||
|
||||
To learn more about Flipper Zero pinout, visit [GPIO & modules in Flipper Docs](https://docs.flipper.net/gpio-and-modules).
|
||||
|
||||
***
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Step 1. Installing Git
|
||||
|
||||
You'll need Git installed on your computer to clone the firmware repository. If you don't have Git, install it following the [official installation guide](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git).
|
||||
|
||||
### Step 2. Building the firmware
|
||||
|
||||
Before starting debugging, you need to clone and build Flipper Zero firmware:
|
||||
|
||||
1. Open the **Terminal** (on Linux & macOS) or **PowerShell** (on Windows) in the directory where you want to store the firmware source code.
|
||||
|
||||
2. Clone the firmware repository:
|
||||
|
||||
```
|
||||
git clone --recursive https://github.com/flipperdevices/flipperzero-firmware.git
|
||||
cd flipperzero-firmware
|
||||
```
|
||||
|
||||
3. Run the **Flipper Build Tool (FBT)** to build the firmware:
|
||||
|
||||
```
|
||||
./fbt
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## Debugging the firmware
|
||||
|
||||
From the **flipperzero-firmware** directory that you cloned earlier, run the following command:
|
||||
|
||||
```
|
||||
./fbt flash
|
||||
```
|
||||
|
||||
This will upload the firmware you've just built to your Flipper Zero via the Developer Board. After that, you can start debugging the firmware. We recommend using **VS Code** with the recommended extensions (as described below), and we have pre-made configurations for it.
|
||||
|
||||
To debug in **VS Code**, do the following:
|
||||
|
||||
1. In VS Code, open the **flipperzero-firmware** directory.
|
||||
|
||||
2. You should see a notification about recommended extensions. Install them.
|
||||
|
||||
If there were no notifications, open the **Extensions** tab, enter `@recommended` in the search bar, and install the workspace recommendations.
|
||||
|
||||
3. Run the `./fbt vscode_dist` command. This will generate the VS Code configuration files needed for debugging.
|
||||
|
||||
4. In VS Code, open the **Run and Debug** tab and select a debugger from the dropdown menu:
|
||||
|
||||
- **Attach FW (blackmagic):** Can be used via **Wi-Fi** or **USB**
|
||||
- **Attach FW (DAP):** Can be used via **USB** only
|
||||
|
||||
Note that when debugging via USB, you need to make sure the selected debugger matches the debug mode on your Devboard. To check the debug mode on your Devboard, access the Devboard's web interface as described [here](#dev_board_wifi_connection) and check the **USB mode** field. If you want to use a different debug mode, enable this mode by following the steps in [Devboard debug modes](#dev_board_debug_modes).
|
||||
|
||||
5. If needed, flash your Flipper Zero with the `./fbt flash` command, then click the ▷ **Start Debugging** button in the debug sidebar to start the debugging session.
|
||||
|
||||
6. Note that starting a debug session halts the execution of the firmware, so you'll need to click the I▷ **Continue** button on the toolbar at the top of your VS Code window to continue execution.
|
||||
|
||||
\image html https://cdn.flipperzero.one/Flipper_Zero_Wi-Fi_devboard_VS_Code.jpg width=900
|
||||
|
||||
> [!note]
|
||||
> If you want to use a different debug mode on your Developer Board, visit [Devboard debug modes](#dev_board_debug_modes).
|
||||
>
|
||||
> If you want to read logs via the Developer Board, see [Reading logs via the Devboard](#dev_board_reading_logs).
|
||||
>
|
||||
> To learn about debugging in VS Code, see [VS Code official guide](https://code.visualstudio.com/docs/editor/debugging).
|
||||
33
documentation/devboard/Devboard debug modes.md
Normal file
33
documentation/devboard/Devboard debug modes.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Devboard debug modes {#dev_board_debug_modes}
|
||||
|
||||
The Wi-Fi Devboard for Flipper Zero supports **Black Magic** and **DAPLink** debug modes, and you can switch between them depending on your needs. Note that available modes depend on connection:
|
||||
|
||||
- **Wi-Fi:** Only **Black Magic** mode is available.
|
||||
- **USB:** Switch between **Black Magic** (default) and **DAPLink**. Learn more about switching debug modes for USB connection below.
|
||||
|
||||
> [!note]
|
||||
> Black Magic mode doesn't support RTOS threads, but you can still perform other debugging operations.
|
||||
|
||||
***
|
||||
|
||||
## Switching debug modes for USB connection
|
||||
|
||||
Switching debug modes for working via USB has to be done wirelessly (yes, you read that correctly). Additionally, depending on how the Devboard wireless connection is configured, you may need to follow different steps for **Wi-Fi access point mode** or **Wi-Fi client mode**:
|
||||
|
||||
1. If the Devboard isn't connected to your Flipper Zero, turn off your Flipper Zero and connect the Developer Board, then turn the device back on.
|
||||
|
||||
2. Access the Devboard's web interface:
|
||||
|
||||
- [Wi-Fi access point mode](#wifi-access-point)
|
||||
|
||||
- [Wi-Fi client mode](#wifi-client-mode)
|
||||
|
||||
3. In the **WiFi** tab, click the **USB mode** option and select **BlackMagicProbe** or **DapLink**.
|
||||
|
||||
4. Click **SAVE**, then click **REBOOT** to apply the changes.
|
||||
|
||||
\image html https://cdn.flipperzero.one/Flipper_Zero_WiFi_devboard_switching_modes_CDN.jpg width=700
|
||||
|
||||
> [!note]
|
||||
> After switching debug modes on your Devboard, remember to select the same debugger in **VS Code** in the **Run and Debug** tab, and click the ▷ **Start Debugging** button.
|
||||
|
||||
@@ -1,122 +1,112 @@
|
||||
# Firmware update on Developer Board {#dev_board_fw_update}
|
||||
|
||||
It's important to regularly update your Developer Board to ensure that you have access to the latest features and bug fixes. This tutorial will guide you through the necessary steps to update the firmware of your Developer Board.
|
||||
It's important to regularly update your Developer Board to ensure that you have access to the latest features and bug fixes. This page will guide you through the necessary steps to update the firmware of your Developer Board.
|
||||
|
||||
This tutorial assumes that you're familiar with the basics of the command line. If you’re not, please refer to the [Windows](https://www.digitalcitizen.life/command-prompt-how-use-basic-commands/) or [MacOS/Linux](https://ubuntu.com/tutorials/command-line-for-beginners#1-overview) command line tutorials.
|
||||
> [!note]
|
||||
> This guide assumes that you're familiar with the basics of the command line. If you're new to it, we recommend checking out these [Windows](https://learn.microsoft.com/en-us/powershell/scripting/learn/ps101/01-getting-started?view=powershell-7.4) or [macOS/Linux](https://ubuntu.com/tutorials/command-line-for-beginners#1-overview) command line tutorials.
|
||||
|
||||
***
|
||||
|
||||
## Installing the micro Flipper Build Tool
|
||||
## Step 1. Install the micro Flipper Build Tool
|
||||
|
||||
Micro Flipper Build Tool (uFBT) is a cross-platform tool that enables basic development tasks for Flipper Zero, such as building and debugging applications, flashing firmware, and creating VS Code development configurations.
|
||||
is a cross-platform tool developed and supported by our team that enables basic development tasks for Flipper Zero, such as building and debugging applications, flashing firmware, creating VS Code development configurations, and flashing firmware to the Wi-Fi Developer Board.
|
||||
|
||||
Install uFBT on your computer by running the following command in the Terminal:
|
||||
**On Linux & macOS:**
|
||||
|
||||
**For Linux & macOS:**
|
||||
Run the following command in the Terminal:
|
||||
|
||||
```text
|
||||
```
|
||||
python3 -m pip install --upgrade ufbt
|
||||
```
|
||||
|
||||
**For Windows:**
|
||||
**On Windows:**
|
||||
|
||||
```text
|
||||
py -m pip install --upgrade ufbt
|
||||
```
|
||||
1. Download the latest version of Python on
|
||||
2. Run the following command in the PowerShell
|
||||
|
||||
If you want to learn more about uFBT, visit [the project's page](https://pypi.org/project/ufbt/).
|
||||
```
|
||||
py -m pip install --upgrade ufbt
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## Connecting the Developer Board to your computer
|
||||
## Step 2. Connect the Devboard to PC
|
||||
|
||||
To update the firmware, you need to switch your Developer Board to Bootloader mode, connect to a PC via a USB cable, and make sure that the PC detects the Developer Board:
|
||||
|
||||
1. List all of the serial devices on your computer.
|
||||
|
||||
**Windows**
|
||||
- **macOS:** Run the `ls /dev/cu.*` command in the Terminal.
|
||||
|
||||
On Windows, go to Device Manager and expand the Ports (COM & LPT) section.
|
||||
- **Linux:** Run the `ls /dev/tty*` command in the Terminal.
|
||||
|
||||
**macOS**
|
||||
|
||||
On macOS, you can run the following command in the Terminal:
|
||||
|
||||
```text
|
||||
ls /dev/cu.*
|
||||
```
|
||||
|
||||
**Linux**
|
||||
|
||||
On Linux, you can run the following command in the Terminal:
|
||||
|
||||
```text
|
||||
ls /dev/tty*
|
||||
```
|
||||
|
||||
View the devices in the list.
|
||||
- **Windows:** Go to **Device Manager** and expand the **Ports (COM & LPT)** section.
|
||||
|
||||
2. Connect the Developer Board to your computer using a USB-C cable.
|
||||

|
||||
|
||||
\image html https://cdn.flipperzero.one/Flipper_Zero_Wi-Fi_devboard_update_wired_connection.jpg width=700
|
||||
|
||||
3. Switch your Developer Board to Bootloader mode:
|
||||
|
||||
3.1. Press and hold the **BOOT** button.
|
||||
1. Press and hold the **BOOT** button.
|
||||
2. Press the **RESET** button while holding the **BOOT** button.
|
||||
3. Release the **BOOT** button.
|
||||
|
||||
3.2. Press the **RESET** button while holding the **BOOT** button.
|
||||
\image html https://cdn.flipperzero.one/Flipper_Zero_Wi-Fi_devboard_reboot_to_bootloader.png width=700
|
||||
|
||||
3.3. Release the **BOOT** button.\
|
||||

|
||||
|
||||
4. Repeat Step 1 and view the name of your Developer Board that appeared in the list.
|
||||
|
||||
For example, on macOS:
|
||||
|
||||
```text
|
||||
/dev/cu.usbmodem01
|
||||
```
|
||||
4. Repeat **Step 1** and view the name of your Developer Board that appeared in the list.
|
||||
|
||||
***
|
||||
|
||||
## Flashing the firmware
|
||||
## Step 3. Flash the firmware
|
||||
|
||||
To flash the firmware onto your Developer Board, run the following command in the terminal:
|
||||
**On Linux & macOS:**
|
||||
|
||||
```text
|
||||
```
|
||||
python3 -m ufbt devboard_flash
|
||||
```
|
||||
|
||||
**On Windows:** Run the following command in the PowerShell:
|
||||
|
||||
```
|
||||
py -m ufbt devboard_flash
|
||||
```
|
||||
|
||||
You should see the following message: `WiFi board flashed successfully`.
|
||||
|
||||
## If flashing failed
|
||||
### If flashing failed
|
||||
|
||||
If you get an error message during the flashing process, such as this:
|
||||
Occasionally, you might get an error message during the flashing process, such as:
|
||||
|
||||
```text
|
||||
```
|
||||
A fatal error occurred: Serial data stream stopped: Possible serial noise or corruption.
|
||||
```
|
||||
|
||||
Or this:
|
||||
*or*
|
||||
|
||||
```text
|
||||
```
|
||||
FileNotFoundError: [Errno 2] No such file or directory: '/dev/cu.usbmodem01'
|
||||
```
|
||||
|
||||
Try doing the following:
|
||||
To fix it, try doing the following:
|
||||
|
||||
* Disconnect the Developer Board from your computer, then reconnect it.
|
||||
- Disconnect the Developer Board from your computer, then reconnect it. After that, switch your Developer Board to Bootloader mode once again, as described in
|
||||
|
||||
* Use a different USB port on your computer.
|
||||
- Use a different USB port on your computer.
|
||||
|
||||
* Use a different USB-C cable.
|
||||
- Use a different USB-C cable.
|
||||
|
||||
***
|
||||
|
||||
## Finishing the installation
|
||||
|
||||
After flashing the firmware:
|
||||
## Step 4. Finish the installation
|
||||
|
||||
1. Reboot the Developer Board by pressing the **RESET** button.
|
||||

|
||||
|
||||
\image html https://cdn.flipperzero.one/Flipper_Zero_Wi-Fi_devboard_reboot_after_flashing.jpg width=700
|
||||
|
||||
2. Disconnect and reconnect the USB-C cable.
|
||||
|
||||
The Developer Board should appear as a serial device on your computer. Now, you can use it with the Black Magic Debug client of your choice.
|
||||
You've successfully updated the firmware of your Developer Board!
|
||||
|
||||
If you followed the **Get started with the Devboard** guide, you're ready for the next step: [Step 3. Plug the Devboard into Flipper Zero](#dev_board_get_started_step-3).
|
||||
|
||||
|
||||
@@ -1,178 +1,80 @@
|
||||
# Get started with the Dev Board {#dev_board_get_started}
|
||||
|
||||
The Wi-Fi Developer Board serves as a tool to debug the Flipper Zero firmware. To debug the firmware, the initial step involves compiling the firmware from its source code. This process enables the debugging functionality within the firmware and generates all the necessary files required for debugging purposes.
|
||||
\image html https://cdn.flipperzero.one/Flipper_Zero_WiFi_developer_board_box_CDN.jpg width=700
|
||||
|
||||
> **NOTE:** Building and debugging the Flipper Zero firmware is fully supported on MacOS and Linux. Support for Windows is in beta test.
|
||||
Before you start using your Devboard, you need to prepare your Flipper Zero and Devboard for debugging. In this guide, we'll walk you through all the necessary steps and provide links to explore the Devboard's capabilities further.
|
||||
|
||||
***
|
||||
|
||||
## Updating the firmware of your Developer Board
|
||||
## Step 1. Enable Debug Mode on your Flipper Zero
|
||||
|
||||
Update the firmware of your Developer Board before using it. For more information, visit [Firmware update on Developer Board](https://docs.flipperzero.one/development/hardware/wifi-debugger-module/update).
|
||||
Since the main purpose of the Developer board is to debug applications on Flipper Zero, you first need to enable Debug Mode. To do so, go to **Settings → System** and set **Debug** to **ON**.
|
||||
|
||||
\image html https://cdn.flipperzero.one/Flipper_Zero_enamble_debug_CDN.jpg width=700
|
||||
|
||||
> [!note]
|
||||
> Debug Mode needs to be re-enabled after each update of Flipper Zero's firmware.
|
||||
|
||||
Debug Mode allows you to debug your apps for Flipper Zero, as well as access debugging options in apps via the user interface and CLI. To learn more about Flipper Zero CLI, visit [Command-line interface in Flipper Docs](https://docs.flipper.net/development/cli).
|
||||
|
||||
\image html https://cdn.flipperzero.one/Flipper_Zero_Command_Line_Interface_CDN.jpg width=700
|
||||
|
||||
***
|
||||
|
||||
## Installing Git
|
||||
## Step 2. Update firmware on the Developer Board
|
||||
|
||||
You'll need Git installed on your computer to clone the firmware repository. If you don't have Git, install it by doing the following:
|
||||
|
||||
* **MacOS**
|
||||
|
||||
On MacOS, install the **Xcode Command Line Tools** package, which includes Git as one of the pre-installed command-line utilities, by running in the Terminal the following command:
|
||||
|
||||
```text
|
||||
xcode-select --install
|
||||
```
|
||||
|
||||
* **Linux**
|
||||
|
||||
On Linux, you can install Git using your package manager. For example, on Ubuntu, run in the Terminal the following command:
|
||||
|
||||
```text
|
||||
sudo apt install git
|
||||
```
|
||||
|
||||
For other distributions, refer to your package manager documentation.
|
||||
The Developer Board comes with stock firmware that may not include all the latest features and bug fixes. To ensure optimal performance, please update your board's firmware using the instructions in [Firmware update on Devboard](#dev_board_fw_update).
|
||||
|
||||
***
|
||||
|
||||
## Building the firmware
|
||||
## Step 3. Plug the Devboard into Flipper Zero {#dev_board_get_started_step-3}
|
||||
|
||||
First, clone the firmware repository:
|
||||
Once your Developer Board firmware is up to date, you can proceed to plug it into your Flipper Zero. Two important things to keep in mind:
|
||||
|
||||
```text
|
||||
git clone --recursive https://github.com/flipperdevices/flipperzero-firmware.git
|
||||
cd flipperzero-firmware
|
||||
```
|
||||
1. **Power off your Flipper Zero before plugging in the Developer Board.**
|
||||
|
||||
Then, run the **Flipper Build Tool** (FBT) to build the firmware:
|
||||
If you skip this step, you may corrupt the data stored on the microSD card. Connecting external modules with a large capacitive load may affect the microSD card's power supply since both the microSD card and external module are powered from the same 3.3 V power source inside Flipper Zero.
|
||||
|
||||
```text
|
||||
./fbt
|
||||
```
|
||||
2. **Make sure the Developer Board is inserted all the way in.**
|
||||
|
||||
If your Flipper Zero isn't in a silicone case, insert the module all the way in so there is no gap between your Flipper Zero and the Devboard. You may need to apply more force to insert it completely. After that, press and hold the **BACK** button to power on your Flipper Zero.
|
||||
|
||||
\image html https://cdn.flipperzero.one/Flipper_Zero_external_module_without_case_CDN.jpg width=700
|
||||
|
||||
If your Flipper Zero is in a silicone case, insert the module all the way in so there is no gap in the middle between the silicone case and the module. After that, press and hold the **BACK** button to power on your Flipper Zero.
|
||||
|
||||
\image html https://cdn.flipperzero.one/Flipper_Zero_external_module_with_case_CDN.jpg width=700
|
||||
|
||||
***
|
||||
|
||||
## Connecting the Developer Board
|
||||
## Step 4. Connect to a computer
|
||||
|
||||
The Developer Board can work in the **Wired** mode and two **Wireless** modes: **Wi-Fi access point (AP)** mode and **Wi-Fi client (STA)** mode. The Wired mode is the simplest to set up, but requires a USB Type-C cable. The Wireless modes are more complex to set up, but they allow you to debug your Flipper Zero wirelessly.
|
||||
Now, you can connect the Developer Board to your computer via USB or Wi-Fi, depending on your needs. We described both methods in separate documents:
|
||||
|
||||
> **NOTE:** Use the following credentials when connecting to the Developer Board in **Wi-Fi access point** mode:\n
|
||||
Name: **blackmagic**\n
|
||||
Password: **iamwitcher**
|
||||
|
||||
## Wired
|
||||
|
||||

|
||||
|
||||
To connect the Developer Board in **Wired** mode, do the following:
|
||||
|
||||
1. Cold-plug the Developer Board by turning off your Flipper Zero and connecting the Developer Board, and then turning it back on.
|
||||
|
||||
2. On your computer, open the **Terminal** and run the following:
|
||||
|
||||
* **MacOS**
|
||||
|
||||
```text
|
||||
ls /dev/cu.*
|
||||
```
|
||||
|
||||
* **Linux**
|
||||
|
||||
```text
|
||||
ls /dev/tty*
|
||||
```
|
||||
|
||||
Note the list of devices.
|
||||
|
||||
3. Connect the Developer Board to your computer via a USB-C cable.
|
||||
|
||||
4. Rerun the command. Two new devices have to appear: this is the Developer Board.
|
||||
|
||||
> **NOTE:** If the Developer Board doesn't appear in the list of devices, try using a different cable, USB port, or computer.
|
||||
>
|
||||
> **NOTE:** Flipper Zero logs can only be viewed when the Developer Board is connected via USB. The option to view logs over Wi-Fi will be added in future updates. For more information, visit [Reading logs via the Dev Board](https://docs.flipperzero.one/development/hardware/wifi-debugger-module/reading-logs).
|
||||
|
||||
## Wireless
|
||||
|
||||
### Wi-Fi access point (AP) mode
|
||||
|
||||

|
||||
|
||||
Out of the box, the Developer Board is configured to work as a **Wi-Fi access point**. This means it'll create its own Wi-Fi network to which you can connect. If your Developer Board doesn't create a Wi-Fi network, it is probably configured to work in **Wi-Fi client** mode. To reset your Developer Board back to **Wi-Fi access point** mode, press and hold the **BOOT** button for 10 seconds, then wait for the module to reboot.
|
||||
|
||||

|
||||
|
||||
To connect the Developer Board in **Wi-Fi access point** mode, do the following:
|
||||
|
||||
1. Cold-plug the Developer Board by turning off your Flipper Zero and connecting the Developer Board, and then turning it back on.
|
||||
|
||||
2. Open Wi-Fi settings on your client device (phone, laptop, or other).
|
||||
|
||||
3. Connect to the network:
|
||||
|
||||
* Name: **blackmagic**
|
||||
|
||||
* Password: **iamwitcher**
|
||||
|
||||
4. To configure the Developer Board, open a browser and go to `http://192.168.4.1`.
|
||||
|
||||
### Wi-Fi client (STA) mode
|
||||
|
||||

|
||||
|
||||
To connect the Developer Board in **Wi-Fi client** mode, you need to configure it to connect to your Wi-Fi network by doing the following:
|
||||
|
||||
1. Cold-plug the Developer Board by turning off your Flipper Zero and connecting the Developer Board, and then turning it back on.
|
||||
|
||||
2. Connect to the Developer Board in **Wi-Fi access point** mode.
|
||||
|
||||
3. In a browser, go to the configuration page on `http://192.168.4.1`.
|
||||
|
||||
4. Select the **STA** mode and enter your network's **SSID** (name) and **password**. For convenience, you can click the **+** button to see the list of nearby networks.
|
||||
|
||||
5. Save the configuration and reboot the Developer Board.
|
||||
|
||||

|
||||
|
||||
After rebooting, the Developer Board connects to your Wi-Fi network. You can connect to the device using the mDNS name **blackmagic.local** or the IP address it got from your router (you'll have to figure this out yourself, every router is different).
|
||||
|
||||
After connecting to your debugger via <http://blackmagic.local>, you can find its IP address in the **SYS** tab. You can also change the debugger's mode to **AP** or **STA** there.
|
||||
|
||||

|
||||
- **[Via USB cable](#dev_board_usb_connection)** for debugging in DAP Link or Black Magic mode, and reading logs.
|
||||
- [Via Wi-Fi](#dev_board_wifi_connection) for debugging in Black Magic mode.
|
||||
|
||||
***
|
||||
|
||||
## Debugging the firmware
|
||||
## Next steps
|
||||
|
||||
Open the **Terminal** in the **flipperzero-firmware** directory that you cloned earlier and run the following command:
|
||||
You are ready to debug now! To further explore what you can do with the Devboard, check out these pages:
|
||||
|
||||
```text
|
||||
./fbt flash
|
||||
```
|
||||
- [Debugging via the Devboard](#dev_board_debugging_guide)
|
||||
- [Devboard debug modes](#dev_board_debug_modes)
|
||||
- [Reading logs via the Devboard](#dev_board_reading_logs)
|
||||
|
||||
These guides should help you get started with your Devboard. If you have any questions or you want to share your experience, don't hesitate to join our community on [Reddit](https://www.reddit.com/r/flipperzero/) and [Discord](https://discord.com/invite/flipper), where we have a dedicated #wifi-devboard channel.
|
||||
|
||||
This will upload the firmware you've just built to your Flipper Zero via the Developer Board. After that, you can start debugging the firmware using the [GDB](https://www.gnu.org/software/gdb/) debugger. We recommend using **VSCode** with the recommended extensions, and we have pre-made configurations for it.
|
||||
|
||||
To debug in **VSCode**, do the following:
|
||||
|
||||
1. In VSCode, open the **flipperzero-firmware** directory.
|
||||
|
||||
2. You should see a notification about recommended extensions. Install them.
|
||||
|
||||
If there were no notifications, open the **Extensions** tab, enter `@recommended` in the search bar, and install the workspace recommendations.
|
||||
|
||||
3. In the **Terminal**, run the `./fbt vscode_dist` command. This will generate the VSCode configuration files needed for debugging.
|
||||
|
||||
4. In VSCode, open the **Run and Debug** tab and select **Attach FW (blackmagic)** from the dropdown menu.
|
||||
|
||||
5. If needed, flash your Flipper Zero with the `./fbt flash` command, then click the **Play** button in the debug sidebar to start the debugging session.
|
||||
|
||||
6. Note that starting a debug session halts the execution of the firmware, so you'll need to click the **Continue** button on the toolbar at the top of your VSCode window to continue execution.
|
||||
|
||||

|
||||
|
||||
To learn about debugging, visit the following pages:
|
||||
|
||||
* [Debugging with GDB](https://sourceware.org/gdb/current/onlinedocs/gdb.pdf)
|
||||
|
||||
* [Debugging in VS Code](https://code.visualstudio.com/docs/editor/debugging)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user