Merge remote-tracking branch 'pr3822/nestednonces' into dev

This commit is contained in:
noproto
2024-10-15 16:06:51 -04:00
164 changed files with 6539 additions and 1385 deletions

View File

@@ -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"],

View File

@@ -0,0 +1,4 @@
let tests = require("tests");
tests.assert_eq(1337, 1337);
tests.assert_eq("hello", "hello");

View File

@@ -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);

View File

@@ -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

View 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));

View 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)

View File

@@ -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) { \

View File

@@ -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)));

View File

@@ -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"],

View 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;
}

View File

@@ -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);

View File

@@ -1,5 +1,6 @@
#pragma once
#include <cli/cli.h>
#include <cli/cli_ansi.h>
void subghz_on_system_start(void);

View File

@@ -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;

View File

@@ -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(

View File

@@ -10,26 +10,12 @@
extern "C" {
#endif
typedef enum {
CliSymbolAsciiSOH = 0x01,
CliSymbolAsciiETX = 0x03,
CliSymbolAsciiEOT = 0x04,
CliSymbolAsciiBell = 0x07,
CliSymbolAsciiBackspace = 0x08,
CliSymbolAsciiTab = 0x09,
CliSymbolAsciiLF = 0x0A,
CliSymbolAsciiCR = 0x0D,
CliSymbolAsciiEsc = 0x1B,
CliSymbolAsciiUS = 0x1F,
CliSymbolAsciiSpace = 0x20,
CliSymbolAsciiDel = 0x7F,
} CliSymbols;
typedef enum {
CliCommandFlagDefault = 0, /**< Default, loader lock is used */
CliCommandFlagParallelSafe =
(1 << 0), /**< Safe to run in parallel with other apps, loader lock is not used */
CliCommandFlagInsomniaSafe = (1 << 1), /**< Safe to run with insomnia mode on */
CliCommandFlagHidden = (1 << 2), /**< Not shown in `help` */
} CliCommandFlag;
#define RECORD_CLI "cli"

View File

@@ -0,0 +1,76 @@
#include "cli_ansi.h"
/**
* @brief Converts a single character representing a special key into the enum
* representation
*/
static CliKey cli_ansi_key_from_mnemonic(char c) {
switch(c) {
case 'A':
return CliKeyUp;
case 'B':
return CliKeyDown;
case 'C':
return CliKeyRight;
case 'D':
return CliKeyLeft;
case 'F':
return CliKeyEnd;
case 'H':
return CliKeyHome;
default:
return CliKeyUnrecognized;
}
}
CliKeyCombo cli_read_ansi_key_combo(Cli* cli) {
char ch = cli_getc(cli);
if(ch != CliKeyEsc)
return (CliKeyCombo){
.modifiers = CliModKeyNo,
.key = ch,
};
ch = cli_getc(cli);
// ESC ESC -> ESC
if(ch == '\e')
return (CliKeyCombo){
.modifiers = CliModKeyNo,
.key = '\e',
};
// ESC <char> -> Alt + <char>
if(ch != '[')
return (CliKeyCombo){
.modifiers = CliModKeyAlt,
.key = cli_getc(cli),
};
ch = cli_getc(cli);
// ESC [ 1
if(ch == '1') {
// ESC [ 1 ; <modifier bitfield> <key mnemonic>
if(cli_getc(cli) == ';') {
CliModKey modifiers = (cli_getc(cli) - '0'); // convert following digit to a number
modifiers &= ~1;
return (CliKeyCombo){
.modifiers = modifiers,
.key = cli_ansi_key_from_mnemonic(cli_getc(cli)),
};
}
return (CliKeyCombo){
.modifiers = CliModKeyNo,
.key = CliKeyUnrecognized,
};
}
// ESC [ <key mnemonic>
return (CliKeyCombo){
.modifiers = CliModKeyNo,
.key = cli_ansi_key_from_mnemonic(ch),
};
}

View File

@@ -0,0 +1,94 @@
#pragma once
#include "cli.h"
#ifdef __cplusplus
extern "C" {
#endif
#define ANSI_RESET "\e[0m"
#define ANSI_BOLD "\e[1m"
#define ANSI_FAINT "\e[2m"
#define ANSI_FG_BLACK "\e[30m"
#define ANSI_FG_RED "\e[31m"
#define ANSI_FG_GREEN "\e[32m"
#define ANSI_FG_YELLOW "\e[33m"
#define ANSI_FG_BLUE "\e[34m"
#define ANSI_FG_MAGENTA "\e[35m"
#define ANSI_FG_CYAN "\e[36m"
#define ANSI_FG_WHITE "\e[37m"
#define ANSI_FG_BR_BLACK "\e[90m"
#define ANSI_FG_BR_RED "\e[91m"
#define ANSI_FG_BR_GREEN "\e[92m"
#define ANSI_FG_BR_YELLOW "\e[93m"
#define ANSI_FG_BR_BLUE "\e[94m"
#define ANSI_FG_BR_MAGENTA "\e[95m"
#define ANSI_FG_BR_CYAN "\e[96m"
#define ANSI_FG_BR_WHITE "\e[97m"
#define ANSI_BG_BLACK "\e[40m"
#define ANSI_BG_RED "\e[41m"
#define ANSI_BG_GREEN "\e[42m"
#define ANSI_BG_YELLOW "\e[43m"
#define ANSI_BG_BLUE "\e[44m"
#define ANSI_BG_MAGENTA "\e[45m"
#define ANSI_BG_CYAN "\e[46m"
#define ANSI_BG_WHITE "\e[47m"
#define ANSI_BG_BR_BLACK "\e[100m"
#define ANSI_BG_BR_RED "\e[101m"
#define ANSI_BG_BR_GREEN "\e[102m"
#define ANSI_BG_BR_YELLOW "\e[103m"
#define ANSI_BG_BR_BLUE "\e[104m"
#define ANSI_BG_BR_MAGENTA "\e[105m"
#define ANSI_BG_BR_CYAN "\e[106m"
#define ANSI_BG_BR_WHITE "\e[107m"
#define ANSI_FLIPPER_BRAND_ORANGE "\e[38;2;255;130;0m"
typedef enum {
CliKeyUnrecognized = 0,
CliKeySOH = 0x01,
CliKeyETX = 0x03,
CliKeyEOT = 0x04,
CliKeyBell = 0x07,
CliKeyBackspace = 0x08,
CliKeyTab = 0x09,
CliKeyLF = 0x0A,
CliKeyCR = 0x0D,
CliKeyETB = 0x17,
CliKeyEsc = 0x1B,
CliKeyUS = 0x1F,
CliKeySpace = 0x20,
CliKeyDEL = 0x7F,
CliKeySpecial = 0x80,
CliKeyLeft,
CliKeyRight,
CliKeyUp,
CliKeyDown,
CliKeyHome,
CliKeyEnd,
} CliKey;
typedef enum {
CliModKeyNo = 0,
CliModKeyAlt = 2,
CliModKeyCtrl = 4,
CliModKeyMeta = 8,
} CliModKey;
typedef struct {
CliModKey modifiers;
CliKey key;
} CliKeyCombo;
/**
* @brief Reads a key or key combination
*/
CliKeyCombo cli_read_ansi_key_combo(Cli* cli);
#ifdef __cplusplus
}
#endif

View File

@@ -1,5 +1,6 @@
#include "cli_commands.h"
#include "cli_command_gpio.h"
#include "cli_ansi.h"
#include <core/thread.h>
#include <furi_hal.h>
@@ -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);

View File

@@ -3,6 +3,7 @@
#include <lib/toolbox/args.h>
#include <cli/cli.h>
#include <cli/cli_ansi.h>
void crypto_cli_print_usage(void) {
printf("Usage:\r\n");
@@ -45,14 +46,14 @@ void crypto_cli_encrypt(Cli* cli, FuriString* args) {
input = furi_string_alloc();
char c;
while(cli_read(cli, (uint8_t*)&c, 1) == 1) {
if(c == CliSymbolAsciiETX) {
if(c == CliKeyETX) {
printf("\r\n");
break;
} else if(c >= 0x20 && c < 0x7F) {
putc(c, stdout);
fflush(stdout);
furi_string_push_back(input, c);
} else if(c == CliSymbolAsciiCR) {
} else if(c == CliKeyCR) {
printf("\r\n");
furi_string_cat(input, "\r\n");
}
@@ -120,14 +121,14 @@ void crypto_cli_decrypt(Cli* cli, FuriString* args) {
hex_input = furi_string_alloc();
char c;
while(cli_read(cli, (uint8_t*)&c, 1) == 1) {
if(c == CliSymbolAsciiETX) {
if(c == CliKeyETX) {
printf("\r\n");
break;
} else if(c >= 0x20 && c < 0x7F) {
putc(c, stdout);
fflush(stdout);
furi_string_push_back(hex_input, c);
} else if(c == CliSymbolAsciiCR) {
} else if(c == CliKeyCR) {
printf("\r\n");
}
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -5,6 +5,12 @@
#define VIEW_DISPATCHER_QUEUE_LEN (16U)
ViewDispatcher* view_dispatcher_alloc(void) {
ViewDispatcher* dispatcher = view_dispatcher_alloc_ex(furi_event_loop_alloc());
dispatcher->is_event_loop_owned = true;
return dispatcher;
}
ViewDispatcher* view_dispatcher_alloc_ex(FuriEventLoop* loop) {
ViewDispatcher* view_dispatcher = malloc(sizeof(ViewDispatcher));
view_dispatcher->view_port = view_port_alloc();
@@ -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);

View File

@@ -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

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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.

View File

@@ -2,6 +2,7 @@
#include <furi_hal.h>
#include <cli/cli.h>
#include <cli/cli_ansi.h>
#include <lib/toolbox/args.h>
#include <lib/toolbox/dir_walk.h>
#include <lib/toolbox/md5_calc.h>
@@ -224,7 +225,7 @@ static void storage_cli_write(Cli* cli, FuriString* path, FuriString* args) {
while(true) {
uint8_t symbol = cli_getc(cli);
if(symbol == CliSymbolAsciiETX) {
if(symbol == CliKeyETX) {
size_t write_size = read_index % buffer_size;
if(write_size > 0) {

View File

@@ -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;

View File

@@ -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"],
)

View File

@@ -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();

View File

@@ -6,4 +6,4 @@ print("2");
delay(1000)
print("3");
delay(1000)
print("end");
print("end");

View File

@@ -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");
}

View File

@@ -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();

View 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"

View 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();

View File

@@ -1,3 +1,3 @@
let math = load("/ext/apps/Scripts/load_api.js");
let result = math.add(5, 10);
print(result);
print(result);

View File

@@ -1,3 +1,3 @@
({
add: function (a, b) { return a + b; },
})
})

View File

@@ -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 !!!");
}

View File

@@ -6,4 +6,4 @@ delay(1000);
for (let i = 0; i < 10; i++) {
notify.blink("red", "short");
delay(500);
}
}

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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));
}
}

View File

@@ -114,7 +114,7 @@ int32_t js_app(void* arg) {
FuriString* start_text =
furi_string_alloc_printf("Running %s", furi_string_get_cstr(name));
console_view_print(app->console_view, furi_string_get_cstr(start_text));
console_view_print(app->console_view, "------------");
console_view_print(app->console_view, "-------------");
furi_string_free(name);
furi_string_free(start_text);

View File

@@ -1,7 +1,11 @@
#include <core/common_defines.h>
#include "js_modules.h"
#include <m-dict.h>
#include <m-array.h>
#include "modules/js_flipper.h"
#ifdef FW_CFG_unit_tests
#include "modules/js_tests.h"
#endif
#define TAG "JS modules"
@@ -9,54 +13,72 @@
#define MODULES_PATH "/ext/apps_data/js_app/plugins"
typedef struct {
JsModeConstructor create;
JsModeDestructor destroy;
FuriString* name;
const JsModuleConstructor create;
const JsModuleDestructor destroy;
void* context;
} JsModuleData;
DICT_DEF2(JsModuleDict, FuriString*, FURI_STRING_OPLIST, JsModuleData, M_POD_OPLIST);
// not using:
// - a dict because ordering is required
// - a bptree because it forces a sorted ordering
// - an rbtree because i deemed it more tedious to implement, and with the
// amount of modules in use (under 10 in the overwhelming majority of cases)
// i bet it's going to be slower than a plain array
ARRAY_DEF(JsModuleArray, JsModuleData, M_POD_OPLIST);
#define M_OPL_JsModuleArray_t() ARRAY_OPLIST(JsModuleArray)
static const JsModuleDescriptor modules_builtin[] = {
{"flipper", js_flipper_create, NULL},
{"flipper", js_flipper_create, NULL, NULL},
#ifdef FW_CFG_unit_tests
{"tests", js_tests_create, NULL, NULL},
#endif
};
struct JsModules {
struct mjs* mjs;
JsModuleDict_t module_dict;
JsModuleArray_t modules;
PluginManager* plugin_manager;
CompositeApiResolver* resolver;
};
JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver) {
JsModules* modules = malloc(sizeof(JsModules));
modules->mjs = mjs;
JsModuleDict_init(modules->module_dict);
JsModuleArray_init(modules->modules);
modules->plugin_manager = plugin_manager_alloc(
PLUGIN_APP_ID, PLUGIN_API_VERSION, composite_api_resolver_get(resolver));
modules->resolver = resolver;
return modules;
}
void js_modules_destroy(JsModules* modules) {
JsModuleDict_it_t it;
for(JsModuleDict_it(it, modules->module_dict); !JsModuleDict_end_p(it);
JsModuleDict_next(it)) {
const JsModuleDict_itref_t* module_itref = JsModuleDict_cref(it);
if(module_itref->value.destroy) {
module_itref->value.destroy(module_itref->value.context);
void js_modules_destroy(JsModules* instance) {
for
M_EACH(module, instance->modules, JsModuleArray_t) {
FURI_LOG_T(TAG, "Tearing down %s", furi_string_get_cstr(module->name));
if(module->destroy) module->destroy(module->context);
furi_string_free(module->name);
}
}
plugin_manager_free(modules->plugin_manager);
JsModuleDict_clear(modules->module_dict);
free(modules);
plugin_manager_free(instance->plugin_manager);
JsModuleArray_clear(instance->modules);
free(instance);
}
JsModuleData* js_find_loaded_module(JsModules* instance, const char* name) {
for
M_EACH(module, instance->modules, JsModuleArray_t) {
if(furi_string_cmp_str(module->name, name) == 0) return module;
}
return NULL;
}
mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_len) {
FuriString* module_name = furi_string_alloc_set_str(name);
// Check if module is already installed
JsModuleData* module_inst = JsModuleDict_get(modules->module_dict, module_name);
JsModuleData* module_inst = js_find_loaded_module(modules, name);
if(module_inst) { //-V547
furi_string_free(module_name);
mjs_prepend_errorf(
modules->mjs, MJS_BAD_ARGS_ERROR, "\"%s\" module is already installed", name);
return MJS_UNDEFINED;
@@ -73,8 +95,11 @@ mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_le
if(strncmp(name, modules_builtin[i].name, name_compare_len) == 0) {
JsModuleData module = {
.create = modules_builtin[i].create, .destroy = modules_builtin[i].destroy};
JsModuleDict_set_at(modules->module_dict, module_name, module);
.create = modules_builtin[i].create,
.destroy = modules_builtin[i].destroy,
.name = furi_string_alloc_set_str(name),
};
JsModuleArray_push_at(modules->modules, 0, module);
module_found = true;
FURI_LOG_I(TAG, "Using built-in module %s", name);
break;
@@ -83,39 +108,57 @@ mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_le
// External module load
if(!module_found) {
FuriString* deslashed_name = furi_string_alloc_set_str(name);
furi_string_replace_all_str(deslashed_name, "/", "__");
FuriString* module_path = furi_string_alloc();
furi_string_printf(module_path, "%s/js_%s.fal", MODULES_PATH, name);
FURI_LOG_I(TAG, "Loading external module %s", furi_string_get_cstr(module_path));
furi_string_printf(
module_path, "%s/js_%s.fal", MODULES_PATH, furi_string_get_cstr(deslashed_name));
FURI_LOG_I(
TAG, "Loading external module %s from %s", name, furi_string_get_cstr(module_path));
do {
uint32_t plugin_cnt_last = plugin_manager_get_count(modules->plugin_manager);
PluginManagerError load_error = plugin_manager_load_single(
modules->plugin_manager, furi_string_get_cstr(module_path));
if(load_error != PluginManagerErrorNone) {
FURI_LOG_E(
TAG,
"Module %s load error. It may depend on other modules that are not yet loaded.",
name);
break;
}
const JsModuleDescriptor* plugin =
plugin_manager_get_ep(modules->plugin_manager, plugin_cnt_last);
furi_assert(plugin);
if(strncmp(name, plugin->name, name_len) != 0) {
FURI_LOG_E(TAG, "Module name missmatch %s", plugin->name);
if(furi_string_cmp_str(deslashed_name, plugin->name) != 0) {
FURI_LOG_E(TAG, "Module name mismatch %s", plugin->name);
break;
}
JsModuleData module = {.create = plugin->create, .destroy = plugin->destroy};
JsModuleDict_set_at(modules->module_dict, module_name, module);
JsModuleData module = {
.create = plugin->create,
.destroy = plugin->destroy,
.name = furi_string_alloc_set_str(name),
};
JsModuleArray_push_at(modules->modules, 0, module);
if(plugin->api_interface) {
FURI_LOG_I(TAG, "Added module API to composite resolver: %s", plugin->name);
composite_api_resolver_add(modules->resolver, plugin->api_interface);
}
module_found = true;
} while(0);
furi_string_free(module_path);
furi_string_free(deslashed_name);
}
// Run module constructor
mjs_val_t module_object = MJS_UNDEFINED;
if(module_found) {
module_inst = JsModuleDict_get(modules->module_dict, module_name);
module_inst = js_find_loaded_module(modules, name);
furi_assert(module_inst);
if(module_inst->create) { //-V779
module_inst->context = module_inst->create(modules->mjs, &module_object);
module_inst->context = module_inst->create(modules->mjs, &module_object, modules);
}
}
@@ -123,7 +166,12 @@ mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_le
mjs_prepend_errorf(modules->mjs, MJS_BAD_ARGS_ERROR, "\"%s\" module load fail", name);
}
furi_string_free(module_name);
return module_object;
}
void* js_module_get(JsModules* modules, const char* name) {
FuriString* module_name = furi_string_alloc_set_str(name);
JsModuleData* module_inst = js_find_loaded_module(modules, name);
furi_string_free(module_name);
return module_inst ? module_inst->context : NULL;
}

View File

@@ -1,4 +1,6 @@
#pragma once
#include <stdint.h>
#include "js_thread_i.h"
#include <flipper_application/flipper_application.h>
#include <flipper_application/plugins/plugin_manager.h>
@@ -7,19 +9,269 @@
#define PLUGIN_APP_ID "js"
#define PLUGIN_API_VERSION 1
typedef void* (*JsModeConstructor)(struct mjs* mjs, mjs_val_t* object);
typedef void (*JsModeDestructor)(void* inst);
/**
* @brief Returns the foreign pointer in `obj["_"]`
*/
#define JS_GET_INST(mjs, obj) mjs_get_ptr(mjs, mjs_get(mjs, obj, INST_PROP_NAME, ~0))
/**
* @brief Returns the foreign pointer in `this["_"]`
*/
#define JS_GET_CONTEXT(mjs) JS_GET_INST(mjs, mjs_get_this(mjs))
/**
* @brief Syntax sugar for constructing an object
*
* @example
* ```c
* mjs_val_t my_obj = mjs_mk_object(mjs);
* JS_ASSIGN_MULTI(mjs, my_obj) {
* JS_FIELD("method1", MJS_MK_FN(js_storage_file_is_open));
* JS_FIELD("method2", MJS_MK_FN(js_storage_file_is_open));
* }
* ```
*/
#define JS_ASSIGN_MULTI(mjs, object) \
for(struct { \
struct mjs* mjs; \
mjs_val_t val; \
int i; \
} _ass_multi = {mjs, object, 0}; \
_ass_multi.i == 0; \
_ass_multi.i++)
#define JS_FIELD(name, value) mjs_set(_ass_multi.mjs, _ass_multi.val, name, ~0, value)
/**
* @brief The first word of structures that foreign pointer JS values point to
*
* This is used to detect situations where JS code mistakenly passes an opaque
* foreign pointer of one type as an argument to a native function which expects
* a struct of another type.
*
* It is recommended to use this functionality in conjunction with the following
* convenience verification macros:
* - `JS_ARG_STRUCT()`
* - `JS_ARG_OBJ_WITH_STRUCT()`
*
* @warning In order for the mechanism to work properly, your struct must store
* the magic value in the first word.
*/
typedef enum {
JsForeignMagicStart = 0x15BAD000,
JsForeignMagic_JsEventLoopContract,
} JsForeignMagic;
// Are you tired of your silly little JS+C glue code functions being 75%
// argument validation code and 25% actual logic? Introducing: ASS (Argument
// Schema for Scripts)! ASS is a set of macros that reduce the typical
// boilerplate code of "check argument count, get arguments, validate arguments,
// extract C values from arguments" down to just one line!
/**
* When passed as the second argument to `JS_FETCH_ARGS_OR_RETURN`, signifies
* that the function requires exactly as many arguments as were specified.
*/
#define JS_EXACTLY ==
/**
* When passed as the second argument to `JS_FETCH_ARGS_OR_RETURN`, signifies
* that the function requires at least as many arguments as were specified.
*/
#define JS_AT_LEAST >=
#define JS_ENUM_MAP(var_name, ...) \
static const JsEnumMapping var_name##_mapping[] = { \
{NULL, sizeof(var_name)}, \
__VA_ARGS__, \
{NULL, 0}, \
};
typedef struct {
const char* name;
size_t value;
} JsEnumMapping;
typedef struct {
void* out;
int (*validator)(mjs_val_t);
void (*converter)(struct mjs*, mjs_val_t*, void* out, const void* extra);
const char* expected_type;
bool (*extended_validator)(struct mjs*, mjs_val_t, const void* extra);
const void* extra_data;
} _js_arg_decl;
static inline void _js_to_int32(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) {
UNUSED(extra);
*(int32_t*)out = mjs_get_int32(mjs, *in);
}
#define JS_ARG_INT32(out) ((_js_arg_decl){out, mjs_is_number, _js_to_int32, "number", NULL, NULL})
static inline void _js_to_ptr(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) {
UNUSED(extra);
*(void**)out = mjs_get_ptr(mjs, *in);
}
#define JS_ARG_PTR(out) \
((_js_arg_decl){out, mjs_is_foreign, _js_to_ptr, "opaque pointer", NULL, NULL})
static inline void _js_to_string(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) {
UNUSED(extra);
*(const char**)out = mjs_get_string(mjs, in, NULL);
}
#define JS_ARG_STR(out) ((_js_arg_decl){out, mjs_is_string, _js_to_string, "string", NULL, NULL})
static inline void _js_to_bool(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) {
UNUSED(extra);
*(bool*)out = !!mjs_get_bool(mjs, *in);
}
#define JS_ARG_BOOL(out) ((_js_arg_decl){out, mjs_is_boolean, _js_to_bool, "boolean", NULL, NULL})
static inline void _js_passthrough(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) {
UNUSED(extra);
UNUSED(mjs);
*(mjs_val_t*)out = *in;
}
#define JS_ARG_ANY(out) ((_js_arg_decl){out, NULL, _js_passthrough, "any", NULL, NULL})
#define JS_ARG_OBJ(out) ((_js_arg_decl){out, mjs_is_object, _js_passthrough, "any", NULL, NULL})
#define JS_ARG_FN(out) \
((_js_arg_decl){out, mjs_is_function, _js_passthrough, "function", NULL, NULL})
#define JS_ARG_ARR(out) ((_js_arg_decl){out, mjs_is_array, _js_passthrough, "array", NULL, NULL})
static inline bool _js_validate_struct(struct mjs* mjs, mjs_val_t val, const void* extra) {
JsForeignMagic expected_magic = (JsForeignMagic)(size_t)extra;
JsForeignMagic struct_magic = *(JsForeignMagic*)mjs_get_ptr(mjs, val);
return struct_magic == expected_magic;
}
#define JS_ARG_STRUCT(type, out) \
((_js_arg_decl){ \
out, \
mjs_is_foreign, \
_js_to_ptr, \
#type, \
_js_validate_struct, \
(void*)JsForeignMagic##_##type})
static inline bool _js_validate_obj_w_struct(struct mjs* mjs, mjs_val_t val, const void* extra) {
JsForeignMagic expected_magic = (JsForeignMagic)(size_t)extra;
JsForeignMagic struct_magic = *(JsForeignMagic*)JS_GET_INST(mjs, val);
return struct_magic == expected_magic;
}
#define JS_ARG_OBJ_WITH_STRUCT(type, out) \
((_js_arg_decl){ \
out, \
mjs_is_object, \
_js_passthrough, \
#type, \
_js_validate_obj_w_struct, \
(void*)JsForeignMagic##_##type})
static inline bool _js_validate_enum(struct mjs* mjs, mjs_val_t val, const void* extra) {
for(const JsEnumMapping* mapping = (JsEnumMapping*)extra + 1; mapping->name; mapping++)
if(strcmp(mapping->name, mjs_get_string(mjs, &val, NULL)) == 0) return true;
return false;
}
static inline void
_js_convert_enum(struct mjs* mjs, mjs_val_t* val, void* out, const void* extra) {
const JsEnumMapping* mapping = (JsEnumMapping*)extra;
size_t size = mapping->value; // get enum size from first entry
for(mapping++; mapping->name; mapping++) {
if(strcmp(mapping->name, mjs_get_string(mjs, val, NULL)) == 0) {
if(size == 1)
*(uint8_t*)out = mapping->value;
else if(size == 2)
*(uint16_t*)out = mapping->value;
else if(size == 4)
*(uint32_t*)out = mapping->value;
else if(size == 8)
*(uint64_t*)out = mapping->value;
return;
}
}
// unreachable, thanks to _js_validate_enum
}
#define JS_ARG_ENUM(var_name, name) \
((_js_arg_decl){ \
&var_name, \
mjs_is_string, \
_js_convert_enum, \
name " enum", \
_js_validate_enum, \
var_name##_mapping})
//-V:JS_FETCH_ARGS_OR_RETURN:1008
/**
* @brief Fetches and validates the arguments passed to a JS function
*
* Example: `int32_t my_arg; JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&my_arg));`
*
* @warning This macro executes `return;` by design in case of an argument count
* mismatch or a validation failure
*/
#define JS_FETCH_ARGS_OR_RETURN(mjs, arg_operator, ...) \
_js_arg_decl _js_args[] = {__VA_ARGS__}; \
int _js_arg_cnt = COUNT_OF(_js_args); \
mjs_val_t _js_arg_vals[_js_arg_cnt]; \
if(!(mjs_nargs(mjs) arg_operator _js_arg_cnt)) \
JS_ERROR_AND_RETURN( \
mjs, \
MJS_BAD_ARGS_ERROR, \
"expected %s%d arguments, got %d", \
#arg_operator, \
_js_arg_cnt, \
mjs_nargs(mjs)); \
for(int _i = 0; _i < _js_arg_cnt; _i++) { \
_js_arg_vals[_i] = mjs_arg(mjs, _i); \
if(_js_args[_i].validator) \
if(!_js_args[_i].validator(_js_arg_vals[_i])) \
JS_ERROR_AND_RETURN( \
mjs, \
MJS_BAD_ARGS_ERROR, \
"argument %d: expected %s", \
_i, \
_js_args[_i].expected_type); \
if(_js_args[_i].extended_validator) \
if(!_js_args[_i].extended_validator(mjs, _js_arg_vals[_i], _js_args[_i].extra_data)) \
JS_ERROR_AND_RETURN( \
mjs, \
MJS_BAD_ARGS_ERROR, \
"argument %d: expected %s", \
_i, \
_js_args[_i].expected_type); \
_js_args[_i].converter( \
mjs, &_js_arg_vals[_i], _js_args[_i].out, _js_args[_i].extra_data); \
}
/**
* @brief Prepends an error, sets the JS return value to `undefined` and returns
* from the C function
* @warning This macro executes `return;` by design
*/
#define JS_ERROR_AND_RETURN(mjs, error_code, ...) \
do { \
mjs_prepend_errorf(mjs, error_code, __VA_ARGS__); \
mjs_return(mjs, MJS_UNDEFINED); \
return; \
} while(0)
typedef struct JsModules JsModules;
typedef void* (*JsModuleConstructor)(struct mjs* mjs, mjs_val_t* object, JsModules* modules);
typedef void (*JsModuleDestructor)(void* inst);
typedef struct {
char* name;
JsModeConstructor create;
JsModeDestructor destroy;
JsModuleConstructor create;
JsModuleDestructor destroy;
const ElfApiInterface* api_interface;
} JsModuleDescriptor;
typedef struct JsModules JsModules;
JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver);
void js_modules_destroy(JsModules* modules);
mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_len);
/**
* @brief Gets a module instance by its name
* This is useful when a module wants to access a stateful API of another
* module.
* @returns Pointer to module context, NULL if the module is not instantiated
*/
void* js_module_get(JsModules* modules, const char* name);

View File

@@ -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);

View File

@@ -1,5 +1,9 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
typedef struct JsThread JsThread;
typedef enum {
@@ -14,3 +18,7 @@ typedef void (*JsThreadCallback)(JsThreadEvent event, const char* msg, void* con
JsThread* js_thread_run(const char* script_path, JsThreadCallback callback, void* context);
void js_thread_stop(JsThread* worker);
#ifdef __cplusplus
}
#endif

View File

@@ -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 = {

View File

@@ -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;
}

View 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;
}

View 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

View File

@@ -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(),
};

View File

@@ -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*))));

View File

@@ -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));

