mirror of
https://github.com/DarkFlippers/unleashed-firmware.git
synced 2025-12-12 20:49:49 +04:00
Merge branch 'nestednonces' of https://github.com/noproto/flipperzero-firmware into nestednonces
This commit is contained in:
@@ -221,6 +221,14 @@ App(
|
|||||||
requires=["unit_tests"],
|
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(
|
App(
|
||||||
appid="test_strint",
|
appid="test_strint",
|
||||||
sources=["tests/common/*.c", "tests/strint/*.c"],
|
sources=["tests/common/*.c", "tests/strint/*.c"],
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
let tests = require("tests");
|
||||||
|
|
||||||
|
tests.assert_eq(1337, 1337);
|
||||||
|
tests.assert_eq("hello", "hello");
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
let tests = require("tests");
|
||||||
|
let event_loop = require("event_loop");
|
||||||
|
|
||||||
|
let ext = {
|
||||||
|
i: 0,
|
||||||
|
received: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let queue = event_loop.queue(16);
|
||||||
|
|
||||||
|
event_loop.subscribe(queue.input, function (_, item, tests, ext) {
|
||||||
|
tests.assert_eq(123, item);
|
||||||
|
ext.received = true;
|
||||||
|
}, tests, ext);
|
||||||
|
|
||||||
|
event_loop.subscribe(event_loop.timer("periodic", 1), function (_, _item, queue, counter, ext) {
|
||||||
|
ext.i++;
|
||||||
|
queue.send(123);
|
||||||
|
if (counter === 10)
|
||||||
|
event_loop.stop();
|
||||||
|
return [queue, counter + 1, ext];
|
||||||
|
}, queue, 1, ext);
|
||||||
|
|
||||||
|
event_loop.subscribe(event_loop.timer("oneshot", 1000), function (_, _item, tests) {
|
||||||
|
tests.fail("event loop was not stopped");
|
||||||
|
}, tests);
|
||||||
|
|
||||||
|
event_loop.run();
|
||||||
|
tests.assert_eq(10, ext.i);
|
||||||
|
tests.assert_eq(true, ext.received);
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
let tests = require("tests");
|
||||||
|
let math = require("math");
|
||||||
|
|
||||||
|
// math.EPSILON on Flipper Zero is 2.22044604925031308085e-16
|
||||||
|
|
||||||
|
// basics
|
||||||
|
tests.assert_float_close(5, math.abs(-5), math.EPSILON);
|
||||||
|
tests.assert_float_close(0.5, math.abs(-0.5), math.EPSILON);
|
||||||
|
tests.assert_float_close(5, math.abs(5), math.EPSILON);
|
||||||
|
tests.assert_float_close(0.5, math.abs(0.5), math.EPSILON);
|
||||||
|
tests.assert_float_close(3, math.cbrt(27), math.EPSILON);
|
||||||
|
tests.assert_float_close(6, math.ceil(5.3), math.EPSILON);
|
||||||
|
tests.assert_float_close(31, math.clz32(1), math.EPSILON);
|
||||||
|
tests.assert_float_close(5, math.floor(5.7), math.EPSILON);
|
||||||
|
tests.assert_float_close(5, math.max(3, 5), math.EPSILON);
|
||||||
|
tests.assert_float_close(3, math.min(3, 5), math.EPSILON);
|
||||||
|
tests.assert_float_close(-1, math.sign(-5), math.EPSILON);
|
||||||
|
tests.assert_float_close(5, math.trunc(5.7), math.EPSILON);
|
||||||
|
|
||||||
|
// trig
|
||||||
|
tests.assert_float_close(1.0471975511965976, math.acos(0.5), math.EPSILON);
|
||||||
|
tests.assert_float_close(1.3169578969248166, math.acosh(2), math.EPSILON);
|
||||||
|
tests.assert_float_close(0.5235987755982988, math.asin(0.5), math.EPSILON);
|
||||||
|
tests.assert_float_close(1.4436354751788103, math.asinh(2), math.EPSILON);
|
||||||
|
tests.assert_float_close(0.7853981633974483, math.atan(1), math.EPSILON);
|
||||||
|
tests.assert_float_close(0.7853981633974483, math.atan2(1, 1), math.EPSILON);
|
||||||
|
tests.assert_float_close(0.5493061443340549, math.atanh(0.5), math.EPSILON);
|
||||||
|
tests.assert_float_close(-1, math.cos(math.PI), math.EPSILON * 18); // Error 3.77475828372553223744e-15
|
||||||
|
tests.assert_float_close(1, math.sin(math.PI / 2), math.EPSILON * 4.5); // Error 9.99200722162640886381e-16
|
||||||
|
|
||||||
|
// powers
|
||||||
|
tests.assert_float_close(5, math.sqrt(25), math.EPSILON);
|
||||||
|
tests.assert_float_close(8, math.pow(2, 3), math.EPSILON);
|
||||||
|
tests.assert_float_close(2.718281828459045, math.exp(1), math.EPSILON * 2); // Error 4.44089209850062616169e-16
|
||||||
136
applications/debug/unit_tests/resources/unit_tests/js/storage.js
Normal file
136
applications/debug/unit_tests/resources/unit_tests/js/storage.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
let storage = require("storage");
|
||||||
|
let tests = require("tests");
|
||||||
|
|
||||||
|
let baseDir = "/ext/.tmp/unit_tests";
|
||||||
|
|
||||||
|
tests.assert_eq(true, storage.rmrf(baseDir));
|
||||||
|
tests.assert_eq(true, storage.makeDirectory(baseDir));
|
||||||
|
|
||||||
|
// write
|
||||||
|
let file = storage.openFile(baseDir + "/helloworld", "w", "create_always");
|
||||||
|
tests.assert_eq(true, !!file);
|
||||||
|
tests.assert_eq(true, file.isOpen());
|
||||||
|
tests.assert_eq(13, file.write("Hello, World!"));
|
||||||
|
tests.assert_eq(true, file.close());
|
||||||
|
tests.assert_eq(false, file.isOpen());
|
||||||
|
|
||||||
|
// read
|
||||||
|
file = storage.openFile(baseDir + "/helloworld", "r", "open_existing");
|
||||||
|
tests.assert_eq(true, !!file);
|
||||||
|
tests.assert_eq(true, file.isOpen());
|
||||||
|
tests.assert_eq(13, file.size());
|
||||||
|
tests.assert_eq("Hello, World!", file.read("ascii", 128));
|
||||||
|
tests.assert_eq(true, file.close());
|
||||||
|
tests.assert_eq(false, file.isOpen());
|
||||||
|
|
||||||
|
// seek
|
||||||
|
file = storage.openFile(baseDir + "/helloworld", "r", "open_existing");
|
||||||
|
tests.assert_eq(true, !!file);
|
||||||
|
tests.assert_eq(true, file.isOpen());
|
||||||
|
tests.assert_eq(13, file.size());
|
||||||
|
tests.assert_eq("Hello, World!", file.read("ascii", 128));
|
||||||
|
tests.assert_eq(true, file.seekAbsolute(1));
|
||||||
|
tests.assert_eq(true, file.seekRelative(2));
|
||||||
|
tests.assert_eq(3, file.tell());
|
||||||
|
tests.assert_eq(false, file.eof());
|
||||||
|
tests.assert_eq("lo, World!", file.read("ascii", 128));
|
||||||
|
tests.assert_eq(true, file.eof());
|
||||||
|
tests.assert_eq(true, file.close());
|
||||||
|
tests.assert_eq(false, file.isOpen());
|
||||||
|
|
||||||
|
// byte-level copy
|
||||||
|
let src = storage.openFile(baseDir + "/helloworld", "r", "open_existing");
|
||||||
|
let dst = storage.openFile(baseDir + "/helloworld2", "rw", "create_always");
|
||||||
|
tests.assert_eq(true, !!src);
|
||||||
|
tests.assert_eq(true, src.isOpen());
|
||||||
|
tests.assert_eq(true, !!dst);
|
||||||
|
tests.assert_eq(true, dst.isOpen());
|
||||||
|
tests.assert_eq(true, src.copyTo(dst, 10));
|
||||||
|
tests.assert_eq(true, dst.seekAbsolute(0));
|
||||||
|
tests.assert_eq("Hello, Wor", dst.read("ascii", 128));
|
||||||
|
tests.assert_eq(true, src.copyTo(dst, 3));
|
||||||
|
tests.assert_eq(true, dst.seekAbsolute(0));
|
||||||
|
tests.assert_eq("Hello, World!", dst.read("ascii", 128));
|
||||||
|
tests.assert_eq(true, src.eof());
|
||||||
|
tests.assert_eq(true, src.close());
|
||||||
|
tests.assert_eq(false, src.isOpen());
|
||||||
|
tests.assert_eq(true, dst.eof());
|
||||||
|
tests.assert_eq(true, dst.close());
|
||||||
|
tests.assert_eq(false, dst.isOpen());
|
||||||
|
|
||||||
|
// truncate
|
||||||
|
tests.assert_eq(true, storage.copy(baseDir + "/helloworld", baseDir + "/helloworld2"));
|
||||||
|
file = storage.openFile(baseDir + "/helloworld2", "w", "open_existing");
|
||||||
|
tests.assert_eq(true, !!file);
|
||||||
|
tests.assert_eq(true, file.seekAbsolute(5));
|
||||||
|
tests.assert_eq(true, file.truncate());
|
||||||
|
tests.assert_eq(true, file.close());
|
||||||
|
file = storage.openFile(baseDir + "/helloworld2", "r", "open_existing");
|
||||||
|
tests.assert_eq(true, !!file);
|
||||||
|
tests.assert_eq("Hello", file.read("ascii", 128));
|
||||||
|
tests.assert_eq(true, file.close());
|
||||||
|
|
||||||
|
// existence
|
||||||
|
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld"));
|
||||||
|
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld2"));
|
||||||
|
tests.assert_eq(false, storage.fileExists(baseDir + "/sus_amogus_123"));
|
||||||
|
tests.assert_eq(false, storage.directoryExists(baseDir + "/helloworld"));
|
||||||
|
tests.assert_eq(false, storage.fileExists(baseDir));
|
||||||
|
tests.assert_eq(true, storage.directoryExists(baseDir));
|
||||||
|
tests.assert_eq(true, storage.fileOrDirExists(baseDir));
|
||||||
|
tests.assert_eq(true, storage.remove(baseDir + "/helloworld2"));
|
||||||
|
tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld2"));
|
||||||
|
|
||||||
|
// stat
|
||||||
|
let stat = storage.stat(baseDir + "/helloworld");
|
||||||
|
tests.assert_eq(true, !!stat);
|
||||||
|
tests.assert_eq(baseDir + "/helloworld", stat.path);
|
||||||
|
tests.assert_eq(false, stat.isDirectory);
|
||||||
|
tests.assert_eq(13, stat.size);
|
||||||
|
|
||||||
|
// rename
|
||||||
|
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld"));
|
||||||
|
tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld123"));
|
||||||
|
tests.assert_eq(true, storage.rename(baseDir + "/helloworld", baseDir + "/helloworld123"));
|
||||||
|
tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld"));
|
||||||
|
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld123"));
|
||||||
|
tests.assert_eq(true, storage.rename(baseDir + "/helloworld123", baseDir + "/helloworld"));
|
||||||
|
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld"));
|
||||||
|
tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld123"));
|
||||||
|
|
||||||
|
// copy
|
||||||
|
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld"));
|
||||||
|
tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld123"));
|
||||||
|
tests.assert_eq(true, storage.copy(baseDir + "/helloworld", baseDir + "/helloworld123"));
|
||||||
|
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld"));
|
||||||
|
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld123"));
|
||||||
|
|
||||||
|
// next avail
|
||||||
|
tests.assert_eq("helloworld1", storage.nextAvailableFilename(baseDir, "helloworld", "", 20));
|
||||||
|
|
||||||
|
// fs info
|
||||||
|
let fsInfo = storage.fsInfo("/ext");
|
||||||
|
tests.assert_eq(true, !!fsInfo);
|
||||||
|
tests.assert_eq(true, fsInfo.freeSpace < fsInfo.totalSpace); // idk \(-_-)/
|
||||||
|
fsInfo = storage.fsInfo("/int");
|
||||||
|
tests.assert_eq(true, !!fsInfo);
|
||||||
|
tests.assert_eq(true, fsInfo.freeSpace < fsInfo.totalSpace);
|
||||||
|
|
||||||
|
// path operations
|
||||||
|
tests.assert_eq(true, storage.arePathsEqual("/ext/test", "/ext/Test"));
|
||||||
|
tests.assert_eq(false, storage.arePathsEqual("/ext/test", "/ext/Testttt"));
|
||||||
|
tests.assert_eq(true, storage.isSubpathOf("/ext/test", "/ext/test/sub"));
|
||||||
|
tests.assert_eq(false, storage.isSubpathOf("/ext/test/sub", "/ext/test"));
|
||||||
|
|
||||||
|
// dir
|
||||||
|
let entries = storage.readDirectory(baseDir);
|
||||||
|
tests.assert_eq(true, !!entries);
|
||||||
|
// FIXME: (-nofl) this test suite assumes that files are listed by
|
||||||
|
// `readDirectory` in the exact order that they were created, which is not
|
||||||
|
// something that is actually guaranteed.
|
||||||
|
// Possible solution: sort and compare the array.
|
||||||
|
tests.assert_eq("helloworld", entries[0].path);
|
||||||
|
tests.assert_eq("helloworld123", entries[1].path);
|
||||||
|
|
||||||
|
tests.assert_eq(true, storage.rmrf(baseDir));
|
||||||
|
tests.assert_eq(true, storage.makeDirectory(baseDir));
|
||||||
88
applications/debug/unit_tests/tests/js/js_test.c
Normal file
88
applications/debug/unit_tests/tests/js/js_test.c
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
#include "../test.h" // IWYU pragma: keep
|
||||||
|
|
||||||
|
#include <furi.h>
|
||||||
|
#include <furi_hal.h>
|
||||||
|
#include <furi_hal_random.h>
|
||||||
|
|
||||||
|
#include <storage/storage.h>
|
||||||
|
#include <applications/system/js_app/js_thread.h>
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define JS_SCRIPT_PATH(name) EXT_PATH("unit_tests/js/" name ".js")
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
JsTestsFinished = 1,
|
||||||
|
JsTestsError = 2,
|
||||||
|
} JsTestFlag;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
FuriEventFlag* event_flags;
|
||||||
|
FuriString* error_string;
|
||||||
|
} JsTestCallbackContext;
|
||||||
|
|
||||||
|
static void js_test_callback(JsThreadEvent event, const char* msg, void* param) {
|
||||||
|
JsTestCallbackContext* context = param;
|
||||||
|
if(event == JsThreadEventPrint) {
|
||||||
|
FURI_LOG_I("js_test", "%s", msg);
|
||||||
|
} else if(event == JsThreadEventError || event == JsThreadEventErrorTrace) {
|
||||||
|
context->error_string = furi_string_alloc_set_str(msg);
|
||||||
|
furi_event_flag_set(context->event_flags, JsTestsFinished | JsTestsError);
|
||||||
|
} else if(event == JsThreadEventDone) {
|
||||||
|
furi_event_flag_set(context->event_flags, JsTestsFinished);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void js_test_run(const char* script_path) {
|
||||||
|
JsTestCallbackContext* context = malloc(sizeof(JsTestCallbackContext));
|
||||||
|
context->event_flags = furi_event_flag_alloc();
|
||||||
|
|
||||||
|
JsThread* thread = js_thread_run(script_path, js_test_callback, context);
|
||||||
|
uint32_t flags = furi_event_flag_wait(
|
||||||
|
context->event_flags, JsTestsFinished, FuriFlagWaitAny, FuriWaitForever);
|
||||||
|
if(flags & FuriFlagError) {
|
||||||
|
// getting the flags themselves should not fail
|
||||||
|
furi_crash();
|
||||||
|
}
|
||||||
|
|
||||||
|
FuriString* error_string = context->error_string;
|
||||||
|
|
||||||
|
js_thread_stop(thread);
|
||||||
|
furi_event_flag_free(context->event_flags);
|
||||||
|
free(context);
|
||||||
|
|
||||||
|
if(flags & JsTestsError) {
|
||||||
|
// memory leak: not freeing the FuriString if the tests fail,
|
||||||
|
// because mu_fail executes a return
|
||||||
|
//
|
||||||
|
// who cares tho?
|
||||||
|
mu_fail(furi_string_get_cstr(error_string));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MU_TEST(js_test_basic) {
|
||||||
|
js_test_run(JS_SCRIPT_PATH("basic"));
|
||||||
|
}
|
||||||
|
MU_TEST(js_test_math) {
|
||||||
|
js_test_run(JS_SCRIPT_PATH("math"));
|
||||||
|
}
|
||||||
|
MU_TEST(js_test_event_loop) {
|
||||||
|
js_test_run(JS_SCRIPT_PATH("event_loop"));
|
||||||
|
}
|
||||||
|
MU_TEST(js_test_storage) {
|
||||||
|
js_test_run(JS_SCRIPT_PATH("storage"));
|
||||||
|
}
|
||||||
|
|
||||||
|
MU_TEST_SUITE(test_js) {
|
||||||
|
MU_RUN_TEST(js_test_basic);
|
||||||
|
MU_RUN_TEST(js_test_math);
|
||||||
|
MU_RUN_TEST(js_test_event_loop);
|
||||||
|
MU_RUN_TEST(js_test_storage);
|
||||||
|
}
|
||||||
|
|
||||||
|
int run_minunit_test_js(void) {
|
||||||
|
MU_RUN_SUITE(test_js);
|
||||||
|
return MU_EXIT_CODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_API_DEFINE(run_minunit_test_js)
|
||||||
@@ -31,7 +31,7 @@ extern "C" {
|
|||||||
#include <Windows.h>
|
#include <Windows.h>
|
||||||
#if defined(_MSC_VER) && _MSC_VER < 1900
|
#if defined(_MSC_VER) && _MSC_VER < 1900
|
||||||
#define snprintf _snprintf
|
#define snprintf _snprintf
|
||||||
#define __func__ __FUNCTION__
|
#define __func__ __FUNCTION__ //-V1059
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#elif defined(__unix__) || defined(__unix) || defined(unix) || \
|
#elif defined(__unix__) || defined(__unix) || defined(unix) || \
|
||||||
@@ -56,7 +56,7 @@ extern "C" {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if __GNUC__ >= 5 && !defined(__STDC_VERSION__)
|
#if __GNUC__ >= 5 && !defined(__STDC_VERSION__)
|
||||||
#define __func__ __extension__ __FUNCTION__
|
#define __func__ __extension__ __FUNCTION__ //-V1059
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#else
|
#else
|
||||||
@@ -102,6 +102,7 @@ void minunit_printf_warning(const char* format, ...);
|
|||||||
MU__SAFE_BLOCK(minunit_setup = setup_fun; minunit_teardown = teardown_fun;)
|
MU__SAFE_BLOCK(minunit_setup = setup_fun; minunit_teardown = teardown_fun;)
|
||||||
|
|
||||||
/* Test runner */
|
/* Test runner */
|
||||||
|
//-V:MU_RUN_TEST:550
|
||||||
#define MU_RUN_TEST(test) \
|
#define MU_RUN_TEST(test) \
|
||||||
MU__SAFE_BLOCK( \
|
MU__SAFE_BLOCK( \
|
||||||
if(minunit_real_timer == 0 && minunit_proc_timer == 0) { \
|
if(minunit_real_timer == 0 && minunit_proc_timer == 0) { \
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
#include <rpc/rpc_i.h>
|
#include <rpc/rpc_i.h>
|
||||||
#include <flipper.pb.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>(
|
static constexpr auto unit_tests_api_table = sort(create_array_t<sym_entry>(
|
||||||
API_METHOD(resource_manifest_reader_alloc, ResourceManifestReader*, (Storage*)),
|
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,
|
xQueueGenericSend,
|
||||||
BaseType_t,
|
BaseType_t,
|
||||||
(QueueHandle_t, const void* const, TickType_t, const 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(
|
API_METHOD(
|
||||||
furi_event_loop_subscribe_message_queue,
|
js_thread_run,
|
||||||
void,
|
JsThread*,
|
||||||
(FuriEventLoop*, FuriMessageQueue*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*)),
|
(const char* script_path, JsThreadCallback callback, void* context)),
|
||||||
API_METHOD(furi_event_loop_unsubscribe, void, (FuriEventLoop*, FuriEventLoopObject*)),
|
API_METHOD(js_thread_stop, void, (JsThread * worker)),
|
||||||
API_METHOD(furi_event_loop_run, void, (FuriEventLoop*)),
|
|
||||||
API_METHOD(furi_event_loop_stop, void, (FuriEventLoop*)),
|
|
||||||
API_VARIABLE(PB_Main_msg, PB_Main_msg_t)));
|
API_VARIABLE(PB_Main_msg, PB_Main_msg_t)));
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ App(
|
|||||||
icon="A_BadUsb_14",
|
icon="A_BadUsb_14",
|
||||||
order=70,
|
order=70,
|
||||||
resources="resources",
|
resources="resources",
|
||||||
fap_libs=["assets"],
|
fap_libs=["assets", "ble_profile"],
|
||||||
fap_icon="icon.png",
|
fap_icon="icon.png",
|
||||||
fap_category="USB",
|
fap_category="USB",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ static void bad_usb_load_settings(BadUsbApp* app) {
|
|||||||
|
|
||||||
FuriString* temp_str = furi_string_alloc();
|
FuriString* temp_str = furi_string_alloc();
|
||||||
uint32_t version = 0;
|
uint32_t version = 0;
|
||||||
|
uint32_t interface = 0;
|
||||||
|
|
||||||
if(flipper_format_file_open_existing(fff, BAD_USB_SETTINGS_PATH)) {
|
if(flipper_format_file_open_existing(fff, BAD_USB_SETTINGS_PATH)) {
|
||||||
do {
|
do {
|
||||||
@@ -44,6 +45,8 @@ static void bad_usb_load_settings(BadUsbApp* app) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
if(!flipper_format_read_string(fff, "layout", temp_str)) break;
|
if(!flipper_format_read_string(fff, "layout", temp_str)) break;
|
||||||
|
if(!flipper_format_read_uint32(fff, "interface", &interface, 1)) break;
|
||||||
|
if(interface > BadUsbHidInterfaceBle) break;
|
||||||
|
|
||||||
state = true;
|
state = true;
|
||||||
} while(0);
|
} while(0);
|
||||||
@@ -53,6 +56,7 @@ static void bad_usb_load_settings(BadUsbApp* app) {
|
|||||||
|
|
||||||
if(state) {
|
if(state) {
|
||||||
furi_string_set(app->keyboard_layout, temp_str);
|
furi_string_set(app->keyboard_layout, temp_str);
|
||||||
|
app->interface = interface;
|
||||||
|
|
||||||
Storage* fs_api = furi_record_open(RECORD_STORAGE);
|
Storage* fs_api = furi_record_open(RECORD_STORAGE);
|
||||||
FileInfo layout_file_info;
|
FileInfo layout_file_info;
|
||||||
@@ -64,6 +68,7 @@ static void bad_usb_load_settings(BadUsbApp* app) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
furi_string_set(app->keyboard_layout, BAD_USB_SETTINGS_DEFAULT_LAYOUT);
|
furi_string_set(app->keyboard_layout, BAD_USB_SETTINGS_DEFAULT_LAYOUT);
|
||||||
|
app->interface = BadUsbHidInterfaceUsb;
|
||||||
}
|
}
|
||||||
|
|
||||||
furi_string_free(temp_str);
|
furi_string_free(temp_str);
|
||||||
@@ -79,6 +84,9 @@ static void bad_usb_save_settings(BadUsbApp* app) {
|
|||||||
fff, BAD_USB_SETTINGS_FILE_TYPE, BAD_USB_SETTINGS_VERSION))
|
fff, BAD_USB_SETTINGS_FILE_TYPE, BAD_USB_SETTINGS_VERSION))
|
||||||
break;
|
break;
|
||||||
if(!flipper_format_write_string(fff, "layout", app->keyboard_layout)) break;
|
if(!flipper_format_write_string(fff, "layout", app->keyboard_layout)) break;
|
||||||
|
uint32_t interface_id = app->interface;
|
||||||
|
if(!flipper_format_write_uint32(fff, "interface", (const uint32_t*)&interface_id, 1))
|
||||||
|
break;
|
||||||
} while(0);
|
} while(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +94,11 @@ static void bad_usb_save_settings(BadUsbApp* app) {
|
|||||||
furi_record_close(RECORD_STORAGE);
|
furi_record_close(RECORD_STORAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void bad_usb_set_interface(BadUsbApp* app, BadUsbHidInterface interface) {
|
||||||
|
app->interface = interface;
|
||||||
|
bad_usb_view_set_interface(app->bad_usb_view, interface);
|
||||||
|
}
|
||||||
|
|
||||||
BadUsbApp* bad_usb_app_alloc(char* arg) {
|
BadUsbApp* bad_usb_app_alloc(char* arg) {
|
||||||
BadUsbApp* app = malloc(sizeof(BadUsbApp));
|
BadUsbApp* app = malloc(sizeof(BadUsbApp));
|
||||||
|
|
||||||
@@ -117,7 +130,11 @@ BadUsbApp* bad_usb_app_alloc(char* arg) {
|
|||||||
// Custom Widget
|
// Custom Widget
|
||||||
app->widget = widget_alloc();
|
app->widget = widget_alloc();
|
||||||
view_dispatcher_add_view(
|
view_dispatcher_add_view(
|
||||||
app->view_dispatcher, BadUsbAppViewError, widget_get_view(app->widget));
|
app->view_dispatcher, BadUsbAppViewWidget, widget_get_view(app->widget));
|
||||||
|
|
||||||
|
// Popup
|
||||||
|
app->popup = popup_alloc();
|
||||||
|
view_dispatcher_add_view(app->view_dispatcher, BadUsbAppViewPopup, popup_get_view(app->popup));
|
||||||
|
|
||||||
app->var_item_list = variable_item_list_alloc();
|
app->var_item_list = variable_item_list_alloc();
|
||||||
view_dispatcher_add_view(
|
view_dispatcher_add_view(
|
||||||
@@ -163,9 +180,13 @@ void bad_usb_app_free(BadUsbApp* app) {
|
|||||||
bad_usb_view_free(app->bad_usb_view);
|
bad_usb_view_free(app->bad_usb_view);
|
||||||
|
|
||||||
// Custom Widget
|
// Custom Widget
|
||||||
view_dispatcher_remove_view(app->view_dispatcher, BadUsbAppViewError);
|
view_dispatcher_remove_view(app->view_dispatcher, BadUsbAppViewWidget);
|
||||||
widget_free(app->widget);
|
widget_free(app->widget);
|
||||||
|
|
||||||
|
// Popup
|
||||||
|
view_dispatcher_remove_view(app->view_dispatcher, BadUsbAppViewPopup);
|
||||||
|
popup_free(app->popup);
|
||||||
|
|
||||||
// Config menu
|
// Config menu
|
||||||
view_dispatcher_remove_view(app->view_dispatcher, BadUsbAppViewConfig);
|
view_dispatcher_remove_view(app->view_dispatcher, BadUsbAppViewConfig);
|
||||||
variable_item_list_free(app->var_item_list);
|
variable_item_list_free(app->var_item_list);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include <notification/notification_messages.h>
|
#include <notification/notification_messages.h>
|
||||||
#include <gui/modules/variable_item_list.h>
|
#include <gui/modules/variable_item_list.h>
|
||||||
#include <gui/modules/widget.h>
|
#include <gui/modules/widget.h>
|
||||||
|
#include <gui/modules/popup.h>
|
||||||
#include "views/bad_usb_view.h"
|
#include "views/bad_usb_view.h"
|
||||||
#include <furi_hal_usb.h>
|
#include <furi_hal_usb.h>
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ struct BadUsbApp {
|
|||||||
NotificationApp* notifications;
|
NotificationApp* notifications;
|
||||||
DialogsApp* dialogs;
|
DialogsApp* dialogs;
|
||||||
Widget* widget;
|
Widget* widget;
|
||||||
|
Popup* popup;
|
||||||
VariableItemList* var_item_list;
|
VariableItemList* var_item_list;
|
||||||
|
|
||||||
BadUsbAppError error;
|
BadUsbAppError error;
|
||||||
@@ -41,11 +43,15 @@ struct BadUsbApp {
|
|||||||
BadUsb* bad_usb_view;
|
BadUsb* bad_usb_view;
|
||||||
BadUsbScript* bad_usb_script;
|
BadUsbScript* bad_usb_script;
|
||||||
|
|
||||||
|
BadUsbHidInterface interface;
|
||||||
FuriHalUsbInterface* usb_if_prev;
|
FuriHalUsbInterface* usb_if_prev;
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
BadUsbAppViewError,
|
BadUsbAppViewWidget,
|
||||||
|
BadUsbAppViewPopup,
|
||||||
BadUsbAppViewWork,
|
BadUsbAppViewWork,
|
||||||
BadUsbAppViewConfig,
|
BadUsbAppViewConfig,
|
||||||
} BadUsbAppView;
|
} BadUsbAppView;
|
||||||
|
|
||||||
|
void bad_usb_set_interface(BadUsbApp* app, BadUsbHidInterface interface);
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
#include "bad_usb_hid.h"
|
#include "bad_usb_hid.h"
|
||||||
#include <extra_profiles/hid_profile.h>
|
#include <extra_profiles/hid_profile.h>
|
||||||
|
#include <bt/bt_service/bt.h>
|
||||||
#include <storage/storage.h>
|
#include <storage/storage.h>
|
||||||
|
|
||||||
#define TAG "BadUSB HID"
|
#define TAG "BadUSB HID"
|
||||||
|
|
||||||
|
#define HID_BT_KEYS_STORAGE_NAME ".bt_hid.keys"
|
||||||
|
|
||||||
void* hid_usb_init(FuriHalUsbHidConfig* hid_cfg) {
|
void* hid_usb_init(FuriHalUsbHidConfig* hid_cfg) {
|
||||||
furi_check(furi_hal_usb_set_config(&usb_hid, hid_cfg));
|
furi_check(furi_hal_usb_set_config(&usb_hid, hid_cfg));
|
||||||
return NULL;
|
return NULL;
|
||||||
@@ -69,6 +72,155 @@ static const BadUsbHidApi hid_api_usb = {
|
|||||||
.release_all = hid_usb_release_all,
|
.release_all = hid_usb_release_all,
|
||||||
.get_led_state = hid_usb_get_led_state,
|
.get_led_state = hid_usb_get_led_state,
|
||||||
};
|
};
|
||||||
const BadUsbHidApi* bad_usb_hid_get_interface() {
|
|
||||||
return &hid_api_usb;
|
typedef struct {
|
||||||
|
Bt* bt;
|
||||||
|
FuriHalBleProfileBase* profile;
|
||||||
|
HidStateCallback state_callback;
|
||||||
|
void* callback_context;
|
||||||
|
bool is_connected;
|
||||||
|
} BleHidInstance;
|
||||||
|
|
||||||
|
static const BleProfileHidParams ble_hid_params = {
|
||||||
|
.device_name_prefix = "BadUSB",
|
||||||
|
.mac_xor = 0x0002,
|
||||||
|
};
|
||||||
|
|
||||||
|
static void hid_ble_connection_status_callback(BtStatus status, void* context) {
|
||||||
|
furi_assert(context);
|
||||||
|
BleHidInstance* ble_hid = context;
|
||||||
|
ble_hid->is_connected = (status == BtStatusConnected);
|
||||||
|
if(ble_hid->state_callback) {
|
||||||
|
ble_hid->state_callback(ble_hid->is_connected, ble_hid->callback_context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void* hid_ble_init(FuriHalUsbHidConfig* hid_cfg) {
|
||||||
|
UNUSED(hid_cfg);
|
||||||
|
BleHidInstance* ble_hid = malloc(sizeof(BleHidInstance));
|
||||||
|
ble_hid->bt = furi_record_open(RECORD_BT);
|
||||||
|
bt_disconnect(ble_hid->bt);
|
||||||
|
|
||||||
|
// Wait 2nd core to update nvm storage
|
||||||
|
furi_delay_ms(200);
|
||||||
|
|
||||||
|
bt_keys_storage_set_storage_path(ble_hid->bt, APP_DATA_PATH(HID_BT_KEYS_STORAGE_NAME));
|
||||||
|
|
||||||
|
ble_hid->profile = bt_profile_start(ble_hid->bt, ble_profile_hid, (void*)&ble_hid_params);
|
||||||
|
furi_check(ble_hid->profile);
|
||||||
|
|
||||||
|
furi_hal_bt_start_advertising();
|
||||||
|
|
||||||
|
bt_set_status_changed_callback(ble_hid->bt, hid_ble_connection_status_callback, ble_hid);
|
||||||
|
|
||||||
|
return ble_hid;
|
||||||
|
}
|
||||||
|
|
||||||
|
void hid_ble_deinit(void* inst) {
|
||||||
|
BleHidInstance* ble_hid = inst;
|
||||||
|
furi_assert(ble_hid);
|
||||||
|
|
||||||
|
bt_set_status_changed_callback(ble_hid->bt, NULL, NULL);
|
||||||
|
bt_disconnect(ble_hid->bt);
|
||||||
|
|
||||||
|
// Wait 2nd core to update nvm storage
|
||||||
|
furi_delay_ms(200);
|
||||||
|
bt_keys_storage_set_default_path(ble_hid->bt);
|
||||||
|
|
||||||
|
furi_check(bt_profile_restore_default(ble_hid->bt));
|
||||||
|
furi_record_close(RECORD_BT);
|
||||||
|
free(ble_hid);
|
||||||
|
}
|
||||||
|
|
||||||
|
void hid_ble_set_state_callback(void* inst, HidStateCallback cb, void* context) {
|
||||||
|
BleHidInstance* ble_hid = inst;
|
||||||
|
furi_assert(ble_hid);
|
||||||
|
ble_hid->state_callback = cb;
|
||||||
|
ble_hid->callback_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hid_ble_is_connected(void* inst) {
|
||||||
|
BleHidInstance* ble_hid = inst;
|
||||||
|
furi_assert(ble_hid);
|
||||||
|
return ble_hid->is_connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hid_ble_kb_press(void* inst, uint16_t button) {
|
||||||
|
BleHidInstance* ble_hid = inst;
|
||||||
|
furi_assert(ble_hid);
|
||||||
|
return ble_profile_hid_kb_press(ble_hid->profile, button);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hid_ble_kb_release(void* inst, uint16_t button) {
|
||||||
|
BleHidInstance* ble_hid = inst;
|
||||||
|
furi_assert(ble_hid);
|
||||||
|
return ble_profile_hid_kb_release(ble_hid->profile, button);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hid_ble_consumer_press(void* inst, uint16_t button) {
|
||||||
|
BleHidInstance* ble_hid = inst;
|
||||||
|
furi_assert(ble_hid);
|
||||||
|
return ble_profile_hid_consumer_key_press(ble_hid->profile, button);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hid_ble_consumer_release(void* inst, uint16_t button) {
|
||||||
|
BleHidInstance* ble_hid = inst;
|
||||||
|
furi_assert(ble_hid);
|
||||||
|
return ble_profile_hid_consumer_key_release(ble_hid->profile, button);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hid_ble_release_all(void* inst) {
|
||||||
|
BleHidInstance* ble_hid = inst;
|
||||||
|
furi_assert(ble_hid);
|
||||||
|
bool state = ble_profile_hid_kb_release_all(ble_hid->profile);
|
||||||
|
state &= ble_profile_hid_consumer_key_release_all(ble_hid->profile);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t hid_ble_get_led_state(void* inst) {
|
||||||
|
UNUSED(inst);
|
||||||
|
FURI_LOG_W(TAG, "hid_ble_get_led_state not implemented");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const BadUsbHidApi hid_api_ble = {
|
||||||
|
.init = hid_ble_init,
|
||||||
|
.deinit = hid_ble_deinit,
|
||||||
|
.set_state_callback = hid_ble_set_state_callback,
|
||||||
|
.is_connected = hid_ble_is_connected,
|
||||||
|
|
||||||
|
.kb_press = hid_ble_kb_press,
|
||||||
|
.kb_release = hid_ble_kb_release,
|
||||||
|
.consumer_press = hid_ble_consumer_press,
|
||||||
|
.consumer_release = hid_ble_consumer_release,
|
||||||
|
.release_all = hid_ble_release_all,
|
||||||
|
.get_led_state = hid_ble_get_led_state,
|
||||||
|
};
|
||||||
|
|
||||||
|
const BadUsbHidApi* bad_usb_hid_get_interface(BadUsbHidInterface interface) {
|
||||||
|
if(interface == BadUsbHidInterfaceUsb) {
|
||||||
|
return &hid_api_usb;
|
||||||
|
} else {
|
||||||
|
return &hid_api_ble;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void bad_usb_hid_ble_remove_pairing(void) {
|
||||||
|
Bt* bt = furi_record_open(RECORD_BT);
|
||||||
|
bt_disconnect(bt);
|
||||||
|
|
||||||
|
// Wait 2nd core to update nvm storage
|
||||||
|
furi_delay_ms(200);
|
||||||
|
|
||||||
|
furi_hal_bt_stop_advertising();
|
||||||
|
|
||||||
|
bt_keys_storage_set_storage_path(bt, APP_DATA_PATH(HID_BT_KEYS_STORAGE_NAME));
|
||||||
|
bt_forget_bonded_devices(bt);
|
||||||
|
|
||||||
|
// Wait 2nd core to update nvm storage
|
||||||
|
furi_delay_ms(200);
|
||||||
|
bt_keys_storage_set_default_path(bt);
|
||||||
|
|
||||||
|
furi_check(bt_profile_restore_default(bt));
|
||||||
|
furi_record_close(RECORD_BT);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ extern "C" {
|
|||||||
#include <furi.h>
|
#include <furi.h>
|
||||||
#include <furi_hal.h>
|
#include <furi_hal.h>
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
BadUsbHidInterfaceUsb,
|
||||||
|
BadUsbHidInterfaceBle,
|
||||||
|
} BadUsbHidInterface;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
void* (*init)(FuriHalUsbHidConfig* hid_cfg);
|
void* (*init)(FuriHalUsbHidConfig* hid_cfg);
|
||||||
void (*deinit)(void* inst);
|
void (*deinit)(void* inst);
|
||||||
@@ -21,7 +26,7 @@ typedef struct {
|
|||||||
uint8_t (*get_led_state)(void* inst);
|
uint8_t (*get_led_state)(void* inst);
|
||||||
} BadUsbHidApi;
|
} BadUsbHidApi;
|
||||||
|
|
||||||
const BadUsbHidApi* bad_usb_hid_get_interface();
|
const BadUsbHidApi* bad_usb_hid_get_interface(BadUsbHidInterface interface);
|
||||||
|
|
||||||
void bad_usb_hid_ble_remove_pairing(void);
|
void bad_usb_hid_ble_remove_pairing(void);
|
||||||
|
|
||||||
|
|||||||
@@ -650,7 +650,7 @@ static void bad_usb_script_set_default_keyboard_layout(BadUsbScript* bad_usb) {
|
|||||||
memcpy(bad_usb->layout, hid_asciimap, MIN(sizeof(hid_asciimap), sizeof(bad_usb->layout)));
|
memcpy(bad_usb->layout, hid_asciimap, MIN(sizeof(hid_asciimap), sizeof(bad_usb->layout)));
|
||||||
}
|
}
|
||||||
|
|
||||||
BadUsbScript* bad_usb_script_open(FuriString* file_path) {
|
BadUsbScript* bad_usb_script_open(FuriString* file_path, BadUsbHidInterface interface) {
|
||||||
furi_assert(file_path);
|
furi_assert(file_path);
|
||||||
|
|
||||||
BadUsbScript* bad_usb = malloc(sizeof(BadUsbScript));
|
BadUsbScript* bad_usb = malloc(sizeof(BadUsbScript));
|
||||||
@@ -660,7 +660,7 @@ BadUsbScript* bad_usb_script_open(FuriString* file_path) {
|
|||||||
|
|
||||||
bad_usb->st.state = BadUsbStateInit;
|
bad_usb->st.state = BadUsbStateInit;
|
||||||
bad_usb->st.error[0] = '\0';
|
bad_usb->st.error[0] = '\0';
|
||||||
bad_usb->hid = bad_usb_hid_get_interface();
|
bad_usb->hid = bad_usb_hid_get_interface(interface);
|
||||||
|
|
||||||
bad_usb->thread = furi_thread_alloc_ex("BadUsbWorker", 2048, bad_usb_worker, bad_usb);
|
bad_usb->thread = furi_thread_alloc_ex("BadUsbWorker", 2048, bad_usb_worker, bad_usb);
|
||||||
furi_thread_start(bad_usb->thread);
|
furi_thread_start(bad_usb->thread);
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ typedef struct {
|
|||||||
|
|
||||||
typedef struct BadUsbScript BadUsbScript;
|
typedef struct BadUsbScript BadUsbScript;
|
||||||
|
|
||||||
BadUsbScript* bad_usb_script_open(FuriString* file_path);
|
BadUsbScript* bad_usb_script_open(FuriString* file_path, BadUsbHidInterface interface);
|
||||||
|
|
||||||
void bad_usb_script_close(BadUsbScript* bad_usb);
|
void bad_usb_script_close(BadUsbScript* bad_usb);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
REM This is BadUSB demo script for ChromeOS by kowalski7cc
|
REM This is BadUSB demo script for Chrome and ChromeOS by kowalski7cc
|
||||||
|
|
||||||
|
REM Exit from Overview
|
||||||
|
ESC
|
||||||
REM Open a new tab
|
REM Open a new tab
|
||||||
CTRL t
|
CTRL t
|
||||||
REM wait for some slower chromebooks
|
REM wait for some slower chromebooks
|
||||||
DELAY 1000
|
DELAY 1000
|
||||||
|
REM Make sure we have omnibox focus
|
||||||
|
CTRL l
|
||||||
|
DELAY 200
|
||||||
REM Open an empty editable page
|
REM Open an empty editable page
|
||||||
DEFAULT_DELAY 50
|
DEFAULT_DELAY 50
|
||||||
STRING data:text/html, <html contenteditable autofocus><style>body{font-family:monospace;}
|
STRING data:text/html, <html contenteditable autofocus><title>Flipper Zero BadUSB Demo</title><style>body{font-family:monospace;}
|
||||||
ENTER
|
ENTER
|
||||||
DELAY 500
|
DELAY 500
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,22 @@ REM Check the `lsusb` command to know your own devices IDs
|
|||||||
|
|
||||||
REM This is BadUSB demo script for Linux/Gnome
|
REM This is BadUSB demo script for Linux/Gnome
|
||||||
|
|
||||||
|
REM Exit from Overview
|
||||||
|
ESC
|
||||||
|
DELAY 200
|
||||||
REM Open terminal window
|
REM Open terminal window
|
||||||
DELAY 1000
|
|
||||||
ALT F2
|
ALT F2
|
||||||
DELAY 500
|
DELAY 1000
|
||||||
STRING gnome-terminal --maximize
|
REM Let's guess user terminal, based on (almost) glib order with ptyxis now default in Fedora 41
|
||||||
DELAY 500
|
STRING sh -c "xdg-terminal-exec||kgx||ptyxis||gnome-terminal||mate-terminal||xfce4-terminal||tilix||konsole||xterm"
|
||||||
|
DELAY 300
|
||||||
|
ENTER
|
||||||
|
REM It can take a bit to open the correct terminal
|
||||||
|
DELAY 1500
|
||||||
|
|
||||||
|
REM Make sure we are running in a POSIX-compliant shell
|
||||||
|
STRING env sh
|
||||||
ENTER
|
ENTER
|
||||||
DELAY 750
|
|
||||||
|
|
||||||
REM Clear the screen in case some banner was displayed
|
REM Clear the screen in case some banner was displayed
|
||||||
STRING clear
|
STRING clear
|
||||||
|
|||||||
59
applications/main/bad_usb/scenes/bad_usb_scene_config.c
Normal file
59
applications/main/bad_usb/scenes/bad_usb_scene_config.c
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
#include "../bad_usb_app_i.h"
|
||||||
|
|
||||||
|
enum SubmenuIndex {
|
||||||
|
ConfigIndexKeyboardLayout,
|
||||||
|
ConfigIndexBleUnpair,
|
||||||
|
};
|
||||||
|
|
||||||
|
void bad_usb_scene_config_select_callback(void* context, uint32_t index) {
|
||||||
|
BadUsbApp* bad_usb = context;
|
||||||
|
|
||||||
|
view_dispatcher_send_custom_event(bad_usb->view_dispatcher, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void draw_menu(BadUsbApp* bad_usb) {
|
||||||
|
VariableItemList* var_item_list = bad_usb->var_item_list;
|
||||||
|
|
||||||
|
variable_item_list_reset(var_item_list);
|
||||||
|
|
||||||
|
variable_item_list_add(var_item_list, "Keyboard Layout (global)", 0, NULL, NULL);
|
||||||
|
|
||||||
|
variable_item_list_add(var_item_list, "Remove Pairing", 0, NULL, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bad_usb_scene_config_on_enter(void* context) {
|
||||||
|
BadUsbApp* bad_usb = context;
|
||||||
|
VariableItemList* var_item_list = bad_usb->var_item_list;
|
||||||
|
|
||||||
|
variable_item_list_set_enter_callback(
|
||||||
|
var_item_list, bad_usb_scene_config_select_callback, bad_usb);
|
||||||
|
draw_menu(bad_usb);
|
||||||
|
variable_item_list_set_selected_item(var_item_list, 0);
|
||||||
|
|
||||||
|
view_dispatcher_switch_to_view(bad_usb->view_dispatcher, BadUsbAppViewConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bad_usb_scene_config_on_event(void* context, SceneManagerEvent event) {
|
||||||
|
BadUsbApp* bad_usb = context;
|
||||||
|
bool consumed = false;
|
||||||
|
|
||||||
|
if(event.type == SceneManagerEventTypeCustom) {
|
||||||
|
consumed = true;
|
||||||
|
if(event.event == ConfigIndexKeyboardLayout) {
|
||||||
|
scene_manager_next_scene(bad_usb->scene_manager, BadUsbSceneConfigLayout);
|
||||||
|
} else if(event.event == ConfigIndexBleUnpair) {
|
||||||
|
scene_manager_next_scene(bad_usb->scene_manager, BadUsbSceneConfirmUnpair);
|
||||||
|
} else {
|
||||||
|
furi_crash("Unknown key type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void bad_usb_scene_config_on_exit(void* context) {
|
||||||
|
BadUsbApp* bad_usb = context;
|
||||||
|
VariableItemList* var_item_list = bad_usb->var_item_list;
|
||||||
|
|
||||||
|
variable_item_list_reset(var_item_list);
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
ADD_SCENE(bad_usb, file_select, FileSelect)
|
ADD_SCENE(bad_usb, file_select, FileSelect)
|
||||||
ADD_SCENE(bad_usb, work, Work)
|
ADD_SCENE(bad_usb, work, Work)
|
||||||
ADD_SCENE(bad_usb, error, Error)
|
ADD_SCENE(bad_usb, error, Error)
|
||||||
|
ADD_SCENE(bad_usb, config, Config)
|
||||||
ADD_SCENE(bad_usb, config_layout, ConfigLayout)
|
ADD_SCENE(bad_usb, config_layout, ConfigLayout)
|
||||||
|
ADD_SCENE(bad_usb, confirm_unpair, ConfirmUnpair)
|
||||||
|
ADD_SCENE(bad_usb, unpair_done, UnpairDone)
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
#include "../bad_ble_app_i.h"
|
#include "../bad_usb_app_i.h"
|
||||||
|
|
||||||
void bad_ble_scene_confirm_unpair_widget_callback(
|
void bad_usb_scene_confirm_unpair_widget_callback(
|
||||||
GuiButtonType type,
|
GuiButtonType type,
|
||||||
InputType input_type,
|
InputType input_type,
|
||||||
void* context) {
|
void* context) {
|
||||||
UNUSED(input_type);
|
UNUSED(input_type);
|
||||||
SceneManagerEvent event = {.type = SceneManagerEventTypeCustom, .event = type};
|
SceneManagerEvent event = {.type = SceneManagerEventTypeCustom, .event = type};
|
||||||
bad_ble_scene_confirm_unpair_on_event(context, event);
|
bad_usb_scene_confirm_unpair_on_event(context, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
void bad_ble_scene_confirm_unpair_on_enter(void* context) {
|
void bad_usb_scene_confirm_unpair_on_enter(void* context) {
|
||||||
BadBleApp* bad_ble = context;
|
BadUsbApp* bad_usb = context;
|
||||||
Widget* widget = bad_ble->widget;
|
Widget* widget = bad_usb->widget;
|
||||||
|
|
||||||
widget_add_button_element(
|
widget_add_button_element(
|
||||||
widget, GuiButtonTypeLeft, "Cancel", bad_ble_scene_confirm_unpair_widget_callback, context);
|
widget, GuiButtonTypeLeft, "Cancel", bad_usb_scene_confirm_unpair_widget_callback, context);
|
||||||
widget_add_button_element(
|
widget_add_button_element(
|
||||||
widget,
|
widget,
|
||||||
GuiButtonTypeRight,
|
GuiButtonTypeRight,
|
||||||
"Unpair",
|
"Unpair",
|
||||||
bad_ble_scene_confirm_unpair_widget_callback,
|
bad_usb_scene_confirm_unpair_widget_callback,
|
||||||
context);
|
context);
|
||||||
|
|
||||||
widget_add_text_box_element(
|
widget_add_text_box_element(
|
||||||
widget, 0, 0, 128, 64, AlignCenter, AlignTop, "\e#Unpair the Device?\e#\n", false);
|
widget, 0, 0, 128, 64, AlignCenter, AlignTop, "\e#Unpair the Device?\e#\n", false);
|
||||||
|
|
||||||
view_dispatcher_switch_to_view(bad_ble->view_dispatcher, BadBleAppViewWidget);
|
view_dispatcher_switch_to_view(bad_usb->view_dispatcher, BadUsbAppViewWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool bad_ble_scene_confirm_unpair_on_event(void* context, SceneManagerEvent event) {
|
bool bad_usb_scene_confirm_unpair_on_event(void* context, SceneManagerEvent event) {
|
||||||
BadBleApp* bad_ble = context;
|
BadUsbApp* bad_usb = context;
|
||||||
SceneManager* scene_manager = bad_ble->scene_manager;
|
SceneManager* scene_manager = bad_usb->scene_manager;
|
||||||
bool consumed = false;
|
bool consumed = false;
|
||||||
|
|
||||||
if(event.type == SceneManagerEventTypeCustom) {
|
if(event.type == SceneManagerEventTypeCustom) {
|
||||||
consumed = true;
|
consumed = true;
|
||||||
if(event.event == GuiButtonTypeRight) {
|
if(event.event == GuiButtonTypeRight) {
|
||||||
scene_manager_next_scene(scene_manager, BadBleSceneUnpairDone);
|
scene_manager_next_scene(scene_manager, BadUsbSceneUnpairDone);
|
||||||
} else if(event.event == GuiButtonTypeLeft) {
|
} else if(event.event == GuiButtonTypeLeft) {
|
||||||
scene_manager_previous_scene(scene_manager);
|
scene_manager_previous_scene(scene_manager);
|
||||||
}
|
}
|
||||||
@@ -45,9 +45,9 @@ bool bad_ble_scene_confirm_unpair_on_event(void* context, SceneManagerEvent even
|
|||||||
return consumed;
|
return consumed;
|
||||||
}
|
}
|
||||||
|
|
||||||
void bad_ble_scene_confirm_unpair_on_exit(void* context) {
|
void bad_usb_scene_confirm_unpair_on_exit(void* context) {
|
||||||
BadBleApp* bad_ble = context;
|
BadUsbApp* bad_usb = context;
|
||||||
Widget* widget = bad_ble->widget;
|
Widget* widget = bad_usb->widget;
|
||||||
|
|
||||||
widget_reset(widget);
|
widget_reset(widget);
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ void bad_usb_scene_error_on_enter(void* context) {
|
|||||||
"Disconnect from\nPC or phone to\nuse this function.");
|
"Disconnect from\nPC or phone to\nuse this function.");
|
||||||
}
|
}
|
||||||
|
|
||||||
view_dispatcher_switch_to_view(app->view_dispatcher, BadUsbAppViewError);
|
view_dispatcher_switch_to_view(app->view_dispatcher, BadUsbAppViewWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool bad_usb_scene_error_on_event(void* context, SceneManagerEvent event) {
|
bool bad_usb_scene_error_on_event(void* context, SceneManagerEvent event) {
|
||||||
|
|||||||
39
applications/main/bad_usb/scenes/bad_usb_scene_unpair_done.c
Normal file
39
applications/main/bad_usb/scenes/bad_usb_scene_unpair_done.c
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#include "../bad_usb_app_i.h"
|
||||||
|
|
||||||
|
static void bad_usb_scene_unpair_done_popup_callback(void* context) {
|
||||||
|
BadUsbApp* bad_usb = context;
|
||||||
|
scene_manager_search_and_switch_to_previous_scene(bad_usb->scene_manager, BadUsbSceneConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bad_usb_scene_unpair_done_on_enter(void* context) {
|
||||||
|
BadUsbApp* bad_usb = context;
|
||||||
|
Popup* popup = bad_usb->popup;
|
||||||
|
|
||||||
|
bad_usb_hid_ble_remove_pairing();
|
||||||
|
|
||||||
|
popup_set_icon(popup, 48, 4, &I_DolphinDone_80x58);
|
||||||
|
popup_set_header(popup, "Done", 20, 19, AlignLeft, AlignBottom);
|
||||||
|
popup_set_callback(popup, bad_usb_scene_unpair_done_popup_callback);
|
||||||
|
popup_set_context(popup, bad_usb);
|
||||||
|
popup_set_timeout(popup, 1500);
|
||||||
|
popup_enable_timeout(popup);
|
||||||
|
|
||||||
|
view_dispatcher_switch_to_view(bad_usb->view_dispatcher, BadUsbAppViewPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bad_usb_scene_unpair_done_on_event(void* context, SceneManagerEvent event) {
|
||||||
|
BadUsbApp* bad_usb = context;
|
||||||
|
UNUSED(bad_usb);
|
||||||
|
UNUSED(event);
|
||||||
|
bool consumed = false;
|
||||||
|
|
||||||
|
return consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void bad_usb_scene_unpair_done_on_exit(void* context) {
|
||||||
|
BadUsbApp* bad_usb = context;
|
||||||
|
Popup* popup = bad_usb->popup;
|
||||||
|
UNUSED(popup);
|
||||||
|
|
||||||
|
popup_reset(popup);
|
||||||
|
}
|
||||||
@@ -20,14 +20,27 @@ bool bad_usb_scene_work_on_event(void* context, SceneManagerEvent event) {
|
|||||||
bad_usb_script_close(app->bad_usb_script);
|
bad_usb_script_close(app->bad_usb_script);
|
||||||
app->bad_usb_script = NULL;
|
app->bad_usb_script = NULL;
|
||||||
|
|
||||||
scene_manager_next_scene(app->scene_manager, BadUsbSceneConfigLayout);
|
if(app->interface == BadUsbHidInterfaceBle) {
|
||||||
|
scene_manager_next_scene(app->scene_manager, BadUsbSceneConfig);
|
||||||
|
} else {
|
||||||
|
scene_manager_next_scene(app->scene_manager, BadUsbSceneConfigLayout);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
consumed = true;
|
consumed = true;
|
||||||
} else if(event.event == InputKeyOk) {
|
} else if(event.event == InputKeyOk) {
|
||||||
bad_usb_script_start_stop(app->bad_usb_script);
|
bad_usb_script_start_stop(app->bad_usb_script);
|
||||||
consumed = true;
|
consumed = true;
|
||||||
} else if(event.event == InputKeyRight) {
|
} else if(event.event == InputKeyRight) {
|
||||||
bad_usb_script_pause_resume(app->bad_usb_script);
|
if(bad_usb_view_is_idle_state(app->bad_usb_view)) {
|
||||||
|
bad_usb_set_interface(
|
||||||
|
app,
|
||||||
|
app->interface == BadUsbHidInterfaceBle ? BadUsbHidInterfaceUsb :
|
||||||
|
BadUsbHidInterfaceBle);
|
||||||
|
bad_usb_script_close(app->bad_usb_script);
|
||||||
|
app->bad_usb_script = bad_usb_script_open(app->file_path, app->interface);
|
||||||
|
} else {
|
||||||
|
bad_usb_script_pause_resume(app->bad_usb_script);
|
||||||
|
}
|
||||||
consumed = true;
|
consumed = true;
|
||||||
}
|
}
|
||||||
} else if(event.type == SceneManagerEventTypeTick) {
|
} else if(event.type == SceneManagerEventTypeTick) {
|
||||||
@@ -39,7 +52,9 @@ bool bad_usb_scene_work_on_event(void* context, SceneManagerEvent event) {
|
|||||||
void bad_usb_scene_work_on_enter(void* context) {
|
void bad_usb_scene_work_on_enter(void* context) {
|
||||||
BadUsbApp* app = context;
|
BadUsbApp* app = context;
|
||||||
|
|
||||||
app->bad_usb_script = bad_usb_script_open(app->file_path);
|
bad_usb_view_set_interface(app->bad_usb_view, app->interface);
|
||||||
|
|
||||||
|
app->bad_usb_script = bad_usb_script_open(app->file_path, app->interface);
|
||||||
bad_usb_script_set_keyboard_layout(app->bad_usb_script, app->keyboard_layout);
|
bad_usb_script_set_keyboard_layout(app->bad_usb_script, app->keyboard_layout);
|
||||||
|
|
||||||
FuriString* file_name;
|
FuriString* file_name;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ typedef struct {
|
|||||||
BadUsbState state;
|
BadUsbState state;
|
||||||
bool pause_wait;
|
bool pause_wait;
|
||||||
uint8_t anim_frame;
|
uint8_t anim_frame;
|
||||||
|
BadUsbHidInterface interface;
|
||||||
} BadUsbModel;
|
} BadUsbModel;
|
||||||
|
|
||||||
static void bad_usb_draw_callback(Canvas* canvas, void* _model) {
|
static void bad_usb_draw_callback(Canvas* canvas, void* _model) {
|
||||||
@@ -40,14 +41,24 @@ static void bad_usb_draw_callback(Canvas* canvas, void* _model) {
|
|||||||
|
|
||||||
furi_string_reset(disp_str);
|
furi_string_reset(disp_str);
|
||||||
|
|
||||||
canvas_draw_icon(canvas, 22, 24, &I_UsbTree_48x22);
|
if(model->interface == BadUsbHidInterfaceBle) {
|
||||||
|
canvas_draw_icon(canvas, 22, 24, &I_Bad_BLE_48x22);
|
||||||
|
} else {
|
||||||
|
canvas_draw_icon(canvas, 22, 24, &I_UsbTree_48x22);
|
||||||
|
}
|
||||||
|
|
||||||
BadUsbWorkerState state = model->state.state;
|
BadUsbWorkerState state = model->state.state;
|
||||||
|
|
||||||
if((state == BadUsbStateIdle) || (state == BadUsbStateDone) ||
|
if((state == BadUsbStateIdle) || (state == BadUsbStateDone) ||
|
||||||
(state == BadUsbStateNotConnected)) {
|
(state == BadUsbStateNotConnected)) {
|
||||||
elements_button_center(canvas, "Run");
|
elements_button_center(canvas, "Run");
|
||||||
elements_button_left(canvas, "Layout");
|
if(model->interface == BadUsbHidInterfaceBle) {
|
||||||
|
elements_button_right(canvas, "USB");
|
||||||
|
elements_button_left(canvas, "Config");
|
||||||
|
} else {
|
||||||
|
elements_button_right(canvas, "BLE");
|
||||||
|
elements_button_left(canvas, "Layout");
|
||||||
|
}
|
||||||
} else if((state == BadUsbStateRunning) || (state == BadUsbStateDelay)) {
|
} else if((state == BadUsbStateRunning) || (state == BadUsbStateDelay)) {
|
||||||
elements_button_center(canvas, "Stop");
|
elements_button_center(canvas, "Stop");
|
||||||
if(!model->pause_wait) {
|
if(!model->pause_wait) {
|
||||||
@@ -266,6 +277,10 @@ void bad_usb_view_set_state(BadUsb* bad_usb, BadUsbState* st) {
|
|||||||
true);
|
true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void bad_usb_view_set_interface(BadUsb* bad_usb, BadUsbHidInterface interface) {
|
||||||
|
with_view_model(bad_usb->view, BadUsbModel * model, { model->interface = interface; }, true);
|
||||||
|
}
|
||||||
|
|
||||||
bool bad_usb_view_is_idle_state(BadUsb* bad_usb) {
|
bool bad_usb_view_is_idle_state(BadUsb* bad_usb) {
|
||||||
bool is_idle = false;
|
bool is_idle = false;
|
||||||
with_view_model(
|
with_view_model(
|
||||||
|
|||||||
@@ -23,4 +23,6 @@ void bad_usb_view_set_layout(BadUsb* bad_usb, const char* layout);
|
|||||||
|
|
||||||
void bad_usb_view_set_state(BadUsb* bad_usb, BadUsbState* st);
|
void bad_usb_view_set_state(BadUsb* bad_usb, BadUsbState* st);
|
||||||
|
|
||||||
|
void bad_usb_view_set_interface(BadUsb* bad_usb, BadUsbHidInterface interface);
|
||||||
|
|
||||||
bool bad_usb_view_is_idle_state(BadUsb* bad_usb);
|
bool bad_usb_view_is_idle_state(BadUsb* bad_usb);
|
||||||
|
|||||||
@@ -200,6 +200,24 @@ App(
|
|||||||
sources=["plugins/supported_cards/skylanders.c"],
|
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="trt_parser",
|
||||||
|
apptype=FlipperAppType.PLUGIN,
|
||||||
|
entry_point="trt_plugin_ep",
|
||||||
|
targets=["f7"],
|
||||||
|
requires=["nfc"],
|
||||||
|
sources=["plugins/supported_cards/trt.c"],
|
||||||
|
)
|
||||||
|
|
||||||
App(
|
App(
|
||||||
appid="nfc_start",
|
appid="nfc_start",
|
||||||
targets=["f7"],
|
targets=["f7"],
|
||||||
|
|||||||
243
applications/main/nfc/plugins/supported_cards/hworld.c
Normal file
243
applications/main/nfc/plugins/supported_cards/hworld.c
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
// Flipper Zero parser for H World Hotel Key Cards
|
||||||
|
// H World operates around 10,000 hotels, most of which in mainland China
|
||||||
|
// Reverse engineering and parser written by @Torron (Github: @zinongli)
|
||||||
|
#include "nfc_supported_card_plugin.h"
|
||||||
|
#include <flipper_application.h>
|
||||||
|
#include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
|
||||||
|
#include <bit_lib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define TAG "H World"
|
||||||
|
#define ROOM_SECTOR 1
|
||||||
|
#define VIP_SECTOR 5
|
||||||
|
#define ROOM_SECTOR_KEY_BLOCK 7
|
||||||
|
#define VIP_SECTOR_KEY_BLOCK 23
|
||||||
|
#define ACCESS_INFO_BLOCK 5
|
||||||
|
#define ROOM_NUM_DECIMAL_BLOCK 6
|
||||||
|
#define H_WORLD_YEAR_OFFSET 2000
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint64_t a;
|
||||||
|
uint64_t b;
|
||||||
|
} MfClassicKeyPair;
|
||||||
|
|
||||||
|
static MfClassicKeyPair hworld_standard_keys[] = {
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 000
|
||||||
|
{.a = 0x543071543071, .b = 0x5F01015F0101}, // 001
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 002
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 003
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 004
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 005
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 006
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 007
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 008
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 009
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 010
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 011
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 012
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 013
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 014
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 015
|
||||||
|
};
|
||||||
|
|
||||||
|
static MfClassicKeyPair hworld_vip_keys[] = {
|
||||||
|
{.a = 0x000000000000, .b = 0xFFFFFFFFFFFF}, // 000
|
||||||
|
{.a = 0x543071543071, .b = 0x5F01015F0101}, // 001
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 002
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 003
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 004
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 005
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 006
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 007
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 008
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 009
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 010
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 011
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 012
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 013
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 014
|
||||||
|
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 015
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool hworld_verify(Nfc* nfc) {
|
||||||
|
bool verified = false;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const uint8_t block_num = mf_classic_get_first_block_num_of_sector(ROOM_SECTOR);
|
||||||
|
|
||||||
|
MfClassicKey standard_key = {0};
|
||||||
|
bit_lib_num_to_bytes_be(
|
||||||
|
hworld_standard_keys[ROOM_SECTOR].a, COUNT_OF(standard_key.data), standard_key.data);
|
||||||
|
|
||||||
|
MfClassicAuthContext auth_context;
|
||||||
|
MfClassicError standard_error = mf_classic_poller_sync_auth(
|
||||||
|
nfc, block_num, &standard_key, MfClassicKeyTypeA, &auth_context);
|
||||||
|
|
||||||
|
if(standard_error != MfClassicErrorNone) {
|
||||||
|
FURI_LOG_D(TAG, "Failed static key check for block %u", block_num);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
MfClassicKey vip_key = {0};
|
||||||
|
bit_lib_num_to_bytes_be(
|
||||||
|
hworld_vip_keys[VIP_SECTOR].b, COUNT_OF(vip_key.data), vip_key.data);
|
||||||
|
|
||||||
|
MfClassicError vip_error = mf_classic_poller_sync_auth(
|
||||||
|
nfc, block_num, &vip_key, MfClassicKeyTypeB, &auth_context);
|
||||||
|
|
||||||
|
if(vip_error == MfClassicErrorNone) {
|
||||||
|
FURI_LOG_D(TAG, "VIP card detected");
|
||||||
|
} else {
|
||||||
|
FURI_LOG_D(TAG, "Standard card detected");
|
||||||
|
}
|
||||||
|
|
||||||
|
verified = true;
|
||||||
|
} while(false);
|
||||||
|
|
||||||
|
return verified;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool hworld_read(Nfc* nfc, NfcDevice* device) {
|
||||||
|
furi_assert(nfc);
|
||||||
|
furi_assert(device);
|
||||||
|
|
||||||
|
bool is_read = false;
|
||||||
|
|
||||||
|
MfClassicData* data = mf_classic_alloc();
|
||||||
|
nfc_device_copy_data(device, NfcProtocolMfClassic, data);
|
||||||
|
|
||||||
|
do {
|
||||||
|
MfClassicType type = MfClassicType1k;
|
||||||
|
MfClassicError standard_error = mf_classic_poller_sync_detect_type(nfc, &type);
|
||||||
|
MfClassicError vip_error = MfClassicErrorNotPresent;
|
||||||
|
if(standard_error != MfClassicErrorNone) break;
|
||||||
|
data->type = type;
|
||||||
|
|
||||||
|
MfClassicDeviceKeys standard_keys = {};
|
||||||
|
for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
|
||||||
|
bit_lib_num_to_bytes_be(
|
||||||
|
hworld_standard_keys[i].a, sizeof(MfClassicKey), standard_keys.key_a[i].data);
|
||||||
|
FURI_BIT_SET(standard_keys.key_a_mask, i);
|
||||||
|
bit_lib_num_to_bytes_be(
|
||||||
|
hworld_standard_keys[i].b, sizeof(MfClassicKey), standard_keys.key_b[i].data);
|
||||||
|
FURI_BIT_SET(standard_keys.key_b_mask, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
standard_error = mf_classic_poller_sync_read(nfc, &standard_keys, data);
|
||||||
|
if(standard_error == MfClassicErrorNone) {
|
||||||
|
FURI_LOG_I(TAG, "Standard card successfully read");
|
||||||
|
} else {
|
||||||
|
MfClassicDeviceKeys vip_keys = {};
|
||||||
|
for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
|
||||||
|
bit_lib_num_to_bytes_be(
|
||||||
|
hworld_vip_keys[i].a, sizeof(MfClassicKey), vip_keys.key_a[i].data);
|
||||||
|
FURI_BIT_SET(vip_keys.key_a_mask, i);
|
||||||
|
bit_lib_num_to_bytes_be(
|
||||||
|
hworld_vip_keys[i].b, sizeof(MfClassicKey), vip_keys.key_b[i].data);
|
||||||
|
FURI_BIT_SET(vip_keys.key_b_mask, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
vip_error = mf_classic_poller_sync_read(nfc, &vip_keys, data);
|
||||||
|
|
||||||
|
if(vip_error == MfClassicErrorNone) {
|
||||||
|
FURI_LOG_I(TAG, "VIP card successfully read");
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nfc_device_set_data(device, NfcProtocolMfClassic, data);
|
||||||
|
|
||||||
|
is_read = (standard_error == MfClassicErrorNone) | (vip_error == MfClassicErrorNone);
|
||||||
|
} while(false);
|
||||||
|
|
||||||
|
mf_classic_free(data);
|
||||||
|
|
||||||
|
return is_read;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hworld_parse(const NfcDevice* device, FuriString* parsed_data) {
|
||||||
|
furi_assert(device);
|
||||||
|
const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
|
||||||
|
bool parsed = false;
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Check card type
|
||||||
|
if(data->type != MfClassicType1k) break;
|
||||||
|
|
||||||
|
// Check static key for verificaiton
|
||||||
|
const uint8_t* data_room_sec_key_a_ptr = &data->block[ROOM_SECTOR_KEY_BLOCK].data[0];
|
||||||
|
const uint8_t* data_room_sec_key_b_ptr = &data->block[ROOM_SECTOR_KEY_BLOCK].data[10];
|
||||||
|
uint64_t data_room_sec_key_a = bit_lib_get_bits_64(data_room_sec_key_a_ptr, 0, 48);
|
||||||
|
uint64_t data_room_sec_key_b = bit_lib_get_bits_64(data_room_sec_key_b_ptr, 0, 48);
|
||||||
|
if((data_room_sec_key_a != hworld_standard_keys[ROOM_SECTOR].a) |
|
||||||
|
(data_room_sec_key_b != hworld_standard_keys[ROOM_SECTOR].b))
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Check whether this card is VIP
|
||||||
|
const uint8_t* data_vip_sec_key_b_ptr = &data->block[VIP_SECTOR_KEY_BLOCK].data[10];
|
||||||
|
uint64_t data_vip_sec_key_b = bit_lib_get_bits_64(data_vip_sec_key_b_ptr, 0, 48);
|
||||||
|
bool is_hworld_vip = (data_vip_sec_key_b == hworld_vip_keys[VIP_SECTOR].b);
|
||||||
|
uint8_t room_floor = data->block[ACCESS_INFO_BLOCK].data[13];
|
||||||
|
uint8_t room_num = data->block[ACCESS_INFO_BLOCK].data[14];
|
||||||
|
|
||||||
|
// Check in date & time
|
||||||
|
uint16_t check_in_year = data->block[ACCESS_INFO_BLOCK].data[2] + H_WORLD_YEAR_OFFSET;
|
||||||
|
uint8_t check_in_month = data->block[ACCESS_INFO_BLOCK].data[3];
|
||||||
|
uint8_t check_in_day = data->block[ACCESS_INFO_BLOCK].data[4];
|
||||||
|
uint8_t check_in_hour = data->block[ACCESS_INFO_BLOCK].data[5];
|
||||||
|
uint8_t check_in_minute = data->block[ACCESS_INFO_BLOCK].data[6];
|
||||||
|
|
||||||
|
// Expire date & time
|
||||||
|
uint16_t expire_year = data->block[ACCESS_INFO_BLOCK].data[7] + H_WORLD_YEAR_OFFSET;
|
||||||
|
uint8_t expire_month = data->block[ACCESS_INFO_BLOCK].data[8];
|
||||||
|
uint8_t expire_day = data->block[ACCESS_INFO_BLOCK].data[9];
|
||||||
|
uint8_t expire_hour = data->block[ACCESS_INFO_BLOCK].data[10];
|
||||||
|
uint8_t expire_minute = data->block[ACCESS_INFO_BLOCK].data[11];
|
||||||
|
|
||||||
|
furi_string_cat_printf(parsed_data, "\e#H World Card\n");
|
||||||
|
furi_string_cat_printf(
|
||||||
|
parsed_data, "%s\n", is_hworld_vip ? "VIP card" : "Standard room key");
|
||||||
|
furi_string_cat_printf(parsed_data, "Room Num: %u%02u\n", room_floor, room_num);
|
||||||
|
furi_string_cat_printf(
|
||||||
|
parsed_data,
|
||||||
|
"Check-in Date: \n%04u-%02d-%02d\n%02d:%02d:00\n",
|
||||||
|
check_in_year,
|
||||||
|
check_in_month,
|
||||||
|
check_in_day,
|
||||||
|
check_in_hour,
|
||||||
|
check_in_minute);
|
||||||
|
furi_string_cat_printf(
|
||||||
|
parsed_data,
|
||||||
|
"Expiration Date: \n%04u-%02d-%02d\n%02d:%02d:00",
|
||||||
|
expire_year,
|
||||||
|
expire_month,
|
||||||
|
expire_day,
|
||||||
|
expire_hour,
|
||||||
|
expire_minute);
|
||||||
|
parsed = true;
|
||||||
|
} while(false);
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Actual implementation of app<>plugin interface */
|
||||||
|
static const NfcSupportedCardsPlugin hworld_plugin = {
|
||||||
|
.protocol = NfcProtocolMfClassic,
|
||||||
|
.verify = hworld_verify,
|
||||||
|
.read = hworld_read,
|
||||||
|
.parse = hworld_parse,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Plugin descriptor to comply with basic plugin specification */
|
||||||
|
static const FlipperAppPluginDescriptor hworld_plugin_descriptor = {
|
||||||
|
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
|
||||||
|
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
|
||||||
|
.entry_point = &hworld_plugin,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Plugin entry point - must return a pointer to const descriptor */
|
||||||
|
const FlipperAppPluginDescriptor* hworld_plugin_ep(void) {
|
||||||
|
return &hworld_plugin_descriptor;
|
||||||
|
}
|
||||||
94
applications/main/nfc/plugins/supported_cards/trt.c
Normal file
94
applications/main/nfc/plugins/supported_cards/trt.c
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
// Flipper Zero parser for for Tianjin Railway Transit (TRT)
|
||||||
|
// https://en.wikipedia.org/wiki/Tianjin_Metro
|
||||||
|
// Reverse engineering and parser development by @Torron (Github: @zinongli)
|
||||||
|
|
||||||
|
#include "nfc_supported_card_plugin.h"
|
||||||
|
#include <flipper_application.h>
|
||||||
|
#include <nfc/protocols/mf_ultralight/mf_ultralight.h>
|
||||||
|
#include <bit_lib.h>
|
||||||
|
|
||||||
|
#define TAG "TrtParser"
|
||||||
|
#define LATEST_SALE_MARKER 0x02
|
||||||
|
#define FULL_SALE_TIME_STAMP_PAGE 0x09
|
||||||
|
#define BALANCE_PAGE 0x08
|
||||||
|
#define SALE_RECORD_TIME_STAMP_A 0x0C
|
||||||
|
#define SALE_RECORD_TIME_STAMP_B 0x0E
|
||||||
|
#define SALE_YEAR_OFFSET 2000
|
||||||
|
|
||||||
|
static bool trt_parse(const NfcDevice* device, FuriString* parsed_data) {
|
||||||
|
furi_assert(device);
|
||||||
|
furi_assert(parsed_data);
|
||||||
|
|
||||||
|
const MfUltralightData* data = nfc_device_get_data(device, NfcProtocolMfUltralight);
|
||||||
|
|
||||||
|
bool parsed = false;
|
||||||
|
|
||||||
|
do {
|
||||||
|
uint8_t latest_sale_page = 0;
|
||||||
|
|
||||||
|
// Look for sale record signature
|
||||||
|
if(data->page[SALE_RECORD_TIME_STAMP_A].data[0] == LATEST_SALE_MARKER) {
|
||||||
|
latest_sale_page = SALE_RECORD_TIME_STAMP_A;
|
||||||
|
} else if(data->page[SALE_RECORD_TIME_STAMP_B].data[0] == LATEST_SALE_MARKER) {
|
||||||
|
latest_sale_page = SALE_RECORD_TIME_STAMP_B;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the sale record was backed up
|
||||||
|
const uint8_t* partial_record_pointer = &data->page[latest_sale_page - 1].data[0];
|
||||||
|
const uint8_t* full_record_pointer = &data->page[FULL_SALE_TIME_STAMP_PAGE].data[0];
|
||||||
|
uint32_t latest_sale_record = bit_lib_get_bits_32(partial_record_pointer, 3, 20);
|
||||||
|
uint32_t latest_sale_full_record = bit_lib_get_bits_32(full_record_pointer, 0, 27);
|
||||||
|
if(latest_sale_record != (latest_sale_full_record & 0xFFFFF)) break;
|
||||||
|
|
||||||
|
// Parse date
|
||||||
|
// yyy yyyymmmm dddddhhh hhnnnnnn
|
||||||
|
uint16_t sale_year = ((latest_sale_full_record & 0x7F00000) >> 20) + SALE_YEAR_OFFSET;
|
||||||
|
uint8_t sale_month = (latest_sale_full_record & 0xF0000) >> 16;
|
||||||
|
uint8_t sale_day = (latest_sale_full_record & 0xF800) >> 11;
|
||||||
|
uint8_t sale_hour = (latest_sale_full_record & 0x7C0) >> 6;
|
||||||
|
uint8_t sale_minute = latest_sale_full_record & 0x3F;
|
||||||
|
|
||||||
|
// Parse balance
|
||||||
|
uint16_t balance = bit_lib_get_bits_16(&data->page[BALANCE_PAGE].data[2], 0, 16);
|
||||||
|
uint16_t balance_yuan = balance / 100;
|
||||||
|
uint8_t balance_cent = balance % 100;
|
||||||
|
|
||||||
|
// Format string for rendering
|
||||||
|
furi_string_cat_printf(parsed_data, "\e#TRT Tianjin Metro\n");
|
||||||
|
furi_string_cat_printf(parsed_data, "Single-Use Ticket\n");
|
||||||
|
furi_string_cat_printf(parsed_data, "Balance: %u.%02u RMB\n", balance_yuan, balance_cent);
|
||||||
|
furi_string_cat_printf(
|
||||||
|
parsed_data,
|
||||||
|
"Sale Date: \n%04u-%02d-%02d %02d:%02d",
|
||||||
|
sale_year,
|
||||||
|
sale_month,
|
||||||
|
sale_day,
|
||||||
|
sale_hour,
|
||||||
|
sale_minute);
|
||||||
|
parsed = true;
|
||||||
|
} while(false);
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Actual implementation of app<>plugin interface */
|
||||||
|
static const NfcSupportedCardsPlugin trt_plugin = {
|
||||||
|
.protocol = NfcProtocolMfUltralight,
|
||||||
|
.verify = NULL,
|
||||||
|
.read = NULL,
|
||||||
|
.parse = trt_parse,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Plugin descriptor to comply with basic plugin specification */
|
||||||
|
static const FlipperAppPluginDescriptor trt_plugin_descriptor = {
|
||||||
|
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
|
||||||
|
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
|
||||||
|
.entry_point = &trt_plugin,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Plugin entry point - must return a pointer to const descriptor */
|
||||||
|
const FlipperAppPluginDescriptor* trt_plugin_ep(void) {
|
||||||
|
return &trt_plugin_descriptor;
|
||||||
|
}
|
||||||
@@ -1351,3 +1351,8 @@ E69DD9015A43
|
|||||||
C8382A233993
|
C8382A233993
|
||||||
7B304F2A12A6
|
7B304F2A12A6
|
||||||
FC9418BF788B
|
FC9418BF788B
|
||||||
|
|
||||||
|
# H World Hotel Chain Room Keys
|
||||||
|
543071543071
|
||||||
|
5F01015F0101
|
||||||
|
200510241234
|
||||||
|
|||||||
@@ -999,13 +999,12 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) {
|
|||||||
chat_event = subghz_chat_worker_get_event_chat(subghz_chat);
|
chat_event = subghz_chat_worker_get_event_chat(subghz_chat);
|
||||||
switch(chat_event.event) {
|
switch(chat_event.event) {
|
||||||
case SubGhzChatEventInputData:
|
case SubGhzChatEventInputData:
|
||||||
if(chat_event.c == CliSymbolAsciiETX) {
|
if(chat_event.c == CliKeyETX) {
|
||||||
printf("\r\n");
|
printf("\r\n");
|
||||||
chat_event.event = SubGhzChatEventUserExit;
|
chat_event.event = SubGhzChatEventUserExit;
|
||||||
subghz_chat_worker_put_event_chat(subghz_chat, &chat_event);
|
subghz_chat_worker_put_event_chat(subghz_chat, &chat_event);
|
||||||
break;
|
break;
|
||||||
} else if(
|
} else if((chat_event.c == CliKeyBackspace) || (chat_event.c == CliKeyDEL)) {
|
||||||
(chat_event.c == CliSymbolAsciiBackspace) || (chat_event.c == CliSymbolAsciiDel)) {
|
|
||||||
size_t len = furi_string_utf8_length(input);
|
size_t len = furi_string_utf8_length(input);
|
||||||
if(len > furi_string_utf8_length(name)) {
|
if(len > furi_string_utf8_length(name)) {
|
||||||
printf("%s", "\e[D\e[1P");
|
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);
|
furi_string_set(input, sysmsg);
|
||||||
}
|
}
|
||||||
} else if(chat_event.c == CliSymbolAsciiCR) {
|
} else if(chat_event.c == CliKeyCR) {
|
||||||
printf("\r\n");
|
printf("\r\n");
|
||||||
furi_string_push_back(input, '\r');
|
furi_string_push_back(input, '\r');
|
||||||
furi_string_push_back(input, '\n');
|
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));
|
furi_string_printf(input, "%s", furi_string_get_cstr(name));
|
||||||
printf("%s", furi_string_get_cstr(input));
|
printf("%s", furi_string_get_cstr(input));
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
} else if(chat_event.c == CliSymbolAsciiLF) {
|
} else if(chat_event.c == CliKeyLF) {
|
||||||
//cut out the symbol \n
|
//cut out the symbol \n
|
||||||
} else {
|
} else {
|
||||||
putc(chat_event.c, stdout);
|
putc(chat_event.c, stdout);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cli/cli.h>
|
#include <cli/cli.h>
|
||||||
|
#include <cli/cli_ansi.h>
|
||||||
|
|
||||||
void subghz_on_system_start(void);
|
void subghz_on_system_start(void);
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
#include "cli_i.h"
|
#include "cli_i.h"
|
||||||
#include "cli_commands.h"
|
#include "cli_commands.h"
|
||||||
#include "cli_vcp.h"
|
#include "cli_vcp.h"
|
||||||
|
#include "cli_ansi.h"
|
||||||
#include <furi_hal_version.h>
|
#include <furi_hal_version.h>
|
||||||
#include <loader/loader.h>
|
#include <loader/loader.h>
|
||||||
|
|
||||||
#define TAG "CliSrv"
|
#define TAG "CliSrv"
|
||||||
|
|
||||||
#define CLI_INPUT_LEN_LIMIT 256
|
#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_alloc(void) {
|
||||||
Cli* cli = malloc(sizeof(Cli));
|
Cli* cli = malloc(sizeof(Cli));
|
||||||
@@ -85,7 +88,7 @@ bool cli_cmd_interrupt_received(Cli* cli) {
|
|||||||
char c = '\0';
|
char c = '\0';
|
||||||
if(cli_is_connected(cli)) {
|
if(cli_is_connected(cli)) {
|
||||||
if(cli->session->rx((uint8_t*)&c, 1, 0) == 1) {
|
if(cli->session->rx((uint8_t*)&c, 1, 0) == 1) {
|
||||||
return c == CliSymbolAsciiETX;
|
return c == CliKeyETX;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return true;
|
return true;
|
||||||
@@ -102,7 +105,8 @@ void cli_print_usage(const char* cmd, const char* usage, const char* arg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void cli_motd(void) {
|
void cli_motd(void) {
|
||||||
printf("\r\n"
|
printf(ANSI_FLIPPER_BRAND_ORANGE
|
||||||
|
"\r\n"
|
||||||
" _.-------.._ -,\r\n"
|
" _.-------.._ -,\r\n"
|
||||||
" .-\"```\"--..,,_/ /`-, -, \\ \r\n"
|
" .-\"```\"--..,,_/ /`-, -, \\ \r\n"
|
||||||
" .:\" /:/ /'\\ \\ ,_..., `. | |\r\n"
|
" .:\" /:/ /'\\ \\ ,_..., `. | |\r\n"
|
||||||
@@ -116,12 +120,11 @@ void cli_motd(void) {
|
|||||||
" _L_ _ ___ ___ ___ ___ ____--\"`___ _ ___\r\n"
|
" _L_ _ ___ ___ ___ ___ ____--\"`___ _ ___\r\n"
|
||||||
"| __|| | |_ _|| _ \\| _ \\| __|| _ \\ / __|| | |_ _|\r\n"
|
"| __|| | |_ _|| _ \\| _ \\| __|| _ \\ / __|| | |_ _|\r\n"
|
||||||
"| _| | |__ | | | _/| _/| _| | / | (__ | |__ | |\r\n"
|
"| _| | |__ | | | _/| _/| _| | / | (__ | |__ | |\r\n"
|
||||||
"|_| |____||___||_| |_| |___||_|_\\ \\___||____||___|\r\n"
|
"|_| |____||___||_| |_| |___||_|_\\ \\___||____||___|\r\n" ANSI_RESET
|
||||||
"\r\n"
|
"\r\n" ANSI_FG_BR_WHITE "Welcome to " ANSI_FLIPPER_BRAND_ORANGE
|
||||||
"Welcome to Flipper Zero Command Line Interface!\r\n"
|
"Flipper Zero" ANSI_FG_BR_WHITE " Command Line Interface!\r\n"
|
||||||
"Read the manual: https://docs.flipper.net/development/cli\r\n"
|
"Read the manual: https://docs.flipper.net/development/cli\r\n"
|
||||||
"Run `help` or `?` to list available commands\r\n"
|
"Run `help` or `?` to list available commands\r\n" ANSI_RESET "\r\n");
|
||||||
"\r\n");
|
|
||||||
|
|
||||||
const Version* firmware_version = furi_hal_version_get_firmware_version();
|
const Version* firmware_version = furi_hal_version_get_firmware_version();
|
||||||
if(firmware_version) {
|
if(firmware_version) {
|
||||||
@@ -142,7 +145,7 @@ void cli_nl(Cli* cli) {
|
|||||||
|
|
||||||
void cli_prompt(Cli* cli) {
|
void cli_prompt(Cli* cli) {
|
||||||
UNUSED(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);
|
fflush(stdout);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +168,7 @@ static void cli_handle_backspace(Cli* cli) {
|
|||||||
|
|
||||||
cli->cursor_position--;
|
cli->cursor_position--;
|
||||||
} else {
|
} else {
|
||||||
cli_putc(cli, CliSymbolAsciiBell);
|
cli_putc(cli, CliKeyBell);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +244,7 @@ static void cli_handle_enter(Cli* cli) {
|
|||||||
printf(
|
printf(
|
||||||
"`%s` command not found, use `help` or `?` to list all available commands",
|
"`%s` command not found, use `help` or `?` to list all available commands",
|
||||||
furi_string_get_cstr(command));
|
furi_string_get_cstr(command));
|
||||||
cli_putc(cli, CliSymbolAsciiBell);
|
cli_putc(cli, CliKeyBell);
|
||||||
}
|
}
|
||||||
|
|
||||||
cli_reset(cli);
|
cli_reset(cli);
|
||||||
@@ -305,8 +308,85 @@ static void cli_handle_autocomplete(Cli* cli) {
|
|||||||
cli_prompt(cli);
|
cli_prompt(cli);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void cli_handle_escape(Cli* cli, char c) {
|
typedef enum {
|
||||||
if(c == 'A') {
|
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
|
// Use previous command if line buffer is empty
|
||||||
if(furi_string_size(cli->line) == 0 && furi_string_cmp(cli->line, cli->last_line) != 0) {
|
if(furi_string_size(cli->line) == 0 && furi_string_cmp(cli->line, cli->last_line) != 0) {
|
||||||
// Set line buffer and cursor position
|
// Set line buffer and cursor position
|
||||||
@@ -315,67 +395,85 @@ static void cli_handle_escape(Cli* cli, char c) {
|
|||||||
// Show new line to user
|
// Show new line to user
|
||||||
printf("%s", furi_string_get_cstr(cli->line));
|
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)) {
|
if(cli->cursor_position < furi_string_size(cli->line)) {
|
||||||
cli->cursor_position++;
|
cli->cursor_position++;
|
||||||
printf("\e[C");
|
printf("\e[C");
|
||||||
}
|
}
|
||||||
} else if(c == 'D') {
|
|
||||||
|
} else if(combo.key == CliKeyLeft && combo.modifiers == CliModKeyNo) {
|
||||||
|
// Move left
|
||||||
if(cli->cursor_position > 0) {
|
if(cli->cursor_position > 0) {
|
||||||
cli->cursor_position--;
|
cli->cursor_position--;
|
||||||
printf("\e[D");
|
printf("\e[D");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
fflush(stdout);
|
|
||||||
}
|
|
||||||
|
|
||||||
void cli_process_input(Cli* cli) {
|
} else if(combo.key == CliKeyHome && combo.modifiers == CliModKeyNo) {
|
||||||
char in_chr = cli_getc(cli);
|
// Move to beginning of line
|
||||||
size_t rx_len;
|
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(
|
} 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)) {
|
(furi_string_size(cli->line) < CLI_INPUT_LEN_LIMIT)) {
|
||||||
if(cli->cursor_position == furi_string_size(cli->line)) {
|
if(cli->cursor_position == furi_string_size(cli->line)) {
|
||||||
furi_string_push_back(cli->line, in_chr);
|
furi_string_push_back(cli->line, combo.key);
|
||||||
cli_putc(cli, in_chr);
|
cli_putc(cli, combo.key);
|
||||||
} else {
|
} else {
|
||||||
// Insert character to line buffer
|
// 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);
|
furi_string_replace_at(cli->line, cli->cursor_position, 0, in_str);
|
||||||
|
|
||||||
// Print character in replace mode
|
// Print character in replace mode
|
||||||
printf("\e[4h%c\e[4l", in_chr);
|
printf("\e[4h%c\e[4l", combo.key);
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
}
|
}
|
||||||
cli->cursor_position++;
|
cli->cursor_position++;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
cli_putc(cli, CliSymbolAsciiBell);
|
cli_putc(cli, CliKeyBell);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fflush(stdout);
|
||||||
}
|
}
|
||||||
|
|
||||||
void cli_add_command(
|
void cli_add_command(
|
||||||
|
|||||||
@@ -10,26 +10,12 @@
|
|||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#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 {
|
typedef enum {
|
||||||
CliCommandFlagDefault = 0, /**< Default, loader lock is used */
|
CliCommandFlagDefault = 0, /**< Default, loader lock is used */
|
||||||
CliCommandFlagParallelSafe =
|
CliCommandFlagParallelSafe =
|
||||||
(1 << 0), /**< Safe to run in parallel with other apps, loader lock is not used */
|
(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 */
|
CliCommandFlagInsomniaSafe = (1 << 1), /**< Safe to run with insomnia mode on */
|
||||||
|
CliCommandFlagHidden = (1 << 2), /**< Not shown in `help` */
|
||||||
} CliCommandFlag;
|
} CliCommandFlag;
|
||||||
|
|
||||||
#define RECORD_CLI "cli"
|
#define RECORD_CLI "cli"
|
||||||
|
|||||||
76
applications/services/cli/cli_ansi.c
Normal file
76
applications/services/cli/cli_ansi.c
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
#include "cli_ansi.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Converts a single character representing a special key into the enum
|
||||||
|
* representation
|
||||||
|
*/
|
||||||
|
static CliKey cli_ansi_key_from_mnemonic(char c) {
|
||||||
|
switch(c) {
|
||||||
|
case 'A':
|
||||||
|
return CliKeyUp;
|
||||||
|
case 'B':
|
||||||
|
return CliKeyDown;
|
||||||
|
case 'C':
|
||||||
|
return CliKeyRight;
|
||||||
|
case 'D':
|
||||||
|
return CliKeyLeft;
|
||||||
|
case 'F':
|
||||||
|
return CliKeyEnd;
|
||||||
|
case 'H':
|
||||||
|
return CliKeyHome;
|
||||||
|
default:
|
||||||
|
return CliKeyUnrecognized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CliKeyCombo cli_read_ansi_key_combo(Cli* cli) {
|
||||||
|
char ch = cli_getc(cli);
|
||||||
|
|
||||||
|
if(ch != CliKeyEsc)
|
||||||
|
return (CliKeyCombo){
|
||||||
|
.modifiers = CliModKeyNo,
|
||||||
|
.key = ch,
|
||||||
|
};
|
||||||
|
|
||||||
|
ch = cli_getc(cli);
|
||||||
|
|
||||||
|
// ESC ESC -> ESC
|
||||||
|
if(ch == '\e')
|
||||||
|
return (CliKeyCombo){
|
||||||
|
.modifiers = CliModKeyNo,
|
||||||
|
.key = '\e',
|
||||||
|
};
|
||||||
|
|
||||||
|
// ESC <char> -> Alt + <char>
|
||||||
|
if(ch != '[')
|
||||||
|
return (CliKeyCombo){
|
||||||
|
.modifiers = CliModKeyAlt,
|
||||||
|
.key = cli_getc(cli),
|
||||||
|
};
|
||||||
|
|
||||||
|
ch = cli_getc(cli);
|
||||||
|
|
||||||
|
// ESC [ 1
|
||||||
|
if(ch == '1') {
|
||||||
|
// ESC [ 1 ; <modifier bitfield> <key mnemonic>
|
||||||
|
if(cli_getc(cli) == ';') {
|
||||||
|
CliModKey modifiers = (cli_getc(cli) - '0'); // convert following digit to a number
|
||||||
|
modifiers &= ~1;
|
||||||
|
return (CliKeyCombo){
|
||||||
|
.modifiers = modifiers,
|
||||||
|
.key = cli_ansi_key_from_mnemonic(cli_getc(cli)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return (CliKeyCombo){
|
||||||
|
.modifiers = CliModKeyNo,
|
||||||
|
.key = CliKeyUnrecognized,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ESC [ <key mnemonic>
|
||||||
|
return (CliKeyCombo){
|
||||||
|
.modifiers = CliModKeyNo,
|
||||||
|
.key = cli_ansi_key_from_mnemonic(ch),
|
||||||
|
};
|
||||||
|
}
|
||||||
94
applications/services/cli/cli_ansi.h
Normal file
94
applications/services/cli/cli_ansi.h
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "cli.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define ANSI_RESET "\e[0m"
|
||||||
|
#define ANSI_BOLD "\e[1m"
|
||||||
|
#define ANSI_FAINT "\e[2m"
|
||||||
|
|
||||||
|
#define ANSI_FG_BLACK "\e[30m"
|
||||||
|
#define ANSI_FG_RED "\e[31m"
|
||||||
|
#define ANSI_FG_GREEN "\e[32m"
|
||||||
|
#define ANSI_FG_YELLOW "\e[33m"
|
||||||
|
#define ANSI_FG_BLUE "\e[34m"
|
||||||
|
#define ANSI_FG_MAGENTA "\e[35m"
|
||||||
|
#define ANSI_FG_CYAN "\e[36m"
|
||||||
|
#define ANSI_FG_WHITE "\e[37m"
|
||||||
|
#define ANSI_FG_BR_BLACK "\e[90m"
|
||||||
|
#define ANSI_FG_BR_RED "\e[91m"
|
||||||
|
#define ANSI_FG_BR_GREEN "\e[92m"
|
||||||
|
#define ANSI_FG_BR_YELLOW "\e[93m"
|
||||||
|
#define ANSI_FG_BR_BLUE "\e[94m"
|
||||||
|
#define ANSI_FG_BR_MAGENTA "\e[95m"
|
||||||
|
#define ANSI_FG_BR_CYAN "\e[96m"
|
||||||
|
#define ANSI_FG_BR_WHITE "\e[97m"
|
||||||
|
|
||||||
|
#define ANSI_BG_BLACK "\e[40m"
|
||||||
|
#define ANSI_BG_RED "\e[41m"
|
||||||
|
#define ANSI_BG_GREEN "\e[42m"
|
||||||
|
#define ANSI_BG_YELLOW "\e[43m"
|
||||||
|
#define ANSI_BG_BLUE "\e[44m"
|
||||||
|
#define ANSI_BG_MAGENTA "\e[45m"
|
||||||
|
#define ANSI_BG_CYAN "\e[46m"
|
||||||
|
#define ANSI_BG_WHITE "\e[47m"
|
||||||
|
#define ANSI_BG_BR_BLACK "\e[100m"
|
||||||
|
#define ANSI_BG_BR_RED "\e[101m"
|
||||||
|
#define ANSI_BG_BR_GREEN "\e[102m"
|
||||||
|
#define ANSI_BG_BR_YELLOW "\e[103m"
|
||||||
|
#define ANSI_BG_BR_BLUE "\e[104m"
|
||||||
|
#define ANSI_BG_BR_MAGENTA "\e[105m"
|
||||||
|
#define ANSI_BG_BR_CYAN "\e[106m"
|
||||||
|
#define ANSI_BG_BR_WHITE "\e[107m"
|
||||||
|
|
||||||
|
#define ANSI_FLIPPER_BRAND_ORANGE "\e[38;2;255;130;0m"
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CliKeyUnrecognized = 0,
|
||||||
|
|
||||||
|
CliKeySOH = 0x01,
|
||||||
|
CliKeyETX = 0x03,
|
||||||
|
CliKeyEOT = 0x04,
|
||||||
|
CliKeyBell = 0x07,
|
||||||
|
CliKeyBackspace = 0x08,
|
||||||
|
CliKeyTab = 0x09,
|
||||||
|
CliKeyLF = 0x0A,
|
||||||
|
CliKeyCR = 0x0D,
|
||||||
|
CliKeyETB = 0x17,
|
||||||
|
CliKeyEsc = 0x1B,
|
||||||
|
CliKeyUS = 0x1F,
|
||||||
|
CliKeySpace = 0x20,
|
||||||
|
CliKeyDEL = 0x7F,
|
||||||
|
|
||||||
|
CliKeySpecial = 0x80,
|
||||||
|
CliKeyLeft,
|
||||||
|
CliKeyRight,
|
||||||
|
CliKeyUp,
|
||||||
|
CliKeyDown,
|
||||||
|
CliKeyHome,
|
||||||
|
CliKeyEnd,
|
||||||
|
} CliKey;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CliModKeyNo = 0,
|
||||||
|
CliModKeyAlt = 2,
|
||||||
|
CliModKeyCtrl = 4,
|
||||||
|
CliModKeyMeta = 8,
|
||||||
|
} CliModKey;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
CliModKey modifiers;
|
||||||
|
CliKey key;
|
||||||
|
} CliKeyCombo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reads a key or key combination
|
||||||
|
*/
|
||||||
|
CliKeyCombo cli_read_ansi_key_combo(Cli* cli);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "cli_commands.h"
|
#include "cli_commands.h"
|
||||||
#include "cli_command_gpio.h"
|
#include "cli_command_gpio.h"
|
||||||
|
#include "cli_ansi.h"
|
||||||
|
|
||||||
#include <core/thread.h>
|
#include <core/thread.h>
|
||||||
#include <furi_hal.h>
|
#include <furi_hal.h>
|
||||||
@@ -10,6 +11,7 @@
|
|||||||
#include <loader/loader.h>
|
#include <loader/loader.h>
|
||||||
#include <lib/toolbox/args.h>
|
#include <lib/toolbox/args.h>
|
||||||
#include <lib/toolbox/strint.h>
|
#include <lib/toolbox/strint.h>
|
||||||
|
#include <storage/storage.h>
|
||||||
|
|
||||||
// Close to ISO, `date +'%Y-%m-%d %H:%M:%S %u'`
|
// Close to ISO, `date +'%Y-%m-%d %H:%M:%S %u'`
|
||||||
#define CLI_DATE_FORMAT "%.4d-%.2d-%.2d %.2d:%.2d:%.2d %d"
|
#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(args);
|
||||||
UNUSED(context);
|
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:");
|
printf("Commands available:");
|
||||||
|
|
||||||
// Command count
|
// Count non-hidden commands
|
||||||
const size_t commands_count = CliCommandTree_size(cli->commands);
|
CliCommandTree_it_t it_count;
|
||||||
const size_t commands_count_mid = commands_count / 2 + commands_count % 2;
|
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
|
// Create iterators starting at different positions
|
||||||
CliCommandTree_it_t it_left;
|
const size_t columns = 3;
|
||||||
CliCommandTree_it(it_left, cli->commands);
|
const size_t commands_per_column = (commands_count / columns) + (commands_count % columns);
|
||||||
CliCommandTree_it_t it_right;
|
CliCommandTree_it_t iterators[columns];
|
||||||
CliCommandTree_it(it_right, cli->commands);
|
for(size_t c = 0; c < columns; c++) {
|
||||||
for(size_t i = 0; i < commands_count_mid; i++)
|
CliCommandTree_it(iterators[c], cli->commands);
|
||||||
CliCommandTree_next(it_right);
|
for(size_t i = 0; i < c * commands_per_column; i++)
|
||||||
|
CliCommandTree_next(iterators[c]);
|
||||||
|
}
|
||||||
|
|
||||||
// Iterate throw tree
|
// Print commands
|
||||||
for(size_t i = 0; i < commands_count_mid; i++) {
|
for(size_t r = 0; r < commands_per_column; r++) {
|
||||||
printf("\r\n");
|
printf("\r\n");
|
||||||
// Left Column
|
|
||||||
if(!CliCommandTree_end_p(it_left)) {
|
for(size_t c = 0; c < columns; c++) {
|
||||||
printf("%-30s", furi_string_get_cstr(*CliCommandTree_ref(it_left)->key_ptr));
|
if(!CliCommandTree_end_p(iterators[c])) {
|
||||||
CliCommandTree_next(it_left);
|
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) {
|
if(furi_string_size(args) > 0) {
|
||||||
cli_nl(cli);
|
cli_nl(cli);
|
||||||
@@ -391,16 +552,18 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) {
|
|||||||
int interval = 1000;
|
int interval = 1000;
|
||||||
args_read_int_and_trim(args, &interval);
|
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();
|
FuriThreadList* thread_list = furi_thread_list_alloc();
|
||||||
while(!cli_cmd_interrupt_received(cli)) {
|
while(!cli_cmd_interrupt_received(cli)) {
|
||||||
uint32_t tick = furi_get_tick();
|
uint32_t tick = furi_get_tick();
|
||||||
furi_thread_enumerate(thread_list);
|
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();
|
uint32_t uptime = tick / furi_kernel_get_tick_frequency();
|
||||||
printf(
|
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),
|
furi_thread_list_size(thread_list),
|
||||||
(double)furi_thread_list_get_isr_time(thread_list),
|
(double)furi_thread_list_get_isr_time(thread_list),
|
||||||
uptime / 60 / 60,
|
uptime / 60 / 60,
|
||||||
@@ -408,14 +571,14 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) {
|
|||||||
uptime % 60);
|
uptime % 60);
|
||||||
|
|
||||||
printf(
|
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_total_heap(),
|
||||||
memmgr_get_free_heap(),
|
memmgr_get_free_heap(),
|
||||||
memmgr_get_minimum_free_heap(),
|
memmgr_get_minimum_free_heap(),
|
||||||
memmgr_heap_get_max_free_block());
|
memmgr_heap_get_max_free_block());
|
||||||
|
|
||||||
printf(
|
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",
|
"AppID",
|
||||||
"Name",
|
"Name",
|
||||||
"State",
|
"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++) {
|
for(size_t i = 0; i < furi_thread_list_size(thread_list); i++) {
|
||||||
const FuriThreadListItem* item = furi_thread_list_get_at(thread_list, i);
|
const FuriThreadListItem* item = furi_thread_list_get_at(thread_list, i);
|
||||||
printf(
|
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->app_id,
|
||||||
item->name,
|
item->name,
|
||||||
item->state,
|
item->state,
|
||||||
@@ -448,6 +611,8 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
furi_thread_list_free(thread_list);
|
furi_thread_list_free(thread_list);
|
||||||
|
|
||||||
|
if(interval) printf("\e[?25h"); // Show cursor
|
||||||
}
|
}
|
||||||
|
|
||||||
void cli_command_free(Cli* cli, FuriString* args, void* context) {
|
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, "!", CliCommandFlagParallelSafe, cli_command_info, (void*)true);
|
||||||
cli_add_command(cli, "info", CliCommandFlagParallelSafe, cli_command_info, NULL);
|
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, "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, "?", CliCommandFlagParallelSafe, cli_command_help, NULL);
|
||||||
cli_add_command(cli, "help", CliCommandFlagParallelSafe, cli_command_help, NULL);
|
cli_add_command(cli, "help", CliCommandFlagParallelSafe, cli_command_help, NULL);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include <lib/toolbox/args.h>
|
#include <lib/toolbox/args.h>
|
||||||
#include <cli/cli.h>
|
#include <cli/cli.h>
|
||||||
|
#include <cli/cli_ansi.h>
|
||||||
|
|
||||||
void crypto_cli_print_usage(void) {
|
void crypto_cli_print_usage(void) {
|
||||||
printf("Usage:\r\n");
|
printf("Usage:\r\n");
|
||||||
@@ -45,14 +46,14 @@ void crypto_cli_encrypt(Cli* cli, FuriString* args) {
|
|||||||
input = furi_string_alloc();
|
input = furi_string_alloc();
|
||||||
char c;
|
char c;
|
||||||
while(cli_read(cli, (uint8_t*)&c, 1) == 1) {
|
while(cli_read(cli, (uint8_t*)&c, 1) == 1) {
|
||||||
if(c == CliSymbolAsciiETX) {
|
if(c == CliKeyETX) {
|
||||||
printf("\r\n");
|
printf("\r\n");
|
||||||
break;
|
break;
|
||||||
} else if(c >= 0x20 && c < 0x7F) {
|
} else if(c >= 0x20 && c < 0x7F) {
|
||||||
putc(c, stdout);
|
putc(c, stdout);
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
furi_string_push_back(input, c);
|
furi_string_push_back(input, c);
|
||||||
} else if(c == CliSymbolAsciiCR) {
|
} else if(c == CliKeyCR) {
|
||||||
printf("\r\n");
|
printf("\r\n");
|
||||||
furi_string_cat(input, "\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();
|
hex_input = furi_string_alloc();
|
||||||
char c;
|
char c;
|
||||||
while(cli_read(cli, (uint8_t*)&c, 1) == 1) {
|
while(cli_read(cli, (uint8_t*)&c, 1) == 1) {
|
||||||
if(c == CliSymbolAsciiETX) {
|
if(c == CliKeyETX) {
|
||||||
printf("\r\n");
|
printf("\r\n");
|
||||||
break;
|
break;
|
||||||
} else if(c >= 0x20 && c < 0x7F) {
|
} else if(c >= 0x20 && c < 0x7F) {
|
||||||
putc(c, stdout);
|
putc(c, stdout);
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
furi_string_push_back(hex_input, c);
|
furi_string_push_back(hex_input, c);
|
||||||
} else if(c == CliSymbolAsciiCR) {
|
} else if(c == CliKeyCR) {
|
||||||
printf("\r\n");
|
printf("\r\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -523,7 +523,7 @@ int32_t desktop_srv(void* p) {
|
|||||||
|
|
||||||
scene_manager_next_scene(desktop->scene_manager, DesktopSceneMain);
|
scene_manager_next_scene(desktop->scene_manager, DesktopSceneMain);
|
||||||
|
|
||||||
if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagLock)) {
|
if(desktop_pin_code_is_set()) {
|
||||||
desktop_lock(desktop);
|
desktop_lock(desktop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -512,9 +512,21 @@ void canvas_draw_xbm(
|
|||||||
size_t height,
|
size_t height,
|
||||||
const uint8_t* bitmap) {
|
const uint8_t* bitmap) {
|
||||||
furi_check(canvas);
|
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;
|
x += canvas->offset_x;
|
||||||
y += canvas->offset_y;
|
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) {
|
void canvas_draw_glyph(Canvas* canvas, int32_t x, int32_t y, uint16_t ch) {
|
||||||
|
|||||||
@@ -287,6 +287,25 @@ void canvas_draw_xbm(
|
|||||||
size_t height,
|
size_t height,
|
||||||
const uint8_t* bitmap);
|
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
|
/** Draw dot at x,y
|
||||||
*
|
*
|
||||||
* @param canvas Canvas instance
|
* @param canvas Canvas instance
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ typedef struct {
|
|||||||
const char* header;
|
const char* header;
|
||||||
char* text_buffer;
|
char* text_buffer;
|
||||||
size_t text_buffer_size;
|
size_t text_buffer_size;
|
||||||
|
size_t minimum_length;
|
||||||
bool clear_default_text;
|
bool clear_default_text;
|
||||||
|
|
||||||
TextInputCallback callback;
|
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->text_buffer, model->validator_text, model->validator_callback_context))) {
|
||||||
model->validator_message_visible = true;
|
model->validator_message_visible = true;
|
||||||
furi_timer_start(text_input->timer, furi_kernel_get_tick_frequency() * 4);
|
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);
|
model->callback(model->callback_context);
|
||||||
}
|
}
|
||||||
} else if(selected == BACKSPACE_KEY) {
|
} else if(selected == BACKSPACE_KEY) {
|
||||||
@@ -487,6 +488,7 @@ void text_input_reset(TextInput* text_input) {
|
|||||||
model->header = "";
|
model->header = "";
|
||||||
model->selected_row = 0;
|
model->selected_row = 0;
|
||||||
model->selected_column = 0;
|
model->selected_column = 0;
|
||||||
|
model->minimum_length = 1;
|
||||||
model->clear_default_text = false;
|
model->clear_default_text = false;
|
||||||
model->text_buffer = NULL;
|
model->text_buffer = NULL;
|
||||||
model->text_buffer_size = 0;
|
model->text_buffer_size = 0;
|
||||||
@@ -531,6 +533,14 @@ void text_input_set_result_callback(
|
|||||||
true);
|
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(
|
void text_input_set_validator(
|
||||||
TextInput* text_input,
|
TextInput* text_input,
|
||||||
TextInputValidatorCallback callback,
|
TextInputValidatorCallback callback,
|
||||||
|
|||||||
@@ -65,6 +65,13 @@ void text_input_set_result_callback(
|
|||||||
size_t text_buffer_size,
|
size_t text_buffer_size,
|
||||||
bool clear_default_text);
|
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(
|
void text_input_set_validator(
|
||||||
TextInput* text_input,
|
TextInput* text_input,
|
||||||
TextInputValidatorCallback callback,
|
TextInputValidatorCallback callback,
|
||||||
|
|||||||
@@ -5,6 +5,12 @@
|
|||||||
#define VIEW_DISPATCHER_QUEUE_LEN (16U)
|
#define VIEW_DISPATCHER_QUEUE_LEN (16U)
|
||||||
|
|
||||||
ViewDispatcher* view_dispatcher_alloc(void) {
|
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));
|
ViewDispatcher* view_dispatcher = malloc(sizeof(ViewDispatcher));
|
||||||
|
|
||||||
view_dispatcher->view_port = view_port_alloc();
|
view_dispatcher->view_port = view_port_alloc();
|
||||||
@@ -16,7 +22,7 @@ ViewDispatcher* view_dispatcher_alloc(void) {
|
|||||||
|
|
||||||
ViewDict_init(view_dispatcher->views);
|
ViewDict_init(view_dispatcher->views);
|
||||||
|
|
||||||
view_dispatcher->event_loop = furi_event_loop_alloc();
|
view_dispatcher->event_loop = loop;
|
||||||
|
|
||||||
view_dispatcher->input_queue =
|
view_dispatcher->input_queue =
|
||||||
furi_message_queue_alloc(VIEW_DISPATCHER_QUEUE_LEN, sizeof(InputEvent));
|
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->input_queue);
|
||||||
furi_message_queue_free(view_dispatcher->event_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 dispatcher
|
||||||
free(view_dispatcher);
|
free(view_dispatcher);
|
||||||
}
|
}
|
||||||
@@ -85,6 +91,7 @@ void view_dispatcher_set_tick_event_callback(
|
|||||||
ViewDispatcherTickEventCallback callback,
|
ViewDispatcherTickEventCallback callback,
|
||||||
uint32_t tick_period) {
|
uint32_t tick_period) {
|
||||||
furi_check(view_dispatcher);
|
furi_check(view_dispatcher);
|
||||||
|
furi_check(view_dispatcher->is_event_loop_owned);
|
||||||
view_dispatcher->tick_event_callback = callback;
|
view_dispatcher->tick_event_callback = callback;
|
||||||
view_dispatcher->tick_period = tick_period;
|
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 :
|
uint32_t tick_period = view_dispatcher->tick_period == 0 ? FuriWaitForever :
|
||||||
view_dispatcher->tick_period;
|
view_dispatcher->tick_period;
|
||||||
|
|
||||||
furi_event_loop_tick_set(
|
if(view_dispatcher->is_event_loop_owned)
|
||||||
view_dispatcher->event_loop,
|
furi_event_loop_tick_set(
|
||||||
tick_period,
|
view_dispatcher->event_loop,
|
||||||
view_dispatcher_handle_tick_event,
|
tick_period,
|
||||||
view_dispatcher);
|
view_dispatcher_handle_tick_event,
|
||||||
|
view_dispatcher);
|
||||||
|
|
||||||
furi_event_loop_run(view_dispatcher->event_loop);
|
furi_event_loop_run(view_dispatcher->event_loop);
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,15 @@ typedef void (*ViewDispatcherTickEventCallback)(void* context);
|
|||||||
*/
|
*/
|
||||||
ViewDispatcher* view_dispatcher_alloc(void);
|
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
|
/** Free ViewDispatcher instance
|
||||||
*
|
*
|
||||||
* @warning All added views MUST be removed using view_dispatcher_remove_view()
|
* @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
|
/** 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 view_dispatcher ViewDispatcher instance
|
||||||
* @param callback ViewDispatcherTickEventCallback
|
* @param callback ViewDispatcherTickEventCallback
|
||||||
* @param tick_period callback call period
|
* @param tick_period callback call period
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
DICT_DEF2(ViewDict, uint32_t, M_DEFAULT_OPLIST, View*, M_PTR_OPLIST) // NOLINT
|
DICT_DEF2(ViewDict, uint32_t, M_DEFAULT_OPLIST, View*, M_PTR_OPLIST) // NOLINT
|
||||||
|
|
||||||
struct ViewDispatcher {
|
struct ViewDispatcher {
|
||||||
|
bool is_event_loop_owned;
|
||||||
FuriEventLoop* event_loop;
|
FuriEventLoop* event_loop;
|
||||||
FuriMessageQueue* input_queue;
|
FuriMessageQueue* input_queue;
|
||||||
FuriMessageQueue* event_queue;
|
FuriMessageQueue* event_queue;
|
||||||
|
|||||||
@@ -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 storage pointer to a storage API instance.
|
||||||
* @param source pointer to a zero-terminated string containing the source path.
|
* @param source pointer to a zero-terminated string containing the source path.
|
||||||
* @param dest pointer to a zero-terminated string containing the destination 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);
|
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 *******************/
|
/******************* 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.
|
* @param error_id numeric identifier of the error in question.
|
||||||
* @return pointer to a statically allocated zero-terminated string containing the respective error text.
|
* @return pointer to a statically allocated zero-terminated string containing the respective error text.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include <furi_hal.h>
|
#include <furi_hal.h>
|
||||||
|
|
||||||
#include <cli/cli.h>
|
#include <cli/cli.h>
|
||||||
|
#include <cli/cli_ansi.h>
|
||||||
#include <lib/toolbox/args.h>
|
#include <lib/toolbox/args.h>
|
||||||
#include <lib/toolbox/dir_walk.h>
|
#include <lib/toolbox/dir_walk.h>
|
||||||
#include <lib/toolbox/md5_calc.h>
|
#include <lib/toolbox/md5_calc.h>
|
||||||
@@ -224,7 +225,7 @@ static void storage_cli_write(Cli* cli, FuriString* path, FuriString* args) {
|
|||||||
while(true) {
|
while(true) {
|
||||||
uint8_t symbol = cli_getc(cli);
|
uint8_t symbol = cli_getc(cli);
|
||||||
|
|
||||||
if(symbol == CliSymbolAsciiETX) {
|
if(symbol == CliKeyETX) {
|
||||||
size_t write_size = read_index % buffer_size;
|
size_t write_size = read_index % buffer_size;
|
||||||
|
|
||||||
if(write_size > 0) {
|
if(write_size > 0) {
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
App(
|
|
||||||
appid="bad_ble",
|
|
||||||
name="Bad BLE",
|
|
||||||
apptype=FlipperAppType.EXTERNAL,
|
|
||||||
entry_point="bad_ble_app",
|
|
||||||
stack_size=2 * 1024,
|
|
||||||
icon="A_BadUsb_14",
|
|
||||||
fap_libs=["assets", "ble_profile"],
|
|
||||||
fap_icon="icon.png",
|
|
||||||
fap_icon_assets="assets",
|
|
||||||
fap_category="Bluetooth",
|
|
||||||
)
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
#include "bad_ble_app_i.h"
|
|
||||||
#include <furi.h>
|
|
||||||
#include <furi_hal.h>
|
|
||||||
#include <storage/storage.h>
|
|
||||||
#include <lib/toolbox/path.h>
|
|
||||||
#include <flipper_format/flipper_format.h>
|
|
||||||
|
|
||||||
#define BAD_BLE_SETTINGS_PATH BAD_BLE_APP_BASE_FOLDER "/.badble.settings"
|
|
||||||
#define BAD_BLE_SETTINGS_FILE_TYPE "Flipper BadBLE Settings File"
|
|
||||||
#define BAD_BLE_SETTINGS_VERSION 1
|
|
||||||
#define BAD_BLE_SETTINGS_DEFAULT_LAYOUT BAD_BLE_APP_PATH_LAYOUT_FOLDER "/en-US.kl"
|
|
||||||
|
|
||||||
static bool bad_ble_app_custom_event_callback(void* context, uint32_t event) {
|
|
||||||
furi_assert(context);
|
|
||||||
BadBleApp* app = context;
|
|
||||||
return scene_manager_handle_custom_event(app->scene_manager, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool bad_ble_app_back_event_callback(void* context) {
|
|
||||||
furi_assert(context);
|
|
||||||
BadBleApp* app = context;
|
|
||||||
return scene_manager_handle_back_event(app->scene_manager);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void bad_ble_app_tick_event_callback(void* context) {
|
|
||||||
furi_assert(context);
|
|
||||||
BadBleApp* app = context;
|
|
||||||
scene_manager_handle_tick_event(app->scene_manager);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void bad_ble_load_settings(BadBleApp* app) {
|
|
||||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
|
||||||
FlipperFormat* fff = flipper_format_file_alloc(storage);
|
|
||||||
bool state = false;
|
|
||||||
|
|
||||||
FuriString* temp_str = furi_string_alloc();
|
|
||||||
uint32_t version = 0;
|
|
||||||
|
|
||||||
if(flipper_format_file_open_existing(fff, BAD_BLE_SETTINGS_PATH)) {
|
|
||||||
do {
|
|
||||||
if(!flipper_format_read_header(fff, temp_str, &version)) break;
|
|
||||||
if((strcmp(furi_string_get_cstr(temp_str), BAD_BLE_SETTINGS_FILE_TYPE) != 0) ||
|
|
||||||
(version != BAD_BLE_SETTINGS_VERSION))
|
|
||||||
break;
|
|
||||||
|
|
||||||
if(!flipper_format_read_string(fff, "layout", temp_str)) break;
|
|
||||||
|
|
||||||
state = true;
|
|
||||||
} while(0);
|
|
||||||
}
|
|
||||||
flipper_format_free(fff);
|
|
||||||
furi_record_close(RECORD_STORAGE);
|
|
||||||
|
|
||||||
if(state) {
|
|
||||||
furi_string_set(app->keyboard_layout, temp_str);
|
|
||||||
|
|
||||||
Storage* fs_api = furi_record_open(RECORD_STORAGE);
|
|
||||||
FileInfo layout_file_info;
|
|
||||||
FS_Error file_check_err = storage_common_stat(
|
|
||||||
fs_api, furi_string_get_cstr(app->keyboard_layout), &layout_file_info);
|
|
||||||
furi_record_close(RECORD_STORAGE);
|
|
||||||
if((file_check_err != FSE_OK) || (layout_file_info.size != 256)) {
|
|
||||||
furi_string_set(app->keyboard_layout, BAD_BLE_SETTINGS_DEFAULT_LAYOUT);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
furi_string_set(app->keyboard_layout, BAD_BLE_SETTINGS_DEFAULT_LAYOUT);
|
|
||||||
}
|
|
||||||
|
|
||||||
furi_string_free(temp_str);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void bad_ble_save_settings(BadBleApp* app) {
|
|
||||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
|
||||||
FlipperFormat* fff = flipper_format_file_alloc(storage);
|
|
||||||
|
|
||||||
if(flipper_format_file_open_always(fff, BAD_BLE_SETTINGS_PATH)) {
|
|
||||||
do {
|
|
||||||
if(!flipper_format_write_header_cstr(
|
|
||||||
fff, BAD_BLE_SETTINGS_FILE_TYPE, BAD_BLE_SETTINGS_VERSION))
|
|
||||||
break;
|
|
||||||
if(!flipper_format_write_string(fff, "layout", app->keyboard_layout)) break;
|
|
||||||
} while(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
flipper_format_free(fff);
|
|
||||||
furi_record_close(RECORD_STORAGE);
|
|
||||||
}
|
|
||||||
|
|
||||||
BadBleApp* bad_ble_app_alloc(char* arg) {
|
|
||||||
BadBleApp* app = malloc(sizeof(BadBleApp));
|
|
||||||
|
|
||||||
app->bad_ble_script = NULL;
|
|
||||||
|
|
||||||
app->file_path = furi_string_alloc();
|
|
||||||
app->keyboard_layout = furi_string_alloc();
|
|
||||||
if(arg && strlen(arg)) {
|
|
||||||
furi_string_set(app->file_path, arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
bad_ble_load_settings(app);
|
|
||||||
|
|
||||||
app->gui = furi_record_open(RECORD_GUI);
|
|
||||||
app->notifications = furi_record_open(RECORD_NOTIFICATION);
|
|
||||||
app->dialogs = furi_record_open(RECORD_DIALOGS);
|
|
||||||
|
|
||||||
app->view_dispatcher = view_dispatcher_alloc();
|
|
||||||
app->scene_manager = scene_manager_alloc(&bad_ble_scene_handlers, app);
|
|
||||||
|
|
||||||
view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
|
|
||||||
view_dispatcher_set_tick_event_callback(
|
|
||||||
app->view_dispatcher, bad_ble_app_tick_event_callback, 500);
|
|
||||||
view_dispatcher_set_custom_event_callback(
|
|
||||||
app->view_dispatcher, bad_ble_app_custom_event_callback);
|
|
||||||
view_dispatcher_set_navigation_event_callback(
|
|
||||||
app->view_dispatcher, bad_ble_app_back_event_callback);
|
|
||||||
|
|
||||||
// Custom Widget
|
|
||||||
app->widget = widget_alloc();
|
|
||||||
view_dispatcher_add_view(
|
|
||||||
app->view_dispatcher, BadBleAppViewWidget, widget_get_view(app->widget));
|
|
||||||
|
|
||||||
// Popup
|
|
||||||
app->popup = popup_alloc();
|
|
||||||
view_dispatcher_add_view(app->view_dispatcher, BadBleAppViewPopup, popup_get_view(app->popup));
|
|
||||||
|
|
||||||
app->var_item_list = variable_item_list_alloc();
|
|
||||||
view_dispatcher_add_view(
|
|
||||||
app->view_dispatcher,
|
|
||||||
BadBleAppViewConfig,
|
|
||||||
variable_item_list_get_view(app->var_item_list));
|
|
||||||
|
|
||||||
app->bad_ble_view = bad_ble_view_alloc();
|
|
||||||
view_dispatcher_add_view(
|
|
||||||
app->view_dispatcher, BadBleAppViewWork, bad_ble_view_get_view(app->bad_ble_view));
|
|
||||||
|
|
||||||
view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
|
|
||||||
|
|
||||||
if(!furi_string_empty(app->file_path)) {
|
|
||||||
scene_manager_next_scene(app->scene_manager, BadBleSceneWork);
|
|
||||||
} else {
|
|
||||||
furi_string_set(app->file_path, BAD_BLE_APP_BASE_FOLDER);
|
|
||||||
scene_manager_next_scene(app->scene_manager, BadBleSceneFileSelect);
|
|
||||||
}
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
void bad_ble_app_free(BadBleApp* app) {
|
|
||||||
furi_assert(app);
|
|
||||||
|
|
||||||
if(app->bad_ble_script) {
|
|
||||||
bad_ble_script_close(app->bad_ble_script);
|
|
||||||
app->bad_ble_script = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Views
|
|
||||||
view_dispatcher_remove_view(app->view_dispatcher, BadBleAppViewWork);
|
|
||||||
bad_ble_view_free(app->bad_ble_view);
|
|
||||||
|
|
||||||
// Custom Widget
|
|
||||||
view_dispatcher_remove_view(app->view_dispatcher, BadBleAppViewWidget);
|
|
||||||
widget_free(app->widget);
|
|
||||||
|
|
||||||
// Popup
|
|
||||||
view_dispatcher_remove_view(app->view_dispatcher, BadBleAppViewPopup);
|
|
||||||
popup_free(app->popup);
|
|
||||||
|
|
||||||
// Config menu
|
|
||||||
view_dispatcher_remove_view(app->view_dispatcher, BadBleAppViewConfig);
|
|
||||||
variable_item_list_free(app->var_item_list);
|
|
||||||
|
|
||||||
// View dispatcher
|
|
||||||
view_dispatcher_free(app->view_dispatcher);
|
|
||||||
scene_manager_free(app->scene_manager);
|
|
||||||
|
|
||||||
// Close records
|
|
||||||
furi_record_close(RECORD_GUI);
|
|
||||||
furi_record_close(RECORD_NOTIFICATION);
|
|
||||||
furi_record_close(RECORD_DIALOGS);
|
|
||||||
|
|
||||||
bad_ble_save_settings(app);
|
|
||||||
|
|
||||||
furi_string_free(app->file_path);
|
|
||||||
furi_string_free(app->keyboard_layout);
|
|
||||||
|
|
||||||
free(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t bad_ble_app(void* p) {
|
|
||||||
BadBleApp* bad_ble_app = bad_ble_app_alloc((char*)p);
|
|
||||||
|
|
||||||
view_dispatcher_run(bad_ble_app->view_dispatcher);
|
|
||||||
|
|
||||||
bad_ble_app_free(bad_ble_app);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
typedef struct BadBleApp BadBleApp;
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "bad_ble_app.h"
|
|
||||||
#include "scenes/bad_ble_scene.h"
|
|
||||||
#include "helpers/ducky_script.h"
|
|
||||||
#include "helpers/bad_ble_hid.h"
|
|
||||||
|
|
||||||
#include <gui/gui.h>
|
|
||||||
#include <assets_icons.h>
|
|
||||||
#include <gui/view_dispatcher.h>
|
|
||||||
#include <gui/scene_manager.h>
|
|
||||||
#include <dialogs/dialogs.h>
|
|
||||||
#include <notification/notification_messages.h>
|
|
||||||
#include <gui/modules/variable_item_list.h>
|
|
||||||
#include <gui/modules/widget.h>
|
|
||||||
#include <gui/modules/popup.h>
|
|
||||||
#include "views/bad_ble_view.h"
|
|
||||||
|
|
||||||
#define BAD_BLE_APP_BASE_FOLDER EXT_PATH("badusb")
|
|
||||||
#define BAD_BLE_APP_PATH_LAYOUT_FOLDER BAD_BLE_APP_BASE_FOLDER "/assets/layouts"
|
|
||||||
#define BAD_BLE_APP_SCRIPT_EXTENSION ".txt"
|
|
||||||
#define BAD_BLE_APP_LAYOUT_EXTENSION ".kl"
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
BadBleAppErrorNoFiles,
|
|
||||||
BadBleAppErrorCloseRpc,
|
|
||||||
} BadBleAppError;
|
|
||||||
|
|
||||||
struct BadBleApp {
|
|
||||||
Gui* gui;
|
|
||||||
ViewDispatcher* view_dispatcher;
|
|
||||||
SceneManager* scene_manager;
|
|
||||||
NotificationApp* notifications;
|
|
||||||
DialogsApp* dialogs;
|
|
||||||
Widget* widget;
|
|
||||||
Popup* popup;
|
|
||||||
VariableItemList* var_item_list;
|
|
||||||
|
|
||||||
BadBleAppError error;
|
|
||||||
FuriString* file_path;
|
|
||||||
FuriString* keyboard_layout;
|
|
||||||
BadBle* bad_ble_view;
|
|
||||||
BadBleScript* bad_ble_script;
|
|
||||||
|
|
||||||
BadBleHidInterface interface;
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
BadBleAppViewWidget,
|
|
||||||
BadBleAppViewPopup,
|
|
||||||
BadBleAppViewWork,
|
|
||||||
BadBleAppViewConfig,
|
|
||||||
} BadBleAppView;
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
#include "bad_ble_hid.h"
|
|
||||||
#include <extra_profiles/hid_profile.h>
|
|
||||||
#include <bt/bt_service/bt.h>
|
|
||||||
#include <storage/storage.h>
|
|
||||||
|
|
||||||
#define TAG "BadBLE HID"
|
|
||||||
|
|
||||||
#define HID_BT_KEYS_STORAGE_NAME ".bt_hid.keys"
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
Bt* bt;
|
|
||||||
FuriHalBleProfileBase* profile;
|
|
||||||
HidStateCallback state_callback;
|
|
||||||
void* callback_context;
|
|
||||||
bool is_connected;
|
|
||||||
} BleHidInstance;
|
|
||||||
|
|
||||||
static const BleProfileHidParams ble_hid_params = {
|
|
||||||
.device_name_prefix = "BadBLE",
|
|
||||||
.mac_xor = 0x0002,
|
|
||||||
};
|
|
||||||
|
|
||||||
static void hid_ble_connection_status_callback(BtStatus status, void* context) {
|
|
||||||
furi_assert(context);
|
|
||||||
BleHidInstance* ble_hid = context;
|
|
||||||
ble_hid->is_connected = (status == BtStatusConnected);
|
|
||||||
if(ble_hid->state_callback) {
|
|
||||||
ble_hid->state_callback(ble_hid->is_connected, ble_hid->callback_context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void* hid_ble_init(FuriHalUsbHidConfig* hid_cfg) {
|
|
||||||
UNUSED(hid_cfg);
|
|
||||||
BleHidInstance* ble_hid = malloc(sizeof(BleHidInstance));
|
|
||||||
ble_hid->bt = furi_record_open(RECORD_BT);
|
|
||||||
bt_disconnect(ble_hid->bt);
|
|
||||||
|
|
||||||
// Wait 2nd core to update nvm storage
|
|
||||||
furi_delay_ms(200);
|
|
||||||
|
|
||||||
bt_keys_storage_set_storage_path(ble_hid->bt, APP_DATA_PATH(HID_BT_KEYS_STORAGE_NAME));
|
|
||||||
|
|
||||||
ble_hid->profile = bt_profile_start(ble_hid->bt, ble_profile_hid, (void*)&ble_hid_params);
|
|
||||||
furi_check(ble_hid->profile);
|
|
||||||
|
|
||||||
furi_hal_bt_start_advertising();
|
|
||||||
|
|
||||||
bt_set_status_changed_callback(ble_hid->bt, hid_ble_connection_status_callback, ble_hid);
|
|
||||||
|
|
||||||
return ble_hid;
|
|
||||||
}
|
|
||||||
|
|
||||||
void hid_ble_deinit(void* inst) {
|
|
||||||
BleHidInstance* ble_hid = inst;
|
|
||||||
furi_assert(ble_hid);
|
|
||||||
|
|
||||||
bt_set_status_changed_callback(ble_hid->bt, NULL, NULL);
|
|
||||||
bt_disconnect(ble_hid->bt);
|
|
||||||
|
|
||||||
// Wait 2nd core to update nvm storage
|
|
||||||
furi_delay_ms(200);
|
|
||||||
bt_keys_storage_set_default_path(ble_hid->bt);
|
|
||||||
|
|
||||||
furi_check(bt_profile_restore_default(ble_hid->bt));
|
|
||||||
furi_record_close(RECORD_BT);
|
|
||||||
free(ble_hid);
|
|
||||||
}
|
|
||||||
|
|
||||||
void hid_ble_set_state_callback(void* inst, HidStateCallback cb, void* context) {
|
|
||||||
BleHidInstance* ble_hid = inst;
|
|
||||||
furi_assert(ble_hid);
|
|
||||||
ble_hid->state_callback = cb;
|
|
||||||
ble_hid->callback_context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool hid_ble_is_connected(void* inst) {
|
|
||||||
BleHidInstance* ble_hid = inst;
|
|
||||||
furi_assert(ble_hid);
|
|
||||||
return ble_hid->is_connected;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool hid_ble_kb_press(void* inst, uint16_t button) {
|
|
||||||
BleHidInstance* ble_hid = inst;
|
|
||||||
furi_assert(ble_hid);
|
|
||||||
return ble_profile_hid_kb_press(ble_hid->profile, button);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool hid_ble_kb_release(void* inst, uint16_t button) {
|
|
||||||
BleHidInstance* ble_hid = inst;
|
|
||||||
furi_assert(ble_hid);
|
|
||||||
return ble_profile_hid_kb_release(ble_hid->profile, button);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool hid_ble_consumer_press(void* inst, uint16_t button) {
|
|
||||||
BleHidInstance* ble_hid = inst;
|
|
||||||
furi_assert(ble_hid);
|
|
||||||
return ble_profile_hid_consumer_key_press(ble_hid->profile, button);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool hid_ble_consumer_release(void* inst, uint16_t button) {
|
|
||||||
BleHidInstance* ble_hid = inst;
|
|
||||||
furi_assert(ble_hid);
|
|
||||||
return ble_profile_hid_consumer_key_release(ble_hid->profile, button);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool hid_ble_release_all(void* inst) {
|
|
||||||
BleHidInstance* ble_hid = inst;
|
|
||||||
furi_assert(ble_hid);
|
|
||||||
bool state = ble_profile_hid_kb_release_all(ble_hid->profile);
|
|
||||||
state &= ble_profile_hid_consumer_key_release_all(ble_hid->profile);
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t hid_ble_get_led_state(void* inst) {
|
|
||||||
UNUSED(inst);
|
|
||||||
FURI_LOG_W(TAG, "hid_ble_get_led_state not implemented");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const BadBleHidApi hid_api_ble = {
|
|
||||||
.init = hid_ble_init,
|
|
||||||
.deinit = hid_ble_deinit,
|
|
||||||
.set_state_callback = hid_ble_set_state_callback,
|
|
||||||
.is_connected = hid_ble_is_connected,
|
|
||||||
|
|
||||||
.kb_press = hid_ble_kb_press,
|
|
||||||
.kb_release = hid_ble_kb_release,
|
|
||||||
.consumer_press = hid_ble_consumer_press,
|
|
||||||
.consumer_release = hid_ble_consumer_release,
|
|
||||||
.release_all = hid_ble_release_all,
|
|
||||||
.get_led_state = hid_ble_get_led_state,
|
|
||||||
};
|
|
||||||
|
|
||||||
const BadBleHidApi* bad_ble_hid_get_interface(BadBleHidInterface interface) {
|
|
||||||
UNUSED(interface);
|
|
||||||
return &hid_api_ble;
|
|
||||||
}
|
|
||||||
|
|
||||||
void bad_ble_hid_ble_remove_pairing(void) {
|
|
||||||
Bt* bt = furi_record_open(RECORD_BT);
|
|
||||||
bt_disconnect(bt);
|
|
||||||
|
|
||||||
// Wait 2nd core to update nvm storage
|
|
||||||
furi_delay_ms(200);
|
|
||||||
|
|
||||||
furi_hal_bt_stop_advertising();
|
|
||||||
|
|
||||||
bt_keys_storage_set_storage_path(bt, APP_DATA_PATH(HID_BT_KEYS_STORAGE_NAME));
|
|
||||||
bt_forget_bonded_devices(bt);
|
|
||||||
|
|
||||||
// Wait 2nd core to update nvm storage
|
|
||||||
furi_delay_ms(200);
|
|
||||||
bt_keys_storage_set_default_path(bt);
|
|
||||||
|
|
||||||
furi_check(bt_profile_restore_default(bt));
|
|
||||||
furi_record_close(RECORD_BT);
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <furi.h>
|
|
||||||
#include <furi_hal.h>
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
BadBleHidInterfaceBle,
|
|
||||||
} BadBleHidInterface;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
void* (*init)(FuriHalUsbHidConfig* hid_cfg);
|
|
||||||
void (*deinit)(void* inst);
|
|
||||||
void (*set_state_callback)(void* inst, HidStateCallback cb, void* context);
|
|
||||||
bool (*is_connected)(void* inst);
|
|
||||||
|
|
||||||
bool (*kb_press)(void* inst, uint16_t button);
|
|
||||||
bool (*kb_release)(void* inst, uint16_t button);
|
|
||||||
bool (*consumer_press)(void* inst, uint16_t button);
|
|
||||||
bool (*consumer_release)(void* inst, uint16_t button);
|
|
||||||
bool (*release_all)(void* inst);
|
|
||||||
uint8_t (*get_led_state)(void* inst);
|
|
||||||
} BadBleHidApi;
|
|
||||||
|
|
||||||
const BadBleHidApi* bad_ble_hid_get_interface(BadBleHidInterface interface);
|
|
||||||
|
|
||||||
void bad_ble_hid_ble_remove_pairing(void);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@@ -1,716 +0,0 @@
|
|||||||
#include <furi.h>
|
|
||||||
#include <furi_hal.h>
|
|
||||||
#include <gui/gui.h>
|
|
||||||
#include <input/input.h>
|
|
||||||
#include <lib/toolbox/args.h>
|
|
||||||
#include <lib/toolbox/strint.h>
|
|
||||||
#include <storage/storage.h>
|
|
||||||
#include "ducky_script.h"
|
|
||||||
#include "ducky_script_i.h"
|
|
||||||
#include <dolphin/dolphin.h>
|
|
||||||
|
|
||||||
#define TAG "BadBle"
|
|
||||||
|
|
||||||
#define WORKER_TAG TAG "Worker"
|
|
||||||
|
|
||||||
#define BADUSB_ASCII_TO_KEY(script, x) \
|
|
||||||
(((uint8_t)x < 128) ? (script->layout[(uint8_t)x]) : HID_KEYBOARD_NONE)
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
WorkerEvtStartStop = (1 << 0),
|
|
||||||
WorkerEvtPauseResume = (1 << 1),
|
|
||||||
WorkerEvtEnd = (1 << 2),
|
|
||||||
WorkerEvtConnect = (1 << 3),
|
|
||||||
WorkerEvtDisconnect = (1 << 4),
|
|
||||||
} WorkerEvtFlags;
|
|
||||||
|
|
||||||
static const char ducky_cmd_id[] = {"ID"};
|
|
||||||
|
|
||||||
static const uint8_t numpad_keys[10] = {
|
|
||||||
HID_KEYPAD_0,
|
|
||||||
HID_KEYPAD_1,
|
|
||||||
HID_KEYPAD_2,
|
|
||||||
HID_KEYPAD_3,
|
|
||||||
HID_KEYPAD_4,
|
|
||||||
HID_KEYPAD_5,
|
|
||||||
HID_KEYPAD_6,
|
|
||||||
HID_KEYPAD_7,
|
|
||||||
HID_KEYPAD_8,
|
|
||||||
HID_KEYPAD_9,
|
|
||||||
};
|
|
||||||
|
|
||||||
uint32_t ducky_get_command_len(const char* line) {
|
|
||||||
uint32_t len = strlen(line);
|
|
||||||
for(uint32_t i = 0; i < len; i++) {
|
|
||||||
if(line[i] == ' ') return i;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ducky_is_line_end(const char chr) {
|
|
||||||
return (chr == ' ') || (chr == '\0') || (chr == '\r') || (chr == '\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t ducky_get_keycode(BadBleScript* bad_ble, const char* param, bool accept_chars) {
|
|
||||||
uint16_t keycode = ducky_get_keycode_by_name(param);
|
|
||||||
if(keycode != HID_KEYBOARD_NONE) {
|
|
||||||
return keycode;
|
|
||||||
}
|
|
||||||
|
|
||||||
if((accept_chars) && (strlen(param) > 0)) {
|
|
||||||
return BADUSB_ASCII_TO_KEY(bad_ble, param[0]) & 0xFF;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ducky_get_number(const char* param, uint32_t* val) {
|
|
||||||
uint32_t value = 0;
|
|
||||||
if(strint_to_uint32(param, NULL, &value, 10) == StrintParseNoError) {
|
|
||||||
*val = value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ducky_numlock_on(BadBleScript* bad_ble) {
|
|
||||||
if((bad_ble->hid->get_led_state(bad_ble->hid_inst) & HID_KB_LED_NUM) == 0) {
|
|
||||||
bad_ble->hid->kb_press(bad_ble->hid_inst, HID_KEYBOARD_LOCK_NUM_LOCK);
|
|
||||||
bad_ble->hid->kb_release(bad_ble->hid_inst, HID_KEYBOARD_LOCK_NUM_LOCK);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ducky_numpad_press(BadBleScript* bad_ble, const char num) {
|
|
||||||
if((num < '0') || (num > '9')) return false;
|
|
||||||
|
|
||||||
uint16_t key = numpad_keys[num - '0'];
|
|
||||||
bad_ble->hid->kb_press(bad_ble->hid_inst, key);
|
|
||||||
bad_ble->hid->kb_release(bad_ble->hid_inst, key);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ducky_altchar(BadBleScript* bad_ble, const char* charcode) {
|
|
||||||
uint8_t i = 0;
|
|
||||||
bool state = false;
|
|
||||||
|
|
||||||
bad_ble->hid->kb_press(bad_ble->hid_inst, KEY_MOD_LEFT_ALT);
|
|
||||||
|
|
||||||
while(!ducky_is_line_end(charcode[i])) {
|
|
||||||
state = ducky_numpad_press(bad_ble, charcode[i]);
|
|
||||||
if(state == false) break;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
bad_ble->hid->kb_release(bad_ble->hid_inst, KEY_MOD_LEFT_ALT);
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ducky_altstring(BadBleScript* bad_ble, const char* param) {
|
|
||||||
uint32_t i = 0;
|
|
||||||
bool state = false;
|
|
||||||
|
|
||||||
while(param[i] != '\0') {
|
|
||||||
if((param[i] < ' ') || (param[i] > '~')) {
|
|
||||||
i++;
|
|
||||||
continue; // Skip non-printable chars
|
|
||||||
}
|
|
||||||
|
|
||||||
char temp_str[4];
|
|
||||||
snprintf(temp_str, 4, "%u", param[i]);
|
|
||||||
|
|
||||||
state = ducky_altchar(bad_ble, temp_str);
|
|
||||||
if(state == false) break;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t ducky_error(BadBleScript* bad_ble, const char* text, ...) {
|
|
||||||
va_list args;
|
|
||||||
va_start(args, text);
|
|
||||||
|
|
||||||
vsnprintf(bad_ble->st.error, sizeof(bad_ble->st.error), text, args);
|
|
||||||
|
|
||||||
va_end(args);
|
|
||||||
return SCRIPT_STATE_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ducky_string(BadBleScript* bad_ble, const char* param) {
|
|
||||||
uint32_t i = 0;
|
|
||||||
|
|
||||||
while(param[i] != '\0') {
|
|
||||||
if(param[i] != '\n') {
|
|
||||||
uint16_t keycode = BADUSB_ASCII_TO_KEY(bad_ble, param[i]);
|
|
||||||
if(keycode != HID_KEYBOARD_NONE) {
|
|
||||||
bad_ble->hid->kb_press(bad_ble->hid_inst, keycode);
|
|
||||||
bad_ble->hid->kb_release(bad_ble->hid_inst, keycode);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bad_ble->hid->kb_press(bad_ble->hid_inst, HID_KEYBOARD_RETURN);
|
|
||||||
bad_ble->hid->kb_release(bad_ble->hid_inst, HID_KEYBOARD_RETURN);
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
bad_ble->stringdelay = 0;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool ducky_string_next(BadBleScript* bad_ble) {
|
|
||||||
if(bad_ble->string_print_pos >= furi_string_size(bad_ble->string_print)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
char print_char = furi_string_get_char(bad_ble->string_print, bad_ble->string_print_pos);
|
|
||||||
|
|
||||||
if(print_char != '\n') {
|
|
||||||
uint16_t keycode = BADUSB_ASCII_TO_KEY(bad_ble, print_char);
|
|
||||||
if(keycode != HID_KEYBOARD_NONE) {
|
|
||||||
bad_ble->hid->kb_press(bad_ble->hid_inst, keycode);
|
|
||||||
bad_ble->hid->kb_release(bad_ble->hid_inst, keycode);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bad_ble->hid->kb_press(bad_ble->hid_inst, HID_KEYBOARD_RETURN);
|
|
||||||
bad_ble->hid->kb_release(bad_ble->hid_inst, HID_KEYBOARD_RETURN);
|
|
||||||
}
|
|
||||||
|
|
||||||
bad_ble->string_print_pos++;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int32_t ducky_parse_line(BadBleScript* bad_ble, FuriString* line) {
|
|
||||||
uint32_t line_len = furi_string_size(line);
|
|
||||||
const char* line_tmp = furi_string_get_cstr(line);
|
|
||||||
|
|
||||||
if(line_len == 0) {
|
|
||||||
return SCRIPT_STATE_NEXT_LINE; // Skip empty lines
|
|
||||||
}
|
|
||||||
FURI_LOG_D(WORKER_TAG, "line:%s", line_tmp);
|
|
||||||
|
|
||||||
// Ducky Lang Functions
|
|
||||||
int32_t cmd_result = ducky_execute_cmd(bad_ble, line_tmp);
|
|
||||||
if(cmd_result != SCRIPT_STATE_CMD_UNKNOWN) {
|
|
||||||
return cmd_result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special keys + modifiers
|
|
||||||
uint16_t key = ducky_get_keycode(bad_ble, line_tmp, false);
|
|
||||||
if(key == HID_KEYBOARD_NONE) {
|
|
||||||
return ducky_error(bad_ble, "No keycode defined for %s", line_tmp);
|
|
||||||
}
|
|
||||||
if((key & 0xFF00) != 0) {
|
|
||||||
// It's a modifier key
|
|
||||||
line_tmp = &line_tmp[ducky_get_command_len(line_tmp) + 1];
|
|
||||||
key |= ducky_get_keycode(bad_ble, line_tmp, true);
|
|
||||||
}
|
|
||||||
bad_ble->hid->kb_press(bad_ble->hid_inst, key);
|
|
||||||
bad_ble->hid->kb_release(bad_ble->hid_inst, key);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool ducky_set_usb_id(BadBleScript* bad_ble, const char* line) {
|
|
||||||
if(sscanf(line, "%lX:%lX", &bad_ble->hid_cfg.vid, &bad_ble->hid_cfg.pid) == 2) {
|
|
||||||
bad_ble->hid_cfg.manuf[0] = '\0';
|
|
||||||
bad_ble->hid_cfg.product[0] = '\0';
|
|
||||||
|
|
||||||
uint8_t id_len = ducky_get_command_len(line);
|
|
||||||
if(!ducky_is_line_end(line[id_len + 1])) {
|
|
||||||
sscanf(
|
|
||||||
&line[id_len + 1],
|
|
||||||
"%31[^\r\n:]:%31[^\r\n]",
|
|
||||||
bad_ble->hid_cfg.manuf,
|
|
||||||
bad_ble->hid_cfg.product);
|
|
||||||
}
|
|
||||||
FURI_LOG_D(
|
|
||||||
WORKER_TAG,
|
|
||||||
"set id: %04lX:%04lX mfr:%s product:%s",
|
|
||||||
bad_ble->hid_cfg.vid,
|
|
||||||
bad_ble->hid_cfg.pid,
|
|
||||||
bad_ble->hid_cfg.manuf,
|
|
||||||
bad_ble->hid_cfg.product);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void bad_ble_hid_state_callback(bool state, void* context) {
|
|
||||||
furi_assert(context);
|
|
||||||
BadBleScript* bad_ble = context;
|
|
||||||
|
|
||||||
if(state == true) {
|
|
||||||
furi_thread_flags_set(furi_thread_get_id(bad_ble->thread), WorkerEvtConnect);
|
|
||||||
} else {
|
|
||||||
furi_thread_flags_set(furi_thread_get_id(bad_ble->thread), WorkerEvtDisconnect);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool ducky_script_preload(BadBleScript* bad_ble, File* script_file) {
|
|
||||||
uint8_t ret = 0;
|
|
||||||
uint32_t line_len = 0;
|
|
||||||
|
|
||||||
furi_string_reset(bad_ble->line);
|
|
||||||
|
|
||||||
do {
|
|
||||||
ret = storage_file_read(script_file, bad_ble->file_buf, FILE_BUFFER_LEN);
|
|
||||||
for(uint16_t i = 0; i < ret; i++) {
|
|
||||||
if(bad_ble->file_buf[i] == '\n' && line_len > 0) {
|
|
||||||
bad_ble->st.line_nb++;
|
|
||||||
line_len = 0;
|
|
||||||
} else {
|
|
||||||
if(bad_ble->st.line_nb == 0) { // Save first line
|
|
||||||
furi_string_push_back(bad_ble->line, bad_ble->file_buf[i]);
|
|
||||||
}
|
|
||||||
line_len++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(storage_file_eof(script_file)) {
|
|
||||||
if(line_len > 0) {
|
|
||||||
bad_ble->st.line_nb++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while(ret > 0);
|
|
||||||
|
|
||||||
const char* line_tmp = furi_string_get_cstr(bad_ble->line);
|
|
||||||
bool id_set = false; // Looking for ID command at first line
|
|
||||||
if(strncmp(line_tmp, ducky_cmd_id, strlen(ducky_cmd_id)) == 0) {
|
|
||||||
id_set = ducky_set_usb_id(bad_ble, &line_tmp[strlen(ducky_cmd_id) + 1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(id_set) {
|
|
||||||
bad_ble->hid_inst = bad_ble->hid->init(&bad_ble->hid_cfg);
|
|
||||||
} else {
|
|
||||||
bad_ble->hid_inst = bad_ble->hid->init(NULL);
|
|
||||||
}
|
|
||||||
bad_ble->hid->set_state_callback(bad_ble->hid_inst, bad_ble_hid_state_callback, bad_ble);
|
|
||||||
|
|
||||||
storage_file_seek(script_file, 0, true);
|
|
||||||
furi_string_reset(bad_ble->line);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int32_t ducky_script_execute_next(BadBleScript* bad_ble, File* script_file) {
|
|
||||||
int32_t delay_val = 0;
|
|
||||||
|
|
||||||
if(bad_ble->repeat_cnt > 0) {
|
|
||||||
bad_ble->repeat_cnt--;
|
|
||||||
delay_val = ducky_parse_line(bad_ble, bad_ble->line_prev);
|
|
||||||
if(delay_val == SCRIPT_STATE_NEXT_LINE) { // Empty line
|
|
||||||
return 0;
|
|
||||||
} else if(delay_val == SCRIPT_STATE_STRING_START) { // Print string with delays
|
|
||||||
return delay_val;
|
|
||||||
} else if(delay_val == SCRIPT_STATE_WAIT_FOR_BTN) { // wait for button
|
|
||||||
return delay_val;
|
|
||||||
} else if(delay_val < 0) { // Script error
|
|
||||||
bad_ble->st.error_line = bad_ble->st.line_cur - 1;
|
|
||||||
FURI_LOG_E(WORKER_TAG, "Unknown command at line %zu", bad_ble->st.line_cur - 1U);
|
|
||||||
return SCRIPT_STATE_ERROR;
|
|
||||||
} else {
|
|
||||||
return delay_val + bad_ble->defdelay;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
furi_string_set(bad_ble->line_prev, bad_ble->line);
|
|
||||||
furi_string_reset(bad_ble->line);
|
|
||||||
|
|
||||||
while(1) {
|
|
||||||
if(bad_ble->buf_len == 0) {
|
|
||||||
bad_ble->buf_len = storage_file_read(script_file, bad_ble->file_buf, FILE_BUFFER_LEN);
|
|
||||||
if(storage_file_eof(script_file)) {
|
|
||||||
if((bad_ble->buf_len < FILE_BUFFER_LEN) && (bad_ble->file_end == false)) {
|
|
||||||
bad_ble->file_buf[bad_ble->buf_len] = '\n';
|
|
||||||
bad_ble->buf_len++;
|
|
||||||
bad_ble->file_end = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bad_ble->buf_start = 0;
|
|
||||||
if(bad_ble->buf_len == 0) return SCRIPT_STATE_END;
|
|
||||||
}
|
|
||||||
for(uint8_t i = bad_ble->buf_start; i < (bad_ble->buf_start + bad_ble->buf_len); i++) {
|
|
||||||
if(bad_ble->file_buf[i] == '\n' && furi_string_size(bad_ble->line) > 0) {
|
|
||||||
bad_ble->st.line_cur++;
|
|
||||||
bad_ble->buf_len = bad_ble->buf_len + bad_ble->buf_start - (i + 1);
|
|
||||||
bad_ble->buf_start = i + 1;
|
|
||||||
furi_string_trim(bad_ble->line);
|
|
||||||
delay_val = ducky_parse_line(bad_ble, bad_ble->line);
|
|
||||||
if(delay_val == SCRIPT_STATE_NEXT_LINE) { // Empty line
|
|
||||||
return 0;
|
|
||||||
} else if(delay_val == SCRIPT_STATE_STRING_START) { // Print string with delays
|
|
||||||
return delay_val;
|
|
||||||
} else if(delay_val == SCRIPT_STATE_WAIT_FOR_BTN) { // wait for button
|
|
||||||
return delay_val;
|
|
||||||
} else if(delay_val < 0) {
|
|
||||||
bad_ble->st.error_line = bad_ble->st.line_cur;
|
|
||||||
FURI_LOG_E(WORKER_TAG, "Unknown command at line %zu", bad_ble->st.line_cur);
|
|
||||||
return SCRIPT_STATE_ERROR;
|
|
||||||
} else {
|
|
||||||
return delay_val + bad_ble->defdelay;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
furi_string_push_back(bad_ble->line, bad_ble->file_buf[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bad_ble->buf_len = 0;
|
|
||||||
if(bad_ble->file_end) return SCRIPT_STATE_END;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static uint32_t bad_ble_flags_get(uint32_t flags_mask, uint32_t timeout) {
|
|
||||||
uint32_t flags = furi_thread_flags_get();
|
|
||||||
furi_check((flags & FuriFlagError) == 0);
|
|
||||||
if(flags == 0) {
|
|
||||||
flags = furi_thread_flags_wait(flags_mask, FuriFlagWaitAny, timeout);
|
|
||||||
furi_check(((flags & FuriFlagError) == 0) || (flags == (unsigned)FuriFlagErrorTimeout));
|
|
||||||
} else {
|
|
||||||
uint32_t state = furi_thread_flags_clear(flags);
|
|
||||||
furi_check((state & FuriFlagError) == 0);
|
|
||||||
}
|
|
||||||
return flags;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int32_t bad_ble_worker(void* context) {
|
|
||||||
BadBleScript* bad_ble = context;
|
|
||||||
|
|
||||||
BadBleWorkerState worker_state = BadBleStateInit;
|
|
||||||
BadBleWorkerState pause_state = BadBleStateRunning;
|
|
||||||
int32_t delay_val = 0;
|
|
||||||
|
|
||||||
FURI_LOG_I(WORKER_TAG, "Init");
|
|
||||||
File* script_file = storage_file_alloc(furi_record_open(RECORD_STORAGE));
|
|
||||||
bad_ble->line = furi_string_alloc();
|
|
||||||
bad_ble->line_prev = furi_string_alloc();
|
|
||||||
bad_ble->string_print = furi_string_alloc();
|
|
||||||
|
|
||||||
while(1) {
|
|
||||||
if(worker_state == BadBleStateInit) { // State: initialization
|
|
||||||
if(storage_file_open(
|
|
||||||
script_file,
|
|
||||||
furi_string_get_cstr(bad_ble->file_path),
|
|
||||||
FSAM_READ,
|
|
||||||
FSOM_OPEN_EXISTING)) {
|
|
||||||
if((ducky_script_preload(bad_ble, script_file)) && (bad_ble->st.line_nb > 0)) {
|
|
||||||
if(bad_ble->hid->is_connected(bad_ble->hid_inst)) {
|
|
||||||
worker_state = BadBleStateIdle; // Ready to run
|
|
||||||
} else {
|
|
||||||
worker_state = BadBleStateNotConnected; // USB not connected
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
worker_state = BadBleStateScriptError; // Script preload error
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
FURI_LOG_E(WORKER_TAG, "File open error");
|
|
||||||
worker_state = BadBleStateFileError; // File open error
|
|
||||||
}
|
|
||||||
bad_ble->st.state = worker_state;
|
|
||||||
|
|
||||||
} else if(worker_state == BadBleStateNotConnected) { // State: USB not connected
|
|
||||||
uint32_t flags = bad_ble_flags_get(
|
|
||||||
WorkerEvtEnd | WorkerEvtConnect | WorkerEvtDisconnect | WorkerEvtStartStop,
|
|
||||||
FuriWaitForever);
|
|
||||||
|
|
||||||
if(flags & WorkerEvtEnd) {
|
|
||||||
break;
|
|
||||||
} else if(flags & WorkerEvtConnect) {
|
|
||||||
worker_state = BadBleStateIdle; // Ready to run
|
|
||||||
} else if(flags & WorkerEvtStartStop) {
|
|
||||||
worker_state = BadBleStateWillRun; // Will run when USB is connected
|
|
||||||
}
|
|
||||||
bad_ble->st.state = worker_state;
|
|
||||||
|
|
||||||
} else if(worker_state == BadBleStateIdle) { // State: ready to start
|
|
||||||
uint32_t flags = bad_ble_flags_get(
|
|
||||||
WorkerEvtEnd | WorkerEvtStartStop | WorkerEvtDisconnect, FuriWaitForever);
|
|
||||||
|
|
||||||
if(flags & WorkerEvtEnd) {
|
|
||||||
break;
|
|
||||||
} else if(flags & WorkerEvtStartStop) { // Start executing script
|
|
||||||
dolphin_deed(DolphinDeedBadUsbPlayScript);
|
|
||||||
delay_val = 0;
|
|
||||||
bad_ble->buf_len = 0;
|
|
||||||
bad_ble->st.line_cur = 0;
|
|
||||||
bad_ble->defdelay = 0;
|
|
||||||
bad_ble->stringdelay = 0;
|
|
||||||
bad_ble->defstringdelay = 0;
|
|
||||||
bad_ble->repeat_cnt = 0;
|
|
||||||
bad_ble->key_hold_nb = 0;
|
|
||||||
bad_ble->file_end = false;
|
|
||||||
storage_file_seek(script_file, 0, true);
|
|
||||||
worker_state = BadBleStateRunning;
|
|
||||||
} else if(flags & WorkerEvtDisconnect) {
|
|
||||||
worker_state = BadBleStateNotConnected; // USB disconnected
|
|
||||||
}
|
|
||||||
bad_ble->st.state = worker_state;
|
|
||||||
|
|
||||||
} else if(worker_state == BadBleStateWillRun) { // State: start on connection
|
|
||||||
uint32_t flags = bad_ble_flags_get(
|
|
||||||
WorkerEvtEnd | WorkerEvtConnect | WorkerEvtStartStop, FuriWaitForever);
|
|
||||||
|
|
||||||
if(flags & WorkerEvtEnd) {
|
|
||||||
break;
|
|
||||||
} else if(flags & WorkerEvtConnect) { // Start executing script
|
|
||||||
dolphin_deed(DolphinDeedBadUsbPlayScript);
|
|
||||||
delay_val = 0;
|
|
||||||
bad_ble->buf_len = 0;
|
|
||||||
bad_ble->st.line_cur = 0;
|
|
||||||
bad_ble->defdelay = 0;
|
|
||||||
bad_ble->stringdelay = 0;
|
|
||||||
bad_ble->defstringdelay = 0;
|
|
||||||
bad_ble->repeat_cnt = 0;
|
|
||||||
bad_ble->file_end = false;
|
|
||||||
storage_file_seek(script_file, 0, true);
|
|
||||||
// extra time for PC to recognize Flipper as keyboard
|
|
||||||
flags = furi_thread_flags_wait(
|
|
||||||
WorkerEvtEnd | WorkerEvtDisconnect | WorkerEvtStartStop,
|
|
||||||
FuriFlagWaitAny | FuriFlagNoClear,
|
|
||||||
1500);
|
|
||||||
if(flags == (unsigned)FuriFlagErrorTimeout) {
|
|
||||||
// If nothing happened - start script execution
|
|
||||||
worker_state = BadBleStateRunning;
|
|
||||||
} else if(flags & WorkerEvtStartStop) {
|
|
||||||
worker_state = BadBleStateIdle;
|
|
||||||
furi_thread_flags_clear(WorkerEvtStartStop);
|
|
||||||
}
|
|
||||||
} else if(flags & WorkerEvtStartStop) { // Cancel scheduled execution
|
|
||||||
worker_state = BadBleStateNotConnected;
|
|
||||||
}
|
|
||||||
bad_ble->st.state = worker_state;
|
|
||||||
|
|
||||||
} else if(worker_state == BadBleStateRunning) { // State: running
|
|
||||||
uint16_t delay_cur = (delay_val > 1000) ? (1000) : (delay_val);
|
|
||||||
uint32_t flags = furi_thread_flags_wait(
|
|
||||||
WorkerEvtEnd | WorkerEvtStartStop | WorkerEvtPauseResume | WorkerEvtDisconnect,
|
|
||||||
FuriFlagWaitAny,
|
|
||||||
delay_cur);
|
|
||||||
|
|
||||||
delay_val -= delay_cur;
|
|
||||||
if(!(flags & FuriFlagError)) {
|
|
||||||
if(flags & WorkerEvtEnd) {
|
|
||||||
break;
|
|
||||||
} else if(flags & WorkerEvtStartStop) {
|
|
||||||
worker_state = BadBleStateIdle; // Stop executing script
|
|
||||||
bad_ble->hid->release_all(bad_ble->hid_inst);
|
|
||||||
} else if(flags & WorkerEvtDisconnect) {
|
|
||||||
worker_state = BadBleStateNotConnected; // USB disconnected
|
|
||||||
bad_ble->hid->release_all(bad_ble->hid_inst);
|
|
||||||
} else if(flags & WorkerEvtPauseResume) {
|
|
||||||
pause_state = BadBleStateRunning;
|
|
||||||
worker_state = BadBleStatePaused; // Pause
|
|
||||||
}
|
|
||||||
bad_ble->st.state = worker_state;
|
|
||||||
continue;
|
|
||||||
} else if(
|
|
||||||
(flags == (unsigned)FuriFlagErrorTimeout) ||
|
|
||||||
(flags == (unsigned)FuriFlagErrorResource)) {
|
|
||||||
if(delay_val > 0) {
|
|
||||||
bad_ble->st.delay_remain--;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
bad_ble->st.state = BadBleStateRunning;
|
|
||||||
delay_val = ducky_script_execute_next(bad_ble, script_file);
|
|
||||||
if(delay_val == SCRIPT_STATE_ERROR) { // Script error
|
|
||||||
delay_val = 0;
|
|
||||||
worker_state = BadBleStateScriptError;
|
|
||||||
bad_ble->st.state = worker_state;
|
|
||||||
bad_ble->hid->release_all(bad_ble->hid_inst);
|
|
||||||
} else if(delay_val == SCRIPT_STATE_END) { // End of script
|
|
||||||
delay_val = 0;
|
|
||||||
worker_state = BadBleStateIdle;
|
|
||||||
bad_ble->st.state = BadBleStateDone;
|
|
||||||
bad_ble->hid->release_all(bad_ble->hid_inst);
|
|
||||||
continue;
|
|
||||||
} else if(delay_val == SCRIPT_STATE_STRING_START) { // Start printing string with delays
|
|
||||||
delay_val = bad_ble->defdelay;
|
|
||||||
bad_ble->string_print_pos = 0;
|
|
||||||
worker_state = BadBleStateStringDelay;
|
|
||||||
} else if(delay_val == SCRIPT_STATE_WAIT_FOR_BTN) { // set state to wait for user input
|
|
||||||
worker_state = BadBleStateWaitForBtn;
|
|
||||||
bad_ble->st.state = BadBleStateWaitForBtn; // Show long delays
|
|
||||||
} else if(delay_val > 1000) {
|
|
||||||
bad_ble->st.state = BadBleStateDelay; // Show long delays
|
|
||||||
bad_ble->st.delay_remain = delay_val / 1000;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
furi_check((flags & FuriFlagError) == 0);
|
|
||||||
}
|
|
||||||
} else if(worker_state == BadBleStateWaitForBtn) { // State: Wait for button Press
|
|
||||||
uint32_t flags = bad_ble_flags_get(
|
|
||||||
WorkerEvtEnd | WorkerEvtStartStop | WorkerEvtPauseResume | WorkerEvtDisconnect,
|
|
||||||
FuriWaitForever);
|
|
||||||
if(!(flags & FuriFlagError)) {
|
|
||||||
if(flags & WorkerEvtEnd) {
|
|
||||||
break;
|
|
||||||
} else if(flags & WorkerEvtStartStop) {
|
|
||||||
delay_val = 0;
|
|
||||||
worker_state = BadBleStateRunning;
|
|
||||||
} else if(flags & WorkerEvtDisconnect) {
|
|
||||||
worker_state = BadBleStateNotConnected; // USB disconnected
|
|
||||||
bad_ble->hid->release_all(bad_ble->hid_inst);
|
|
||||||
}
|
|
||||||
bad_ble->st.state = worker_state;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else if(worker_state == BadBleStatePaused) { // State: Paused
|
|
||||||
uint32_t flags = bad_ble_flags_get(
|
|
||||||
WorkerEvtEnd | WorkerEvtStartStop | WorkerEvtPauseResume | WorkerEvtDisconnect,
|
|
||||||
FuriWaitForever);
|
|
||||||
if(!(flags & FuriFlagError)) {
|
|
||||||
if(flags & WorkerEvtEnd) {
|
|
||||||
break;
|
|
||||||
} else if(flags & WorkerEvtStartStop) {
|
|
||||||
worker_state = BadBleStateIdle; // Stop executing script
|
|
||||||
bad_ble->st.state = worker_state;
|
|
||||||
bad_ble->hid->release_all(bad_ble->hid_inst);
|
|
||||||
} else if(flags & WorkerEvtDisconnect) {
|
|
||||||
worker_state = BadBleStateNotConnected; // USB disconnected
|
|
||||||
bad_ble->st.state = worker_state;
|
|
||||||
bad_ble->hid->release_all(bad_ble->hid_inst);
|
|
||||||
} else if(flags & WorkerEvtPauseResume) {
|
|
||||||
if(pause_state == BadBleStateRunning) {
|
|
||||||
if(delay_val > 0) {
|
|
||||||
bad_ble->st.state = BadBleStateDelay;
|
|
||||||
bad_ble->st.delay_remain = delay_val / 1000;
|
|
||||||
} else {
|
|
||||||
bad_ble->st.state = BadBleStateRunning;
|
|
||||||
delay_val = 0;
|
|
||||||
}
|
|
||||||
worker_state = BadBleStateRunning; // Resume
|
|
||||||
} else if(pause_state == BadBleStateStringDelay) {
|
|
||||||
bad_ble->st.state = BadBleStateRunning;
|
|
||||||
worker_state = BadBleStateStringDelay; // Resume
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else if(worker_state == BadBleStateStringDelay) { // State: print string with delays
|
|
||||||
uint32_t delay = (bad_ble->stringdelay == 0) ? bad_ble->defstringdelay :
|
|
||||||
bad_ble->stringdelay;
|
|
||||||
uint32_t flags = bad_ble_flags_get(
|
|
||||||
WorkerEvtEnd | WorkerEvtStartStop | WorkerEvtPauseResume | WorkerEvtDisconnect,
|
|
||||||
delay);
|
|
||||||
|
|
||||||
if(!(flags & FuriFlagError)) {
|
|
||||||
if(flags & WorkerEvtEnd) {
|
|
||||||
break;
|
|
||||||
} else if(flags & WorkerEvtStartStop) {
|
|
||||||
worker_state = BadBleStateIdle; // Stop executing script
|
|
||||||
bad_ble->hid->release_all(bad_ble->hid_inst);
|
|
||||||
} else if(flags & WorkerEvtDisconnect) {
|
|
||||||
worker_state = BadBleStateNotConnected; // USB disconnected
|
|
||||||
bad_ble->hid->release_all(bad_ble->hid_inst);
|
|
||||||
} else if(flags & WorkerEvtPauseResume) {
|
|
||||||
pause_state = BadBleStateStringDelay;
|
|
||||||
worker_state = BadBleStatePaused; // Pause
|
|
||||||
}
|
|
||||||
bad_ble->st.state = worker_state;
|
|
||||||
continue;
|
|
||||||
} else if(
|
|
||||||
(flags == (unsigned)FuriFlagErrorTimeout) ||
|
|
||||||
(flags == (unsigned)FuriFlagErrorResource)) {
|
|
||||||
bool string_end = ducky_string_next(bad_ble);
|
|
||||||
if(string_end) {
|
|
||||||
bad_ble->stringdelay = 0;
|
|
||||||
worker_state = BadBleStateRunning;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
furi_check((flags & FuriFlagError) == 0);
|
|
||||||
}
|
|
||||||
} else if(
|
|
||||||
(worker_state == BadBleStateFileError) ||
|
|
||||||
(worker_state == BadBleStateScriptError)) { // State: error
|
|
||||||
uint32_t flags =
|
|
||||||
bad_ble_flags_get(WorkerEvtEnd, FuriWaitForever); // Waiting for exit command
|
|
||||||
|
|
||||||
if(flags & WorkerEvtEnd) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bad_ble->hid->set_state_callback(bad_ble->hid_inst, NULL, NULL);
|
|
||||||
bad_ble->hid->deinit(bad_ble->hid_inst);
|
|
||||||
|
|
||||||
storage_file_close(script_file);
|
|
||||||
storage_file_free(script_file);
|
|
||||||
furi_string_free(bad_ble->line);
|
|
||||||
furi_string_free(bad_ble->line_prev);
|
|
||||||
furi_string_free(bad_ble->string_print);
|
|
||||||
|
|
||||||
FURI_LOG_I(WORKER_TAG, "End");
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void bad_ble_script_set_default_keyboard_layout(BadBleScript* bad_ble) {
|
|
||||||
furi_assert(bad_ble);
|
|
||||||
memset(bad_ble->layout, HID_KEYBOARD_NONE, sizeof(bad_ble->layout));
|
|
||||||
memcpy(bad_ble->layout, hid_asciimap, MIN(sizeof(hid_asciimap), sizeof(bad_ble->layout)));
|
|
||||||
}
|
|
||||||
|
|
||||||
BadBleScript* bad_ble_script_open(FuriString* file_path, BadBleHidInterface interface) {
|
|
||||||
furi_assert(file_path);
|
|
||||||
|
|
||||||
BadBleScript* bad_ble = malloc(sizeof(BadBleScript));
|
|
||||||
bad_ble->file_path = furi_string_alloc();
|
|
||||||
furi_string_set(bad_ble->file_path, file_path);
|
|
||||||
bad_ble_script_set_default_keyboard_layout(bad_ble);
|
|
||||||
|
|
||||||
bad_ble->st.state = BadBleStateInit;
|
|
||||||
bad_ble->st.error[0] = '\0';
|
|
||||||
bad_ble->hid = bad_ble_hid_get_interface(interface);
|
|
||||||
|
|
||||||
bad_ble->thread = furi_thread_alloc_ex("BadBleWorker", 2048, bad_ble_worker, bad_ble);
|
|
||||||
furi_thread_start(bad_ble->thread);
|
|
||||||
return bad_ble;
|
|
||||||
} //-V773
|
|
||||||
|
|
||||||
void bad_ble_script_close(BadBleScript* bad_ble) {
|
|
||||||
furi_assert(bad_ble);
|
|
||||||
furi_thread_flags_set(furi_thread_get_id(bad_ble->thread), WorkerEvtEnd);
|
|
||||||
furi_thread_join(bad_ble->thread);
|
|
||||||
furi_thread_free(bad_ble->thread);
|
|
||||||
furi_string_free(bad_ble->file_path);
|
|
||||||
free(bad_ble);
|
|
||||||
}
|
|
||||||
|
|
||||||
void bad_ble_script_set_keyboard_layout(BadBleScript* bad_ble, FuriString* layout_path) {
|
|
||||||
furi_assert(bad_ble);
|
|
||||||
|
|
||||||
if((bad_ble->st.state == BadBleStateRunning) || (bad_ble->st.state == BadBleStateDelay)) {
|
|
||||||
// do not update keyboard layout while a script is running
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
File* layout_file = storage_file_alloc(furi_record_open(RECORD_STORAGE));
|
|
||||||
if(!furi_string_empty(layout_path)) { //-V1051
|
|
||||||
if(storage_file_open(
|
|
||||||
layout_file, furi_string_get_cstr(layout_path), FSAM_READ, FSOM_OPEN_EXISTING)) {
|
|
||||||
uint16_t layout[128];
|
|
||||||
if(storage_file_read(layout_file, layout, sizeof(layout)) == sizeof(layout)) {
|
|
||||||
memcpy(bad_ble->layout, layout, sizeof(layout));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
storage_file_close(layout_file);
|
|
||||||
} else {
|
|
||||||
bad_ble_script_set_default_keyboard_layout(bad_ble);
|
|
||||||
}
|
|
||||||
storage_file_free(layout_file);
|
|
||||||
}
|
|
||||||
|
|
||||||
void bad_ble_script_start_stop(BadBleScript* bad_ble) {
|
|
||||||
furi_assert(bad_ble);
|
|
||||||
furi_thread_flags_set(furi_thread_get_id(bad_ble->thread), WorkerEvtStartStop);
|
|
||||||
}
|
|
||||||
|
|
||||||
void bad_ble_script_pause_resume(BadBleScript* bad_ble) {
|
|
||||||
furi_assert(bad_ble);
|
|
||||||
furi_thread_flags_set(furi_thread_get_id(bad_ble->thread), WorkerEvtPauseResume);
|
|
||||||
}
|
|
||||||
|
|
||||||
BadBleState* bad_ble_script_get_state(BadBleScript* bad_ble) {
|
|
||||||
furi_assert(bad_ble);
|
|
||||||
return &(bad_ble->st);
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <furi.h>
|
|
||||||
#include <furi_hal.h>
|
|
||||||
#include "bad_ble_hid.h"
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
BadBleStateInit,
|
|
||||||
BadBleStateNotConnected,
|
|
||||||
BadBleStateIdle,
|
|
||||||
BadBleStateWillRun,
|
|
||||||
BadBleStateRunning,
|
|
||||||
BadBleStateDelay,
|
|
||||||
BadBleStateStringDelay,
|
|
||||||
BadBleStateWaitForBtn,
|
|
||||||
BadBleStatePaused,
|
|
||||||
BadBleStateDone,
|
|
||||||
BadBleStateScriptError,
|
|
||||||
BadBleStateFileError,
|
|
||||||
} BadBleWorkerState;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
BadBleWorkerState state;
|
|
||||||
size_t line_cur;
|
|
||||||
size_t line_nb;
|
|
||||||
uint32_t delay_remain;
|
|
||||||
size_t error_line;
|
|
||||||
char error[64];
|
|
||||||
} BadBleState;
|
|
||||||
|
|
||||||
typedef struct BadBleScript BadBleScript;
|
|
||||||
|
|
||||||
BadBleScript* bad_ble_script_open(FuriString* file_path, BadBleHidInterface interface);
|
|
||||||
|
|
||||||
void bad_ble_script_close(BadBleScript* bad_ble);
|
|
||||||
|
|
||||||
void bad_ble_script_set_keyboard_layout(BadBleScript* bad_ble, FuriString* layout_path);
|
|
||||||
|
|
||||||
void bad_ble_script_start(BadBleScript* bad_ble);
|
|
||||||
|
|
||||||
void bad_ble_script_stop(BadBleScript* bad_ble);
|
|
||||||
|
|
||||||
void bad_ble_script_start_stop(BadBleScript* bad_ble);
|
|
||||||
|
|
||||||
void bad_ble_script_pause_resume(BadBleScript* bad_ble);
|
|
||||||
|
|
||||||
BadBleState* bad_ble_script_get_state(BadBleScript* bad_ble);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
#include <furi_hal.h>
|
|
||||||
#include "ducky_script.h"
|
|
||||||
#include "ducky_script_i.h"
|
|
||||||
|
|
||||||
typedef int32_t (*DuckyCmdCallback)(BadBleScript* bad_usb, const char* line, int32_t param);
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
char* name;
|
|
||||||
DuckyCmdCallback callback;
|
|
||||||
int32_t param;
|
|
||||||
} DuckyCmd;
|
|
||||||
|
|
||||||
static int32_t ducky_fnc_delay(BadBleScript* bad_usb, const char* line, int32_t param) {
|
|
||||||
UNUSED(param);
|
|
||||||
|
|
||||||
line = &line[ducky_get_command_len(line) + 1];
|
|
||||||
uint32_t delay_val = 0;
|
|
||||||
bool state = ducky_get_number(line, &delay_val);
|
|
||||||
if((state) && (delay_val > 0)) {
|
|
||||||
return (int32_t)delay_val;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ducky_error(bad_usb, "Invalid number %s", line);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int32_t ducky_fnc_defdelay(BadBleScript* bad_usb, const char* line, int32_t param) {
|
|
||||||
UNUSED(param);
|
|
||||||
|
|
||||||
line = &line[ducky_get_command_len(line) + 1];
|
|
||||||
bool state = ducky_get_number(line, &bad_usb->defdelay);
|
|
||||||
if(!state) {
|
|
||||||
return ducky_error(bad_usb, "Invalid number %s", line);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int32_t ducky_fnc_strdelay(BadBleScript* bad_usb, const char* line, int32_t param) {
|
|
||||||
UNUSED(param);
|
|
||||||
|
|
||||||
line = &line[ducky_get_command_len(line) + 1];
|
|
||||||
bool state = ducky_get_number(line, &bad_usb->stringdelay);
|
|
||||||
if(!state) {
|
|
||||||
return ducky_error(bad_usb, "Invalid number %s", line);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int32_t ducky_fnc_defstrdelay(BadBleScript* bad_usb, const char* line, int32_t param) {
|
|
||||||
UNUSED(param);
|
|
||||||
|
|
||||||
line = &line[ducky_get_command_len(line) + 1];
|
|
||||||
bool state = ducky_get_number(line, &bad_usb->defstringdelay);
|
|
||||||
if(!state) {
|
|
||||||
return ducky_error(bad_usb, "Invalid number %s", line);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int32_t ducky_fnc_string(BadBleScript* bad_usb, const char* line, int32_t param) {
|
|
||||||
line = &line[ducky_get_command_len(line) + 1];
|
|
||||||
furi_string_set_str(bad_usb->string_print, line);
|
|
||||||
if(param == 1) {
|
|
||||||
furi_string_cat(bad_usb->string_print, "\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(bad_usb->stringdelay == 0 &&
|
|
||||||
bad_usb->defstringdelay == 0) { // stringdelay not set - run command immediately
|
|
||||||
bool state = ducky_string(bad_usb, furi_string_get_cstr(bad_usb->string_print));
|
|
||||||
if(!state) {
|
|
||||||
return ducky_error(bad_usb, "Invalid string %s", line);
|
|
||||||
}
|
|
||||||
} else { // stringdelay is set - run command in thread to keep handling external events
|
|
||||||
return SCRIPT_STATE_STRING_START;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int32_t ducky_fnc_repeat(BadBleScript* bad_usb, const char* line, int32_t param) {
|
|
||||||
UNUSED(param);
|
|
||||||
|
|
||||||
line = &line[ducky_get_command_len(line) + 1];
|
|
||||||
bool state = ducky_get_number(line, &bad_usb->repeat_cnt);
|
|
||||||
if((!state) || (bad_usb->repeat_cnt == 0)) {
|
|
||||||
return ducky_error(bad_usb, "Invalid number %s", line);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int32_t ducky_fnc_sysrq(BadBleScript* bad_usb, const char* line, int32_t param) {
|
|
||||||
UNUSED(param);
|
|
||||||
|
|
||||||
line = &line[ducky_get_command_len(line) + 1];
|
|
||||||
uint16_t key = ducky_get_keycode(bad_usb, line, true);
|
|
||||||
bad_usb->hid->kb_press(bad_usb->hid_inst, KEY_MOD_LEFT_ALT | HID_KEYBOARD_PRINT_SCREEN);
|
|
||||||
bad_usb->hid->kb_press(bad_usb->hid_inst, key);
|
|
||||||
bad_usb->hid->release_all(bad_usb->hid_inst);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int32_t ducky_fnc_altchar(BadBleScript* bad_usb, const char* line, int32_t param) {
|
|
||||||
UNUSED(param);
|
|
||||||
|
|
||||||
line = &line[ducky_get_command_len(line) + 1];
|
|
||||||
ducky_numlock_on(bad_usb);
|
|
||||||
bool state = ducky_altchar(bad_usb, line);
|
|
||||||
if(!state) {
|
|
||||||
return ducky_error(bad_usb, "Invalid altchar %s", line);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int32_t ducky_fnc_altstring(BadBleScript* bad_usb, const char* line, int32_t param) {
|
|
||||||
UNUSED(param);
|
|
||||||
|
|
||||||
line = &line[ducky_get_command_len(line) + 1];
|
|
||||||
ducky_numlock_on(bad_usb);
|
|
||||||
bool state = ducky_altstring(bad_usb, line);
|
|
||||||
if(!state) {
|
|
||||||
return ducky_error(bad_usb, "Invalid altstring %s", line);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int32_t ducky_fnc_hold(BadBleScript* bad_usb, const char* line, int32_t param) {
|
|
||||||
UNUSED(param);
|
|
||||||
|
|
||||||
line = &line[ducky_get_command_len(line) + 1];
|
|
||||||
uint16_t key = ducky_get_keycode(bad_usb, line, true);
|
|
||||||
if(key == HID_KEYBOARD_NONE) {
|
|
||||||
return ducky_error(bad_usb, "No keycode defined for %s", line);
|
|
||||||
}
|
|
||||||
bad_usb->key_hold_nb++;
|
|
||||||
if(bad_usb->key_hold_nb > (HID_KB_MAX_KEYS - 1)) {
|
|
||||||
return ducky_error(bad_usb, "Too many keys are hold");
|
|
||||||
}
|
|
||||||
bad_usb->hid->kb_press(bad_usb->hid_inst, key);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int32_t ducky_fnc_release(BadBleScript* bad_usb, const char* line, int32_t param) {
|
|
||||||
UNUSED(param);
|
|
||||||
|
|
||||||
line = &line[ducky_get_command_len(line) + 1];
|
|
||||||
uint16_t key = ducky_get_keycode(bad_usb, line, true);
|
|
||||||
if(key == HID_KEYBOARD_NONE) {
|
|
||||||
return ducky_error(bad_usb, "No keycode defined for %s", line);
|
|
||||||
}
|
|
||||||
if(bad_usb->key_hold_nb == 0) {
|
|
||||||
return ducky_error(bad_usb, "No keys are hold");
|
|
||||||
}
|
|
||||||
bad_usb->key_hold_nb--;
|
|
||||||
bad_usb->hid->kb_release(bad_usb->hid_inst, key);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int32_t ducky_fnc_media(BadBleScript* bad_usb, const char* line, int32_t param) {
|
|
||||||
UNUSED(param);
|
|
||||||
|
|
||||||
line = &line[ducky_get_command_len(line) + 1];
|
|
||||||
uint16_t key = ducky_get_media_keycode_by_name(line);
|
|
||||||
if(key == HID_CONSUMER_UNASSIGNED) {
|
|
||||||
return ducky_error(bad_usb, "No keycode defined for %s", line);
|
|
||||||
}
|
|
||||||
bad_usb->hid->consumer_press(bad_usb->hid_inst, key);
|
|
||||||
bad_usb->hid->consumer_release(bad_usb->hid_inst, key);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int32_t ducky_fnc_globe(BadBleScript* bad_usb, const char* line, int32_t param) {
|
|
||||||
UNUSED(param);
|
|
||||||
|
|
||||||
line = &line[ducky_get_command_len(line) + 1];
|
|
||||||
uint16_t key = ducky_get_keycode(bad_usb, line, true);
|
|
||||||
if(key == HID_KEYBOARD_NONE) {
|
|
||||||
return ducky_error(bad_usb, "No keycode defined for %s", line);
|
|
||||||
}
|
|
||||||
|
|
||||||
bad_usb->hid->consumer_press(bad_usb->hid_inst, HID_CONSUMER_FN_GLOBE);
|
|
||||||
bad_usb->hid->kb_press(bad_usb->hid_inst, key);
|
|
||||||
bad_usb->hid->kb_release(bad_usb->hid_inst, key);
|
|
||||||
bad_usb->hid->consumer_release(bad_usb->hid_inst, HID_CONSUMER_FN_GLOBE);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int32_t ducky_fnc_waitforbutton(BadBleScript* bad_usb, const char* line, int32_t param) {
|
|
||||||
UNUSED(param);
|
|
||||||
UNUSED(bad_usb);
|
|
||||||
UNUSED(line);
|
|
||||||
|
|
||||||
return SCRIPT_STATE_WAIT_FOR_BTN;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const DuckyCmd ducky_commands[] = {
|
|
||||||
{"REM", NULL, -1},
|
|
||||||
{"ID", NULL, -1},
|
|
||||||
{"DELAY", ducky_fnc_delay, -1},
|
|
||||||
{"STRING", ducky_fnc_string, 0},
|
|
||||||
{"STRINGLN", ducky_fnc_string, 1},
|
|
||||||
{"DEFAULT_DELAY", ducky_fnc_defdelay, -1},
|
|
||||||
{"DEFAULTDELAY", ducky_fnc_defdelay, -1},
|
|
||||||
{"STRINGDELAY", ducky_fnc_strdelay, -1},
|
|
||||||
{"STRING_DELAY", ducky_fnc_strdelay, -1},
|
|
||||||
{"DEFAULT_STRING_DELAY", ducky_fnc_defstrdelay, -1},
|
|
||||||
{"DEFAULTSTRINGDELAY", ducky_fnc_defstrdelay, -1},
|
|
||||||
{"REPEAT", ducky_fnc_repeat, -1},
|
|
||||||
{"SYSRQ", ducky_fnc_sysrq, -1},
|
|
||||||
{"ALTCHAR", ducky_fnc_altchar, -1},
|
|
||||||
{"ALTSTRING", ducky_fnc_altstring, -1},
|
|
||||||
{"ALTCODE", ducky_fnc_altstring, -1},
|
|
||||||
{"HOLD", ducky_fnc_hold, -1},
|
|
||||||
{"RELEASE", ducky_fnc_release, -1},
|
|
||||||
{"WAIT_FOR_BUTTON_PRESS", ducky_fnc_waitforbutton, -1},
|
|
||||||
{"MEDIA", ducky_fnc_media, -1},
|
|
||||||
{"GLOBE", ducky_fnc_globe, -1},
|
|
||||||
};
|
|
||||||
|
|
||||||
#define TAG "BadBle"
|
|
||||||
|
|
||||||
#define WORKER_TAG TAG "Worker"
|
|
||||||
|
|
||||||
int32_t ducky_execute_cmd(BadBleScript* bad_usb, const char* line) {
|
|
||||||
size_t cmd_word_len = strcspn(line, " ");
|
|
||||||
for(size_t i = 0; i < COUNT_OF(ducky_commands); i++) {
|
|
||||||
size_t cmd_compare_len = strlen(ducky_commands[i].name);
|
|
||||||
|
|
||||||
if(cmd_compare_len != cmd_word_len) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(strncmp(line, ducky_commands[i].name, cmd_compare_len) == 0) {
|
|
||||||
if(ducky_commands[i].callback == NULL) {
|
|
||||||
return 0;
|
|
||||||
} else {
|
|
||||||
return (ducky_commands[i].callback)(bad_usb, line, ducky_commands[i].param);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return SCRIPT_STATE_CMD_UNKNOWN;
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <furi.h>
|
|
||||||
#include <furi_hal.h>
|
|
||||||
#include "ducky_script.h"
|
|
||||||
#include "bad_ble_hid.h"
|
|
||||||
|
|
||||||
#define SCRIPT_STATE_ERROR (-1)
|
|
||||||
#define SCRIPT_STATE_END (-2)
|
|
||||||
#define SCRIPT_STATE_NEXT_LINE (-3)
|
|
||||||
#define SCRIPT_STATE_CMD_UNKNOWN (-4)
|
|
||||||
#define SCRIPT_STATE_STRING_START (-5)
|
|
||||||
#define SCRIPT_STATE_WAIT_FOR_BTN (-6)
|
|
||||||
|
|
||||||
#define FILE_BUFFER_LEN 16
|
|
||||||
|
|
||||||
struct BadBleScript {
|
|
||||||
FuriHalUsbHidConfig hid_cfg;
|
|
||||||
const BadBleHidApi* hid;
|
|
||||||
void* hid_inst;
|
|
||||||
FuriThread* thread;
|
|
||||||
BadBleState st;
|
|
||||||
|
|
||||||
FuriString* file_path;
|
|
||||||
uint8_t file_buf[FILE_BUFFER_LEN + 1];
|
|
||||||
uint8_t buf_start;
|
|
||||||
uint8_t buf_len;
|
|
||||||
bool file_end;
|
|
||||||
|
|
||||||
uint32_t defdelay;
|
|
||||||
uint32_t stringdelay;
|
|
||||||
uint32_t defstringdelay;
|
|
||||||
uint16_t layout[128];
|
|
||||||
|
|
||||||
FuriString* line;
|
|
||||||
FuriString* line_prev;
|
|
||||||
uint32_t repeat_cnt;
|
|
||||||
uint8_t key_hold_nb;
|
|
||||||
|
|
||||||
FuriString* string_print;
|
|
||||||
size_t string_print_pos;
|
|
||||||
};
|
|
||||||
|
|
||||||
uint16_t ducky_get_keycode(BadBleScript* bad_usb, const char* param, bool accept_chars);
|
|
||||||
|
|
||||||
uint32_t ducky_get_command_len(const char* line);
|
|
||||||
|
|
||||||
bool ducky_is_line_end(const char chr);
|
|
||||||
|
|
||||||
uint16_t ducky_get_keycode_by_name(const char* param);
|
|
||||||
|
|
||||||
uint16_t ducky_get_media_keycode_by_name(const char* param);
|
|
||||||
|
|
||||||
bool ducky_get_number(const char* param, uint32_t* val);
|
|
||||||
|
|
||||||
void ducky_numlock_on(BadBleScript* bad_usb);
|
|
||||||
|
|
||||||
bool ducky_numpad_press(BadBleScript* bad_usb, const char num);
|
|
||||||
|
|
||||||
bool ducky_altchar(BadBleScript* bad_usb, const char* charcode);
|
|
||||||
|
|
||||||
bool ducky_altstring(BadBleScript* bad_usb, const char* param);
|
|
||||||
|
|
||||||
bool ducky_string(BadBleScript* bad_usb, const char* param);
|
|
||||||
|
|
||||||
int32_t ducky_execute_cmd(BadBleScript* bad_usb, const char* line);
|
|
||||||
|
|
||||||
int32_t ducky_error(BadBleScript* bad_usb, const char* text, ...);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
#include <furi_hal.h>
|
|
||||||
#include "ducky_script_i.h"
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
char* name;
|
|
||||||
uint16_t keycode;
|
|
||||||
} DuckyKey;
|
|
||||||
|
|
||||||
static const DuckyKey ducky_keys[] = {
|
|
||||||
{"CTRL-ALT", KEY_MOD_LEFT_CTRL | KEY_MOD_LEFT_ALT},
|
|
||||||
{"CTRL-SHIFT", KEY_MOD_LEFT_CTRL | KEY_MOD_LEFT_SHIFT},
|
|
||||||
{"ALT-SHIFT", KEY_MOD_LEFT_ALT | KEY_MOD_LEFT_SHIFT},
|
|
||||||
{"ALT-GUI", KEY_MOD_LEFT_ALT | KEY_MOD_LEFT_GUI},
|
|
||||||
{"GUI-SHIFT", KEY_MOD_LEFT_GUI | KEY_MOD_LEFT_SHIFT},
|
|
||||||
{"GUI-CTRL", KEY_MOD_LEFT_GUI | KEY_MOD_LEFT_CTRL},
|
|
||||||
|
|
||||||
{"CTRL", KEY_MOD_LEFT_CTRL},
|
|
||||||
{"CONTROL", KEY_MOD_LEFT_CTRL},
|
|
||||||
{"SHIFT", KEY_MOD_LEFT_SHIFT},
|
|
||||||
{"ALT", KEY_MOD_LEFT_ALT},
|
|
||||||
{"GUI", KEY_MOD_LEFT_GUI},
|
|
||||||
{"WINDOWS", KEY_MOD_LEFT_GUI},
|
|
||||||
|
|
||||||
{"DOWNARROW", HID_KEYBOARD_DOWN_ARROW},
|
|
||||||
{"DOWN", HID_KEYBOARD_DOWN_ARROW},
|
|
||||||
{"LEFTARROW", HID_KEYBOARD_LEFT_ARROW},
|
|
||||||
{"LEFT", HID_KEYBOARD_LEFT_ARROW},
|
|
||||||
{"RIGHTARROW", HID_KEYBOARD_RIGHT_ARROW},
|
|
||||||
{"RIGHT", HID_KEYBOARD_RIGHT_ARROW},
|
|
||||||
{"UPARROW", HID_KEYBOARD_UP_ARROW},
|
|
||||||
{"UP", HID_KEYBOARD_UP_ARROW},
|
|
||||||
|
|
||||||
{"ENTER", HID_KEYBOARD_RETURN},
|
|
||||||
{"BREAK", HID_KEYBOARD_PAUSE},
|
|
||||||
{"PAUSE", HID_KEYBOARD_PAUSE},
|
|
||||||
{"CAPSLOCK", HID_KEYBOARD_CAPS_LOCK},
|
|
||||||
{"DELETE", HID_KEYBOARD_DELETE_FORWARD},
|
|
||||||
{"BACKSPACE", HID_KEYBOARD_DELETE},
|
|
||||||
{"END", HID_KEYBOARD_END},
|
|
||||||
{"ESC", HID_KEYBOARD_ESCAPE},
|
|
||||||
{"ESCAPE", HID_KEYBOARD_ESCAPE},
|
|
||||||
{"HOME", HID_KEYBOARD_HOME},
|
|
||||||
{"INSERT", HID_KEYBOARD_INSERT},
|
|
||||||
{"NUMLOCK", HID_KEYPAD_NUMLOCK},
|
|
||||||
{"PAGEUP", HID_KEYBOARD_PAGE_UP},
|
|
||||||
{"PAGEDOWN", HID_KEYBOARD_PAGE_DOWN},
|
|
||||||
{"PRINTSCREEN", HID_KEYBOARD_PRINT_SCREEN},
|
|
||||||
{"SCROLLLOCK", HID_KEYBOARD_SCROLL_LOCK},
|
|
||||||
{"SPACE", HID_KEYBOARD_SPACEBAR},
|
|
||||||
{"TAB", HID_KEYBOARD_TAB},
|
|
||||||
{"MENU", HID_KEYBOARD_APPLICATION},
|
|
||||||
{"APP", HID_KEYBOARD_APPLICATION},
|
|
||||||
|
|
||||||
{"F1", HID_KEYBOARD_F1},
|
|
||||||
{"F2", HID_KEYBOARD_F2},
|
|
||||||
{"F3", HID_KEYBOARD_F3},
|
|
||||||
{"F4", HID_KEYBOARD_F4},
|
|
||||||
{"F5", HID_KEYBOARD_F5},
|
|
||||||
{"F6", HID_KEYBOARD_F6},
|
|
||||||
{"F7", HID_KEYBOARD_F7},
|
|
||||||
{"F8", HID_KEYBOARD_F8},
|
|
||||||
{"F9", HID_KEYBOARD_F9},
|
|
||||||
{"F10", HID_KEYBOARD_F10},
|
|
||||||
{"F11", HID_KEYBOARD_F11},
|
|
||||||
{"F12", HID_KEYBOARD_F12},
|
|
||||||
{"F13", HID_KEYBOARD_F13},
|
|
||||||
{"F14", HID_KEYBOARD_F14},
|
|
||||||
{"F15", HID_KEYBOARD_F15},
|
|
||||||
{"F16", HID_KEYBOARD_F16},
|
|
||||||
{"F17", HID_KEYBOARD_F17},
|
|
||||||
{"F18", HID_KEYBOARD_F18},
|
|
||||||
{"F19", HID_KEYBOARD_F19},
|
|
||||||
{"F20", HID_KEYBOARD_F20},
|
|
||||||
{"F21", HID_KEYBOARD_F21},
|
|
||||||
{"F22", HID_KEYBOARD_F22},
|
|
||||||
{"F23", HID_KEYBOARD_F23},
|
|
||||||
{"F24", HID_KEYBOARD_F24},
|
|
||||||
};
|
|
||||||
|
|
||||||
static const DuckyKey ducky_media_keys[] = {
|
|
||||||
{"POWER", HID_CONSUMER_POWER},
|
|
||||||
{"REBOOT", HID_CONSUMER_RESET},
|
|
||||||
{"SLEEP", HID_CONSUMER_SLEEP},
|
|
||||||
{"LOGOFF", HID_CONSUMER_AL_LOGOFF},
|
|
||||||
|
|
||||||
{"EXIT", HID_CONSUMER_AC_EXIT},
|
|
||||||
{"HOME", HID_CONSUMER_AC_HOME},
|
|
||||||
{"BACK", HID_CONSUMER_AC_BACK},
|
|
||||||
{"FORWARD", HID_CONSUMER_AC_FORWARD},
|
|
||||||
{"REFRESH", HID_CONSUMER_AC_REFRESH},
|
|
||||||
|
|
||||||
{"SNAPSHOT", HID_CONSUMER_SNAPSHOT},
|
|
||||||
|
|
||||||
{"PLAY", HID_CONSUMER_PLAY},
|
|
||||||
{"PAUSE", HID_CONSUMER_PAUSE},
|
|
||||||
{"PLAY_PAUSE", HID_CONSUMER_PLAY_PAUSE},
|
|
||||||
{"NEXT_TRACK", HID_CONSUMER_SCAN_NEXT_TRACK},
|
|
||||||
{"PREV_TRACK", HID_CONSUMER_SCAN_PREVIOUS_TRACK},
|
|
||||||
{"STOP", HID_CONSUMER_STOP},
|
|
||||||
{"EJECT", HID_CONSUMER_EJECT},
|
|
||||||
|
|
||||||
{"MUTE", HID_CONSUMER_MUTE},
|
|
||||||
{"VOLUME_UP", HID_CONSUMER_VOLUME_INCREMENT},
|
|
||||||
{"VOLUME_DOWN", HID_CONSUMER_VOLUME_DECREMENT},
|
|
||||||
|
|
||||||
{"FN", HID_CONSUMER_FN_GLOBE},
|
|
||||||
{"BRIGHT_UP", HID_CONSUMER_BRIGHTNESS_INCREMENT},
|
|
||||||
{"BRIGHT_DOWN", HID_CONSUMER_BRIGHTNESS_DECREMENT},
|
|
||||||
};
|
|
||||||
|
|
||||||
uint16_t ducky_get_keycode_by_name(const char* param) {
|
|
||||||
for(size_t i = 0; i < COUNT_OF(ducky_keys); i++) {
|
|
||||||
size_t key_cmd_len = strlen(ducky_keys[i].name);
|
|
||||||
if((strncmp(param, ducky_keys[i].name, key_cmd_len) == 0) &&
|
|
||||||
(ducky_is_line_end(param[key_cmd_len]))) {
|
|
||||||
return ducky_keys[i].keycode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return HID_KEYBOARD_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t ducky_get_media_keycode_by_name(const char* param) {
|
|
||||||
for(size_t i = 0; i < COUNT_OF(ducky_media_keys); i++) {
|
|
||||||
size_t key_cmd_len = strlen(ducky_media_keys[i].name);
|
|
||||||
if((strncmp(param, ducky_media_keys[i].name, key_cmd_len) == 0) &&
|
|
||||||
(ducky_is_line_end(param[key_cmd_len]))) {
|
|
||||||
return ducky_media_keys[i].keycode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return HID_CONSUMER_UNASSIGNED;
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 96 B |
@@ -1,30 +0,0 @@
|
|||||||
#include "bad_ble_scene.h"
|
|
||||||
|
|
||||||
// Generate scene on_enter handlers array
|
|
||||||
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
|
|
||||||
void (*const bad_ble_scene_on_enter_handlers[])(void*) = {
|
|
||||||
#include "bad_ble_scene_config.h"
|
|
||||||
};
|
|
||||||
#undef ADD_SCENE
|
|
||||||
|
|
||||||
// Generate scene on_event handlers array
|
|
||||||
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
|
|
||||||
bool (*const bad_ble_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = {
|
|
||||||
#include "bad_ble_scene_config.h"
|
|
||||||
};
|
|
||||||
#undef ADD_SCENE
|
|
||||||
|
|
||||||
// Generate scene on_exit handlers array
|
|
||||||
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
|
|
||||||
void (*const bad_ble_scene_on_exit_handlers[])(void* context) = {
|
|
||||||
#include "bad_ble_scene_config.h"
|
|
||||||
};
|
|
||||||
#undef ADD_SCENE
|
|
||||||
|
|
||||||
// Initialize scene handlers configuration structure
|
|
||||||
const SceneManagerHandlers bad_ble_scene_handlers = {
|
|
||||||
.on_enter_handlers = bad_ble_scene_on_enter_handlers,
|
|
||||||
.on_event_handlers = bad_ble_scene_on_event_handlers,
|
|
||||||
.on_exit_handlers = bad_ble_scene_on_exit_handlers,
|
|
||||||
.scene_num = BadBleSceneNum,
|
|
||||||
};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <gui/scene_manager.h>
|
|
||||||
|
|
||||||
// Generate scene id and total number
|
|
||||||
#define ADD_SCENE(prefix, name, id) BadBleScene##id,
|
|
||||||
typedef enum {
|
|
||||||
#include "bad_ble_scene_config.h"
|
|
||||||
BadBleSceneNum,
|
|
||||||
} BadBleScene;
|
|
||||||
#undef ADD_SCENE
|
|
||||||
|
|
||||||
extern const SceneManagerHandlers bad_ble_scene_handlers;
|
|
||||||
|
|
||||||
// Generate scene on_enter handlers declaration
|
|
||||||
#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
|
|
||||||
#include "bad_ble_scene_config.h"
|
|
||||||
#undef ADD_SCENE
|
|
||||||
|
|
||||||
// Generate scene on_event handlers declaration
|
|
||||||
#define ADD_SCENE(prefix, name, id) \
|
|
||||||
bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event);
|
|
||||||
#include "bad_ble_scene_config.h"
|
|
||||||
#undef ADD_SCENE
|
|
||||||
|
|
||||||
// Generate scene on_exit handlers declaration
|
|
||||||
#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context);
|
|
||||||
#include "bad_ble_scene_config.h"
|
|
||||||
#undef ADD_SCENE
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
#include "../bad_ble_app_i.h"
|
|
||||||
|
|
||||||
enum SubmenuIndex {
|
|
||||||
ConfigIndexKeyboardLayout,
|
|
||||||
ConfigIndexBleUnpair,
|
|
||||||
};
|
|
||||||
|
|
||||||
void bad_ble_scene_config_select_callback(void* context, uint32_t index) {
|
|
||||||
BadBleApp* bad_ble = context;
|
|
||||||
|
|
||||||
view_dispatcher_send_custom_event(bad_ble->view_dispatcher, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void draw_menu(BadBleApp* bad_ble) {
|
|
||||||
VariableItemList* var_item_list = bad_ble->var_item_list;
|
|
||||||
|
|
||||||
variable_item_list_reset(var_item_list);
|
|
||||||
|
|
||||||
variable_item_list_add(var_item_list, "Keyboard Layout (Global)", 0, NULL, NULL);
|
|
||||||
|
|
||||||
variable_item_list_add(var_item_list, "Unpair Device", 0, NULL, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
void bad_ble_scene_config_on_enter(void* context) {
|
|
||||||
BadBleApp* bad_ble = context;
|
|
||||||
VariableItemList* var_item_list = bad_ble->var_item_list;
|
|
||||||
|
|
||||||
variable_item_list_set_enter_callback(
|
|
||||||
var_item_list, bad_ble_scene_config_select_callback, bad_ble);
|
|
||||||
draw_menu(bad_ble);
|
|
||||||
variable_item_list_set_selected_item(var_item_list, 0);
|
|
||||||
|
|
||||||
view_dispatcher_switch_to_view(bad_ble->view_dispatcher, BadBleAppViewConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool bad_ble_scene_config_on_event(void* context, SceneManagerEvent event) {
|
|
||||||
BadBleApp* bad_ble = context;
|
|
||||||
bool consumed = false;
|
|
||||||
|
|
||||||
if(event.type == SceneManagerEventTypeCustom) {
|
|
||||||
consumed = true;
|
|
||||||
if(event.event == ConfigIndexKeyboardLayout) {
|
|
||||||
scene_manager_next_scene(bad_ble->scene_manager, BadBleSceneConfigLayout);
|
|
||||||
} else if(event.event == ConfigIndexBleUnpair) {
|
|
||||||
scene_manager_next_scene(bad_ble->scene_manager, BadBleSceneConfirmUnpair);
|
|
||||||
} else {
|
|
||||||
furi_crash("Unknown key type");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return consumed;
|
|
||||||
}
|
|
||||||
|
|
||||||
void bad_ble_scene_config_on_exit(void* context) {
|
|
||||||
BadBleApp* bad_ble = context;
|
|
||||||
VariableItemList* var_item_list = bad_ble->var_item_list;
|
|
||||||
|
|
||||||
variable_item_list_reset(var_item_list);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
ADD_SCENE(bad_ble, file_select, FileSelect)
|
|
||||||
ADD_SCENE(bad_ble, work, Work)
|
|
||||||
ADD_SCENE(bad_ble, error, Error)
|
|
||||||
ADD_SCENE(bad_ble, config, Config)
|
|
||||||
ADD_SCENE(bad_ble, config_layout, ConfigLayout)
|
|
||||||
ADD_SCENE(bad_ble, confirm_unpair, ConfirmUnpair)
|
|
||||||
ADD_SCENE(bad_ble, unpair_done, UnpairDone)
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
#include "../bad_ble_app_i.h"
|
|
||||||
#include <storage/storage.h>
|
|
||||||
|
|
||||||
static bool bad_ble_layout_select(BadBleApp* bad_ble) {
|
|
||||||
furi_assert(bad_ble);
|
|
||||||
|
|
||||||
FuriString* predefined_path;
|
|
||||||
predefined_path = furi_string_alloc();
|
|
||||||
if(!furi_string_empty(bad_ble->keyboard_layout)) {
|
|
||||||
furi_string_set(predefined_path, bad_ble->keyboard_layout);
|
|
||||||
} else {
|
|
||||||
furi_string_set(predefined_path, BAD_BLE_APP_PATH_LAYOUT_FOLDER);
|
|
||||||
}
|
|
||||||
|
|
||||||
DialogsFileBrowserOptions browser_options;
|
|
||||||
dialog_file_browser_set_basic_options(
|
|
||||||
&browser_options, BAD_BLE_APP_LAYOUT_EXTENSION, &I_keyboard_10px);
|
|
||||||
browser_options.base_path = BAD_BLE_APP_PATH_LAYOUT_FOLDER;
|
|
||||||
browser_options.skip_assets = false;
|
|
||||||
|
|
||||||
// Input events and views are managed by file_browser
|
|
||||||
bool res = dialog_file_browser_show(
|
|
||||||
bad_ble->dialogs, bad_ble->keyboard_layout, predefined_path, &browser_options);
|
|
||||||
|
|
||||||
furi_string_free(predefined_path);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
void bad_ble_scene_config_layout_on_enter(void* context) {
|
|
||||||
BadBleApp* bad_ble = context;
|
|
||||||
|
|
||||||
if(bad_ble_layout_select(bad_ble)) {
|
|
||||||
scene_manager_search_and_switch_to_previous_scene(bad_ble->scene_manager, BadBleSceneWork);
|
|
||||||
} else {
|
|
||||||
scene_manager_previous_scene(bad_ble->scene_manager);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool bad_ble_scene_config_layout_on_event(void* context, SceneManagerEvent event) {
|
|
||||||
UNUSED(context);
|
|
||||||
UNUSED(event);
|
|
||||||
// BadBleApp* bad_ble = context;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void bad_ble_scene_config_layout_on_exit(void* context) {
|
|
||||||
UNUSED(context);
|
|
||||||
// BadBleApp* bad_ble = context;
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
#include "../bad_ble_app_i.h"
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
BadBleCustomEventErrorBack,
|
|
||||||
} BadBleCustomEvent;
|
|
||||||
|
|
||||||
static void
|
|
||||||
bad_ble_scene_error_event_callback(GuiButtonType result, InputType type, void* context) {
|
|
||||||
furi_assert(context);
|
|
||||||
BadBleApp* app = context;
|
|
||||||
|
|
||||||
if((result == GuiButtonTypeLeft) && (type == InputTypeShort)) {
|
|
||||||
view_dispatcher_send_custom_event(app->view_dispatcher, BadBleCustomEventErrorBack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void bad_ble_scene_error_on_enter(void* context) {
|
|
||||||
BadBleApp* app = context;
|
|
||||||
|
|
||||||
if(app->error == BadBleAppErrorNoFiles) {
|
|
||||||
widget_add_icon_element(app->widget, 0, 0, &I_SDQuestion_35x43);
|
|
||||||
widget_add_string_multiline_element(
|
|
||||||
app->widget,
|
|
||||||
81,
|
|
||||||
4,
|
|
||||||
AlignCenter,
|
|
||||||
AlignTop,
|
|
||||||
FontSecondary,
|
|
||||||
"No SD card or\napp data found.\nThis app will not\nwork without\nrequired files.");
|
|
||||||
widget_add_button_element(
|
|
||||||
app->widget, GuiButtonTypeLeft, "Back", bad_ble_scene_error_event_callback, app);
|
|
||||||
} else if(app->error == BadBleAppErrorCloseRpc) {
|
|
||||||
widget_add_icon_element(app->widget, 78, 0, &I_ActiveConnection_50x64);
|
|
||||||
widget_add_string_multiline_element(
|
|
||||||
app->widget, 3, 2, AlignLeft, AlignTop, FontPrimary, "Connection\nIs Active!");
|
|
||||||
widget_add_string_multiline_element(
|
|
||||||
app->widget,
|
|
||||||
3,
|
|
||||||
30,
|
|
||||||
AlignLeft,
|
|
||||||
AlignTop,
|
|
||||||
FontSecondary,
|
|
||||||
"Disconnect from\nPC or phone to\nuse this function.");
|
|
||||||
}
|
|
||||||
|
|
||||||
view_dispatcher_switch_to_view(app->view_dispatcher, BadBleAppViewWidget);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool bad_ble_scene_error_on_event(void* context, SceneManagerEvent event) {
|
|
||||||
BadBleApp* app = context;
|
|
||||||
bool consumed = false;
|
|
||||||
|
|
||||||
if(event.type == SceneManagerEventTypeCustom) {
|
|
||||||
if(event.event == BadBleCustomEventErrorBack) {
|
|
||||||
view_dispatcher_stop(app->view_dispatcher);
|
|
||||||
consumed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return consumed;
|
|
||||||
}
|
|
||||||
|
|
||||||
void bad_ble_scene_error_on_exit(void* context) {
|
|
||||||
BadBleApp* app = context;
|
|
||||||
widget_reset(app->widget);
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
#include "../bad_ble_app_i.h"
|
|
||||||
#include <furi_hal_power.h>
|
|
||||||
#include <storage/storage.h>
|
|
||||||
|
|
||||||
static bool bad_ble_file_select(BadBleApp* bad_ble) {
|
|
||||||
furi_assert(bad_ble);
|
|
||||||
|
|
||||||
DialogsFileBrowserOptions browser_options;
|
|
||||||
dialog_file_browser_set_basic_options(
|
|
||||||
&browser_options, BAD_BLE_APP_SCRIPT_EXTENSION, &I_badusb_10px);
|
|
||||||
browser_options.base_path = BAD_BLE_APP_BASE_FOLDER;
|
|
||||||
browser_options.skip_assets = true;
|
|
||||||
|
|
||||||
// Input events and views are managed by file_browser
|
|
||||||
bool res = dialog_file_browser_show(
|
|
||||||
bad_ble->dialogs, bad_ble->file_path, bad_ble->file_path, &browser_options);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
void bad_ble_scene_file_select_on_enter(void* context) {
|
|
||||||
BadBleApp* bad_ble = context;
|
|
||||||
|
|
||||||
if(bad_ble->bad_ble_script) {
|
|
||||||
bad_ble_script_close(bad_ble->bad_ble_script);
|
|
||||||
bad_ble->bad_ble_script = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(bad_ble_file_select(bad_ble)) {
|
|
||||||
scene_manager_next_scene(bad_ble->scene_manager, BadBleSceneWork);
|
|
||||||
} else {
|
|
||||||
view_dispatcher_stop(bad_ble->view_dispatcher);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool bad_ble_scene_file_select_on_event(void* context, SceneManagerEvent event) {
|
|
||||||
UNUSED(context);
|
|
||||||
UNUSED(event);
|
|
||||||
// BadBleApp* bad_ble = context;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void bad_ble_scene_file_select_on_exit(void* context) {
|
|
||||||
UNUSED(context);
|
|
||||||
// BadBleApp* bad_ble = context;
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
#include "../bad_ble_app_i.h"
|
|
||||||
|
|
||||||
static void bad_ble_scene_unpair_done_popup_callback(void* context) {
|
|
||||||
BadBleApp* bad_ble = context;
|
|
||||||
scene_manager_search_and_switch_to_previous_scene(bad_ble->scene_manager, BadBleSceneConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
void bad_ble_scene_unpair_done_on_enter(void* context) {
|
|
||||||
BadBleApp* bad_ble = context;
|
|
||||||
Popup* popup = bad_ble->popup;
|
|
||||||
|
|
||||||
popup_set_icon(popup, 48, 4, &I_DolphinDone_80x58);
|
|
||||||
popup_set_header(popup, "Done", 20, 19, AlignLeft, AlignBottom);
|
|
||||||
popup_set_callback(popup, bad_ble_scene_unpair_done_popup_callback);
|
|
||||||
popup_set_context(popup, bad_ble);
|
|
||||||
popup_set_timeout(popup, 1000);
|
|
||||||
popup_enable_timeout(popup);
|
|
||||||
|
|
||||||
view_dispatcher_switch_to_view(bad_ble->view_dispatcher, BadBleAppViewPopup);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool bad_ble_scene_unpair_done_on_event(void* context, SceneManagerEvent event) {
|
|
||||||
BadBleApp* bad_ble = context;
|
|
||||||
UNUSED(bad_ble);
|
|
||||||
UNUSED(event);
|
|
||||||
|
|
||||||
bool consumed = false;
|
|
||||||
|
|
||||||
return consumed;
|
|
||||||
}
|
|
||||||
|
|
||||||
void bad_ble_scene_unpair_done_on_exit(void* context) {
|
|
||||||
BadBleApp* bad_ble = context;
|
|
||||||
Popup* popup = bad_ble->popup;
|
|
||||||
|
|
||||||
popup_reset(popup);
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
#include "../helpers/ducky_script.h"
|
|
||||||
#include "../bad_ble_app_i.h"
|
|
||||||
#include "../views/bad_ble_view.h"
|
|
||||||
#include <furi_hal.h>
|
|
||||||
#include "toolbox/path.h"
|
|
||||||
|
|
||||||
void bad_ble_scene_work_button_callback(InputKey key, void* context) {
|
|
||||||
furi_assert(context);
|
|
||||||
BadBleApp* app = context;
|
|
||||||
view_dispatcher_send_custom_event(app->view_dispatcher, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool bad_ble_scene_work_on_event(void* context, SceneManagerEvent event) {
|
|
||||||
BadBleApp* app = context;
|
|
||||||
bool consumed = false;
|
|
||||||
|
|
||||||
if(event.type == SceneManagerEventTypeCustom) {
|
|
||||||
if(event.event == InputKeyLeft) {
|
|
||||||
if(bad_ble_view_is_idle_state(app->bad_ble_view)) {
|
|
||||||
bad_ble_script_close(app->bad_ble_script);
|
|
||||||
app->bad_ble_script = NULL;
|
|
||||||
|
|
||||||
scene_manager_next_scene(app->scene_manager, BadBleSceneConfig);
|
|
||||||
}
|
|
||||||
consumed = true;
|
|
||||||
} else if(event.event == InputKeyOk) {
|
|
||||||
bad_ble_script_start_stop(app->bad_ble_script);
|
|
||||||
consumed = true;
|
|
||||||
} else if(event.event == InputKeyRight) {
|
|
||||||
bad_ble_script_pause_resume(app->bad_ble_script);
|
|
||||||
consumed = true;
|
|
||||||
}
|
|
||||||
} else if(event.type == SceneManagerEventTypeTick) {
|
|
||||||
bad_ble_view_set_state(app->bad_ble_view, bad_ble_script_get_state(app->bad_ble_script));
|
|
||||||
}
|
|
||||||
return consumed;
|
|
||||||
}
|
|
||||||
|
|
||||||
void bad_ble_scene_work_on_enter(void* context) {
|
|
||||||
BadBleApp* app = context;
|
|
||||||
|
|
||||||
app->bad_ble_script = bad_ble_script_open(app->file_path, app->interface);
|
|
||||||
bad_ble_script_set_keyboard_layout(app->bad_ble_script, app->keyboard_layout);
|
|
||||||
|
|
||||||
FuriString* file_name;
|
|
||||||
file_name = furi_string_alloc();
|
|
||||||
path_extract_filename(app->file_path, file_name, true);
|
|
||||||
bad_ble_view_set_file_name(app->bad_ble_view, furi_string_get_cstr(file_name));
|
|
||||||
furi_string_free(file_name);
|
|
||||||
|
|
||||||
FuriString* layout;
|
|
||||||
layout = furi_string_alloc();
|
|
||||||
path_extract_filename(app->keyboard_layout, layout, true);
|
|
||||||
bad_ble_view_set_layout(app->bad_ble_view, furi_string_get_cstr(layout));
|
|
||||||
furi_string_free(layout);
|
|
||||||
|
|
||||||
bad_ble_view_set_state(app->bad_ble_view, bad_ble_script_get_state(app->bad_ble_script));
|
|
||||||
|
|
||||||
bad_ble_view_set_button_callback(app->bad_ble_view, bad_ble_scene_work_button_callback, app);
|
|
||||||
view_dispatcher_switch_to_view(app->view_dispatcher, BadBleAppViewWork);
|
|
||||||
}
|
|
||||||
|
|
||||||
void bad_ble_scene_work_on_exit(void* context) {
|
|
||||||
UNUSED(context);
|
|
||||||
}
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
#include "bad_ble_view.h"
|
|
||||||
#include "../helpers/ducky_script.h"
|
|
||||||
#include <toolbox/path.h>
|
|
||||||
#include <gui/elements.h>
|
|
||||||
#include <assets_icons.h>
|
|
||||||
#include "bad_ble_icons.h"
|
|
||||||
|
|
||||||
#define MAX_NAME_LEN 64
|
|
||||||
|
|
||||||
struct BadBle {
|
|
||||||
View* view;
|
|
||||||
BadBleButtonCallback callback;
|
|
||||||
void* context;
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
char file_name[MAX_NAME_LEN];
|
|
||||||
char layout[MAX_NAME_LEN];
|
|
||||||
BadBleState state;
|
|
||||||
bool pause_wait;
|
|
||||||
uint8_t anim_frame;
|
|
||||||
} BadBleModel;
|
|
||||||
|
|
||||||
static void bad_ble_draw_callback(Canvas* canvas, void* _model) {
|
|
||||||
BadBleModel* model = _model;
|
|
||||||
|
|
||||||
FuriString* disp_str;
|
|
||||||
disp_str = furi_string_alloc_set(model->file_name);
|
|
||||||
elements_string_fit_width(canvas, disp_str, 128 - 2);
|
|
||||||
canvas_set_font(canvas, FontSecondary);
|
|
||||||
canvas_draw_str(canvas, 2, 8, furi_string_get_cstr(disp_str));
|
|
||||||
|
|
||||||
if(strlen(model->layout) == 0) {
|
|
||||||
furi_string_set(disp_str, "(default)");
|
|
||||||
} else {
|
|
||||||
furi_string_printf(disp_str, "(%s)", model->layout);
|
|
||||||
}
|
|
||||||
elements_string_fit_width(canvas, disp_str, 128 - 2);
|
|
||||||
canvas_draw_str(
|
|
||||||
canvas, 2, 8 + canvas_current_font_height(canvas), furi_string_get_cstr(disp_str));
|
|
||||||
|
|
||||||
furi_string_reset(disp_str);
|
|
||||||
|
|
||||||
canvas_draw_icon(canvas, 22, 24, &I_Bad_BLE_48x22);
|
|
||||||
|
|
||||||
BadBleWorkerState state = model->state.state;
|
|
||||||
|
|
||||||
if((state == BadBleStateIdle) || (state == BadBleStateDone) ||
|
|
||||||
(state == BadBleStateNotConnected)) {
|
|
||||||
elements_button_center(canvas, "Run");
|
|
||||||
elements_button_left(canvas, "Config");
|
|
||||||
} else if((state == BadBleStateRunning) || (state == BadBleStateDelay)) {
|
|
||||||
elements_button_center(canvas, "Stop");
|
|
||||||
if(!model->pause_wait) {
|
|
||||||
elements_button_right(canvas, "Pause");
|
|
||||||
}
|
|
||||||
} else if(state == BadBleStatePaused) {
|
|
||||||
elements_button_center(canvas, "End");
|
|
||||||
elements_button_right(canvas, "Resume");
|
|
||||||
} else if(state == BadBleStateWaitForBtn) {
|
|
||||||
elements_button_center(canvas, "Press to continue");
|
|
||||||
} else if(state == BadBleStateWillRun) {
|
|
||||||
elements_button_center(canvas, "Cancel");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(state == BadBleStateNotConnected) {
|
|
||||||
canvas_draw_icon(canvas, 4, 26, &I_Clock_18x18);
|
|
||||||
canvas_set_font(canvas, FontPrimary);
|
|
||||||
canvas_draw_str_aligned(canvas, 127, 31, AlignRight, AlignBottom, "Connect");
|
|
||||||
canvas_draw_str_aligned(canvas, 127, 43, AlignRight, AlignBottom, "to device");
|
|
||||||
} else if(state == BadBleStateWillRun) {
|
|
||||||
canvas_draw_icon(canvas, 4, 26, &I_Clock_18x18);
|
|
||||||
canvas_set_font(canvas, FontPrimary);
|
|
||||||
canvas_draw_str_aligned(canvas, 127, 31, AlignRight, AlignBottom, "Will run");
|
|
||||||
canvas_draw_str_aligned(canvas, 127, 43, AlignRight, AlignBottom, "on connect");
|
|
||||||
} else if(state == BadBleStateFileError) {
|
|
||||||
canvas_draw_icon(canvas, 4, 26, &I_Error_18x18);
|
|
||||||
canvas_set_font(canvas, FontPrimary);
|
|
||||||
canvas_draw_str_aligned(canvas, 127, 31, AlignRight, AlignBottom, "File");
|
|
||||||
canvas_draw_str_aligned(canvas, 127, 43, AlignRight, AlignBottom, "ERROR");
|
|
||||||
} else if(state == BadBleStateScriptError) {
|
|
||||||
canvas_draw_icon(canvas, 4, 26, &I_Error_18x18);
|
|
||||||
canvas_set_font(canvas, FontPrimary);
|
|
||||||
canvas_draw_str_aligned(canvas, 127, 33, AlignRight, AlignBottom, "ERROR:");
|
|
||||||
canvas_set_font(canvas, FontSecondary);
|
|
||||||
furi_string_printf(disp_str, "line %zu", model->state.error_line);
|
|
||||||
canvas_draw_str_aligned(
|
|
||||||
canvas, 127, 46, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
|
|
||||||
furi_string_reset(disp_str);
|
|
||||||
|
|
||||||
furi_string_set_str(disp_str, model->state.error);
|
|
||||||
elements_string_fit_width(canvas, disp_str, canvas_width(canvas));
|
|
||||||
canvas_draw_str_aligned(
|
|
||||||
canvas, 127, 56, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
|
|
||||||
furi_string_reset(disp_str);
|
|
||||||
} else if(state == BadBleStateIdle) {
|
|
||||||
canvas_draw_icon(canvas, 4, 26, &I_Smile_18x18);
|
|
||||||
canvas_set_font(canvas, FontBigNumbers);
|
|
||||||
canvas_draw_str_aligned(canvas, 114, 40, AlignRight, AlignBottom, "0");
|
|
||||||
canvas_draw_icon(canvas, 117, 26, &I_Percent_10x14);
|
|
||||||
} else if(state == BadBleStateRunning) {
|
|
||||||
if(model->anim_frame == 0) {
|
|
||||||
canvas_draw_icon(canvas, 4, 23, &I_EviSmile1_18x21);
|
|
||||||
} else {
|
|
||||||
canvas_draw_icon(canvas, 4, 23, &I_EviSmile2_18x21);
|
|
||||||
}
|
|
||||||
canvas_set_font(canvas, FontBigNumbers);
|
|
||||||
furi_string_printf(
|
|
||||||
disp_str, "%zu", ((model->state.line_cur - 1) * 100) / model->state.line_nb);
|
|
||||||
canvas_draw_str_aligned(
|
|
||||||
canvas, 114, 40, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
|
|
||||||
furi_string_reset(disp_str);
|
|
||||||
canvas_draw_icon(canvas, 117, 26, &I_Percent_10x14);
|
|
||||||
} else if(state == BadBleStateDone) {
|
|
||||||
canvas_draw_icon(canvas, 4, 23, &I_EviSmile1_18x21);
|
|
||||||
canvas_set_font(canvas, FontBigNumbers);
|
|
||||||
canvas_draw_str_aligned(canvas, 114, 40, AlignRight, AlignBottom, "100");
|
|
||||||
furi_string_reset(disp_str);
|
|
||||||
canvas_draw_icon(canvas, 117, 26, &I_Percent_10x14);
|
|
||||||
} else if(state == BadBleStateDelay) {
|
|
||||||
if(model->anim_frame == 0) {
|
|
||||||
canvas_draw_icon(canvas, 4, 23, &I_EviWaiting1_18x21);
|
|
||||||
} else {
|
|
||||||
canvas_draw_icon(canvas, 4, 23, &I_EviWaiting2_18x21);
|
|
||||||
}
|
|
||||||
canvas_set_font(canvas, FontBigNumbers);
|
|
||||||
furi_string_printf(
|
|
||||||
disp_str, "%zu", ((model->state.line_cur - 1) * 100) / model->state.line_nb);
|
|
||||||
canvas_draw_str_aligned(
|
|
||||||
canvas, 114, 40, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
|
|
||||||
furi_string_reset(disp_str);
|
|
||||||
canvas_draw_icon(canvas, 117, 26, &I_Percent_10x14);
|
|
||||||
canvas_set_font(canvas, FontSecondary);
|
|
||||||
furi_string_printf(disp_str, "delay %lus", model->state.delay_remain);
|
|
||||||
canvas_draw_str_aligned(
|
|
||||||
canvas, 127, 50, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
|
|
||||||
furi_string_reset(disp_str);
|
|
||||||
} else if((state == BadBleStatePaused) || (state == BadBleStateWaitForBtn)) {
|
|
||||||
if(model->anim_frame == 0) {
|
|
||||||
canvas_draw_icon(canvas, 4, 23, &I_EviWaiting1_18x21);
|
|
||||||
} else {
|
|
||||||
canvas_draw_icon(canvas, 4, 23, &I_EviWaiting2_18x21);
|
|
||||||
}
|
|
||||||
canvas_set_font(canvas, FontBigNumbers);
|
|
||||||
furi_string_printf(
|
|
||||||
disp_str, "%zu", ((model->state.line_cur - 1) * 100) / model->state.line_nb);
|
|
||||||
canvas_draw_str_aligned(
|
|
||||||
canvas, 114, 40, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
|
|
||||||
furi_string_reset(disp_str);
|
|
||||||
canvas_draw_icon(canvas, 117, 26, &I_Percent_10x14);
|
|
||||||
canvas_set_font(canvas, FontSecondary);
|
|
||||||
canvas_draw_str_aligned(canvas, 127, 50, AlignRight, AlignBottom, "Paused");
|
|
||||||
furi_string_reset(disp_str);
|
|
||||||
} else {
|
|
||||||
canvas_draw_icon(canvas, 4, 26, &I_Clock_18x18);
|
|
||||||
}
|
|
||||||
|
|
||||||
furi_string_free(disp_str);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool bad_ble_input_callback(InputEvent* event, void* context) {
|
|
||||||
furi_assert(context);
|
|
||||||
BadBle* bad_ble = context;
|
|
||||||
bool consumed = false;
|
|
||||||
|
|
||||||
if(event->type == InputTypeShort) {
|
|
||||||
if(event->key == InputKeyLeft) {
|
|
||||||
consumed = true;
|
|
||||||
furi_assert(bad_ble->callback);
|
|
||||||
bad_ble->callback(event->key, bad_ble->context);
|
|
||||||
} else if(event->key == InputKeyOk) {
|
|
||||||
with_view_model(
|
|
||||||
bad_ble->view, BadBleModel * model, { model->pause_wait = false; }, true);
|
|
||||||
consumed = true;
|
|
||||||
furi_assert(bad_ble->callback);
|
|
||||||
bad_ble->callback(event->key, bad_ble->context);
|
|
||||||
} else if(event->key == InputKeyRight) {
|
|
||||||
with_view_model(
|
|
||||||
bad_ble->view,
|
|
||||||
BadBleModel * model,
|
|
||||||
{
|
|
||||||
if((model->state.state == BadBleStateRunning) ||
|
|
||||||
(model->state.state == BadBleStateDelay)) {
|
|
||||||
model->pause_wait = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
true);
|
|
||||||
consumed = true;
|
|
||||||
furi_assert(bad_ble->callback);
|
|
||||||
bad_ble->callback(event->key, bad_ble->context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return consumed;
|
|
||||||
}
|
|
||||||
|
|
||||||
BadBle* bad_ble_view_alloc(void) {
|
|
||||||
BadBle* bad_ble = malloc(sizeof(BadBle));
|
|
||||||
|
|
||||||
bad_ble->view = view_alloc();
|
|
||||||
view_allocate_model(bad_ble->view, ViewModelTypeLocking, sizeof(BadBleModel));
|
|
||||||
view_set_context(bad_ble->view, bad_ble);
|
|
||||||
view_set_draw_callback(bad_ble->view, bad_ble_draw_callback);
|
|
||||||
view_set_input_callback(bad_ble->view, bad_ble_input_callback);
|
|
||||||
|
|
||||||
return bad_ble;
|
|
||||||
}
|
|
||||||
|
|
||||||
void bad_ble_view_free(BadBle* bad_ble) {
|
|
||||||
furi_assert(bad_ble);
|
|
||||||
view_free(bad_ble->view);
|
|
||||||
free(bad_ble);
|
|
||||||
}
|
|
||||||
|
|
||||||
View* bad_ble_view_get_view(BadBle* bad_ble) {
|
|
||||||
furi_assert(bad_ble);
|
|
||||||
return bad_ble->view;
|
|
||||||
}
|
|
||||||
|
|
||||||
void bad_ble_view_set_button_callback(
|
|
||||||
BadBle* bad_ble,
|
|
||||||
BadBleButtonCallback callback,
|
|
||||||
void* context) {
|
|
||||||
furi_assert(bad_ble);
|
|
||||||
furi_assert(callback);
|
|
||||||
with_view_model(
|
|
||||||
bad_ble->view,
|
|
||||||
BadBleModel * model,
|
|
||||||
{
|
|
||||||
UNUSED(model);
|
|
||||||
bad_ble->callback = callback;
|
|
||||||
bad_ble->context = context;
|
|
||||||
},
|
|
||||||
true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void bad_ble_view_set_file_name(BadBle* bad_ble, const char* name) {
|
|
||||||
furi_assert(name);
|
|
||||||
with_view_model(
|
|
||||||
bad_ble->view,
|
|
||||||
BadBleModel * model,
|
|
||||||
{ strlcpy(model->file_name, name, MAX_NAME_LEN); },
|
|
||||||
true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void bad_ble_view_set_layout(BadBle* bad_ble, const char* layout) {
|
|
||||||
furi_assert(layout);
|
|
||||||
with_view_model(
|
|
||||||
bad_ble->view,
|
|
||||||
BadBleModel * model,
|
|
||||||
{ strlcpy(model->layout, layout, MAX_NAME_LEN); },
|
|
||||||
true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void bad_ble_view_set_state(BadBle* bad_ble, BadBleState* st) {
|
|
||||||
furi_assert(st);
|
|
||||||
with_view_model(
|
|
||||||
bad_ble->view,
|
|
||||||
BadBleModel * model,
|
|
||||||
{
|
|
||||||
memcpy(&(model->state), st, sizeof(BadBleState));
|
|
||||||
model->anim_frame ^= 1;
|
|
||||||
if(model->state.state == BadBleStatePaused) {
|
|
||||||
model->pause_wait = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
true);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool bad_ble_view_is_idle_state(BadBle* bad_ble) {
|
|
||||||
bool is_idle = false;
|
|
||||||
with_view_model(
|
|
||||||
bad_ble->view,
|
|
||||||
BadBleModel * model,
|
|
||||||
{
|
|
||||||
if((model->state.state == BadBleStateIdle) ||
|
|
||||||
(model->state.state == BadBleStateDone) ||
|
|
||||||
(model->state.state == BadBleStateNotConnected)) {
|
|
||||||
is_idle = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
false);
|
|
||||||
return is_idle;
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <gui/view.h>
|
|
||||||
#include "../helpers/ducky_script.h"
|
|
||||||
|
|
||||||
typedef struct BadBle BadBle;
|
|
||||||
typedef void (*BadBleButtonCallback)(InputKey key, void* context);
|
|
||||||
|
|
||||||
BadBle* bad_ble_view_alloc(void);
|
|
||||||
|
|
||||||
void bad_ble_view_free(BadBle* bad_ble);
|
|
||||||
|
|
||||||
View* bad_ble_view_get_view(BadBle* bad_ble);
|
|
||||||
|
|
||||||
void bad_ble_view_set_button_callback(
|
|
||||||
BadBle* bad_ble,
|
|
||||||
BadBleButtonCallback callback,
|
|
||||||
void* context);
|
|
||||||
|
|
||||||
void bad_ble_view_set_file_name(BadBle* bad_ble, const char* name);
|
|
||||||
|
|
||||||
void bad_ble_view_set_layout(BadBle* bad_ble, const char* layout);
|
|
||||||
|
|
||||||
void bad_ble_view_set_state(BadBle* bad_ble, BadBleState* st);
|
|
||||||
|
|
||||||
bool bad_ble_view_is_idle_state(BadBle* bad_ble);
|
|
||||||
@@ -16,11 +16,70 @@ App(
|
|||||||
)
|
)
|
||||||
|
|
||||||
App(
|
App(
|
||||||
appid="js_dialog",
|
appid="js_event_loop",
|
||||||
apptype=FlipperAppType.PLUGIN,
|
apptype=FlipperAppType.PLUGIN,
|
||||||
entry_point="js_dialog_ep",
|
entry_point="js_event_loop_ep",
|
||||||
requires=["js_app"],
|
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(
|
App(
|
||||||
@@ -48,11 +107,11 @@ App(
|
|||||||
)
|
)
|
||||||
|
|
||||||
App(
|
App(
|
||||||
appid="js_submenu",
|
appid="js_gpio",
|
||||||
apptype=FlipperAppType.PLUGIN,
|
apptype=FlipperAppType.PLUGIN,
|
||||||
entry_point="js_submenu_ep",
|
entry_point="js_gpio_ep",
|
||||||
requires=["js_app"],
|
requires=["js_app", "js_event_loop"],
|
||||||
sources=["modules/js_submenu.c"],
|
sources=["modules/js_gpio.c"],
|
||||||
)
|
)
|
||||||
|
|
||||||
App(
|
App(
|
||||||
@@ -64,9 +123,9 @@ App(
|
|||||||
)
|
)
|
||||||
|
|
||||||
App(
|
App(
|
||||||
appid="js_textbox",
|
appid="js_storage",
|
||||||
apptype=FlipperAppType.PLUGIN,
|
apptype=FlipperAppType.PLUGIN,
|
||||||
entry_point="js_textbox_ep",
|
entry_point="js_storage_ep",
|
||||||
requires=["js_app"],
|
requires=["js_app"],
|
||||||
sources=["modules/js_textbox.c"],
|
sources=["modules/js_storage.c"],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,33 +1,58 @@
|
|||||||
let badusb = require("badusb");
|
let badusb = require("badusb");
|
||||||
let notify = require("notification");
|
let notify = require("notification");
|
||||||
let flipper = require("flipper");
|
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" });
|
let views = {
|
||||||
dialog.message("BadUSB demo", "Press OK to start");
|
dialog: dialog.makeWith({
|
||||||
|
header: "BadUSB demo",
|
||||||
|
text: "Press OK to start",
|
||||||
|
center: "Start",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
if (badusb.isConnected()) {
|
badusb.setup({ vid: 0xAAAA, pid: 0xBBBB, mfrName: "Flipper", prodName: "Zero" });
|
||||||
notify.blink("green", "short");
|
|
||||||
print("USB is connected");
|
|
||||||
|
|
||||||
badusb.println("Hello, world!");
|
eventLoop.subscribe(views.dialog.input, function (_sub, button, eventLoop, gui) {
|
||||||
|
if (button !== "center")
|
||||||
|
return;
|
||||||
|
|
||||||
badusb.press("CTRL", "a");
|
gui.viewDispatcher.sendTo("back");
|
||||||
badusb.press("CTRL", "c");
|
|
||||||
badusb.press("DOWN");
|
|
||||||
delay(1000);
|
|
||||||
badusb.press("CTRL", "v");
|
|
||||||
delay(1000);
|
|
||||||
badusb.press("CTRL", "v");
|
|
||||||
|
|
||||||
badusb.println("1234", 200);
|
if (badusb.isConnected()) {
|
||||||
|
notify.blink("green", "short");
|
||||||
|
print("USB is connected");
|
||||||
|
|
||||||
badusb.println("Flipper Model: " + flipper.getModel());
|
badusb.println("Hello, world!");
|
||||||
badusb.println("Flipper Name: " + flipper.getName());
|
|
||||||
badusb.println("Battery level: " + to_string(flipper.getBatteryCharge()) + "%");
|
|
||||||
|
|
||||||
notify.success();
|
badusb.press("CTRL", "a");
|
||||||
} else {
|
badusb.press("CTRL", "c");
|
||||||
print("USB not connected");
|
badusb.press("DOWN");
|
||||||
notify.error();
|
delay(1000);
|
||||||
}
|
badusb.press("CTRL", "v");
|
||||||
|
delay(1000);
|
||||||
|
badusb.press("CTRL", "v");
|
||||||
|
|
||||||
|
badusb.println("1234", 200);
|
||||||
|
|
||||||
|
badusb.println("Flipper Model: " + flipper.getModel());
|
||||||
|
badusb.println("Flipper Name: " + flipper.getName());
|
||||||
|
badusb.println("Battery level: " + toString(flipper.getBatteryCharge()) + "%");
|
||||||
|
|
||||||
|
notify.success();
|
||||||
|
} else {
|
||||||
|
print("USB not connected");
|
||||||
|
notify.error();
|
||||||
|
}
|
||||||
|
|
||||||
|
eventLoop.stop();
|
||||||
|
}, eventLoop, gui);
|
||||||
|
|
||||||
|
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _item, eventLoop) {
|
||||||
|
eventLoop.stop();
|
||||||
|
}, eventLoop);
|
||||||
|
|
||||||
|
gui.viewDispatcher.switchTo(views.dialog);
|
||||||
|
eventLoop.run();
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ print("2");
|
|||||||
delay(1000)
|
delay(1000)
|
||||||
print("3");
|
print("3");
|
||||||
delay(1000)
|
delay(1000)
|
||||||
print("end");
|
print("end");
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
let dialog = require("dialog");
|
|
||||||
|
|
||||||
let result1 = dialog.message("Dialog demo", "Press OK to start");
|
|
||||||
print(result1);
|
|
||||||
|
|
||||||
let dialog_params = ({
|
|
||||||
header: "Test_header",
|
|
||||||
text: "Test_text",
|
|
||||||
button_left: "Left",
|
|
||||||
button_right: "Right",
|
|
||||||
button_center: "OK"
|
|
||||||
});
|
|
||||||
|
|
||||||
let result2 = dialog.custom(dialog_params);
|
|
||||||
if (result2 === "") {
|
|
||||||
print("Back is pressed");
|
|
||||||
} else {
|
|
||||||
print(result2, "is pressed");
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
let eventLoop = require("event_loop");
|
||||||
|
|
||||||
|
// print a string after 1337 milliseconds
|
||||||
|
eventLoop.subscribe(eventLoop.timer("oneshot", 1337), function (_subscription, _item) {
|
||||||
|
print("Hi after 1337 ms");
|
||||||
|
});
|
||||||
|
|
||||||
|
// count up to 5 with a delay of 100ms between increments
|
||||||
|
eventLoop.subscribe(eventLoop.timer("periodic", 100), function (subscription, _item, counter) {
|
||||||
|
print("Counter two:", counter);
|
||||||
|
if (counter === 5)
|
||||||
|
subscription.cancel();
|
||||||
|
return [counter + 1];
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// count up to 15 with a delay of 100ms between increments
|
||||||
|
// and stop the program when the count reaches 15
|
||||||
|
eventLoop.subscribe(eventLoop.timer("periodic", 100), function (subscription, _item, event_loop, counter) {
|
||||||
|
print("Counter one:", counter);
|
||||||
|
if (counter === 15)
|
||||||
|
event_loop.stop();
|
||||||
|
return [event_loop, counter + 1];
|
||||||
|
}, eventLoop, 0);
|
||||||
|
|
||||||
|
eventLoop.run();
|
||||||
57
applications/system/js_app/examples/apps/Scripts/gpio.js
Normal file
57
applications/system/js_app/examples/apps/Scripts/gpio.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
let eventLoop = require("event_loop");
|
||||||
|
let gpio = require("gpio");
|
||||||
|
|
||||||
|
// initialize pins
|
||||||
|
let led = gpio.get("pc3"); // same as `gpio.get(7)`
|
||||||
|
let pot = gpio.get("pc0"); // same as `gpio.get(16)`
|
||||||
|
let button = gpio.get("pc1"); // same as `gpio.get(15)`
|
||||||
|
led.init({ direction: "out", outMode: "push_pull" });
|
||||||
|
pot.init({ direction: "in", inMode: "analog" });
|
||||||
|
button.init({ direction: "in", pull: "up", inMode: "interrupt", edge: "falling" });
|
||||||
|
|
||||||
|
// blink led
|
||||||
|
print("Commencing blinking (PC3)");
|
||||||
|
eventLoop.subscribe(eventLoop.timer("periodic", 1000), function (_, _item, led, state) {
|
||||||
|
led.write(state);
|
||||||
|
return [led, !state];
|
||||||
|
}, led, true);
|
||||||
|
|
||||||
|
// read potentiometer when button is pressed
|
||||||
|
print("Press the button (PC1)");
|
||||||
|
eventLoop.subscribe(button.interrupt(), function (_, _item, pot) {
|
||||||
|
print("PC0 is at", pot.read_analog(), "mV");
|
||||||
|
}, pot);
|
||||||
|
|
||||||
|
// the program will just exit unless this is here
|
||||||
|
eventLoop.run();
|
||||||
|
|
||||||
|
// possible pins https://docs.flipper.net/gpio-and-modules#miFsS
|
||||||
|
// "PA7" aka 2
|
||||||
|
// "PA6" aka 3
|
||||||
|
// "PA4" aka 4
|
||||||
|
// "PB3" aka 5
|
||||||
|
// "PB2" aka 6
|
||||||
|
// "PC3" aka 7
|
||||||
|
// "PA14" aka 10
|
||||||
|
// "PA13" aka 12
|
||||||
|
// "PB6" aka 13
|
||||||
|
// "PB7" aka 14
|
||||||
|
// "PC1" aka 15
|
||||||
|
// "PC0" aka 16
|
||||||
|
// "PB14" aka 17
|
||||||
|
|
||||||
|
// possible modes
|
||||||
|
// { direction: "out", outMode: "push_pull" }
|
||||||
|
// { direction: "out", outMode: "open_drain" }
|
||||||
|
// { direction: "out", outMode: "push_pull", altFn: true }
|
||||||
|
// { direction: "out", outMode: "open_drain", altFn: true }
|
||||||
|
// { direction: "in", inMode: "analog" }
|
||||||
|
// { direction: "in", inMode: "plain_digital" }
|
||||||
|
// { direction: "in", inMode: "interrupt", edge: "rising" }
|
||||||
|
// { direction: "in", inMode: "interrupt", edge: "falling" }
|
||||||
|
// { direction: "in", inMode: "interrupt", edge: "both" }
|
||||||
|
// { direction: "in", inMode: "event", edge: "rising" }
|
||||||
|
// { direction: "in", inMode: "event", edge: "falling" }
|
||||||
|
// { direction: "in", inMode: "event", edge: "both" }
|
||||||
|
// all variants support an optional `pull` field which can either be undefined,
|
||||||
|
// "up" or "down"
|
||||||
77
applications/system/js_app/examples/apps/Scripts/gui.js
Normal file
77
applications/system/js_app/examples/apps/Scripts/gui.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// import modules
|
||||||
|
let eventLoop = require("event_loop");
|
||||||
|
let gui = require("gui");
|
||||||
|
let loadingView = require("gui/loading");
|
||||||
|
let submenuView = require("gui/submenu");
|
||||||
|
let emptyView = require("gui/empty_screen");
|
||||||
|
let textInputView = require("gui/text_input");
|
||||||
|
let textBoxView = require("gui/text_box");
|
||||||
|
let dialogView = require("gui/dialog");
|
||||||
|
|
||||||
|
// declare view instances
|
||||||
|
let views = {
|
||||||
|
loading: loadingView.make(),
|
||||||
|
empty: emptyView.make(),
|
||||||
|
keyboard: textInputView.makeWith({
|
||||||
|
header: "Enter your name",
|
||||||
|
minLength: 0,
|
||||||
|
maxLength: 32,
|
||||||
|
}),
|
||||||
|
helloDialog: dialogView.makeWith({
|
||||||
|
center: "Hi Flipper! :)",
|
||||||
|
}),
|
||||||
|
longText: textBoxView.makeWith({
|
||||||
|
text: "This is a very long string that demonstrates the TextBox view. Use the D-Pad to scroll backwards and forwards.\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rhoncus est malesuada quam egestas ultrices. Maecenas non eros a nulla eleifend vulputate et ut risus. Quisque in mauris mattis, venenatis risus eget, aliquam diam. Fusce pretium feugiat mauris, ut faucibus ex volutpat in. Phasellus volutpat ex sed gravida consectetur. Aliquam sed lectus feugiat, tristique lectus et, bibendum lacus. Ut sit amet augue eu sapien elementum aliquam quis vitae tortor. Vestibulum quis commodo odio. In elementum fermentum massa, eu pellentesque nibh cursus at. Integer eleifend lacus nec purus elementum sodales. Nulla elementum neque urna, non vulputate massa semper sed. Fusce ut nisi vitae dui blandit congue pretium vitae turpis.",
|
||||||
|
}),
|
||||||
|
demos: submenuView.makeWith({
|
||||||
|
header: "Choose a demo",
|
||||||
|
items: [
|
||||||
|
"Hourglass screen",
|
||||||
|
"Empty screen",
|
||||||
|
"Text input & Dialog",
|
||||||
|
"Text box",
|
||||||
|
"Exit app",
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// demo selector
|
||||||
|
eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, views) {
|
||||||
|
if (index === 0) {
|
||||||
|
gui.viewDispatcher.switchTo(views.loading);
|
||||||
|
// the loading view captures all back events, preventing our navigation callback from firing
|
||||||
|
// switch to the demo chooser after a second
|
||||||
|
eventLoop.subscribe(eventLoop.timer("oneshot", 1000), function (_sub, _, gui, views) {
|
||||||
|
gui.viewDispatcher.switchTo(views.demos);
|
||||||
|
}, gui, views);
|
||||||
|
} else if (index === 1) {
|
||||||
|
gui.viewDispatcher.switchTo(views.empty);
|
||||||
|
} else if (index === 2) {
|
||||||
|
gui.viewDispatcher.switchTo(views.keyboard);
|
||||||
|
} else if (index === 3) {
|
||||||
|
gui.viewDispatcher.switchTo(views.longText);
|
||||||
|
} else if (index === 4) {
|
||||||
|
eventLoop.stop();
|
||||||
|
}
|
||||||
|
}, gui, eventLoop, views);
|
||||||
|
|
||||||
|
// say hi after keyboard input
|
||||||
|
eventLoop.subscribe(views.keyboard.input, function (_sub, name, gui, views) {
|
||||||
|
views.helloDialog.set("text", "Hi " + name + "! :)");
|
||||||
|
gui.viewDispatcher.switchTo(views.helloDialog);
|
||||||
|
}, gui, views);
|
||||||
|
|
||||||
|
// go back after the greeting dialog
|
||||||
|
eventLoop.subscribe(views.helloDialog.input, function (_sub, button, gui, views) {
|
||||||
|
if (button === "center")
|
||||||
|
gui.viewDispatcher.switchTo(views.demos);
|
||||||
|
}, gui, views);
|
||||||
|
|
||||||
|
// go to the demo chooser screen when the back key is pressed
|
||||||
|
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, gui, views) {
|
||||||
|
gui.viewDispatcher.switchTo(views.demos);
|
||||||
|
}, gui, views);
|
||||||
|
|
||||||
|
// run UI
|
||||||
|
gui.viewDispatcher.switchTo(views.demos);
|
||||||
|
eventLoop.run();
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
let math = load("/ext/apps/Scripts/load_api.js");
|
let math = load("/ext/apps/Scripts/load_api.js");
|
||||||
let result = math.add(5, 10);
|
let result = math.add(5, 10);
|
||||||
print(result);
|
print(result);
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
({
|
({
|
||||||
add: function (a, b) { return a + b; },
|
add: function (a, b) { return a + b; },
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -22,48 +22,3 @@ print("math.sign(-5):", math.sign(-5));
|
|||||||
print("math.sin(math.PI/2):", math.sin(math.PI / 2));
|
print("math.sin(math.PI/2):", math.sin(math.PI / 2));
|
||||||
print("math.sqrt(25):", math.sqrt(25));
|
print("math.sqrt(25):", math.sqrt(25));
|
||||||
print("math.trunc(5.7):", math.trunc(5.7));
|
print("math.trunc(5.7):", math.trunc(5.7));
|
||||||
|
|
||||||
// Unit tests. Please add more if you have time and knowledge.
|
|
||||||
// math.EPSILON on Flipper Zero is 2.22044604925031308085e-16
|
|
||||||
|
|
||||||
let succeeded = 0;
|
|
||||||
let failed = 0;
|
|
||||||
|
|
||||||
function test(text, result, expected, epsilon) {
|
|
||||||
let is_equal = math.is_equal(result, expected, epsilon);
|
|
||||||
if (is_equal) {
|
|
||||||
succeeded += 1;
|
|
||||||
} else {
|
|
||||||
failed += 1;
|
|
||||||
print(text, "expected", expected, "got", result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test("math.abs(5)", math.abs(-5), 5, math.EPSILON);
|
|
||||||
test("math.abs(0.5)", math.abs(-0.5), 0.5, math.EPSILON);
|
|
||||||
test("math.abs(5)", math.abs(5), 5, math.EPSILON);
|
|
||||||
test("math.abs(-0.5)", math.abs(0.5), 0.5, math.EPSILON);
|
|
||||||
test("math.acos(0.5)", math.acos(0.5), 1.0471975511965976, math.EPSILON);
|
|
||||||
test("math.acosh(2)", math.acosh(2), 1.3169578969248166, math.EPSILON);
|
|
||||||
test("math.asin(0.5)", math.asin(0.5), 0.5235987755982988, math.EPSILON);
|
|
||||||
test("math.asinh(2)", math.asinh(2), 1.4436354751788103, math.EPSILON);
|
|
||||||
test("math.atan(1)", math.atan(1), 0.7853981633974483, math.EPSILON);
|
|
||||||
test("math.atan2(1, 1)", math.atan2(1, 1), 0.7853981633974483, math.EPSILON);
|
|
||||||
test("math.atanh(0.5)", math.atanh(0.5), 0.5493061443340549, math.EPSILON);
|
|
||||||
test("math.cbrt(27)", math.cbrt(27), 3, math.EPSILON);
|
|
||||||
test("math.ceil(5.3)", math.ceil(5.3), 6, math.EPSILON);
|
|
||||||
test("math.clz32(1)", math.clz32(1), 31, math.EPSILON);
|
|
||||||
test("math.floor(5.7)", math.floor(5.7), 5, math.EPSILON);
|
|
||||||
test("math.max(3, 5)", math.max(3, 5), 5, math.EPSILON);
|
|
||||||
test("math.min(3, 5)", math.min(3, 5), 3, math.EPSILON);
|
|
||||||
test("math.pow(2, 3)", math.pow(2, 3), 8, math.EPSILON);
|
|
||||||
test("math.sign(-5)", math.sign(-5), -1, math.EPSILON);
|
|
||||||
test("math.sqrt(25)", math.sqrt(25), 5, math.EPSILON);
|
|
||||||
test("math.trunc(5.7)", math.trunc(5.7), 5, math.EPSILON);
|
|
||||||
test("math.cos(math.PI)", math.cos(math.PI), -1, math.EPSILON * 18); // Error 3.77475828372553223744e-15
|
|
||||||
test("math.exp(1)", math.exp(1), 2.718281828459045, math.EPSILON * 2); // Error 4.44089209850062616169e-16
|
|
||||||
test("math.sin(math.PI / 2)", math.sin(math.PI / 2), 1, math.EPSILON * 4.5); // Error 9.99200722162640886381e-16
|
|
||||||
|
|
||||||
if (failed > 0) {
|
|
||||||
print("!!!", failed, "Unit tests failed !!!");
|
|
||||||
}
|
|
||||||
@@ -6,4 +6,4 @@ delay(1000);
|
|||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
notify.blink("red", "short");
|
notify.blink("red", "short");
|
||||||
delay(500);
|
delay(500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
let submenu = require("submenu");
|
|
||||||
|
|
||||||
submenu.addItem("Item 1", 0);
|
|
||||||
submenu.addItem("Item 2", 1);
|
|
||||||
submenu.addItem("Item 3", 2);
|
|
||||||
|
|
||||||
submenu.setHeader("Select an option:");
|
|
||||||
|
|
||||||
let result = submenu.show();
|
|
||||||
// Returns undefined when pressing back
|
|
||||||
print("Result:", result);
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
let textbox = require("textbox");
|
|
||||||
|
|
||||||
// You should set config before adding text
|
|
||||||
// Focus (start / end), Font (text / hex)
|
|
||||||
textbox.setConfig("end", "text");
|
|
||||||
|
|
||||||
// Can make sure it's cleared before showing, in case of reusing in same script
|
|
||||||
// (Closing textbox already clears the text, but maybe you added more in a loop for example)
|
|
||||||
textbox.clearText();
|
|
||||||
|
|
||||||
// Add default text
|
|
||||||
textbox.addText("Example dynamic updating textbox\n");
|
|
||||||
|
|
||||||
// Non-blocking, can keep updating text after, can close in JS or in GUI
|
|
||||||
textbox.show();
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
while (textbox.isOpen() && i < 20) {
|
|
||||||
print("console", i++);
|
|
||||||
|
|
||||||
// Add text to textbox buffer
|
|
||||||
textbox.addText("textbox " + to_string(i) + "\n");
|
|
||||||
|
|
||||||
delay(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not closed by user (instead i < 20 is false above), close forcefully
|
|
||||||
if (textbox.isOpen()) {
|
|
||||||
textbox.close();
|
|
||||||
}
|
|
||||||
@@ -2,10 +2,10 @@ let serial = require("serial");
|
|||||||
serial.setup("usart", 230400);
|
serial.setup("usart", 230400);
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
let rx_data = serial.readBytes(1, 0);
|
let rx_data = serial.readBytes(1, 1000);
|
||||||
if (rx_data !== undefined) {
|
if (rx_data !== undefined) {
|
||||||
serial.write(rx_data);
|
serial.write(rx_data);
|
||||||
let data_view = Uint8Array(rx_data);
|
let data_view = Uint8Array(rx_data);
|
||||||
print("0x" + to_hex_string(data_view[0]));
|
print("0x" + toString(data_view[0], 16));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ int32_t js_app(void* arg) {
|
|||||||
FuriString* start_text =
|
FuriString* start_text =
|
||||||
furi_string_alloc_printf("Running %s", furi_string_get_cstr(name));
|
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, 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(name);
|
||||||
furi_string_free(start_text);
|
furi_string_free(start_text);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
#include <core/common_defines.h>
|
#include <core/common_defines.h>
|
||||||
#include "js_modules.h"
|
#include "js_modules.h"
|
||||||
#include <m-dict.h>
|
#include <m-array.h>
|
||||||
|
|
||||||
#include "modules/js_flipper.h"
|
#include "modules/js_flipper.h"
|
||||||
|
#ifdef FW_CFG_unit_tests
|
||||||
|
#include "modules/js_tests.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#define TAG "JS modules"
|
#define TAG "JS modules"
|
||||||
|
|
||||||
@@ -9,54 +13,72 @@
|
|||||||
#define MODULES_PATH "/ext/apps_data/js_app/plugins"
|
#define MODULES_PATH "/ext/apps_data/js_app/plugins"
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
JsModeConstructor create;
|
FuriString* name;
|
||||||
JsModeDestructor destroy;
|
const JsModuleConstructor create;
|
||||||
|
const JsModuleDestructor destroy;
|
||||||
void* context;
|
void* context;
|
||||||
} JsModuleData;
|
} 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[] = {
|
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 JsModules {
|
||||||
struct mjs* mjs;
|
struct mjs* mjs;
|
||||||
JsModuleDict_t module_dict;
|
JsModuleArray_t modules;
|
||||||
PluginManager* plugin_manager;
|
PluginManager* plugin_manager;
|
||||||
|
CompositeApiResolver* resolver;
|
||||||
};
|
};
|
||||||
|
|
||||||
JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver) {
|
JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver) {
|
||||||
JsModules* modules = malloc(sizeof(JsModules));
|
JsModules* modules = malloc(sizeof(JsModules));
|
||||||
modules->mjs = mjs;
|
modules->mjs = mjs;
|
||||||
JsModuleDict_init(modules->module_dict);
|
JsModuleArray_init(modules->modules);
|
||||||
|
|
||||||
modules->plugin_manager = plugin_manager_alloc(
|
modules->plugin_manager = plugin_manager_alloc(
|
||||||
PLUGIN_APP_ID, PLUGIN_API_VERSION, composite_api_resolver_get(resolver));
|
PLUGIN_APP_ID, PLUGIN_API_VERSION, composite_api_resolver_get(resolver));
|
||||||
|
|
||||||
|
modules->resolver = resolver;
|
||||||
|
|
||||||
return modules;
|
return modules;
|
||||||
}
|
}
|
||||||
|
|
||||||
void js_modules_destroy(JsModules* modules) {
|
void js_modules_destroy(JsModules* instance) {
|
||||||
JsModuleDict_it_t it;
|
for
|
||||||
for(JsModuleDict_it(it, modules->module_dict); !JsModuleDict_end_p(it);
|
M_EACH(module, instance->modules, JsModuleArray_t) {
|
||||||
JsModuleDict_next(it)) {
|
FURI_LOG_T(TAG, "Tearing down %s", furi_string_get_cstr(module->name));
|
||||||
const JsModuleDict_itref_t* module_itref = JsModuleDict_cref(it);
|
if(module->destroy) module->destroy(module->context);
|
||||||
if(module_itref->value.destroy) {
|
furi_string_free(module->name);
|
||||||
module_itref->value.destroy(module_itref->value.context);
|
|
||||||
}
|
}
|
||||||
}
|
plugin_manager_free(instance->plugin_manager);
|
||||||
plugin_manager_free(modules->plugin_manager);
|
JsModuleArray_clear(instance->modules);
|
||||||
JsModuleDict_clear(modules->module_dict);
|
free(instance);
|
||||||
free(modules);
|
}
|
||||||
|
|
||||||
|
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) {
|
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
|
// 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
|
if(module_inst) { //-V547
|
||||||
furi_string_free(module_name);
|
|
||||||
mjs_prepend_errorf(
|
mjs_prepend_errorf(
|
||||||
modules->mjs, MJS_BAD_ARGS_ERROR, "\"%s\" module is already installed", name);
|
modules->mjs, MJS_BAD_ARGS_ERROR, "\"%s\" module is already installed", name);
|
||||||
return MJS_UNDEFINED;
|
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) {
|
if(strncmp(name, modules_builtin[i].name, name_compare_len) == 0) {
|
||||||
JsModuleData module = {
|
JsModuleData module = {
|
||||||
.create = modules_builtin[i].create, .destroy = modules_builtin[i].destroy};
|
.create = modules_builtin[i].create,
|
||||||
JsModuleDict_set_at(modules->module_dict, module_name, module);
|
.destroy = modules_builtin[i].destroy,
|
||||||
|
.name = furi_string_alloc_set_str(name),
|
||||||
|
};
|
||||||
|
JsModuleArray_push_at(modules->modules, 0, module);
|
||||||
module_found = true;
|
module_found = true;
|
||||||
FURI_LOG_I(TAG, "Using built-in module %s", name);
|
FURI_LOG_I(TAG, "Using built-in module %s", name);
|
||||||
break;
|
break;
|
||||||
@@ -83,39 +108,57 @@ mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_le
|
|||||||
|
|
||||||
// External module load
|
// External module load
|
||||||
if(!module_found) {
|
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();
|
FuriString* module_path = furi_string_alloc();
|
||||||
furi_string_printf(module_path, "%s/js_%s.fal", MODULES_PATH, name);
|
furi_string_printf(
|
||||||
FURI_LOG_I(TAG, "Loading external module %s", furi_string_get_cstr(module_path));
|
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 {
|
do {
|
||||||
uint32_t plugin_cnt_last = plugin_manager_get_count(modules->plugin_manager);
|
uint32_t plugin_cnt_last = plugin_manager_get_count(modules->plugin_manager);
|
||||||
PluginManagerError load_error = plugin_manager_load_single(
|
PluginManagerError load_error = plugin_manager_load_single(
|
||||||
modules->plugin_manager, furi_string_get_cstr(module_path));
|
modules->plugin_manager, furi_string_get_cstr(module_path));
|
||||||
if(load_error != PluginManagerErrorNone) {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
const JsModuleDescriptor* plugin =
|
const JsModuleDescriptor* plugin =
|
||||||
plugin_manager_get_ep(modules->plugin_manager, plugin_cnt_last);
|
plugin_manager_get_ep(modules->plugin_manager, plugin_cnt_last);
|
||||||
furi_assert(plugin);
|
furi_assert(plugin);
|
||||||
|
|
||||||
if(strncmp(name, plugin->name, name_len) != 0) {
|
if(furi_string_cmp_str(deslashed_name, plugin->name) != 0) {
|
||||||
FURI_LOG_E(TAG, "Module name missmatch %s", plugin->name);
|
FURI_LOG_E(TAG, "Module name mismatch %s", plugin->name);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
JsModuleData module = {.create = plugin->create, .destroy = plugin->destroy};
|
JsModuleData module = {
|
||||||
JsModuleDict_set_at(modules->module_dict, module_name, 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;
|
module_found = true;
|
||||||
} while(0);
|
} while(0);
|
||||||
furi_string_free(module_path);
|
furi_string_free(module_path);
|
||||||
|
furi_string_free(deslashed_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run module constructor
|
// Run module constructor
|
||||||
mjs_val_t module_object = MJS_UNDEFINED;
|
mjs_val_t module_object = MJS_UNDEFINED;
|
||||||
if(module_found) {
|
if(module_found) {
|
||||||
module_inst = JsModuleDict_get(modules->module_dict, module_name);
|
module_inst = js_find_loaded_module(modules, name);
|
||||||
furi_assert(module_inst);
|
furi_assert(module_inst);
|
||||||
if(module_inst->create) { //-V779
|
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);
|
mjs_prepend_errorf(modules->mjs, MJS_BAD_ARGS_ERROR, "\"%s\" module load fail", name);
|
||||||
}
|
}
|
||||||
|
|
||||||
furi_string_free(module_name);
|
|
||||||
|
|
||||||
return module_object;
|
return module_object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void* js_module_get(JsModules* modules, const char* name) {
|
||||||
|
FuriString* module_name = furi_string_alloc_set_str(name);
|
||||||
|
JsModuleData* module_inst = js_find_loaded_module(modules, name);
|
||||||
|
furi_string_free(module_name);
|
||||||
|
return module_inst ? module_inst->context : NULL;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
#include "js_thread_i.h"
|
#include "js_thread_i.h"
|
||||||
#include <flipper_application/flipper_application.h>
|
#include <flipper_application/flipper_application.h>
|
||||||
#include <flipper_application/plugins/plugin_manager.h>
|
#include <flipper_application/plugins/plugin_manager.h>
|
||||||
@@ -7,19 +9,269 @@
|
|||||||
#define PLUGIN_APP_ID "js"
|
#define PLUGIN_APP_ID "js"
|
||||||
#define PLUGIN_API_VERSION 1
|
#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 {
|
typedef struct {
|
||||||
char* name;
|
char* name;
|
||||||
JsModeConstructor create;
|
JsModuleConstructor create;
|
||||||
JsModeDestructor destroy;
|
JsModuleDestructor destroy;
|
||||||
|
const ElfApiInterface* api_interface;
|
||||||
} JsModuleDescriptor;
|
} JsModuleDescriptor;
|
||||||
|
|
||||||
typedef struct JsModules JsModules;
|
|
||||||
|
|
||||||
JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver);
|
JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver);
|
||||||
|
|
||||||
void js_modules_destroy(JsModules* modules);
|
void js_modules_destroy(JsModules* modules);
|
||||||
|
|
||||||
mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_len);
|
mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_len);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Gets a module instance by its name
|
||||||
|
* This is useful when a module wants to access a stateful API of another
|
||||||
|
* module.
|
||||||
|
* @returns Pointer to module context, NULL if the module is not instantiated
|
||||||
|
*/
|
||||||
|
void* js_module_get(JsModules* modules, const char* name);
|
||||||
|
|||||||
@@ -195,17 +195,11 @@ static void js_require(struct mjs* mjs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void js_global_to_string(struct mjs* mjs) {
|
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));
|
double num = mjs_get_int(mjs, mjs_arg(mjs, 0));
|
||||||
char tmp_str[] = "-2147483648";
|
char tmp_str[] = "-2147483648";
|
||||||
itoa(num, tmp_str, 10);
|
itoa(num, tmp_str, base);
|
||||||
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);
|
|
||||||
mjs_val_t ret = mjs_mk_string(mjs, tmp_str, ~0, true);
|
mjs_val_t ret = mjs_mk_string(mjs, tmp_str, ~0, true);
|
||||||
mjs_return(mjs, ret);
|
mjs_return(mjs, ret);
|
||||||
}
|
}
|
||||||
@@ -239,8 +233,7 @@ static int32_t js_thread(void* arg) {
|
|||||||
mjs_val_t global = mjs_get_global(mjs);
|
mjs_val_t global = mjs_get_global(mjs);
|
||||||
mjs_set(mjs, global, "print", ~0, MJS_MK_FN(js_print));
|
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, "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, "toString", ~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, "ffi_address", ~0, MJS_MK_FN(js_ffi_address));
|
mjs_set(mjs, global, "ffi_address", ~0, MJS_MK_FN(js_ffi_address));
|
||||||
mjs_set(mjs, global, "require", ~0, MJS_MK_FN(js_require));
|
mjs_set(mjs, global, "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);
|
mjs_destroy(mjs);
|
||||||
|
js_modules_destroy(worker->modules);
|
||||||
|
|
||||||
composite_api_resolver_free(worker->resolver);
|
composite_api_resolver_free(worker->resolver);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
typedef struct JsThread JsThread;
|
typedef struct JsThread JsThread;
|
||||||
|
|
||||||
typedef enum {
|
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);
|
JsThread* js_thread_run(const char* script_path, JsThreadCallback callback, void* context);
|
||||||
|
|
||||||
void js_thread_stop(JsThread* worker);
|
void js_thread_stop(JsThread* worker);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ static bool setup_parse_params(struct mjs* mjs, mjs_val_t arg, FuriHalUsbHidConf
|
|||||||
}
|
}
|
||||||
mjs_val_t vid_obj = mjs_get(mjs, arg, "vid", ~0);
|
mjs_val_t vid_obj = mjs_get(mjs, arg, "vid", ~0);
|
||||||
mjs_val_t pid_obj = mjs_get(mjs, arg, "pid", ~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 mfr_obj = mjs_get(mjs, arg, "mfrName", ~0);
|
||||||
mjs_val_t prod_obj = mjs_get(mjs, arg, "prod_name", ~0);
|
mjs_val_t prod_obj = mjs_get(mjs, arg, "prodName", ~0);
|
||||||
|
|
||||||
if(mjs_is_number(vid_obj) && mjs_is_number(pid_obj)) {
|
if(mjs_is_number(vid_obj) && mjs_is_number(pid_obj)) {
|
||||||
hid_cfg->vid = mjs_get_int32(mjs, vid_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);
|
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));
|
JsBadusbInst* badusb = malloc(sizeof(JsBadusbInst));
|
||||||
mjs_val_t badusb_obj = mjs_mk_object(mjs);
|
mjs_val_t badusb_obj = mjs_mk_object(mjs);
|
||||||
mjs_set(mjs, badusb_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, badusb));
|
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",
|
"badusb",
|
||||||
js_badusb_create,
|
js_badusb_create,
|
||||||
js_badusb_destroy,
|
js_badusb_destroy,
|
||||||
|
NULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
#include <core/common_defines.h>
|
|
||||||
#include "../js_modules.h"
|
|
||||||
#include <dialogs/dialogs.h>
|
|
||||||
|
|
||||||
static bool js_dialog_msg_parse_params(struct mjs* mjs, const char** hdr, const char** msg) {
|
|
||||||
size_t num_args = mjs_nargs(mjs);
|
|
||||||
if(num_args != 2) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
mjs_val_t header_obj = mjs_arg(mjs, 0);
|
|
||||||
mjs_val_t msg_obj = mjs_arg(mjs, 1);
|
|
||||||
if((!mjs_is_string(header_obj)) || (!mjs_is_string(msg_obj))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t arg_len = 0;
|
|
||||||
*hdr = mjs_get_string(mjs, &header_obj, &arg_len);
|
|
||||||
if(arg_len == 0) {
|
|
||||||
*hdr = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
*msg = mjs_get_string(mjs, &msg_obj, &arg_len);
|
|
||||||
if(arg_len == 0) {
|
|
||||||
*msg = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void js_dialog_message(struct mjs* mjs) {
|
|
||||||
const char* dialog_header = NULL;
|
|
||||||
const char* dialog_msg = NULL;
|
|
||||||
if(!js_dialog_msg_parse_params(mjs, &dialog_header, &dialog_msg)) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
|
|
||||||
DialogMessage* message = dialog_message_alloc();
|
|
||||||
dialog_message_set_buttons(message, NULL, "OK", NULL);
|
|
||||||
if(dialog_header) {
|
|
||||||
dialog_message_set_header(message, dialog_header, 64, 3, AlignCenter, AlignTop);
|
|
||||||
}
|
|
||||||
if(dialog_msg) {
|
|
||||||
dialog_message_set_text(message, dialog_msg, 64, 26, AlignCenter, AlignTop);
|
|
||||||
}
|
|
||||||
DialogMessageButton result = dialog_message_show(dialogs, message);
|
|
||||||
dialog_message_free(message);
|
|
||||||
furi_record_close(RECORD_DIALOGS);
|
|
||||||
mjs_return(mjs, mjs_mk_boolean(mjs, result == DialogMessageButtonCenter));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void js_dialog_custom(struct mjs* mjs) {
|
|
||||||
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
|
|
||||||
DialogMessage* message = dialog_message_alloc();
|
|
||||||
|
|
||||||
bool params_correct = false;
|
|
||||||
|
|
||||||
do {
|
|
||||||
if(mjs_nargs(mjs) != 1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
mjs_val_t params_obj = mjs_arg(mjs, 0);
|
|
||||||
if(!mjs_is_object(params_obj)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
mjs_val_t text_obj = mjs_get(mjs, params_obj, "header", ~0);
|
|
||||||
size_t arg_len = 0;
|
|
||||||
const char* text_str = mjs_get_string(mjs, &text_obj, &arg_len);
|
|
||||||
if(arg_len == 0) {
|
|
||||||
text_str = NULL;
|
|
||||||
}
|
|
||||||
if(text_str) {
|
|
||||||
dialog_message_set_header(message, text_str, 64, 3, AlignCenter, AlignTop);
|
|
||||||
}
|
|
||||||
|
|
||||||
text_obj = mjs_get(mjs, params_obj, "text", ~0);
|
|
||||||
text_str = mjs_get_string(mjs, &text_obj, &arg_len);
|
|
||||||
if(arg_len == 0) {
|
|
||||||
text_str = NULL;
|
|
||||||
}
|
|
||||||
if(text_str) {
|
|
||||||
dialog_message_set_text(message, text_str, 64, 26, AlignCenter, AlignTop);
|
|
||||||
}
|
|
||||||
|
|
||||||
mjs_val_t btn_obj[3] = {
|
|
||||||
mjs_get(mjs, params_obj, "button_left", ~0),
|
|
||||||
mjs_get(mjs, params_obj, "button_center", ~0),
|
|
||||||
mjs_get(mjs, params_obj, "button_right", ~0),
|
|
||||||
};
|
|
||||||
const char* btn_text[3] = {NULL, NULL, NULL};
|
|
||||||
|
|
||||||
for(uint8_t i = 0; i < 3; i++) {
|
|
||||||
if(!mjs_is_string(btn_obj[i])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
btn_text[i] = mjs_get_string(mjs, &btn_obj[i], &arg_len);
|
|
||||||
if(arg_len == 0) {
|
|
||||||
btn_text[i] = NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog_message_set_buttons(message, btn_text[0], btn_text[1], btn_text[2]);
|
|
||||||
|
|
||||||
DialogMessageButton result = dialog_message_show(dialogs, message);
|
|
||||||
mjs_val_t return_obj = MJS_UNDEFINED;
|
|
||||||
if(result == DialogMessageButtonLeft) {
|
|
||||||
return_obj = mjs_mk_string(mjs, btn_text[0], ~0, true);
|
|
||||||
} else if(result == DialogMessageButtonCenter) {
|
|
||||||
return_obj = mjs_mk_string(mjs, btn_text[1], ~0, true);
|
|
||||||
} else if(result == DialogMessageButtonRight) {
|
|
||||||
return_obj = mjs_mk_string(mjs, btn_text[2], ~0, true);
|
|
||||||
} else {
|
|
||||||
return_obj = mjs_mk_string(mjs, "", ~0, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
mjs_return(mjs, return_obj);
|
|
||||||
params_correct = true;
|
|
||||||
} while(0);
|
|
||||||
|
|
||||||
dialog_message_free(message);
|
|
||||||
furi_record_close(RECORD_DIALOGS);
|
|
||||||
|
|
||||||
if(!params_correct) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void* js_dialog_create(struct mjs* mjs, mjs_val_t* object) {
|
|
||||||
mjs_val_t dialog_obj = mjs_mk_object(mjs);
|
|
||||||
mjs_set(mjs, dialog_obj, "message", ~0, MJS_MK_FN(js_dialog_message));
|
|
||||||
mjs_set(mjs, dialog_obj, "custom", ~0, MJS_MK_FN(js_dialog_custom));
|
|
||||||
*object = dialog_obj;
|
|
||||||
|
|
||||||
return (void*)1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const JsModuleDescriptor js_dialog_desc = {
|
|
||||||
"dialog",
|
|
||||||
js_dialog_create,
|
|
||||||
NULL,
|
|
||||||
};
|
|
||||||
|
|
||||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
|
||||||
.appid = PLUGIN_APP_ID,
|
|
||||||
.ep_api_version = PLUGIN_API_VERSION,
|
|
||||||
.entry_point = &js_dialog_desc,
|
|
||||||
};
|
|
||||||
|
|
||||||
const FlipperAppPluginDescriptor* js_dialog_ep(void) {
|
|
||||||
return &plugin_descriptor;
|
|
||||||
}
|
|
||||||
451
applications/system/js_app/modules/js_event_loop/js_event_loop.c
Normal file
451
applications/system/js_app/modules/js_event_loop/js_event_loop.c
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
#include "js_event_loop.h"
|
||||||
|
#include "../../js_modules.h" // IWYU pragma: keep
|
||||||
|
#include <expansion/expansion.h>
|
||||||
|
#include <mlib/m-array.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Number of arguments that callbacks receive from this module that they can't modify
|
||||||
|
*/
|
||||||
|
#define SYSTEM_ARGS 2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Context passed to the generic event callback
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
JsEventLoopObjectType object_type;
|
||||||
|
|
||||||
|
struct mjs* mjs;
|
||||||
|
mjs_val_t callback;
|
||||||
|
// NOTE: not using an mlib array because resizing is not needed.
|
||||||
|
mjs_val_t* arguments;
|
||||||
|
size_t arity;
|
||||||
|
|
||||||
|
JsEventLoopTransformer transformer;
|
||||||
|
void* transformer_context;
|
||||||
|
} JsEventLoopCallbackContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Contains data needed to cancel a subscription
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
FuriEventLoop* loop;
|
||||||
|
JsEventLoopObjectType object_type;
|
||||||
|
FuriEventLoopObject* object;
|
||||||
|
JsEventLoopCallbackContext* context;
|
||||||
|
JsEventLoopContract* contract;
|
||||||
|
void* subscriptions; // SubscriptionArray_t, which we can't reference in this definition
|
||||||
|
} JsEventLoopSubscription;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
FuriEventLoop* loop;
|
||||||
|
struct mjs* mjs;
|
||||||
|
} JsEventLoopTickContext;
|
||||||
|
|
||||||
|
ARRAY_DEF(SubscriptionArray, JsEventLoopSubscription*, M_PTR_OPLIST); //-V575
|
||||||
|
ARRAY_DEF(ContractArray, JsEventLoopContract*, M_PTR_OPLIST); //-V575
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Per-module instance control structure
|
||||||
|
*/
|
||||||
|
struct JsEventLoop {
|
||||||
|
FuriEventLoop* loop;
|
||||||
|
SubscriptionArray_t subscriptions;
|
||||||
|
ContractArray_t owned_contracts; //<! Contracts that were produced by this module
|
||||||
|
JsEventLoopTickContext* tick_context;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Generic event callback, handles all events by calling the JS callbacks
|
||||||
|
*/
|
||||||
|
static void js_event_loop_callback_generic(void* param) {
|
||||||
|
JsEventLoopCallbackContext* context = param;
|
||||||
|
mjs_val_t result;
|
||||||
|
mjs_apply(
|
||||||
|
context->mjs,
|
||||||
|
&result,
|
||||||
|
context->callback,
|
||||||
|
MJS_UNDEFINED,
|
||||||
|
context->arity,
|
||||||
|
context->arguments);
|
||||||
|
|
||||||
|
// save returned args for next call
|
||||||
|
if(mjs_array_length(context->mjs, result) != context->arity - SYSTEM_ARGS) return;
|
||||||
|
for(size_t i = 0; i < context->arity - SYSTEM_ARGS; i++) {
|
||||||
|
mjs_disown(context->mjs, &context->arguments[i + SYSTEM_ARGS]);
|
||||||
|
context->arguments[i + SYSTEM_ARGS] = mjs_array_get(context->mjs, result, i);
|
||||||
|
mjs_own(context->mjs, &context->arguments[i + SYSTEM_ARGS]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handles non-timer events
|
||||||
|
*/
|
||||||
|
static bool js_event_loop_callback(void* object, void* param) {
|
||||||
|
JsEventLoopCallbackContext* context = param;
|
||||||
|
|
||||||
|
if(context->transformer) {
|
||||||
|
mjs_disown(context->mjs, &context->arguments[1]);
|
||||||
|
context->arguments[1] =
|
||||||
|
context->transformer(context->mjs, object, context->transformer_context);
|
||||||
|
mjs_own(context->mjs, &context->arguments[1]);
|
||||||
|
} else {
|
||||||
|
// default behavior: take semaphores and mutexes
|
||||||
|
switch(context->object_type) {
|
||||||
|
case JsEventLoopObjectTypeSemaphore: {
|
||||||
|
FuriSemaphore* semaphore = object;
|
||||||
|
furi_check(furi_semaphore_acquire(semaphore, 0) == FuriStatusOk);
|
||||||
|
} break;
|
||||||
|
default:
|
||||||
|
// the corresponding check has been performed when we were given the contract
|
||||||
|
furi_crash();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
js_event_loop_callback_generic(param);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Cancels an event subscription
|
||||||
|
*/
|
||||||
|
static void js_event_loop_subscription_cancel(struct mjs* mjs) {
|
||||||
|
JsEventLoopSubscription* subscription = JS_GET_CONTEXT(mjs);
|
||||||
|
|
||||||
|
if(subscription->object_type == JsEventLoopObjectTypeTimer) {
|
||||||
|
furi_event_loop_timer_stop(subscription->object);
|
||||||
|
} else {
|
||||||
|
furi_event_loop_unsubscribe(subscription->loop, subscription->object);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(subscription->context->arguments);
|
||||||
|
free(subscription->context);
|
||||||
|
|
||||||
|
// find and remove ourselves from the array
|
||||||
|
SubscriptionArray_it_t iterator;
|
||||||
|
for(SubscriptionArray_it(iterator, subscription->subscriptions);
|
||||||
|
!SubscriptionArray_end_p(iterator);
|
||||||
|
SubscriptionArray_next(iterator)) {
|
||||||
|
JsEventLoopSubscription* item = *SubscriptionArray_cref(iterator);
|
||||||
|
if(item == subscription) break;
|
||||||
|
}
|
||||||
|
SubscriptionArray_remove(subscription->subscriptions, iterator);
|
||||||
|
free(subscription);
|
||||||
|
|
||||||
|
mjs_return(mjs, MJS_UNDEFINED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Subscribes a JavaScript function to an event
|
||||||
|
*/
|
||||||
|
static void js_event_loop_subscribe(struct mjs* mjs) {
|
||||||
|
JsEventLoop* module = JS_GET_CONTEXT(mjs);
|
||||||
|
|
||||||
|
// get arguments
|
||||||
|
JsEventLoopContract* contract;
|
||||||
|
mjs_val_t callback;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(
|
||||||
|
mjs, JS_AT_LEAST, JS_ARG_STRUCT(JsEventLoopContract, &contract), JS_ARG_FN(&callback));
|
||||||
|
|
||||||
|
// create subscription object
|
||||||
|
JsEventLoopSubscription* subscription = malloc(sizeof(JsEventLoopSubscription));
|
||||||
|
JsEventLoopCallbackContext* context = malloc(sizeof(JsEventLoopCallbackContext));
|
||||||
|
subscription->loop = module->loop;
|
||||||
|
subscription->object_type = contract->object_type;
|
||||||
|
subscription->context = context;
|
||||||
|
subscription->subscriptions = module->subscriptions;
|
||||||
|
if(contract->object_type == JsEventLoopObjectTypeTimer) subscription->contract = contract;
|
||||||
|
mjs_val_t subscription_obj = mjs_mk_object(mjs);
|
||||||
|
mjs_set(mjs, subscription_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, subscription));
|
||||||
|
mjs_set(mjs, subscription_obj, "cancel", ~0, MJS_MK_FN(js_event_loop_subscription_cancel));
|
||||||
|
|
||||||
|
// create callback context
|
||||||
|
context->object_type = contract->object_type;
|
||||||
|
context->arity = mjs_nargs(mjs) - SYSTEM_ARGS + 2;
|
||||||
|
context->arguments = calloc(context->arity, sizeof(mjs_val_t));
|
||||||
|
context->arguments[0] = subscription_obj;
|
||||||
|
context->arguments[1] = MJS_UNDEFINED;
|
||||||
|
for(size_t i = SYSTEM_ARGS; i < context->arity; i++) {
|
||||||
|
mjs_val_t arg = mjs_arg(mjs, i - SYSTEM_ARGS + 2);
|
||||||
|
context->arguments[i] = arg;
|
||||||
|
mjs_own(mjs, &context->arguments[i]);
|
||||||
|
}
|
||||||
|
context->mjs = mjs;
|
||||||
|
context->callback = callback;
|
||||||
|
mjs_own(mjs, &context->callback);
|
||||||
|
mjs_own(mjs, &context->arguments[0]);
|
||||||
|
mjs_own(mjs, &context->arguments[1]);
|
||||||
|
|
||||||
|
// queue and stream contracts must have a transform callback, others are allowed to delegate
|
||||||
|
// the obvious default behavior to this module
|
||||||
|
if(contract->object_type == JsEventLoopObjectTypeQueue ||
|
||||||
|
contract->object_type == JsEventLoopObjectTypeStream) {
|
||||||
|
furi_check(contract->non_timer.transformer);
|
||||||
|
}
|
||||||
|
context->transformer = contract->non_timer.transformer;
|
||||||
|
context->transformer_context = contract->non_timer.transformer_context;
|
||||||
|
|
||||||
|
// subscribe
|
||||||
|
switch(contract->object_type) {
|
||||||
|
case JsEventLoopObjectTypeTimer: {
|
||||||
|
FuriEventLoopTimer* timer = furi_event_loop_timer_alloc(
|
||||||
|
module->loop, js_event_loop_callback_generic, contract->timer.type, context);
|
||||||
|
furi_event_loop_timer_start(timer, contract->timer.interval_ticks);
|
||||||
|
contract->object = timer;
|
||||||
|
} break;
|
||||||
|
case JsEventLoopObjectTypeSemaphore:
|
||||||
|
furi_event_loop_subscribe_semaphore(
|
||||||
|
module->loop,
|
||||||
|
contract->object,
|
||||||
|
contract->non_timer.event,
|
||||||
|
js_event_loop_callback,
|
||||||
|
context);
|
||||||
|
break;
|
||||||
|
case JsEventLoopObjectTypeQueue:
|
||||||
|
furi_event_loop_subscribe_message_queue(
|
||||||
|
module->loop,
|
||||||
|
contract->object,
|
||||||
|
contract->non_timer.event,
|
||||||
|
js_event_loop_callback,
|
||||||
|
context);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
furi_crash("unimplemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription->object = contract->object;
|
||||||
|
SubscriptionArray_push_back(module->subscriptions, subscription);
|
||||||
|
mjs_return(mjs, subscription_obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Runs the event loop until it is stopped
|
||||||
|
*/
|
||||||
|
static void js_event_loop_run(struct mjs* mjs) {
|
||||||
|
JsEventLoop* module = JS_GET_CONTEXT(mjs);
|
||||||
|
furi_event_loop_run(module->loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Stops a running event loop
|
||||||
|
*/
|
||||||
|
static void js_event_loop_stop(struct mjs* mjs) {
|
||||||
|
JsEventLoop* module = JS_GET_CONTEXT(mjs);
|
||||||
|
furi_event_loop_stop(module->loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Creates a timer event that can be subscribed to just like any other
|
||||||
|
* event
|
||||||
|
*/
|
||||||
|
static void js_event_loop_timer(struct mjs* mjs) {
|
||||||
|
// get arguments
|
||||||
|
const char* mode_str;
|
||||||
|
int32_t interval;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&mode_str), JS_ARG_INT32(&interval));
|
||||||
|
JsEventLoop* module = JS_GET_CONTEXT(mjs);
|
||||||
|
|
||||||
|
FuriEventLoopTimerType mode;
|
||||||
|
if(strcasecmp(mode_str, "periodic") == 0) {
|
||||||
|
mode = FuriEventLoopTimerTypePeriodic;
|
||||||
|
} else if(strcasecmp(mode_str, "oneshot") == 0) {
|
||||||
|
mode = FuriEventLoopTimerTypeOnce;
|
||||||
|
} else {
|
||||||
|
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "argument 0: unknown mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
// make timer contract
|
||||||
|
JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract));
|
||||||
|
*contract = (JsEventLoopContract){
|
||||||
|
.magic = JsForeignMagic_JsEventLoopContract,
|
||||||
|
.object_type = JsEventLoopObjectTypeTimer,
|
||||||
|
.object = NULL,
|
||||||
|
.timer =
|
||||||
|
{
|
||||||
|
.interval_ticks = furi_ms_to_ticks((uint32_t)interval),
|
||||||
|
.type = mode,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ContractArray_push_back(module->owned_contracts, contract);
|
||||||
|
mjs_return(mjs, mjs_mk_foreign(mjs, contract));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Queue transformer. Takes `mjs_val_t` pointers out of a queue and
|
||||||
|
* returns their dereferenced value
|
||||||
|
*/
|
||||||
|
static mjs_val_t
|
||||||
|
js_event_loop_queue_transformer(struct mjs* mjs, FuriEventLoopObject* object, void* context) {
|
||||||
|
UNUSED(context);
|
||||||
|
mjs_val_t* message_ptr;
|
||||||
|
furi_check(furi_message_queue_get(object, &message_ptr, 0) == FuriStatusOk);
|
||||||
|
mjs_val_t message = *message_ptr;
|
||||||
|
mjs_disown(mjs, message_ptr);
|
||||||
|
free(message_ptr);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sends a message to a queue
|
||||||
|
*/
|
||||||
|
static void js_event_loop_queue_send(struct mjs* mjs) {
|
||||||
|
// get arguments
|
||||||
|
mjs_val_t message;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&message));
|
||||||
|
JsEventLoopContract* contract = JS_GET_CONTEXT(mjs);
|
||||||
|
|
||||||
|
// send message
|
||||||
|
mjs_val_t* message_ptr = malloc(sizeof(mjs_val_t));
|
||||||
|
*message_ptr = message;
|
||||||
|
mjs_own(mjs, message_ptr);
|
||||||
|
furi_message_queue_put(contract->object, &message_ptr, 0);
|
||||||
|
|
||||||
|
mjs_return(mjs, MJS_UNDEFINED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Creates a queue
|
||||||
|
*/
|
||||||
|
static void js_event_loop_queue(struct mjs* mjs) {
|
||||||
|
// get arguments
|
||||||
|
int32_t length;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&length));
|
||||||
|
JsEventLoop* module = JS_GET_CONTEXT(mjs);
|
||||||
|
|
||||||
|
// make queue contract
|
||||||
|
JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract));
|
||||||
|
*contract = (JsEventLoopContract){
|
||||||
|
.magic = JsForeignMagic_JsEventLoopContract,
|
||||||
|
.object_type = JsEventLoopObjectTypeQueue,
|
||||||
|
// we could store `mjs_val_t`s in the queue directly if not for mJS' requirement to have consistent pointers to owned values
|
||||||
|
.object = furi_message_queue_alloc((size_t)length, sizeof(mjs_val_t*)),
|
||||||
|
.non_timer =
|
||||||
|
{
|
||||||
|
.event = FuriEventLoopEventIn,
|
||||||
|
.transformer = js_event_loop_queue_transformer,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ContractArray_push_back(module->owned_contracts, contract);
|
||||||
|
|
||||||
|
// return object with control methods
|
||||||
|
mjs_val_t queue = mjs_mk_object(mjs);
|
||||||
|
mjs_set(mjs, queue, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, contract));
|
||||||
|
mjs_set(mjs, queue, "input", ~0, mjs_mk_foreign(mjs, contract));
|
||||||
|
mjs_set(mjs, queue, "send", ~0, MJS_MK_FN(js_event_loop_queue_send));
|
||||||
|
mjs_return(mjs, queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void js_event_loop_tick(void* param) {
|
||||||
|
JsEventLoopTickContext* context = param;
|
||||||
|
uint32_t flags = furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny | FuriFlagNoClear, 0);
|
||||||
|
if(flags & FuriFlagError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(flags & ThreadEventStop) {
|
||||||
|
furi_event_loop_stop(context->loop);
|
||||||
|
mjs_exit(context->mjs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void* js_event_loop_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||||
|
UNUSED(modules);
|
||||||
|
mjs_val_t event_loop_obj = mjs_mk_object(mjs);
|
||||||
|
JsEventLoop* module = malloc(sizeof(JsEventLoop));
|
||||||
|
JsEventLoopTickContext* tick_ctx = malloc(sizeof(JsEventLoopTickContext));
|
||||||
|
module->loop = furi_event_loop_alloc();
|
||||||
|
tick_ctx->loop = module->loop;
|
||||||
|
tick_ctx->mjs = mjs;
|
||||||
|
module->tick_context = tick_ctx;
|
||||||
|
furi_event_loop_tick_set(module->loop, 10, js_event_loop_tick, tick_ctx);
|
||||||
|
SubscriptionArray_init(module->subscriptions);
|
||||||
|
ContractArray_init(module->owned_contracts);
|
||||||
|
|
||||||
|
mjs_set(mjs, event_loop_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, module));
|
||||||
|
mjs_set(mjs, event_loop_obj, "subscribe", ~0, MJS_MK_FN(js_event_loop_subscribe));
|
||||||
|
mjs_set(mjs, event_loop_obj, "run", ~0, MJS_MK_FN(js_event_loop_run));
|
||||||
|
mjs_set(mjs, event_loop_obj, "stop", ~0, MJS_MK_FN(js_event_loop_stop));
|
||||||
|
mjs_set(mjs, event_loop_obj, "timer", ~0, MJS_MK_FN(js_event_loop_timer));
|
||||||
|
mjs_set(mjs, event_loop_obj, "queue", ~0, MJS_MK_FN(js_event_loop_queue));
|
||||||
|
|
||||||
|
*object = event_loop_obj;
|
||||||
|
return module;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void js_event_loop_destroy(void* inst) {
|
||||||
|
if(inst) {
|
||||||
|
JsEventLoop* module = inst;
|
||||||
|
furi_event_loop_stop(module->loop);
|
||||||
|
|
||||||
|
// free subscriptions
|
||||||
|
SubscriptionArray_it_t sub_iterator;
|
||||||
|
for(SubscriptionArray_it(sub_iterator, module->subscriptions);
|
||||||
|
!SubscriptionArray_end_p(sub_iterator);
|
||||||
|
SubscriptionArray_next(sub_iterator)) {
|
||||||
|
JsEventLoopSubscription* const* sub = SubscriptionArray_cref(sub_iterator);
|
||||||
|
free((*sub)->context->arguments);
|
||||||
|
free((*sub)->context);
|
||||||
|
free(*sub);
|
||||||
|
}
|
||||||
|
SubscriptionArray_clear(module->subscriptions);
|
||||||
|
|
||||||
|
// free owned contracts
|
||||||
|
ContractArray_it_t iterator;
|
||||||
|
for(ContractArray_it(iterator, module->owned_contracts); !ContractArray_end_p(iterator);
|
||||||
|
ContractArray_next(iterator)) {
|
||||||
|
// unsubscribe object
|
||||||
|
JsEventLoopContract* contract = *ContractArray_cref(iterator);
|
||||||
|
if(contract->object_type == JsEventLoopObjectTypeTimer) {
|
||||||
|
furi_event_loop_timer_stop(contract->object);
|
||||||
|
} else {
|
||||||
|
furi_event_loop_unsubscribe(module->loop, contract->object);
|
||||||
|
}
|
||||||
|
|
||||||
|
// free object
|
||||||
|
switch(contract->object_type) {
|
||||||
|
case JsEventLoopObjectTypeTimer:
|
||||||
|
furi_event_loop_timer_free(contract->object);
|
||||||
|
break;
|
||||||
|
case JsEventLoopObjectTypeSemaphore:
|
||||||
|
furi_semaphore_free(contract->object);
|
||||||
|
break;
|
||||||
|
case JsEventLoopObjectTypeQueue:
|
||||||
|
furi_message_queue_free(contract->object);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
furi_crash("unimplemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
free(contract);
|
||||||
|
}
|
||||||
|
ContractArray_clear(module->owned_contracts);
|
||||||
|
|
||||||
|
furi_event_loop_free(module->loop);
|
||||||
|
free(module->tick_context);
|
||||||
|
free(module);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern const ElfApiInterface js_event_loop_hashtable_api_interface;
|
||||||
|
|
||||||
|
static const JsModuleDescriptor js_event_loop_desc = {
|
||||||
|
"event_loop",
|
||||||
|
js_event_loop_create,
|
||||||
|
js_event_loop_destroy,
|
||||||
|
&js_event_loop_hashtable_api_interface,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||||
|
.appid = PLUGIN_APP_ID,
|
||||||
|
.ep_api_version = PLUGIN_API_VERSION,
|
||||||
|
.entry_point = &js_event_loop_desc,
|
||||||
|
};
|
||||||
|
|
||||||
|
const FlipperAppPluginDescriptor* js_event_loop_ep(void) {
|
||||||
|
return &plugin_descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
FuriEventLoop* js_event_loop_get_loop(JsEventLoop* loop) {
|
||||||
|
// porta: not the proudest function that i ever wrote
|
||||||
|
furi_check(loop);
|
||||||
|
return loop->loop;
|
||||||
|
}
|
||||||
104
applications/system/js_app/modules/js_event_loop/js_event_loop.h
Normal file
104
applications/system/js_app/modules/js_event_loop/js_event_loop.h
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
#include "../../js_modules.h" // IWYU pragma: keep
|
||||||
|
#include <furi/core/event_loop.h>
|
||||||
|
#include <furi/core/event_loop_timer.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file js_event_loop.h
|
||||||
|
*
|
||||||
|
* In JS interpreter code, `js_event_loop` always creates and maintains the
|
||||||
|
* event loop. There are two ways in which other modules can integrate with this
|
||||||
|
* loop:
|
||||||
|
* - Via contracts: The user of your module would have to acquire an opaque
|
||||||
|
* JS value from you and pass it to `js_event_loop`. This is useful for
|
||||||
|
* events that they user may be interested in. For more info, look at
|
||||||
|
* `JsEventLoopContract`. Also look at `js_event_loop_get_loop`, which
|
||||||
|
* you will need to unsubscribe the event loop from your object.
|
||||||
|
* - Directly: When your module is created, you can acquire an instance of
|
||||||
|
* `JsEventLoop` which you can use to acquire an instance of
|
||||||
|
* `FuriEventLoop` that you can manipulate directly, without the JS
|
||||||
|
* programmer having to pass contracts around. This is useful for
|
||||||
|
* "behind-the-scenes" events that the user does not need to know about. For
|
||||||
|
* more info, look at `js_event_loop_get_loop`.
|
||||||
|
*
|
||||||
|
* In both cases, your module is responsible for both instantiating,
|
||||||
|
* unsubscribing and freeing the object that the event loop subscribes to.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef struct JsEventLoop JsEventLoop;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
JsEventLoopObjectTypeTimer,
|
||||||
|
JsEventLoopObjectTypeQueue,
|
||||||
|
JsEventLoopObjectTypeMutex,
|
||||||
|
JsEventLoopObjectTypeSemaphore,
|
||||||
|
JsEventLoopObjectTypeStream,
|
||||||
|
} JsEventLoopObjectType;
|
||||||
|
|
||||||
|
typedef mjs_val_t (
|
||||||
|
*JsEventLoopTransformer)(struct mjs* mjs, FuriEventLoopObject* object, void* context);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
FuriEventLoopEvent event;
|
||||||
|
JsEventLoopTransformer transformer;
|
||||||
|
void* transformer_context;
|
||||||
|
} JsEventLoopNonTimerContract;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
FuriEventLoopTimerType type;
|
||||||
|
uint32_t interval_ticks;
|
||||||
|
} JsEventLoopTimerContract;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Adapter for other JS modules that wish to integrate with the event
|
||||||
|
* loop JS module
|
||||||
|
*
|
||||||
|
* If another module wishes to integrate with `js_event_loop`, it needs to
|
||||||
|
* implement a function callable from JS that returns an mJS foreign pointer to
|
||||||
|
* an instance of this structure. This value is then read by `event_loop`'s
|
||||||
|
* `subscribe` function.
|
||||||
|
*
|
||||||
|
* There are two fundamental variants of this structure:
|
||||||
|
* - `object_type` is `JsEventLoopObjectTypeTimer`: the `timer` field is
|
||||||
|
* valid, and the `non_timer` field is invalid.
|
||||||
|
* - `object_type` is something else: the `timer` field is invalid, and the
|
||||||
|
* `non_timer` field is valid. `non_timer.event` will be passed to
|
||||||
|
* `furi_event_loop_subscribe`. `non_timer.transformer` will be called to
|
||||||
|
* transform an object into a JS value (called an item) that's passed to the
|
||||||
|
* JS callback. This is useful for example to take an item out of a message
|
||||||
|
* queue and pass it to JS code in a convenient format. If
|
||||||
|
* `non_timer.transformer` is NULL, the event loop will take semaphores and
|
||||||
|
* mutexes on its own.
|
||||||
|
*
|
||||||
|
* The producer of the contract is responsible for freeing both the contract and
|
||||||
|
* the object that it points to when the interpreter is torn down.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
JsForeignMagic magic; // <! `JsForeignMagic_JsEventLoopContract`
|
||||||
|
JsEventLoopObjectType object_type;
|
||||||
|
FuriEventLoopObject* object;
|
||||||
|
union {
|
||||||
|
JsEventLoopNonTimerContract non_timer;
|
||||||
|
JsEventLoopTimerContract timer;
|
||||||
|
};
|
||||||
|
} JsEventLoopContract;
|
||||||
|
|
||||||
|
static_assert(offsetof(JsEventLoopContract, magic) == 0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Gets the FuriEventLoop owned by a JsEventLoop
|
||||||
|
*
|
||||||
|
* This function is useful in case your JS module wishes to integrate with
|
||||||
|
* the event loop without passing contracts through JS code. Your module will be
|
||||||
|
* dynamically linked to this one if you use this function, but only if JS code
|
||||||
|
* imports `event_loop` _before_ your module. An instance of `JsEventLoop` may
|
||||||
|
* be obtained via `js_module_get`.
|
||||||
|
*/
|
||||||
|
FuriEventLoop* js_event_loop_get_loop(JsEventLoop* loop);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#include <flipper_application/api_hashtable/api_hashtable.h>
|
||||||
|
#include <flipper_application/api_hashtable/compilesort.hpp>
|
||||||
|
|
||||||
|
#include "js_event_loop_api_table_i.h"
|
||||||
|
|
||||||
|
static_assert(!has_hash_collisions(js_event_loop_api_table), "Detected API method hash collision!");
|
||||||
|
|
||||||
|
extern "C" constexpr HashtableApiInterface js_event_loop_hashtable_api_interface{
|
||||||
|
{
|
||||||
|
.api_version_major = 0,
|
||||||
|
.api_version_minor = 0,
|
||||||
|
.resolver_callback = &elf_resolve_from_hashtable,
|
||||||
|
},
|
||||||
|
js_event_loop_api_table.cbegin(),
|
||||||
|
js_event_loop_api_table.cend(),
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#include "js_event_loop.h"
|
||||||
|
|
||||||
|
static constexpr auto js_event_loop_api_table = sort(
|
||||||
|
create_array_t<sym_entry>(API_METHOD(js_event_loop_get_loop, FuriEventLoop*, (JsEventLoop*))));
|
||||||
@@ -25,7 +25,8 @@ static void js_flipper_get_battery(struct mjs* mjs) {
|
|||||||
mjs_return(mjs, mjs_mk_number(mjs, info.charge));
|
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_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, "getModel", ~0, MJS_MK_FN(js_flipper_get_model));
|
||||||
mjs_set(mjs, flipper_obj, "getName", ~0, MJS_MK_FN(js_flipper_get_name));
|
mjs_set(mjs, flipper_obj, "getName", ~0, MJS_MK_FN(js_flipper_get_name));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "../js_thread_i.h"
|
#include "../js_thread_i.h"
|
||||||
|
|
||||||
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object);
|
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules);
|
||||||
|
|||||||
345
applications/system/js_app/modules/js_gpio.c
Normal file
345
applications/system/js_app/modules/js_gpio.c
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
#include "../js_modules.h" // IWYU pragma: keep
|
||||||
|
#include "./js_event_loop/js_event_loop.h"
|
||||||
|
#include <furi_hal_gpio.h>
|
||||||
|
#include <furi_hal_resources.h>
|
||||||
|
#include <expansion/expansion.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <mlib/m-array.h>
|
||||||
|
|
||||||
|
#define INTERRUPT_QUEUE_LEN 16
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-pin control structure
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
const GpioPin* pin;
|
||||||
|
bool had_interrupt;
|
||||||
|
FuriSemaphore* interrupt_semaphore;
|
||||||
|
JsEventLoopContract* interrupt_contract;
|
||||||
|
FuriHalAdcChannel adc_channel;
|
||||||
|
FuriHalAdcHandle* adc_handle;
|
||||||
|
} JsGpioPinInst;
|
||||||
|
|
||||||
|
ARRAY_DEF(ManagedPinsArray, JsGpioPinInst*, M_PTR_OPLIST); //-V575
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-module instance control structure
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
FuriEventLoop* loop;
|
||||||
|
ManagedPinsArray_t managed_pins;
|
||||||
|
FuriHalAdcHandle* adc_handle;
|
||||||
|
} JsGpioInst;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Interrupt callback
|
||||||
|
*/
|
||||||
|
static void js_gpio_int_cb(void* arg) {
|
||||||
|
furi_assert(arg);
|
||||||
|
FuriSemaphore* semaphore = arg;
|
||||||
|
furi_semaphore_release(semaphore);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initializes a GPIO pin according to the provided mode object
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* let gpio = require("gpio");
|
||||||
|
* let led = gpio.get("pc3");
|
||||||
|
* led.init({ direction: "out", outMode: "push_pull" });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
static void js_gpio_init(struct mjs* mjs) {
|
||||||
|
// deconstruct mode object
|
||||||
|
mjs_val_t mode_arg;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&mode_arg));
|
||||||
|
mjs_val_t direction_arg = mjs_get(mjs, mode_arg, "direction", ~0);
|
||||||
|
mjs_val_t out_mode_arg = mjs_get(mjs, mode_arg, "outMode", ~0);
|
||||||
|
mjs_val_t in_mode_arg = mjs_get(mjs, mode_arg, "inMode", ~0);
|
||||||
|
mjs_val_t edge_arg = mjs_get(mjs, mode_arg, "edge", ~0);
|
||||||
|
mjs_val_t pull_arg = mjs_get(mjs, mode_arg, "pull", ~0);
|
||||||
|
|
||||||
|
// get strings
|
||||||
|
const char* direction = mjs_get_string(mjs, &direction_arg, NULL);
|
||||||
|
const char* out_mode = mjs_get_string(mjs, &out_mode_arg, NULL);
|
||||||
|
const char* in_mode = mjs_get_string(mjs, &in_mode_arg, NULL);
|
||||||
|
const char* edge = mjs_get_string(mjs, &edge_arg, NULL);
|
||||||
|
const char* pull = mjs_get_string(mjs, &pull_arg, NULL);
|
||||||
|
if(!direction)
|
||||||
|
JS_ERROR_AND_RETURN(
|
||||||
|
mjs, MJS_BAD_ARGS_ERROR, "Expected string in \"direction\" field of mode object");
|
||||||
|
if(!out_mode) out_mode = "open_drain";
|
||||||
|
if(!in_mode) in_mode = "plain_digital";
|
||||||
|
if(!edge) edge = "rising";
|
||||||
|
|
||||||
|
// convert strings to mode
|
||||||
|
GpioMode mode;
|
||||||
|
if(strcmp(direction, "out") == 0) {
|
||||||
|
if(strcmp(out_mode, "push_pull") == 0)
|
||||||
|
mode = GpioModeOutputPushPull;
|
||||||
|
else if(strcmp(out_mode, "open_drain") == 0)
|
||||||
|
mode = GpioModeOutputOpenDrain;
|
||||||
|
else
|
||||||
|
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid outMode");
|
||||||
|
} else if(strcmp(direction, "in") == 0) {
|
||||||
|
if(strcmp(in_mode, "analog") == 0) {
|
||||||
|
mode = GpioModeAnalog;
|
||||||
|
} else if(strcmp(in_mode, "plain_digital") == 0) {
|
||||||
|
mode = GpioModeInput;
|
||||||
|
} else if(strcmp(in_mode, "interrupt") == 0) {
|
||||||
|
if(strcmp(edge, "rising") == 0)
|
||||||
|
mode = GpioModeInterruptRise;
|
||||||
|
else if(strcmp(edge, "falling") == 0)
|
||||||
|
mode = GpioModeInterruptFall;
|
||||||
|
else if(strcmp(edge, "both") == 0)
|
||||||
|
mode = GpioModeInterruptRiseFall;
|
||||||
|
else
|
||||||
|
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid edge");
|
||||||
|
} else if(strcmp(in_mode, "event") == 0) {
|
||||||
|
if(strcmp(edge, "rising") == 0)
|
||||||
|
mode = GpioModeEventRise;
|
||||||
|
else if(strcmp(edge, "falling") == 0)
|
||||||
|
mode = GpioModeEventFall;
|
||||||
|
else if(strcmp(edge, "both") == 0)
|
||||||
|
mode = GpioModeEventRiseFall;
|
||||||
|
else
|
||||||
|
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid edge");
|
||||||
|
} else {
|
||||||
|
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid inMode");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid direction");
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert pull
|
||||||
|
GpioPull pull_mode;
|
||||||
|
if(!pull) {
|
||||||
|
pull_mode = GpioPullNo;
|
||||||
|
} else if(strcmp(pull, "up") == 0) {
|
||||||
|
pull_mode = GpioPullUp;
|
||||||
|
} else if(strcmp(pull, "down") == 0) {
|
||||||
|
pull_mode = GpioPullDown;
|
||||||
|
} else {
|
||||||
|
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid pull");
|
||||||
|
}
|
||||||
|
|
||||||
|
// init GPIO
|
||||||
|
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||||
|
furi_hal_gpio_init(manager_data->pin, mode, pull_mode, GpioSpeedVeryHigh);
|
||||||
|
mjs_return(mjs, MJS_UNDEFINED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Writes a logic value to a GPIO pin
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* let gpio = require("gpio");
|
||||||
|
* let led = gpio.get("pc3");
|
||||||
|
* led.init({ direction: "out", outMode: "push_pull" });
|
||||||
|
* led.write(true);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
static void js_gpio_write(struct mjs* mjs) {
|
||||||
|
bool level;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_BOOL(&level));
|
||||||
|
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||||
|
furi_hal_gpio_write(manager_data->pin, level);
|
||||||
|
mjs_return(mjs, MJS_UNDEFINED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reads a logic value from a GPIO pin
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* let gpio = require("gpio");
|
||||||
|
* let button = gpio.get("pc1");
|
||||||
|
* button.init({ direction: "in" });
|
||||||
|
* if(button.read())
|
||||||
|
* print("hi button!!!!!");
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
static void js_gpio_read(struct mjs* mjs) {
|
||||||
|
// get level
|
||||||
|
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||||
|
bool value = furi_hal_gpio_read(manager_data->pin);
|
||||||
|
mjs_return(mjs, mjs_mk_boolean(mjs, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns a event loop contract that can be used to listen to interrupts
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* let gpio = require("gpio");
|
||||||
|
* let button = gpio.get("pc1");
|
||||||
|
* let event_loop = require("event_loop");
|
||||||
|
* button.init({ direction: "in", pull: "up", inMode: "interrupt", edge: "falling" });
|
||||||
|
* event_loop.subscribe(button.interrupt(), function (_) { print("Hi!"); });
|
||||||
|
* event_loop.run();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
static void js_gpio_interrupt(struct mjs* mjs) {
|
||||||
|
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||||
|
|
||||||
|
// interrupt handling
|
||||||
|
if(!manager_data->had_interrupt) {
|
||||||
|
furi_hal_gpio_add_int_callback(
|
||||||
|
manager_data->pin, js_gpio_int_cb, manager_data->interrupt_semaphore);
|
||||||
|
furi_hal_gpio_enable_int_callback(manager_data->pin);
|
||||||
|
manager_data->had_interrupt = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make contract
|
||||||
|
JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract));
|
||||||
|
*contract = (JsEventLoopContract){
|
||||||
|
.magic = JsForeignMagic_JsEventLoopContract,
|
||||||
|
.object_type = JsEventLoopObjectTypeSemaphore,
|
||||||
|
.object = manager_data->interrupt_semaphore,
|
||||||
|
.non_timer =
|
||||||
|
{
|
||||||
|
.event = FuriEventLoopEventIn,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
manager_data->interrupt_contract = contract;
|
||||||
|
mjs_return(mjs, mjs_mk_foreign(mjs, contract));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reads a voltage from a GPIO pin in analog mode
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* let gpio = require("gpio");
|
||||||
|
* let pot = gpio.get("pc0");
|
||||||
|
* pot.init({ direction: "in", inMode: "analog" });
|
||||||
|
* print("voltage:" pot.read_analog(), "mV");
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
static void js_gpio_read_analog(struct mjs* mjs) {
|
||||||
|
// get mV (ADC is configured for 12 bits and 2048 mV max)
|
||||||
|
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||||
|
uint16_t millivolts =
|
||||||
|
furi_hal_adc_read(manager_data->adc_handle, manager_data->adc_channel) / 2;
|
||||||
|
mjs_return(mjs, mjs_mk_number(mjs, (double)millivolts));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns an object that manages a specified pin.
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* let gpio = require("gpio");
|
||||||
|
* let led = gpio.get("pc3");
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
static void js_gpio_get(struct mjs* mjs) {
|
||||||
|
mjs_val_t name_arg;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&name_arg));
|
||||||
|
const char* name_string = mjs_get_string(mjs, &name_arg, NULL);
|
||||||
|
const GpioPinRecord* pin_record = NULL;
|
||||||
|
|
||||||
|
// parse input argument to a pin pointer
|
||||||
|
if(name_string) {
|
||||||
|
pin_record = furi_hal_resources_pin_by_name(name_string);
|
||||||
|
} else if(mjs_is_number(name_arg)) {
|
||||||
|
int name_int = mjs_get_int(mjs, name_arg);
|
||||||
|
pin_record = furi_hal_resources_pin_by_number(name_int);
|
||||||
|
} else {
|
||||||
|
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Must be either a string or a number");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!pin_record) JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Pin not found on device");
|
||||||
|
if(pin_record->debug)
|
||||||
|
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Pin is used for debugging");
|
||||||
|
|
||||||
|
// return pin manager object
|
||||||
|
JsGpioInst* module = JS_GET_CONTEXT(mjs);
|
||||||
|
mjs_val_t manager = mjs_mk_object(mjs);
|
||||||
|
JsGpioPinInst* manager_data = malloc(sizeof(JsGpioPinInst));
|
||||||
|
manager_data->pin = pin_record->pin;
|
||||||
|
manager_data->interrupt_semaphore = furi_semaphore_alloc(UINT32_MAX, 0);
|
||||||
|
manager_data->adc_handle = module->adc_handle;
|
||||||
|
manager_data->adc_channel = pin_record->channel;
|
||||||
|
mjs_own(mjs, &manager);
|
||||||
|
mjs_set(mjs, manager, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, manager_data));
|
||||||
|
mjs_set(mjs, manager, "init", ~0, MJS_MK_FN(js_gpio_init));
|
||||||
|
mjs_set(mjs, manager, "write", ~0, MJS_MK_FN(js_gpio_write));
|
||||||
|
mjs_set(mjs, manager, "read", ~0, MJS_MK_FN(js_gpio_read));
|
||||||
|
mjs_set(mjs, manager, "read_analog", ~0, MJS_MK_FN(js_gpio_read_analog));
|
||||||
|
mjs_set(mjs, manager, "interrupt", ~0, MJS_MK_FN(js_gpio_interrupt));
|
||||||
|
mjs_return(mjs, manager);
|
||||||
|
|
||||||
|
// remember pin
|
||||||
|
ManagedPinsArray_push_back(module->managed_pins, manager_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void* js_gpio_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||||
|
JsEventLoop* js_loop = js_module_get(modules, "event_loop");
|
||||||
|
if(M_UNLIKELY(!js_loop)) return NULL;
|
||||||
|
FuriEventLoop* loop = js_event_loop_get_loop(js_loop);
|
||||||
|
|
||||||
|
JsGpioInst* module = malloc(sizeof(JsGpioInst));
|
||||||
|
ManagedPinsArray_init(module->managed_pins);
|
||||||
|
module->adc_handle = furi_hal_adc_acquire();
|
||||||
|
module->loop = loop;
|
||||||
|
furi_hal_adc_configure(module->adc_handle);
|
||||||
|
|
||||||
|
mjs_val_t gpio_obj = mjs_mk_object(mjs);
|
||||||
|
mjs_set(mjs, gpio_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, module));
|
||||||
|
mjs_set(mjs, gpio_obj, "get", ~0, MJS_MK_FN(js_gpio_get));
|
||||||
|
*object = gpio_obj;
|
||||||
|
|
||||||
|
return (void*)module;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void js_gpio_destroy(void* inst) {
|
||||||
|
furi_assert(inst);
|
||||||
|
JsGpioInst* module = (JsGpioInst*)inst;
|
||||||
|
|
||||||
|
// reset pins
|
||||||
|
ManagedPinsArray_it_t iterator;
|
||||||
|
for(ManagedPinsArray_it(iterator, module->managed_pins); !ManagedPinsArray_end_p(iterator);
|
||||||
|
ManagedPinsArray_next(iterator)) {
|
||||||
|
JsGpioPinInst* manager_data = *ManagedPinsArray_cref(iterator);
|
||||||
|
if(manager_data->had_interrupt) {
|
||||||
|
furi_hal_gpio_disable_int_callback(manager_data->pin);
|
||||||
|
furi_hal_gpio_remove_int_callback(manager_data->pin);
|
||||||
|
}
|
||||||
|
furi_hal_gpio_init(manager_data->pin, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
|
||||||
|
furi_event_loop_maybe_unsubscribe(module->loop, manager_data->interrupt_semaphore);
|
||||||
|
furi_semaphore_free(manager_data->interrupt_semaphore);
|
||||||
|
free(manager_data->interrupt_contract);
|
||||||
|
free(manager_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// free buffers
|
||||||
|
furi_hal_adc_release(module->adc_handle);
|
||||||
|
ManagedPinsArray_clear(module->managed_pins);
|
||||||
|
free(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const JsModuleDescriptor js_gpio_desc = {
|
||||||
|
"gpio",
|
||||||
|
js_gpio_create,
|
||||||
|
js_gpio_destroy,
|
||||||
|
NULL,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||||
|
.appid = PLUGIN_APP_ID,
|
||||||
|
.ep_api_version = PLUGIN_API_VERSION,
|
||||||
|
.entry_point = &js_gpio_desc,
|
||||||
|
};
|
||||||
|
|
||||||
|
const FlipperAppPluginDescriptor* js_gpio_ep(void) {
|
||||||
|
return &plugin_descriptor;
|
||||||
|
}
|
||||||
129
applications/system/js_app/modules/js_gui/dialog.c
Normal file
129
applications/system/js_app/modules/js_gui/dialog.c
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
#include "../../js_modules.h" // IWYU pragma: keep
|
||||||
|
#include "js_gui.h"
|
||||||
|
#include "../js_event_loop/js_event_loop.h"
|
||||||
|
#include <gui/modules/dialog_ex.h>
|
||||||
|
|
||||||
|
#define QUEUE_LEN 2
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
FuriMessageQueue* queue;
|
||||||
|
JsEventLoopContract contract;
|
||||||
|
} JsDialogCtx;
|
||||||
|
|
||||||
|
static mjs_val_t
|
||||||
|
input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsDialogCtx* context) {
|
||||||
|
UNUSED(context);
|
||||||
|
DialogExResult result;
|
||||||
|
furi_check(furi_message_queue_get(queue, &result, 0) == FuriStatusOk);
|
||||||
|
const char* string;
|
||||||
|
if(result == DialogExResultLeft) {
|
||||||
|
string = "left";
|
||||||
|
} else if(result == DialogExResultCenter) {
|
||||||
|
string = "center";
|
||||||
|
} else if(result == DialogExResultRight) {
|
||||||
|
string = "right";
|
||||||
|
} else {
|
||||||
|
furi_crash();
|
||||||
|
}
|
||||||
|
return mjs_mk_string(mjs, string, ~0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void input_callback(DialogExResult result, JsDialogCtx* context) {
|
||||||
|
furi_check(furi_message_queue_put(context->queue, &result, 0) == FuriStatusOk);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
header_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
|
||||||
|
UNUSED(mjs);
|
||||||
|
UNUSED(context);
|
||||||
|
dialog_ex_set_header(dialog, value.string, 64, 0, AlignCenter, AlignTop);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
text_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
|
||||||
|
UNUSED(mjs);
|
||||||
|
UNUSED(context);
|
||||||
|
dialog_ex_set_text(dialog, value.string, 64, 32, AlignCenter, AlignCenter);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
left_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
|
||||||
|
UNUSED(mjs);
|
||||||
|
UNUSED(context);
|
||||||
|
dialog_ex_set_left_button_text(dialog, value.string);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static bool
|
||||||
|
center_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
|
||||||
|
UNUSED(mjs);
|
||||||
|
UNUSED(context);
|
||||||
|
dialog_ex_set_center_button_text(dialog, value.string);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static bool
|
||||||
|
right_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
|
||||||
|
UNUSED(mjs);
|
||||||
|
UNUSED(context);
|
||||||
|
dialog_ex_set_right_button_text(dialog, value.string);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static JsDialogCtx* ctx_make(struct mjs* mjs, DialogEx* dialog, mjs_val_t view_obj) {
|
||||||
|
JsDialogCtx* context = malloc(sizeof(JsDialogCtx));
|
||||||
|
context->queue = furi_message_queue_alloc(QUEUE_LEN, sizeof(DialogExResult));
|
||||||
|
context->contract = (JsEventLoopContract){
|
||||||
|
.magic = JsForeignMagic_JsEventLoopContract,
|
||||||
|
.object_type = JsEventLoopObjectTypeQueue,
|
||||||
|
.object = context->queue,
|
||||||
|
.non_timer =
|
||||||
|
{
|
||||||
|
.event = FuriEventLoopEventIn,
|
||||||
|
.transformer = (JsEventLoopTransformer)input_transformer,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
|
||||||
|
dialog_ex_set_result_callback(dialog, (DialogExResultCallback)input_callback);
|
||||||
|
dialog_ex_set_context(dialog, context);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ctx_destroy(DialogEx* input, JsDialogCtx* context, FuriEventLoop* loop) {
|
||||||
|
UNUSED(input);
|
||||||
|
furi_event_loop_maybe_unsubscribe(loop, context->queue);
|
||||||
|
furi_message_queue_free(context->queue);
|
||||||
|
free(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const JsViewDescriptor view_descriptor = {
|
||||||
|
.alloc = (JsViewAlloc)dialog_ex_alloc,
|
||||||
|
.free = (JsViewFree)dialog_ex_free,
|
||||||
|
.get_view = (JsViewGetView)dialog_ex_get_view,
|
||||||
|
.custom_make = (JsViewCustomMake)ctx_make,
|
||||||
|
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
|
||||||
|
.prop_cnt = 5,
|
||||||
|
.props = {
|
||||||
|
(JsViewPropDescriptor){
|
||||||
|
.name = "header",
|
||||||
|
.type = JsViewPropTypeString,
|
||||||
|
.assign = (JsViewPropAssign)header_assign},
|
||||||
|
(JsViewPropDescriptor){
|
||||||
|
.name = "text",
|
||||||
|
.type = JsViewPropTypeString,
|
||||||
|
.assign = (JsViewPropAssign)text_assign},
|
||||||
|
(JsViewPropDescriptor){
|
||||||
|
.name = "left",
|
||||||
|
.type = JsViewPropTypeString,
|
||||||
|
.assign = (JsViewPropAssign)left_assign},
|
||||||
|
(JsViewPropDescriptor){
|
||||||
|
.name = "center",
|
||||||
|
.type = JsViewPropTypeString,
|
||||||
|
.assign = (JsViewPropAssign)center_assign},
|
||||||
|
(JsViewPropDescriptor){
|
||||||
|
.name = "right",
|
||||||
|
.type = JsViewPropTypeString,
|
||||||
|
.assign = (JsViewPropAssign)right_assign},
|
||||||
|
}};
|
||||||
|
|
||||||
|
JS_GUI_VIEW_DEF(dialog, &view_descriptor);
|
||||||
12
applications/system/js_app/modules/js_gui/empty_screen.c
Normal file
12
applications/system/js_app/modules/js_gui/empty_screen.c
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#include "../../js_modules.h" // IWYU pragma: keep
|
||||||
|
#include "js_gui.h"
|
||||||
|
#include <gui/modules/empty_screen.h>
|
||||||
|
|
||||||
|
static const JsViewDescriptor view_descriptor = {
|
||||||
|
.alloc = (JsViewAlloc)empty_screen_alloc,
|
||||||
|
.free = (JsViewFree)empty_screen_free,
|
||||||
|
.get_view = (JsViewGetView)empty_screen_get_view,
|
||||||
|
.prop_cnt = 0,
|
||||||
|
.props = {},
|
||||||
|
};
|
||||||
|
JS_GUI_VIEW_DEF(empty_screen, &view_descriptor);
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user