diff --git a/.ci_files/rgb.patch b/.ci_files/rgb.patch index a9908125f..ad0a8d80d 100644 --- a/.ci_files/rgb.patch +++ b/.ci_files/rgb.patch @@ -527,7 +527,7 @@ index 0000000..572e1df + +void SK6805_update(void) { + SK6805_init(); -+ furi_kernel_lock(); ++ FURI_CRITICAL_ENTER(); + uint32_t end; + /* Последовательная отправка цветов светодиодов */ + for(uint8_t lednumber = 0; lednumber < SK6805_LED_COUNT; lednumber++) { @@ -567,7 +567,7 @@ index 0000000..572e1df + } + } + } -+ furi_kernel_unlock(); ++ FURI_CRITICAL_EXIT(); +} diff --git a/lib/drivers/SK6805.h b/lib/drivers/SK6805.h new file mode 100644 diff --git a/applications/services/expansion/expansion.c b/applications/services/expansion/expansion.c index 5b834b48d..9c64d0b5a 100644 --- a/applications/services/expansion/expansion.c +++ b/applications/services/expansion/expansion.c @@ -18,6 +18,7 @@ typedef enum { ExpansionStateDisabled, ExpansionStateEnabled, ExpansionStateRunning, + ExpansionStateConnectionEstablished, } ExpansionState; typedef enum { @@ -26,10 +27,15 @@ typedef enum { ExpansionMessageTypeSetListenSerial, ExpansionMessageTypeModuleConnected, ExpansionMessageTypeModuleDisconnected, + ExpansionMessageTypeConnectionEstablished, + ExpansionMessageTypeIsConnected, } ExpansionMessageType; typedef union { - FuriHalSerialId serial_id; + union { + FuriHalSerialId serial_id; + bool* is_connected; + }; } ExpansionMessageData; typedef struct { @@ -68,13 +74,21 @@ static void expansion_detect_callback(void* context) { UNUSED(status); } -static void expansion_worker_callback(void* context) { +static void expansion_worker_callback(void* context, ExpansionWorkerCallbackReason reason) { furi_assert(context); Expansion* instance = context; - ExpansionMessage message = { - .type = ExpansionMessageTypeModuleDisconnected, - .api_lock = NULL, // Not locking the API here to avoid a deadlock + ExpansionMessage message; + switch(reason) { + case ExpansionWorkerCallbackReasonExit: + message.type = ExpansionMessageTypeModuleDisconnected; + message.api_lock = NULL; // Not locking the API here to avoid a deadlock + break; + + case ExpansionWorkerCallbackReasonConnected: + message.type = ExpansionMessageTypeConnectionEstablished; + message.api_lock = api_lock_alloc_locked(); + break; }; const FuriStatus status = furi_message_queue_put(instance->queue, &message, FuriWaitForever); @@ -105,7 +119,9 @@ static void if(instance->state == ExpansionStateDisabled) { return; - } else if(instance->state == ExpansionStateRunning) { + } else if( + instance->state == ExpansionStateRunning || + instance->state == ExpansionStateConnectionEstablished) { expansion_worker_stop(instance->worker); expansion_worker_free(instance->worker); } else { @@ -122,7 +138,8 @@ static void expansion_control_handler_set_listen_serial( const ExpansionMessageData* data) { furi_check(data->serial_id < FuriHalSerialIdMax); - if(instance->state == ExpansionStateRunning) { + if(instance->state == ExpansionStateRunning || + instance->state == ExpansionStateConnectionEstablished) { expansion_worker_stop(instance->worker); expansion_worker_free(instance->worker); @@ -160,7 +177,8 @@ static void expansion_control_handler_module_disconnected( Expansion* instance, const ExpansionMessageData* data) { UNUSED(data); - if(instance->state != ExpansionStateRunning) { + if(instance->state != ExpansionStateRunning && + instance->state != ExpansionStateConnectionEstablished) { return; } @@ -170,6 +188,23 @@ static void expansion_control_handler_module_disconnected( instance->serial_id, expansion_detect_callback, instance); } +static void expansion_control_handler_connection_established( + Expansion* instance, + const ExpansionMessageData* data) { + UNUSED(data); + if(instance->state != ExpansionStateRunning && + instance->state != ExpansionStateConnectionEstablished) { + return; + } + + instance->state = ExpansionStateConnectionEstablished; +} + +static void + expansion_control_handler_is_connected(Expansion* instance, const ExpansionMessageData* data) { + *data->is_connected = instance->state == ExpansionStateConnectionEstablished; +} + typedef void (*ExpansionControlHandler)(Expansion*, const ExpansionMessageData*); static const ExpansionControlHandler expansion_control_handlers[] = { @@ -178,6 +213,8 @@ static const ExpansionControlHandler expansion_control_handlers[] = { [ExpansionMessageTypeSetListenSerial] = expansion_control_handler_set_listen_serial, [ExpansionMessageTypeModuleConnected] = expansion_control_handler_module_connected, [ExpansionMessageTypeModuleDisconnected] = expansion_control_handler_module_disconnected, + [ExpansionMessageTypeConnectionEstablished] = expansion_control_handler_connection_established, + [ExpansionMessageTypeIsConnected] = expansion_control_handler_is_connected, }; static int32_t expansion_control(void* context) { @@ -249,6 +286,22 @@ void expansion_disable(Expansion* instance) { api_lock_wait_unlock_and_free(message.api_lock); } +bool expansion_is_connected(Expansion* instance) { + furi_check(instance); + bool is_connected; + + ExpansionMessage message = { + .type = ExpansionMessageTypeIsConnected, + .data.is_connected = &is_connected, + .api_lock = api_lock_alloc_locked(), + }; + + furi_message_queue_put(instance->queue, &message, FuriWaitForever); + api_lock_wait_unlock_and_free(message.api_lock); + + return is_connected; +} + void expansion_set_listen_serial(Expansion* instance, FuriHalSerialId serial_id) { furi_check(instance); furi_check(serial_id < FuriHalSerialIdMax); diff --git a/applications/services/expansion/expansion.h b/applications/services/expansion/expansion.h index e169b3c15..1b0879b1e 100644 --- a/applications/services/expansion/expansion.h +++ b/applications/services/expansion/expansion.h @@ -50,6 +50,15 @@ void expansion_enable(Expansion* instance); */ void expansion_disable(Expansion* instance); +/** + * @brief Check if an expansion module is connected. + * + * @param[in,out] instance pointer to the Expansion instance. + * + * @returns true if the module is connected and initialized, false otherwise. + */ +bool expansion_is_connected(Expansion* instance); + /** * @brief Enable support for expansion modules on designated serial port. * diff --git a/applications/services/expansion/expansion_worker.c b/applications/services/expansion/expansion_worker.c index fd92063d2..4047212eb 100644 --- a/applications/services/expansion/expansion_worker.c +++ b/applications/services/expansion/expansion_worker.c @@ -223,6 +223,7 @@ static bool expansion_worker_handle_state_handshake( if(furi_hal_serial_is_baud_rate_supported(instance->serial_handle, baud_rate)) { instance->state = ExpansionWorkerStateConnected; + instance->callback(instance->cb_context, ExpansionWorkerCallbackReasonConnected); // Send response at previous baud rate if(!expansion_worker_send_status_response(instance, ExpansionFrameErrorNone)) break; furi_hal_serial_set_br(instance->serial_handle, baud_rate); @@ -351,7 +352,7 @@ static int32_t expansion_worker(void* context) { // Do not invoke worker callback on user-requested exit if((instance->exit_reason != ExpansionWorkerExitReasonUser) && (instance->callback != NULL)) { - instance->callback(instance->cb_context); + instance->callback(instance->cb_context, ExpansionWorkerCallbackReasonExit); } return 0; diff --git a/applications/services/expansion/expansion_worker.h b/applications/services/expansion/expansion_worker.h index 761f79c1d..faab2887f 100644 --- a/applications/services/expansion/expansion_worker.h +++ b/applications/services/expansion/expansion_worker.h @@ -17,14 +17,20 @@ */ typedef struct ExpansionWorker ExpansionWorker; +typedef enum { + ExpansionWorkerCallbackReasonExit, + ExpansionWorkerCallbackReasonConnected, +} ExpansionWorkerCallbackReason; + /** * @brief Worker callback type. * * @see expansion_worker_set_callback() * * @param[in,out] context pointer to a user-defined object. + * @param[in] reason reason for the callback. */ -typedef void (*ExpansionWorkerCallback)(void* context); +typedef void (*ExpansionWorkerCallback)(void* context, ExpansionWorkerCallbackReason reason); /** * @brief Create an expansion worker instance. diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index b586f1623..0b9b1f51f 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -47,3 +47,42 @@ App( requires=["js_app"], sources=["modules/js_usbdisk/*.c"], ) + +App( + appid="js_submenu", + apptype=FlipperAppType.PLUGIN, + entry_point="js_submenu_ep", + requires=["js_app"], + sources=["modules/js_submenu.c"], +) + +App( + appid="js_blebeacon", + apptype=FlipperAppType.PLUGIN, + entry_point="js_blebeacon_ep", + requires=["js_app"], + sources=["modules/js_blebeacon.c"], +) + +App( + appid="js_math", + apptype=FlipperAppType.PLUGIN, + entry_point="js_math_ep", + requires=["js_app"], + sources=["modules/js_math.c"], +) + +App( + appid="js_keyboard", + apptype=FlipperAppType.PLUGIN, + entry_point="js_keyboard_ep", + requires=["js_app"], + sources=["modules/js_keyboard.c"], +) +App( + appid="js_subghz", + apptype=FlipperAppType.PLUGIN, + entry_point="js_subghz_ep", + requires=["js_app"], + sources=["modules/js_subghz/*.c"], +) diff --git a/applications/system/js_app/examples/apps/Scripts/blebeacon.js b/applications/system/js_app/examples/apps/Scripts/blebeacon.js new file mode 100644 index 000000000..53983a745 --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/blebeacon.js @@ -0,0 +1,59 @@ +let blebeacon = require("blebeacon"); + +// Stop if previous background beacon is active +if (blebeacon.isActive()) { + blebeacon.stop(); +} + +// Make sure it resets at script exit, true will keep advertising in background +// This is false by default, can be omitted +blebeacon.keepAlive(false); + + +let math = require("math"); + +let currentIndex = 0; +let watchValues = [ + 0x1A, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0A, 0x0B, 0x0C, 0x11, 0x12, 0x13, 0x14, 0x15, + 0x16, 0x17, 0x18, 0xE4, 0xE5, 0x1B, 0x1C, 0x1D, 0x1E, + 0x20, 0xEC, 0xEF +]; + +function generateRandomMac() { + let mac = []; + for (let i = 0; i < 6; i++) { + mac.push(math.floor(math.random() * 256)); + } + return Uint8Array(mac); +} + +function sendRandomModelAdvertisement() { + let model = watchValues[currentIndex]; + + let packet = [ + 14, 0xFF, 0x75, 0x00, 0x01, 0x00, 0x02, 0x00, 0x01, 0x01, 0xFF, 0x00, 0x00, 0x43, + model + ]; + + let intervalMs = 50; + + // Power level, min interval and max interval are optional + blebeacon.setConfig(generateRandomMac(), 0x1F, intervalMs, intervalMs * 3); + + blebeacon.setData(Uint8Array(packet)); + + blebeacon.start(); + + print("Sent data for model ID " + to_string(model)); + + currentIndex = (currentIndex + 1) % watchValues.length; + + delay(intervalMs); + + blebeacon.stop(); +} + +while (true) { + sendRandomModelAdvertisement(); +} \ No newline at end of file diff --git a/applications/system/js_app/examples/apps/Scripts/keyboard.js b/applications/system/js_app/examples/apps/Scripts/keyboard.js new file mode 100644 index 000000000..a34607c29 --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/keyboard.js @@ -0,0 +1,19 @@ +let keyboard = require("keyboard"); + +keyboard.setHeader("Example Text Input"); + +// Default text is optional +let text = keyboard.text(100, "Default text", true); +print("Got text:", text); + +keyboard.setHeader("Example Byte Input"); + +// Default data is optional +let data = keyboard.byte(6, Uint8Array([1, 2, 3, 4, 5, 6])); +data = Uint8Array(data); +let result = "0x"; +for (let i = 0; i < data.byteLength; i++) { + if (data[i] < 0x10) result += "0"; + result += to_hex_string(data[i]); +} +print("Got data:", result); \ No newline at end of file diff --git a/applications/system/js_app/examples/apps/Scripts/math.js b/applications/system/js_app/examples/apps/Scripts/math.js new file mode 100644 index 000000000..49212f904 --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/math.js @@ -0,0 +1,47 @@ +let math = require("math"); + +let absResult = math.abs(-5); +let acosResult = math.acos(0.5); +let acoshResult = math.acosh(2); +let asinResult = math.asin(0.5); +let asinhResult = math.asinh(2); +let atanResult = math.atan(1); +let atan2Result = math.atan2(1, 1); +let atanhResult = math.atanh(0.5); +let cbrtResult = math.cbrt(27); +let ceilResult = math.ceil(5.3); +let clz32Result = math.clz32(1); +let cosResult = math.cos(math.PI); +let expResult = math.exp(1); +let floorResult = math.floor(5.7); +let maxResult = math.max(3, 5); +let minResult = math.min(3, 5); +let powResult = math.pow(2, 3); +let randomResult = math.random(); +let signResult = math.sign(-5); +let sinResult = math.sin(math.PI / 2); +let sqrtResult = math.sqrt(25); +let truncResult = math.trunc(5.7); + +print("math.abs(-5):", absResult); +print("math.acos(0.5):", acosResult); +print("math.acosh(2):", acoshResult); +print("math.asin(0.5):", asinResult); +print("math.asinh(2):", asinhResult); +print("math.atan(1):", atanResult); +print("math.atan2(1, 1):", atan2Result); +print("math.atanh(0.5):", atanhResult); +print("math.cbrt(27):", cbrtResult); +print("math.ceil(5.3):", ceilResult); +print("math.clz32(1):", clz32Result); +print("math.cos(math.PI):", cosResult); +print("math.exp(1):", expResult); +print("math.floor(5.7):", floorResult); +print("math.max(3, 5):", maxResult); +print("math.min(3, 5):", minResult); +print("math.pow(2, 3):", powResult); +print("math.random():", randomResult); +print("math.sign(-5):", signResult); +print("math.sin(math.PI/2):", sinResult); +print("math.sqrt(25):", sqrtResult); +print("math.trunc(5.7):", truncResult); \ No newline at end of file diff --git a/applications/system/js_app/examples/apps/Scripts/subghz.js b/applications/system/js_app/examples/apps/Scripts/subghz.js new file mode 100644 index 000000000..39dadf070 --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/subghz.js @@ -0,0 +1,37 @@ +let subghz = require("subghz"); +subghz.setup(); + +function printRXline() { + if (subghz.getState() !== "RX") { + subghz.setRx(); // to RX + } + + let rssi = subghz.getRssi(); + let freq = subghz.getFrequency(); + let ext = subghz.isExternal(); + + print("rssi: ", rssi, "dBm", "@", freq, "MHz", "ext: ", ext); +} + +function changeFrequency(freq) { + if (subghz.getState() !== "IDLE") { + subghz.setIdle(); // need to be idle to change frequency + } + subghz.setFrequency(freq); +} + +subghz.setIdle(); +print(subghz.getState()); // "IDLE" +subghz.setRx(); +print(subghz.getState()); // "RX" + +changeFrequency(433920000); +printRXline(); +delay(1000); + +let result = subghz.transmitFile("/ext/subghz/0.sub"); +print(result ? "Send success" : "Send failed"); +delay(1000); + +changeFrequency(315000000); +printRXline(); \ No newline at end of file diff --git a/applications/system/js_app/examples/apps/Scripts/submenu.js b/applications/system/js_app/examples/apps/Scripts/submenu.js new file mode 100644 index 000000000..6744ca452 --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/submenu.js @@ -0,0 +1,11 @@ +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(); + +print("Result:", result); diff --git a/applications/system/js_app/modules/js_blebeacon.c b/applications/system/js_app/modules/js_blebeacon.c new file mode 100644 index 000000000..4d19accb1 --- /dev/null +++ b/applications/system/js_app/modules/js_blebeacon.c @@ -0,0 +1,245 @@ +#include "../js_modules.h" +#include +#include + +typedef struct { + bool saved_prev_cfg; + bool prev_cfg_set; + GapExtraBeaconConfig prev_cfg; + + bool saved_prev_data; + uint8_t prev_data[EXTRA_BEACON_MAX_DATA_SIZE]; + uint8_t prev_data_len; + + bool saved_prev_active; + bool prev_active; + + bool keep_alive; +} JsBlebeaconInst; + +static JsBlebeaconInst* get_this_ctx(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsBlebeaconInst* storage = mjs_get_ptr(mjs, obj_inst); + furi_assert(storage); + return storage; +} + +static void ret_bad_args(struct mjs* mjs, const char* error) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error); + mjs_return(mjs, MJS_UNDEFINED); +} + +static void ret_int_err(struct mjs* mjs, const char* error) { + mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "%s", error); + mjs_return(mjs, MJS_UNDEFINED); +} + +static bool check_arg_count(struct mjs* mjs, size_t count) { + size_t num_args = mjs_nargs(mjs); + if(num_args != count) { + ret_bad_args(mjs, "Wrong argument count"); + return false; + } + return true; +} + +static bool get_int_arg(struct mjs* mjs, size_t index, uint8_t* value, bool error) { + mjs_val_t int_obj = mjs_arg(mjs, index); + if(!mjs_is_number(int_obj)) { + if(error) ret_bad_args(mjs, "Argument must be a number"); + return false; + } + *value = mjs_get_int(mjs, int_obj); + return true; +} + +static void js_blebeacon_is_active(struct mjs* mjs) { + JsBlebeaconInst* blebeacon = get_this_ctx(mjs); + if(!check_arg_count(mjs, 0)) return; + UNUSED(blebeacon); + + mjs_return(mjs, mjs_mk_boolean(mjs, furi_hal_bt_extra_beacon_is_active())); +} + +static void js_blebeacon_set_config(struct mjs* mjs) { + JsBlebeaconInst* blebeacon = get_this_ctx(mjs); + if(mjs_nargs(mjs) < 1 || mjs_nargs(mjs) > 4) { + ret_bad_args(mjs, "Wrong argument count"); + return; + } + + char* mac = NULL; + size_t mac_len = 0; + mjs_val_t mac_arg = mjs_arg(mjs, 0); + if(mjs_is_typed_array(mac_arg)) { + if(mjs_is_data_view(mac_arg)) { + mac_arg = mjs_dataview_get_buf(mjs, mac_arg); + } + mac = mjs_array_buf_get_ptr(mjs, mac_arg, &mac_len); + } + if(!mac || mac_len != EXTRA_BEACON_MAC_ADDR_SIZE) { + ret_bad_args(mjs, "Wrong MAC address"); + return; + } + + uint8_t power = GapAdvPowerLevel_0dBm; + get_int_arg(mjs, 1, &power, false); + power = CLAMP(power, GapAdvPowerLevel_6dBm, GapAdvPowerLevel_Neg40dBm); + + uint8_t intv_min = 50; + get_int_arg(mjs, 2, &intv_min, false); + intv_min = MAX(intv_min, 20); + + uint8_t intv_max = 150; + get_int_arg(mjs, 3, &intv_max, false); + intv_max = MAX(intv_max, intv_min); + + GapExtraBeaconConfig config = { + .min_adv_interval_ms = intv_min, + .max_adv_interval_ms = intv_max, + .adv_channel_map = GapAdvChannelMapAll, + .adv_power_level = power, + .address_type = GapAddressTypePublic, + }; + memcpy(config.address, (uint8_t*)mac, sizeof(config.address)); + + if(!blebeacon->saved_prev_cfg) { + blebeacon->saved_prev_cfg = true; + const GapExtraBeaconConfig* prev_cfg_ptr = furi_hal_bt_extra_beacon_get_config(); + if(prev_cfg_ptr) { + blebeacon->prev_cfg_set = true; + memcpy(&blebeacon->prev_cfg, prev_cfg_ptr, sizeof(blebeacon->prev_cfg)); + } else { + blebeacon->prev_cfg_set = false; + } + } + if(!furi_hal_bt_extra_beacon_set_config(&config)) { + ret_int_err(mjs, "Failed setting beacon config"); + return; + } + + mjs_return(mjs, MJS_UNDEFINED); +} + +static void js_blebeacon_set_data(struct mjs* mjs) { + JsBlebeaconInst* blebeacon = get_this_ctx(mjs); + if(!check_arg_count(mjs, 1)) return; + + char* data = NULL; + size_t data_len = 0; + mjs_val_t data_arg = mjs_arg(mjs, 0); + if(mjs_is_typed_array(data_arg)) { + if(mjs_is_data_view(data_arg)) { + data_arg = mjs_dataview_get_buf(mjs, data_arg); + } + data = mjs_array_buf_get_ptr(mjs, data_arg, &data_len); + } + if(!data) { + ret_bad_args(mjs, "Data must be a Uint8Array"); + return; + } + + if(!blebeacon->saved_prev_data) { + blebeacon->saved_prev_data = true; + blebeacon->prev_data_len = furi_hal_bt_extra_beacon_get_data(blebeacon->prev_data); + } + if(!furi_hal_bt_extra_beacon_set_data((uint8_t*)data, data_len)) { + ret_int_err(mjs, "Failed setting beacon data"); + return; + } + + mjs_return(mjs, MJS_UNDEFINED); +} + +static void js_blebeacon_start(struct mjs* mjs) { + JsBlebeaconInst* blebeacon = get_this_ctx(mjs); + if(!check_arg_count(mjs, 0)) return; + + if(!blebeacon->saved_prev_active) { + blebeacon->saved_prev_active = true; + blebeacon->prev_active = furi_hal_bt_extra_beacon_is_active(); + } + if(!furi_hal_bt_extra_beacon_start()) { + ret_int_err(mjs, "Failed starting beacon"); + return; + } + + mjs_return(mjs, MJS_UNDEFINED); +} + +static void js_blebeacon_stop(struct mjs* mjs) { + JsBlebeaconInst* blebeacon = get_this_ctx(mjs); + if(!check_arg_count(mjs, 0)) return; + UNUSED(blebeacon); + + if(!blebeacon->saved_prev_active) { + blebeacon->saved_prev_active = true; + blebeacon->prev_active = furi_hal_bt_extra_beacon_is_active(); + } + if(!furi_hal_bt_extra_beacon_stop()) { + ret_int_err(mjs, "Failed stopping beacon"); + return; + } + + mjs_return(mjs, MJS_UNDEFINED); +} + +static void js_blebeacon_keep_alive(struct mjs* mjs) { + JsBlebeaconInst* blebeacon = get_this_ctx(mjs); + if(!check_arg_count(mjs, 1)) return; + + mjs_val_t bool_obj = mjs_arg(mjs, 0); + blebeacon->keep_alive = mjs_get_bool(mjs, bool_obj); + + mjs_return(mjs, MJS_UNDEFINED); +} + +static void* js_blebeacon_create(struct mjs* mjs, mjs_val_t* object) { + JsBlebeaconInst* blebeacon = malloc(sizeof(JsBlebeaconInst)); + mjs_val_t blebeacon_obj = mjs_mk_object(mjs); + mjs_set(mjs, blebeacon_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, blebeacon)); + mjs_set(mjs, blebeacon_obj, "isActive", ~0, MJS_MK_FN(js_blebeacon_is_active)); + mjs_set(mjs, blebeacon_obj, "setConfig", ~0, MJS_MK_FN(js_blebeacon_set_config)); + mjs_set(mjs, blebeacon_obj, "setData", ~0, MJS_MK_FN(js_blebeacon_set_data)); + mjs_set(mjs, blebeacon_obj, "start", ~0, MJS_MK_FN(js_blebeacon_start)); + mjs_set(mjs, blebeacon_obj, "stop", ~0, MJS_MK_FN(js_blebeacon_stop)); + mjs_set(mjs, blebeacon_obj, "keepAlive", ~0, MJS_MK_FN(js_blebeacon_keep_alive)); + *object = blebeacon_obj; + return blebeacon; +} + +static void js_blebeacon_destroy(void* inst) { + JsBlebeaconInst* blebeacon = inst; + if(!blebeacon->keep_alive) { + if(furi_hal_bt_extra_beacon_is_active()) { + furi_check(furi_hal_bt_extra_beacon_stop()); + } + if(blebeacon->saved_prev_cfg && blebeacon->prev_cfg_set) { + furi_check(furi_hal_bt_extra_beacon_set_config(&blebeacon->prev_cfg)); + } + if(blebeacon->saved_prev_data) { + furi_check( + furi_hal_bt_extra_beacon_set_data(blebeacon->prev_data, blebeacon->prev_data_len)); + } + if(blebeacon->prev_active) { + furi_check(furi_hal_bt_extra_beacon_start()); + } + } + free(blebeacon); +} + +static const JsModuleDescriptor js_blebeacon_desc = { + "blebeacon", + js_blebeacon_create, + js_blebeacon_destroy, +}; + +static const FlipperAppPluginDescriptor plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_blebeacon_desc, +}; + +const FlipperAppPluginDescriptor* js_blebeacon_ep(void) { + return &plugin_descriptor; +} \ No newline at end of file diff --git a/applications/system/js_app/modules/js_keyboard.c b/applications/system/js_app/modules/js_keyboard.c new file mode 100644 index 000000000..8958dcaf8 --- /dev/null +++ b/applications/system/js_app/modules/js_keyboard.c @@ -0,0 +1,195 @@ +#include "../js_modules.h" +#include +#include +#include + +#define membersof(x) (sizeof(x) / sizeof(x[0])) + +typedef struct { + char* data; + TextInput* text_input; + ByteInput* byte_input; + ViewDispatcher* view_dispatcher; + uint8_t* byteinput; +} JsKeyboardInst; + +typedef enum { + JsKeyboardViewTextInput, + JsKeyboardViewByteInput, +} JsKeyboardView; + +static void ret_bad_args(struct mjs* mjs, const char* error) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error); + mjs_return(mjs, MJS_UNDEFINED); +} + +static bool get_str_arg(struct mjs* mjs, size_t index, const char** value, bool error) { + mjs_val_t str_obj = mjs_arg(mjs, index); + if(!mjs_is_string(str_obj)) { + if(error) ret_bad_args(mjs, "Argument must be a string"); + return false; + } + size_t str_len = 0; + *value = mjs_get_string(mjs, &str_obj, &str_len); + if((str_len == 0) || (*value == NULL)) { + if(error) ret_bad_args(mjs, "Bad string argument"); + return false; + } + return true; +} + +static bool get_int_arg(struct mjs* mjs, size_t index, size_t* value, bool error) { + mjs_val_t int_obj = mjs_arg(mjs, index); + if(!mjs_is_number(int_obj)) { + if(error) ret_bad_args(mjs, "Argument must be a number"); + return false; + } + *value = mjs_get_int(mjs, int_obj); + return true; +} + +static JsKeyboardInst* get_this_ctx(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsKeyboardInst* storage = mjs_get_ptr(mjs, obj_inst); + furi_assert(storage); + return storage; +} + +void text_input_callback(void* context) { + JsKeyboardInst* keyboard = (JsKeyboardInst*)context; + view_dispatcher_stop(keyboard->view_dispatcher); +} + +void byte_input_callback(void* context) { + JsKeyboardInst* keyboard = (JsKeyboardInst*)context; + view_dispatcher_stop(keyboard->view_dispatcher); +} + +static void js_keyboard_set_header(struct mjs* mjs) { + JsKeyboardInst* keyboard = get_this_ctx(mjs); + + const char* header; + if(!get_str_arg(mjs, 0, &header, true)) return; + + text_input_set_header_text(keyboard->text_input, header); + byte_input_set_header_text(keyboard->byte_input, header); + + mjs_return(mjs, MJS_UNDEFINED); +} + +static void js_keyboard_text(struct mjs* mjs) { + JsKeyboardInst* keyboard = get_this_ctx(mjs); + + size_t input_length; + if(!get_int_arg(mjs, 0, &input_length, true)) return; + char* buffer = malloc(input_length); + + const char* default_text = ""; + bool clear_default = false; + if(get_str_arg(mjs, 1, &default_text, false)) { + strlcpy(buffer, default_text, input_length); + mjs_val_t bool_obj = mjs_arg(mjs, 2); + clear_default = mjs_get_bool(mjs, bool_obj); + } + + view_dispatcher_attach_to_gui( + keyboard->view_dispatcher, furi_record_open(RECORD_GUI), ViewDispatcherTypeFullscreen); + furi_record_close(RECORD_GUI); + + text_input_set_result_callback( + keyboard->text_input, text_input_callback, keyboard, buffer, input_length, clear_default); + + view_dispatcher_switch_to_view(keyboard->view_dispatcher, JsKeyboardViewTextInput); + + view_dispatcher_run(keyboard->view_dispatcher); + + text_input_reset(keyboard->text_input); + + mjs_return(mjs, mjs_mk_string(mjs, buffer, ~0, true)); + free(buffer); +} + +static void js_keyboard_byte(struct mjs* mjs) { + JsKeyboardInst* keyboard = get_this_ctx(mjs); + + size_t input_length; + if(!get_int_arg(mjs, 0, &input_length, true)) return; + uint8_t* buffer = malloc(input_length); + + mjs_val_t default_data_arg = mjs_arg(mjs, 1); + if(mjs_is_typed_array(default_data_arg)) { + if(mjs_is_data_view(default_data_arg)) { + default_data_arg = mjs_dataview_get_buf(mjs, default_data_arg); + } + size_t default_data_len = 0; + char* default_data = mjs_array_buf_get_ptr(mjs, default_data_arg, &default_data_len); + memcpy(buffer, (uint8_t*)default_data, MIN((size_t)input_length, default_data_len)); + } + + view_dispatcher_attach_to_gui( + keyboard->view_dispatcher, furi_record_open(RECORD_GUI), ViewDispatcherTypeFullscreen); + furi_record_close(RECORD_GUI); + + byte_input_set_result_callback( + keyboard->byte_input, byte_input_callback, NULL, keyboard, buffer, input_length); + + view_dispatcher_switch_to_view(keyboard->view_dispatcher, JsKeyboardViewByteInput); + + view_dispatcher_run(keyboard->view_dispatcher); + + byte_input_set_result_callback(keyboard->byte_input, NULL, NULL, NULL, NULL, 0); + byte_input_set_header_text(keyboard->byte_input, ""); + + mjs_return(mjs, mjs_mk_array_buf(mjs, (char*)buffer, input_length)); + free(buffer); +} + +static void* js_keyboard_create(struct mjs* mjs, mjs_val_t* object) { + JsKeyboardInst* keyboard = malloc(sizeof(JsKeyboardInst)); + mjs_val_t keyboard_obj = mjs_mk_object(mjs); + mjs_set(mjs, keyboard_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, keyboard)); + mjs_set(mjs, keyboard_obj, "setHeader", ~0, MJS_MK_FN(js_keyboard_set_header)); + mjs_set(mjs, keyboard_obj, "text", ~0, MJS_MK_FN(js_keyboard_text)); + mjs_set(mjs, keyboard_obj, "byte", ~0, MJS_MK_FN(js_keyboard_byte)); + keyboard->byte_input = byte_input_alloc(); + keyboard->text_input = text_input_alloc(); + keyboard->view_dispatcher = view_dispatcher_alloc(); + view_dispatcher_enable_queue(keyboard->view_dispatcher); + view_dispatcher_add_view( + keyboard->view_dispatcher, + JsKeyboardViewTextInput, + text_input_get_view(keyboard->text_input)); + view_dispatcher_add_view( + keyboard->view_dispatcher, + JsKeyboardViewByteInput, + byte_input_get_view(keyboard->byte_input)); + *object = keyboard_obj; + return keyboard; +} + +static void js_keyboard_destroy(void* inst) { + JsKeyboardInst* keyboard = inst; + view_dispatcher_remove_view(keyboard->view_dispatcher, JsKeyboardViewByteInput); + byte_input_free(keyboard->byte_input); + view_dispatcher_remove_view(keyboard->view_dispatcher, JsKeyboardViewTextInput); + text_input_free(keyboard->text_input); + view_dispatcher_free(keyboard->view_dispatcher); + free(keyboard->data); + free(keyboard); +} + +static const JsModuleDescriptor js_keyboard_desc = { + "keyboard", + js_keyboard_create, + js_keyboard_destroy, +}; + +static const FlipperAppPluginDescriptor plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_keyboard_desc, +}; + +const FlipperAppPluginDescriptor* js_keyboard_ep(void) { + return &plugin_descriptor; +} \ No newline at end of file diff --git a/applications/system/js_app/modules/js_math.c b/applications/system/js_app/modules/js_math.c new file mode 100644 index 000000000..80d97fb9c --- /dev/null +++ b/applications/system/js_app/modules/js_math.c @@ -0,0 +1,309 @@ +#include "../js_modules.h" +#include "furi_hal_random.h" + +#define JS_MATH_PI (double)3.14159265358979323846 +#define JS_MATH_E (double)2.7182818284590452354 + +static void ret_bad_args(struct mjs* mjs, const char* error) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error); + mjs_return(mjs, mjs_mk_undefined()); +} + +static bool check_arg_count(struct mjs* mjs, size_t count) { + size_t num_args = mjs_nargs(mjs); + if(num_args != count) { + ret_bad_args(mjs, "Wrong argument count"); + return false; + } + return true; +} + +void js_math_abs(struct mjs* mjs) { + if(!check_arg_count(mjs, 1) || !mjs_is_number(mjs_arg(mjs, 0))) { + mjs_return(mjs, MJS_UNDEFINED); + } + double x = mjs_get_double(mjs, mjs_arg(mjs, 0)); + mjs_return(mjs, x < 0 ? mjs_mk_number(mjs, -x) : mjs_arg(mjs, 0)); +} + +void js_math_acos(struct mjs* mjs) { + if(!check_arg_count(mjs, 1) || !mjs_is_number(mjs_arg(mjs, 0))) { + mjs_return(mjs, MJS_UNDEFINED); + } + double x = mjs_get_double(mjs, mjs_arg(mjs, 0)); + if(x < -1 || x > 1) { + ret_bad_args(mjs, "Invalid input value for Math.acos"); + mjs_return(mjs, MJS_UNDEFINED); + } + mjs_return(mjs, mjs_mk_number(mjs, JS_MATH_PI / (double)2 - atan(x / sqrt(1 - x * x)))); +} + +void js_math_acosh(struct mjs* mjs) { + if(!check_arg_count(mjs, 1) || !mjs_is_number(mjs_arg(mjs, 0))) { + mjs_return(mjs, MJS_UNDEFINED); + } + double x = mjs_get_double(mjs, mjs_arg(mjs, 0)); + if(x < 1) { + ret_bad_args(mjs, "Invalid input value for Math.acosh"); + mjs_return(mjs, MJS_UNDEFINED); + } + mjs_return(mjs, mjs_mk_number(mjs, log(x + sqrt(x * x - 1)))); +} + +void js_math_asin(struct mjs* mjs) { + if(!check_arg_count(mjs, 1) || !mjs_is_number(mjs_arg(mjs, 0))) { + mjs_return(mjs, MJS_UNDEFINED); + } + double x = mjs_get_double(mjs, mjs_arg(mjs, 0)); + mjs_return(mjs, mjs_mk_number(mjs, atan(x / sqrt(1 - x * x)))); +} + +void js_math_asinh(struct mjs* mjs) { + if(!check_arg_count(mjs, 1) || !mjs_is_number(mjs_arg(mjs, 0))) { + mjs_return(mjs, MJS_UNDEFINED); + } + double x = mjs_get_double(mjs, mjs_arg(mjs, 0)); + mjs_return(mjs, mjs_mk_number(mjs, log(x + sqrt(x * x + 1)))); +} + +void js_math_atan(struct mjs* mjs) { + if(!check_arg_count(mjs, 1) || !mjs_is_number(mjs_arg(mjs, 0))) { + mjs_return(mjs, MJS_UNDEFINED); + } + double x = mjs_get_double(mjs, mjs_arg(mjs, 0)); + mjs_return(mjs, mjs_mk_number(mjs, atan(x))); +} + +void js_math_atan2(struct mjs* mjs) { + if(!check_arg_count(mjs, 2) || !mjs_is_number(mjs_arg(mjs, 0)) || + !mjs_is_number(mjs_arg(mjs, 1))) { + mjs_return(mjs, MJS_UNDEFINED); + } + double y = mjs_get_double(mjs, mjs_arg(mjs, 0)); + double x = mjs_get_double(mjs, mjs_arg(mjs, 1)); + mjs_return(mjs, mjs_mk_number(mjs, atan2(y, x))); +} + +void js_math_atanh(struct mjs* mjs) { + if(!check_arg_count(mjs, 1) || !mjs_is_number(mjs_arg(mjs, 0))) { + mjs_return(mjs, MJS_UNDEFINED); + } + double x = mjs_get_double(mjs, mjs_arg(mjs, 0)); + if(x <= -1 || x >= 1) { + ret_bad_args(mjs, "Invalid input value for Math.atanh"); + mjs_return(mjs, MJS_UNDEFINED); + } + mjs_return(mjs, mjs_mk_number(mjs, (double)0.5 * log((1 + x) / (1 - x)))); +} + +void js_math_cbrt(struct mjs* mjs) { + if(!check_arg_count(mjs, 1) || !mjs_is_number(mjs_arg(mjs, 0))) { + mjs_return(mjs, MJS_UNDEFINED); + } + double x = mjs_get_double(mjs, mjs_arg(mjs, 0)); + mjs_return(mjs, mjs_mk_number(mjs, pow(x, 1.0 / 3.0))); +} + +void js_math_ceil(struct mjs* mjs) { + if(!check_arg_count(mjs, 1) || !mjs_is_number(mjs_arg(mjs, 0))) { + mjs_return(mjs, MJS_UNDEFINED); + } + double x = mjs_get_double(mjs, mjs_arg(mjs, 0)); + mjs_return(mjs, mjs_mk_number(mjs, (int)(x + (double)0.5))); +} + +void js_math_clz32(struct mjs* mjs) { + if(!check_arg_count(mjs, 1) || !mjs_is_number(mjs_arg(mjs, 0))) { + mjs_return(mjs, MJS_UNDEFINED); + } + unsigned int x = (unsigned int)mjs_get_int(mjs, mjs_arg(mjs, 0)); + int count = 0; + while(x) { + x >>= 1; + count++; + } + mjs_return(mjs, mjs_mk_number(mjs, 32 - count)); +} + +void js_math_cos(struct mjs* mjs) { + if(!check_arg_count(mjs, 1) || !mjs_is_number(mjs_arg(mjs, 0))) { + mjs_return(mjs, MJS_UNDEFINED); + } + double x = mjs_get_double(mjs, mjs_arg(mjs, 0)); + mjs_return(mjs, mjs_mk_number(mjs, cos(x))); +} + +void js_math_exp(struct mjs* mjs) { + if(!check_arg_count(mjs, 1) || !mjs_is_number(mjs_arg(mjs, 0))) { + mjs_return(mjs, MJS_UNDEFINED); + } + double x = mjs_get_double(mjs, mjs_arg(mjs, 0)); + double result = 1; + double term = 1; + for(int i = 1; i < 100; i++) { + term *= x / i; + result += term; + } + mjs_return(mjs, mjs_mk_number(mjs, result)); +} + +void js_math_floor(struct mjs* mjs) { + if(!check_arg_count(mjs, 1) || !mjs_is_number(mjs_arg(mjs, 0))) { + mjs_return(mjs, MJS_UNDEFINED); + } + double x = mjs_get_double(mjs, mjs_arg(mjs, 0)); + mjs_return(mjs, mjs_mk_number(mjs, (int)x)); +} + +void js_math_log(struct mjs* mjs) { + if(!check_arg_count(mjs, 1) || !mjs_is_number(mjs_arg(mjs, 0))) { + mjs_return(mjs, MJS_UNDEFINED); + } + double x = mjs_get_double(mjs, mjs_arg(mjs, 0)); + if(x <= 0) { + ret_bad_args(mjs, "Invalid input value for Math.log"); + mjs_return(mjs, MJS_UNDEFINED); + } + double result = 0; + while(x >= JS_MATH_E) { + x /= JS_MATH_E; + result++; + } + mjs_return(mjs, mjs_mk_number(mjs, result + log(x))); +} + +void js_math_max(struct mjs* mjs) { + if(!check_arg_count(mjs, 2) || !mjs_is_number(mjs_arg(mjs, 0)) || + !mjs_is_number(mjs_arg(mjs, 1))) { + mjs_return(mjs, MJS_UNDEFINED); + } + double x = mjs_get_double(mjs, mjs_arg(mjs, 0)); + double y = mjs_get_double(mjs, mjs_arg(mjs, 1)); + mjs_return(mjs, mjs_mk_number(mjs, x > y ? x : y)); +} + +void js_math_min(struct mjs* mjs) { + if(!check_arg_count(mjs, 2) || !mjs_is_number(mjs_arg(mjs, 0)) || + !mjs_is_number(mjs_arg(mjs, 1))) { + mjs_return(mjs, MJS_UNDEFINED); + } + double x = mjs_get_double(mjs, mjs_arg(mjs, 0)); + double y = mjs_get_double(mjs, mjs_arg(mjs, 1)); + mjs_return(mjs, mjs_mk_number(mjs, x < y ? x : y)); +} + +void js_math_pow(struct mjs* mjs) { + if(!check_arg_count(mjs, 2) || !mjs_is_number(mjs_arg(mjs, 0)) || + !mjs_is_number(mjs_arg(mjs, 1))) { + mjs_return(mjs, MJS_UNDEFINED); + } + double base = mjs_get_double(mjs, mjs_arg(mjs, 0)); + double exponent = mjs_get_double(mjs, mjs_arg(mjs, 1)); + double result = 1; + for(int i = 0; i < exponent; i++) { + result *= base; + } + mjs_return(mjs, mjs_mk_number(mjs, result)); +} + +void js_math_random(struct mjs* mjs) { + if(!check_arg_count(mjs, 0)) { + mjs_return(mjs, MJS_UNDEFINED); + } + const uint32_t random_val = furi_hal_random_get(); + double rnd = (double)random_val / RAND_MAX; + mjs_return(mjs, mjs_mk_number(mjs, rnd)); +} + +void js_math_sign(struct mjs* mjs) { + if(!check_arg_count(mjs, 1) || !mjs_is_number(mjs_arg(mjs, 0))) { + mjs_return(mjs, MJS_UNDEFINED); + } + double x = mjs_get_double(mjs, mjs_arg(mjs, 0)); + mjs_return(mjs, mjs_mk_number(mjs, x == 0 ? 0 : (x < 0 ? -1 : 1))); +} + +void js_math_sin(struct mjs* mjs) { + if(!check_arg_count(mjs, 1) || !mjs_is_number(mjs_arg(mjs, 0))) { + mjs_return(mjs, MJS_UNDEFINED); + } + double x = mjs_get_double(mjs, mjs_arg(mjs, 0)); + double result = x; + double term = x; + for(int i = 1; i < 10; i++) { + term *= -x * x / ((2 * i) * (2 * i + 1)); + result += term; + } + mjs_return(mjs, mjs_mk_number(mjs, result)); +} + +void js_math_sqrt(struct mjs* mjs) { + if(!check_arg_count(mjs, 1) || !mjs_is_number(mjs_arg(mjs, 0))) { + mjs_return(mjs, MJS_UNDEFINED); + } + double x = mjs_get_double(mjs, mjs_arg(mjs, 0)); + if(x < 0) { + ret_bad_args(mjs, "Invalid input value for Math.sqrt"); + mjs_return(mjs, MJS_UNDEFINED); + } + double result = 1; + while(result * result < x) { + result += (double)0.001; + } + mjs_return(mjs, mjs_mk_number(mjs, result)); +} + +void js_math_trunc(struct mjs* mjs) { + if(!check_arg_count(mjs, 1) || !mjs_is_number(mjs_arg(mjs, 0))) { + mjs_return(mjs, MJS_UNDEFINED); + } + double x = mjs_get_double(mjs, mjs_arg(mjs, 0)); + mjs_return(mjs, mjs_mk_number(mjs, x < 0 ? ceil(x) : floor(x))); +} + +static void* js_math_create(struct mjs* mjs, mjs_val_t* object) { + mjs_val_t math_obj = mjs_mk_object(mjs); + mjs_set(mjs, math_obj, "abs", ~0, MJS_MK_FN(js_math_abs)); + mjs_set(mjs, math_obj, "acos", ~0, MJS_MK_FN(js_math_acos)); + mjs_set(mjs, math_obj, "acosh", ~0, MJS_MK_FN(js_math_acosh)); + mjs_set(mjs, math_obj, "asin", ~0, MJS_MK_FN(js_math_asin)); + mjs_set(mjs, math_obj, "asinh", ~0, MJS_MK_FN(js_math_asinh)); + mjs_set(mjs, math_obj, "atan", ~0, MJS_MK_FN(js_math_atan)); + mjs_set(mjs, math_obj, "atan2", ~0, MJS_MK_FN(js_math_atan2)); + mjs_set(mjs, math_obj, "atanh", ~0, MJS_MK_FN(js_math_atanh)); + mjs_set(mjs, math_obj, "cbrt", ~0, MJS_MK_FN(js_math_cbrt)); + mjs_set(mjs, math_obj, "ceil", ~0, MJS_MK_FN(js_math_ceil)); + mjs_set(mjs, math_obj, "clz32", ~0, MJS_MK_FN(js_math_clz32)); + mjs_set(mjs, math_obj, "cos", ~0, MJS_MK_FN(js_math_cos)); + mjs_set(mjs, math_obj, "exp", ~0, MJS_MK_FN(js_math_exp)); + mjs_set(mjs, math_obj, "floor", ~0, MJS_MK_FN(js_math_floor)); + mjs_set(mjs, math_obj, "log", ~0, MJS_MK_FN(js_math_log)); + mjs_set(mjs, math_obj, "max", ~0, MJS_MK_FN(js_math_max)); + mjs_set(mjs, math_obj, "min", ~0, MJS_MK_FN(js_math_min)); + mjs_set(mjs, math_obj, "pow", ~0, MJS_MK_FN(js_math_pow)); + mjs_set(mjs, math_obj, "random", ~0, MJS_MK_FN(js_math_random)); + mjs_set(mjs, math_obj, "sign", ~0, MJS_MK_FN(js_math_sign)); + mjs_set(mjs, math_obj, "sin", ~0, MJS_MK_FN(js_math_sin)); + mjs_set(mjs, math_obj, "sqrt", ~0, MJS_MK_FN(js_math_sqrt)); + mjs_set(mjs, math_obj, "trunc", ~0, MJS_MK_FN(js_math_trunc)); + mjs_set(mjs, math_obj, "PI", ~0, mjs_mk_number(mjs, JS_MATH_PI)); + mjs_set(mjs, math_obj, "E", ~0, mjs_mk_number(mjs, JS_MATH_E)); + *object = math_obj; + return (void*)1; +} + +static const JsModuleDescriptor js_math_desc = { + "math", + js_math_create, + NULL, +}; + +static const FlipperAppPluginDescriptor plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_math_desc, +}; + +const FlipperAppPluginDescriptor* js_math_ep(void) { + return &plugin_descriptor; +} \ No newline at end of file diff --git a/applications/system/js_app/modules/js_subghz/js_subghz.c b/applications/system/js_app/modules/js_subghz/js_subghz.c new file mode 100644 index 000000000..913f8c670 --- /dev/null +++ b/applications/system/js_app/modules/js_subghz/js_subghz.c @@ -0,0 +1,391 @@ +#include "../../js_modules.h" +#include "radio_device_loader.h" + +#include +#include +#include + +#include + +#define tag "js_subghz" + +typedef enum { + JsSubghzRadioStateRX, + JsSubghzRadioStateTX, + JsSubghzRadioStateIDLE, +} JsSubghzRadioState; + +typedef struct { + const SubGhzDevice* radio_device; + int frequency; + bool is_external; + JsSubghzRadioState state; +} JsSubghzInst; + +// from subghz cli +static FuriHalSubGhzPreset js_subghz_get_preset_name(const char* preset_name) { + FuriHalSubGhzPreset preset = FuriHalSubGhzPresetIDLE; + if(!strcmp(preset_name, "FuriHalSubGhzPresetOok270Async")) { + preset = FuriHalSubGhzPresetOok270Async; + } else if(!strcmp(preset_name, "FuriHalSubGhzPresetOok650Async")) { + preset = FuriHalSubGhzPresetOok650Async; + } else if(!strcmp(preset_name, "FuriHalSubGhzPreset2FSKDev238Async")) { + preset = FuriHalSubGhzPreset2FSKDev238Async; + } else if(!strcmp(preset_name, "FuriHalSubGhzPreset2FSKDev476Async")) { + preset = FuriHalSubGhzPreset2FSKDev476Async; + } else if(!strcmp(preset_name, "FuriHalSubGhzPresetCustom")) { + preset = FuriHalSubGhzPresetCustom; + } else { + FURI_LOG_I(tag, "unknown preset"); + } + return preset; +} + +static int32_t get_int_arg(struct mjs* mjs, size_t index, int32_t* value) { + mjs_val_t int_obj = mjs_arg(mjs, index); + if(!mjs_is_number(int_obj)) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a number"); + mjs_return(mjs, MJS_UNDEFINED); + return false; + } + *value = mjs_get_int(mjs, int_obj); + return true; +} + +static void js_subghz_set_rx(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsSubghzInst* js_subghz = mjs_get_ptr(mjs, obj_inst); + furi_assert(js_subghz); + + if(js_subghz->state == JsSubghzRadioStateRX) { + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + subghz_devices_set_rx(js_subghz->radio_device); + js_subghz->state = JsSubghzRadioStateRX; +} + +static void js_subgjz_set_idle(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsSubghzInst* js_subghz = mjs_get_ptr(mjs, obj_inst); + furi_assert(js_subghz); + + if(js_subghz->state == JsSubghzRadioStateIDLE) { + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + subghz_devices_idle(js_subghz->radio_device); + js_subghz->state = JsSubghzRadioStateIDLE; +} + +static void js_subghz_get_rssi(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsSubghzInst* js_subghz = mjs_get_ptr(mjs, obj_inst); + furi_assert(js_subghz); + + if(js_subghz->state != JsSubghzRadioStateRX) { + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + float rssi = subghz_devices_get_rssi(js_subghz->radio_device); + mjs_return(mjs, mjs_mk_number(mjs, (double)rssi)); +} + +static void js_subghz_get_state(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsSubghzInst* js_subghz = mjs_get_ptr(mjs, obj_inst); + furi_assert(js_subghz); + + const char* state; + switch(js_subghz->state) { + case JsSubghzRadioStateRX: + state = "RX"; + break; + case JsSubghzRadioStateTX: + state = "TX"; + break; + case JsSubghzRadioStateIDLE: + state = "IDLE"; + break; + default: + state = ""; + break; + } + + mjs_return(mjs, mjs_mk_string(mjs, state, ~0, true)); +} + +static void js_subghz_is_external(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsSubghzInst* js_subghz = mjs_get_ptr(mjs, obj_inst); + furi_assert(js_subghz); + + mjs_return(mjs, mjs_mk_boolean(mjs, js_subghz->is_external)); +} + +static void js_subghz_set_frequency(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsSubghzInst* js_subghz = mjs_get_ptr(mjs, obj_inst); + furi_assert(js_subghz); + + if(js_subghz->state != JsSubghzRadioStateIDLE) { + mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Radio is not in IDLE state"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + int32_t frequency; + if(!get_int_arg(mjs, 0, &frequency)) return; + + if(!subghz_devices_is_frequency_valid(js_subghz->radio_device, frequency)) { + mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Invalid frequency"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + js_subghz->frequency = subghz_devices_set_frequency(js_subghz->radio_device, frequency); + + mjs_return(mjs, mjs_mk_number(mjs, (double)js_subghz->frequency)); +} + +static void js_subghz_get_frequency(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsSubghzInst* js_subghz = mjs_get_ptr(mjs, obj_inst); + furi_assert(js_subghz); + + mjs_return(mjs, mjs_mk_number(mjs, (double)js_subghz->frequency)); +} + +static void js_subghz_transmit_file(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsSubghzInst* js_subghz = mjs_get_ptr(mjs, obj_inst); + furi_assert(js_subghz); + + mjs_val_t file = mjs_arg(mjs, 0); + + if(!mjs_is_string(file)) { + mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "File must be a string"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + const char* file_path = mjs_get_string(mjs, &file, NULL); + if(!file_path) { + mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Failed to get file path"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + Storage* storage = furi_record_open(RECORD_STORAGE); + FlipperFormat* fff_file = flipper_format_file_alloc(storage); + FlipperFormat* fff_data_raw = flipper_format_string_alloc(); + + if(!flipper_format_file_open_existing(fff_file, file_path)) { + flipper_format_free(fff_file); + furi_record_close(RECORD_STORAGE); + mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Failed to open file"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + SubGhzEnvironment* environment = subghz_environment_alloc(); + if(!subghz_environment_load_keystore(environment, SUBGHZ_KEYSTORE_DIR_NAME)) { + FURI_LOG_I(tag, "Load_keystore keeloq_mfcodes \033[0;31mERROR\033[0m\r\n"); + } + if(!subghz_environment_load_keystore(environment, SUBGHZ_KEYSTORE_DIR_USER_NAME)) { + FURI_LOG_I(tag, "Load_keystore keeloq_mfcodes_user \033[0;33mAbsent\033[0m\r\n"); + } + subghz_environment_set_alutech_at_4n_rainbow_table_file_name( + environment, SUBGHZ_ALUTECH_AT_4N_DIR_NAME); + subghz_environment_set_nice_flor_s_rainbow_table_file_name( + environment, SUBGHZ_NICE_FLOR_S_DIR_NAME); + subghz_environment_set_protocol_registry(environment, (void*)&subghz_protocol_registry); + + FuriString* temp_str = furi_string_alloc(); + SubGhzTransmitter* transmitter = NULL; + bool is_init_protocol = true; + bool is_sent = false; + uint32_t frequency = 0; + uint32_t repeat = 10; + + do { + //Load frequency + if(!flipper_format_read_uint32(fff_file, "Frequency", &frequency, 1)) { + mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Failed to read frequency from file"); + mjs_return(mjs, MJS_UNDEFINED); + break; + } + + if(!subghz_devices_is_frequency_valid(js_subghz->radio_device, frequency)) { + mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Invalid frequency"); + mjs_return(mjs, MJS_UNDEFINED); + break; + } + + if(!flipper_format_read_string(fff_file, "Preset", temp_str)) { + mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Failed to read preset from file"); + mjs_return(mjs, MJS_UNDEFINED); + break; + } + + subghz_devices_reset(js_subghz->radio_device); + + if(!strcmp(furi_string_get_cstr(temp_str), "FuriHalSubGhzPresetCustom")) { + mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Custom presets are not supported (yet)"); + mjs_return(mjs, MJS_UNDEFINED); + break; + } else { + subghz_devices_load_preset( + js_subghz->radio_device, + js_subghz_get_preset_name(furi_string_get_cstr(temp_str)), + NULL); + } + + js_subghz->frequency = subghz_devices_set_frequency(js_subghz->radio_device, frequency); + + if(!flipper_format_read_string(fff_file, "Protocol", temp_str)) { + mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Failed to read protocol from file"); + mjs_return(mjs, MJS_UNDEFINED); + break; + } + + SubGhzProtocolStatus status; + bool is_raw = false; + + if(!strcmp(furi_string_get_cstr(temp_str), "RAW")) { + subghz_protocol_raw_gen_fff_data( + fff_data_raw, file_path, subghz_devices_get_name(js_subghz->radio_device)); + is_raw = true; + } + + transmitter = subghz_transmitter_alloc_init(environment, furi_string_get_cstr(temp_str)); + if(transmitter == NULL) { + is_init_protocol = false; + } + + if(is_init_protocol) { + status = subghz_transmitter_deserialize(transmitter, is_raw ? fff_data_raw : fff_file); + if(status != SubGhzProtocolStatusOk) { + FURI_LOG_I(tag, "failed to deserialize transmitter"); + is_init_protocol = false; + } + } else { + FURI_LOG_I(tag, "failed to allocate transmitter"); + subghz_devices_idle(js_subghz->radio_device); + js_subghz->state = JsSubghzRadioStateIDLE; + } + } while(false); + + if(is_init_protocol) { + if(!js_subghz->is_external) { + furi_hal_power_suppress_charge_enter(); + } + + FURI_LOG_I(tag, "transmitting file %s", file_path); + + do { + furi_delay_ms(200); + if(subghz_devices_start_async_tx( + js_subghz->radio_device, subghz_transmitter_yield, transmitter)) { + while(!subghz_devices_is_async_complete_tx(js_subghz->radio_device)) { + furi_delay_ms(333); + } + subghz_devices_stop_async_tx(js_subghz->radio_device); + is_sent = true; + } else { + FURI_LOG_E(tag, "failed to start async tx"); + } + + } while(repeat && !strcmp(furi_string_get_cstr(temp_str), "RAW")); + + subghz_devices_idle(js_subghz->radio_device); + js_subghz->state = JsSubghzRadioStateIDLE; + + if(!js_subghz->is_external) { + furi_hal_power_suppress_charge_exit(); + } + } + + furi_string_free(temp_str); + flipper_format_free(fff_file); + flipper_format_free(fff_data_raw); + furi_record_close(RECORD_STORAGE); + + subghz_environment_reset_keeloq(environment); + subghz_environment_free(environment); + subghz_transmitter_free(transmitter); + + mjs_return(mjs, mjs_mk_boolean(mjs, is_sent)); +} + +static void js_subghz_setup(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsSubghzInst* js_subghz = mjs_get_ptr(mjs, obj_inst); + furi_assert(js_subghz); + + js_subghz->radio_device = + radio_device_loader_set(js_subghz->radio_device, SubGhzRadioDeviceTypeExternalCC1101); + + if(!subghz_devices_is_connect(js_subghz->radio_device)) { + js_subghz->is_external = true; + } else { + js_subghz->is_external = false; + } + + js_subghz->state = JsSubghzRadioStateIDLE; + js_subghz->frequency = 433920000; + + subghz_devices_reset(js_subghz->radio_device); + subghz_devices_idle(js_subghz->radio_device); + + mjs_return(mjs, MJS_UNDEFINED); +} + +static void* js_subghz_create(struct mjs* mjs, mjs_val_t* object) { + JsSubghzInst* js_subghz = malloc(sizeof(JsSubghzInst)); + mjs_val_t subghz_obj = mjs_mk_object(mjs); + + subghz_devices_init(); + + mjs_set(mjs, subghz_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, js_subghz)); + mjs_set(mjs, subghz_obj, "setup", ~0, MJS_MK_FN(js_subghz_setup)); + mjs_set(mjs, subghz_obj, "setRx", ~0, MJS_MK_FN(js_subghz_set_rx)); + mjs_set(mjs, subghz_obj, "setIdle", ~0, MJS_MK_FN(js_subgjz_set_idle)); + mjs_set(mjs, subghz_obj, "getRssi", ~0, MJS_MK_FN(js_subghz_get_rssi)); + mjs_set(mjs, subghz_obj, "getState", ~0, MJS_MK_FN(js_subghz_get_state)); + mjs_set(mjs, subghz_obj, "getFrequency", ~0, MJS_MK_FN(js_subghz_get_frequency)); + mjs_set(mjs, subghz_obj, "setFrequency", ~0, MJS_MK_FN(js_subghz_set_frequency)); + mjs_set(mjs, subghz_obj, "isExternal", ~0, MJS_MK_FN(js_subghz_is_external)); + mjs_set(mjs, subghz_obj, "transmitFile", ~0, MJS_MK_FN(js_subghz_transmit_file)); + + *object = subghz_obj; + + return js_subghz; +} + +static void js_subghz_destroy(void* inst) { + JsSubghzInst* js_subghz = inst; + + subghz_devices_deinit(); + + free(js_subghz); +} + +static const JsModuleDescriptor js_subghz_desc = { + "subghz", + js_subghz_create, + js_subghz_destroy, +}; + +static const FlipperAppPluginDescriptor plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_subghz_desc, +}; + +const FlipperAppPluginDescriptor* js_subghz_ep(void) { + return &plugin_descriptor; +} diff --git a/applications/system/js_app/modules/js_subghz/radio_device_loader.c b/applications/system/js_app/modules/js_subghz/radio_device_loader.c new file mode 100644 index 000000000..d2cffde58 --- /dev/null +++ b/applications/system/js_app/modules/js_subghz/radio_device_loader.c @@ -0,0 +1,64 @@ +#include "radio_device_loader.h" + +#include +#include + +static void radio_device_loader_power_on() { + uint8_t attempts = 0; + while(!furi_hal_power_is_otg_enabled() && attempts++ < 5) { + furi_hal_power_enable_otg(); + //CC1101 power-up time + furi_delay_ms(10); + } +} + +static void radio_device_loader_power_off() { + if(furi_hal_power_is_otg_enabled()) furi_hal_power_disable_otg(); +} + +bool radio_device_loader_is_connect_external(const char* name) { + bool is_connect = false; + bool is_otg_enabled = furi_hal_power_is_otg_enabled(); + + if(!is_otg_enabled) { + radio_device_loader_power_on(); + } + + const SubGhzDevice* device = subghz_devices_get_by_name(name); + if(device) { + is_connect = subghz_devices_is_connect(device); + } + + if(!is_otg_enabled) { + radio_device_loader_power_off(); + } + return is_connect; +} + +const SubGhzDevice* radio_device_loader_set( + const SubGhzDevice* current_radio_device, + SubGhzRadioDeviceType radio_device_type) { + const SubGhzDevice* radio_device; + + if(radio_device_type == SubGhzRadioDeviceTypeExternalCC1101 && + radio_device_loader_is_connect_external(SUBGHZ_DEVICE_CC1101_EXT_NAME)) { + radio_device_loader_power_on(); + radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_EXT_NAME); + subghz_devices_begin(radio_device); + } else if(current_radio_device == NULL) { + radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME); + } else { + radio_device_loader_end(current_radio_device); + radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME); + } + + return radio_device; +} + +void radio_device_loader_end(const SubGhzDevice* radio_device) { + furi_assert(radio_device); + radio_device_loader_power_off(); + if(radio_device != subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME)) { + subghz_devices_end(radio_device); + } +} \ No newline at end of file diff --git a/applications/system/js_app/modules/js_subghz/radio_device_loader.h b/applications/system/js_app/modules/js_subghz/radio_device_loader.h new file mode 100644 index 000000000..bee4e2c36 --- /dev/null +++ b/applications/system/js_app/modules/js_subghz/radio_device_loader.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +/** SubGhzRadioDeviceType */ +typedef enum { + SubGhzRadioDeviceTypeInternal, + SubGhzRadioDeviceTypeExternalCC1101, +} SubGhzRadioDeviceType; + +const SubGhzDevice* radio_device_loader_set( + const SubGhzDevice* current_radio_device, + SubGhzRadioDeviceType radio_device_type); + +void radio_device_loader_end(const SubGhzDevice* radio_device); \ No newline at end of file diff --git a/applications/system/js_app/modules/js_submenu.c b/applications/system/js_app/modules/js_submenu.c new file mode 100644 index 000000000..b87f34fa8 --- /dev/null +++ b/applications/system/js_app/modules/js_submenu.c @@ -0,0 +1,152 @@ +#include +#include +#include +#include "../js_modules.h" + +typedef struct { + Submenu* submenu; + ViewDispatcher* view_dispatcher; + uint32_t result; +} JsSubmenuInst; + +typedef enum { + JsSubmenuViewSubmenu, +} JsSubmenuView; + +static JsSubmenuInst* get_this_ctx(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsSubmenuInst* storage = mjs_get_ptr(mjs, obj_inst); + furi_assert(storage); + return storage; +} + +static void ret_bad_args(struct mjs* mjs, const char* error) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error); + mjs_return(mjs, MJS_UNDEFINED); +} + +static bool check_arg_count(struct mjs* mjs, size_t count) { + size_t num_args = mjs_nargs(mjs); + if(num_args != count) { + ret_bad_args(mjs, "Wrong argument count"); + return false; + } + return true; +} + +static bool get_str_arg(struct mjs* mjs, size_t index, const char** value) { + mjs_val_t str_obj = mjs_arg(mjs, index); + if(!mjs_is_string(str_obj)) { + ret_bad_args(mjs, "Argument must be a string"); + return false; + } + size_t str_len = 0; + *value = mjs_get_string(mjs, &str_obj, &str_len); + if((str_len == 0) || (*value == NULL)) { + ret_bad_args(mjs, "Bad string argument"); + return false; + } + return true; +} + +static int32_t get_int_arg(struct mjs* mjs, size_t index, int32_t* value) { + mjs_val_t int_obj = mjs_arg(mjs, index); + if(!mjs_is_number(int_obj)) { + ret_bad_args(mjs, "Argument must be a number"); + return false; + } + *value = mjs_get_int32(mjs, int_obj); + return true; +} + +static void submenu_callback(void* context, uint32_t id) { + UNUSED(id); + JsSubmenuInst* submenu = context; + submenu->result = id; + view_dispatcher_stop(submenu->view_dispatcher); +} + +static void js_submenu_add_item(struct mjs* mjs) { + JsSubmenuInst* submenu = get_this_ctx(mjs); + if(!check_arg_count(mjs, 2)) return; + + const char* label; + if(!get_str_arg(mjs, 0, &label)) return; + + int32_t id; + if(!get_int_arg(mjs, 1, &id)) return; + + submenu_add_item(submenu->submenu, label, id, submenu_callback, submenu); + + mjs_return(mjs, MJS_UNDEFINED); +} + +static void js_submenu_set_header(struct mjs* mjs) { + JsSubmenuInst* submenu = get_this_ctx(mjs); + if(!check_arg_count(mjs, 1)) return; + + const char* header; + if(!get_str_arg(mjs, 0, &header)) return; + + submenu_set_header(submenu->submenu, header); + + mjs_return(mjs, MJS_UNDEFINED); +} + +static void js_submenu_show(struct mjs* mjs) { + JsSubmenuInst* submenu = get_this_ctx(mjs); + if(!check_arg_count(mjs, 0)) return; + submenu->result = 0; + + view_dispatcher_attach_to_gui( + submenu->view_dispatcher, furi_record_open(RECORD_GUI), ViewDispatcherTypeFullscreen); + furi_record_close(RECORD_GUI); + + view_dispatcher_switch_to_view(submenu->view_dispatcher, JsSubmenuViewSubmenu); + + view_dispatcher_run(submenu->view_dispatcher); + + submenu_reset(submenu->submenu); + + mjs_return(mjs, mjs_mk_number(mjs, submenu->result)); +} + +static void* js_submenu_create(struct mjs* mjs, mjs_val_t* object) { + JsSubmenuInst* submenu = malloc(sizeof(JsSubmenuInst)); + mjs_val_t submenu_obj = mjs_mk_object(mjs); + mjs_set(mjs, submenu_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, submenu)); + mjs_set(mjs, submenu_obj, "addItem", ~0, MJS_MK_FN(js_submenu_add_item)); + mjs_set(mjs, submenu_obj, "setHeader", ~0, MJS_MK_FN(js_submenu_set_header)); + mjs_set(mjs, submenu_obj, "show", ~0, MJS_MK_FN(js_submenu_show)); + submenu->submenu = submenu_alloc(); + submenu->view_dispatcher = view_dispatcher_alloc(); + view_dispatcher_enable_queue(submenu->view_dispatcher); + view_dispatcher_add_view( + submenu->view_dispatcher, JsSubmenuViewSubmenu, submenu_get_view(submenu->submenu)); + *object = submenu_obj; + return submenu; +} + +static void js_submenu_destroy(void* inst) { + JsSubmenuInst* submenu = inst; + view_dispatcher_remove_view(submenu->view_dispatcher, JsSubmenuViewSubmenu); + submenu_free(submenu->submenu); + view_dispatcher_free(submenu->view_dispatcher); + free(submenu); +} + +static const JsModuleDescriptor js_submenu_desc = { + "submenu", + js_submenu_create, + js_submenu_destroy, +}; + +static const FlipperAppPluginDescriptor submenu_plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_submenu_desc, +}; + +const FlipperAppPluginDescriptor* js_submenu_ep(void) { + return &submenu_plugin_descriptor; +} \ No newline at end of file diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 03c12a66b..85f25d00e 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -991,6 +991,7 @@ Function,-,exp2f,float,float Function,-,exp2l,long double,long double Function,+,expansion_disable,void,Expansion* Function,+,expansion_enable,void,Expansion* +Function,+,expansion_is_connected,_Bool,Expansion* Function,+,expansion_set_listen_serial,void,"Expansion*, FuriHalSerialId" Function,-,expf,float,float Function,-,expl,long double,long double