View File

@@ -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);

View 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;
}

View 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);

View 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);

View 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;
}

View 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

View File

@@ -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(),
};

View File

@@ -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*))));

View 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);

View 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);

View 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);

View 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);

View File

@@ -305,7 +305,8 @@ void js_math_trunc(struct mjs* mjs) {
mjs_return(mjs, mjs_mk_number(mjs, x < (double)0. ? ceil(x) : floor(x)));
}
static void* js_math_create(struct mjs* mjs, mjs_val_t* object) {
static void* js_math_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
mjs_val_t math_obj = mjs_mk_object(mjs);
mjs_set(mjs, math_obj, "is_equal", ~0, MJS_MK_FN(js_math_is_equal));
mjs_set(mjs, math_obj, "abs", ~0, MJS_MK_FN(js_math_abs));
@@ -342,6 +343,7 @@ static const JsModuleDescriptor js_math_desc = {
"math",
js_math_create,
NULL,
NULL,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {

View File

@@ -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 = {

View File

@@ -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 = {

View 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), &timestamp) ==
FSE_OK);
mjs_val_t obj = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, obj) {
JS_FIELD("path", mjs_mk_string(mjs, name, ~0, true));
JS_FIELD("isDirectory", mjs_mk_boolean(mjs, file_info_is_dir(&file_info)));
JS_FIELD("size", mjs_mk_number(mjs, file_info.size));
JS_FIELD("timestamp", mjs_mk_number(mjs, timestamp));
}
mjs_array_push(mjs, ret, obj);
}
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, &timestamp)) != FSE_OK) {
mjs_return(mjs, MJS_UNDEFINED);
return;
}
mjs_val_t ret = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, ret) {
JS_FIELD("path", mjs_mk_string(mjs, path, ~0, 1));
JS_FIELD("isDirectory", mjs_mk_boolean(mjs, file_info_is_dir(&file_info)));
JS_FIELD("size", mjs_mk_number(mjs, file_info.size));
JS_FIELD("accessTime", mjs_mk_number(mjs, timestamp));
}
mjs_return(mjs, ret);
}
static void js_storage_remove(struct mjs* mjs) {
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;
}

