From ad94694fbd8aa35a72ea53d03420d56281a69f04 Mon Sep 17 00:00:00 2001 From: WillyJL Date: Wed, 24 Sep 2025 11:19:18 +0200 Subject: [PATCH 01/12] CLI: Fix long delay with quick connect/disconnect (#4251) * CLI: Fix long delay with quick connect/disconnect noticeable with qflipper, for some reason it sets DTR on/off/on again so flipper starts CLI, stops it, then starts again but cli shell is trying to print motd, and pipe is already broken so each character of motd times out for 100ms changing pipe timeout to 10ms works too, but is just a workaround it didnt always happen, i think that variable time of loading ext cmds made it so on ofw it usually manages to print motd before pipe is broken on cfw with more ext commands it always hung, on ofw only sometimes important part is bailing early in cli shell in cli vcp i made it cleanup cli shell on disconnect so it doesnt stay around after disconnecting usb, might free a little ram maybe * cli_shell: possibly more robust fix? * Fix use after free crash * cli_shell: waste even less time Co-Authored-By: WillyJL --------- Co-authored-by: Anna Antonenko Co-authored-by: hedger --- applications/services/cli/cli_vcp.c | 13 +++++-------- lib/toolbox/cli/shell/cli_shell.c | 16 +++++++++++----- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/applications/services/cli/cli_vcp.c b/applications/services/cli/cli_vcp.c index 1f9c77b08..9db1bec43 100644 --- a/applications/services/cli/cli_vcp.c +++ b/applications/services/cli/cli_vcp.c @@ -218,6 +218,11 @@ static void cli_vcp_internal_event_happened(FuriEventLoopObject* object, void* c pipe_detach_from_event_loop(cli_vcp->own_pipe); pipe_free(cli_vcp->own_pipe); cli_vcp->own_pipe = NULL; + + // wait for shell to stop + cli_shell_join(cli_vcp->shell); + cli_shell_free(cli_vcp->shell); + pipe_free(cli_vcp->shell_pipe); break; } @@ -226,14 +231,6 @@ static void cli_vcp_internal_event_happened(FuriEventLoopObject* object, void* c FURI_LOG_D(TAG, "Connected"); cli_vcp->is_connected = true; - // wait for previous shell to stop - furi_check(!cli_vcp->own_pipe); - if(cli_vcp->shell) { - cli_shell_join(cli_vcp->shell); - cli_shell_free(cli_vcp->shell); - pipe_free(cli_vcp->shell_pipe); - } - // start shell thread PipeSideBundle bundle = pipe_alloc(VCP_BUF_SIZE, 1); cli_vcp->own_pipe = bundle.alices_side; diff --git a/lib/toolbox/cli/shell/cli_shell.c b/lib/toolbox/cli/shell/cli_shell.c index 7a4c7ec1f..b2648d127 100644 --- a/lib/toolbox/cli/shell/cli_shell.c +++ b/lib/toolbox/cli/shell/cli_shell.c @@ -16,7 +16,8 @@ #define TAG "CliShell" -#define ANSI_TIMEOUT_MS 10 +#define ANSI_TIMEOUT_MS 10 +#define TRANSIENT_SESSION_WINDOW_MS 100 typedef enum { CliShellComponentCompletions, @@ -415,10 +416,15 @@ static void cli_shell_deinit(CliShell* shell) { static int32_t cli_shell_thread(void* context) { CliShell* shell = context; - // Sometimes, the other side closes the pipe even before our thread is started. Although the - // rest of the code will eventually find this out if this check is removed, there's no point in - // wasting time. - if(pipe_state(shell->pipe) == PipeStateBroken) return 0; + // Sometimes, the other side (e.g. qFlipper) closes the pipe even before our thread is started. + // Although the rest of the code will eventually find this out if this check is removed, + // there's no point in wasting time. This gives qFlipper a chance to quickly close and re-open + // the session. + const size_t delay_step = 10; + for(size_t i = 0; i < TRANSIENT_SESSION_WINDOW_MS / delay_step; i++) { + furi_delay_ms(delay_step); + if(pipe_state(shell->pipe) == PipeStateBroken) return 0; + } cli_shell_init(shell); FURI_LOG_D(TAG, "Started"); From dfd753703ad10177aa133154c8125d53720c8d2e Mon Sep 17 00:00:00 2001 From: dogtopus Date: Wed, 24 Sep 2025 19:08:40 +0900 Subject: [PATCH 02/12] FeliCa Emulation: Handle certain Polling commands in firmware (#4204) * FeliCa: Handle non-hardware Polling commands NFC TagInfo and possibly other readers rely on Polling commands with Request Code of 1 (default System Code request) or non-FFFF System Code to detect card type. Since the NFC controller doesn't seem to handle them in hardware and simply bubbles them up, and then the Flipper firmware will just ignore them and refuse to respond afterwards, this causes the reading operation to fail. This commit adds a simple handler for such Polling commands so that readers behaving like NFC TagInfo could read the emulated card without failing. * Only handle cases when System Code is not FFFF The NFC controller should handle Polling commands with the System Code set to FFFF, so it's not necessary for the firmware to handle it. * Remove system code logging * More cleanups * Remove the claim that we need a poller change We already have enough information to determine whether or not the card supports NDEF since SYS_OP register value is included in all current Lite-S card dumps. * Respond to 12FC polling command when needed * Handle Polling with NDEF and Lite-S Service Code This allows the reader to specifically select the service by naming the Service Code. * Introduce API for manual handling of Polling commands Introduce nfc_felica_listener_timer_anticol_start() and nfc_felica_listener_timer_anticol_stop(). These are for now just wrappers around the block_tx timer that can be used to delay the response until the desired Time Slot. Thanks to the loose timing constraints of FeliCa collision resolution protocol, no compensation seems to be necessary. Also enabled the block_tx timer for FeliCa listener, but with both compensation and fdt set to 0 to keep the original behavior of not using the timer during normal data exchange. This API is now being used for handling Polling commands that are not handled by the NFC controller on the hardware side. * Document target_time_slot * Implement changes suggested by @RebornedBrain * api: f18 version sync * nfc: added stubs for `nfc_felica_listener_timer_anticol` for unit tests --------- Co-authored-by: hedger Co-authored-by: hedger --- lib/nfc/nfc.c | 19 ++++ lib/nfc/nfc.h | 18 ++++ lib/nfc/nfc_mock.c | 10 +++ lib/nfc/protocols/felica/felica.h | 3 +- lib/nfc/protocols/felica/felica_listener.c | 93 +++++++++++++++++++- lib/nfc/protocols/felica/felica_listener_i.c | 13 --- lib/nfc/protocols/felica/felica_listener_i.h | 37 +++++++- targets/f18/api_symbols.csv | 2 +- targets/f7/api_symbols.csv | 4 +- targets/f7/furi_hal/furi_hal_nfc_felica.c | 3 +- 10 files changed, 182 insertions(+), 20 deletions(-) diff --git a/lib/nfc/nfc.c b/lib/nfc/nfc.c index 90e65b282..e843ef70c 100644 --- a/lib/nfc/nfc.c +++ b/lib/nfc/nfc.c @@ -9,6 +9,9 @@ #define NFC_MAX_BUFFER_SIZE (256) +#define NFC_FELICA_LISTENER_RESPONSE_TIME_A_FC (512 * 64) +#define NFC_FELICA_LISTENER_RESPONSE_TIME_B_FC (256 * 64) + typedef enum { NfcStateIdle, NfcStateRunning, @@ -661,4 +664,20 @@ NfcError nfc_felica_listener_set_sensf_res_data( return nfc_process_hal_error(error); } +void nfc_felica_listener_timer_anticol_start(Nfc* instance, uint8_t target_time_slot) { + furi_check(instance); + + furi_hal_nfc_timer_block_tx_start( + NFC_FELICA_LISTENER_RESPONSE_TIME_A_FC + + target_time_slot * NFC_FELICA_LISTENER_RESPONSE_TIME_B_FC); +} + +void nfc_felica_listener_timer_anticol_stop(Nfc* instance) { + furi_check(instance); + + if(furi_hal_nfc_timer_block_tx_is_running()) { + furi_hal_nfc_timer_block_tx_stop(); + } +} + #endif // FW_CFG_unit_tests diff --git a/lib/nfc/nfc.h b/lib/nfc/nfc.h index 8fbf90d1f..bbbaf306b 100644 --- a/lib/nfc/nfc.h +++ b/lib/nfc/nfc.h @@ -380,6 +380,24 @@ NfcError nfc_felica_listener_set_sensf_res_data( */ NfcError nfc_iso15693_listener_tx_sof(Nfc* instance); +/** + * @brief Start the timer used for manual FeliCa collision resolution in listener mode. + * + * This blocks TX until the desired Time Slot, and should be called as soon as the listener + * determines that a collision resolution needs to be handled manually. + * + * @param[in, out] instance instance pointer to the instance to be configured. + * @param[in] target_time_slot Target Time Slot number. Should be a value within the range of 0-15 (double-inclusive). + */ +void nfc_felica_listener_timer_anticol_start(Nfc* instance, uint8_t target_time_slot); + +/** + * @brief Cancel the timer used for manual FeliCa collision resolution in listener mode. + * + * @param[in, out] instance instance pointer to the instance to be configured. + */ +void nfc_felica_listener_timer_anticol_stop(Nfc* instance); + #ifdef __cplusplus } #endif diff --git a/lib/nfc/nfc_mock.c b/lib/nfc/nfc_mock.c index ee4bb09cc..717c93519 100644 --- a/lib/nfc/nfc_mock.c +++ b/lib/nfc/nfc_mock.c @@ -518,4 +518,14 @@ NfcError nfc_felica_listener_set_sensf_res_data( return NfcErrorNone; } +void nfc_felica_listener_timer_anticol_start(Nfc* instance, uint8_t target_time_slot) { + furi_check(instance); + + UNUSED(target_time_slot); +} + +void nfc_felica_listener_timer_anticol_stop(Nfc* instance) { + furi_check(instance); +} + #endif diff --git a/lib/nfc/protocols/felica/felica.h b/lib/nfc/protocols/felica/felica.h index 7b9c78e07..b4e7af9d1 100644 --- a/lib/nfc/protocols/felica/felica.h +++ b/lib/nfc/protocols/felica/felica.h @@ -38,7 +38,7 @@ extern "C" { #define FELICA_FDT_POLL_FC (10000U) #define FELICA_POLL_POLL_MIN_US (1280U) -#define FELICA_FDT_LISTEN_FC (1172) +#define FELICA_FDT_LISTEN_FC (0) #define FELICA_SYSTEM_CODE_CODE (0xFFFFU) #define FELICA_TIME_SLOT_1 (0x00U) @@ -58,6 +58,7 @@ typedef enum { FelicaErrorWrongCrc, FelicaErrorProtocol, FelicaErrorTimeout, + FelicaErrorFeatureUnsupported, } FelicaError; typedef struct { diff --git a/lib/nfc/protocols/felica/felica_listener.c b/lib/nfc/protocols/felica/felica_listener.c index 90954de97..3bf109366 100644 --- a/lib/nfc/protocols/felica/felica_listener.c +++ b/lib/nfc/protocols/felica/felica_listener.c @@ -5,9 +5,20 @@ #include #define FELICA_LISTENER_MAX_BUFFER_SIZE (128) +#define FELICA_LISTENER_CMD_POLLING (0x00U) +#define FELICA_LISTENER_RESPONSE_POLLING (0x01U) #define FELICA_LISTENER_RESPONSE_CODE_READ (0x07) #define FELICA_LISTENER_RESPONSE_CODE_WRITE (0x09) +#define FELICA_LISTENER_REQUEST_NONE (0x00U) +#define FELICA_LISTENER_REQUEST_SYSTEM_CODE (0x01U) +#define FELICA_LISTENER_REQUEST_PERFORMANCE (0x02U) + +#define FELICA_LISTENER_SYSTEM_CODE_NDEF (__builtin_bswap16(0x12FCU)) +#define FELICA_LISTENER_SYSTEM_CODE_LITES (__builtin_bswap16(0x88B4U)) + +#define FELICA_LISTENER_PERFORMANCE_VALUE (__builtin_bswap16(0x0083U)) + #define TAG "FelicaListener" FelicaListener* felica_listener_alloc(Nfc* nfc, FelicaData* data) { @@ -151,6 +162,70 @@ static FelicaError felica_listener_process_request( } } +static void felica_listener_populate_polling_response_header( + FelicaListener* instance, + FelicaListenerPollingResponseHeader* resp) { + resp->idm = instance->data->idm; + resp->pmm = instance->data->pmm; + resp->response_code = FELICA_LISTENER_RESPONSE_POLLING; +} + +static bool felica_listener_check_system_code( + const FelicaListenerGenericRequest* const generic_request, + uint16_t code) { + return ( + generic_request->polling.system_code == code || + generic_request->polling.system_code == (code | 0x00FFU) || + generic_request->polling.system_code == (code | 0xFF00U)); +} + +static uint16_t felica_listener_get_response_system_code( + FelicaListener* instance, + const FelicaListenerGenericRequest* const generic_request) { + uint16_t resp_system_code = FELICA_SYSTEM_CODE_CODE; + if(felica_listener_check_system_code(generic_request, FELICA_LISTENER_SYSTEM_CODE_NDEF) && + instance->data->data.fs.mc.data[FELICA_MC_SYS_OP] == 1) { + // NDEF + resp_system_code = FELICA_LISTENER_SYSTEM_CODE_NDEF; + } else if(felica_listener_check_system_code( + generic_request, FELICA_LISTENER_SYSTEM_CODE_LITES)) { + // Lite-S + resp_system_code = FELICA_LISTENER_SYSTEM_CODE_LITES; + } + return resp_system_code; +} + +static FelicaError felica_listener_process_system_code( + FelicaListener* instance, + const FelicaListenerGenericRequest* const generic_request) { + FelicaError result = FelicaErrorFeatureUnsupported; + do { + uint16_t resp_system_code = + felica_listener_get_response_system_code(instance, generic_request); + if(resp_system_code == FELICA_SYSTEM_CODE_CODE) break; + + FelicaListenerPollingResponse* resp = malloc(sizeof(FelicaListenerPollingResponse)); + felica_listener_populate_polling_response_header(instance, &resp->header); + + resp->header.length = sizeof(FelicaListenerPollingResponse); + if(generic_request->polling.request_code == FELICA_LISTENER_REQUEST_SYSTEM_CODE) { + resp->optional_request_data = resp_system_code; + } else if(generic_request->polling.request_code == FELICA_LISTENER_REQUEST_PERFORMANCE) { + resp->optional_request_data = FELICA_LISTENER_PERFORMANCE_VALUE; + } else { + resp->header.length = sizeof(FelicaListenerPollingResponseHeader); + } + + bit_buffer_reset(instance->tx_buffer); + bit_buffer_append_bytes(instance->tx_buffer, (uint8_t*)resp, resp->header.length); + free(resp); + + result = felica_listener_frame_exchange(instance, instance->tx_buffer); + } while(false); + + return result; +} + NfcCommand felica_listener_run(NfcGenericEvent event, void* context) { furi_assert(context); furi_assert(event.protocol == NfcProtocolInvalid); @@ -187,7 +262,23 @@ NfcCommand felica_listener_run(NfcGenericEvent event, void* context) { break; } - if(!felica_listener_check_idm(instance, &request->header.idm)) { + if(request->header.code == FELICA_LISTENER_CMD_POLLING) { + // Will always respond at Time Slot 0 for now. + nfc_felica_listener_timer_anticol_start(instance->nfc, 0); + if(request->polling.system_code != FELICA_SYSTEM_CODE_CODE) { + FelicaError error = felica_listener_process_system_code(instance, request); + if(error == FelicaErrorFeatureUnsupported) { + command = NfcCommandReset; + } else if(error != FelicaErrorNone) { + FURI_LOG_E( + TAG, "Error when handling Polling with System Code: %2X", error); + } + break; + } else { + FURI_LOG_E(TAG, "Hardware Polling command leaking through"); + break; + } + } else if(!felica_listener_check_idm(instance, &request->header.idm)) { FURI_LOG_E(TAG, "Wrong IDm"); break; } diff --git a/lib/nfc/protocols/felica/felica_listener_i.c b/lib/nfc/protocols/felica/felica_listener_i.c index b3ae24635..d69c8c66a 100644 --- a/lib/nfc/protocols/felica/felica_listener_i.c +++ b/lib/nfc/protocols/felica/felica_listener_i.c @@ -7,19 +7,6 @@ #define FELICA_WCNT_MC2_00_WARNING_BEGIN_VALUE (0x00001027U) #define FELICA_WCNT_MC2_00_WARNING_END_VALUE (0x00FFFDFFU) -#define FELICA_MC_SP_REG_ALL_RW_BYTES_0_1 (0U) -#define FELICA_MC_ALL_BYTE (2U) -#define FELICA_MC_SYS_OP (3U) -#define FELICA_MC_RF_PRM (4U) -#define FELICA_MC_CKCKV_W_MAC_A (5U) -#define FELICA_MC_SP_REG_R_RESTR_BYTES_6_7 (6U) -#define FELICA_MC_SP_REG_W_RESTR_BYTES_8_9 (8U) -#define FELICA_MC_SP_REG_W_MAC_A_BYTES_10_11 (10U) -#define FELICA_MC_STATE_W_MAC_A (12U) -#define FELICA_MC_RESERVED_13 (13U) -#define FELICA_MC_RESERVED_14 (14U) -#define FELICA_MC_RESERVED_15 (15U) - #define FELICA_MC_BYTE_GET(data, byte) (data->data.fs.mc.data[byte]) #define FELICA_SYSTEM_BLOCK_RO_ACCESS(data) (FELICA_MC_BYTE_GET(data, FELICA_MC_ALL_BYTE) == 0x00) #define FELICA_SYSTEM_BLOCK_RW_ACCESS(data) (FELICA_MC_BYTE_GET(data, FELICA_MC_ALL_BYTE) == 0xFF) diff --git a/lib/nfc/protocols/felica/felica_listener_i.h b/lib/nfc/protocols/felica/felica_listener_i.h index 0ea144249..f64a598cc 100644 --- a/lib/nfc/protocols/felica/felica_listener_i.h +++ b/lib/nfc/protocols/felica/felica_listener_i.h @@ -7,15 +7,50 @@ #define FELICA_LISTENER_WRITE_BLOCK_COUNT_MAX (2U) #define FELICA_LISTENER_WRITE_BLOCK_COUNT_MIN (1U) +#define FELICA_MC_SP_REG_ALL_RW_BYTES_0_1 (0U) +#define FELICA_MC_ALL_BYTE (2U) +#define FELICA_MC_SYS_OP (3U) +#define FELICA_MC_RF_PRM (4U) +#define FELICA_MC_CKCKV_W_MAC_A (5U) +#define FELICA_MC_SP_REG_R_RESTR_BYTES_6_7 (6U) +#define FELICA_MC_SP_REG_W_RESTR_BYTES_8_9 (8U) +#define FELICA_MC_SP_REG_W_MAC_A_BYTES_10_11 (10U) +#define FELICA_MC_STATE_W_MAC_A (12U) +#define FELICA_MC_RESERVED_13 (13U) +#define FELICA_MC_RESERVED_14 (14U) +#define FELICA_MC_RESERVED_15 (15U) + typedef enum { Felica_ListenerStateIdle, Felica_ListenerStateActivated, } FelicaListenerState; +typedef struct FURI_PACKED { + uint8_t code; + uint16_t system_code; + uint8_t request_code; + uint8_t time_slot; +} FelicaListenerPollingHeader; + +typedef struct { + uint8_t length; + uint8_t response_code; + FelicaIDm idm; + FelicaPMm pmm; +} FelicaListenerPollingResponseHeader; + +typedef struct FURI_PACKED { + FelicaListenerPollingResponseHeader header; + uint16_t optional_request_data; +} FelicaListenerPollingResponse; + /** Generic Felica request same for both read and write operations. */ typedef struct { uint8_t length; - FelicaCommandHeader header; + union { + FelicaCommandHeader header; + FelicaListenerPollingHeader polling; + }; } FelicaListenerGenericRequest; /** Generic request but with list of requested elements. */ diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 23c9edb63..fbbb5d13f 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,86.0,, +Version,+,86.1,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index d142a6374..f2b13e4a9 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,86.0,, +Version,+,86.1,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -2859,6 +2859,8 @@ Function,+,nfc_device_set_data,void,"NfcDevice*, NfcProtocol, const NfcDeviceDat Function,+,nfc_device_set_loading_callback,void,"NfcDevice*, NfcLoadingCallback, void*" Function,+,nfc_device_set_uid,_Bool,"NfcDevice*, const uint8_t*, size_t" Function,+,nfc_felica_listener_set_sensf_res_data,NfcError,"Nfc*, const uint8_t*, const uint8_t, const uint8_t*, const uint8_t, const uint16_t" +Function,+,nfc_felica_listener_timer_anticol_start,void,"Nfc*, uint8_t" +Function,+,nfc_felica_listener_timer_anticol_stop,void,Nfc* Function,+,nfc_free,void,Nfc* Function,+,nfc_iso14443a_listener_set_col_res_data,NfcError,"Nfc*, uint8_t*, uint8_t, uint8_t*, uint8_t" Function,+,nfc_iso14443a_listener_tx_custom_parity,NfcError,"Nfc*, const BitBuffer*" diff --git a/targets/f7/furi_hal/furi_hal_nfc_felica.c b/targets/f7/furi_hal/furi_hal_nfc_felica.c index 1267b7b13..49c59eb79 100644 --- a/targets/f7/furi_hal/furi_hal_nfc_felica.c +++ b/targets/f7/furi_hal/furi_hal_nfc_felica.c @@ -1,8 +1,7 @@ #include "furi_hal_nfc_i.h" #include "furi_hal_nfc_tech_i.h" -// Prevent FDT timer from starting -#define FURI_HAL_NFC_FELICA_LISTENER_FDT_COMP_FC (INT32_MAX) +#define FURI_HAL_NFC_FELICA_LISTENER_FDT_COMP_FC (0) #define FURI_HAL_FELICA_COMMUNICATION_PERFORMANCE (0x0083U) #define FURI_HAL_FELICA_RESPONSE_CODE (0x01) From 7f0e8f39d37f1ae858cbdae9e9f1b1c2a4e0d409 Mon Sep 17 00:00:00 2001 From: WillyJL Date: Wed, 24 Sep 2025 12:24:09 +0200 Subject: [PATCH 03/12] FuriHalSerial: Fix RXFNE interrupt hang (#4246) * Expansion: Wake thread on UART error flag * Expansion: Stop UART async rx early * FuriHalSerial: Fix RXFNE interrupt hang --------- Co-authored-by: hedger --- .../services/expansion/expansion_worker.c | 5 +- targets/f7/furi_hal/furi_hal_serial.c | 67 +++++++++++-------- 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/applications/services/expansion/expansion_worker.c b/applications/services/expansion/expansion_worker.c index ac2a5935b..703a72607 100644 --- a/applications/services/expansion/expansion_worker.c +++ b/applications/services/expansion/expansion_worker.c @@ -35,7 +35,8 @@ typedef enum { ExpansionWorkerFlagError = 1 << 2, } ExpansionWorkerFlag; -#define EXPANSION_ALL_FLAGS (ExpansionWorkerFlagData | ExpansionWorkerFlagStop) +#define EXPANSION_ALL_FLAGS \ + (ExpansionWorkerFlagData | ExpansionWorkerFlagStop | ExpansionWorkerFlagError) struct ExpansionWorker { FuriThread* thread; @@ -360,6 +361,8 @@ static int32_t expansion_worker(void* context) { expansion_worker_state_machine(instance); } + furi_hal_serial_async_rx_stop(instance->serial_handle); + if(instance->state == ExpansionWorkerStateRpcActive) { expansion_worker_rpc_session_close(instance); } diff --git a/targets/f7/furi_hal/furi_hal_serial.c b/targets/f7/furi_hal/furi_hal_serial.c index 8ad9794a8..e5c30a278 100644 --- a/targets/f7/furi_hal/furi_hal_serial.c +++ b/targets/f7/furi_hal/furi_hal_serial.c @@ -817,6 +817,21 @@ static void furi_hal_serial_async_rx_configure( FuriHalSerialHandle* handle, FuriHalSerialAsyncRxCallback callback, void* context) { + // Disable RXFNE interrupts before unsetting the user callback that reads data + // Otherwise interrupt runs without reading data and without clearing RXFNE flag + // This would cause a system hang as the same interrupt runs in loop forever + if(!callback) { + if(handle->id == FuriHalSerialIdUsart) { + LL_USART_DisableIT_RXNE_RXFNE(USART1); + furi_hal_interrupt_set_isr(FuriHalInterruptIdUart1, NULL, NULL); + furi_hal_serial_usart_deinit_dma_rx(); + } else if(handle->id == FuriHalSerialIdLpuart) { + LL_LPUART_DisableIT_RXNE_RXFNE(LPUART1); + furi_hal_interrupt_set_isr(FuriHalInterruptIdLpUart1, NULL, NULL); + furi_hal_serial_lpuart_deinit_dma_rx(); + } + } + // Handle must be configured before enabling RX interrupt // as it might be triggered right away on a misconfigured handle furi_hal_serial[handle->id].rx_byte_callback = callback; @@ -824,27 +839,17 @@ static void furi_hal_serial_async_rx_configure( furi_hal_serial[handle->id].rx_dma_callback = NULL; furi_hal_serial[handle->id].context = context; - if(handle->id == FuriHalSerialIdUsart) { - if(callback) { + if(callback) { + if(handle->id == FuriHalSerialIdUsart) { furi_hal_serial_usart_deinit_dma_rx(); furi_hal_interrupt_set_isr( FuriHalInterruptIdUart1, furi_hal_serial_usart_irq_callback, NULL); LL_USART_EnableIT_RXNE_RXFNE(USART1); - } else { - furi_hal_interrupt_set_isr(FuriHalInterruptIdUart1, NULL, NULL); - furi_hal_serial_usart_deinit_dma_rx(); - LL_USART_DisableIT_RXNE_RXFNE(USART1); - } - } else if(handle->id == FuriHalSerialIdLpuart) { - if(callback) { + } else if(handle->id == FuriHalSerialIdLpuart) { furi_hal_serial_lpuart_deinit_dma_rx(); furi_hal_interrupt_set_isr( FuriHalInterruptIdLpUart1, furi_hal_serial_lpuart_irq_callback, NULL); LL_LPUART_EnableIT_RXNE_RXFNE(LPUART1); - } else { - furi_hal_interrupt_set_isr(FuriHalInterruptIdLpUart1, NULL, NULL); - furi_hal_serial_lpuart_deinit_dma_rx(); - LL_LPUART_DisableIT_RXNE_RXFNE(LPUART1); } } } @@ -944,33 +949,39 @@ static void furi_hal_serial_dma_configure( FuriHalSerialHandle* handle, FuriHalSerialDmaRxCallback callback, void* context) { - furi_check(handle); - - if(handle->id == FuriHalSerialIdUsart) { - if(callback) { - furi_hal_serial_usart_init_dma_rx(); - furi_hal_interrupt_set_isr( - FuriHalInterruptIdUart1, furi_hal_serial_usart_irq_callback, NULL); - } else { + // Disable RXFNE interrupts before unsetting the user callback that reads data + // Otherwise interrupt runs without reading data and without clearing RXFNE flag + // This would cause a system hang as the same interrupt runs in loop forever + if(!callback) { + if(handle->id == FuriHalSerialIdUsart) { LL_USART_DisableIT_RXNE_RXFNE(USART1); furi_hal_interrupt_set_isr(FuriHalInterruptIdUart1, NULL, NULL); furi_hal_serial_usart_deinit_dma_rx(); - } - } else if(handle->id == FuriHalSerialIdLpuart) { - if(callback) { - furi_hal_serial_lpuart_init_dma_rx(); - furi_hal_interrupt_set_isr( - FuriHalInterruptIdLpUart1, furi_hal_serial_lpuart_irq_callback, NULL); - } else { + } else if(handle->id == FuriHalSerialIdLpuart) { LL_LPUART_DisableIT_RXNE_RXFNE(LPUART1); furi_hal_interrupt_set_isr(FuriHalInterruptIdLpUart1, NULL, NULL); furi_hal_serial_lpuart_deinit_dma_rx(); } } + + // Handle must be configured before enabling RX interrupt + // as it might be triggered right away on a misconfigured handle furi_hal_serial[handle->id].rx_byte_callback = NULL; furi_hal_serial[handle->id].handle = handle; furi_hal_serial[handle->id].rx_dma_callback = callback; furi_hal_serial[handle->id].context = context; + + if(callback) { + if(handle->id == FuriHalSerialIdUsart) { + furi_hal_serial_usart_init_dma_rx(); + furi_hal_interrupt_set_isr( + FuriHalInterruptIdUart1, furi_hal_serial_usart_irq_callback, NULL); + } else if(handle->id == FuriHalSerialIdLpuart) { + furi_hal_serial_lpuart_init_dma_rx(); + furi_hal_interrupt_set_isr( + FuriHalInterruptIdLpUart1, furi_hal_serial_lpuart_irq_callback, NULL); + } + } } void furi_hal_serial_dma_rx_start( From 6a49131ae96d48fd4581e9851911c40269192f44 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Wed, 24 Sep 2025 14:34:28 +0400 Subject: [PATCH 04/12] js_gpio: stop pwm on exit (#4206) Co-authored-by: hedger --- applications/system/js_app/modules/js_gpio.c | 33 ++++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/applications/system/js_app/modules/js_gpio.c b/applications/system/js_app/modules/js_gpio.c index 63de6900a..df7d494a6 100644 --- a/applications/system/js_app/modules/js_gpio.c +++ b/applications/system/js_app/modules/js_gpio.c @@ -23,6 +23,7 @@ typedef struct { } JsGpioPinInst; ARRAY_DEF(ManagedPinsArray, JsGpioPinInst*, M_PTR_OPLIST); //-V575 +#define M_OPL_ManagedPinsArray_t() ARRAY_OPLIST(ManagedPinsArray) /** * Per-module instance control structure @@ -444,20 +445,26 @@ static void js_gpio_destroy(void* 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); + for + M_EACH(item, module->managed_pins, ManagedPinsArray_t) { + JsGpioPinInst* manager_data = *item; + + if(manager_data->had_interrupt) { + furi_hal_gpio_disable_int_callback(manager_data->pin); + furi_hal_gpio_remove_int_callback(manager_data->pin); + } + if(manager_data->pwm_output != FuriHalPwmOutputIdNone) { + if(furi_hal_pwm_is_running(manager_data->pwm_output)) + furi_hal_pwm_stop(manager_data->pwm_output); + } + 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); } - 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); From 7554b325382628eac47972e232397e461c7fe362 Mon Sep 17 00:00:00 2001 From: MMX <10697207+xMasterX@users.noreply.github.com> Date: Wed, 24 Sep 2025 13:45:03 +0300 Subject: [PATCH 05/12] SubGHz: Linear and Dickert MAHS protocols fixes & improvements (#4267) * Linear and dickert mahs fixes * fix dip pattern * proper fix by WillyJL * Fix unit tests Linear Decoder now decodes more linear signals --------- Co-authored-by: hedger --- .../unit_tests/tests/subghz/subghz_test.c | 2 +- lib/subghz/protocols/dickert_mahs.c | 5 ++-- lib/subghz/protocols/linear.c | 29 ++++++++++--------- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/applications/debug/unit_tests/tests/subghz/subghz_test.c b/applications/debug/unit_tests/tests/subghz/subghz_test.c index ac14bce6a..a58875fe8 100644 --- a/applications/debug/unit_tests/tests/subghz/subghz_test.c +++ b/applications/debug/unit_tests/tests/subghz/subghz_test.c @@ -17,7 +17,7 @@ #define NICE_FLOR_S_DIR_NAME EXT_PATH("subghz/assets/nice_flor_s") #define ALUTECH_AT_4N_DIR_NAME EXT_PATH("subghz/assets/alutech_at_4n") #define TEST_RANDOM_DIR_NAME EXT_PATH("unit_tests/subghz/test_random_raw.sub") -#define TEST_RANDOM_COUNT_PARSE 329 +#define TEST_RANDOM_COUNT_PARSE 331 #define TEST_TIMEOUT 10000 static SubGhzEnvironment* environment_handler; diff --git a/lib/subghz/protocols/dickert_mahs.c b/lib/subghz/protocols/dickert_mahs.c index 4691e3423..3f614412b 100644 --- a/lib/subghz/protocols/dickert_mahs.c +++ b/lib/subghz/protocols/dickert_mahs.c @@ -288,8 +288,9 @@ void subghz_protocol_decoder_dickert_mahs_feed(void* context, bool level, uint32 instance->decoder.decode_count_bit = 0; } - if((!level) && (duration > 10 * subghz_protocol_dickert_mahs_const.te_short)) { - //Found header DICKERT_MAHS + if((!level) && (DURATION_DIFF(duration, subghz_protocol_dickert_mahs_const.te_long * 50) < + subghz_protocol_dickert_mahs_const.te_delta * 70)) { + //Found header DICKERT_MAHS 44k us instance->decoder.parser_step = DickertMAHSDecoderStepInitial; } break; diff --git a/lib/subghz/protocols/linear.c b/lib/subghz/protocols/linear.c index f9d7aaca8..7671dc428 100644 --- a/lib/subghz/protocols/linear.c +++ b/lib/subghz/protocols/linear.c @@ -18,7 +18,7 @@ static const SubGhzBlockConst subghz_protocol_linear_const = { .te_short = 500, .te_long = 1500, - .te_delta = 150, + .te_delta = 350, .min_count_bit_for_found = 10, }; @@ -116,7 +116,7 @@ static bool subghz_protocol_encoder_linear_get_upload(SubGhzProtocolEncoderLinea if(bit_read(instance->generic.data, i - 1)) { //send bit 1 instance->encoder.upload[index++] = - level_duration_make(true, (uint32_t)subghz_protocol_linear_const.te_short * 3); + level_duration_make(true, (uint32_t)subghz_protocol_linear_const.te_long); instance->encoder.upload[index++] = level_duration_make(false, (uint32_t)subghz_protocol_linear_const.te_short); } else { @@ -124,22 +124,22 @@ static bool subghz_protocol_encoder_linear_get_upload(SubGhzProtocolEncoderLinea instance->encoder.upload[index++] = level_duration_make(true, (uint32_t)subghz_protocol_linear_const.te_short); instance->encoder.upload[index++] = - level_duration_make(false, (uint32_t)subghz_protocol_linear_const.te_short * 3); + level_duration_make(false, (uint32_t)subghz_protocol_linear_const.te_long); } } //Send end bit if(bit_read(instance->generic.data, 0)) { //send bit 1 instance->encoder.upload[index++] = - level_duration_make(true, (uint32_t)subghz_protocol_linear_const.te_short * 3); - //Send PT_GUARD + level_duration_make(true, (uint32_t)subghz_protocol_linear_const.te_long); + //Send gap instance->encoder.upload[index++] = level_duration_make(false, (uint32_t)subghz_protocol_linear_const.te_short * 42); } else { //send bit 0 instance->encoder.upload[index++] = level_duration_make(true, (uint32_t)subghz_protocol_linear_const.te_short); - //Send PT_GUARD + //Send gap instance->encoder.upload[index++] = level_duration_make(false, (uint32_t)subghz_protocol_linear_const.te_short * 44); } @@ -223,7 +223,7 @@ void subghz_protocol_decoder_linear_feed(void* context, bool level, uint32_t dur switch(instance->decoder.parser_step) { case LinearDecoderStepReset: if((!level) && (DURATION_DIFF(duration, subghz_protocol_linear_const.te_short * 42) < - subghz_protocol_linear_const.te_delta * 20)) { + subghz_protocol_linear_const.te_delta * 15)) { //Found header Linear instance->decoder.decode_data = 0; instance->decoder.decode_count_bit = 0; @@ -244,7 +244,7 @@ void subghz_protocol_decoder_linear_feed(void* context, bool level, uint32_t dur instance->decoder.parser_step = LinearDecoderStepReset; //checking that the duration matches the guardtime if(DURATION_DIFF(duration, subghz_protocol_linear_const.te_short * 42) > - subghz_protocol_linear_const.te_delta * 20) { + subghz_protocol_linear_const.te_delta * 15) { break; } if(DURATION_DIFF(instance->decoder.te_last, subghz_protocol_linear_const.te_short) < @@ -321,18 +321,21 @@ void subghz_protocol_decoder_linear_get_string(void* context, FuriString* output furi_assert(context); SubGhzProtocolDecoderLinear* instance = context; - uint32_t code_found_lo = instance->generic.data & 0x00000000ffffffff; + // Protocol is actually implemented wrong way around, bits are inverted. + // Instead of fixing it and breaking old saved remotes, + // only the display here is inverted (~) to show correct values. + uint32_t code_found_lo = ~instance->generic.data & 0x00000000000003ff; uint64_t code_found_reverse = subghz_protocol_blocks_reverse_key( - instance->generic.data, instance->generic.data_count_bit); + ~instance->generic.data, instance->generic.data_count_bit); - uint32_t code_found_reverse_lo = code_found_reverse & 0x00000000ffffffff; + uint32_t code_found_reverse_lo = code_found_reverse & 0x00000000000003ff; furi_string_cat_printf( output, "%s %dbit\r\n" - "Key:0x%08lX\r\n" - "Yek:0x%08lX\r\n" + "Key:0x%03lX\r\n" + "Yek:0x%03lX\r\n" "DIP:" DIP_PATTERN "\r\n", instance->generic.protocol_name, instance->generic.data_count_bit, From 7483069d1049d1208d624fed254b34123a59ee95 Mon Sep 17 00:00:00 2001 From: LordMZTE Date: Wed, 24 Sep 2025 16:52:06 +0200 Subject: [PATCH 06/12] hid_app mouse clicker: make mouse button selectable (#4270) * hid_app mouse clicker: make mouse button selectable * hid_app: fixed uninit var warning --------- Co-authored-by: hedger Co-authored-by: hedger --- .../system/hid_app/views/hid_mouse_clicker.c | 58 ++++++++++++++++++- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/applications/system/hid_app/views/hid_mouse_clicker.c b/applications/system/hid_app/views/hid_mouse_clicker.c index 491f9962b..e9fd9170d 100644 --- a/applications/system/hid_app/views/hid_mouse_clicker.c +++ b/applications/system/hid_app/views/hid_mouse_clicker.c @@ -19,6 +19,7 @@ typedef struct { bool connected; bool running; int rate; + enum HidMouseButtons btn; } HidMouseClickerModel; static void hid_mouse_clicker_start_or_restart_timer(void* context) { @@ -61,6 +62,26 @@ static void hid_mouse_clicker_draw_callback(Canvas* canvas, void* context) { // Ok canvas_draw_icon(canvas, 58, 25, &I_Space_65x18); + canvas_draw_icon(canvas, 61, 50, &I_ButtonLeft_4x7); + canvas_draw_icon(canvas, 117, 50, &I_ButtonRight_4x7); + + const char* btn_label; + switch(model->btn) { + case HID_MOUSE_BTN_LEFT: + btn_label = "Left"; + break; + case HID_MOUSE_BTN_WHEEL: + btn_label = "Middle"; + break; + case HID_MOUSE_BTN_RIGHT: + btn_label = "Right"; + break; + default: + furi_crash(); + } + + elements_multiline_text_aligned(canvas, 89, 57, AlignCenter, AlignBottom, btn_label); + if(model->running) { elements_slightly_rounded_box(canvas, 61, 27, 60, 13); canvas_set_color(canvas, ColorWhite); @@ -100,8 +121,8 @@ static void hid_mouse_clicker_timer_callback(void* context) { HidMouseClickerModel * model, { if(model->running) { - hid_hal_mouse_press(hid_mouse_clicker->hid, HID_MOUSE_BTN_LEFT); - hid_hal_mouse_release(hid_mouse_clicker->hid, HID_MOUSE_BTN_LEFT); + hid_hal_mouse_press(hid_mouse_clicker->hid, model->btn); + hid_hal_mouse_release(hid_mouse_clicker->hid, model->btn); } }, false); @@ -154,6 +175,34 @@ static bool hid_mouse_clicker_input_callback(InputEvent* event, void* context) { case InputKeyBack: model->running = false; break; + case InputKeyLeft: + switch(model->btn) { + case HID_MOUSE_BTN_LEFT: + model->btn = HID_MOUSE_BTN_RIGHT; + break; + case HID_MOUSE_BTN_WHEEL: + model->btn = HID_MOUSE_BTN_LEFT; + break; + case HID_MOUSE_BTN_RIGHT: + model->btn = HID_MOUSE_BTN_WHEEL; + break; + } + consumed = true; + break; + case InputKeyRight: + switch(model->btn) { + case HID_MOUSE_BTN_LEFT: + model->btn = HID_MOUSE_BTN_WHEEL; + break; + case HID_MOUSE_BTN_WHEEL: + model->btn = HID_MOUSE_BTN_RIGHT; + break; + case HID_MOUSE_BTN_RIGHT: + model->btn = HID_MOUSE_BTN_LEFT; + break; + } + consumed = true; + break; default: consumed = true; break; @@ -188,7 +237,10 @@ HidMouseClicker* hid_mouse_clicker_alloc(Hid* hid) { with_view_model( hid_mouse_clicker->view, HidMouseClickerModel * model, - { model->rate = DEFAULT_CLICK_RATE; }, + { + model->rate = DEFAULT_CLICK_RATE; + model->btn = HID_MOUSE_BTN_LEFT; + }, true); return hid_mouse_clicker; From f0d4210d19f3637c79aa0a010b098d7aa390202a Mon Sep 17 00:00:00 2001 From: Zinong Li <131403964+zinongli@users.noreply.github.com> Date: Wed, 24 Sep 2025 19:14:43 +0400 Subject: [PATCH 07/12] NFC: Amusement IC Card Parser (FeliCa Lite & Lite-S) (#4259) * type/brand parse * debug * decryptions * corrections * comments * AIC access code decryption and CRC check * easier than we thought! only acc_code[0] == 5 needs parsing * finishing touches * can't support serial number decode * Revert "can't support serial number decode" This reverts commit 7761b31bcf7effb97e608578ab1e090625078784. * serial number CAN be decrypted * format * print fix * fix CRC check * delete unnecessary line * linter fixes --------- Co-authored-by: hedger Co-authored-by: hedger --- applications/main/nfc/application.fam | 9 + .../main/nfc/plugins/supported_cards/aic.c | 678 ++++++++++++++++++ 2 files changed, 687 insertions(+) create mode 100644 applications/main/nfc/plugins/supported_cards/aic.c diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index 6342d18bf..a9c8808a8 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -274,3 +274,12 @@ App( requires=["nfc"], sources=["plugins/supported_cards/banapass.c"], ) + +App( + appid="aic_parser", + apptype=FlipperAppType.PLUGIN, + entry_point="aic_plugin_ep", + targets=["f7"], + requires=["nfc"], + sources=["plugins/supported_cards/aic.c"], +) diff --git a/applications/main/nfc/plugins/supported_cards/aic.c b/applications/main/nfc/plugins/supported_cards/aic.c new file mode 100644 index 000000000..c8f303eb9 --- /dev/null +++ b/applications/main/nfc/plugins/supported_cards/aic.c @@ -0,0 +1,678 @@ +// https://sega.bsnk.me/allnet/amusement_ic/ + +#include "nfc_supported_card_plugin.h" +#include +#include +#include +#include +#include +#include + +#define TAG "Amusement IC" + +#define N_TABLES 8 +#define ITERATION_ADD 5 + +static const uint8_t s_box[9][256] = { + {0x24, 0x3c, 0xba, 0x36, 0xe3, 0x85, 0xa4, 0xd0, 0x93, 0x43, 0x73, 0xb9, 0x70, 0x6e, 0xc9, + 0xf1, 0x10, 0x0e, 0x9b, 0x2c, 0x97, 0xe7, 0x0b, 0x63, 0x6c, 0x29, 0x20, 0xfe, 0x86, 0xf3, + 0xe1, 0xf5, 0xf6, 0x9f, 0xb6, 0x16, 0x04, 0x7b, 0x8f, 0xab, 0xb1, 0x39, 0x2a, 0x1b, 0xeb, + 0x5c, 0xa8, 0xac, 0x38, 0x11, 0x12, 0x5f, 0x89, 0x3e, 0x7d, 0xca, 0xec, 0x53, 0xdb, 0x6d, + 0x1e, 0xd2, 0x81, 0x78, 0x96, 0x46, 0xff, 0xf9, 0x54, 0x1c, 0x28, 0x7a, 0x4f, 0xd3, 0xc0, + 0xdc, 0xc1, 0x6a, 0xf2, 0xbc, 0xcb, 0x57, 0xfd, 0x4a, 0xe4, 0xf0, 0xb2, 0xc7, 0x95, 0x40, + 0x62, 0x52, 0x41, 0xe2, 0xad, 0x49, 0xa6, 0xb5, 0x1f, 0x02, 0xc8, 0xda, 0x92, 0xe5, 0xb0, + 0xc5, 0x64, 0x76, 0x48, 0x21, 0xde, 0x0f, 0x45, 0x58, 0x4c, 0xdf, 0xa7, 0x84, 0xcf, 0xd5, + 0x15, 0x4e, 0x27, 0x80, 0x6b, 0x7f, 0xfc, 0x44, 0x71, 0x47, 0x22, 0xdd, 0x30, 0x0c, 0xa1, + 0x3b, 0xe0, 0x37, 0xa0, 0x35, 0x23, 0x90, 0x32, 0x74, 0xbf, 0x8d, 0xc2, 0xea, 0xd6, 0x50, + 0xbb, 0xd9, 0xc4, 0x83, 0xb4, 0x31, 0x68, 0x55, 0x8b, 0x5d, 0xf4, 0x72, 0x18, 0xb7, 0xef, + 0xf7, 0x98, 0x2d, 0x01, 0x03, 0x61, 0xcc, 0x0a, 0x8e, 0x13, 0x00, 0xe8, 0x14, 0xaf, 0x09, + 0x51, 0x75, 0xa5, 0x2f, 0x1d, 0x0d, 0x65, 0x8a, 0xcd, 0x66, 0x07, 0x3d, 0x05, 0xd8, 0x4b, + 0xe9, 0x9d, 0x99, 0x7c, 0x91, 0xd1, 0xb8, 0x19, 0xc3, 0x2b, 0x42, 0x69, 0x88, 0xc6, 0x79, + 0x17, 0x3a, 0x4d, 0x5b, 0xa2, 0x5a, 0xfb, 0x25, 0x3f, 0xbd, 0xf8, 0xed, 0xce, 0xe6, 0x87, + 0xa3, 0x26, 0xa9, 0x2e, 0xbe, 0x94, 0x08, 0x7e, 0x67, 0x60, 0x8c, 0x9c, 0x5e, 0x6f, 0xb3, + 0x9e, 0x06, 0xfa, 0x82, 0xae, 0xee, 0x59, 0x77, 0xd7, 0x1a, 0x9a, 0x34, 0xd4, 0x56, 0xaa, + 0x33}, + {0xf9, 0x81, 0x7c, 0x00, 0xb9, 0x30, 0x37, 0xd5, 0x90, 0x51, 0x6e, 0xf0, 0xb2, 0x06, 0xfb, + 0xcd, 0x39, 0x14, 0x5f, 0xf8, 0x1b, 0xc7, 0x4a, 0x82, 0x70, 0x8c, 0x92, 0x1d, 0xea, 0xc3, + 0x4e, 0xc8, 0x12, 0xe6, 0xb6, 0x10, 0xd3, 0x2f, 0x95, 0x84, 0x25, 0x42, 0xa5, 0x72, 0xe5, + 0x8f, 0x55, 0xef, 0x86, 0xa2, 0x53, 0xae, 0xed, 0x26, 0x20, 0x47, 0xde, 0x78, 0x68, 0x28, + 0xe4, 0x45, 0xcc, 0x35, 0xbc, 0x3e, 0xbb, 0x8e, 0xc0, 0xbe, 0x34, 0xaf, 0xa6, 0x09, 0x64, + 0x01, 0x7b, 0x44, 0xf5, 0xf3, 0xb1, 0x3f, 0xa4, 0xb3, 0x32, 0x80, 0xc9, 0x4f, 0x6f, 0x40, + 0xbf, 0x21, 0x4c, 0x74, 0xe1, 0x11, 0x5e, 0xeb, 0x3d, 0x71, 0x2e, 0x9d, 0x62, 0x75, 0xd4, + 0x16, 0x48, 0x77, 0x13, 0x67, 0xad, 0x6b, 0x5d, 0x07, 0x87, 0xf7, 0xa8, 0x9a, 0x59, 0x76, + 0x8b, 0x9b, 0x1e, 0xd8, 0xee, 0xaa, 0xe9, 0x99, 0x2d, 0xc5, 0x97, 0xf1, 0xa7, 0x83, 0xfc, + 0xca, 0xdc, 0xba, 0xb8, 0x4b, 0xe8, 0x89, 0x17, 0x05, 0x0b, 0xa0, 0x65, 0x23, 0xd1, 0xce, + 0x36, 0x03, 0xd7, 0xe0, 0xf2, 0x91, 0xdf, 0x0e, 0x9c, 0x2c, 0x0d, 0x66, 0xd6, 0x73, 0x58, + 0x5c, 0xb4, 0x0a, 0x4d, 0xc2, 0x3b, 0xd2, 0xb7, 0xe2, 0xac, 0x33, 0xdb, 0x60, 0x27, 0xbd, + 0x56, 0x43, 0x24, 0x04, 0x02, 0x8d, 0x7d, 0x7f, 0xf6, 0xb5, 0xf4, 0xfd, 0xdd, 0x5b, 0xec, + 0x79, 0xc4, 0x22, 0x1f, 0xa1, 0x88, 0x54, 0xe7, 0x19, 0x98, 0x94, 0x7e, 0x31, 0xa3, 0x29, + 0xe3, 0x5a, 0xcb, 0x6a, 0xb0, 0xcf, 0x6d, 0x93, 0x6c, 0x7a, 0x08, 0xa9, 0x3c, 0x1c, 0xd0, + 0x63, 0x50, 0xc6, 0x85, 0x38, 0xc1, 0x41, 0x49, 0xab, 0x61, 0x52, 0x2a, 0x9f, 0xd9, 0x18, + 0x1a, 0x57, 0x46, 0x2b, 0x69, 0xda, 0xfa, 0xff, 0x0f, 0x15, 0x96, 0x3a, 0x8a, 0xfe, 0x9e, + 0x0c}, + {0x11, 0xa2, 0x9a, 0xc3, 0x94, 0xa0, 0x51, 0x01, 0x5b, 0xf7, 0xd1, 0x30, 0xee, 0x1f, 0x06, + 0xd5, 0x77, 0x67, 0xd3, 0xc1, 0x6e, 0xe7, 0x6a, 0x1a, 0xa4, 0x8e, 0x9f, 0x65, 0x66, 0xd6, + 0x0c, 0x2d, 0xc6, 0xe4, 0x69, 0xb7, 0x56, 0x5a, 0x57, 0x60, 0xfc, 0x79, 0x1e, 0xe1, 0x16, + 0x52, 0xf0, 0x07, 0xd0, 0xcd, 0xca, 0x78, 0xc9, 0x8b, 0xb3, 0x88, 0x27, 0xf6, 0xe0, 0xc0, + 0x84, 0xcf, 0x2f, 0xa3, 0x04, 0x00, 0x80, 0x40, 0xa7, 0xfa, 0x75, 0x9d, 0x4b, 0x89, 0x46, + 0x22, 0x28, 0x37, 0x34, 0x13, 0xff, 0x20, 0xb4, 0xda, 0x08, 0x26, 0x6c, 0x8f, 0xf2, 0x5d, + 0x7b, 0x73, 0x5c, 0x4c, 0x90, 0x71, 0x41, 0x8a, 0x9e, 0xbb, 0x12, 0xe8, 0x55, 0x6d, 0x2a, + 0x25, 0x4a, 0xaf, 0xbd, 0x95, 0x19, 0xa5, 0x31, 0xba, 0xad, 0xd9, 0xc5, 0xa6, 0x6b, 0x83, + 0xc4, 0xb6, 0xa9, 0xcc, 0xf9, 0xa1, 0xb1, 0x7a, 0xdc, 0xc7, 0xdb, 0x2e, 0x7e, 0x7f, 0x8d, + 0x4e, 0x62, 0x0f, 0x03, 0x39, 0xfd, 0xb8, 0xd4, 0x18, 0xc2, 0xec, 0x61, 0x02, 0x53, 0x1c, + 0x3b, 0x7d, 0xbc, 0x1d, 0x59, 0xf5, 0x8c, 0x98, 0xf1, 0x6f, 0x93, 0x85, 0xf8, 0x2c, 0x74, + 0xd8, 0x5e, 0x4d, 0x97, 0xab, 0x87, 0x82, 0x0a, 0x21, 0x42, 0x5f, 0x0d, 0x58, 0x33, 0x44, + 0x3a, 0xdd, 0xe9, 0xfb, 0x50, 0x47, 0xc8, 0xd7, 0x81, 0x9b, 0xe5, 0x1b, 0x17, 0x54, 0x86, + 0x2b, 0xef, 0xf4, 0x29, 0x32, 0x0e, 0x7c, 0x23, 0x15, 0xeb, 0xe6, 0x3c, 0x35, 0xe2, 0x96, + 0x68, 0x3f, 0x0b, 0x43, 0xce, 0xde, 0x9c, 0xbe, 0x4f, 0x45, 0x72, 0x76, 0xb5, 0x10, 0xe3, + 0xdf, 0x92, 0xd2, 0xa8, 0x48, 0x91, 0xb9, 0xac, 0xbf, 0x49, 0xb2, 0x99, 0x64, 0x70, 0xea, + 0xf3, 0x3e, 0x63, 0x14, 0xae, 0xed, 0xaa, 0x24, 0xfe, 0x3d, 0xcb, 0xb0, 0x09, 0x38, 0x36, + 0x05}, + {0xc4, 0xf0, 0x28, 0x49, 0x55, 0x4a, 0xf5, 0xfd, 0x75, 0xaf, 0x20, 0x69, 0xc8, 0x43, 0x86, + 0x6b, 0xc9, 0xa8, 0xc6, 0x54, 0x4c, 0xdd, 0x02, 0x5b, 0xe8, 0x9b, 0x59, 0x77, 0x34, 0xd7, + 0xc0, 0x51, 0x5f, 0xf3, 0x7e, 0xd4, 0xf1, 0x90, 0x81, 0xce, 0x19, 0x8a, 0x78, 0x33, 0xcd, + 0x97, 0x8c, 0xd6, 0x6a, 0x4b, 0x8f, 0xa4, 0x80, 0xdc, 0x1a, 0x17, 0x14, 0xc5, 0x07, 0x87, + 0x66, 0xad, 0x9d, 0x85, 0xb2, 0xf8, 0x8d, 0x98, 0xdf, 0x3e, 0x1f, 0x3f, 0x64, 0x58, 0x40, + 0xac, 0x6c, 0x5c, 0x08, 0x5d, 0x53, 0xba, 0xff, 0xd2, 0xa9, 0x2c, 0x25, 0xab, 0xe4, 0x32, + 0x38, 0x9a, 0xf9, 0xcb, 0xc2, 0x91, 0x67, 0xd0, 0xe3, 0x22, 0x29, 0x2d, 0x92, 0x48, 0xb4, + 0x0e, 0x99, 0x05, 0xa3, 0x6f, 0x0a, 0x15, 0xef, 0xd5, 0x10, 0x4f, 0xd8, 0xf6, 0x00, 0xe6, + 0x46, 0x2b, 0x31, 0x57, 0x83, 0x94, 0xae, 0x03, 0xeb, 0x8e, 0x13, 0x24, 0xa1, 0x1e, 0x5e, + 0xd1, 0x26, 0xd9, 0xa7, 0xca, 0x36, 0x6e, 0x71, 0x37, 0xee, 0xb1, 0xfa, 0x7c, 0x76, 0x06, + 0xb8, 0x23, 0xbe, 0x21, 0xbc, 0x9e, 0xc1, 0xda, 0x7a, 0x3b, 0x62, 0x27, 0x7b, 0xb0, 0xe5, + 0x63, 0x18, 0x82, 0x7f, 0x0f, 0xed, 0xa0, 0x2a, 0x9c, 0xbf, 0x11, 0xbd, 0xe0, 0x88, 0x0d, + 0x3a, 0x79, 0x52, 0x56, 0xf7, 0xb6, 0xd3, 0x09, 0x16, 0x1b, 0x70, 0xb3, 0x42, 0x60, 0x2f, + 0x1c, 0xa2, 0x9f, 0x72, 0x12, 0x45, 0x47, 0xbb, 0xe1, 0x0b, 0x01, 0x8b, 0x1d, 0xde, 0xec, + 0x0c, 0x4e, 0xe9, 0xcf, 0xcc, 0x95, 0x74, 0xf4, 0xa5, 0x93, 0xc7, 0xdb, 0x4d, 0xfb, 0x35, + 0x65, 0xfe, 0xe7, 0x39, 0xb5, 0xea, 0x96, 0xc3, 0x04, 0x41, 0x44, 0x84, 0xfc, 0x6d, 0x30, + 0xe2, 0xf2, 0x68, 0xb7, 0x89, 0x7d, 0x73, 0x3d, 0xb9, 0x5a, 0x50, 0xa6, 0x3c, 0x61, 0xaa, + 0x2e}, + {0x1a, 0x59, 0x9c, 0xad, 0xc8, 0xe4, 0x11, 0x54, 0xed, 0x37, 0x0f, 0x3a, 0xe6, 0x5f, 0x3c, + 0x4b, 0xb8, 0x15, 0x89, 0xb1, 0xe8, 0xda, 0x69, 0x77, 0x91, 0x56, 0x8b, 0xdb, 0x06, 0x24, + 0xcf, 0x18, 0xf8, 0xb0, 0x87, 0xdf, 0x8c, 0x35, 0xcb, 0x86, 0x53, 0x9d, 0xa4, 0x66, 0x4a, + 0x7a, 0x71, 0x0a, 0x48, 0x38, 0xff, 0xdc, 0x83, 0x20, 0xce, 0x98, 0x32, 0xf6, 0xd7, 0xaf, + 0x70, 0x5e, 0x73, 0x8a, 0x14, 0x72, 0x1e, 0xc1, 0x29, 0x79, 0x07, 0x08, 0xe9, 0x43, 0x46, + 0xd0, 0x2f, 0xde, 0x2a, 0x4f, 0x3d, 0x2c, 0x50, 0xb3, 0x75, 0xfc, 0x0b, 0x64, 0xae, 0x31, + 0x7b, 0x61, 0xa5, 0x30, 0xa0, 0x93, 0xd2, 0xbe, 0xc2, 0x55, 0xba, 0x6c, 0xf3, 0x62, 0x68, + 0xfd, 0xac, 0x3b, 0x95, 0x49, 0x1f, 0x6b, 0xb4, 0x85, 0xf7, 0xa6, 0x03, 0xec, 0x6e, 0x9a, + 0x81, 0x09, 0x0c, 0x6a, 0xee, 0x9e, 0x4e, 0xf0, 0xab, 0x2d, 0x7e, 0xa1, 0xe1, 0xbb, 0xc9, + 0xbc, 0x41, 0xf2, 0xfa, 0x2e, 0x1b, 0xdd, 0x27, 0x34, 0xd3, 0x60, 0x04, 0xb5, 0x01, 0xd6, + 0x40, 0xf9, 0xd5, 0x02, 0x47, 0xd9, 0xd8, 0xe0, 0x8f, 0x2b, 0xfb, 0xb7, 0xc5, 0xeb, 0x57, + 0x16, 0xe5, 0x78, 0x8d, 0xf4, 0x00, 0xcd, 0x82, 0x39, 0xc6, 0x96, 0xbd, 0xbf, 0xe3, 0x36, + 0x45, 0x0e, 0x26, 0x1d, 0x63, 0xe7, 0x84, 0x3e, 0xb6, 0x9b, 0x22, 0xa3, 0xf5, 0x8e, 0x23, + 0x6d, 0xc3, 0x99, 0x7d, 0x90, 0x97, 0x10, 0x25, 0x80, 0xd4, 0x4c, 0xe2, 0x74, 0xcc, 0xb2, + 0x5c, 0x33, 0xc7, 0xea, 0x05, 0x12, 0x3f, 0x51, 0xa8, 0xfe, 0xf1, 0x1c, 0x42, 0xca, 0xd1, + 0x76, 0x28, 0x6f, 0x92, 0xa7, 0x67, 0xa9, 0x19, 0xa2, 0x44, 0x5b, 0xaa, 0xef, 0x58, 0x7f, + 0x21, 0xc0, 0x88, 0x65, 0xb9, 0x5d, 0x4d, 0x0d, 0x5a, 0xc4, 0x13, 0x52, 0x7c, 0x9f, 0x94, + 0x17}, + {0xc7, 0x2b, 0x82, 0x61, 0x5b, 0xd0, 0x96, 0x84, 0xd3, 0x4a, 0x70, 0xa1, 0x9b, 0x59, 0x33, + 0x9f, 0xc0, 0x20, 0x14, 0x53, 0x29, 0x17, 0xc5, 0x0b, 0xc9, 0x7b, 0x97, 0x02, 0x0d, 0x3a, + 0x1e, 0x7c, 0x3f, 0x6b, 0x52, 0xe8, 0x75, 0x3d, 0xf6, 0xe4, 0x0c, 0x8a, 0x4f, 0xc3, 0x5f, + 0x26, 0x65, 0x73, 0x31, 0x23, 0x28, 0x48, 0x74, 0xaa, 0xa7, 0x36, 0x09, 0xb1, 0xe2, 0x91, + 0x04, 0x51, 0x22, 0xfc, 0x08, 0xa6, 0x05, 0xa4, 0xf1, 0x12, 0x1c, 0x19, 0xeb, 0x40, 0x37, + 0xc2, 0xa0, 0x41, 0x1d, 0xd4, 0xdc, 0x07, 0x43, 0x8f, 0x47, 0xaf, 0xd1, 0x2e, 0x98, 0xab, + 0x01, 0xba, 0xf0, 0x66, 0x68, 0xac, 0xf9, 0xe7, 0x69, 0xb6, 0xcb, 0x8d, 0x78, 0x87, 0x15, + 0x03, 0xd5, 0xdf, 0xa3, 0x1a, 0x9d, 0x6a, 0xea, 0x2f, 0x94, 0x4e, 0x9e, 0x42, 0xd7, 0xb8, + 0x38, 0x92, 0xd8, 0xbb, 0xde, 0xdd, 0x9a, 0xbc, 0xb0, 0x4c, 0x79, 0xf4, 0x58, 0x3e, 0xe9, + 0x83, 0x81, 0xff, 0xe3, 0x55, 0xfd, 0x5d, 0xb2, 0xef, 0x9c, 0x6d, 0x54, 0x99, 0x60, 0xda, + 0x3b, 0xec, 0xfa, 0x11, 0xd6, 0xc4, 0x2a, 0xed, 0x4b, 0xae, 0x13, 0xbf, 0xb9, 0x06, 0x8b, + 0xe0, 0x1f, 0x7f, 0x5a, 0xad, 0x90, 0x39, 0x0f, 0xf3, 0xbd, 0x46, 0x6c, 0x2c, 0xf2, 0xf8, + 0xfe, 0xd9, 0xe6, 0x72, 0x0e, 0x89, 0xbe, 0x5e, 0xc6, 0xa8, 0xcf, 0xf5, 0x57, 0x7e, 0x8c, + 0xb7, 0xe1, 0x88, 0x7a, 0xc8, 0x1b, 0x18, 0xdb, 0x6f, 0x35, 0xd2, 0x16, 0x32, 0x64, 0x63, + 0xa5, 0x8e, 0xf7, 0xb4, 0x76, 0x95, 0x86, 0x7d, 0xe5, 0xa9, 0x5c, 0x85, 0x00, 0xc1, 0x93, + 0x4d, 0xfb, 0x30, 0xa2, 0xb5, 0x25, 0x34, 0x10, 0x77, 0x3c, 0x67, 0x71, 0x2d, 0x44, 0x45, + 0x56, 0x0a, 0x50, 0xb3, 0xcd, 0x62, 0x80, 0xce, 0x21, 0x49, 0x24, 0xca, 0xee, 0x27, 0x6e, + 0xcc}, + {0xa5, 0xb0, 0x6d, 0xb2, 0x51, 0x55, 0x1f, 0xbf, 0x3e, 0x8d, 0xdd, 0x19, 0x92, 0xea, 0x1b, + 0xbd, 0x32, 0x65, 0x29, 0xa4, 0x89, 0x67, 0xb1, 0x90, 0x68, 0x00, 0xda, 0x0d, 0xa6, 0x59, + 0x54, 0xf2, 0x10, 0x14, 0x2f, 0x45, 0xe0, 0x3b, 0x23, 0xfa, 0xd7, 0x50, 0xac, 0x93, 0xe6, + 0x43, 0x01, 0x0a, 0x5c, 0x78, 0x70, 0x98, 0x46, 0xa9, 0x7b, 0xf3, 0x95, 0x07, 0x2a, 0xd3, + 0x74, 0xb3, 0x3f, 0xa3, 0x60, 0x82, 0x39, 0x4e, 0x34, 0x48, 0xc0, 0x1c, 0xf6, 0xc2, 0x91, + 0x64, 0x4d, 0x3c, 0xf8, 0x9d, 0x35, 0x9a, 0x94, 0xe3, 0x7a, 0xf0, 0xf9, 0x6e, 0xb9, 0x12, + 0xb8, 0x04, 0x2d, 0x02, 0x28, 0x13, 0x85, 0x72, 0x80, 0x87, 0xdb, 0x2b, 0xf1, 0x4f, 0x26, + 0xa2, 0xe1, 0x49, 0x7e, 0x9c, 0xcc, 0xa7, 0xb6, 0xa0, 0xd0, 0x9b, 0x36, 0x77, 0xad, 0x8b, + 0x6b, 0x4a, 0x03, 0x1d, 0x05, 0x8a, 0x06, 0x4b, 0xaf, 0xe5, 0x31, 0xb5, 0xd4, 0xc6, 0x0c, + 0x66, 0xba, 0x83, 0xfd, 0x09, 0x0f, 0xae, 0x71, 0xb7, 0x6f, 0xdc, 0x41, 0xe8, 0x17, 0x8e, + 0x40, 0x7f, 0x62, 0x30, 0xff, 0xa8, 0x84, 0x25, 0xfb, 0x16, 0xce, 0x37, 0x44, 0xab, 0x99, + 0x1e, 0xeb, 0x18, 0x3a, 0x47, 0xf7, 0x5f, 0x81, 0xcf, 0xed, 0x58, 0x2c, 0x6c, 0xfe, 0x9e, + 0x57, 0x53, 0x97, 0x20, 0xca, 0x79, 0x1a, 0x5a, 0x88, 0xf5, 0x69, 0x9f, 0xe7, 0xd9, 0x0e, + 0xbe, 0x42, 0xdf, 0x56, 0xe4, 0x4c, 0x22, 0xaa, 0x73, 0x0b, 0x15, 0xc5, 0xee, 0xfc, 0xc7, + 0xd6, 0xcb, 0xcd, 0x8c, 0xe2, 0x76, 0x21, 0xe9, 0xd1, 0xec, 0xc8, 0x7d, 0xd8, 0x8f, 0x61, + 0x7c, 0x2e, 0xbc, 0xde, 0xb4, 0x75, 0xd2, 0xc4, 0x63, 0x3d, 0xa1, 0x5e, 0x5d, 0x6a, 0x08, + 0x24, 0xc9, 0x27, 0xbb, 0xef, 0x33, 0x86, 0x5b, 0xd5, 0x38, 0x52, 0x11, 0xf4, 0xc3, 0x96, + 0xc1}, + {0x34, 0x5a, 0xdb, 0x2c, 0x59, 0x57, 0x12, 0x2b, 0x30, 0xc2, 0xa0, 0x92, 0xbf, 0xed, 0xbc, + 0x45, 0xde, 0x27, 0x9b, 0x96, 0xd3, 0xe6, 0xc5, 0xeb, 0xd8, 0x24, 0x4b, 0xa4, 0x21, 0xcc, + 0xa8, 0xd6, 0xce, 0x3c, 0xba, 0xb1, 0x09, 0xe0, 0xd7, 0x32, 0x66, 0x4a, 0x83, 0x1d, 0x19, + 0xca, 0x89, 0x67, 0x0f, 0x42, 0x07, 0x71, 0xe9, 0xbb, 0x44, 0x5e, 0x85, 0x17, 0xc4, 0xae, + 0x9c, 0x3f, 0x6b, 0x78, 0xe7, 0x6d, 0x02, 0xa7, 0xfe, 0x33, 0xe3, 0xd9, 0x0a, 0x7d, 0xea, + 0xf1, 0x18, 0x87, 0x7b, 0x62, 0x03, 0x79, 0x52, 0x23, 0x00, 0x3e, 0x25, 0xb9, 0xdd, 0x16, + 0x68, 0x3b, 0x1f, 0x90, 0xa6, 0x2a, 0xc6, 0x29, 0x91, 0x8a, 0x9d, 0xef, 0x1e, 0x84, 0x76, + 0xee, 0x4f, 0x39, 0xfb, 0x11, 0x1b, 0x0b, 0x93, 0xad, 0x49, 0xb7, 0x05, 0xe1, 0x4d, 0xcb, + 0xe8, 0x38, 0xd0, 0x55, 0xf2, 0xf7, 0x0d, 0x8b, 0x65, 0xcd, 0xfa, 0xd4, 0x9e, 0xc9, 0x81, + 0x4e, 0x14, 0xc8, 0x26, 0xb6, 0xf9, 0xaa, 0xf5, 0x80, 0xf8, 0x6a, 0x98, 0xab, 0x58, 0xf3, + 0xb8, 0x8e, 0xb5, 0x97, 0x43, 0x72, 0xa2, 0xda, 0x64, 0xc1, 0x40, 0x5c, 0x13, 0xb0, 0xe5, + 0xbd, 0x08, 0x9f, 0x1c, 0x7e, 0x8f, 0x06, 0xbe, 0x6e, 0x50, 0x1a, 0x2f, 0x37, 0xa1, 0xfc, + 0x10, 0x2e, 0x70, 0x53, 0x04, 0x20, 0x63, 0x48, 0xe2, 0xfd, 0x6f, 0x7a, 0x75, 0x61, 0xb3, + 0x35, 0x3a, 0xcf, 0x5b, 0x88, 0x82, 0x51, 0xd2, 0x47, 0xa5, 0xa9, 0x74, 0x6c, 0x31, 0x94, + 0x7c, 0x9a, 0xc0, 0xec, 0x15, 0x0e, 0x28, 0x01, 0x2d, 0xc3, 0xf0, 0xb4, 0xe4, 0x8d, 0xdc, + 0xac, 0x3d, 0x5f, 0x86, 0xc7, 0x95, 0x22, 0xf4, 0xb2, 0xa3, 0x54, 0x46, 0xd1, 0x77, 0x8c, + 0x0c, 0xff, 0xaf, 0x56, 0x5d, 0x36, 0x69, 0x7f, 0x99, 0x4c, 0xd5, 0x60, 0x73, 0xdf, 0x41, + 0xf6}, + {0x04, 0xda, 0x79, 0x63, 0x1e, 0xbd, 0xea, 0xe3, 0x0b, 0x65, 0x25, 0x01, 0xcd, 0xa9, 0x92, + 0xed, 0x18, 0x75, 0x57, 0x30, 0x89, 0x03, 0x09, 0x6a, 0xdc, 0xc7, 0x98, 0xa4, 0x50, 0x91, + 0x1f, 0xb1, 0x0c, 0x77, 0x85, 0x66, 0xca, 0x12, 0x1c, 0x67, 0xc2, 0xe0, 0x17, 0x83, 0x59, + 0x9d, 0xd5, 0x22, 0x82, 0x56, 0xa8, 0x9f, 0xc6, 0x60, 0x48, 0xb3, 0x11, 0xfc, 0xa3, 0x28, + 0xfb, 0x06, 0xdd, 0x5d, 0xba, 0x29, 0x7a, 0x4f, 0xe8, 0xfe, 0xe9, 0x10, 0xcb, 0xf3, 0x93, + 0x7b, 0x6c, 0x69, 0x54, 0xe7, 0x44, 0xa2, 0x84, 0x1d, 0x8d, 0xce, 0xff, 0xfa, 0x1a, 0x87, + 0x90, 0x74, 0xa1, 0xf8, 0x14, 0xaa, 0xbc, 0xc0, 0xcf, 0x31, 0xb4, 0xd9, 0xbf, 0xd8, 0x5e, + 0x26, 0x2d, 0xd0, 0xe4, 0x3f, 0x19, 0xd6, 0x8c, 0x2f, 0xab, 0x39, 0x58, 0x72, 0x6e, 0xdf, + 0x3b, 0xe6, 0x3c, 0xb6, 0x62, 0x88, 0xde, 0x40, 0xb2, 0x8f, 0x9b, 0x7c, 0x95, 0x43, 0xd1, + 0x9e, 0xac, 0x9c, 0xd4, 0x23, 0xe1, 0x0e, 0x4b, 0x53, 0x33, 0x46, 0x20, 0x13, 0x34, 0x1b, + 0x97, 0xf7, 0xf1, 0xc3, 0x61, 0x4a, 0x6f, 0x5a, 0x21, 0x7f, 0x70, 0x2e, 0x55, 0x41, 0x05, + 0xc5, 0xd7, 0x76, 0xe5, 0x27, 0x15, 0xec, 0x42, 0x5c, 0x4d, 0x78, 0x35, 0x8b, 0xef, 0xd2, + 0xee, 0xb5, 0xbe, 0xae, 0x02, 0x3a, 0xd3, 0x5f, 0xc4, 0x24, 0xf0, 0xf9, 0x51, 0x4e, 0xeb, + 0x00, 0x0f, 0xfd, 0xaf, 0x3e, 0xc1, 0x9a, 0x52, 0x86, 0x81, 0x80, 0x7e, 0xf6, 0x2b, 0xcc, + 0xb9, 0x7d, 0x68, 0xf2, 0xad, 0x99, 0xa7, 0x07, 0x2c, 0x73, 0x38, 0xb0, 0x6b, 0xb7, 0x8e, + 0x71, 0xa0, 0xf4, 0x3d, 0xa6, 0x0d, 0x37, 0xdb, 0x0a, 0x47, 0x36, 0x16, 0x96, 0x6d, 0x32, + 0x2a, 0x5b, 0xe2, 0x45, 0x94, 0xf5, 0xa5, 0x4c, 0xc8, 0x8a, 0x49, 0x64, 0xbb, 0x08, 0xc9, + 0xb8}, +}; + +static const uint8_t access_code_table_1[5][10][100] = { + { + {11, 38, 3, 63, 36, 39, 79, 19, 87, 68, 76, 51, 20, 56, 77, 61, 52, 73, 74, 94, + 45, 31, 99, 28, 29, 90, 81, 96, 65, 75, 13, 32, 70, 21, 55, 33, 9, 15, 92, 14, + 82, 97, 78, 37, 49, 0, 80, 57, 58, 7, 1, 89, 98, 53, 64, 5, 6, 66, 43, 83, + 10, 50, 47, 84, 62, 71, 4, 67, 86, 42, 35, 34, 91, 12, 17, 69, 44, 48, 85, 30, + 16, 95, 54, 23, 46, 18, 88, 59, 40, 60, 41, 25, 27, 22, 2, 24, 72, 93, 26, 8}, + {28, 58, 74, 34, 6, 14, 16, 46, 87, 24, 59, 57, 23, 88, 75, 65, 79, 38, 82, 13, + 49, 99, 8, 94, 15, 19, 0, 96, 41, 95, 45, 35, 1, 91, 2, 52, 42, 76, 90, 84, + 20, 54, 4, 63, 25, 5, 47, 85, 93, 12, 51, 81, 71, 69, 22, 17, 36, 64, 77, 80, + 98, 53, 44, 18, 10, 68, 97, 61, 56, 70, 92, 27, 40, 9, 60, 7, 72, 21, 43, 3, + 33, 32, 50, 26, 67, 62, 39, 48, 73, 86, 29, 89, 37, 78, 55, 66, 31, 30, 11, 83}, + {4, 16, 22, 66, 37, 14, 47, 89, 59, 60, 51, 92, 71, 70, 96, 13, 17, 85, 58, 93, + 81, 69, 28, 90, 78, 39, 63, 88, 0, 75, 2, 53, 23, 99, 94, 68, 54, 74, 41, 29, + 8, 26, 50, 98, 82, 95, 42, 7, 24, 77, 30, 9, 21, 1, 87, 62, 46, 15, 43, 38, + 36, 55, 49, 57, 52, 56, 91, 65, 11, 84, 6, 73, 97, 33, 83, 27, 35, 25, 40, 32, + 18, 31, 86, 48, 45, 3, 79, 76, 72, 64, 44, 20, 80, 19, 10, 5, 61, 34, 67, 12}, + {80, 91, 71, 26, 75, 8, 22, 92, 7, 70, 65, 51, 89, 31, 81, 30, 83, 47, 44, 85, + 55, 19, 57, 87, 74, 6, 73, 40, 49, 32, 52, 78, 0, 12, 54, 15, 64, 28, 4, 24, + 68, 17, 72, 53, 1, 94, 58, 88, 11, 82, 43, 86, 60, 63, 99, 96, 37, 77, 67, 33, + 18, 5, 41, 59, 16, 36, 79, 97, 61, 23, 56, 48, 98, 38, 66, 21, 34, 29, 10, 3, + 93, 46, 50, 39, 14, 9, 2, 13, 69, 42, 62, 35, 27, 90, 25, 76, 20, 95, 45, 84}, + {48, 16, 54, 47, 9, 39, 81, 91, 33, 53, 63, 21, 50, 85, 97, 90, 49, 36, 84, 74, + 51, 76, 37, 67, 59, 58, 23, 20, 65, 82, 7, 77, 12, 31, 46, 1, 5, 14, 3, 30, + 94, 95, 2, 34, 70, 89, 40, 87, 43, 79, 64, 57, 68, 26, 56, 44, 88, 61, 0, 24, + 71, 18, 60, 4, 80, 96, 27, 15, 6, 13, 98, 29, 28, 25, 72, 93, 55, 75, 52, 66, + 11, 8, 86, 45, 22, 83, 17, 10, 35, 38, 73, 99, 62, 41, 19, 69, 78, 92, 42, 32}, + {23, 19, 88, 44, 5, 39, 75, 81, 30, 14, 49, 54, 62, 55, 18, 83, 77, 76, 74, 92, + 6, 95, 52, 36, 15, 27, 53, 38, 28, 50, 48, 99, 78, 67, 90, 87, 16, 41, 58, 65, + 79, 11, 68, 84, 24, 97, 64, 47, 9, 22, 43, 86, 37, 8, 33, 17, 93, 61, 25, 72, + 57, 98, 42, 80, 13, 89, 2, 12, 31, 56, 26, 85, 3, 29, 66, 63, 82, 10, 20, 94, + 51, 0, 1, 4, 21, 60, 34, 35, 32, 45, 59, 71, 46, 69, 7, 96, 91, 40, 70, 73}, + {89, 62, 33, 19, 21, 97, 15, 2, 83, 22, 65, 30, 55, 7, 63, 73, 39, 61, 44, 37, + 11, 26, 31, 79, 91, 0, 72, 68, 23, 87, 28, 24, 45, 74, 98, 14, 70, 54, 88, 42, + 35, 59, 46, 16, 27, 92, 69, 66, 94, 32, 75, 77, 64, 57, 50, 71, 10, 3, 67, 81, + 90, 58, 99, 5, 82, 85, 25, 52, 12, 8, 48, 49, 76, 96, 80, 20, 29, 34, 53, 51, + 86, 93, 38, 84, 9, 36, 6, 60, 1, 78, 13, 95, 41, 17, 47, 18, 43, 40, 4, 56}, + {40, 68, 88, 47, 43, 28, 67, 23, 42, 99, 48, 3, 12, 27, 79, 25, 6, 55, 19, 80, + 81, 62, 83, 69, 39, 73, 36, 30, 44, 82, 70, 52, 31, 34, 58, 74, 54, 2, 13, 45, + 93, 49, 75, 72, 29, 87, 5, 7, 17, 76, 90, 63, 84, 51, 59, 35, 20, 97, 22, 65, + 57, 66, 78, 86, 11, 9, 71, 98, 53, 56, 15, 46, 92, 0, 26, 21, 32, 38, 77, 91, + 4, 24, 61, 85, 94, 1, 96, 64, 8, 41, 33, 50, 18, 60, 10, 16, 37, 95, 14, 89}, + {96, 23, 99, 42, 94, 91, 19, 53, 41, 25, 39, 73, 70, 72, 33, 35, 88, 2, 63, 49, + 95, 60, 48, 97, 5, 54, 47, 40, 98, 18, 57, 28, 67, 92, 10, 79, 13, 76, 8, 55, + 90, 62, 50, 87, 65, 82, 84, 21, 0, 14, 45, 44, 20, 38, 17, 3, 81, 16, 58, 4, + 31, 29, 86, 15, 43, 64, 26, 52, 46, 69, 37, 68, 6, 22, 61, 7, 83, 71, 30, 75, + 85, 78, 24, 27, 89, 9, 80, 34, 66, 77, 36, 93, 56, 59, 1, 12, 51, 32, 11, 74}, + {75, 99, 97, 8, 15, 48, 6, 25, 60, 90, 91, 95, 23, 3, 89, 77, 22, 78, 94, 55, + 98, 35, 53, 96, 18, 52, 12, 16, 50, 20, 49, 9, 17, 13, 57, 44, 14, 47, 86, 56, + 88, 41, 29, 32, 70, 1, 62, 37, 85, 38, 59, 36, 5, 68, 71, 69, 67, 27, 39, 4, + 54, 0, 84, 45, 61, 73, 79, 93, 58, 66, 10, 76, 51, 92, 19, 87, 24, 7, 28, 2, + 11, 80, 81, 33, 64, 74, 46, 65, 26, 72, 40, 82, 34, 42, 21, 83, 31, 30, 43, 63}, + }, + { + {98, 58, 34, 65, 83, 74, 88, 5, 89, 46, 80, 43, 18, 87, 9, 28, 71, 90, 4, 36, + 40, 11, 39, 31, 76, 92, 82, 48, 66, 41, 49, 99, 78, 79, 97, 86, 75, 68, 3, 7, + 62, 64, 26, 22, 13, 77, 54, 50, 10, 96, 15, 35, 94, 73, 57, 93, 16, 61, 37, 38, + 53, 2, 63, 23, 67, 72, 95, 70, 29, 33, 45, 47, 27, 24, 69, 44, 55, 85, 14, 8, + 91, 12, 42, 6, 84, 81, 19, 25, 32, 51, 1, 17, 52, 56, 60, 0, 59, 30, 21, 20}, + {26, 63, 99, 57, 77, 53, 9, 75, 76, 80, 46, 97, 37, 14, 1, 12, 35, 20, 0, 15, + 28, 19, 48, 41, 67, 30, 60, 69, 29, 34, 73, 10, 51, 47, 66, 82, 7, 98, 39, 96, + 8, 62, 42, 3, 95, 5, 11, 13, 90, 94, 32, 31, 83, 64, 36, 88, 72, 52, 38, 54, + 45, 17, 61, 81, 78, 44, 79, 55, 56, 86, 2, 40, 16, 24, 4, 71, 49, 33, 23, 92, + 84, 74, 65, 43, 87, 59, 89, 68, 22, 70, 25, 58, 21, 85, 6, 50, 27, 93, 91, 18}, + {22, 31, 11, 72, 37, 70, 90, 60, 24, 56, 28, 69, 54, 85, 5, 98, 1, 76, 59, 20, + 86, 32, 61, 49, 63, 42, 74, 53, 84, 3, 95, 23, 15, 36, 4, 81, 6, 21, 19, 27, + 17, 83, 30, 45, 25, 65, 10, 50, 80, 66, 46, 75, 93, 82, 78, 99, 97, 68, 8, 13, + 58, 94, 41, 14, 48, 52, 29, 79, 91, 89, 9, 55, 57, 2, 39, 92, 0, 44, 18, 62, + 73, 96, 34, 88, 16, 77, 71, 43, 33, 40, 7, 12, 87, 51, 26, 47, 64, 35, 67, 38}, + {0, 80, 63, 52, 49, 69, 15, 87, 55, 60, 22, 91, 31, 89, 78, 28, 48, 12, 10, 42, + 82, 23, 29, 18, 21, 2, 19, 7, 68, 37, 54, 90, 81, 43, 57, 94, 26, 95, 96, 88, + 14, 51, 79, 71, 66, 76, 34, 24, 30, 9, 62, 44, 77, 65, 6, 27, 4, 97, 38, 36, + 59, 3, 1, 50, 72, 46, 64, 35, 20, 11, 53, 13, 58, 39, 74, 8, 83, 75, 40, 98, + 17, 99, 85, 32, 92, 67, 84, 5, 25, 56, 16, 47, 70, 93, 86, 61, 73, 41, 45, 33}, + {64, 57, 56, 7, 86, 9, 65, 16, 1, 63, 71, 26, 53, 98, 69, 5, 84, 28, 36, 58, + 54, 99, 42, 74, 68, 3, 88, 22, 82, 97, 85, 45, 21, 66, 43, 59, 83, 51, 89, 87, + 31, 12, 95, 52, 60, 17, 80, 27, 72, 93, 10, 39, 91, 30, 75, 61, 24, 8, 23, 2, + 18, 73, 94, 6, 38, 44, 13, 40, 48, 4, 81, 37, 96, 46, 19, 49, 34, 50, 0, 20, + 14, 15, 25, 92, 33, 29, 77, 90, 78, 67, 62, 41, 35, 47, 79, 32, 11, 70, 76, 55}, + {24, 99, 28, 52, 64, 3, 63, 16, 15, 87, 47, 6, 59, 98, 13, 78, 57, 81, 56, 94, + 0, 37, 70, 73, 65, 83, 90, 92, 60, 82, 67, 40, 74, 34, 91, 2, 33, 85, 21, 54, + 12, 31, 84, 55, 53, 49, 36, 25, 39, 20, 29, 93, 51, 76, 42, 96, 71, 86, 95, 66, + 45, 19, 8, 58, 22, 69, 80, 79, 89, 17, 61, 18, 88, 38, 72, 75, 48, 30, 7, 10, + 11, 35, 23, 9, 26, 4, 62, 44, 1, 43, 46, 14, 41, 77, 97, 27, 68, 32, 5, 50}, + {90, 80, 22, 27, 16, 23, 11, 31, 8, 75, 54, 82, 68, 66, 10, 40, 95, 99, 14, 42, + 56, 88, 12, 55, 38, 6, 79, 84, 26, 92, 4, 67, 48, 5, 46, 77, 19, 53, 97, 34, + 64, 73, 58, 47, 18, 39, 81, 59, 74, 62, 87, 98, 50, 1, 25, 9, 72, 69, 61, 60, + 86, 3, 24, 28, 45, 30, 96, 36, 57, 83, 89, 63, 0, 33, 17, 7, 94, 15, 43, 2, + 44, 41, 20, 85, 32, 65, 93, 49, 91, 52, 71, 37, 35, 13, 21, 51, 70, 29, 78, 76}, + {95, 50, 72, 0, 68, 98, 49, 53, 56, 41, 54, 22, 57, 78, 66, 31, 52, 29, 86, 30, + 62, 26, 32, 46, 42, 48, 14, 21, 88, 94, 77, 92, 7, 63, 91, 28, 93, 38, 90, 71, + 34, 25, 11, 83, 55, 70, 97, 82, 89, 76, 10, 99, 9, 45, 60, 39, 44, 16, 17, 37, + 47, 79, 33, 36, 59, 8, 80, 23, 1, 18, 64, 51, 61, 81, 75, 40, 35, 73, 13, 65, + 12, 4, 19, 85, 96, 6, 2, 58, 27, 43, 5, 87, 20, 15, 3, 24, 84, 67, 74, 69}, + {38, 33, 29, 88, 49, 47, 60, 69, 75, 12, 44, 89, 35, 45, 23, 30, 21, 28, 65, 86, + 81, 40, 93, 97, 67, 2, 94, 9, 61, 92, 90, 32, 62, 36, 58, 51, 37, 73, 50, 63, + 39, 43, 16, 71, 46, 91, 13, 7, 20, 72, 41, 59, 70, 48, 10, 52, 5, 34, 76, 15, + 87, 6, 98, 18, 57, 77, 24, 79, 27, 19, 3, 0, 8, 95, 74, 64, 42, 4, 68, 31, + 84, 54, 99, 25, 1, 80, 85, 26, 83, 11, 56, 55, 78, 66, 82, 14, 53, 22, 17, 96}, + {26, 12, 62, 5, 78, 53, 31, 46, 51, 8, 69, 80, 32, 68, 76, 42, 29, 87, 28, 75, + 40, 91, 58, 7, 88, 92, 82, 96, 70, 50, 47, 65, 85, 3, 48, 73, 90, 89, 97, 84, + 81, 39, 43, 79, 64, 54, 2, 41, 38, 10, 24, 22, 25, 93, 44, 23, 49, 37, 14, 95, + 72, 74, 15, 11, 55, 98, 99, 0, 9, 16, 18, 20, 63, 30, 57, 19, 4, 17, 67, 35, + 60, 45, 59, 56, 36, 77, 66, 33, 27, 71, 86, 1, 13, 34, 21, 61, 6, 94, 52, 83}, + }, + { + {10, 87, 64, 11, 25, 97, 93, 18, 99, 38, 17, 72, 50, 23, 90, 57, 98, 12, 29, 46, + 19, 77, 42, 15, 73, 52, 53, 89, 35, 20, 9, 41, 79, 13, 95, 48, 85, 54, 61, 94, + 0, 74, 62, 40, 78, 68, 24, 28, 86, 21, 59, 14, 82, 84, 92, 58, 34, 31, 6, 16, + 75, 81, 43, 47, 7, 3, 2, 37, 67, 8, 66, 22, 30, 32, 71, 44, 70, 1, 36, 26, + 33, 63, 80, 83, 91, 76, 56, 39, 69, 55, 96, 60, 27, 49, 65, 88, 5, 4, 51, 45}, + {73, 87, 48, 43, 71, 40, 88, 23, 60, 35, 30, 5, 75, 25, 59, 95, 58, 79, 4, 89, + 96, 17, 38, 6, 39, 9, 68, 81, 3, 90, 93, 99, 16, 42, 21, 45, 74, 84, 77, 65, + 63, 98, 11, 51, 0, 33, 91, 22, 69, 34, 31, 20, 46, 2, 76, 54, 15, 62, 13, 70, + 92, 55, 82, 47, 78, 7, 24, 27, 86, 1, 56, 61, 12, 44, 85, 57, 49, 19, 18, 67, + 66, 52, 72, 53, 83, 41, 64, 50, 26, 94, 32, 80, 28, 10, 37, 14, 8, 97, 29, 36}, + {60, 50, 53, 78, 63, 90, 11, 33, 21, 24, 20, 55, 8, 25, 6, 58, 3, 98, 44, 82, + 96, 14, 1, 59, 92, 75, 51, 36, 30, 64, 72, 40, 16, 94, 2, 74, 93, 85, 12, 10, + 43, 32, 42, 39, 79, 97, 17, 38, 89, 29, 68, 52, 49, 0, 34, 19, 70, 84, 54, 9, + 23, 71, 65, 28, 83, 56, 67, 57, 13, 18, 77, 5, 4, 88, 41, 22, 46, 76, 48, 61, + 81, 45, 86, 35, 47, 87, 91, 99, 73, 66, 27, 37, 31, 15, 7, 80, 62, 95, 69, 26}, + {49, 73, 63, 66, 38, 35, 32, 96, 87, 26, 36, 92, 58, 6, 17, 47, 29, 84, 11, 18, + 67, 45, 78, 4, 52, 81, 59, 30, 91, 54, 56, 89, 48, 46, 20, 79, 1, 62, 9, 53, + 10, 95, 88, 34, 40, 80, 41, 64, 83, 55, 43, 7, 24, 61, 77, 5, 14, 93, 16, 21, + 85, 39, 76, 50, 57, 65, 37, 60, 28, 19, 2, 97, 42, 33, 13, 68, 90, 25, 69, 99, + 98, 74, 3, 8, 12, 15, 82, 71, 31, 23, 75, 27, 51, 94, 86, 0, 44, 22, 72, 70}, + {30, 32, 52, 21, 15, 79, 53, 40, 56, 44, 55, 6, 63, 39, 61, 4, 93, 43, 23, 99, + 38, 66, 88, 18, 1, 86, 33, 9, 82, 76, 70, 51, 48, 69, 85, 11, 5, 91, 59, 77, + 49, 75, 22, 87, 65, 41, 26, 50, 47, 98, 57, 10, 58, 62, 46, 28, 20, 71, 78, 95, + 0, 72, 80, 73, 92, 36, 96, 37, 68, 2, 35, 83, 90, 25, 13, 24, 34, 42, 45, 74, + 67, 16, 84, 97, 19, 64, 27, 14, 31, 12, 54, 60, 3, 8, 17, 89, 81, 94, 7, 29}, + {1, 18, 25, 13, 57, 53, 92, 95, 94, 88, 10, 82, 34, 83, 7, 28, 55, 11, 5, 58, + 8, 9, 74, 99, 52, 32, 62, 45, 65, 50, 41, 56, 96, 44, 38, 47, 3, 39, 81, 19, + 2, 22, 49, 12, 40, 67, 89, 76, 64, 70, 61, 21, 31, 54, 51, 17, 46, 30, 24, 60, + 66, 26, 77, 63, 37, 35, 23, 6, 4, 87, 79, 97, 20, 48, 36, 85, 73, 71, 27, 43, + 0, 15, 69, 78, 59, 86, 75, 33, 42, 84, 91, 93, 68, 80, 16, 98, 90, 72, 14, 29}, + {30, 16, 10, 67, 83, 81, 42, 94, 35, 59, 22, 5, 53, 61, 1, 96, 68, 45, 95, 66, + 36, 37, 97, 87, 77, 93, 86, 33, 49, 13, 43, 14, 0, 34, 7, 2, 41, 21, 31, 32, + 76, 55, 62, 72, 90, 71, 38, 23, 46, 12, 51, 19, 6, 44, 52, 48, 73, 24, 29, 74, + 85, 99, 80, 28, 47, 9, 39, 84, 64, 57, 75, 98, 88, 26, 17, 63, 27, 56, 69, 8, + 54, 25, 91, 78, 3, 50, 65, 70, 11, 4, 92, 60, 40, 58, 15, 89, 82, 18, 79, 20}, + {60, 88, 79, 35, 40, 43, 52, 4, 17, 31, 84, 44, 50, 94, 91, 38, 87, 25, 92, 62, + 14, 97, 37, 18, 86, 28, 65, 90, 19, 29, 30, 75, 68, 49, 69, 11, 59, 41, 89, 33, + 47, 54, 98, 42, 80, 56, 78, 99, 23, 82, 9, 48, 21, 1, 63, 22, 58, 95, 10, 12, + 26, 74, 85, 13, 8, 27, 24, 53, 61, 34, 64, 71, 66, 32, 83, 73, 15, 67, 0, 20, + 16, 6, 77, 45, 5, 93, 76, 39, 57, 2, 96, 46, 51, 3, 55, 7, 81, 70, 36, 72}, + {35, 56, 77, 58, 23, 83, 91, 81, 53, 70, 20, 74, 84, 50, 79, 51, 88, 44, 45, 71, + 37, 26, 54, 48, 27, 43, 0, 12, 95, 97, 1, 63, 67, 98, 59, 40, 13, 19, 31, 87, + 99, 16, 57, 30, 17, 92, 55, 68, 5, 46, 64, 8, 7, 42, 90, 33, 29, 9, 85, 24, + 2, 69, 32, 10, 21, 65, 39, 86, 52, 62, 76, 89, 82, 6, 78, 4, 47, 28, 3, 75, + 41, 15, 18, 14, 66, 73, 38, 80, 61, 49, 11, 22, 34, 93, 36, 25, 94, 96, 60, 72}, + {6, 90, 16, 43, 20, 55, 94, 8, 62, 59, 47, 32, 81, 67, 42, 35, 71, 19, 54, 49, + 2, 88, 38, 5, 53, 77, 85, 11, 73, 34, 3, 72, 95, 58, 82, 64, 44, 29, 93, 14, + 56, 98, 4, 37, 45, 75, 86, 60, 87, 7, 31, 17, 89, 97, 10, 70, 36, 99, 46, 63, + 76, 41, 48, 84, 9, 52, 1, 24, 83, 25, 68, 18, 50, 26, 30, 27, 61, 0, 91, 33, + 51, 40, 79, 39, 15, 74, 28, 78, 69, 96, 57, 65, 21, 80, 22, 23, 12, 13, 92, 66}, + }, + { + {93, 42, 18, 65, 28, 87, 39, 35, 71, 23, 96, 90, 15, 58, 78, 33, 92, 88, 49, 64, + 24, 84, 77, 8, 97, 50, 4, 53, 27, 83, 95, 31, 80, 47, 86, 55, 11, 40, 37, 14, + 34, 60, 54, 12, 94, 9, 41, 2, 26, 10, 6, 57, 75, 63, 32, 43, 13, 52, 82, 73, + 20, 30, 17, 5, 3, 68, 51, 89, 62, 79, 69, 70, 66, 45, 25, 22, 85, 0, 91, 76, + 38, 7, 99, 61, 29, 98, 16, 36, 44, 67, 59, 56, 48, 81, 21, 46, 74, 72, 19, 1}, + {78, 91, 97, 77, 75, 85, 32, 2, 40, 86, 38, 18, 56, 47, 50, 87, 27, 92, 64, 39, + 98, 23, 33, 72, 84, 58, 16, 99, 19, 65, 55, 73, 74, 42, 4, 48, 95, 8, 51, 3, + 37, 82, 96, 69, 14, 44, 12, 21, 76, 70, 36, 15, 71, 41, 20, 31, 54, 13, 0, 9, + 94, 67, 1, 60, 81, 61, 11, 6, 90, 5, 49, 10, 34, 7, 35, 30, 25, 88, 79, 93, + 66, 62, 22, 24, 52, 17, 89, 45, 83, 57, 28, 46, 63, 68, 26, 29, 80, 59, 43, 53}, + {78, 91, 97, 77, 75, 85, 32, 2, 40, 86, 38, 18, 56, 47, 50, 87, 27, 92, 64, 39, + 98, 23, 33, 72, 84, 58, 16, 99, 19, 65, 55, 73, 74, 42, 4, 48, 95, 8, 51, 3, + 37, 82, 96, 69, 14, 44, 12, 21, 76, 70, 36, 15, 71, 41, 20, 31, 54, 13, 0, 9, + 94, 67, 1, 60, 81, 61, 11, 6, 90, 5, 49, 10, 34, 7, 35, 30, 25, 88, 79, 93, + 66, 62, 22, 24, 52, 17, 89, 45, 83, 57, 28, 46, 63, 68, 26, 29, 80, 59, 43, 53}, + {41, 65, 16, 86, 77, 67, 51, 81, 50, 25, 90, 87, 98, 99, 23, 9, 4, 97, 39, 36, + 26, 42, 13, 66, 82, 22, 47, 88, 49, 46, 59, 78, 20, 19, 7, 93, 56, 84, 40, 61, + 89, 21, 74, 1, 54, 44, 34, 6, 28, 8, 43, 76, 48, 71, 85, 38, 83, 18, 24, 79, + 68, 80, 45, 33, 91, 27, 32, 70, 95, 3, 58, 60, 73, 10, 75, 52, 64, 2, 94, 30, + 17, 55, 62, 35, 11, 12, 53, 63, 14, 31, 57, 0, 37, 72, 92, 15, 5, 29, 69, 96}, + {74, 50, 44, 80, 11, 46, 39, 54, 24, 3, 7, 62, 41, 8, 75, 23, 57, 82, 51, 86, + 97, 79, 22, 60, 42, 14, 28, 67, 32, 73, 48, 56, 89, 26, 25, 77, 21, 68, 10, 2, + 58, 76, 92, 95, 38, 29, 43, 35, 12, 16, 55, 17, 63, 91, 45, 64, 59, 13, 47, 31, + 83, 5, 99, 49, 78, 71, 96, 53, 36, 90, 1, 66, 15, 0, 85, 65, 27, 4, 98, 94, + 6, 84, 72, 30, 70, 20, 61, 69, 37, 93, 34, 87, 9, 52, 88, 81, 33, 19, 40, 18}, + {35, 67, 27, 53, 26, 79, 20, 46, 38, 4, 72, 84, 70, 58, 65, 91, 92, 21, 32, 71, + 54, 24, 89, 0, 37, 8, 22, 81, 12, 87, 43, 18, 82, 17, 33, 76, 25, 13, 74, 51, + 99, 3, 55, 60, 19, 73, 86, 62, 93, 52, 31, 64, 96, 66, 14, 40, 42, 69, 45, 6, + 61, 34, 41, 59, 49, 10, 39, 2, 75, 80, 15, 94, 7, 23, 95, 78, 90, 5, 1, 28, + 56, 30, 9, 98, 44, 48, 68, 77, 47, 11, 63, 83, 88, 36, 97, 29, 57, 50, 16, 85}, + {8, 51, 32, 96, 34, 24, 88, 54, 87, 83, 45, 99, 49, 30, 55, 61, 95, 60, 59, 76, + 57, 5, 19, 56, 89, 66, 27, 94, 14, 6, 21, 58, 43, 74, 10, 2, 17, 62, 47, 16, + 69, 84, 63, 39, 4, 92, 35, 48, 53, 64, 71, 50, 72, 28, 42, 46, 91, 23, 15, 40, + 11, 78, 29, 86, 13, 90, 85, 20, 65, 93, 73, 80, 41, 37, 82, 79, 52, 9, 97, 38, + 75, 25, 44, 7, 26, 12, 22, 0, 33, 1, 31, 68, 70, 67, 98, 18, 77, 36, 3, 81}, + {34, 17, 91, 45, 63, 64, 1, 71, 98, 78, 94, 3, 99, 12, 15, 93, 69, 19, 61, 80, + 92, 81, 33, 51, 40, 10, 75, 24, 5, 87, 73, 46, 52, 86, 4, 70, 89, 56, 2, 67, + 72, 58, 49, 84, 29, 18, 22, 38, 95, 36, 43, 35, 37, 85, 57, 8, 76, 7, 27, 28, + 55, 74, 42, 13, 77, 31, 60, 39, 48, 68, 97, 79, 44, 54, 88, 82, 21, 66, 9, 23, + 59, 50, 47, 16, 83, 25, 6, 30, 41, 96, 90, 14, 62, 20, 65, 11, 26, 32, 53, 0}, + {7, 51, 77, 80, 0, 53, 56, 27, 2, 78, 28, 32, 26, 22, 73, 93, 36, 54, 64, 99, + 60, 50, 21, 11, 39, 42, 81, 8, 1, 79, 61, 5, 15, 31, 59, 33, 20, 94, 23, 88, + 69, 17, 35, 95, 40, 75, 85, 87, 43, 47, 89, 12, 71, 72, 9, 83, 67, 38, 90, 82, + 29, 10, 37, 91, 97, 74, 6, 86, 30, 63, 24, 49, 3, 13, 4, 57, 76, 70, 98, 34, + 58, 52, 96, 84, 25, 55, 44, 46, 18, 65, 62, 68, 48, 66, 41, 19, 14, 45, 16, 92}, + {96, 2, 76, 15, 31, 22, 71, 38, 88, 26, 67, 0, 21, 10, 58, 39, 63, 77, 23, 50, + 48, 19, 78, 30, 93, 91, 14, 12, 28, 11, 53, 73, 60, 61, 35, 79, 57, 70, 40, 20, + 56, 32, 65, 64, 68, 55, 82, 94, 45, 95, 66, 7, 43, 16, 37, 99, 72, 62, 13, 4, + 47, 8, 98, 89, 46, 29, 51, 85, 74, 17, 97, 84, 33, 18, 25, 83, 69, 44, 87, 49, + 92, 9, 90, 36, 41, 81, 59, 54, 34, 6, 42, 5, 80, 3, 1, 86, 27, 52, 24, 75}, + }, + { + {61, 85, 41, 37, 12, 25, 27, 17, 92, 52, 44, 66, 13, 15, 19, 78, 77, 9, 40, 20, + 58, 43, 76, 24, 59, 42, 28, 30, 96, 86, 62, 89, 67, 35, 45, 6, 36, 50, 74, 26, + 84, 95, 7, 68, 97, 79, 51, 16, 80, 3, 82, 2, 93, 21, 31, 98, 18, 60, 39, 72, + 54, 65, 73, 32, 5, 70, 99, 10, 14, 71, 34, 4, 64, 33, 55, 48, 75, 87, 57, 81, + 22, 46, 69, 63, 90, 23, 38, 56, 8, 29, 83, 88, 11, 0, 47, 53, 91, 94, 49, 1}, + {35, 22, 67, 24, 94, 82, 12, 68, 30, 43, 58, 96, 27, 69, 65, 18, 7, 79, 14, 2, + 44, 60, 54, 47, 51, 83, 89, 3, 20, 11, 40, 85, 37, 48, 49, 8, 73, 15, 90, 93, + 70, 84, 34, 4, 86, 98, 17, 46, 72, 63, 26, 56, 97, 74, 91, 99, 36, 80, 81, 41, + 53, 88, 77, 21, 0, 23, 64, 76, 95, 33, 62, 1, 75, 87, 10, 66, 57, 6, 28, 25, + 42, 9, 50, 39, 29, 78, 13, 19, 31, 45, 55, 61, 38, 71, 59, 5, 92, 52, 32, 16}, + {70, 29, 50, 57, 31, 9, 59, 45, 87, 71, 98, 89, 77, 97, 86, 5, 21, 61, 47, 41, + 76, 37, 65, 12, 58, 75, 78, 91, 27, 63, 64, 83, 55, 95, 4, 11, 82, 23, 39, 40, + 42, 68, 69, 48, 54, 13, 99, 33, 26, 0, 66, 79, 46, 72, 49, 44, 25, 74, 73, 28, + 38, 10, 2, 85, 15, 19, 52, 94, 7, 36, 17, 32, 96, 3, 20, 30, 92, 51, 14, 90, + 24, 67, 43, 34, 93, 84, 62, 81, 1, 56, 53, 88, 80, 35, 22, 6, 18, 16, 8, 60}, + {62, 75, 38, 50, 52, 86, 53, 73, 99, 59, 26, 46, 48, 90, 64, 1, 65, 13, 30, 36, + 57, 16, 97, 91, 51, 33, 80, 67, 5, 25, 8, 96, 94, 84, 6, 89, 88, 20, 10, 85, + 41, 56, 9, 34, 63, 0, 18, 12, 42, 15, 87, 4, 61, 43, 82, 17, 98, 60, 92, 70, + 49, 71, 47, 54, 79, 83, 27, 74, 58, 2, 76, 93, 22, 69, 78, 44, 28, 95, 72, 68, + 7, 55, 29, 77, 14, 23, 31, 32, 3, 37, 11, 66, 35, 39, 24, 81, 19, 21, 40, 45}, + {65, 78, 7, 83, 80, 77, 71, 72, 22, 85, 63, 73, 44, 87, 86, 88, 41, 30, 95, 67, + 58, 21, 4, 5, 26, 27, 75, 31, 99, 52, 92, 64, 28, 8, 19, 98, 84, 34, 9, 48, + 60, 0, 56, 1, 62, 16, 91, 32, 89, 79, 40, 11, 54, 51, 43, 12, 45, 66, 23, 13, + 96, 81, 47, 36, 46, 93, 2, 69, 33, 3, 20, 6, 35, 18, 74, 68, 53, 37, 29, 61, + 90, 57, 14, 42, 24, 76, 10, 38, 97, 39, 25, 50, 49, 70, 55, 82, 15, 17, 59, 94}, + {11, 17, 21, 48, 63, 0, 73, 14, 10, 88, 2, 28, 6, 9, 19, 69, 81, 13, 35, 95, + 60, 56, 30, 47, 91, 92, 86, 29, 76, 50, 16, 25, 82, 93, 8, 5, 58, 94, 98, 20, + 74, 77, 4, 52, 97, 79, 27, 42, 36, 12, 26, 72, 71, 45, 24, 87, 1, 39, 22, 89, + 53, 31, 61, 37, 33, 18, 59, 43, 80, 15, 54, 38, 78, 90, 32, 83, 23, 44, 55, 66, + 64, 3, 67, 49, 84, 62, 51, 41, 46, 85, 96, 34, 99, 75, 7, 40, 70, 68, 65, 57}, + {43, 38, 48, 47, 74, 88, 64, 54, 59, 35, 29, 18, 53, 91, 68, 66, 12, 79, 55, 41, + 34, 36, 33, 94, 56, 87, 25, 6, 1, 8, 63, 96, 93, 44, 40, 65, 13, 77, 61, 85, + 99, 80, 11, 51, 14, 46, 5, 39, 92, 27, 75, 52, 72, 83, 19, 62, 32, 24, 82, 95, + 17, 37, 0, 15, 97, 4, 42, 86, 84, 3, 26, 81, 98, 9, 73, 22, 28, 78, 60, 76, + 16, 23, 71, 89, 31, 57, 58, 20, 50, 10, 49, 45, 21, 67, 90, 30, 7, 2, 70, 69}, + {78, 35, 94, 59, 8, 23, 10, 76, 96, 80, 93, 9, 27, 95, 61, 84, 88, 91, 2, 33, + 1, 85, 3, 36, 79, 18, 92, 5, 34, 81, 98, 40, 38, 68, 90, 70, 89, 65, 48, 6, + 77, 30, 72, 62, 16, 83, 32, 54, 99, 82, 75, 41, 53, 17, 67, 43, 28, 21, 46, 7, + 25, 64, 13, 14, 87, 58, 63, 31, 39, 47, 24, 71, 74, 55, 20, 11, 22, 45, 44, 51, + 37, 15, 42, 56, 66, 29, 86, 49, 69, 57, 73, 50, 60, 0, 4, 19, 52, 97, 12, 26}, + {88, 86, 41, 1, 70, 10, 19, 2, 11, 44, 87, 26, 53, 59, 40, 18, 61, 13, 21, 56, + 85, 75, 25, 5, 97, 7, 36, 67, 76, 79, 90, 65, 91, 0, 98, 89, 20, 55, 93, 58, + 28, 63, 6, 83, 47, 71, 73, 49, 39, 31, 48, 69, 82, 50, 78, 62, 14, 74, 29, 54, + 57, 80, 32, 94, 3, 66, 30, 35, 8, 46, 16, 51, 24, 17, 4, 84, 45, 60, 34, 15, + 72, 77, 42, 12, 64, 9, 33, 95, 81, 96, 38, 43, 52, 99, 22, 92, 27, 23, 37, 68}, + {90, 93, 83, 21, 96, 71, 67, 28, 37, 75, 55, 58, 56, 39, 16, 46, 91, 41, 77, 92, + 99, 65, 27, 5, 24, 1, 69, 7, 31, 35, 33, 64, 12, 14, 45, 11, 98, 0, 57, 72, + 74, 13, 61, 40, 63, 89, 53, 85, 18, 76, 2, 70, 20, 60, 84, 52, 25, 43, 54, 17, + 36, 19, 97, 73, 94, 62, 3, 23, 34, 88, 29, 80, 50, 22, 81, 87, 6, 30, 78, 48, + 79, 44, 66, 8, 51, 86, 4, 15, 42, 10, 82, 32, 68, 26, 38, 47, 49, 9, 95, 59}, + }, +}; + +static const uint8_t access_code_table_2[10][10] = { + {4, 7, 8, 0, 9, 1, 3, 6, 2, 5}, + {5, 0, 4, 1, 3, 8, 9, 7, 6, 2}, + {0, 2, 7, 8, 9, 1, 6, 3, 5, 4}, + {5, 9, 8, 4, 1, 7, 0, 2, 3, 6}, + {7, 3, 0, 1, 8, 9, 6, 5, 2, 4}, + {7, 2, 4, 0, 1, 9, 6, 3, 5, 8}, + {4, 1, 6, 3, 5, 8, 0, 7, 2, 9}, + {6, 8, 4, 1, 5, 3, 7, 2, 9, 0}, + {4, 1, 8, 3, 7, 2, 0, 6, 5, 9}, + {7, 4, 5, 6, 2, 3, 9, 1, 8, 0}}; + +static void rotate_right(uint8_t* data, int n_bytes, int n_bits) { + uint8_t prior = data[n_bytes - 1]; + for(int i = 0; i < n_bytes; i++) { + uint8_t tmp = data[i]; + data[i] = (data[i] >> n_bits) | ((prior & ((1 << n_bits) - 1)) << (8 - n_bits)); + prior = tmp; + } +} + +static void decrypt_spad_0(const uint8_t* spad, uint8_t* decrypted) { + for(int i = 0; i < 16; i++) { + decrypted[i] = s_box[N_TABLES][spad[i]]; + } + + int count = (decrypted[15] >> 4) + 7; + int table = decrypted[15] + ITERATION_ADD * count; + + for(int iter = 0; iter < count; iter++) { + table -= ITERATION_ADD; + rotate_right(decrypted, 15, 5); // only the first 15 bytes + for(int i = 0; i < 15; i++) { + decrypted[i] = s_box[table % N_TABLES][decrypted[i]]; + } + } +} + +static uint16_t crc16(uint64_t data, int bits, uint16_t init, uint16_t poly) { + uint16_t v = init; + for(int i = 0; i < bits; ++i) { + v = (v >> 1) ^ (((v ^ data) & 1ULL) ? poly : 0); + data >>= 1; + } + return v; +} + +static bool + check_access_code_crc(const uint8_t ac[3], const uint8_t body[6], const uint8_t crc[5]) { + uint64_t msg = 0; + for(int i = 0; i < 3; ++i) { + if(ac[i] > 0xF) return false; + msg = (msg << 4) | ac[i]; + } + for(int i = 0; i < 6; ++i) { + uint8_t v = body[i]; + if(v > 99) return false; + msg = (msg << 4) | (v / 10); + msg = (msg << 4) | (v % 10); + } + uint16_t calculated_crc = crc16(msg, 60, 0xFFFF, 0x8408); + uint16_t expected_crc = crc[0] * 10000 + crc[1] * 1000 + crc[2] * 100 + crc[3] * 10 + crc[4]; + return (calculated_crc == expected_crc); +} + +static void parse_access_code(const uint8_t* access_code, FuriString* parsed_data) { + furi_assert(access_code); + furi_assert(parsed_data); + + uint8_t decrypted[6]; + // decrypted contains the decoded serial bytes (as 6 BCD bytes) + for(int i = 0, j = 3; i < 6; ++i, j += 2) { + decrypted[i] = (access_code[j]) * 10 + (access_code[j + 1]); + } + + uint8_t crc[5] = {0}; + memcpy(crc, access_code + 15, 5); + + int boxes1[6] = { + (crc[3] + crc[2]) % 10, crc[2], crc[3], crc[4], (crc[4] + crc[0]) % 10, crc[1]}; + + for(int n = 0; n < 6; ++n) { + decrypted[n] = access_code_table_1[4][boxes1[n]][decrypted[n]]; + } + + int rv = decrypted[0] / 10; + + int boxes2[6] = { + (crc[1] + crc[0]) % 10, crc[1], crc[2], crc[3], crc[4], (crc[4] + crc[1]) % 10}; + for(int n = 0; n < 6; ++n) { + if(n == 0) { + decrypted[n] = (decrypted[n] & 0xF0) | + access_code_table_2[boxes2[n]][decrypted[n] & 0x0F]; + } else { + decrypted[n] = access_code_table_1[rv][boxes2[n]][decrypted[n]]; + } + } + + furi_string_cat_printf( + parsed_data, + "Decrypted serial number:\n%02d%02d%02d%02d%02d%02d\n", + decrypted[0], + decrypted[1], + decrypted[2], + decrypted[3], + decrypted[4], + decrypted[5]); + + furi_string_cat_printf( + parsed_data, + "CRC check: %s\n", + check_access_code_crc(access_code, decrypted, crc) ? "Passed" : "Invalid"); +} + +bool aic_parse(const NfcDevice* device, FuriString* parsed_data) { + furi_assert(device); + furi_assert(parsed_data); + bool parsed = false; + + if(nfc_device_get_protocol(device) != NfcProtocolFelica) return false; + + const FelicaData* data = nfc_device_get_data(device, NfcProtocolFelica); + + const uint8_t ic_type = data->pmm.data[1]; + if(ic_type != 0xF0 && ic_type != 0xF1) { + // Must be Felica Lite (0xF0) or Lite-S (0xF1) to parse + return false; + } + + const uint8_t data_format_code_1 = data->data.fs.id.data[8]; + if(data_format_code_1 != 0) { + // We only know Data Format Code {0x00, 0xXX} + return false; + } + + parsed = true; + furi_string_printf(parsed_data, "\e#Amusement IC Card\n"); + furi_string_cat_str( + parsed_data, "::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::"); + furi_string_cat_str(parsed_data, "\nType:\n"); + + // Determine card brand and type + const uint8_t data_format_code_2 = data->data.fs.id.data[9]; + switch(data_format_code_2) { + case 0x2A: + furi_string_cat_str(parsed_data, "Bandai Namco Passport\n"); + break; + case 0x3A: + furi_string_cat_str(parsed_data, "Bandai Namco Passport (Old)\n"); + break; + case 0x68: + furi_string_cat_str(parsed_data, "Konami e-amusement pass\n"); + break; + case 0x78: + furi_string_cat_str(parsed_data, "SEGA Aime\n"); + break; + case 0x79: + furi_string_cat_str(parsed_data, "Taito NESiCA\n"); + break; + default: + parsed = false; + return parsed; // Unknown vendor + } + furi_string_cat_str( + parsed_data, "::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::"); + + // decrypt_spad_0 S-PAD 0 + uint8_t decrypted[16] = {0}; + decrypt_spad_0(data->data.fs.spad[0].data, decrypted); + + // Get Access Code + uint8_t access_code[20] = {0}; + for(int i = 0; i < 10; i++) { + access_code[i * 2] = (decrypted[i + 6] & 0xF0) >> 4; // Get upper nibble + access_code[i * 2 + 1] = decrypted[i + 6] & 0x0F; // Get lower nibble + } + furi_string_cat_str(parsed_data, "\nAccess Code:\n"); + bool access_code_is_bcd = true; + for(int i = 0; i < 20; i++) { + furi_string_cat_printf(parsed_data, "%d", access_code[i]); + if(i % 4 == 3) { + furi_string_cat_str(parsed_data, " "); + } + if(access_code[i] > 9) access_code_is_bcd = false; + } + furi_string_cat_str(parsed_data, "\n"); + + furi_string_cat_printf(parsed_data, "BCD valid: %s\n", access_code_is_bcd ? "Yes" : "No"); + furi_string_cat_str( + parsed_data, "::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::"); + + // Parse Access Code + if(access_code_is_bcd && access_code[0] == 5) { + furi_string_cat_str(parsed_data, "\n"); + parse_access_code(access_code, parsed_data); + } else { + furi_string_cat_printf( + parsed_data, "\nAccess code preamble wrong: expected 5, got %d\n", access_code[0]); + } + + furi_string_cat_str( + parsed_data, "::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::"); + furi_string_cat_str(parsed_data, "\nDecrypted S-PAD 0:\n"); + for(int i = 0; i < 16; i++) { + furi_string_cat_printf(parsed_data, "%02X ", decrypted[i]); + if(i == 7) { + furi_string_cat_str(parsed_data, "\n"); + } + } + furi_string_cat_str(parsed_data, "\n"); + furi_string_cat_str( + parsed_data, "::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::"); + + return parsed; +} + +/* Actual implementation of app<>plugin interface */ +static const NfcSupportedCardsPlugin aic_plugin = { + .protocol = NfcProtocolFelica, + .verify = NULL, + .read = NULL, + .parse = aic_parse, +}; + +/* Plugin descriptor to comply with basic plugin specification */ +static const FlipperAppPluginDescriptor aic_plugin_descriptor = { + .appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID, + .ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION, + .entry_point = &aic_plugin, +}; + +/* Plugin entry point - must return a pointer to const descriptor */ +const FlipperAppPluginDescriptor* aic_plugin_ep(void) { + return &aic_plugin_descriptor; +} From 973fb26f575b70083f328bfc56e409d15a972fd0 Mon Sep 17 00:00:00 2001 From: minchogaydarov <134236905+minchogaydarov@users.noreply.github.com> Date: Wed, 24 Sep 2025 18:23:01 +0300 Subject: [PATCH 08/12] Add Daikin FTXN25LV1B9 and Toyotomi KTN22-12R32 (#4232) Co-authored-by: hedger --- .../infrared/resources/infrared/assets/ac.ir | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/applications/main/infrared/resources/infrared/assets/ac.ir b/applications/main/infrared/resources/infrared/assets/ac.ir index 96b4eb1c8..a7555e007 100644 --- a/applications/main/infrared/resources/infrared/assets/ac.ir +++ b/applications/main/infrared/resources/infrared/assets/ac.ir @@ -1935,3 +1935,79 @@ frequency: 38000 duty_cycle: 0.330000 data: 39670 99106 3227 1570 424 406 422 407 422 1183 424 405 424 1184 448 380 423 407 421 406 424 1183 424 1185 421 406 423 404 424 405 424 1184 423 1182 425 407 448 380 423 407 422 405 423 406 422 406 424 405 423 407 421 406 423 407 422 405 424 405 423 406 423 1184 422 408 421 408 422 405 424 406 421 407 422 406 423 405 423 1183 424 406 423 405 423 405 423 405 423 1186 421 1184 422 1184 422 1185 422 1184 447 1159 423 1184 422 1184 422 408 421 407 423 1184 421 407 448 381 422 405 423 409 421 406 422 406 422 407 422 406 423 1183 423 1185 422 406 423 405 424 1184 423 408 421 405 424 405 424 1184 422 1185 422 1184 422 407 423 408 420 409 420 1185 447 382 423 405 423 408 421 406 423 407 422 406 423 406 423 408 421 406 423 1183 424 407 422 406 424 405 424 406 423 407 423 406 423 408 422 407 422 405 424 408 421 407 422 407 422 406 423 406 423 407 422 406 423 406 422 408 421 407 422 408 421 407 422 406 423 408 422 406 423 405 423 409 422 406 422 406 423 406 423 407 422 407 423 405 424 1184 423 407 421 406 424 1184 423 1184 422 407 423 1183 423 405 424 1184 423 409 420 407 422 # +# Model: Daikin FTXN25LV1B9 +# +name: Off +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9876 9703 9956 9681 4738 2366 489 310 423 822 488 822 488 311 422 823 487 311 423 310 424 310 423 310 424 823 487 311 422 310 424 823 487 310 424 311 422 310 424 311 422 823 487 311 422 310 423 310 424 823 488 311 423 310 423 310 423 823 488 310 423 310 424 310 424 823 487 310 424 311 423 824 487 823 487 823 487 310 423 823 487 311 423 311 423 310 424 311 422 824 487 310 423 311 422 824 487 311 422 310 424 310 423 824 486 824 487 310 424 310 423 311 423 824 487 311 422 310 423 310 424 310 423 824 486 824 486 310 424 824 486 824 486 824 486 20121 4685 +# +name: Dh +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9907 9648 9958 9677 4742 2362 492 311 422 820 490 820 435 312 477 821 434 312 421 312 422 312 422 876 434 312 422 312 422 312 422 876 434 312 422 312 422 312 421 312 422 876 434 312 422 312 421 312 422 876 434 312 422 311 422 312 477 821 434 312 422 312 421 312 477 821 434 312 422 312 422 876 434 877 434 876 434 312 422 876 434 312 422 311 422 312 422 312 422 876 434 312 422 312 422 876 434 312 422 312 421 312 422 877 433 876 434 312 422 312 422 312 422 876 434 312 422 312 421 312 422 312 422 877 433 312 422 877 433 312 422 877 433 312 422 20174 4740 +# +name: Heat_lo +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9827 9757 9876 9732 4687 2418 435 311 422 876 434 876 434 311 423 877 433 312 421 311 423 311 422 311 423 311 423 311 423 877 433 877 433 311 423 311 423 312 422 877 433 877 433 311 423 311 423 311 423 877 434 311 422 311 423 311 422 877 433 311 423 312 421 312 422 877 433 311 423 312 421 877 433 877 433 877 434 311 423 877 433 311 423 311 423 311 422 311 423 877 433 312 422 311 423 877 433 311 423 312 422 311 423 877 433 878 433 311 423 312 422 312 422 877 433 311 423 311 422 311 423 312 422 878 432 312 422 877 433 311 423 877 433 878 433 20174 4685 +# +name: Heat_hi +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9898 9680 9901 9734 4685 2420 434 312 421 876 434 876 434 312 422 876 434 312 422 312 422 312 422 312 421 312 422 312 422 877 433 877 433 312 422 312 421 312 422 877 433 877 433 312 422 312 422 312 422 876 434 312 422 312 422 312 422 877 434 312 422 312 421 312 422 877 433 312 421 312 422 877 433 877 434 877 433 312 422 877 433 312 422 312 421 312 422 312 422 877 433 312 421 312 422 877 433 312 422 312 422 312 421 312 422 312 422 312 422 312 421 877 433 877 434 312 421 312 422 312 422 312 422 877 433 312 422 877 433 877 433 312 421 877 433 20174 4684 +# +name: Cool_hi +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9932 9649 9959 9677 4742 2362 492 311 422 819 491 819 491 311 422 820 490 311 423 311 423 311 423 311 422 820 490 310 423 310 424 820 490 311 423 311 423 310 424 820 490 311 422 820 491 311 423 311 422 820 491 311 422 311 423 311 423 820 490 311 423 311 423 311 423 820 490 310 424 311 423 820 490 820 490 820 490 310 424 820 490 311 423 310 424 310 424 311 422 820 491 311 422 310 424 820 490 310 424 311 423 310 423 311 423 820 490 820 490 311 423 820 490 311 422 311 423 311 423 311 423 311 422 821 490 311 422 821 490 820 490 311 422 821 490 20118 4740 +# +name: Cool_lo +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9878 9704 9903 9733 4687 2418 435 311 422 875 435 875 435 311 423 875 435 311 423 311 422 311 423 311 423 875 435 311 423 311 423 875 435 311 423 311 423 311 422 876 434 311 423 876 434 311 423 311 423 876 434 311 423 311 423 311 423 876 434 311 423 311 423 311 423 875 435 311 422 311 423 876 434 876 435 876 434 311 423 876 434 311 423 311 423 311 422 311 423 876 434 311 422 311 423 876 434 311 423 311 422 311 423 876 434 876 434 311 423 311 422 311 423 876 435 311 422 311 422 311 423 311 423 876 434 311 423 876 434 311 423 311 423 876 434 20174 4685 +# +# Model: Toyotomi KTN22-12R32 +# +name: Off +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9067 4414 724 1584 753 452 754 453 753 478 728 478 727 479 726 480 726 481 725 1584 725 1585 724 1584 725 482 724 482 725 482 725 482 724 483 724 483 723 482 725 483 723 483 723 482 724 1585 724 482 724 482 724 483 724 482 725 483 723 483 723 1584 724 482 724 1585 723 482 724 483 723 1585 723 483 723 19925 724 483 724 1585 724 482 724 482 724 482 724 482 724 483 724 482 724 482 724 1585 724 483 724 483 724 482 725 483 724 1585 723 1585 724 483 723 483 724 483 724 483 724 482 723 483 724 482 724 482 724 483 723 483 723 483 724 483 723 483 724 1585 724 1585 723 1585 724 39949 9071 4415 725 482 725 481 725 482 725 483 723 481 725 482 724 483 724 482 724 482 724 482 724 482 724 482 725 483 723 483 723 482 725 483 722 483 724 483 723 482 724 483 724 483 723 482 724 483 723 483 724 483 723 483 723 483 723 483 724 482 724 1585 723 483 724 1585 725 482 724 1585 724 483 724 19926 724 483 723 482 725 482 724 482 724 482 725 482 724 482 725 483 724 482 723 482 725 483 723 482 725 482 725 483 723 483 724 483 723 483 724 483 723 482 724 483 723 483 724 482 725 482 724 483 724 483 723 482 724 483 723 483 723 482 725 1585 723 483 724 1585 724 +# +name: Dh +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9042 4416 751 454 753 1555 753 453 754 1556 753 1558 750 479 726 480 726 482 724 1584 725 481 724 482 725 1585 725 482 725 482 724 482 724 482 725 482 725 483 723 483 724 482 724 482 724 1586 723 1585 723 482 724 483 724 483 724 483 724 482 725 1585 724 482 724 1585 724 484 723 482 724 1585 723 483 724 19928 723 482 725 482 724 482 725 483 724 483 724 482 725 482 724 483 724 483 723 1585 723 482 724 483 723 482 724 483 724 1585 724 1585 724 482 724 482 724 482 725 482 725 482 724 483 724 483 723 483 724 482 724 483 724 483 724 483 725 1585 724 483 723 482 724 1585 723 39946 9070 4414 725 481 725 481 725 482 724 482 724 482 724 481 725 482 724 482 724 482 725 483 723 482 725 483 724 482 725 482 724 482 724 482 724 483 723 483 724 483 724 482 725 483 724 482 724 482 725 483 723 482 725 483 723 482 724 483 724 482 724 1584 724 482 724 1585 724 483 724 1586 723 482 725 19926 724 482 724 482 724 482 725 483 723 482 725 482 725 482 724 483 724 482 724 482 725 483 724 482 725 483 723 483 723 483 723 482 724 483 724 483 723 482 724 483 724 482 724 483 723 483 723 483 724 483 723 483 723 482 724 483 723 483 724 1585 724 483 723 1586 723 +# +name: Heat_lo +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9040 4431 702 503 702 503 702 1604 758 1548 756 448 729 475 730 475 729 477 728 1579 726 1579 727 1580 726 479 726 481 724 480 724 481 724 482 724 482 723 482 723 482 723 482 723 483 722 1583 724 1584 722 483 723 483 722 482 723 483 722 483 723 1584 722 482 724 1584 722 482 723 483 722 1585 721 483 723 19907 724 481 723 482 723 482 723 482 723 482 723 482 723 483 723 483 722 482 723 1584 722 482 722 482 723 483 723 482 723 1584 723 1584 721 483 722 483 722 483 722 484 721 482 723 483 723 483 722 483 722 483 723 483 722 484 721 484 721 1607 699 483 722 483 722 1608 698 39905 9065 4406 726 479 726 480 724 480 725 481 725 481 724 482 723 482 723 482 724 482 723 483 723 482 723 482 723 482 723 482 724 483 722 482 723 483 723 483 723 483 722 483 723 483 722 483 722 483 722 507 699 482 723 484 722 483 722 483 722 483 722 1607 699 483 722 1585 722 483 722 1584 723 506 698 19908 723 481 724 482 723 482 724 482 722 482 724 482 724 482 723 483 722 482 723 483 722 482 723 483 722 483 722 483 722 482 723 506 699 483 722 484 722 482 723 507 700 484 721 483 722 484 722 483 722 507 698 506 699 507 699 507 698 507 698 1608 698 507 698 1607 699 +# +name: Heat_hi +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9074 4437 701 506 700 506 700 1607 702 1607 702 505 701 505 701 505 726 480 727 480 726 1582 726 1584 725 1583 724 481 726 481 725 482 724 483 724 482 724 482 725 482 724 482 725 483 724 1584 725 1584 725 482 724 482 726 482 724 482 725 483 724 1584 724 483 723 1585 723 483 724 482 725 1584 725 483 723 19925 724 482 724 482 724 482 725 482 725 482 725 483 724 482 724 482 724 482 724 1584 724 483 724 482 724 481 724 482 724 1585 724 1585 724 482 724 482 724 482 725 483 723 483 724 483 724 482 724 482 724 483 723 482 725 482 724 482 724 483 724 482 724 483 724 483 723 39945 9072 4414 725 481 726 481 725 482 724 482 725 481 725 482 724 482 724 482 725 482 724 483 724 483 724 483 723 482 725 482 724 482 724 483 723 482 724 483 724 483 723 483 724 483 724 482 725 482 725 483 724 483 724 482 725 483 724 482 724 482 725 1585 724 483 724 1584 724 482 724 1585 724 483 723 19929 723 482 724 482 725 482 724 483 724 483 723 483 723 482 724 482 724 482 724 482 724 482 725 482 724 482 724 483 724 483 723 482 724 482 724 483 724 483 723 482 725 483 724 482 724 482 725 482 724 483 724 483 724 483 724 483 724 482 724 1585 723 483 724 1585 724 +# +name: Cool_hi +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9150 4356 783 1525 815 419 787 419 759 1549 760 447 758 447 758 449 756 451 755 451 755 451 754 452 754 453 754 452 754 453 753 453 753 454 753 454 752 453 753 454 752 454 753 455 751 1557 725 1585 725 481 725 481 725 482 725 482 725 482 724 1584 725 482 724 1584 725 482 724 481 726 1584 724 482 724 19927 724 481 726 482 724 482 725 481 725 481 725 482 725 482 724 481 726 481 725 1584 724 481 725 481 725 482 725 481 725 1584 725 1585 724 482 725 482 724 482 725 482 724 482 725 482 724 481 725 482 725 482 724 482 725 482 724 482 724 1584 724 1584 724 1584 724 1585 725 39945 9071 4412 726 480 726 480 726 481 725 481 725 481 725 481 725 481 725 482 725 481 724 481 726 482 725 481 725 482 724 482 724 482 725 482 724 481 725 482 725 482 725 482 724 482 724 482 725 481 725 482 725 482 725 481 726 482 724 482 724 481 726 1584 725 482 724 1584 724 482 724 1585 724 482 725 19924 725 481 725 481 726 481 725 481 725 481 726 481 724 482 725 482 724 481 725 481 725 481 725 481 726 482 725 481 725 481 725 482 724 481 752 455 752 454 753 455 751 454 752 454 753 454 751 453 753 454 752 454 753 454 753 454 752 455 752 1556 753 453 754 1557 752 +# +name: Cool_lo +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9049 4436 702 1606 703 504 730 476 759 1548 758 448 730 476 730 476 730 477 729 1580 727 1581 727 1581 727 480 726 481 725 481 725 481 724 482 724 507 699 483 724 483 723 507 699 506 701 1609 699 1609 699 507 700 507 699 507 700 507 699 507 700 1610 699 507 700 1609 700 507 699 507 700 1610 699 507 700 19928 723 482 724 483 723 483 723 484 722 507 699 507 700 507 699 507 699 507 700 1609 700 508 699 507 700 507 699 507 699 1609 700 1609 699 507 699 508 699 507 699 507 700 507 700 507 700 507 699 507 699 507 700 507 699 507 700 507 699 507 700 1610 699 1610 699 507 700 39944 9074 4410 727 479 727 480 726 481 726 481 725 482 725 483 723 483 724 507 699 507 700 484 722 507 699 507 699 507 699 506 700 507 699 507 700 507 699 507 698 507 699 508 699 507 699 507 699 507 700 507 700 507 698 507 699 508 699 507 699 507 699 1610 699 507 699 1610 699 507 699 1609 699 507 699 19926 724 483 723 506 700 507 699 507 700 507 700 507 700 507 699 507 700 507 700 507 699 507 700 507 699 507 699 507 700 506 700 508 699 507 700 507 700 507 699 507 699 507 700 507 700 507 700 508 699 507 699 507 699 508 699 507 699 507 700 1610 698 507 699 1609 699 +# \ No newline at end of file From 5c54be4d9c241b1244da8487944f7b13c96bae95 Mon Sep 17 00:00:00 2001 From: Alexander Bays Date: Wed, 24 Sep 2025 10:45:40 -0500 Subject: [PATCH 09/12] Infrared: Add text scroll to remote buttons (#4210) * Infrared: Add text scroll to universal remote buttons Replaces center aligned text in the infrared universal remote with scrollable text if wider than the button and is cut off. Allows long descriptive button functions to be seen in some remotes. * linter fixes --------- Co-authored-by: hedger Co-authored-by: hedger --- .../services/gui/modules/button_menu.c | 116 +++++++++++++----- 1 file changed, 88 insertions(+), 28 deletions(-) diff --git a/applications/services/gui/modules/button_menu.c b/applications/services/gui/modules/button_menu.c index d9c178dd2..5204c6f22 100644 --- a/applications/services/gui/modules/button_menu.c +++ b/applications/services/gui/modules/button_menu.c @@ -10,6 +10,7 @@ #include #include +#define SCROLL_INTERVAL (333) #define ITEM_FIRST_OFFSET 17 #define ITEM_NEXT_OFFSET 4 #define ITEM_HEIGHT 14 @@ -35,13 +36,56 @@ typedef struct { ButtonMenuItemArray_t items; size_t position; const char* header; + size_t scroll_counter; + FuriTimer* scroll_timer; } ButtonMenuModel; +static void button_menu_draw_text( + Canvas* canvas, + uint8_t item_x, + uint8_t item_y, + const char* text, + bool selected, + ButtonMenuModel* model) { + FuriString* disp_str; + disp_str = furi_string_alloc_set(text); + bool draw_static = true; + + if(selected) { + size_t text_width = canvas_string_width(canvas, furi_string_get_cstr(disp_str)); + if(text_width >= ITEM_WIDTH - 8) { + elements_scrollable_text_line( + canvas, + item_x + 4, + item_y + ITEM_HEIGHT - 4, + ITEM_WIDTH - 8, + disp_str, + model->scroll_counter, + false); + draw_static = false; + } + } + + if(draw_static) { + elements_string_fit_width(canvas, disp_str, ITEM_WIDTH - 6); + canvas_draw_str_aligned( + canvas, + item_x + (ITEM_WIDTH / 2), + item_y + (ITEM_HEIGHT / 2), + AlignCenter, + AlignCenter, + furi_string_get_cstr(disp_str)); + } + + furi_string_free(disp_str); +} + static void button_menu_draw_control_button( Canvas* canvas, uint8_t item_position, const char* text, - bool selected) { + bool selected, + ButtonMenuModel* model) { furi_assert(canvas); furi_assert(text); @@ -54,20 +98,16 @@ static void button_menu_draw_control_button( elements_slightly_rounded_box(canvas, item_x, item_y, ITEM_WIDTH, ITEM_HEIGHT); canvas_set_color(canvas, ColorWhite); } - canvas_draw_str_aligned( - canvas, - item_x + (ITEM_WIDTH / 2), - item_y + (ITEM_HEIGHT / 2), - AlignCenter, - AlignCenter, - text); + + button_menu_draw_text(canvas, item_x, item_y, text, selected, model); } static void button_menu_draw_common_button( Canvas* canvas, uint8_t item_position, const char* text, - bool selected) { + bool selected, + ButtonMenuModel* model) { furi_assert(canvas); furi_assert(text); @@ -83,19 +123,7 @@ static void button_menu_draw_common_button( canvas_draw_rframe(canvas, item_x, item_y, ITEM_WIDTH, ITEM_HEIGHT, 5); } - FuriString* disp_str; - disp_str = furi_string_alloc_set(text); - elements_string_fit_width(canvas, disp_str, ITEM_WIDTH - 6); - - canvas_draw_str_aligned( - canvas, - item_x + (ITEM_WIDTH / 2), - item_y + (ITEM_HEIGHT / 2), - AlignCenter, - AlignCenter, - furi_string_get_cstr(disp_str)); - - furi_string_free(disp_str); + button_menu_draw_text(canvas, item_x, item_y, text, selected, model); } static void button_menu_view_draw_callback(Canvas* canvas, void* _model) { @@ -120,9 +148,17 @@ static void button_menu_view_draw_callback(Canvas* canvas, void* _model) { if(model->header) { FuriString* disp_str; disp_str = furi_string_alloc_set(model->header); - elements_string_fit_width(canvas, disp_str, ITEM_WIDTH - 6); - canvas_draw_str_aligned( - canvas, 32, 10, AlignCenter, AlignCenter, furi_string_get_cstr(disp_str)); + size_t header_width = canvas_string_width(canvas, furi_string_get_cstr(disp_str)); + + if(header_width >= ITEM_WIDTH - 8) { + elements_scrollable_text_line( + canvas, 3, 13, ITEM_WIDTH - 8, disp_str, model->scroll_counter, false); + } else { + elements_string_fit_width(canvas, disp_str, ITEM_WIDTH - 8); + canvas_draw_str_aligned( + canvas, 32, 10, AlignCenter, AlignCenter, furi_string_get_cstr(disp_str)); + } + furi_string_free(disp_str); } @@ -137,13 +173,15 @@ static void button_menu_view_draw_callback(Canvas* canvas, void* _model) { canvas, item_position % BUTTONS_PER_SCREEN, ButtonMenuItemArray_cref(it)->label, - (item_position == model->position)); + (item_position == model->position), + model); } else if(ButtonMenuItemArray_cref(it)->type == ButtonMenuItemTypeCommon) { button_menu_draw_common_button( canvas, item_position % BUTTONS_PER_SCREEN, ButtonMenuItemArray_cref(it)->label, - (item_position == model->position)); + (item_position == model->position), + model); } } } @@ -158,8 +196,10 @@ static void button_menu_process_up(ButtonMenu* button_menu) { { if(model->position > 0) { model->position--; + model->scroll_counter = 0; } else { model->position = ButtonMenuItemArray_size(model->items) - 1; + model->scroll_counter = 0; } }, true); @@ -174,8 +214,10 @@ static void button_menu_process_down(ButtonMenu* button_menu) { { if(model->position < (ButtonMenuItemArray_size(model->items) - 1)) { model->position++; + model->scroll_counter = 0; } else { model->position = 0; + model->scroll_counter = 0; } }, true); @@ -193,8 +235,10 @@ static void button_menu_process_right(ButtonMenu* button_menu) { position_candidate -= position_candidate % BUTTONS_PER_SCREEN; if(position_candidate < (ButtonMenuItemArray_size(model->items))) { model->position = position_candidate; + model->scroll_counter = 0; } else { model->position = 0; + model->scroll_counter = 0; } } }, @@ -217,6 +261,7 @@ static void button_menu_process_left(ButtonMenu* button_menu) { }; position_candidate -= position_candidate % BUTTONS_PER_SCREEN; model->position = position_candidate; + model->scroll_counter = 0; } }, true); @@ -314,6 +359,7 @@ void button_menu_reset(ButtonMenu* button_menu) { ButtonMenuItemArray_reset(model->items); model->position = 0; model->header = NULL; + model->scroll_counter = 0; }, true); } @@ -351,6 +397,12 @@ ButtonMenuItem* button_menu_add_item( return item; } +static void button_menu_process_timer_callback(void* context) { + ButtonMenu* button_menu = context; + with_view_model( + button_menu->view, ButtonMenuModel * model, { model->scroll_counter++; }, true); +} + ButtonMenu* button_menu_alloc(void) { ButtonMenu* button_menu = malloc(sizeof(ButtonMenu)); button_menu->view = view_alloc(); @@ -367,6 +419,10 @@ ButtonMenu* button_menu_alloc(void) { ButtonMenuItemArray_init(model->items); model->position = 0; model->header = NULL; + model->scroll_counter = 0; + model->scroll_timer = furi_timer_alloc( + button_menu_process_timer_callback, FuriTimerTypePeriodic, button_menu); + furi_timer_start(model->scroll_timer, SCROLL_INTERVAL); }, true); @@ -380,7 +436,11 @@ void button_menu_free(ButtonMenu* button_menu) { with_view_model( button_menu->view, ButtonMenuModel * model, - { ButtonMenuItemArray_clear(model->items); }, + { + ButtonMenuItemArray_clear(model->items); + furi_timer_stop(model->scroll_timer); + furi_timer_free(model->scroll_timer); + }, true); view_free(button_menu->view); free(button_menu); From 30077dd512deb6e0c8033025e4413e755507296e Mon Sep 17 00:00:00 2001 From: MMX <10697207+xMasterX@users.noreply.github.com> Date: Wed, 24 Sep 2025 20:00:39 +0300 Subject: [PATCH 10/12] Fix PVS warnings (#4277) * Fixing PVS warns * pvs: additional fixes --------- Co-authored-by: hedger --- applications/debug/bt_debug_app/views/bt_test.c | 2 +- applications/main/infrared/infrared_brute_force.c | 2 +- applications/main/nfc/helpers/mfkey32_logger.c | 2 +- applications/main/nfc/helpers/nfc_supported_cards.c | 2 +- applications/main/onewire/onewire_cli.c | 2 +- .../subghz_frequency_analyzer_log_item_array.h | 2 +- applications/main/subghz/subghz_history.c | 2 +- applications/main/subghz/views/receiver.c | 2 +- .../desktop/animations/views/bubble_animation_view.c | 11 ++++++++--- applications/services/gui/canvas_i.h | 2 +- applications/services/gui/elements.c | 2 +- applications/services/gui/modules/button_menu.c | 2 +- applications/services/gui/modules/button_panel.c | 2 +- .../services/gui/modules/file_browser_worker.c | 4 ++-- applications/services/gui/modules/menu.c | 2 +- .../services/gui/modules/variable_item_list.c | 2 +- .../widget_elements/widget_element_text_scroll.c | 2 +- applications/services/gui/scene_manager_i.h | 2 +- applications/services/loader/loader.c | 10 ++++++++-- applications/services/rpc/rpc_storage.c | 2 +- applications/services/rpc/rpc_system.c | 3 ++- .../scenes/desktop_settings_scene_favorite.c | 4 ++-- applications/system/js_app/js_modules.c | 2 +- applications/system/js_app/modules/js_serial.c | 2 +- lib/drivers/lp5562.c | 3 +++ lib/music_worker/music_worker.c | 2 +- lib/print/printf_tiny.c | 2 +- lib/subghz/protocols/bin_raw.c | 4 ++-- lib/subghz/receiver.c | 2 +- lib/subghz/subghz_keystore.h | 2 +- lib/subghz/subghz_setting.c | 2 +- lib/toolbox/cli/shell/cli_shell_completions.c | 2 +- lib/toolbox/strint.c | 2 +- targets/f7/src/stm32wb55_startup.c | 2 +- 34 files changed, 54 insertions(+), 39 deletions(-) diff --git a/applications/debug/bt_debug_app/views/bt_test.c b/applications/debug/bt_debug_app/views/bt_test.c index 58b65e194..c15c6a5e4 100644 --- a/applications/debug/bt_debug_app/views/bt_test.c +++ b/applications/debug/bt_debug_app/views/bt_test.c @@ -18,7 +18,7 @@ struct BtTestParam { void* context; }; -ARRAY_DEF(BtTestParamArray, BtTestParam, M_POD_OPLIST); +ARRAY_DEF(BtTestParamArray, BtTestParam, M_POD_OPLIST); //-V658 struct BtTest { View* view; diff --git a/applications/main/infrared/infrared_brute_force.c b/applications/main/infrared/infrared_brute_force.c index 1ec4645e9..519bbbb4b 100644 --- a/applications/main/infrared/infrared_brute_force.c +++ b/applications/main/infrared/infrared_brute_force.c @@ -7,7 +7,7 @@ #include "infrared_signal.h" -ARRAY_DEF(SignalPositionArray, size_t, M_DEFAULT_OPLIST); +ARRAY_DEF(SignalPositionArray, size_t, M_DEFAULT_OPLIST); //-V658 typedef struct { size_t index; diff --git a/applications/main/nfc/helpers/mfkey32_logger.c b/applications/main/nfc/helpers/mfkey32_logger.c index bdd899a36..ce3b875cf 100644 --- a/applications/main/nfc/helpers/mfkey32_logger.c +++ b/applications/main/nfc/helpers/mfkey32_logger.c @@ -21,7 +21,7 @@ typedef struct { uint32_t ar1; } Mfkey32LoggerParams; -ARRAY_DEF(Mfkey32LoggerParams, Mfkey32LoggerParams, M_POD_OPLIST); +ARRAY_DEF(Mfkey32LoggerParams, Mfkey32LoggerParams, M_POD_OPLIST); //-V658 struct Mfkey32Logger { uint32_t cuid; diff --git a/applications/main/nfc/helpers/nfc_supported_cards.c b/applications/main/nfc/helpers/nfc_supported_cards.c index 6513eef5f..77c33b142 100644 --- a/applications/main/nfc/helpers/nfc_supported_cards.c +++ b/applications/main/nfc/helpers/nfc_supported_cards.c @@ -29,7 +29,7 @@ typedef struct { NfcSupportedCardsPluginFeature feature; } NfcSupportedCardsPluginCache; -ARRAY_DEF(NfcSupportedCardsPluginCache, NfcSupportedCardsPluginCache, M_POD_OPLIST); +ARRAY_DEF(NfcSupportedCardsPluginCache, NfcSupportedCardsPluginCache, M_POD_OPLIST); //-V658 typedef enum { NfcSupportedCardsLoadStateIdle, diff --git a/applications/main/onewire/onewire_cli.c b/applications/main/onewire/onewire_cli.c index 193de76e4..17e5aaf55 100644 --- a/applications/main/onewire/onewire_cli.c +++ b/applications/main/onewire/onewire_cli.c @@ -25,7 +25,7 @@ static void onewire_cli_search(PipeSide* pipe) { onewire_host_start(onewire); power_enable_otg(power, true); - while(!done) { + while(!done) { //-V1044 if(onewire_host_search(onewire, address, OneWireHostSearchModeNormal) != 1) { printf("Search finished\r\n"); onewire_host_reset_search(onewire); diff --git a/applications/main/subghz/helpers/subghz_frequency_analyzer_log_item_array.h b/applications/main/subghz/helpers/subghz_frequency_analyzer_log_item_array.h index 6e6b553e0..a3086c508 100644 --- a/applications/main/subghz/helpers/subghz_frequency_analyzer_log_item_array.h +++ b/applications/main/subghz/helpers/subghz_frequency_analyzer_log_item_array.h @@ -35,7 +35,7 @@ TUPLE_DEF2( //-V1048 M_DEFAULT_OPLIST) /* Define the array, register the oplist and define further algorithms on it */ -ARRAY_DEF(SubGhzFrequencyAnalyzerLogItemArray, SubGhzFrequencyAnalyzerLogItem_t) //-V779 +ARRAY_DEF(SubGhzFrequencyAnalyzerLogItemArray, SubGhzFrequencyAnalyzerLogItem_t) //-V779 //-V658 #define M_OPL_SubGhzFrequencyAnalyzerLogItemArray_t() \ ARRAY_OPLIST(SubGhzFrequencyAnalyzerLogItemArray, M_OPL_SubGhzFrequencyAnalyzerLogItem_t()) ALGO_DEF(SubGhzFrequencyAnalyzerLogItemArray, SubGhzFrequencyAnalyzerLogItemArray_t) diff --git a/applications/main/subghz/subghz_history.c b/applications/main/subghz/subghz_history.c index 85e59ae07..8043b5752 100644 --- a/applications/main/subghz/subghz_history.c +++ b/applications/main/subghz/subghz_history.c @@ -16,7 +16,7 @@ typedef struct { SubGhzRadioPreset* preset; } SubGhzHistoryItem; -ARRAY_DEF(SubGhzHistoryItemArray, SubGhzHistoryItem, M_POD_OPLIST) +ARRAY_DEF(SubGhzHistoryItemArray, SubGhzHistoryItem, M_POD_OPLIST) //-V658 #define M_OPL_SubGhzHistoryItemArray_t() ARRAY_OPLIST(SubGhzHistoryItemArray, M_POD_OPLIST) diff --git a/applications/main/subghz/views/receiver.c b/applications/main/subghz/views/receiver.c index ea74f8c6c..cec68abbf 100644 --- a/applications/main/subghz/views/receiver.c +++ b/applications/main/subghz/views/receiver.c @@ -18,7 +18,7 @@ typedef struct { uint8_t type; } SubGhzReceiverMenuItem; -ARRAY_DEF(SubGhzReceiverMenuItemArray, SubGhzReceiverMenuItem, M_POD_OPLIST) +ARRAY_DEF(SubGhzReceiverMenuItemArray, SubGhzReceiverMenuItem, M_POD_OPLIST) //-V658 #define M_OPL_SubGhzReceiverMenuItemArray_t() \ ARRAY_OPLIST(SubGhzReceiverMenuItemArray, M_POD_OPLIST) diff --git a/applications/services/desktop/animations/views/bubble_animation_view.c b/applications/services/desktop/animations/views/bubble_animation_view.c index cea039671..82740237c 100644 --- a/applications/services/desktop/animations/views/bubble_animation_view.c +++ b/applications/services/desktop/animations/views/bubble_animation_view.c @@ -93,12 +93,17 @@ static const FrameBubble* bubble_animation_pick_bubble(BubbleAnimationViewModel* model, bool active) { const FrameBubble* bubble = NULL; - if((model->active_bubbles == 0) && (model->passive_bubbles == 0)) { + // Check for division by zero based on the active parameter + if((active && model->active_bubbles == 0) || (!active && model->passive_bubbles == 0)) { return NULL; } - uint8_t index = - furi_hal_random_get() % (active ? model->active_bubbles : model->passive_bubbles); + uint8_t random_value = furi_hal_random_get(); + // In case random generator return zero lets set it to 3 + if(random_value == 0) { + random_value = 3; + } + uint8_t index = random_value % (active ? model->active_bubbles : model->passive_bubbles); const BubbleAnimation* animation = model->current; for(int i = 0; i < animation->frame_bubble_sequences_count; ++i) { diff --git a/applications/services/gui/canvas_i.h b/applications/services/gui/canvas_i.h index 449db71db..cc918f8df 100644 --- a/applications/services/gui/canvas_i.h +++ b/applications/services/gui/canvas_i.h @@ -29,7 +29,7 @@ typedef struct { void* context; } CanvasCallbackPair; -ARRAY_DEF(CanvasCallbackPairArray, CanvasCallbackPair, M_POD_OPLIST); +ARRAY_DEF(CanvasCallbackPairArray, CanvasCallbackPair, M_POD_OPLIST); //-V658 #define M_OPL_CanvasCallbackPairArray_t() ARRAY_OPLIST(CanvasCallbackPairArray, M_POD_OPLIST) diff --git a/applications/services/gui/elements.c b/applications/services/gui/elements.c index 3e12c09cc..f4caf7da7 100644 --- a/applications/services/gui/elements.c +++ b/applications/services/gui/elements.c @@ -854,7 +854,7 @@ void elements_text_box( for(size_t i = 0; i < line_num; i++) { for(size_t j = 0; j < line[i].len; j++) { // Process format symbols - if(line[i].text[j] == '\e' && j < line[i].len - 1) { + if(line[i].text[j] == '\e' && j < line[i].len - 1) { //-V781 ++j; if(line[i].text[j] == ELEMENTS_BOLD_MARKER) { if(bold) { diff --git a/applications/services/gui/modules/button_menu.c b/applications/services/gui/modules/button_menu.c index 5204c6f22..095c57b3c 100644 --- a/applications/services/gui/modules/button_menu.c +++ b/applications/services/gui/modules/button_menu.c @@ -25,7 +25,7 @@ struct ButtonMenuItem { void* callback_context; }; -ARRAY_DEF(ButtonMenuItemArray, ButtonMenuItem, M_POD_OPLIST); +ARRAY_DEF(ButtonMenuItemArray, ButtonMenuItem, M_POD_OPLIST); //-V658 struct ButtonMenu { View* view; diff --git a/applications/services/gui/modules/button_panel.c b/applications/services/gui/modules/button_panel.c index efc512bc5..c718649b1 100644 --- a/applications/services/gui/modules/button_panel.c +++ b/applications/services/gui/modules/button_panel.c @@ -98,7 +98,7 @@ void button_panel_reserve(ButtonPanel* button_panel, size_t reserve_x, size_t re furi_check(reserve_x > 0); furi_check(reserve_y > 0); - with_view_model( + with_view_model( //-V621 button_panel->view, ButtonPanelModel * model, { diff --git a/applications/services/gui/modules/file_browser_worker.c b/applications/services/gui/modules/file_browser_worker.c index 521666e6d..457345ff2 100644 --- a/applications/services/gui/modules/file_browser_worker.c +++ b/applications/services/gui/modules/file_browser_worker.c @@ -32,8 +32,8 @@ typedef enum { (WorkerEvtStop | WorkerEvtLoad | WorkerEvtFolderEnter | WorkerEvtFolderExit | \ WorkerEvtFolderRefresh | WorkerEvtConfigChange) -ARRAY_DEF(IdxLastArray, int32_t) -ARRAY_DEF(ExtFilterArray, FuriString*, FURI_STRING_OPLIST) +ARRAY_DEF(IdxLastArray, int32_t) //-V658 +ARRAY_DEF(ExtFilterArray, FuriString*, FURI_STRING_OPLIST) //-V658 struct BrowserWorker { FuriThread* thread; diff --git a/applications/services/gui/modules/menu.c b/applications/services/gui/modules/menu.c index 012fa17d6..5e9452b53 100644 --- a/applications/services/gui/modules/menu.c +++ b/applications/services/gui/modules/menu.c @@ -17,7 +17,7 @@ typedef struct { void* callback_context; } MenuItem; -ARRAY_DEF(MenuItemArray, MenuItem, M_POD_OPLIST); +ARRAY_DEF(MenuItemArray, MenuItem, M_POD_OPLIST); //-V658 #define M_OPL_MenuItemArray_t() ARRAY_OPLIST(MenuItemArray, M_POD_OPLIST) diff --git a/applications/services/gui/modules/variable_item_list.c b/applications/services/gui/modules/variable_item_list.c index f8110034e..aec0c7080 100644 --- a/applications/services/gui/modules/variable_item_list.c +++ b/applications/services/gui/modules/variable_item_list.c @@ -14,7 +14,7 @@ struct VariableItem { void* context; }; -ARRAY_DEF(VariableItemArray, VariableItem, M_POD_OPLIST); +ARRAY_DEF(VariableItemArray, VariableItem, M_POD_OPLIST); //-V658 struct VariableItemList { View* view; diff --git a/applications/services/gui/modules/widget_elements/widget_element_text_scroll.c b/applications/services/gui/modules/widget_elements/widget_element_text_scroll.c index 491ffc6bc..866aad815 100644 --- a/applications/services/gui/modules/widget_elements/widget_element_text_scroll.c +++ b/applications/services/gui/modules/widget_elements/widget_element_text_scroll.c @@ -10,7 +10,7 @@ typedef struct { FuriString* text; } TextScrollLineArray; -ARRAY_DEF(TextScrollLineArray, TextScrollLineArray, M_POD_OPLIST) +ARRAY_DEF(TextScrollLineArray, TextScrollLineArray, M_POD_OPLIST) //-V658 typedef struct { TextScrollLineArray_t line_array; diff --git a/applications/services/gui/scene_manager_i.h b/applications/services/gui/scene_manager_i.h index 85f7e7bee..8bfe46993 100644 --- a/applications/services/gui/scene_manager_i.h +++ b/applications/services/gui/scene_manager_i.h @@ -8,7 +8,7 @@ #include "scene_manager.h" #include -ARRAY_DEF(SceneManagerIdStack, uint32_t, M_DEFAULT_OPLIST); +ARRAY_DEF(SceneManagerIdStack, uint32_t, M_DEFAULT_OPLIST); //-V658 typedef struct { uint32_t state; diff --git a/applications/services/loader/loader.c b/applications/services/loader/loader.c index 7b37f2510..341994a23 100644 --- a/applications/services/loader/loader.c +++ b/applications/services/loader/loader.c @@ -88,6 +88,7 @@ static void loader_show_gui_error( LoaderMessageLoaderStatusResult status, const char* name, FuriString* error_message) { + furi_check(name); DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); DialogMessage* message = dialog_message_alloc(); @@ -609,6 +610,8 @@ static LoaderMessageLoaderStatusResult loader_do_start_by_name( status.value = loader_make_success_status(error_message); status.error = LoaderStatusErrorUnknown; + if(name == NULL) return status; + LoaderEvent event; event.type = LoaderEventTypeApplicationBeforeLoad; furi_pubsub_publish(loader->pubsub, &event); @@ -835,7 +838,10 @@ int32_t loader_srv(void* p) { switch(message.type) { case LoaderMessageTypeStartByName: { LoaderMessageLoaderStatusResult status = loader_do_start_by_name( - loader, message.start.name, message.start.args, message.start.error_message); + loader, + message.start.name, + message.start.args, + message.start.error_message); //-V595 *(message.status_value) = status; if(status.value != LoaderStatusOk) loader_do_emit_queue_empty_event(loader); api_lock_unlock(message.api_lock); @@ -844,7 +850,7 @@ int32_t loader_srv(void* p) { case LoaderMessageTypeStartByNameDetachedWithGuiError: { FuriString* error_message = furi_string_alloc(); LoaderMessageLoaderStatusResult status = loader_do_start_by_name( - loader, message.start.name, message.start.args, error_message); + loader, message.start.name, message.start.args, error_message); //-V595 loader_show_gui_error(status, message.start.name, error_message); if(status.value != LoaderStatusOk) loader_do_emit_queue_empty_event(loader); if(message.start.name) free((void*)message.start.name); diff --git a/applications/services/rpc/rpc_storage.c b/applications/services/rpc/rpc_storage.c index 163535f9a..df65f84a1 100644 --- a/applications/services/rpc/rpc_storage.c +++ b/applications/services/rpc/rpc_storage.c @@ -293,7 +293,7 @@ static void rpc_system_storage_list_process(const PB_Main* request, void* contex finish = true; } - while(!finish) { + while(!finish) { //-V1044 FileInfo fileinfo; char* name = malloc(MAX_NAME_LENGTH + 1); if(storage_dir_read(dir, &fileinfo, name, MAX_NAME_LENGTH)) { diff --git a/applications/services/rpc/rpc_system.c b/applications/services/rpc/rpc_system.c index 1cc0f90eb..4bf923d7f 100644 --- a/applications/services/rpc/rpc_system.c +++ b/applications/services/rpc/rpc_system.c @@ -30,7 +30,8 @@ static void rpc_system_system_ping_process(const PB_Main* request, void* context } PB_Main response = PB_Main_init_default; - response.command_status = PB_CommandStatus_OK; + // PB_CommandStatus_OK is 0 in current case, and var by default is 0 too, commenting PVS warn + response.command_status = PB_CommandStatus_OK; //-V1048 response.command_id = request->command_id; response.which_content = PB_Main_system_ping_response_tag; diff --git a/applications/settings/desktop_settings/scenes/desktop_settings_scene_favorite.c b/applications/settings/desktop_settings/scenes/desktop_settings_scene_favorite.c index 17ef88358..dee7ad7bd 100644 --- a/applications/settings/desktop_settings/scenes/desktop_settings_scene_favorite.c +++ b/applications/settings/desktop_settings/scenes/desktop_settings_scene_favorite.c @@ -76,7 +76,7 @@ void desktop_settings_scene_favorite_on_enter(void* context) { default_passport = true; } } else { - favorite_id &= ~(SCENE_STATE_SET_DUMMY_APP); + favorite_id &= ~(SCENE_STATE_SET_DUMMY_APP); //-V784 furi_assert(favorite_id < DummyAppNumber); curr_favorite_app = &app->settings.dummy_apps[favorite_id]; default_passport = true; @@ -182,7 +182,7 @@ bool desktop_settings_scene_favorite_on_event(void* context, SceneManagerEvent e furi_assert(favorite_id < FavoriteAppNumber); curr_favorite_app = &app->settings.favorite_apps[favorite_id]; } else { - favorite_id &= ~(SCENE_STATE_SET_DUMMY_APP); + favorite_id &= ~(SCENE_STATE_SET_DUMMY_APP); //-V784 furi_assert(favorite_id < DummyAppNumber); curr_favorite_app = &app->settings.dummy_apps[favorite_id]; } diff --git a/applications/system/js_app/js_modules.c b/applications/system/js_app/js_modules.c index 095880f17..d7f38ba04 100644 --- a/applications/system/js_app/js_modules.c +++ b/applications/system/js_app/js_modules.c @@ -27,7 +27,7 @@ typedef struct { // - 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); +ARRAY_DEF(JsModuleArray, JsModuleData, M_POD_OPLIST); //-V658 #define M_OPL_JsModuleArray_t() ARRAY_OPLIST(JsModuleArray) static const JsModuleDescriptor modules_builtin[] = { diff --git a/applications/system/js_app/modules/js_serial.c b/applications/system/js_app/modules/js_serial.c index d903939ce..78f4db63a 100644 --- a/applications/system/js_app/modules/js_serial.c +++ b/applications/system/js_app/modules/js_serial.c @@ -20,7 +20,7 @@ typedef struct { char* data; } PatternArrayItem; -ARRAY_DEF(PatternArray, PatternArrayItem, M_POD_OPLIST); +ARRAY_DEF(PatternArray, PatternArrayItem, M_POD_OPLIST); //-V658 static void js_serial_on_async_rx(FuriHalSerialHandle* handle, FuriHalSerialRxEvent event, void* context) { diff --git a/lib/drivers/lp5562.c b/lib/drivers/lp5562.c index 7db9bbce4..18f998997 100644 --- a/lib/drivers/lp5562.c +++ b/lib/drivers/lp5562.c @@ -192,6 +192,9 @@ void lp5562_execute_ramp( // Prepare command sequence uint16_t program[16]; uint8_t diff = (val_end > val_start) ? (val_end - val_start) : (val_start - val_end); + if(diff == 0) { // Making division below safer + diff = 1; + } uint16_t time_step = time * 2 / diff; uint8_t prescaller = 0; if(time_step > 0x3F) { diff --git a/lib/music_worker/music_worker.c b/lib/music_worker/music_worker.c index ce91f6029..420f17f29 100644 --- a/lib/music_worker/music_worker.c +++ b/lib/music_worker/music_worker.c @@ -26,7 +26,7 @@ typedef struct { uint8_t dots; } NoteBlock; -ARRAY_DEF(NoteBlockArray, NoteBlock, M_POD_OPLIST); +ARRAY_DEF(NoteBlockArray, NoteBlock, M_POD_OPLIST); //-V658 struct MusicWorker { FuriThread* thread; diff --git a/lib/print/printf_tiny.c b/lib/print/printf_tiny.c index 387a6451e..96d979450 100644 --- a/lib/print/printf_tiny.c +++ b/lib/print/printf_tiny.c @@ -650,7 +650,7 @@ static int // evaluate flags flags = 0U; - do { + do { //-V1044 switch(*format) { case '0': flags |= FLAGS_ZEROPAD; diff --git a/lib/subghz/protocols/bin_raw.c b/lib/subghz/protocols/bin_raw.c index e90f1508e..a1e0e816b 100644 --- a/lib/subghz/protocols/bin_raw.c +++ b/lib/subghz/protocols/bin_raw.c @@ -455,7 +455,7 @@ static bool //sort by number of occurrences bool swap = true; - while(swap) { + while(swap) { //-V1044 swap = false; for(size_t i = 1; i < BIN_RAW_SEARCH_CLASSES; i++) { if(classes[i].count > classes[i - 1].count) { @@ -571,7 +571,7 @@ static bool bit_count = 0; if(data_markup_ind == BIN_RAW_MAX_MARKUP_COUNT) break; - ind &= 0xFFFFFFF8; //jump to the pre whole byte + ind &= 0xFFFFFFF8; //jump to the pre whole byte //-V784 } } while(gap_ind != 0); if((data_markup_ind != BIN_RAW_MAX_MARKUP_COUNT) && (ind != 0)) { diff --git a/lib/subghz/receiver.c b/lib/subghz/receiver.c index 88fac4e34..e7dff3b9a 100644 --- a/lib/subghz/receiver.c +++ b/lib/subghz/receiver.c @@ -8,7 +8,7 @@ typedef struct { SubGhzProtocolEncoderBase* base; } SubGhzReceiverSlot; -ARRAY_DEF(SubGhzReceiverSlotArray, SubGhzReceiverSlot, M_POD_OPLIST); +ARRAY_DEF(SubGhzReceiverSlotArray, SubGhzReceiverSlot, M_POD_OPLIST); //-V658 #define M_OPL_SubGhzReceiverSlotArray_t() ARRAY_OPLIST(SubGhzReceiverSlotArray, M_POD_OPLIST) struct SubGhzReceiver { diff --git a/lib/subghz/subghz_keystore.h b/lib/subghz/subghz_keystore.h index 6138651eb..b1bca9625 100644 --- a/lib/subghz/subghz_keystore.h +++ b/lib/subghz/subghz_keystore.h @@ -14,7 +14,7 @@ typedef struct { uint16_t type; } SubGhzKey; -ARRAY_DEF(SubGhzKeyArray, SubGhzKey, M_POD_OPLIST) +ARRAY_DEF(SubGhzKeyArray, SubGhzKey, M_POD_OPLIST) //-V658 #define M_OPL_SubGhzKeyArray_t() ARRAY_OPLIST(SubGhzKeyArray, M_POD_OPLIST) diff --git a/lib/subghz/subghz_setting.c b/lib/subghz/subghz_setting.c index 12b7bd16a..ff1753d25 100644 --- a/lib/subghz/subghz_setting.c +++ b/lib/subghz/subghz_setting.c @@ -163,7 +163,7 @@ typedef struct { size_t custom_preset_data_size; } SubGhzSettingCustomPresetItem; -ARRAY_DEF(SubGhzSettingCustomPresetItemArray, SubGhzSettingCustomPresetItem, M_POD_OPLIST) +ARRAY_DEF(SubGhzSettingCustomPresetItemArray, SubGhzSettingCustomPresetItem, M_POD_OPLIST) //-V658 #define M_OPL_SubGhzSettingCustomPresetItemArray_t() \ ARRAY_OPLIST(SubGhzSettingCustomPresetItemArray, M_POD_OPLIST) diff --git a/lib/toolbox/cli/shell/cli_shell_completions.c b/lib/toolbox/cli/shell/cli_shell_completions.c index 6b6634dbb..64133fe93 100644 --- a/lib/toolbox/cli/shell/cli_shell_completions.c +++ b/lib/toolbox/cli/shell/cli_shell_completions.c @@ -1,6 +1,6 @@ #include "cli_shell_completions.h" -ARRAY_DEF(CommandCompletions, FuriString*, FURI_STRING_OPLIST); // -V524 +ARRAY_DEF(CommandCompletions, FuriString*, FURI_STRING_OPLIST); // -V524 //-V658 #define M_OPL_CommandCompletions_t() ARRAY_OPLIST(CommandCompletions) struct CliShellCompletions { diff --git a/lib/toolbox/strint.c b/lib/toolbox/strint.c index 8c7f36976..113becd05 100644 --- a/lib/toolbox/strint.c +++ b/lib/toolbox/strint.c @@ -80,7 +80,7 @@ StrintParseError strint_to_uint64_internal( if(result > mul_limit) return StrintParseOverflowError; result *= base; - if(result > limit - digit_value) return StrintParseOverflowError; + if(result > limit - digit_value) return StrintParseOverflowError; //-V658 result += digit_value; read_total++; diff --git a/targets/f7/src/stm32wb55_startup.c b/targets/f7/src/stm32wb55_startup.c index 1c1cfdcc3..6aecab06b 100644 --- a/targets/f7/src/stm32wb55_startup.c +++ b/targets/f7/src/stm32wb55_startup.c @@ -68,7 +68,7 @@ void SystemInit(void) { RCC->PLLSAI1CFGR = 0x22041000U; #endif // Reset HSEBYP bit - RCC->CR &= 0xFFFBFFFFU; + RCC->CR &= 0xFFFBFFFFU; //-V784 // Disable all RCC related interrupts RCC->CIER = 0x00000000; } From 0d5beedb012f1a676b48106c7a21996d83906081 Mon Sep 17 00:00:00 2001 From: hedger Date: Wed, 24 Sep 2025 19:36:45 +0100 Subject: [PATCH 11/12] BLE: improved pairing security (#4240) * ble: use unique root security keys for new pairings after pairing reset; added migrations for existing pairing data; unit_tests: added migration tests * bt: lower logging level * hal: bt: updated doxygen strings * hal: ble: Added checks for root_keys ptr * service: ble: bt_keys_storage minor cleanup --- .../debug/unit_tests/tests/bt/bt_test.c | 159 +++++++++++++ applications/services/bt/bt_service/bt.c | 10 +- .../services/bt/bt_service/bt_keys_storage.c | 219 ++++++++++++++---- .../services/bt/bt_service/bt_keys_storage.h | 4 + applications/services/bt/bt_settings.c | 8 +- targets/f18/api_symbols.csv | 9 +- targets/f7/api_symbols.csv | 9 +- targets/f7/ble_glue/gap.c | 26 +-- targets/f7/ble_glue/gap.h | 14 +- targets/f7/furi_hal/furi_hal_bt.c | 15 +- targets/furi_hal_include/furi_hal_bt.h | 4 + 11 files changed, 391 insertions(+), 86 deletions(-) diff --git a/applications/debug/unit_tests/tests/bt/bt_test.c b/applications/debug/unit_tests/tests/bt/bt_test.c index a0f189fdb..be7e8f1ff 100644 --- a/applications/debug/unit_tests/tests/bt/bt_test.c +++ b/applications/debug/unit_tests/tests/bt/bt_test.c @@ -4,10 +4,23 @@ #include #include +#include #define BT_TEST_KEY_STORAGE_FILE_PATH EXT_PATH("unit_tests/bt_test.keys") +#define BT_TEST_MIGRATION_FILE_PATH EXT_PATH("unit_tests/bt_migration_test.keys") #define BT_TEST_NVM_RAM_BUFF_SIZE (507 * 4) // The same as in ble NVM storage +// Identity root key +static const uint8_t gap_legacy_irk[16] = + {0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}; +// Encryption root key +static const uint8_t gap_legacy_erk[16] = + {0xfe, 0xdc, 0xba, 0x09, 0x87, 0x65, 0x43, 0x21, 0xfe, 0xdc, 0xba, 0x09, 0x87, 0x65, 0x43, 0x21}; + +// Test constants for migration (matching bt_keys_storage.c) +#define BT_KEYS_STORAGE_MAGIC_TEST (0x18) +#define BT_KEYS_STORAGE_VERSION_1_TEST (1) + typedef struct { Storage* storage; BtKeysStorage* bt_keys_storage; @@ -88,6 +101,134 @@ static void bt_test_keys_remove_test_file(void) { "Can't remove test file"); } +// Helper function to create a version 0 file manually +static bool + bt_test_create_v0_file(const char* file_path, const uint8_t* nvm_data, size_t nvm_size) { + // Version 0 files use saved_struct format with magic 0x18, version 0, containing only BLE pairing data + return saved_struct_save( + file_path, + nvm_data, + nvm_size, + BT_KEYS_STORAGE_MAGIC_TEST, + 0); // Version 0 +} + +// Helper function to verify file format version +static bool bt_test_verify_file_version(const char* file_path, uint32_t expected_version) { + uint8_t magic, version; + size_t size; + + if(!saved_struct_get_metadata(file_path, &magic, &version, &size)) { + return false; + } + + return (magic == BT_KEYS_STORAGE_MAGIC_TEST && version == expected_version); +} + +// Test migration from version 0 to version 1, including root key preservation +static void bt_test_migration_v0_to_v1(void) { + // Create test NVM data + const size_t test_nvm_size = 100; + uint8_t test_nvm_data[test_nvm_size]; + for(size_t i = 0; i < test_nvm_size; i++) { + test_nvm_data[i] = (uint8_t)(i & 0xFF); + } + + // Create a version 0 file + mu_assert( + bt_test_create_v0_file(BT_TEST_MIGRATION_FILE_PATH, test_nvm_data, test_nvm_size), + "Failed to create version 0 test file"); + + // Create BT keys storage and load the v0 file (should trigger migration) + BtKeysStorage* migration_storage = bt_keys_storage_alloc(BT_TEST_MIGRATION_FILE_PATH); + uint8_t loaded_buffer[BT_TEST_NVM_RAM_BUFF_SIZE]; + memset(loaded_buffer, 0, sizeof(loaded_buffer)); + bt_keys_storage_set_ram_params(migration_storage, loaded_buffer, sizeof(loaded_buffer)); + + // Load should succeed and migrate v0 to v1 + mu_assert(bt_keys_storage_load(migration_storage), "Failed to load and migrate v0 file"); + + // Verify the file is now version 1 + mu_assert( + bt_test_verify_file_version(BT_TEST_MIGRATION_FILE_PATH, BT_KEYS_STORAGE_VERSION_1_TEST), + "File was not migrated to version 1"); + + // Verify the NVM data was preserved during migration + mu_assert( + memcmp(test_nvm_data, loaded_buffer, test_nvm_size) == 0, + "NVM data was corrupted during migration"); + + // Verify that legacy root keys are used after migration + const GapRootSecurityKeys* migrated_keys = bt_keys_storage_get_root_keys(migration_storage); + mu_assert( + memcmp(migrated_keys->irk, gap_legacy_irk, sizeof(gap_legacy_irk)) == 0, + "IRK not set to legacy after migration"); + mu_assert( + memcmp(migrated_keys->erk, gap_legacy_erk, sizeof(gap_legacy_erk)) == 0, + "ERK not set to legacy after migration"); + + bt_keys_storage_free(migration_storage); + storage_simply_remove(bt_test->storage, BT_TEST_MIGRATION_FILE_PATH); +} + +// Test that migration preserves existing pairing data and root keys are not changed on reload +static void bt_test_migration_preserves_pairings_and_keys(void) { + const size_t pairing_data_size = 200; + uint8_t pairing_data[pairing_data_size]; + for(size_t i = 0; i < pairing_data_size; i++) { + pairing_data[i] = (uint8_t)((i * 7 + 42) & 0xFF); + } + mu_assert( + bt_test_create_v0_file(BT_TEST_MIGRATION_FILE_PATH, pairing_data, pairing_data_size), + "Failed to create v0 file with pairing data"); + + GapRootSecurityKeys keys_after_first_load; + for(int iteration = 0; iteration < 2; iteration++) { + BtKeysStorage* storage = bt_keys_storage_alloc(BT_TEST_MIGRATION_FILE_PATH); + uint8_t buffer[BT_TEST_NVM_RAM_BUFF_SIZE]; + memset(buffer, 0, sizeof(buffer)); + bt_keys_storage_set_ram_params(storage, buffer, sizeof(buffer)); + mu_assert(bt_keys_storage_load(storage), "Failed to load on iteration"); + mu_assert( + memcmp(pairing_data, buffer, pairing_data_size) == 0, + "Pairing data corrupted on iteration"); + const GapRootSecurityKeys* keys = bt_keys_storage_get_root_keys(storage); + if(iteration == 0) + memcpy(&keys_after_first_load, keys, sizeof(GapRootSecurityKeys)); + else + mu_assert( + memcmp(&keys_after_first_load, keys, sizeof(GapRootSecurityKeys)) == 0, + "Root keys changed after reload"); + bt_keys_storage_free(storage); + } + storage_simply_remove(bt_test->storage, BT_TEST_MIGRATION_FILE_PATH); +} + +// Test that delete operation generates new secure keys in v1 and does not match legacy +static void bt_test_delete_generates_new_keys_and_not_legacy(void) { + BtKeysStorage* storage = bt_keys_storage_alloc(BT_TEST_MIGRATION_FILE_PATH); + uint8_t buffer[BT_TEST_NVM_RAM_BUFF_SIZE]; + memset(buffer, 0x55, sizeof(buffer)); + bt_keys_storage_set_ram_params(storage, buffer, sizeof(buffer)); + mu_assert(bt_keys_storage_update(storage, buffer, 100), "Failed to create initial v1 file"); + const GapRootSecurityKeys* original_keys = bt_keys_storage_get_root_keys(storage); + uint8_t original_keys_copy[sizeof(GapRootSecurityKeys)]; + memcpy(original_keys_copy, original_keys, sizeof(original_keys_copy)); + bt_keys_storage_delete(storage); + const GapRootSecurityKeys* new_keys = bt_keys_storage_get_root_keys(storage); + mu_assert( + memcmp(original_keys_copy, new_keys, sizeof(original_keys_copy)) != 0, + "Root keys were not regenerated after delete"); + mu_assert( + memcmp(new_keys->irk, gap_legacy_irk, sizeof(gap_legacy_irk)) != 0, + "IRK after delete should not match legacy"); + mu_assert( + memcmp(new_keys->erk, gap_legacy_erk, sizeof(gap_legacy_erk)) != 0, + "ERK after delete should not match legacy"); + bt_keys_storage_free(storage); + storage_simply_remove(bt_test->storage, BT_TEST_MIGRATION_FILE_PATH); +} + MU_TEST(bt_test_keys_storage_serial_profile) { furi_check(bt_test); @@ -96,10 +237,28 @@ MU_TEST(bt_test_keys_storage_serial_profile) { bt_test_keys_remove_test_file(); } +MU_TEST(bt_test_migration_v0_to_v1_test) { + furi_check(bt_test); + bt_test_migration_v0_to_v1(); +} + +MU_TEST(bt_test_migration_preserves_pairings_and_keys_test) { + furi_check(bt_test); + bt_test_migration_preserves_pairings_and_keys(); +} + +MU_TEST(bt_test_delete_generates_new_keys_and_not_legacy_test) { + furi_check(bt_test); + bt_test_delete_generates_new_keys_and_not_legacy(); +} + MU_TEST_SUITE(test_bt) { bt_test_alloc(); MU_RUN_TEST(bt_test_keys_storage_serial_profile); + MU_RUN_TEST(bt_test_migration_v0_to_v1_test); + MU_RUN_TEST(bt_test_migration_preserves_pairings_and_keys_test); + MU_RUN_TEST(bt_test_delete_generates_new_keys_and_not_legacy_test); bt_test_free(); } diff --git a/applications/services/bt/bt_service/bt.c b/applications/services/bt/bt_service/bt.c index 2eeeab286..7554d87ef 100644 --- a/applications/services/bt/bt_service/bt.c +++ b/applications/services/bt/bt_service/bt.c @@ -403,6 +403,7 @@ static void bt_change_profile(Bt* bt, BtMessage* message) { bt->current_profile = furi_hal_bt_change_app( message->data.profile.template, message->data.profile.params, + bt_keys_storage_get_root_keys(bt->keys_storage), bt_on_gap_event_callback, bt); if(bt->current_profile) { @@ -458,7 +459,6 @@ static void bt_load_keys(Bt* bt) { bt_keys_storage_load(bt->keys_storage); bt->current_profile = NULL; - } else { FURI_LOG_I(TAG, "Keys unchanged"); } @@ -466,8 +466,12 @@ static void bt_load_keys(Bt* bt) { static void bt_start_application(Bt* bt) { if(!bt->current_profile) { - bt->current_profile = - furi_hal_bt_change_app(ble_profile_serial, NULL, bt_on_gap_event_callback, bt); + bt->current_profile = furi_hal_bt_change_app( + ble_profile_serial, + NULL, + bt_keys_storage_get_root_keys(bt->keys_storage), + bt_on_gap_event_callback, + bt); if(!bt->current_profile) { FURI_LOG_E(TAG, "BLE App start failed"); diff --git a/applications/services/bt/bt_service/bt_keys_storage.c b/applications/services/bt/bt_service/bt_keys_storage.c index 57742e8e2..0735a52b7 100644 --- a/applications/services/bt/bt_service/bt_keys_storage.c +++ b/applications/services/bt/bt_service/bt_keys_storage.c @@ -2,21 +2,92 @@ #include #include -#include -#include +#include -#define BT_KEYS_STORAGE_VERSION (0) -#define BT_KEYS_STORAGE_MAGIC (0x18) +#include + +#include +#include + +#define BT_KEYS_STORAGE_MAGIC (0x18) +#define BT_KEYS_STORAGE_VERSION (1) +#define BT_KEYS_STORAGE_LEGACY_VERSION (0) // Legacy version with no root keys #define TAG "BtKeyStorage" +// Identity root key +static const uint8_t gap_legacy_irk[16] = + {0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}; +// Encryption root key +static const uint8_t gap_legacy_erk[16] = + {0xfe, 0xdc, 0xba, 0x09, 0x87, 0x65, 0x43, 0x21, 0xfe, 0xdc, 0xba, 0x09, 0x87, 0x65, 0x43, 0x21}; + struct BtKeysStorage { uint8_t* nvm_sram_buff; uint16_t nvm_sram_buff_size; uint16_t current_size; FuriString* file_path; + GapRootSecurityKeys root_keys; }; +typedef struct { + GapRootSecurityKeys root_keys; + uint8_t pairing_data[]; +} BtKeysStorageFile; + +static bool bt_keys_storage_save(BtKeysStorage* instance) { + furi_assert(instance); + + size_t total_size = sizeof(BtKeysStorageFile) + instance->current_size; + BtKeysStorageFile* save_data = malloc(total_size); + + memcpy(&save_data->root_keys, &instance->root_keys, sizeof(GapRootSecurityKeys)); + + furi_hal_bt_nvm_sram_sem_acquire(); + memcpy(save_data->pairing_data, instance->nvm_sram_buff, instance->current_size); + furi_hal_bt_nvm_sram_sem_release(); + + bool saved = saved_struct_save( + furi_string_get_cstr(instance->file_path), + save_data, + sizeof(GapRootSecurityKeys) + instance->current_size, + BT_KEYS_STORAGE_MAGIC, + BT_KEYS_STORAGE_VERSION); + + free(save_data); + return saved; +} + +static bool bt_keys_storage_load_keys_and_pairings( + BtKeysStorage* instance, + const uint8_t* file_data, + size_t data_size) { + if(data_size < sizeof(GapRootSecurityKeys)) { + return false; + } + + const BtKeysStorageFile* loaded = (const BtKeysStorageFile*)file_data; + memcpy(&instance->root_keys, &loaded->root_keys, sizeof(GapRootSecurityKeys)); + + size_t ble_data_size = data_size - sizeof(GapRootSecurityKeys); + if(ble_data_size > instance->nvm_sram_buff_size) { + FURI_LOG_E(TAG, "BLE data too large for SRAM buffer"); + return false; + } + + furi_hal_bt_nvm_sram_sem_acquire(); + memcpy(instance->nvm_sram_buff, loaded->pairing_data, ble_data_size); + instance->current_size = ble_data_size; + furi_hal_bt_nvm_sram_sem_release(); + + return true; +} + +static void bt_keys_storage_regenerate_root_keys(BtKeysStorage* instance) { + furi_hal_random_fill_buf(instance->root_keys.erk, sizeof(instance->root_keys.erk)); + furi_hal_random_fill_buf(instance->root_keys.irk, sizeof(instance->root_keys.irk)); +} + bool bt_keys_storage_delete(BtKeysStorage* instance) { furi_assert(instance); @@ -25,6 +96,15 @@ bool bt_keys_storage_delete(BtKeysStorage* instance) { furi_hal_bt_stop_advertising(); delete_succeed = furi_hal_bt_clear_white_list(); + + FURI_LOG_I(TAG, "Root keys regen"); + bt_keys_storage_regenerate_root_keys(instance); + + instance->current_size = 0; + if(!bt_keys_storage_save(instance)) { + FURI_LOG_E(TAG, "Save after delete failed"); + } + if(bt_is_active) { furi_hal_bt_start_advertising(); } @@ -42,6 +122,7 @@ BtKeysStorage* bt_keys_storage_alloc(const char* keys_storage_path) { instance->file_path = furi_string_alloc(); furi_string_set_str(instance->file_path, keys_storage_path); + bt_keys_storage_regenerate_root_keys(instance); return instance; } @@ -76,20 +157,31 @@ static bool bt_keys_storage_file_exists(const char* file_path) { return ret; } -static bool bt_keys_storage_validate_file(const char* file_path, size_t* payload_size) { +static bool bt_keys_storage_validate_file( + const char* file_path, + size_t* payload_size, + uint8_t* file_version) { uint8_t magic, version; size_t size; if(!saved_struct_get_metadata(file_path, &magic, &version, &size)) { - FURI_LOG_E(TAG, "Failed to get metadata"); + FURI_LOG_W(TAG, "Failed to get metadata"); return false; - } else if(magic != BT_KEYS_STORAGE_MAGIC || version != BT_KEYS_STORAGE_VERSION) { - FURI_LOG_E(TAG, "File version mismatch"); + } else if(magic != BT_KEYS_STORAGE_MAGIC) { + FURI_LOG_W(TAG, "File magic mismatch"); + return false; + } else if(version > BT_KEYS_STORAGE_VERSION) { + FURI_LOG_E( + TAG, + "File version %d is newer than supported version %d", + version, + BT_KEYS_STORAGE_VERSION); return false; } *payload_size = size; + *file_version = version; return true; } @@ -102,32 +194,45 @@ bool bt_keys_storage_is_changed(BtKeysStorage* instance) { do { const char* file_path = furi_string_get_cstr(instance->file_path); size_t payload_size; + uint8_t file_version; if(!bt_keys_storage_file_exists(file_path)) { FURI_LOG_W(TAG, "Missing or empty file"); + is_changed = true; break; + } - } else if(!bt_keys_storage_validate_file(file_path, &payload_size)) { - FURI_LOG_E(TAG, "Invalid or corrupted file"); + if(!bt_keys_storage_validate_file(file_path, &payload_size, &file_version)) { + FURI_LOG_W(TAG, "Invalid or corrupted file"); + is_changed = true; + break; + } + + // Early check for legacy version: always considered changed, no need to load + if(file_version == BT_KEYS_STORAGE_LEGACY_VERSION) { + is_changed = true; break; } data_buffer = malloc(payload_size); - const bool data_loaded = saved_struct_load( - file_path, data_buffer, payload_size, BT_KEYS_STORAGE_MAGIC, BT_KEYS_STORAGE_VERSION); + file_path, data_buffer, payload_size, BT_KEYS_STORAGE_MAGIC, file_version); if(!data_loaded) { FURI_LOG_E(TAG, "Failed to load file"); break; + } - } else if(payload_size == instance->current_size) { + // At this point, it's version 1 file we have + const BtKeysStorageFile* loaded = (const BtKeysStorageFile*)data_buffer; + size_t expected_file_size = sizeof(GapRootSecurityKeys) + instance->current_size; + if(payload_size == expected_file_size) { furi_hal_bt_nvm_sram_sem_acquire(); - is_changed = memcmp(data_buffer, instance->nvm_sram_buff, payload_size); + is_changed = + memcmp(loaded->pairing_data, instance->nvm_sram_buff, instance->current_size); furi_hal_bt_nvm_sram_sem_release(); - } else { - FURI_LOG_D(TAG, "Size mismatch"); + FURI_LOG_D(TAG, "NVRAM sz mismatch (v1)"); is_changed = true; } } while(false); @@ -139,45 +244,59 @@ bool bt_keys_storage_is_changed(BtKeysStorage* instance) { return is_changed; } +static bool bt_keys_storage_load_legacy_pairings( + BtKeysStorage* instance, + const uint8_t* file_data, + size_t payload_size) { + FURI_LOG_I(TAG, "Loaded v0, upgrading to v1"); + memcpy(instance->root_keys.irk, gap_legacy_irk, sizeof(instance->root_keys.irk)); + memcpy(instance->root_keys.erk, gap_legacy_erk, sizeof(instance->root_keys.erk)); + if(payload_size > instance->nvm_sram_buff_size) { + FURI_LOG_E(TAG, "Pairing too large"); + return false; + } + furi_hal_bt_nvm_sram_sem_acquire(); + memcpy(instance->nvm_sram_buff, file_data, payload_size); + instance->current_size = payload_size; + furi_hal_bt_nvm_sram_sem_release(); + if(!bt_keys_storage_save(instance)) { + FURI_LOG_W(TAG, "Upgrade to v1 failed"); + } else { + FURI_LOG_I(TAG, "Upgraded to v1"); + } + return true; +} + bool bt_keys_storage_load(BtKeysStorage* instance) { furi_assert(instance); + const char* file_path = furi_string_get_cstr(instance->file_path); + size_t payload_size; + uint8_t file_version; + if(!bt_keys_storage_validate_file(file_path, &payload_size, &file_version)) { + FURI_LOG_E(TAG, "Invalid or corrupted file"); + return false; + } + bool loaded = false; + uint8_t* file_data = malloc(payload_size); do { - const char* file_path = furi_string_get_cstr(instance->file_path); - - // Get payload size - size_t payload_size; - if(!bt_keys_storage_validate_file(file_path, &payload_size)) { - FURI_LOG_E(TAG, "Invalid or corrupted file"); - break; - - } else if(payload_size > instance->nvm_sram_buff_size) { - FURI_LOG_E(TAG, "NVM RAM buffer overflow"); + if(!saved_struct_load( + file_path, file_data, payload_size, BT_KEYS_STORAGE_MAGIC, file_version)) { + FURI_LOG_E(TAG, "Failed to load"); break; } - // Load saved data to ram - furi_hal_bt_nvm_sram_sem_acquire(); - const bool data_loaded = saved_struct_load( - file_path, - instance->nvm_sram_buff, - payload_size, - BT_KEYS_STORAGE_MAGIC, - BT_KEYS_STORAGE_VERSION); - furi_hal_bt_nvm_sram_sem_release(); - - if(!data_loaded) { - FURI_LOG_E(TAG, "Failed to load file"); + if(file_version == BT_KEYS_STORAGE_LEGACY_VERSION) { + loaded = bt_keys_storage_load_legacy_pairings(instance, file_data, payload_size); break; } - - instance->current_size = payload_size; - - loaded = true; + // Only v1 left + loaded = bt_keys_storage_load_keys_and_pairings(instance, file_data, payload_size); } while(false); + free(file_data); return loaded; } @@ -203,14 +322,8 @@ bool bt_keys_storage_update(BtKeysStorage* instance, uint8_t* start_addr, uint32 instance->current_size = new_size; - furi_hal_bt_nvm_sram_sem_acquire(); - bool data_updated = saved_struct_save( - furi_string_get_cstr(instance->file_path), - instance->nvm_sram_buff, - new_size, - BT_KEYS_STORAGE_MAGIC, - BT_KEYS_STORAGE_VERSION); - furi_hal_bt_nvm_sram_sem_release(); + // Save using version 1 format with embedded root keys + bool data_updated = bt_keys_storage_save(instance); if(!data_updated) { FURI_LOG_E(TAG, "Failed to update key storage"); @@ -222,3 +335,9 @@ bool bt_keys_storage_update(BtKeysStorage* instance, uint8_t* start_addr, uint32 return updated; } + +const GapRootSecurityKeys* bt_keys_storage_get_root_keys(BtKeysStorage* instance) { + furi_assert(instance); + + return &instance->root_keys; +} diff --git a/applications/services/bt/bt_service/bt_keys_storage.h b/applications/services/bt/bt_service/bt_keys_storage.h index b7a127035..2033b055b 100644 --- a/applications/services/bt/bt_service/bt_keys_storage.h +++ b/applications/services/bt/bt_service/bt_keys_storage.h @@ -3,6 +3,8 @@ #include #include +#include + #ifdef __cplusplus extern "C" { #endif @@ -19,6 +21,8 @@ void bt_keys_storage_set_ram_params(BtKeysStorage* instance, uint8_t* buff, uint bool bt_keys_storage_is_changed(BtKeysStorage* instance); +const GapRootSecurityKeys* bt_keys_storage_get_root_keys(BtKeysStorage* instance); + bool bt_keys_storage_load(BtKeysStorage* instance); bool bt_keys_storage_update(BtKeysStorage* instance, uint8_t* start_addr, uint32_t size); diff --git a/applications/services/bt/bt_settings.c b/applications/services/bt/bt_settings.c index abdc97f7e..da607e540 100644 --- a/applications/services/bt/bt_settings.c +++ b/applications/services/bt/bt_settings.c @@ -2,6 +2,7 @@ #include "bt_settings_filename.h" #include + #include #include @@ -14,12 +15,13 @@ void bt_settings_load(BtSettings* bt_settings) { furi_assert(bt_settings); - const bool success = saved_struct_load( + const bool load_success = saved_struct_load( BT_SETTINGS_PATH, bt_settings, sizeof(BtSettings), BT_SETTINGS_MAGIC, BT_SETTINGS_VERSION); - if(!success) { + if(!load_success) { FURI_LOG_W(TAG, "Failed to load settings, using defaults"); - memset(bt_settings, 0, sizeof(BtSettings)); + + bt_settings->enabled = false; bt_settings_save(bt_settings); } } diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index fbbb5d13f..2bbf81e7a 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,86.1,, +Version,+,87.0,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, @@ -703,6 +703,7 @@ Function,+,bt_forget_bonded_devices,void,Bt* Function,+,bt_keys_storage_alloc,BtKeysStorage*,const char* Function,+,bt_keys_storage_delete,_Bool,BtKeysStorage* Function,+,bt_keys_storage_free,void,BtKeysStorage* +Function,+,bt_keys_storage_get_root_keys,const GapRootSecurityKeys*,BtKeysStorage* Function,+,bt_keys_storage_is_changed,_Bool,BtKeysStorage* Function,+,bt_keys_storage_load,_Bool,BtKeysStorage* Function,+,bt_keys_storage_set_default_path,void,Bt* @@ -1173,7 +1174,7 @@ Function,+,furi_hal_adc_convert_vref,float,"FuriHalAdcHandle*, uint16_t" Function,+,furi_hal_adc_init,void, Function,+,furi_hal_adc_read,uint16_t,"FuriHalAdcHandle*, FuriHalAdcChannel" Function,+,furi_hal_adc_release,void,FuriHalAdcHandle* -Function,+,furi_hal_bt_change_app,FuriHalBleProfileBase*,"const FuriHalBleProfileTemplate*, FuriHalBleProfileParams, GapEventCallback, void*" +Function,+,furi_hal_bt_change_app,FuriHalBleProfileBase*,"const FuriHalBleProfileTemplate*, FuriHalBleProfileParams, const GapRootSecurityKeys*, GapEventCallback, void*" Function,+,furi_hal_bt_check_profile_type,_Bool,"FuriHalBleProfileBase*, const FuriHalBleProfileTemplate*" Function,+,furi_hal_bt_clear_white_list,_Bool, Function,+,furi_hal_bt_dump_state,void,FuriString* @@ -1200,7 +1201,7 @@ Function,+,furi_hal_bt_nvm_sram_sem_release,void, Function,+,furi_hal_bt_reinit,void, Function,+,furi_hal_bt_set_key_storage_change_callback,void,"BleGlueKeyStorageChangedCallback, void*" Function,+,furi_hal_bt_start_advertising,void, -Function,+,furi_hal_bt_start_app,FuriHalBleProfileBase*,"const FuriHalBleProfileTemplate*, FuriHalBleProfileParams, GapEventCallback, void*" +Function,+,furi_hal_bt_start_app,FuriHalBleProfileBase*,"const FuriHalBleProfileTemplate*, FuriHalBleProfileParams, const GapRootSecurityKeys*, GapEventCallback, void*" Function,+,furi_hal_bt_start_packet_rx,void,"uint8_t, uint8_t" Function,+,furi_hal_bt_start_packet_tx,void,"uint8_t, uint8_t, uint8_t" Function,+,furi_hal_bt_start_radio_stack,_Bool, @@ -1740,7 +1741,7 @@ Function,-,gap_extra_beacon_set_data,_Bool,"const uint8_t*, uint8_t" Function,-,gap_extra_beacon_start,_Bool, Function,-,gap_extra_beacon_stop,_Bool, Function,-,gap_get_state,GapState, -Function,-,gap_init,_Bool,"GapConfig*, GapEventCallback, void*" +Function,-,gap_init,_Bool,"GapConfig*, const GapRootSecurityKeys*, GapEventCallback, void*" Function,-,gap_start_advertising,void, Function,-,gap_stop_advertising,void, Function,-,gap_thread_stop,void, diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index f2b13e4a9..2370133d6 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,86.1,, +Version,+,87.0,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -780,6 +780,7 @@ Function,+,bt_forget_bonded_devices,void,Bt* Function,+,bt_keys_storage_alloc,BtKeysStorage*,const char* Function,+,bt_keys_storage_delete,_Bool,BtKeysStorage* Function,+,bt_keys_storage_free,void,BtKeysStorage* +Function,+,bt_keys_storage_get_root_keys,const GapRootSecurityKeys*,BtKeysStorage* Function,+,bt_keys_storage_is_changed,_Bool,BtKeysStorage* Function,+,bt_keys_storage_load,_Bool,BtKeysStorage* Function,+,bt_keys_storage_set_default_path,void,Bt* @@ -1285,7 +1286,7 @@ Function,+,furi_hal_adc_convert_vref,float,"FuriHalAdcHandle*, uint16_t" Function,+,furi_hal_adc_init,void, Function,+,furi_hal_adc_read,uint16_t,"FuriHalAdcHandle*, FuriHalAdcChannel" Function,+,furi_hal_adc_release,void,FuriHalAdcHandle* -Function,+,furi_hal_bt_change_app,FuriHalBleProfileBase*,"const FuriHalBleProfileTemplate*, FuriHalBleProfileParams, GapEventCallback, void*" +Function,+,furi_hal_bt_change_app,FuriHalBleProfileBase*,"const FuriHalBleProfileTemplate*, FuriHalBleProfileParams, const GapRootSecurityKeys*, GapEventCallback, void*" Function,+,furi_hal_bt_check_profile_type,_Bool,"FuriHalBleProfileBase*, const FuriHalBleProfileTemplate*" Function,+,furi_hal_bt_clear_white_list,_Bool, Function,+,furi_hal_bt_dump_state,void,FuriString* @@ -1312,7 +1313,7 @@ Function,+,furi_hal_bt_nvm_sram_sem_release,void, Function,+,furi_hal_bt_reinit,void, Function,+,furi_hal_bt_set_key_storage_change_callback,void,"BleGlueKeyStorageChangedCallback, void*" Function,+,furi_hal_bt_start_advertising,void, -Function,+,furi_hal_bt_start_app,FuriHalBleProfileBase*,"const FuriHalBleProfileTemplate*, FuriHalBleProfileParams, GapEventCallback, void*" +Function,+,furi_hal_bt_start_app,FuriHalBleProfileBase*,"const FuriHalBleProfileTemplate*, FuriHalBleProfileParams, const GapRootSecurityKeys*, GapEventCallback, void*" Function,+,furi_hal_bt_start_packet_rx,void,"uint8_t, uint8_t" Function,+,furi_hal_bt_start_packet_tx,void,"uint8_t, uint8_t, uint8_t" Function,+,furi_hal_bt_start_radio_stack,_Bool, @@ -1961,7 +1962,7 @@ Function,-,gap_extra_beacon_set_data,_Bool,"const uint8_t*, uint8_t" Function,-,gap_extra_beacon_start,_Bool, Function,-,gap_extra_beacon_stop,_Bool, Function,-,gap_get_state,GapState, -Function,-,gap_init,_Bool,"GapConfig*, GapEventCallback, void*" +Function,-,gap_init,_Bool,"GapConfig*, const GapRootSecurityKeys*, GapEventCallback, void*" Function,-,gap_start_advertising,void, Function,-,gap_stop_advertising,void, Function,-,gap_thread_stop,void, diff --git a/targets/f7/ble_glue/gap.c b/targets/f7/ble_glue/gap.c index 70c9d1c6f..38fc5e420 100644 --- a/targets/f7/ble_glue/gap.c +++ b/targets/f7/ble_glue/gap.c @@ -51,13 +51,6 @@ typedef enum { GapCommandKillThread, } GapCommand; -// Identity root key -static const uint8_t gap_irk[16] = - {0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}; -// Encryption root key -static const uint8_t gap_erk[16] = - {0xfe, 0xdc, 0xba, 0x09, 0x87, 0x65, 0x43, 0x21, 0xfe, 0xdc, 0xba, 0x09, 0x87, 0x65, 0x43, 0x21}; - static Gap* gap = NULL; static void gap_advertise_start(GapState new_state); @@ -333,7 +326,9 @@ static void set_manufacturer_data(uint8_t* mfg_data, uint8_t mfg_data_len) { gap->service.mfg_data_len += mfg_data_len; } -static void gap_init_svc(Gap* gap) { +static void gap_init_svc(Gap* gap, const GapRootSecurityKeys* root_keys) { + furi_check(root_keys); + tBleStatus status; uint32_t srd_bd_addr[2]; @@ -351,9 +346,9 @@ static void gap_init_svc(Gap* gap) { aci_hal_write_config_data( CONFIG_DATA_RANDOM_ADDRESS_OFFSET, CONFIG_DATA_RANDOM_ADDRESS_LEN, (uint8_t*)srd_bd_addr); // Set Identity root key used to derive LTK and CSRK - aci_hal_write_config_data(CONFIG_DATA_IR_OFFSET, CONFIG_DATA_IR_LEN, (uint8_t*)gap_irk); + aci_hal_write_config_data(CONFIG_DATA_IR_OFFSET, CONFIG_DATA_IR_LEN, root_keys->irk); // Set Encryption root key used to derive LTK and CSRK - aci_hal_write_config_data(CONFIG_DATA_ER_OFFSET, CONFIG_DATA_ER_LEN, (uint8_t*)gap_erk); + aci_hal_write_config_data(CONFIG_DATA_ER_OFFSET, CONFIG_DATA_ER_LEN, root_keys->erk); // Set TX Power to 0 dBm aci_hal_set_tx_power_level(1, 0x19); // Initialize GATT interface @@ -535,7 +530,11 @@ static void gap_advetise_timer_callback(void* context) { furi_check(furi_message_queue_put(gap->command_queue, &command, 0) == FuriStatusOk); } -bool gap_init(GapConfig* config, GapEventCallback on_event_cb, void* context) { +bool gap_init( + GapConfig* config, + const GapRootSecurityKeys* root_keys, + GapEventCallback on_event_cb, + void* context) { if(!ble_glue_is_radio_stack_ready()) { return false; } @@ -548,7 +547,7 @@ bool gap_init(GapConfig* config, GapEventCallback on_event_cb, void* context) { gap->advertise_timer = furi_timer_alloc(gap_advetise_timer_callback, FuriTimerTypeOnce, NULL); // Initialization of GATT & GAP layer gap->service.adv_name = config->adv_name; - gap_init_svc(gap); + gap_init_svc(gap, root_keys); ble_event_dispatcher_init(); // Initialization of the GAP state gap->state_mutex = furi_mutex_alloc(FuriMutexTypeNormal); @@ -573,14 +572,13 @@ bool gap_init(GapConfig* config, GapEventCallback on_event_cb, void* context) { set_manufacturer_data(gap->config->mfg_data, gap->config->mfg_data_len); } + gap->service.adv_svc_uuid_len = 1; if(gap->config->adv_service.UUID_Type == UUID_TYPE_16) { uint8_t adv_service_uid[2]; - gap->service.adv_svc_uuid_len = 1; adv_service_uid[0] = gap->config->adv_service.Service_UUID_16 & 0xff; adv_service_uid[1] = gap->config->adv_service.Service_UUID_16 >> 8; set_advertisment_service_uid(adv_service_uid, sizeof(adv_service_uid)); } else if(gap->config->adv_service.UUID_Type == UUID_TYPE_128) { - gap->service.adv_svc_uuid_len = 1; set_advertisment_service_uid( gap->config->adv_service.Service_UUID_128, sizeof(gap->config->adv_service.Service_UUID_128)); diff --git a/targets/f7/ble_glue/gap.h b/targets/f7/ble_glue/gap.h index b2bf0ffc1..57a0bb79d 100644 --- a/targets/f7/ble_glue/gap.h +++ b/targets/f7/ble_glue/gap.h @@ -6,6 +6,7 @@ #include #define GAP_MAC_ADDR_SIZE (6) +#define GAP_KEY_SIZE (0x10) /* * GAP helpers - background thread that handles BLE GAP events and advertising. @@ -83,7 +84,18 @@ typedef struct { GapConnectionParamsRequest conn_param; } GapConfig; -bool gap_init(GapConfig* config, GapEventCallback on_event_cb, void* context); +typedef struct { + // Encryption Root key. Must be unique per-device (or app) + uint8_t erk[GAP_KEY_SIZE]; + // Identity Root key. Used for resolving RPAs, if configured + uint8_t irk[GAP_KEY_SIZE]; +} GapRootSecurityKeys; + +bool gap_init( + GapConfig* config, + const GapRootSecurityKeys* root_keys, + GapEventCallback on_event_cb, + void* context); void gap_start_advertising(void); diff --git a/targets/f7/furi_hal/furi_hal_bt.c b/targets/f7/furi_hal/furi_hal_bt.c index 2c1a9367b..f1881f94f 100644 --- a/targets/f7/furi_hal/furi_hal_bt.c +++ b/targets/f7/furi_hal/furi_hal_bt.c @@ -36,6 +36,9 @@ static FuriHalBt furi_hal_bt = { .stack = FuriHalBtStackUnknown, }; +static FuriHalBleProfileBase* current_profile = NULL; +static GapConfig current_config = {0}; + void furi_hal_bt_init(void) { FURI_LOG_I(TAG, "Start BT initialization"); furi_hal_bus_enable(FuriHalBusHSEM); @@ -149,9 +152,6 @@ bool furi_hal_bt_is_testing_supported(void) { } } -static FuriHalBleProfileBase* current_profile = NULL; -static GapConfig current_config = {0}; - bool furi_hal_bt_check_profile_type( FuriHalBleProfileBase* profile, const FuriHalBleProfileTemplate* profile_template) { @@ -165,10 +165,12 @@ bool furi_hal_bt_check_profile_type( FuriHalBleProfileBase* furi_hal_bt_start_app( const FuriHalBleProfileTemplate* profile_template, FuriHalBleProfileParams params, + const GapRootSecurityKeys* root_keys, GapEventCallback event_cb, void* context) { furi_check(event_cb); furi_check(profile_template); + furi_check(root_keys); furi_check(current_profile == NULL); do { @@ -183,7 +185,7 @@ FuriHalBleProfileBase* furi_hal_bt_start_app( profile_template->get_gap_config(¤t_config, params); - if(!gap_init(¤t_config, event_cb, context)) { + if(!gap_init(¤t_config, root_keys, event_cb, context)) { gap_thread_stop(); FURI_LOG_E(TAG, "Failed to init GAP"); break; @@ -239,12 +241,11 @@ void furi_hal_bt_reinit(void) { FuriHalBleProfileBase* furi_hal_bt_change_app( const FuriHalBleProfileTemplate* profile_template, FuriHalBleProfileParams profile_params, + const GapRootSecurityKeys* root_keys, GapEventCallback event_cb, void* context) { - furi_check(event_cb); - furi_hal_bt_reinit(); - return furi_hal_bt_start_app(profile_template, profile_params, event_cb, context); + return furi_hal_bt_start_app(profile_template, profile_params, root_keys, event_cb, context); } bool furi_hal_bt_is_active(void) { diff --git a/targets/furi_hal_include/furi_hal_bt.h b/targets/furi_hal_include/furi_hal_bt.h index 6da723311..1711f703d 100644 --- a/targets/furi_hal_include/furi_hal_bt.h +++ b/targets/furi_hal_include/furi_hal_bt.h @@ -77,6 +77,7 @@ bool furi_hal_bt_check_profile_type( * * @param profile_template FuriHalBleProfileTemplate instance * @param params Parameters to pass to the profile. Can be NULL + * @param root_keys pointer to root keys * @param event_cb GapEventCallback instance * @param context pointer to context * @@ -85,6 +86,7 @@ bool furi_hal_bt_check_profile_type( FURI_WARN_UNUSED FuriHalBleProfileBase* furi_hal_bt_start_app( const FuriHalBleProfileTemplate* profile_template, FuriHalBleProfileParams params, + const GapRootSecurityKeys* root_keys, GapEventCallback event_cb, void* context); @@ -100,6 +102,7 @@ void furi_hal_bt_reinit(void); * @param profile_template FuriHalBleProfileTemplate instance * @param profile_params Parameters to pass to the profile. Can be NULL * @param event_cb GapEventCallback instance + * @param root_keys pointer to root keys * @param context pointer to context * * @return instance of profile, NULL on failure @@ -107,6 +110,7 @@ void furi_hal_bt_reinit(void); FURI_WARN_UNUSED FuriHalBleProfileBase* furi_hal_bt_change_app( const FuriHalBleProfileTemplate* profile_template, FuriHalBleProfileParams profile_params, + const GapRootSecurityKeys* root_keys, GapEventCallback event_cb, void* context); From d0360625d6e1344eb87c80cd47f699717721f9ec Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Wed, 24 Sep 2025 23:24:28 +0400 Subject: [PATCH 12/12] [FL-3925] JS views finished (#4155) * js: value destructuring and tests * js: temporary fix to see size impact * js_val: reduce code size 1 * i may be stupid. * test: js_value args * Revert "js: temporary fix to see size impact" This reverts commit f51d726dbafc4300d3552020de1c3b8f9ecd3ac1. * pvs: silence warnings * style: formatting * pvs: silence warnings? * pvs: silence warnings?? * js_value: redesign declaration types for less code * js: temporary fix to see size impact * style: formatting * pvs: fix helpful warnings * js_value: reduce .rodata size * pvs: fix helpful warning * js_value: reduce code size 1 * fix build error * style: format * Revert "js: temporary fix to see size impact" This reverts commit d6a46f01794132e882e03fd273dec24386a4f8ba. * style: format * js: move to new arg parser * style: format * feat: all js views done * js, toolbox: generalize string owning * toolbox: silence pvs warning --------- Co-authored-by: hedger Co-authored-by: hedger --- applications/services/gui/modules/popup.c | 5 +- applications/services/gui/modules/popup.h | 2 +- applications/system/js_app/application.fam | 48 +++ .../js_app/examples/apps/Scripts/gui.js | 120 +++++++- .../js_app/modules/js_gui/button_menu.c | 169 +++++++++++ .../js_app/modules/js_gui/button_panel.c | 274 ++++++++++++++++++ .../system/js_app/modules/js_gui/icon.c | 10 + .../system/js_app/modules/js_gui/js_gui.c | 21 +- .../system/js_app/modules/js_gui/js_gui.h | 5 + .../modules/js_gui/js_gui_api_table_i.h | 3 +- .../system/js_app/modules/js_gui/menu.c | 105 +++++++ .../js_app/modules/js_gui/number_input.c | 130 +++++++++ .../system/js_app/modules/js_gui/popup.c | 102 +++++++ .../system/js_app/modules/js_gui/submenu.c | 33 ++- .../system/js_app/modules/js_gui/vi_list.c | 163 +++++++++++ .../packages/fz-sdk/gui/button_menu.d.ts | 40 +++ .../packages/fz-sdk/gui/button_panel.d.ts | 49 ++++ .../js_app/packages/fz-sdk/gui/icon.d.ts | 5 +- .../js_app/packages/fz-sdk/gui/index.d.ts | 18 +- .../js_app/packages/fz-sdk/gui/menu.d.ts | 38 +++ .../packages/fz-sdk/gui/number_input.d.ts | 44 +++ .../js_app/packages/fz-sdk/gui/popup.d.ts | 43 +++ .../js_app/packages/fz-sdk/gui/submenu.d.ts | 5 +- .../js_app/packages/fz-sdk/gui/vi_list.d.ts | 38 +++ .../js_app/packages/fz-sdk/package.json | 2 +- lib/toolbox/SConscript | 1 + lib/toolbox/str_buffer.c | 18 ++ lib/toolbox/str_buffer.h | 47 +++ targets/f18/api_symbols.csv | 3 + targets/f7/api_symbols.csv | 3 + 30 files changed, 1496 insertions(+), 48 deletions(-) create mode 100644 applications/system/js_app/modules/js_gui/button_menu.c create mode 100644 applications/system/js_app/modules/js_gui/button_panel.c create mode 100644 applications/system/js_app/modules/js_gui/menu.c create mode 100644 applications/system/js_app/modules/js_gui/number_input.c create mode 100644 applications/system/js_app/modules/js_gui/popup.c create mode 100644 applications/system/js_app/modules/js_gui/vi_list.c create mode 100644 applications/system/js_app/packages/fz-sdk/gui/button_menu.d.ts create mode 100644 applications/system/js_app/packages/fz-sdk/gui/button_panel.d.ts create mode 100644 applications/system/js_app/packages/fz-sdk/gui/menu.d.ts create mode 100644 applications/system/js_app/packages/fz-sdk/gui/number_input.d.ts create mode 100644 applications/system/js_app/packages/fz-sdk/gui/popup.d.ts create mode 100644 applications/system/js_app/packages/fz-sdk/gui/vi_list.d.ts create mode 100644 lib/toolbox/str_buffer.c create mode 100644 lib/toolbox/str_buffer.h diff --git a/applications/services/gui/modules/popup.c b/applications/services/gui/modules/popup.c index 5c9c75e4a..6b5cbfdd0 100644 --- a/applications/services/gui/modules/popup.c +++ b/applications/services/gui/modules/popup.c @@ -93,13 +93,12 @@ static bool popup_view_input_callback(InputEvent* event, void* context) { void popup_start_timer(void* context) { Popup* popup = context; if(popup->timer_enabled) { - uint32_t timer_period = - popup->timer_period_in_ms / (1000.0f / furi_kernel_get_tick_frequency()); + uint32_t timer_period = furi_ms_to_ticks(popup->timer_period_in_ms); if(timer_period == 0) timer_period = 1; if(furi_timer_start(popup->timer, timer_period) != FuriStatusOk) { furi_crash(); - }; + } } } diff --git a/applications/services/gui/modules/popup.h b/applications/services/gui/modules/popup.h index a3e8dc773..d694dc45b 100644 --- a/applications/services/gui/modules/popup.h +++ b/applications/services/gui/modules/popup.h @@ -41,7 +41,7 @@ void popup_free(Popup* popup); */ View* popup_get_view(Popup* popup); -/** Set popup header text +/** Set popup timeout callback * * @param popup Popup instance * @param callback PopupCallback diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index 843ab5543..43e2cbeff 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -78,6 +78,54 @@ App( sources=["modules/js_gui/text_input.c"], ) +App( + appid="js_gui__number_input", + apptype=FlipperAppType.PLUGIN, + entry_point="js_view_number_input_ep", + requires=["js_app"], + sources=["modules/js_gui/number_input.c"], +) + +App( + appid="js_gui__button_panel", + apptype=FlipperAppType.PLUGIN, + entry_point="js_view_button_panel_ep", + requires=["js_app"], + sources=["modules/js_gui/button_panel.c"], +) + +App( + appid="js_gui__popup", + apptype=FlipperAppType.PLUGIN, + entry_point="js_view_popup_ep", + requires=["js_app"], + sources=["modules/js_gui/popup.c"], +) + +App( + appid="js_gui__button_menu", + apptype=FlipperAppType.PLUGIN, + entry_point="js_view_button_menu_ep", + requires=["js_app"], + sources=["modules/js_gui/button_menu.c"], +) + +App( + appid="js_gui__menu", + apptype=FlipperAppType.PLUGIN, + entry_point="js_view_menu_ep", + requires=["js_app"], + sources=["modules/js_gui/menu.c"], +) + +App( + appid="js_gui__vi_list", + apptype=FlipperAppType.PLUGIN, + entry_point="js_view_vi_list_ep", + requires=["js_app"], + sources=["modules/js_gui/vi_list.c"], +) + App( appid="js_gui__byte_input", apptype=FlipperAppType.PLUGIN, diff --git a/applications/system/js_app/examples/apps/Scripts/gui.js b/applications/system/js_app/examples/apps/Scripts/gui.js index bc569cee0..3a179fc9f 100644 --- a/applications/system/js_app/examples/apps/Scripts/gui.js +++ b/applications/system/js_app/examples/apps/Scripts/gui.js @@ -9,6 +9,12 @@ let byteInputView = require("gui/byte_input"); let textBoxView = require("gui/text_box"); let dialogView = require("gui/dialog"); let filePicker = require("gui/file_picker"); +let buttonMenuView = require("gui/button_menu"); +let buttonPanelView = require("gui/button_panel"); +let menuView = require("gui/menu"); +let numberInputView = require("gui/number_input"); +let popupView = require("gui/popup"); +let viListView = require("gui/vi_list"); let widget = require("gui/widget"); let icon = require("gui/icon"); let flipper = require("flipper"); @@ -27,6 +33,11 @@ let stopwatchWidgetElements = [ { element: "button", button: "right", text: "Back" }, ]; +// icons for the button panel +let offIcons = [icon.getBuiltin("off_19x20"), icon.getBuiltin("off_hover_19x20")]; +let powerIcons = [icon.getBuiltin("power_19x20"), icon.getBuiltin("power_hover_19x20")]; +let settingsIcon = icon.getBuiltin("Settings_14"); + // declare view instances let views = { loading: loadingView.make(), @@ -48,19 +59,57 @@ let views = { 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.", }), stopwatchWidget: widget.makeWith({}, stopwatchWidgetElements), + buttonMenu: buttonMenuView.makeWith({ + header: "Header" + }, [ + { type: "common", label: "Test" }, + { type: "control", label: "Test2" }, + ]), + buttonPanel: buttonPanelView.makeWith({ + matrixSizeX: 2, + matrixSizeY: 2, + }, [ + { type: "button", x: 0, y: 0, matrixX: 0, matrixY: 0, icon: offIcons[0], iconSelected: offIcons[1] }, + { type: "button", x: 30, y: 30, matrixX: 1, matrixY: 1, icon: powerIcons[0], iconSelected: powerIcons[1] }, + { type: "label", x: 0, y: 50, text: "Label", font: "primary" }, + ]), + menu: menuView.makeWith({}, [ + { label: "One", icon: settingsIcon }, + { label: "Two", icon: settingsIcon }, + { label: "three", icon: settingsIcon }, + ]), + numberKbd: numberInputView.makeWith({ + header: "Number input", + defaultValue: 100, + minValue: 0, + maxValue: 200, + }), + popup: popupView.makeWith({ + header: "Hello", + text: "I'm going to be gone\nin 2 seconds", + }), + viList: viListView.makeWith({}, [ + { label: "One", variants: ["1", "1.0"] }, + { label: "Two", variants: ["2", "2.0"] }, + ]), demos: submenuView.makeWith({ header: "Choose a demo", - items: [ - "Hourglass screen", - "Empty screen", - "Text input & Dialog", - "Byte input", - "Text box", - "File picker", - "Widget", - "Exit app", - ], - }), + }, [ + "Hourglass screen", + "Empty screen", + "Text input & Dialog", + "Byte input", + "Text box", + "File picker", + "Widget", + "Button menu", + "Button panel", + "Menu", + "Number input", + "Popup", + "Var. item list", + "Exit app", + ]), }; // demo selector @@ -92,6 +141,19 @@ eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, v } else if (index === 6) { gui.viewDispatcher.switchTo(views.stopwatchWidget); } else if (index === 7) { + gui.viewDispatcher.switchTo(views.buttonMenu); + } else if (index === 8) { + gui.viewDispatcher.switchTo(views.buttonPanel); + } else if (index === 9) { + gui.viewDispatcher.switchTo(views.menu); + } else if (index === 10) { + gui.viewDispatcher.switchTo(views.numberKbd); + } else if (index === 11) { + views.popup.set("timeout", 2000); + gui.viewDispatcher.switchTo(views.popup); + } else if (index === 12) { + gui.viewDispatcher.switchTo(views.viList); + } else if (index === 13) { eventLoop.stop(); } }, gui, eventLoop, views); @@ -156,6 +218,42 @@ eventLoop.subscribe(eventLoop.timer("periodic", 500), function (_sub, _item, vie return [views, stopwatchWidgetElements, halfSeconds]; }, views, stopwatchWidgetElements, 0); +// go back after popup times out +eventLoop.subscribe(views.popup.timeout, function (_sub, _item, gui, views) { + gui.viewDispatcher.switchTo(views.demos); +}, gui, views); + +// button menu callback +eventLoop.subscribe(views.buttonMenu.input, function (_sub, input, gui, views) { + views.helloDialog.set("text", "You selected #" + input.index.toString()); + views.helloDialog.set("center", "Cool!"); + gui.viewDispatcher.switchTo(views.helloDialog); +}, gui, views); + +// button panel callback +eventLoop.subscribe(views.buttonPanel.input, function (_sub, input, gui, views) { + views.helloDialog.set("text", "You selected #" + input.index.toString()); + views.helloDialog.set("center", "Cool!"); + gui.viewDispatcher.switchTo(views.helloDialog); +}, gui, views); + +// menu callback +eventLoop.subscribe(views.menu.chosen, function (_sub, index, gui, views) { + views.helloDialog.set("text", "You selected #" + index.toString()); + views.helloDialog.set("center", "Cool!"); + gui.viewDispatcher.switchTo(views.helloDialog); +}, gui, views); + +// menu callback +eventLoop.subscribe(views.numberKbd.input, function (_sub, number, gui, views) { + views.helloDialog.set("text", "You typed " + number.toString()); + views.helloDialog.set("center", "Cool!"); + gui.viewDispatcher.switchTo(views.helloDialog); +}, gui, views); + +// ignore VI list +eventLoop.subscribe(views.viList.valueUpdate, function (_sub, _item) {}); + // run UI gui.viewDispatcher.switchTo(views.demos); eventLoop.run(); diff --git a/applications/system/js_app/modules/js_gui/button_menu.c b/applications/system/js_app/modules/js_gui/button_menu.c new file mode 100644 index 000000000..e247e1635 --- /dev/null +++ b/applications/system/js_app/modules/js_gui/button_menu.c @@ -0,0 +1,169 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include "js_gui.h" +#include "../js_event_loop/js_event_loop.h" +#include +#include + +typedef struct { + int32_t next_index; + StrBuffer str_buffer; + + FuriMessageQueue* input_queue; + JsEventLoopContract contract; +} JsBtnMenuContext; + +typedef struct { + int32_t index; + InputType input_type; +} JsBtnMenuEvent; + +static const char* js_input_type_to_str(InputType type) { + switch(type) { + case InputTypePress: + return "press"; + case InputTypeRelease: + return "release"; + case InputTypeShort: + return "short"; + case InputTypeLong: + return "long"; + case InputTypeRepeat: + return "repeat"; + default: + furi_crash(); + } +} + +static mjs_val_t + input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsBtnMenuContext* context) { + UNUSED(context); + JsBtnMenuEvent event; + furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk); + + mjs_val_t event_obj = mjs_mk_object(mjs); + JS_ASSIGN_MULTI(mjs, event_obj) { + JS_FIELD("index", mjs_mk_number(mjs, event.index)); + JS_FIELD("type", mjs_mk_string(mjs, js_input_type_to_str(event.input_type), ~0, false)); + } + + return event_obj; +} + +static void input_callback(void* ctx, int32_t index, InputType type) { + JsBtnMenuContext* context = ctx; + JsBtnMenuEvent event = { + .index = index, + .input_type = type, + }; + furi_check(furi_message_queue_put(context->input_queue, &event, 0) == FuriStatusOk); +} + +static bool matrix_header_assign( + struct mjs* mjs, + ButtonMenu* menu, + JsViewPropValue value, + JsBtnMenuContext* context) { + UNUSED(mjs); + button_menu_set_header(menu, str_buffer_make_owned_clone(&context->str_buffer, value.string)); + return true; +} + +static bool js_button_menu_add_child( + struct mjs* mjs, + ButtonMenu* menu, + JsBtnMenuContext* context, + mjs_val_t child_obj) { + static const JsValueEnumVariant js_button_menu_item_type_variants[] = { + {"common", ButtonMenuItemTypeCommon}, + {"control", ButtonMenuItemTypeControl}, + }; + static const JsValueDeclaration js_button_menu_item_type = + JS_VALUE_ENUM(ButtonMenuItemType, js_button_menu_item_type_variants); + + static const JsValueDeclaration js_button_menu_string = JS_VALUE_SIMPLE(JsValueTypeString); + + static const JsValueObjectField js_button_menu_child_fields[] = { + {"type", &js_button_menu_item_type}, + {"label", &js_button_menu_string}, + }; + static const JsValueDeclaration js_button_menu_child = + JS_VALUE_OBJECT(js_button_menu_child_fields); + + ButtonMenuItemType item_type; + const char* label; + JsValueParseStatus status; + JS_VALUE_PARSE( + mjs, + JS_VALUE_PARSE_SOURCE_VALUE(&js_button_menu_child), + JsValueParseFlagReturnOnError, + &status, + &child_obj, + &item_type, + &label); + if(status != JsValueParseStatusOk) return false; + + button_menu_add_item( + menu, + str_buffer_make_owned_clone(&context->str_buffer, label), + context->next_index++, + input_callback, + item_type, + context); + + return true; +} + +static void js_button_menu_reset_children(ButtonMenu* menu, JsBtnMenuContext* context) { + context->next_index = 0; + button_menu_reset(menu); + str_buffer_clear_all_clones(&context->str_buffer); +} + +static JsBtnMenuContext* ctx_make(struct mjs* mjs, ButtonMenu* menu, mjs_val_t view_obj) { + UNUSED(menu); + JsBtnMenuContext* context = malloc(sizeof(JsBtnMenuContext)); + *context = (JsBtnMenuContext){ + .next_index = 0, + .str_buffer = {0}, + .input_queue = furi_message_queue_alloc(1, sizeof(JsBtnMenuEvent)), + }; + context->contract = (JsEventLoopContract){ + .magic = JsForeignMagic_JsEventLoopContract, + .object_type = JsEventLoopObjectTypeQueue, + .object = context->input_queue, + .non_timer = + { + .event = FuriEventLoopEventIn, + .transformer = (JsEventLoopTransformer)input_transformer, + .transformer_context = context, + }, + }; + mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract)); + return context; +} + +static void ctx_destroy(ButtonMenu* input, JsBtnMenuContext* context, FuriEventLoop* loop) { + UNUSED(input); + furi_event_loop_maybe_unsubscribe(loop, context->input_queue); + furi_message_queue_free(context->input_queue); + str_buffer_clear_all_clones(&context->str_buffer); + free(context); +} + +static const JsViewDescriptor view_descriptor = { + .alloc = (JsViewAlloc)button_menu_alloc, + .free = (JsViewFree)button_menu_free, + .get_view = (JsViewGetView)button_menu_get_view, + .custom_make = (JsViewCustomMake)ctx_make, + .custom_destroy = (JsViewCustomDestroy)ctx_destroy, + .add_child = (JsViewAddChild)js_button_menu_add_child, + .reset_children = (JsViewResetChildren)js_button_menu_reset_children, + .prop_cnt = 1, + .props = { + (JsViewPropDescriptor){ + .name = "header", + .type = JsViewPropTypeString, + .assign = (JsViewPropAssign)matrix_header_assign}, + }}; + +JS_GUI_VIEW_DEF(button_menu, &view_descriptor); diff --git a/applications/system/js_app/modules/js_gui/button_panel.c b/applications/system/js_app/modules/js_gui/button_panel.c new file mode 100644 index 000000000..ebd4edb57 --- /dev/null +++ b/applications/system/js_app/modules/js_gui/button_panel.c @@ -0,0 +1,274 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include "js_gui.h" +#include "../js_event_loop/js_event_loop.h" +#include +#include + +typedef struct { + size_t matrix_x, matrix_y; + int32_t next_index; + StrBuffer str_buffer; + + FuriMessageQueue* input_queue; + JsEventLoopContract contract; +} JsBtnPanelContext; + +typedef struct { + int32_t index; + InputType input_type; +} JsBtnPanelEvent; + +static const char* js_input_type_to_str(InputType type) { + switch(type) { + case InputTypePress: + return "press"; + case InputTypeRelease: + return "release"; + case InputTypeShort: + return "short"; + case InputTypeLong: + return "long"; + case InputTypeRepeat: + return "repeat"; + default: + furi_crash(); + } +} + +static mjs_val_t + input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsBtnPanelContext* context) { + UNUSED(context); + JsBtnPanelEvent event; + furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk); + + mjs_val_t event_obj = mjs_mk_object(mjs); + JS_ASSIGN_MULTI(mjs, event_obj) { + JS_FIELD("index", mjs_mk_number(mjs, event.index)); + JS_FIELD("type", mjs_mk_string(mjs, js_input_type_to_str(event.input_type), ~0, false)); + } + + return event_obj; +} + +static void input_callback(void* ctx, int32_t index, InputType type) { + JsBtnPanelContext* context = ctx; + JsBtnPanelEvent event = { + .index = index, + .input_type = type, + }; + furi_check(furi_message_queue_put(context->input_queue, &event, 0) == FuriStatusOk); +} + +static bool matrix_size_x_assign( + struct mjs* mjs, + ButtonPanel* panel, + JsViewPropValue value, + JsBtnPanelContext* context) { + UNUSED(mjs); + context->matrix_x = value.number; + button_panel_reserve(panel, context->matrix_x, context->matrix_y); + return true; +} + +static bool matrix_size_y_assign( + struct mjs* mjs, + ButtonPanel* panel, + JsViewPropValue value, + JsBtnPanelContext* context) { + UNUSED(mjs); + context->matrix_y = value.number; + button_panel_reserve(panel, context->matrix_x, context->matrix_y); + return true; +} + +static bool js_button_panel_add_child( + struct mjs* mjs, + ButtonPanel* panel, + JsBtnPanelContext* context, + mjs_val_t child_obj) { + typedef enum { + JsButtonPanelChildTypeButton, + JsButtonPanelChildTypeLabel, + JsButtonPanelChildTypeIcon, + } JsButtonPanelChildType; + static const JsValueEnumVariant js_button_panel_child_type_variants[] = { + {"button", JsButtonPanelChildTypeButton}, + {"label", JsButtonPanelChildTypeLabel}, + {"icon", JsButtonPanelChildTypeIcon}, + }; + static const JsValueDeclaration js_button_panel_child_type = + JS_VALUE_ENUM(JsButtonPanelChildType, js_button_panel_child_type_variants); + + static const JsValueDeclaration js_button_panel_number = JS_VALUE_SIMPLE(JsValueTypeInt32); + static const JsValueObjectField js_button_panel_common_fields[] = { + {"type", &js_button_panel_child_type}, + {"x", &js_button_panel_number}, + {"y", &js_button_panel_number}, + }; + static const JsValueDeclaration js_button_panel_common = + JS_VALUE_OBJECT(js_button_panel_common_fields); + + static const JsValueDeclaration js_button_panel_pointer = + JS_VALUE_SIMPLE(JsValueTypeRawPointer); + static const JsValueObjectField js_button_panel_button_fields[] = { + {"matrixX", &js_button_panel_number}, + {"matrixY", &js_button_panel_number}, + {"icon", &js_button_panel_pointer}, + {"iconSelected", &js_button_panel_pointer}, + }; + static const JsValueDeclaration js_button_panel_button = + JS_VALUE_OBJECT(js_button_panel_button_fields); + + static const JsValueDeclaration js_button_panel_string = JS_VALUE_SIMPLE(JsValueTypeString); + static const JsValueObjectField js_button_panel_label_fields[] = { + {"text", &js_button_panel_string}, + {"font", &js_gui_font_declaration}, + }; + static const JsValueDeclaration js_button_panel_label = + JS_VALUE_OBJECT(js_button_panel_label_fields); + + static const JsValueObjectField js_button_panel_icon_fields[] = { + {"icon", &js_button_panel_pointer}, + }; + static const JsValueDeclaration js_button_panel_icon = + JS_VALUE_OBJECT(js_button_panel_icon_fields); + + JsButtonPanelChildType child_type; + int32_t x, y; + JsValueParseStatus status; + JS_VALUE_PARSE( + mjs, + JS_VALUE_PARSE_SOURCE_VALUE(&js_button_panel_common), + JsValueParseFlagReturnOnError, + &status, + &child_obj, + &child_type, + &x, + &y); + if(status != JsValueParseStatusOk) return false; + + switch(child_type) { + case JsButtonPanelChildTypeButton: { + int32_t matrix_x, matrix_y; + const Icon *icon, *icon_selected; + JS_VALUE_PARSE( + mjs, + JS_VALUE_PARSE_SOURCE_VALUE(&js_button_panel_button), + JsValueParseFlagReturnOnError, + &status, + &child_obj, + &matrix_x, + &matrix_y, + &icon, + &icon_selected); + if(status != JsValueParseStatusOk) return false; + button_panel_add_item( + panel, + context->next_index++, + matrix_x, + matrix_y, + x, + y, + icon, + icon_selected, + (ButtonItemCallback)input_callback, + context); + break; + } + + case JsButtonPanelChildTypeLabel: { + const char* text; + Font font; + JS_VALUE_PARSE( + mjs, + JS_VALUE_PARSE_SOURCE_VALUE(&js_button_panel_label), + JsValueParseFlagReturnOnError, + &status, + &child_obj, + &text, + &font); + if(status != JsValueParseStatusOk) return false; + button_panel_add_label( + panel, x, y, font, str_buffer_make_owned_clone(&context->str_buffer, text)); + break; + } + + case JsButtonPanelChildTypeIcon: { + const Icon* icon; + JS_VALUE_PARSE( + mjs, + JS_VALUE_PARSE_SOURCE_VALUE(&js_button_panel_icon), + JsValueParseFlagReturnOnError, + &status, + &child_obj, + &icon); + if(status != JsValueParseStatusOk) return false; + button_panel_add_icon(panel, x, y, icon); + break; + } + } + + return true; +} + +static void js_button_panel_reset_children(ButtonPanel* panel, JsBtnPanelContext* context) { + context->next_index = 0; + button_panel_reset(panel); + button_panel_reserve(panel, context->matrix_x, context->matrix_y); + str_buffer_clear_all_clones(&context->str_buffer); +} + +static JsBtnPanelContext* ctx_make(struct mjs* mjs, ButtonPanel* panel, mjs_val_t view_obj) { + UNUSED(panel); + JsBtnPanelContext* context = malloc(sizeof(JsBtnPanelContext)); + *context = (JsBtnPanelContext){ + .matrix_x = 1, + .matrix_y = 1, + .next_index = 0, + .str_buffer = {0}, + .input_queue = furi_message_queue_alloc(1, sizeof(JsBtnPanelEvent)), + }; + context->contract = (JsEventLoopContract){ + .magic = JsForeignMagic_JsEventLoopContract, + .object_type = JsEventLoopObjectTypeQueue, + .object = context->input_queue, + .non_timer = + { + .event = FuriEventLoopEventIn, + .transformer = (JsEventLoopTransformer)input_transformer, + .transformer_context = context, + }, + }; + mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract)); + return context; +} + +static void ctx_destroy(ButtonPanel* input, JsBtnPanelContext* context, FuriEventLoop* loop) { + UNUSED(input); + furi_event_loop_maybe_unsubscribe(loop, context->input_queue); + furi_message_queue_free(context->input_queue); + str_buffer_clear_all_clones(&context->str_buffer); + free(context); +} + +static const JsViewDescriptor view_descriptor = { + .alloc = (JsViewAlloc)button_panel_alloc, + .free = (JsViewFree)button_panel_free, + .get_view = (JsViewGetView)button_panel_get_view, + .custom_make = (JsViewCustomMake)ctx_make, + .custom_destroy = (JsViewCustomDestroy)ctx_destroy, + .add_child = (JsViewAddChild)js_button_panel_add_child, + .reset_children = (JsViewResetChildren)js_button_panel_reset_children, + .prop_cnt = 2, + .props = { + (JsViewPropDescriptor){ + .name = "matrixSizeX", + .type = JsViewPropTypeNumber, + .assign = (JsViewPropAssign)matrix_size_x_assign}, + (JsViewPropDescriptor){ + .name = "matrixSizeY", + .type = JsViewPropTypeNumber, + .assign = (JsViewPropAssign)matrix_size_y_assign}, + }}; + +JS_GUI_VIEW_DEF(button_panel, &view_descriptor); diff --git a/applications/system/js_app/modules/js_gui/icon.c b/applications/system/js_app/modules/js_gui/icon.c index 4fc6da2e0..11e26d02f 100644 --- a/applications/system/js_app/modules/js_gui/icon.c +++ b/applications/system/js_app/modules/js_gui/icon.c @@ -14,9 +14,19 @@ typedef struct { .name = #icon, .data = &I_##icon \ } +#define ANIM_ICON_DEF(icon) \ + (IconDefinition) { \ + .name = #icon, .data = &A_##icon \ + } + static const IconDefinition builtin_icons[] = { ICON_DEF(DolphinWait_59x54), ICON_DEF(js_script_10px), + ICON_DEF(off_19x20), + ICON_DEF(off_hover_19x20), + ICON_DEF(power_19x20), + ICON_DEF(power_hover_19x20), + ANIM_ICON_DEF(Settings_14), }; // Firmware's Icon struct needs a frames array, and uses a small CompressHeader diff --git a/applications/system/js_app/modules/js_gui/js_gui.c b/applications/system/js_app/modules/js_gui/js_gui.c index c20d980aa..2f7825717 100644 --- a/applications/system/js_app/modules/js_gui/js_gui.c +++ b/applications/system/js_app/modules/js_gui/js_gui.c @@ -31,6 +31,14 @@ typedef struct { void* custom_data; } JsGuiViewData; +static const JsValueEnumVariant js_gui_font_variants[] = { + {"primary", FontPrimary}, + {"secondary", FontSecondary}, + {"keyboard", FontKeyboard}, + {"bit_numbers", FontBigNumbers}, +}; +const JsValueDeclaration js_gui_font_declaration = JS_VALUE_ENUM(Font, js_gui_font_variants); + /** * @brief Transformer for custom events */ @@ -273,9 +281,12 @@ static bool /** * @brief Sets the list of children. Not available from JS. */ -static bool - js_gui_view_internal_set_children(struct mjs* mjs, mjs_val_t children, JsGuiViewData* data) { - data->descriptor->reset_children(data->specific_view, data->custom_data); +static bool js_gui_view_internal_set_children( + struct mjs* mjs, + mjs_val_t children, + JsGuiViewData* data, + bool do_reset) { + if(do_reset) data->descriptor->reset_children(data->specific_view, data->custom_data); for(size_t i = 0; i < mjs_array_length(mjs, children); i++) { mjs_val_t child = mjs_array_get(mjs, children, i); @@ -357,7 +368,7 @@ static void js_gui_view_set_children(struct mjs* mjs) { if(!data->descriptor->add_child || !data->descriptor->reset_children) JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children"); - js_gui_view_internal_set_children(mjs, children, data); + js_gui_view_internal_set_children(mjs, children, data, true); } /** @@ -450,7 +461,7 @@ static void js_gui_vf_make_with(struct mjs* mjs) { if(!data->descriptor->add_child || !data->descriptor->reset_children) JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children"); - if(!js_gui_view_internal_set_children(mjs, children, data)) return; + if(!js_gui_view_internal_set_children(mjs, children, data, false)) return; } mjs_return(mjs, view_obj); diff --git a/applications/system/js_app/modules/js_gui/js_gui.h b/applications/system/js_app/modules/js_gui/js_gui.h index d9d98df39..552eaee7c 100644 --- a/applications/system/js_app/modules/js_gui/js_gui.h +++ b/applications/system/js_app/modules/js_gui/js_gui.h @@ -20,6 +20,11 @@ typedef union { mjs_val_t term; } JsViewPropValue; +/** + * JS-to-C font enum mapping + */ +extern const JsValueDeclaration js_gui_font_declaration; + /** * @brief Assigns a value to a view property * diff --git a/applications/system/js_app/modules/js_gui/js_gui_api_table_i.h b/applications/system/js_app/modules/js_gui/js_gui_api_table_i.h index 852b3d107..c83a38e30 100644 --- a/applications/system/js_app/modules/js_gui/js_gui_api_table_i.h +++ b/applications/system/js_app/modules/js_gui/js_gui_api_table_i.h @@ -1,4 +1,5 @@ #include "js_gui.h" static constexpr auto js_gui_api_table = sort(create_array_t( - API_METHOD(js_gui_make_view_factory, mjs_val_t, (struct mjs*, const JsViewDescriptor*)))); + API_METHOD(js_gui_make_view_factory, mjs_val_t, (struct mjs*, const JsViewDescriptor*)), + API_VARIABLE(js_gui_font_declaration, const JsValueDeclaration))); diff --git a/applications/system/js_app/modules/js_gui/menu.c b/applications/system/js_app/modules/js_gui/menu.c new file mode 100644 index 000000000..c8e23470f --- /dev/null +++ b/applications/system/js_app/modules/js_gui/menu.c @@ -0,0 +1,105 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include "js_gui.h" +#include "../js_event_loop/js_event_loop.h" +#include +#include + +typedef struct { + int32_t next_index; + StrBuffer str_buffer; + + FuriMessageQueue* queue; + JsEventLoopContract contract; +} JsMenuCtx; + +static mjs_val_t choose_transformer(struct mjs* mjs, FuriMessageQueue* queue, void* context) { + UNUSED(context); + uint32_t index; + furi_check(furi_message_queue_get(queue, &index, 0) == FuriStatusOk); + return mjs_mk_number(mjs, (double)index); +} + +static void choose_callback(void* context, uint32_t index) { + JsMenuCtx* ctx = context; + furi_check(furi_message_queue_put(ctx->queue, &index, 0) == FuriStatusOk); +} + +static bool + js_menu_add_child(struct mjs* mjs, Menu* menu, JsMenuCtx* context, mjs_val_t child_obj) { + static const JsValueDeclaration js_menu_string = JS_VALUE_SIMPLE(JsValueTypeString); + static const JsValueDeclaration js_menu_pointer = JS_VALUE_SIMPLE(JsValueTypeRawPointer); + + static const JsValueObjectField js_menu_child_fields[] = { + {"icon", &js_menu_pointer}, + {"label", &js_menu_string}, + }; + static const JsValueDeclaration js_menu_child = JS_VALUE_OBJECT(js_menu_child_fields); + + const Icon* icon; + const char* label; + JsValueParseStatus status; + JS_VALUE_PARSE( + mjs, + JS_VALUE_PARSE_SOURCE_VALUE(&js_menu_child), + JsValueParseFlagReturnOnError, + &status, + &child_obj, + &icon, + &label); + if(status != JsValueParseStatusOk) return false; + + menu_add_item( + menu, + str_buffer_make_owned_clone(&context->str_buffer, label), + icon, + context->next_index++, + choose_callback, + context); + + return true; +} + +static void js_menu_reset_children(Menu* menu, JsMenuCtx* context) { + context->next_index = 0; + menu_reset(menu); + str_buffer_clear_all_clones(&context->str_buffer); +} + +static JsMenuCtx* ctx_make(struct mjs* mjs, Menu* input, mjs_val_t view_obj) { + UNUSED(input); + JsMenuCtx* context = malloc(sizeof(JsMenuCtx)); + context->queue = furi_message_queue_alloc(1, sizeof(uint32_t)); + context->contract = (JsEventLoopContract){ + .magic = JsForeignMagic_JsEventLoopContract, + .object_type = JsEventLoopObjectTypeQueue, + .object = context->queue, + .non_timer = + { + .event = FuriEventLoopEventIn, + .transformer = (JsEventLoopTransformer)choose_transformer, + }, + }; + mjs_set(mjs, view_obj, "chosen", ~0, mjs_mk_foreign(mjs, &context->contract)); + return context; +} + +static void ctx_destroy(Menu* input, JsMenuCtx* context, FuriEventLoop* loop) { + UNUSED(input); + furi_event_loop_maybe_unsubscribe(loop, context->queue); + furi_message_queue_free(context->queue); + str_buffer_clear_all_clones(&context->str_buffer); + free(context); +} + +static const JsViewDescriptor view_descriptor = { + .alloc = (JsViewAlloc)menu_alloc, + .free = (JsViewFree)menu_free, + .get_view = (JsViewGetView)menu_get_view, + .custom_make = (JsViewCustomMake)ctx_make, + .custom_destroy = (JsViewCustomDestroy)ctx_destroy, + .add_child = (JsViewAddChild)js_menu_add_child, + .reset_children = (JsViewResetChildren)js_menu_reset_children, + .prop_cnt = 0, + .props = {}, +}; +JS_GUI_VIEW_DEF(menu, &view_descriptor); diff --git a/applications/system/js_app/modules/js_gui/number_input.c b/applications/system/js_app/modules/js_gui/number_input.c new file mode 100644 index 000000000..2a1aa6ee6 --- /dev/null +++ b/applications/system/js_app/modules/js_gui/number_input.c @@ -0,0 +1,130 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include "js_gui.h" +#include "../js_event_loop/js_event_loop.h" +#include + +typedef struct { + int32_t default_val, min_val, max_val; + FuriMessageQueue* input_queue; + JsEventLoopContract contract; +} JsNumKbdContext; + +static mjs_val_t + input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsNumKbdContext* context) { + UNUSED(context); + int32_t number; + furi_check(furi_message_queue_get(queue, &number, 0) == FuriStatusOk); + return mjs_mk_number(mjs, number); +} + +static void input_callback(void* ctx, int32_t value) { + JsNumKbdContext* context = ctx; + furi_check(furi_message_queue_put(context->input_queue, &value, 0) == FuriStatusOk); +} + +static bool header_assign( + struct mjs* mjs, + NumberInput* input, + JsViewPropValue value, + JsNumKbdContext* context) { + UNUSED(mjs); + UNUSED(context); + number_input_set_header_text(input, value.string); + return true; +} + +static bool min_val_assign( + struct mjs* mjs, + NumberInput* input, + JsViewPropValue value, + JsNumKbdContext* context) { + UNUSED(mjs); + context->min_val = value.number; + number_input_set_result_callback( + input, input_callback, context, context->default_val, context->min_val, context->max_val); + return true; +} + +static bool max_val_assign( + struct mjs* mjs, + NumberInput* input, + JsViewPropValue value, + JsNumKbdContext* context) { + UNUSED(mjs); + context->max_val = value.number; + number_input_set_result_callback( + input, input_callback, context, context->default_val, context->min_val, context->max_val); + return true; +} + +static bool default_val_assign( + struct mjs* mjs, + NumberInput* input, + JsViewPropValue value, + JsNumKbdContext* context) { + UNUSED(mjs); + context->default_val = value.number; + number_input_set_result_callback( + input, input_callback, context, context->default_val, context->min_val, context->max_val); + return true; +} + +static JsNumKbdContext* ctx_make(struct mjs* mjs, NumberInput* input, mjs_val_t view_obj) { + JsNumKbdContext* context = malloc(sizeof(JsNumKbdContext)); + *context = (JsNumKbdContext){ + .default_val = 0, + .max_val = 100, + .min_val = 0, + .input_queue = furi_message_queue_alloc(1, sizeof(int32_t)), + }; + context->contract = (JsEventLoopContract){ + .magic = JsForeignMagic_JsEventLoopContract, + .object_type = JsEventLoopObjectTypeQueue, + .object = context->input_queue, + .non_timer = + { + .event = FuriEventLoopEventIn, + .transformer = (JsEventLoopTransformer)input_transformer, + .transformer_context = context, + }, + }; + number_input_set_result_callback( + input, input_callback, context, context->default_val, context->min_val, context->max_val); + mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract)); + return context; +} + +static void ctx_destroy(NumberInput* input, JsNumKbdContext* context, FuriEventLoop* loop) { + UNUSED(input); + furi_event_loop_maybe_unsubscribe(loop, context->input_queue); + furi_message_queue_free(context->input_queue); + free(context); +} + +static const JsViewDescriptor view_descriptor = { + .alloc = (JsViewAlloc)number_input_alloc, + .free = (JsViewFree)number_input_free, + .get_view = (JsViewGetView)number_input_get_view, + .custom_make = (JsViewCustomMake)ctx_make, + .custom_destroy = (JsViewCustomDestroy)ctx_destroy, + .prop_cnt = 4, + .props = { + (JsViewPropDescriptor){ + .name = "header", + .type = JsViewPropTypeString, + .assign = (JsViewPropAssign)header_assign}, + (JsViewPropDescriptor){ + .name = "minValue", + .type = JsViewPropTypeNumber, + .assign = (JsViewPropAssign)min_val_assign}, + (JsViewPropDescriptor){ + .name = "maxValue", + .type = JsViewPropTypeNumber, + .assign = (JsViewPropAssign)max_val_assign}, + (JsViewPropDescriptor){ + .name = "defaultValue", + .type = JsViewPropTypeNumber, + .assign = (JsViewPropAssign)default_val_assign}, + }}; + +JS_GUI_VIEW_DEF(number_input, &view_descriptor); diff --git a/applications/system/js_app/modules/js_gui/popup.c b/applications/system/js_app/modules/js_gui/popup.c new file mode 100644 index 000000000..65f827ab1 --- /dev/null +++ b/applications/system/js_app/modules/js_gui/popup.c @@ -0,0 +1,102 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include "js_gui.h" +#include "../js_event_loop/js_event_loop.h" +#include +#include + +typedef struct { + StrBuffer str_buffer; + FuriSemaphore* semaphore; + JsEventLoopContract contract; +} JsPopupCtx; + +static void timeout_callback(JsPopupCtx* context) { + furi_check(furi_semaphore_release(context->semaphore) == FuriStatusOk); +} + +static bool + header_assign(struct mjs* mjs, Popup* popup, JsViewPropValue value, JsPopupCtx* context) { + UNUSED(mjs); + UNUSED(context); + popup_set_header( + popup, + str_buffer_make_owned_clone(&context->str_buffer, value.string), + 64, + 0, + AlignCenter, + AlignTop); + return true; +} + +static bool + text_assign(struct mjs* mjs, Popup* popup, JsViewPropValue value, JsPopupCtx* context) { + UNUSED(mjs); + UNUSED(context); + popup_set_text( + popup, + str_buffer_make_owned_clone(&context->str_buffer, value.string), + 64, + 32, + AlignCenter, + AlignCenter); + return true; +} + +static bool + timeout_assign(struct mjs* mjs, Popup* popup, JsViewPropValue value, JsPopupCtx* context) { + UNUSED(mjs); + UNUSED(context); + popup_set_timeout(popup, value.number); + popup_enable_timeout(popup); + return true; +} + +static JsPopupCtx* ctx_make(struct mjs* mjs, Popup* popup, mjs_val_t view_obj) { + JsPopupCtx* context = malloc(sizeof(JsPopupCtx)); + context->semaphore = furi_semaphore_alloc(1, 0); + context->contract = (JsEventLoopContract){ + .magic = JsForeignMagic_JsEventLoopContract, + .object_type = JsEventLoopObjectTypeSemaphore, + .object = context->semaphore, + .non_timer = + { + .event = FuriEventLoopEventIn, + }, + }; + mjs_set(mjs, view_obj, "timeout", ~0, mjs_mk_foreign(mjs, &context->contract)); + popup_set_callback(popup, (PopupCallback)timeout_callback); + popup_set_context(popup, context); + return context; +} + +static void ctx_destroy(Popup* popup, JsPopupCtx* context, FuriEventLoop* loop) { + UNUSED(popup); + furi_event_loop_maybe_unsubscribe(loop, context->semaphore); + furi_semaphore_free(context->semaphore); + str_buffer_clear_all_clones(&context->str_buffer); + free(context); +} + +static const JsViewDescriptor view_descriptor = { + .alloc = (JsViewAlloc)popup_alloc, + .free = (JsViewFree)popup_free, + .get_view = (JsViewGetView)popup_get_view, + .custom_make = (JsViewCustomMake)ctx_make, + .custom_destroy = (JsViewCustomDestroy)ctx_destroy, + .prop_cnt = 3, + .props = { + (JsViewPropDescriptor){ + .name = "header", + .type = JsViewPropTypeString, + .assign = (JsViewPropAssign)header_assign}, + (JsViewPropDescriptor){ + .name = "text", + .type = JsViewPropTypeString, + .assign = (JsViewPropAssign)text_assign}, + (JsViewPropDescriptor){ + .name = "timeout", + .type = JsViewPropTypeNumber, + .assign = (JsViewPropAssign)timeout_assign}, + }}; + +JS_GUI_VIEW_DEF(popup, &view_descriptor); diff --git a/applications/system/js_app/modules/js_gui/submenu.c b/applications/system/js_app/modules/js_gui/submenu.c index c142bcddb..64ab1b906 100644 --- a/applications/system/js_app/modules/js_gui/submenu.c +++ b/applications/system/js_app/modules/js_gui/submenu.c @@ -6,6 +6,7 @@ #define QUEUE_LEN 2 typedef struct { + int32_t next_index; FuriMessageQueue* queue; JsEventLoopContract contract; } JsSubmenuCtx; @@ -30,18 +31,24 @@ static bool return true; } -static bool items_assign(struct mjs* mjs, Submenu* submenu, JsViewPropValue value, void* context) { - UNUSED(mjs); - submenu_reset(submenu); - size_t len = mjs_array_length(mjs, value.term); - for(size_t i = 0; i < len; i++) { - mjs_val_t item = mjs_array_get(mjs, value.term, i); - if(!mjs_is_string(item)) return false; - submenu_add_item(submenu, mjs_get_string(mjs, &item, NULL), i, choose_callback, context); - } +static bool js_submenu_add_child( + struct mjs* mjs, + Submenu* submenu, + JsSubmenuCtx* context, + mjs_val_t child_obj) { + const char* str = mjs_get_string(mjs, &child_obj, NULL); + if(!str) return false; + + submenu_add_item(submenu, str, context->next_index++, choose_callback, context); + return true; } +static void js_submenu_reset_children(Submenu* submenu, JsSubmenuCtx* context) { + context->next_index = 0; + submenu_reset(submenu); +} + static JsSubmenuCtx* ctx_make(struct mjs* mjs, Submenu* input, mjs_val_t view_obj) { UNUSED(input); JsSubmenuCtx* context = malloc(sizeof(JsSubmenuCtx)); @@ -73,15 +80,13 @@ static const JsViewDescriptor view_descriptor = { .get_view = (JsViewGetView)submenu_get_view, .custom_make = (JsViewCustomMake)ctx_make, .custom_destroy = (JsViewCustomDestroy)ctx_destroy, - .prop_cnt = 2, + .add_child = (JsViewAddChild)js_submenu_add_child, + .reset_children = (JsViewResetChildren)js_submenu_reset_children, + .prop_cnt = 1, .props = { (JsViewPropDescriptor){ .name = "header", .type = JsViewPropTypeString, .assign = (JsViewPropAssign)header_assign}, - (JsViewPropDescriptor){ - .name = "items", - .type = JsViewPropTypeArr, - .assign = (JsViewPropAssign)items_assign}, }}; JS_GUI_VIEW_DEF(submenu, &view_descriptor); diff --git a/applications/system/js_app/modules/js_gui/vi_list.c b/applications/system/js_app/modules/js_gui/vi_list.c new file mode 100644 index 000000000..bdea2fba1 --- /dev/null +++ b/applications/system/js_app/modules/js_gui/vi_list.c @@ -0,0 +1,163 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include "js_gui.h" +#include "../js_event_loop/js_event_loop.h" +#include +#include + +typedef struct { + StrBuffer str_buffer; + + // let mjs do the memory management heavy lifting, store children in a js array + struct mjs* mjs; + mjs_val_t children; + VariableItemList* list; + + FuriMessageQueue* input_queue; + JsEventLoopContract contract; +} JsViListContext; + +typedef struct { + int32_t item_index; + int32_t value_index; +} JsViListEvent; + +static mjs_val_t + input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsViListContext* context) { + UNUSED(context); + JsViListEvent event; + furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk); + + mjs_val_t event_obj = mjs_mk_object(mjs); + JS_ASSIGN_MULTI(mjs, event_obj) { + JS_FIELD("itemIndex", mjs_mk_number(mjs, event.item_index)); + JS_FIELD("valueIndex", mjs_mk_number(mjs, event.value_index)); + } + + return event_obj; +} + +static void js_vi_list_change_callback(VariableItem* item) { + JsViListContext* context = variable_item_get_context(item); + struct mjs* mjs = context->mjs; + uint8_t item_index = variable_item_list_get_selected_item_index(context->list); + uint8_t value_index = variable_item_get_current_value_index(item); + + // type safety ensured in add_child + mjs_val_t variants = mjs_array_get(mjs, context->children, item_index); + mjs_val_t variant = mjs_array_get(mjs, variants, value_index); + variable_item_set_current_value_text(item, mjs_get_string(mjs, &variant, NULL)); + + JsViListEvent event = { + .item_index = item_index, + .value_index = value_index, + }; + furi_check(furi_message_queue_put(context->input_queue, &event, 0) == FuriStatusOk); +} + +static bool js_vi_list_add_child( + struct mjs* mjs, + VariableItemList* list, + JsViListContext* context, + mjs_val_t child_obj) { + static const JsValueDeclaration js_vi_list_string = JS_VALUE_SIMPLE(JsValueTypeString); + static const JsValueDeclaration js_vi_list_arr = JS_VALUE_SIMPLE(JsValueTypeAnyArray); + static const JsValueDeclaration js_vi_list_int_default_0 = + JS_VALUE_SIMPLE_W_DEFAULT(JsValueTypeInt32, int32_val, 0); + + static const JsValueObjectField js_vi_list_child_fields[] = { + {"label", &js_vi_list_string}, + {"variants", &js_vi_list_arr}, + {"defaultSelected", &js_vi_list_int_default_0}, + }; + static const JsValueDeclaration js_vi_list_child = + JS_VALUE_OBJECT_W_DEFAULTS(js_vi_list_child_fields); + + JsValueParseStatus status; + const char* label; + mjs_val_t variants; + int32_t default_selected; + JS_VALUE_PARSE( + mjs, + JS_VALUE_PARSE_SOURCE_VALUE(&js_vi_list_child), + JsValueParseFlagReturnOnError, + &status, + &child_obj, + &label, + &variants, + &default_selected); + if(status != JsValueParseStatusOk) return false; + + size_t variants_cnt = mjs_array_length(mjs, variants); + for(size_t i = 0; i < variants_cnt; i++) + if(!mjs_is_string(mjs_array_get(mjs, variants, i))) return false; + + VariableItem* item = variable_item_list_add( + list, + str_buffer_make_owned_clone(&context->str_buffer, label), + variants_cnt, + js_vi_list_change_callback, + context); + variable_item_set_current_value_index(item, default_selected); + mjs_val_t default_variant = mjs_array_get(mjs, variants, default_selected); + variable_item_set_current_value_text(item, mjs_get_string(mjs, &default_variant, NULL)); + + mjs_array_push(context->mjs, context->children, variants); + + return true; +} + +static void js_vi_list_reset_children(VariableItemList* list, JsViListContext* context) { + mjs_disown(context->mjs, &context->children); + context->children = mjs_mk_array(context->mjs); + mjs_own(context->mjs, &context->children); + + variable_item_list_reset(list); + str_buffer_clear_all_clones(&context->str_buffer); +} + +static JsViListContext* ctx_make(struct mjs* mjs, VariableItemList* list, mjs_val_t view_obj) { + JsViListContext* context = malloc(sizeof(JsViListContext)); + *context = (JsViListContext){ + .str_buffer = {0}, + .mjs = mjs, + .children = mjs_mk_array(mjs), + .list = list, + .input_queue = furi_message_queue_alloc(1, sizeof(JsViListEvent)), + }; + mjs_own(context->mjs, &context->children); + context->contract = (JsEventLoopContract){ + .magic = JsForeignMagic_JsEventLoopContract, + .object_type = JsEventLoopObjectTypeQueue, + .object = context->input_queue, + .non_timer = + { + .event = FuriEventLoopEventIn, + .transformer = (JsEventLoopTransformer)input_transformer, + .transformer_context = context, + }, + }; + mjs_set(mjs, view_obj, "valueUpdate", ~0, mjs_mk_foreign(mjs, &context->contract)); + return context; +} + +static void ctx_destroy(VariableItemList* input, JsViListContext* context, FuriEventLoop* loop) { + UNUSED(input); + furi_event_loop_maybe_unsubscribe(loop, context->input_queue); + furi_message_queue_free(context->input_queue); + str_buffer_clear_all_clones(&context->str_buffer); + free(context); +} + +static const JsViewDescriptor view_descriptor = { + .alloc = (JsViewAlloc)variable_item_list_alloc, + .free = (JsViewFree)variable_item_list_free, + .get_view = (JsViewGetView)variable_item_list_get_view, + .custom_make = (JsViewCustomMake)ctx_make, + .custom_destroy = (JsViewCustomDestroy)ctx_destroy, + .add_child = (JsViewAddChild)js_vi_list_add_child, + .reset_children = (JsViewResetChildren)js_vi_list_reset_children, + .prop_cnt = 0, + .props = {}, +}; + +JS_GUI_VIEW_DEF(vi_list, &view_descriptor); diff --git a/applications/system/js_app/packages/fz-sdk/gui/button_menu.d.ts b/applications/system/js_app/packages/fz-sdk/gui/button_menu.d.ts new file mode 100644 index 000000000..3b5dd2359 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/button_menu.d.ts @@ -0,0 +1,40 @@ +/** + * Displays a list of buttons. + * + * Sample screenshot of the view + * + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * let buttonMenuView = require("gui/button_menu"); + * ``` + * + * This module depends on the `gui` module, which in turn depends on the + * `event_loop` module, so they _must_ be imported in this order. It is also + * recommended to conceptualize these modules first before using this one. + * + * # Example + * For an example refer to the `gui.js` example script. + * + * # View props + * - `header`: Textual header above the buttons + * + * @version Added in JS SDK 0.4 + * @module + */ + +import type { View, ViewFactory, InputType } from "."; +import type { Contract } from "../event_loop"; + +type Props = { + header: string; +}; + +type Child = { type: "common" | "control", label: string }; + +declare class ButtonMenu extends View { + input: Contract<{ index: number, type: InputType }>; +} +declare class ButtonMenuFactory extends ViewFactory { } +declare const factory: ButtonMenuFactory; +export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/gui/button_panel.d.ts b/applications/system/js_app/packages/fz-sdk/gui/button_panel.d.ts new file mode 100644 index 000000000..dcc956fed --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/button_panel.d.ts @@ -0,0 +1,49 @@ +/** + * Displays a button matrix. + * + * Sample screenshot of the view + * + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * let buttonPanelView = require("gui/button_panel"); + * ``` + * + * This module depends on the `gui` module, which in turn depends on the + * `event_loop` module, so they _must_ be imported in this order. It is also + * recommended to conceptualize these modules first before using this one. + * + * # Example + * For an example refer to the `gui.js` example script. + * + * # View props + * - `matrixSizeX`: Width of imaginary grid used for navigation + * - `matrixSizeY`: Height of imaginary grid used for navigation + * + * @version Added in JS SDK 0.4 + * @module + */ + +import type { View, ViewFactory, Font, InputType } from "."; +import type { Contract } from "../event_loop"; +import { IconData } from "./icon"; + +type Props = { + matrixSizeX: number, + matrixSizeY: number, +}; + +type Position = { x: number, y: number }; + +type ButtonChild = { type: "button", matrixX: number, matrixY: number, icon: IconData, iconSelected: IconData } & Position; +type LabelChild = { type: "label", font: Font, text: string } & Position; +type IconChild = { type: "icon", icon: IconData }; + +type Child = ButtonChild | LabelChild | IconChild; + +declare class ButtonPanel extends View { + input: Contract<{ index: number, type: InputType }>; +} +declare class ButtonPanelFactory extends ViewFactory { } +declare const factory: ButtonPanelFactory; +export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/gui/icon.d.ts b/applications/system/js_app/packages/fz-sdk/gui/icon.d.ts index 03fc8c53b..8f6e69311 100644 --- a/applications/system/js_app/packages/fz-sdk/gui/icon.d.ts +++ b/applications/system/js_app/packages/fz-sdk/gui/icon.d.ts @@ -1,4 +1,7 @@ -export type BuiltinIcon = "DolphinWait_59x54" | "js_script_10px"; +export type BuiltinIcon = "DolphinWait_59x54" | "js_script_10px" + | "off_19x20" | "off_hover_19x20" + | "power_19x20" | "power_hover_19x20" + | "Settings_14"; export type IconData = symbol & { "__tag__": "icon" }; // introducing a nominal type in a hacky way; the `__tag__` property doesn't really exist. diff --git a/applications/system/js_app/packages/fz-sdk/gui/index.d.ts b/applications/system/js_app/packages/fz-sdk/gui/index.d.ts index 77e5891cd..1c1d14a79 100644 --- a/applications/system/js_app/packages/fz-sdk/gui/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/gui/index.d.ts @@ -24,24 +24,23 @@ * ### View * In Flipper's terminology, a "View" is a fullscreen design element that * assumes control over the entire viewport and all input events. Different - * types of views are available (not all of which are unfortunately currently - * implemented in JS): + * types of views are available: * | View | Has JS adapter? | * |----------------------|-----------------------| - * | `button_menu` | ❌ | - * | `button_panel` | ❌ | + * | `button_menu` | ✅ | + * | `button_panel` | ✅ | * | `byte_input` | ✅ | * | `dialog_ex` | ✅ (as `dialog`) | * | `empty_screen` | ✅ | * | `file_browser` | ✅ (as `file_picker`) | * | `loading` | ✅ | - * | `menu` | ❌ | - * | `number_input` | ❌ | - * | `popup` | ❌ | + * | `menu` | ✅ | + * | `number_input` | ✅ | + * | `popup` | ✅ | * | `submenu` | ✅ | * | `text_box` | ✅ | * | `text_input` | ✅ | - * | `variable_item_list` | ❌ | + * | `variable_item_list` | ✅ (as `vi_list`) | * | `widget` | ✅ | * * In JS, each view has its own set of properties (or just "props"). The @@ -119,6 +118,9 @@ import type { Contract } from "../event_loop"; +export type Font = "primary" | "secondary" | "keyboard" | "big_numbers"; +export type InputType = "press" | "release" | "short" | "long" | "repeat"; + type Properties = { [K: string]: any }; export declare class View { diff --git a/applications/system/js_app/packages/fz-sdk/gui/menu.d.ts b/applications/system/js_app/packages/fz-sdk/gui/menu.d.ts new file mode 100644 index 000000000..a72fe4b77 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/menu.d.ts @@ -0,0 +1,38 @@ +/** + * A list of selectable entries consisting of an icon and a label. + * + * Sample screenshot of the view + * + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * let submenuView = require("gui/menu"); + * ``` + * + * This module depends on the `gui` module, which in turn depends on the + * `event_loop` module, so they _must_ be imported in this order. It is also + * recommended to conceptualize these modules first before using this one. + * + * # Example + * For an example refer to the GUI example. + * + * # View props + * This view doesn't have any props. + * + * @version Added in JS SDK 0.1 + * @version API changed in JS SDK 0.4 + * @module + */ + +import type { View, ViewFactory } from "."; +import type { Contract } from "../event_loop"; +import type { IconData } from "./icon"; + +type Props = {}; +type Child = { icon: IconData, label: string }; +declare class Submenu extends View { + chosen: Contract; +} +declare class SubmenuFactory extends ViewFactory { } +declare const factory: SubmenuFactory; +export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/gui/number_input.d.ts b/applications/system/js_app/packages/fz-sdk/gui/number_input.d.ts new file mode 100644 index 000000000..4882c85fc --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/number_input.d.ts @@ -0,0 +1,44 @@ +/** + * Displays a number input keyboard. + * + * Sample screenshot of the view + * + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * let numberInputView = require("gui/number_input"); + * ``` + * + * This module depends on the `gui` module, which in turn depends on the + * `event_loop` module, so they _must_ be imported in this order. It is also + * recommended to conceptualize these modules first before using this one. + * + * # Example + * For an example refer to the `gui.js` example script. + * + * # View props + * - `header`: Text displayed at the top of the screen + * - `minValue`: Minimum allowed numeric value + * - `maxValue`: Maximum allowed numeric value + * - `defaultValue`: Default numeric value + * + * @version Added in JS SDK 0.4 + * @module + */ + +import type { View, ViewFactory } from "."; +import type { Contract } from "../event_loop"; + +type Props = { + header: string, + minValue: number, + maxValue: number, + defaultValue: number, +} +type Child = never; +declare class NumberInput extends View { + input: Contract; +} +declare class NumberInputFactory extends ViewFactory { } +declare const factory: NumberInputFactory; +export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/gui/popup.d.ts b/applications/system/js_app/packages/fz-sdk/gui/popup.d.ts new file mode 100644 index 000000000..7f77b79f8 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/popup.d.ts @@ -0,0 +1,43 @@ +/** + * Like a Dialog, but with a built-in timer. + * + * Sample screenshot of the view + * + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * let popupView = require("gui/popup"); + * ``` + * + * This module depends on the `gui` module, which in turn depends on the + * `event_loop` module, so they _must_ be imported in this order. It is also + * recommended to conceptualize these modules first before using this one. + * + * # Example + * For an example refer to the `gui.js` example script. + * + * # View props + * - `header`: Text displayed in bold at the top of the screen + * - `text`: Text displayed in the middle of the string + * - `timeout`: Timeout, in milliseconds, after which the event will fire. The + * timer starts counting down when this property is assigned. + * + * @version Added in JS SDK 0.1 + * @module + */ + +import type { View, ViewFactory } from "."; +import type { Contract } from "../event_loop"; + +type Props = { + header: string, + text: string, + timeout: number, +} +type Child = never; +declare class Popup extends View { + timeout: Contract; +} +declare class PopupFactory extends ViewFactory { } +declare const factory: PopupFactory; +export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/gui/submenu.d.ts b/applications/system/js_app/packages/fz-sdk/gui/submenu.d.ts index e73856bee..9fb8d51cf 100644 --- a/applications/system/js_app/packages/fz-sdk/gui/submenu.d.ts +++ b/applications/system/js_app/packages/fz-sdk/gui/submenu.d.ts @@ -18,9 +18,9 @@ * * # View props * - `header`: Text displayed at the top of the screen in bold - * - `items`: Array of selectable textual items * * @version Added in JS SDK 0.1 + * @version API changed in JS SDK 0.4 * @module */ @@ -29,9 +29,8 @@ import type { Contract } from "../event_loop"; type Props = { header: string, - items: string[], }; -type Child = never; +type Child = string; declare class Submenu extends View { chosen: Contract; } diff --git a/applications/system/js_app/packages/fz-sdk/gui/vi_list.d.ts b/applications/system/js_app/packages/fz-sdk/gui/vi_list.d.ts new file mode 100644 index 000000000..73826338c --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/vi_list.d.ts @@ -0,0 +1,38 @@ +/** + * Displays a list of settings-like variable items. + * + * Sample screenshot of the view + * + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * let viListView = require("gui/vi_list"); + * ``` + * + * This module depends on the `gui` module, which in turn depends on the + * `event_loop` module, so they _must_ be imported in this order. It is also + * recommended to conceptualize these modules first before using this one. + * + * # Example + * For an example refer to the `gui.js` example script. + * + * # View props + * This view doesn't have any props + * + * @version Added in JS SDK 0.4 + * @module + */ + +import type { View, ViewFactory } from "."; +import type { Contract } from "../event_loop"; + +type Props = {}; + +type Child = { label: string, variants: string[] }; + +declare class ViList extends View { + valueUpdate: Contract<{ itemIndex: number, valueIndex: number }>; +} +declare class ViListFactory extends ViewFactory { } +declare const factory: ViListFactory; +export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/package.json b/applications/system/js_app/packages/fz-sdk/package.json index 020f3d45c..aa504008b 100644 --- a/applications/system/js_app/packages/fz-sdk/package.json +++ b/applications/system/js_app/packages/fz-sdk/package.json @@ -24,4 +24,4 @@ "prompts": "^2.4.2", "serialport": "^12.0.0" } -} \ No newline at end of file +} diff --git a/lib/toolbox/SConscript b/lib/toolbox/SConscript index ad368e2a1..eb92c9c00 100644 --- a/lib/toolbox/SConscript +++ b/lib/toolbox/SConscript @@ -29,6 +29,7 @@ env.Append( File("float_tools.h"), File("value_index.h"), File("tar/tar_archive.h"), + File("str_buffer.h"), File("stream/stream.h"), File("stream/file_stream.h"), File("stream/string_stream.h"), diff --git a/lib/toolbox/str_buffer.c b/lib/toolbox/str_buffer.c new file mode 100644 index 000000000..a46a3f27b --- /dev/null +++ b/lib/toolbox/str_buffer.c @@ -0,0 +1,18 @@ +#include "str_buffer.h" + +const char* str_buffer_make_owned_clone(StrBuffer* buffer, const char* str) { + char* owned = strdup(str); + buffer->n_owned_strings++; + buffer->owned_strings = + realloc(buffer->owned_strings, buffer->n_owned_strings * sizeof(const char*)); // -V701 + buffer->owned_strings[buffer->n_owned_strings - 1] = owned; + return owned; +} + +void str_buffer_clear_all_clones(StrBuffer* buffer) { + for(size_t i = 0; i < buffer->n_owned_strings; i++) { + free(buffer->owned_strings[i]); + } + free(buffer->owned_strings); + buffer->owned_strings = NULL; +} diff --git a/lib/toolbox/str_buffer.h b/lib/toolbox/str_buffer.h new file mode 100644 index 000000000..9623065c8 --- /dev/null +++ b/lib/toolbox/str_buffer.h @@ -0,0 +1,47 @@ +/** + * @file str_buffer.h + * + * Allows you to create an owned clone of however many strings that you need, + * then free all of them at once. Essentially the simplest possible append-only + * unindexable array of owned C-style strings. + */ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief StrBuffer instance + * + * Place this struct directly wherever you want, it doesn't have to be `alloc`ed + * and `free`d. + */ +typedef struct { + char** owned_strings; + size_t n_owned_strings; +} StrBuffer; + +/** + * @brief Makes a owned duplicate of the provided string + * + * @param[in] buffer StrBuffer instance + * @param[in] str Input C-style string + * + * @returns C-style string that contains to be valid event after `str` becomes + * invalid + */ +const char* str_buffer_make_owned_clone(StrBuffer* buffer, const char* str); + +/** + * @brief Clears all owned duplicates + * + * @param[in] buffer StrBuffer instance + */ +void str_buffer_clear_all_clones(StrBuffer* buffer); + +#ifdef __cplusplus +} +#endif diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 2bbf81e7a..0590a16b8 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -173,6 +173,7 @@ Header,+,lib/toolbox/protocols/protocol_dict.h,, Header,+,lib/toolbox/pulse_protocols/pulse_glue.h,, Header,+,lib/toolbox/saved_struct.h,, Header,+,lib/toolbox/simple_array.h,, +Header,+,lib/toolbox/str_buffer.h,, Header,+,lib/toolbox/stream/buffered_file_stream.h,, Header,+,lib/toolbox/stream/file_stream.h,, Header,+,lib/toolbox/stream/stream.h,, @@ -2632,6 +2633,8 @@ Function,+,storage_simply_remove,_Bool,"Storage*, const char*" Function,+,storage_simply_remove_recursive,_Bool,"Storage*, const char*" Function,-,stpcpy,char*,"char*, const char*" Function,-,stpncpy,char*,"char*, const char*, size_t" +Function,+,str_buffer_clear_all_clones,void,StrBuffer* +Function,+,str_buffer_make_owned_clone,const char*,"StrBuffer*, const char*" Function,+,strcasecmp,int,"const char*, const char*" Function,-,strcasecmp_l,int,"const char*, const char*, locale_t" Function,+,strcasestr,char*,"const char*, const char*" diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 2370133d6..1169749cd 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -245,6 +245,7 @@ Header,+,lib/toolbox/protocols/protocol_dict.h,, Header,+,lib/toolbox/pulse_protocols/pulse_glue.h,, Header,+,lib/toolbox/saved_struct.h,, Header,+,lib/toolbox/simple_array.h,, +Header,+,lib/toolbox/str_buffer.h,, Header,+,lib/toolbox/stream/buffered_file_stream.h,, Header,+,lib/toolbox/stream/file_stream.h,, Header,+,lib/toolbox/stream/stream.h,, @@ -3322,6 +3323,8 @@ Function,+,storage_simply_remove,_Bool,"Storage*, const char*" Function,+,storage_simply_remove_recursive,_Bool,"Storage*, const char*" Function,-,stpcpy,char*,"char*, const char*" Function,-,stpncpy,char*,"char*, const char*, size_t" +Function,+,str_buffer_clear_all_clones,void,StrBuffer* +Function,+,str_buffer_make_owned_clone,const char*,"StrBuffer*, const char*" Function,+,strcasecmp,int,"const char*, const char*" Function,-,strcasecmp_l,int,"const char*, const char*, locale_t" Function,+,strcasestr,char*,"const char*, const char*"