View File

@@ -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;
}

View 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;
}

View 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);

View File

@@ -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;
}

View File

@@ -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*))));

View File

@@ -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

View 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;

View 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>;

View 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;

View 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] };

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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).

View 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.

View File

@@ -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 youre 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.
![The Developer Board in Wired mode](https://archbee-image-uploads.s3.amazonaws.com/3StCFqarJkJQZV-7N79yY/Aq7gfMI-m_5H6sGGjwb4I_monosnap-miro-2023-07-19-19-47-39.jpg)
\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.\
![You can easily switch the Dev Board to Bootloader mode](https://archbee-image-uploads.s3.amazonaws.com/3StCFqarJkJQZV-7N79yY/KynP9iT6sJ3mXLaLyI82__image.png)
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.
![Reset the Developer Board](https://archbee-image-uploads.s3.amazonaws.com/3StCFqarJkJQZV-7N79yY/rcQeKARgrVwa51tLoo-qY_monosnap-miro-2023-07-20-18-29-33.jpg)
\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).

View File

@@ -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
![The Developer Board in Wired mode](https://archbee-image-uploads.s3.amazonaws.com/3StCFqarJkJQZV-7N79yY/jZdVlRTPVdSQVegzCyXp7_monosnap-miro-2023-06-22-16-28-06.jpg)
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
![The Developer Board in Wi-Fi access point mode](https://archbee-image-uploads.s3.amazonaws.com/3StCFqarJkJQZV-7N79yY/tKRTMHAuruiLSEce2a8Ve_monosnap-miro-2023-06-22-16-39-17.jpg)
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.
![You can reconfigure the Developer Board mode by pressing and holding the BOOT button](https://archbee-image-uploads.s3.amazonaws.com/3StCFqarJkJQZV-7N79yY/57eELJsAwMxeZCEA1NMJw_monosnap-miro-2023-06-22-20-33-27.jpg)
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
![The Developer Board in Wi-Fi client mode](https://archbee-image-uploads.s3.amazonaws.com/3StCFqarJkJQZV-7N79yY/xLQpFyYPfUS5Cx0uQhrNd_monosnap-miro-2023-06-23-12-34-36.jpg)
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.
![In the Wi-Fi tab, you can set the Developer Board mode](https://archbee-image-uploads.s3.amazonaws.com/3StCFqarJkJQZV-7N79yY/klbLVj8lz2bEvm7j4wRaj_monosnap-miro-2023-06-23-13-06-32.jpg)
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.
![In the SYS tab, you can view the IP address of your Developer Board](https://archbee-image-uploads.s3.amazonaws.com/3StCFqarJkJQZV-7N79yY/5XbUptlfqzlV0p6hRUqiG_monosnap-miro-2023-06-22-18-11-30.jpg)
- **[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.
![Click Continue in the toolbar to continue execution of the firmware](https://archbee-image-uploads.s3.amazonaws.com/3StCFqarJkJQZV-7N79yY/lp8ygGaZ3DvWD3OSI9yGO_monosnap-miro-2023-06-23-17-58-09.jpg)
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