From bafd4e89c93d85585b7dbb9ff8d066283e9a1023 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 9 Mar 2026 00:43:08 +0300 Subject: [PATCH 01/71] upd changelog apps sync --- CHANGELOG.md | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6956a8e5d..aca368515 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,21 +1,8 @@ ## Main changes - Current API: 87.6 -* SubGHz: Add **Ditec GOL4** 54bit dynamic protocol (with programming mode, button switch, add manually) (by @xMasterX (MMX) & @zero-mega) -* SubGHz: Add **KeyFinder** 24bit static protocol (thx @mishamyte for RAWs) -* SubGHz: **Fix transmission bug on Read screen** -* SubGHz: **BFT Mitto fix decode bug** (seed was not resetting after one successful decode) -* SubGHz: **Somfy Keytis** button switch and **Add Manually support** -* SubGHz: **KeeLoq** change delta size -* SubGHz: **Genius Echo/Bravo** add 2 buttons hold simulation (0xB btn code) -* SubGHz: Signal **Settings Improvements** (PR #968 | by @Dmitry422) -* SubGHz: KeeLoq **fix display** of **AN-Motors** and **HCS101** keys -* OFW PR 4343: LFRFID: add Indala 224-bit (long format) protocol support (by @kuzaxak) -* OFW PR 4297: LFRFID: Make FDX-B readout more descriptive (by @snowsign) -* MFKey: Update to v4.1 (by @noproto & @dchristle) -* Apps: Build tag (**22feb2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) +* Apps: Build tag (**9mar2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes -* OFW PR 4320: API: Make view_port_send_to_back public (by @loftyinclination) -* OFW PR 4338: HID: Fix USB HID keyboard LED state reporting (by @Caballosanex) +* None yet

#### Known NFC post-refactor regressions list: - Mifare Mini clones reading is broken (original mini working fine) (OFW) From 834e338273b5b391d772f3b13c59ffa97b2b16a7 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 9 Mar 2026 02:28:08 +0300 Subject: [PATCH 02/71] Extend lcd contrast range to full ST756x 6-bit range by ShaTie --- .../notification_settings_app.c | 45 +++++-------------- 1 file changed, 10 insertions(+), 35 deletions(-) diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index e82b02c87..bb4563227 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -24,44 +24,19 @@ static const NotificationSequence sequence_note_c = { NULL, }; -#define CONTRAST_COUNT 17 +#define CONTRAST_COUNT 64 const char* const contrast_text[CONTRAST_COUNT] = { - "-8", - "-7", - "-6", - "-5", - "-4", - "-3", - "-2", - "-1", - "0", - "+1", - "+2", - "+3", - "+4", - "+5", - "+6", - "+7", - "+8", + "-32", "-31", "-30", "-29", "-28", "-27", "-26", "-25", "-24", "-23", "-22", "-21", "-20", + "-19", "-18", "-17", "-16", "-15", "-14", "-13", "-12", "-11", "-10", "-9", "-8", "-7", + "-6", "-5", "-4", "-3", "-2", "-1", "0", "+1", "+2", "+3", "+4", "+5", "+6", + "+7", "+8", "+9", "+10", "+11", "+12", "+13", "+14", "+15", "+16", "+17", "+18", "+19", + "+20", "+21", "+22", "+23", "+24", "+25", "+26", "+27", "+28", "+29", "+30", "+31", }; const int32_t contrast_value[CONTRAST_COUNT] = { - -8, - -7, - -6, - -5, - -4, - -3, - -2, - -1, - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, + -32, -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, + -16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, }; #define BACKLIGHT_COUNT 21 From 9041334b425291b73913497890787303ca00976f Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 9 Mar 2026 02:36:40 +0300 Subject: [PATCH 03/71] NFC: Add Mifare Ultralight C Write Support by haw8411 --- .../mf_ultralight/mf_ultralight.c | 171 +++++++++++++++++- applications/main/nfc/nfc_app_i.h | 13 ++ .../nfc_scene_mf_ultralight_c_dict_attack.c | 12 ++ .../protocols/mf_ultralight/mf_ultralight.c | 6 +- .../mf_ultralight/mf_ultralight_listener.c | 22 ++- .../mf_ultralight/mf_ultralight_poller.c | 118 +++++++++++- .../mf_ultralight/mf_ultralight_poller.h | 2 + .../mf_ultralight/mf_ultralight_poller_i.h | 2 + 8 files changed, 329 insertions(+), 17 deletions(-) diff --git a/applications/main/nfc/helpers/protocol_support/mf_ultralight/mf_ultralight.c b/applications/main/nfc/helpers/protocol_support/mf_ultralight/mf_ultralight.c index 5a0c88cf7..642f9b460 100644 --- a/applications/main/nfc/helpers/protocol_support/mf_ultralight/mf_ultralight.c +++ b/applications/main/nfc/helpers/protocol_support/mf_ultralight/mf_ultralight.c @@ -14,7 +14,9 @@ enum { SubmenuIndexUnlock = SubmenuIndexCommonMax, SubmenuIndexUnlockByReader, SubmenuIndexUnlockByPassword, - SubmenuIndexDictAttack + SubmenuIndexDictAttack, + SubmenuIndexWriteKeepKey, // ULC: write data pages, keep target card's existing key + SubmenuIndexWriteCopyKey, // ULC: write all pages including key from source card }; enum { @@ -214,8 +216,26 @@ static void nfc_scene_read_and_saved_menu_on_enter_mf_ultralight(NfcApp* instanc if(is_locked || (data->type != MfUltralightTypeNTAG213 && data->type != MfUltralightTypeNTAG215 && data->type != MfUltralightTypeNTAG216 && data->type != MfUltralightTypeUL11 && - data->type != MfUltralightTypeUL21 && data->type != MfUltralightTypeOrigin)) { + data->type != MfUltralightTypeUL21 && data->type != MfUltralightTypeOrigin && + data->type != MfUltralightTypeMfulC)) { submenu_remove_item(submenu, SubmenuIndexCommonWrite); + } else if(data->type == MfUltralightTypeMfulC) { + // Replace the generic Write item with two ULC-specific options so the user + // can choose whether to keep or overwrite the target card's 3DES key. + // This avoids any mid-write dialog/view-switching complexity entirely. + submenu_remove_item(submenu, SubmenuIndexCommonWrite); + submenu_add_item( + submenu, + "Write (Keep Key)", + SubmenuIndexWriteKeepKey, + nfc_protocol_support_common_submenu_callback, + instance); + submenu_add_item( + submenu, + "Write (Copy Key)", + SubmenuIndexWriteCopyKey, + nfc_protocol_support_common_submenu_callback, + instance); } if(is_locked) { @@ -291,6 +311,14 @@ static bool nfc_scene_read_and_saved_menu_on_event_mf_ultralight( scene_manager_next_scene(instance->scene_manager, NfcSceneMfUltralightCDictAttack); } consumed = true; + } else if(event.event == SubmenuIndexWriteKeepKey) { + instance->mf_ultralight_c_write_context.copy_key = false; + scene_manager_next_scene(instance->scene_manager, NfcSceneWrite); + consumed = true; + } else if(event.event == SubmenuIndexWriteCopyKey) { + instance->mf_ultralight_c_write_context.copy_key = true; + scene_manager_next_scene(instance->scene_manager, NfcSceneWrite); + consumed = true; } } return consumed; @@ -307,12 +335,139 @@ static NfcCommand if(mf_ultralight_event->type == MfUltralightPollerEventTypeRequestMode) { mf_ultralight_event->data->poller_mode = MfUltralightPollerModeWrite; furi_string_reset(instance->text_box_store); + if(instance->mf_ultralight_c_dict_context.dict) { + keys_dict_free(instance->mf_ultralight_c_dict_context.dict); + } + instance->mf_ultralight_c_dict_context.dict = NULL; + instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictIdle; view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventCardDetected); } else if(mf_ultralight_event->type == MfUltralightPollerEventTypeAuthRequest) { + // Skip auth during the read phase of write - we'll authenticate + // against the target card in RequestWriteData using source key or dict attack mf_ultralight_event->data->auth_context.skip_auth = true; + } else if(mf_ultralight_event->type == MfUltralightPollerEventTypeRequestKey) { + // Dict attack key provider - user dict first, then system dict + if(!instance->mf_ultralight_c_dict_context.dict && + instance->mf_ultralight_c_write_context.dict_state == NfcMfUltralightCWriteDictIdle) { + if(keys_dict_check_presence(NFC_APP_MF_ULTRALIGHT_C_DICT_USER_PATH)) { + instance->mf_ultralight_c_dict_context.dict = keys_dict_alloc( + NFC_APP_MF_ULTRALIGHT_C_DICT_USER_PATH, + KeysDictModeOpenExisting, + sizeof(MfUltralightC3DesAuthKey)); + instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictUser; + } + if(!instance->mf_ultralight_c_dict_context.dict) { + instance->mf_ultralight_c_dict_context.dict = keys_dict_alloc( + NFC_APP_MF_ULTRALIGHT_C_DICT_SYSTEM_PATH, + KeysDictModeOpenExisting, + sizeof(MfUltralightC3DesAuthKey)); + instance->mf_ultralight_c_write_context.dict_state = + NfcMfUltralightCWriteDictSystem; + } + } + MfUltralightC3DesAuthKey key = {}; + bool got_key = false; + if(instance->mf_ultralight_c_dict_context.dict) { + got_key = keys_dict_get_next_key( + instance->mf_ultralight_c_dict_context.dict, + key.data, + sizeof(MfUltralightC3DesAuthKey)); + } + if(!got_key && + instance->mf_ultralight_c_write_context.dict_state == NfcMfUltralightCWriteDictUser) { + // Exhausted user dict, switch to system dict + if(instance->mf_ultralight_c_dict_context.dict) { + keys_dict_free(instance->mf_ultralight_c_dict_context.dict); + } + instance->mf_ultralight_c_dict_context.dict = keys_dict_alloc( + NFC_APP_MF_ULTRALIGHT_C_DICT_SYSTEM_PATH, + KeysDictModeOpenExisting, + sizeof(MfUltralightC3DesAuthKey)); + instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictSystem; + if(instance->mf_ultralight_c_dict_context.dict) { + got_key = keys_dict_get_next_key( + instance->mf_ultralight_c_dict_context.dict, + key.data, + sizeof(MfUltralightC3DesAuthKey)); + } + } + if(got_key) { + mf_ultralight_event->data->key_request_data.key = key; + mf_ultralight_event->data->key_request_data.key_provided = true; + FURI_LOG_D( + "MfULC", + "Trying dict key: " + "%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", + key.data[0], + key.data[1], + key.data[2], + key.data[3], + key.data[4], + key.data[5], + key.data[6], + key.data[7], + key.data[8], + key.data[9], + key.data[10], + key.data[11], + key.data[12], + key.data[13], + key.data[14], + key.data[15]); + } else { + mf_ultralight_event->data->key_request_data.key_provided = false; + FURI_LOG_D("MfULC", "Dict exhausted - no more keys"); + if(instance->mf_ultralight_c_dict_context.dict) { + keys_dict_free(instance->mf_ultralight_c_dict_context.dict); + instance->mf_ultralight_c_dict_context.dict = NULL; + } + instance->mf_ultralight_c_write_context.dict_state = + NfcMfUltralightCWriteDictExhausted; + } } else if(mf_ultralight_event->type == MfUltralightPollerEventTypeRequestWriteData) { mf_ultralight_event->data->write_data = nfc_device_get_data(instance->nfc_device, NfcProtocolMfUltralight); + // Reset dict context so RequestKey starts fresh for the write-phase auth + if(instance->mf_ultralight_c_dict_context.dict) { + keys_dict_free(instance->mf_ultralight_c_dict_context.dict); + instance->mf_ultralight_c_dict_context.dict = NULL; + } + instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictIdle; + } else if(mf_ultralight_event->type == MfUltralightPollerEventTypeWriteKeyRequest) { + // Apply the user's key choice - read from static, not scene state (scene manager + // resets state to 0 on scene entry, wiping any value set before next_scene). + bool keep_key = !instance->mf_ultralight_c_write_context.copy_key; + mf_ultralight_event->data->write_key_skip = keep_key; + + if(mf_ultralight_event->data->key_request_data.key_provided) { + MfUltralightC3DesAuthKey found_key = mf_ultralight_event->data->key_request_data.key; + FURI_LOG_D( + "MfULC", + "WriteKeyRequest: target key = " + "%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", + found_key.data[0], + found_key.data[1], + found_key.data[2], + found_key.data[3], + found_key.data[4], + found_key.data[5], + found_key.data[6], + found_key.data[7], + found_key.data[8], + found_key.data[9], + found_key.data[10], + found_key.data[11], + found_key.data[12], + found_key.data[13], + found_key.data[14], + found_key.data[15]); + } + FURI_LOG_D( + "MfULC", + "WriteKeyRequest: decision = %s (copy_key=%d)", + keep_key ? "KEEP target key (pages 44-47 NOT written)" : + "OVERWRITE with source key (pages 44-47 WILL be written)", + (int)instance->mf_ultralight_c_write_context.copy_key); } else if(mf_ultralight_event->type == MfUltralightPollerEventTypeCardMismatch) { furi_string_set(instance->text_box_store, "Card of the same\ntype should be\n presented"); view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventWrongCard); @@ -323,6 +478,7 @@ static NfcCommand view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventPollerFailure); command = NfcCommandStop; } else if(mf_ultralight_event->type == MfUltralightPollerEventTypeWriteFail) { + view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventPollerFailure); command = NfcCommandStop; } else if(mf_ultralight_event->type == MfUltralightPollerEventTypeWriteSuccess) { furi_string_reset(instance->text_box_store); @@ -334,9 +490,18 @@ static NfcCommand } static void nfc_scene_write_on_enter_mf_ultralight(NfcApp* instance) { + // Free any dict the write callback opened (dict_state != Idle means we own it). + // After a DictAttack scene, on_exit now NULLs the pointer so a simple NULL check + // is safe here too — but the state enum is the authoritative ownership record. + if(instance->mf_ultralight_c_write_context.dict_state != NfcMfUltralightCWriteDictIdle && + instance->mf_ultralight_c_dict_context.dict) { + keys_dict_free(instance->mf_ultralight_c_dict_context.dict); + } + instance->mf_ultralight_c_dict_context.dict = NULL; + instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictIdle; + furi_string_set(instance->text_box_store, "\nApply the\ntarget\ncard now"); instance->poller = nfc_poller_alloc(instance->nfc, NfcProtocolMfUltralight); nfc_poller_start(instance->poller, nfc_scene_write_poller_callback_mf_ultralight, instance); - furi_string_set(instance->text_box_store, "Apply the initial\ncard only"); } const NfcProtocolSupportBase nfc_protocol_support_mf_ultralight = { diff --git a/applications/main/nfc/nfc_app_i.h b/applications/main/nfc/nfc_app_i.h index 0e2c70942..f1113144d 100644 --- a/applications/main/nfc/nfc_app_i.h +++ b/applications/main/nfc/nfc_app_i.h @@ -126,6 +126,18 @@ typedef struct { size_t dict_keys_current; } NfcMfUltralightCDictContext; +typedef enum { + NfcMfUltralightCWriteDictIdle, /**< No dict open; safe to open either dict. */ + NfcMfUltralightCWriteDictUser, /**< User dict currently open. */ + NfcMfUltralightCWriteDictSystem, /**< System dict currently open. */ + NfcMfUltralightCWriteDictExhausted, /**< All dicts tried; do not re-open. */ +} NfcMfUltralightCWriteDictState; + +typedef struct { + bool copy_key; /**< True = overwrite target 3DES key with source key pages. */ + NfcMfUltralightCWriteDictState dict_state; /**< Which dict is open for write-phase auth. */ +} NfcMfUltralightCWriteContext; + struct NfcApp { DialogsApp* dialogs; Storage* storage; @@ -165,6 +177,7 @@ struct NfcApp { SlixUnlock* slix_unlock; NfcMfClassicDictAttackContext nfc_dict_context; NfcMfUltralightCDictContext mf_ultralight_c_dict_context; + NfcMfUltralightCWriteContext mf_ultralight_c_write_context; Mfkey32Logger* mfkey32_logger; MfUserDict* mf_user_dict; MfClassicKeyCache* mfc_key_cache; diff --git a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_dict_attack.c b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_dict_attack.c index b1e24def2..6eb1cfc42 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_dict_attack.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_c_dict_attack.c @@ -77,6 +77,15 @@ void nfc_scene_mf_ultralight_c_dict_attack_prepare_view(NfcApp* instance) { // Set attack type to Ultralight C dict_attack_set_type(instance->dict_attack, DictAttackTypeMfUltralightC); + // Guard: if a previous write phase left a dict handle open, close it now. + // Without this, navigating write->back->read->dict-attack would open the same + // file twice, corrupting VFS state and causing a ViewPort lockup. + if(instance->mf_ultralight_c_dict_context.dict) { + keys_dict_free(instance->mf_ultralight_c_dict_context.dict); + instance->mf_ultralight_c_dict_context.dict = NULL; + instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictIdle; + } + if(state == DictAttackStateUserDictInProgress) { do { if(!keys_dict_check_presence(NFC_APP_MF_ULTRALIGHT_C_DICT_USER_PATH)) { @@ -167,6 +176,7 @@ bool nfc_scene_mf_ultralight_c_dict_attack_on_event(void* context, SceneManagerE nfc_poller_stop(instance->poller); nfc_poller_free(instance->poller); keys_dict_free(instance->mf_ultralight_c_dict_context.dict); + instance->mf_ultralight_c_dict_context.dict = NULL; scene_manager_set_scene_state( instance->scene_manager, NfcSceneMfUltralightCDictAttack, @@ -199,6 +209,7 @@ bool nfc_scene_mf_ultralight_c_dict_attack_on_event(void* context, SceneManagerE nfc_poller_stop(instance->poller); nfc_poller_free(instance->poller); keys_dict_free(instance->mf_ultralight_c_dict_context.dict); + instance->mf_ultralight_c_dict_context.dict = NULL; scene_manager_set_scene_state( instance->scene_manager, NfcSceneMfUltralightCDictAttack, @@ -230,6 +241,7 @@ void nfc_scene_mf_ultralight_c_dict_attack_on_exit(void* context) { NfcSceneMfUltralightCDictAttack, DictAttackStateUserDictInProgress); keys_dict_free(instance->mf_ultralight_c_dict_context.dict); + instance->mf_ultralight_c_dict_context.dict = NULL; instance->mf_ultralight_c_dict_context.dict_keys_total = 0; instance->mf_ultralight_c_dict_context.dict_keys_current = 0; instance->mf_ultralight_c_dict_context.auth_success = false; diff --git a/lib/nfc/protocols/mf_ultralight/mf_ultralight.c b/lib/nfc/protocols/mf_ultralight/mf_ultralight.c index d1f08294f..8abfb05d2 100644 --- a/lib/nfc/protocols/mf_ultralight/mf_ultralight.c +++ b/lib/nfc/protocols/mf_ultralight/mf_ultralight.c @@ -575,13 +575,15 @@ uint8_t mf_ultralight_get_write_end_page(MfUltralightType type) { furi_assert( type == MfUltralightTypeUL11 || type == MfUltralightTypeUL21 || type == MfUltralightTypeNTAG213 || type == MfUltralightTypeNTAG215 || - type == MfUltralightTypeNTAG216 || type == MfUltralightTypeOrigin); + type == MfUltralightTypeNTAG216 || type == MfUltralightTypeOrigin || + type == MfUltralightTypeMfulC); uint8_t end_page = mf_ultralight_get_config_page_num(type); if(type == MfUltralightTypeNTAG213 || type == MfUltralightTypeNTAG215 || type == MfUltralightTypeNTAG216) { end_page -= 1; - } else if(type == MfUltralightTypeOrigin) { + } else if(type == MfUltralightTypeOrigin || type == MfUltralightTypeMfulC) { + // ULC: 48 pages total, write pages 4-47 (includes auth config + 3DES key) end_page = mf_ultralight_features[type].total_pages; } diff --git a/lib/nfc/protocols/mf_ultralight/mf_ultralight_listener.c b/lib/nfc/protocols/mf_ultralight/mf_ultralight_listener.c index 050f9abc1..d8c5fb1e6 100644 --- a/lib/nfc/protocols/mf_ultralight/mf_ultralight_listener.c +++ b/lib/nfc/protocols/mf_ultralight/mf_ultralight_listener.c @@ -186,7 +186,7 @@ static MfUltralightCommand uint16_t pages_total = instance->data->pages_total; MfUltralightCommand command = MfUltralightCommandNotProcessedNAK; - FURI_LOG_T(TAG, "CMD_WRITE"); + FURI_LOG_T(TAG, "CMD_WRITE page %d", start_page); do { bool do_i2c_check = mf_ultralight_is_i2c_tag(instance->data->type); @@ -197,12 +197,22 @@ static MfUltralightCommand break; } - if(!mf_ultralight_listener_check_access( - instance, start_page, MfUltralightListenerAccessTypeWrite)) - break; + // PATCHED: For Ultralight-C, allow writes to pages 44-47 (3DES key area) + // This enables "magic card" emulation for key grabbing + bool is_ulc_key_page = (instance->data->type == MfUltralightTypeMfulC) && + (start_page >= 44 && start_page <= 47); - if(mf_ultralight_static_lock_check_page(instance->static_lock, start_page)) break; - if(mf_ultralight_dynamic_lock_check_page(instance, start_page)) break; + if(!is_ulc_key_page) { + // Normal access check for all other pages + if(!mf_ultralight_listener_check_access( + instance, start_page, MfUltralightListenerAccessTypeWrite)) + break; + + if(mf_ultralight_static_lock_check_page(instance->static_lock, start_page)) break; + if(mf_ultralight_dynamic_lock_check_page(instance, start_page)) break; + } else { + FURI_LOG_I(TAG, "MAGIC: Allowing write to ULC key page %d", start_page); + } const uint8_t* rx_data = bit_buffer_get_data(buffer); command = diff --git a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.c b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.c index e29e1cb6b..6dd761af8 100644 --- a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.c +++ b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.c @@ -456,7 +456,8 @@ static NfcCommand mf_ultralight_poller_handler_auth_ultralight_c(MfUltralightPol if(instance->mfu_event.data->key_request_data.key_provided) { instance->auth_context.tdes_key = instance->mfu_event.data->key_request_data.key; } else if(instance->mode == MfUltralightPollerModeDictAttack) { - // TODO: Can logic be rearranged to request this key before reaching mf_ultralight_poller_handler_auth_ultralight_c in poller? + // TODO: -nofl Can logic be rearranged to request this key + // before reaching mf_ultralight_poller_handler_auth_ultralight_c in poller? FURI_LOG_D(TAG, "No initial key provided, requesting key from dictionary"); // Trigger dictionary key request instance->mfu_event.type = MfUltralightPollerEventTypeRequestKey; @@ -697,9 +698,12 @@ static NfcCommand mf_ultralight_poller_handler_request_write_data(MfUltralightPo instance->mfu_event.type = MfUltralightPollerEventTypeRequestWriteData; instance->callback(instance->general_event, instance->context); - const MfUltralightData* write_data = instance->mfu_event.data->write_data; + // Save write_data to instance field before any further events clobber the union + instance->write_data = instance->mfu_event.data->write_data; + const MfUltralightData* write_data = instance->write_data; const MfUltralightData* tag_data = instance->data; uint32_t features = mf_ultralight_get_feature_support_set(tag_data->type); + instance->write_skip_key = false; bool check_passed = false; do { @@ -737,6 +741,71 @@ static NfcCommand mf_ultralight_poller_handler_request_write_data(MfUltralightPo check_passed = true; } while(false); + // ULC: authenticate the target card before writing. + // The read phase left the card unauthenticated (write-mode callback skips read-phase auth). + // Ask callback for keys (cache first, then dict) until one authenticates, then fire + // WriteKeyRequest so the callback can cache the found key and return the skip decision. + if(check_passed && + mf_ultralight_support_feature(features, MfUltralightFeatureSupportAuthenticate)) { + bool auth_ok = false; + + while(!auth_ok) { + // Request next key from callback (tries dict entries) + memset(instance->mfu_event.data, 0, sizeof(MfUltralightPollerEventData)); + instance->mfu_event.type = MfUltralightPollerEventTypeRequestKey; + instance->callback(instance->general_event, instance->context); + + if(!instance->mfu_event.data->key_request_data.key_provided) { + FURI_LOG_D(TAG, "ULC write: all keys exhausted"); + break; + } + instance->auth_context.tdes_key = instance->mfu_event.data->key_request_data.key; + + // Halt+activate so the card is in a clean state for auth + iso14443_3a_poller_halt(instance->iso14443_3a_poller); + if(iso14443_3a_poller_activate(instance->iso14443_3a_poller, NULL) != + Iso14443_3aErrorNone) { + FURI_LOG_E(TAG, "ULC write: card not responding (locked out?)"); + break; + } + + uint8_t output[MF_ULTRALIGHT_C_AUTH_DATA_SIZE]; + uint8_t RndA[MF_ULTRALIGHT_C_AUTH_RND_BLOCK_SIZE] = {0}; + furi_hal_random_fill_buf(RndA, sizeof(RndA)); + if(mf_ultralight_poller_authenticate_start(instance, RndA, output) != + MfUltralightErrorNone) { + break; + } + uint8_t decoded_RndA[MF_ULTRALIGHT_C_AUTH_RND_BLOCK_SIZE] = {0}; + const uint8_t* RndB = output + MF_ULTRALIGHT_C_AUTH_RND_B_BLOCK_OFFSET; + if(mf_ultralight_poller_authenticate_end(instance, RndB, output, decoded_RndA) != + MfUltralightErrorNone) + continue; + mf_ultralight_3des_shift_data(RndA); + auth_ok = (memcmp(RndA, decoded_RndA, sizeof(decoded_RndA)) == 0); + FURI_LOG_D(TAG, "ULC write auth attempt: %s", auth_ok ? "success" : "fail"); + } + + if(auth_ok) { + // Notify callback with the found key: it caches it and returns the skip decision + MfUltralightC3DesAuthKey found_key = instance->auth_context.tdes_key; + memset(instance->mfu_event.data, 0, sizeof(MfUltralightPollerEventData)); + instance->mfu_event.data->key_request_data.key = found_key; + instance->mfu_event.data->key_request_data.key_provided = true; + instance->mfu_event.type = MfUltralightPollerEventTypeWriteKeyRequest; + instance->callback(instance->general_event, instance->context); + instance->write_skip_key = instance->mfu_event.data->write_key_skip; + FURI_LOG_D( + TAG, + "ULC write: key %s", + instance->write_skip_key ? "kept (target unchanged)" : "overwrite with source"); + } else { + FURI_LOG_E(TAG, "ULC write auth failed - card locked"); + check_passed = false; + instance->mfu_event.type = MfUltralightPollerEventTypeCardLocked; + } + } + if(!check_passed) { iso14443_3a_poller_halt(instance->iso14443_3a_poller); command = instance->callback(instance->general_event, instance->context); @@ -751,15 +820,52 @@ static NfcCommand mf_ultralight_poller_handler_write_pages(MfUltralightPoller* i NfcCommand command = NfcCommandContinue; do { - const MfUltralightData* write_data = instance->mfu_event.data->write_data; + // Use the saved write_data pointer - the union was overwritten by WriteKeyRequest + const MfUltralightData* write_data = instance->write_data; uint8_t end_page = mf_ultralight_get_write_end_page(write_data->type); + + // If user chose to keep target's key, stop before the ULC key pages (44-47) + if(instance->write_skip_key && write_data->type == MfUltralightTypeMfulC && + end_page > 44) { + end_page = 44; + } + if(instance->current_page == end_page) { instance->state = MfUltralightPollerStateWriteSuccess; break; } - FURI_LOG_D(TAG, "Writing page %d", instance->current_page); - MfUltralightError error = mf_ultralight_poller_write_page( - instance, instance->current_page, &write_data->page[instance->current_page]); + + // For ULC key pages (44-47): byte-order correction required. + // Flipper stores each 8-byte DES sub-key MSB-first; the card expects each half reversed. + // Transform: card_bytes = reverse(flipper[0..7]) || reverse(flipper[8..15]) + MfUltralightPage page_to_write; + if(instance->current_page >= 44 && instance->current_page <= 47 && + write_data->type == MfUltralightTypeMfulC) { + const uint8_t* raw = (const uint8_t*)&write_data->page[44]; + uint8_t xformed[16]; + for(int i = 0; i < 8; i++) { + xformed[i] = raw[7 - i]; + } + for(int i = 0; i < 8; i++) { + xformed[8 + i] = raw[15 - i]; + } + uint8_t page_offset = (instance->current_page - 44) * 4; + memcpy(page_to_write.data, xformed + page_offset, 4); + FURI_LOG_D( + TAG, + "Writing KEY page %d (byte-order corrected): %02X %02X %02X %02X", + instance->current_page, + page_to_write.data[0], + page_to_write.data[1], + page_to_write.data[2], + page_to_write.data[3]); + } else { + page_to_write = write_data->page[instance->current_page]; + FURI_LOG_D(TAG, "Writing page %d", instance->current_page); + } + + MfUltralightError error = + mf_ultralight_poller_write_page(instance, instance->current_page, &page_to_write); if(error != MfUltralightErrorNone) { instance->state = MfUltralightPollerStateWriteFail; instance->error = error; diff --git a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.h b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.h index 2552abeb5..c0a38cfe9 100644 --- a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.h +++ b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.h @@ -28,6 +28,7 @@ typedef enum { MfUltralightPollerEventTypeWriteSuccess, /**< Poller wrote card successfully. */ MfUltralightPollerEventTypeWriteFail, /**< Poller failed to write card. */ MfUltralightPollerEventTypeRequestKey, /**< Poller requests key for dict attack. */ + MfUltralightPollerEventTypeWriteKeyRequest, /**< Poller asks user whether to overwrite 3DES key on target. */ } MfUltralightPollerEventType; /** @@ -67,6 +68,7 @@ typedef union { const MfUltralightData* write_data; /**< Data to be written to card. */ MfUltralightPollerMode poller_mode; /**< Mode to operate in. */ MfUltralightPollerKeyRequestData key_request_data; /**< Key request data. */ + bool write_key_skip; /**< Set to true by callback to skip writing 3DES key pages. */ } MfUltralightPollerEventData; /** diff --git a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_i.h b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_i.h index b35c49aea..a6d9235b0 100644 --- a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_i.h +++ b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_i.h @@ -88,6 +88,8 @@ struct MfUltralightPoller { uint8_t tearing_flag_read; uint8_t tearing_flag_total; uint16_t current_page; + bool write_skip_key; // If true, skip writing pages 44-47 (3DES key) during ULC write + const MfUltralightData* write_data; // Saved pointer to source data for write phase MfUltralightError error; mbedtls_des3_context des_context; From 7239c42d25fc4125afe89f0c2b1caf4c18a0fa6e Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 9 Mar 2026 02:55:07 +0300 Subject: [PATCH 04/71] upd changelog --- CHANGELOG.md | 5 +++-- .../notification_settings_app.c | 16 ++++++---------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aca368515..b70783b32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ ## Main changes - Current API: 87.6 -* Apps: Build tag (**9mar2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) +* NFC: Add Mifare Ultralight C Write Support (by @haw8411) +* Apps: Build tag (**9mar2026p2**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes -* None yet +* Display: Extend lcd contrast range to full ST756x 6-bit range (by @ShaTie) (reduced to -10 to +18 to avoid fully unreadable state)

#### Known NFC post-refactor regressions list: - Mifare Mini clones reading is broken (original mini working fine) (OFW) diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index bb4563227..33a16c13e 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -24,19 +24,15 @@ static const NotificationSequence sequence_note_c = { NULL, }; -#define CONTRAST_COUNT 64 +#define CONTRAST_COUNT 29 const char* const contrast_text[CONTRAST_COUNT] = { - "-32", "-31", "-30", "-29", "-28", "-27", "-26", "-25", "-24", "-23", "-22", "-21", "-20", - "-19", "-18", "-17", "-16", "-15", "-14", "-13", "-12", "-11", "-10", "-9", "-8", "-7", - "-6", "-5", "-4", "-3", "-2", "-1", "0", "+1", "+2", "+3", "+4", "+5", "+6", - "+7", "+8", "+9", "+10", "+11", "+12", "+13", "+14", "+15", "+16", "+17", "+18", "+19", - "+20", "+21", "+22", "+23", "+24", "+25", "+26", "+27", "+28", "+29", "+30", "+31", + "-10", "-9", "-8", "-7", "-6", "-5", "-4", "-3", "-2", "-1", + "0", "+1", "+2", "+3", "+4", "+5", "+6", "+7", "+8", "+9", + "+10", "+11", "+12", "+13", "+14", "+15", "+16", "+17", "+18", }; const int32_t contrast_value[CONTRAST_COUNT] = { - -32, -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, - -16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, + 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, }; #define BACKLIGHT_COUNT 21 From d8568f3e8b4f494ba9418a3bbebc8cbb093df1ac Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 9 Mar 2026 03:37:10 +0300 Subject: [PATCH 05/71] upd changelog sync apps --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b70783b32..cdfa1d12d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## Main changes - Current API: 87.6 * NFC: Add Mifare Ultralight C Write Support (by @haw8411) -* Apps: Build tag (**9mar2026p2**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) +* Apps: Build tag (**9mar2026p3**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * Display: Extend lcd contrast range to full ST756x 6-bit range (by @ShaTie) (reduced to -10 to +18 to avoid fully unreadable state)

From 04d7b46ce9eaca1d70d35347c27283e73f523735 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:15:14 +0300 Subject: [PATCH 06/71] add came twee support for TOP44FGN --- CHANGELOG.md | 1 + documentation/SubGHzSupportedSystems.md | 2 +- lib/subghz/protocols/came_twee.c | 10 +++++++--- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdfa1d12d..530fd147f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## Main changes - Current API: 87.6 +* SubGHz: Add **CAME TOP44FGN** support in CAME TWEE protocol * NFC: Add Mifare Ultralight C Write Support (by @haw8411) * Apps: Build tag (**9mar2026p3**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes diff --git a/documentation/SubGHzSupportedSystems.md b/documentation/SubGHzSupportedSystems.md index 12f409eb4..5b6e427c8 100644 --- a/documentation/SubGHzSupportedSystems.md +++ b/documentation/SubGHzSupportedSystems.md @@ -23,7 +23,7 @@ That list is only for default SubGHz app, apps like *Weather Station* have their - Beninca ARC (TOGO2VA) `433.92MHz` `AM650` (128 bits, Dynamic AES128) (button code `0` emulates `hidden button` option on the remote) - BFT Mitto `433.92MHz` `AM650` (64 bits, Dynamic, KeeLoq based with Seed taken from serial) - CAME Atomo `433.92MHz, 868MHz` `AM650` (62 bits, Dynamic) -- CAME TWEE `433.92MHz` `AM650` (54 bits, Static) +- CAME TWEE `433.92MHz` `AM650` (54 bits, Pseudo-Dynamic) (+ TOP44FGN) (aka New Fixed Code) - CAME `433.92MHz, 868MHz` `AM650` (12, 24 bits, Static) - Ditec GOL4 `433.92MHz` `AM650` (54 bits, Dynamic) (should be compatible with BIXLG4, BIXLS2, BIXLP2) - (right arrow emulates button `0` (hidden button)) - Prastel `433.92MHz, 868MHz` `AM650` (25, 42 bits, Static) diff --git a/lib/subghz/protocols/came_twee.c b/lib/subghz/protocols/came_twee.c index d6b656a57..55947efbf 100644 --- a/lib/subghz/protocols/came_twee.c +++ b/lib/subghz/protocols/came_twee.c @@ -322,9 +322,13 @@ void subghz_protocol_decoder_came_twee_feed(void* context, bool level, uint32_t ManchesterEvent event = ManchesterEventReset; switch(instance->decoder.parser_step) { case CameTweeDecoderStepReset: - if((!level) && (DURATION_DIFF(duration, subghz_protocol_came_twee_const.te_long * 51) < - subghz_protocol_came_twee_const.te_delta * 20)) { - //Found header CAME + if((!level) && ((DURATION_DIFF(duration, subghz_protocol_came_twee_const.te_long * 51) < + subghz_protocol_came_twee_const.te_delta * 20) || + (DURATION_DIFF(duration, subghz_protocol_came_twee_const.te_long * 12) < + subghz_protocol_came_twee_const.te_delta * 10))) { + // Found header CAME + // Original TWEE uses 51k us delay + // TOP44FGN uses 12k us delay instance->decoder.parser_step = CameTweeDecoderStepDecoderData; instance->decoder.decode_data = 0; instance->decoder.decode_count_bit = 0; From a5f47e3e6d162550b4a2cbdb958195e69a923730 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:52:18 +0300 Subject: [PATCH 07/71] subghz add nord ice protocol --- CHANGELOG.md | 1 + documentation/SubGHzSupportedSystems.md | 3 +- lib/subghz/protocols/nord_ice.c | 350 ++++++++++++++++++++++++ lib/subghz/protocols/nord_ice.h | 109 ++++++++ lib/subghz/protocols/protocol_items.c | 1 + lib/subghz/protocols/protocol_items.h | 1 + 6 files changed, 464 insertions(+), 1 deletion(-) create mode 100644 lib/subghz/protocols/nord_ice.c create mode 100644 lib/subghz/protocols/nord_ice.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 530fd147f..e73a1a45b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## Main changes - Current API: 87.6 +* SubGHz: Add **Nord ICE** protocol (33 bits, Static) * SubGHz: Add **CAME TOP44FGN** support in CAME TWEE protocol * NFC: Add Mifare Ultralight C Write Support (by @haw8411) * Apps: Build tag (**9mar2026p3**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) diff --git a/documentation/SubGHzSupportedSystems.md b/documentation/SubGHzSupportedSystems.md index 5b6e427c8..8c552e9c7 100644 --- a/documentation/SubGHzSupportedSystems.md +++ b/documentation/SubGHzSupportedSystems.md @@ -41,6 +41,7 @@ That list is only for default SubGHz app, apps like *Weather Station* have their - KingGates Stylo 4k `433.92MHz` `AM650` (89 bits, Dynamic, KeeLoq based) - Mastercode `AM650` (36 bits, Static) - Megacode `AM650` (24 bits, Static) +- Nord ICE `433.92MHz` `AM650` (33 bits, Static) - Nero Sketch `AM650` (40 bits, Static) - Nice Flo `433.92MHz` `AM650` (12, 24 bits, Static) - Nice FloR-S `433.92MHz` `AM650` (52 bits, Dynamic) @@ -106,7 +107,7 @@ The following manufacturers have KeeLoq support in Unleashed firmware: - DEA Mio - `433.92MHz` `AM650` (KeeLoq, 64 bits) (modified serial in Hop, uses last 3 digits modifying first one (example - 419 -> C19) - simple learning) - DoorHan - 315MHz, `433.92MHz` `AM650` (KeeLoq, 64 bits) - DTM Neo - `433.92MHz` `AM650` (KeeLoq, 64 bits) (12bit serial part in Hop - simple learning) -- Elmes Poland - `433.92MHz` `AM650` (KeeLoq, 64 bits) (normal learning) +- Elmes Poland - `303, 433.92MHz` `AM650` (KeeLoq, 64 bits) (normal learning) - FAAC RC,XT - `433.92MHz, 868MHz` `AM650` (KeeLoq, 64 bits) (12bit serial part in Hop - normal learning) - Genius Bravo - `433.92MHz` `AM650` (KeeLoq, 64 bits) (12bit serial part in Hop - normal learning) (Genius ECHO, Genius Bravo (Button code 0xB for prog. mode)) - Gibidi - `433.92MHz` `AM650` (KeeLoq, 64 bits) diff --git a/lib/subghz/protocols/nord_ice.c b/lib/subghz/protocols/nord_ice.c new file mode 100644 index 000000000..2e7d0caa9 --- /dev/null +++ b/lib/subghz/protocols/nord_ice.c @@ -0,0 +1,350 @@ +#include "nord_ice.h" +#include "../blocks/const.h" +#include "../blocks/decoder.h" +#include "../blocks/encoder.h" +#include "../blocks/generic.h" +#include "../blocks/math.h" + +#define TAG "SubGhzProtocolNord_Ice" + +static const SubGhzBlockConst subghz_protocol_nord_ice_const = { + .te_short = 300, + .te_long = 800, + .te_delta = 150, + .min_count_bit_for_found = 33, +}; + +struct SubGhzProtocolDecoderNord_Ice { + SubGhzProtocolDecoderBase base; + + SubGhzBlockDecoder decoder; + SubGhzBlockGeneric generic; +}; + +struct SubGhzProtocolEncoderNord_Ice { + SubGhzProtocolEncoderBase base; + + SubGhzProtocolBlockEncoder encoder; + SubGhzBlockGeneric generic; +}; + +typedef enum { + Nord_IceDecoderStepReset = 0, + Nord_IceDecoderStepSaveDuration, + Nord_IceDecoderStepCheckDuration, +} Nord_IceDecoderStep; + +const SubGhzProtocolDecoder subghz_protocol_nord_ice_decoder = { + .alloc = subghz_protocol_decoder_nord_ice_alloc, + .free = subghz_protocol_decoder_nord_ice_free, + + .feed = subghz_protocol_decoder_nord_ice_feed, + .reset = subghz_protocol_decoder_nord_ice_reset, + + .get_hash_data = subghz_protocol_decoder_nord_ice_get_hash_data, + .serialize = subghz_protocol_decoder_nord_ice_serialize, + .deserialize = subghz_protocol_decoder_nord_ice_deserialize, + .get_string = subghz_protocol_decoder_nord_ice_get_string, +}; + +const SubGhzProtocolEncoder subghz_protocol_nord_ice_encoder = { + .alloc = subghz_protocol_encoder_nord_ice_alloc, + .free = subghz_protocol_encoder_nord_ice_free, + + .deserialize = subghz_protocol_encoder_nord_ice_deserialize, + .stop = subghz_protocol_encoder_nord_ice_stop, + .yield = subghz_protocol_encoder_nord_ice_yield, +}; + +const SubGhzProtocol subghz_protocol_nord_ice = { + .name = SUBGHZ_PROTOCOL_NORD_ICE_NAME, + .type = SubGhzProtocolTypeStatic, + .flag = SubGhzProtocolFlag_433 | SubGhzProtocolFlag_AM | SubGhzProtocolFlag_Decodable | + SubGhzProtocolFlag_Load | SubGhzProtocolFlag_Save | SubGhzProtocolFlag_Send, + + .decoder = &subghz_protocol_nord_ice_decoder, + .encoder = &subghz_protocol_nord_ice_encoder, +}; + +void* subghz_protocol_encoder_nord_ice_alloc(SubGhzEnvironment* environment) { + UNUSED(environment); + SubGhzProtocolEncoderNord_Ice* instance = malloc(sizeof(SubGhzProtocolEncoderNord_Ice)); + + instance->base.protocol = &subghz_protocol_nord_ice; + instance->generic.protocol_name = instance->base.protocol->name; + + instance->encoder.repeat = 3; + instance->encoder.size_upload = 128; + instance->encoder.upload = malloc(instance->encoder.size_upload * sizeof(LevelDuration)); + instance->encoder.is_running = false; + return instance; +} + +void subghz_protocol_encoder_nord_ice_free(void* context) { + furi_assert(context); + SubGhzProtocolEncoderNord_Ice* instance = context; + free(instance->encoder.upload); + free(instance); +} + +/** + * Generating an upload from data. + * @param instance Pointer to a SubGhzProtocolEncoderNord_Ice instance + */ +static void subghz_protocol_encoder_nord_ice_get_upload(SubGhzProtocolEncoderNord_Ice* instance) { + furi_assert(instance); + size_t index = 0; + + // Send key and GAP + for(uint8_t i = instance->generic.data_count_bit; i > 0; i--) { + if(bit_read(instance->generic.data, i - 1)) { + // Send bit 1 + instance->encoder.upload[index++] = + level_duration_make(true, (uint32_t)subghz_protocol_nord_ice_const.te_long); + if(i == 1) { + //Send gap if bit was last + instance->encoder.upload[index++] = level_duration_make( + false, (uint32_t)subghz_protocol_nord_ice_const.te_short * 25); + } else { + instance->encoder.upload[index++] = + level_duration_make(false, (uint32_t)subghz_protocol_nord_ice_const.te_short); + } + } else { + // Send bit 0 + instance->encoder.upload[index++] = + level_duration_make(true, (uint32_t)subghz_protocol_nord_ice_const.te_short); + if(i == 1) { + //Send gap if bit was last + instance->encoder.upload[index++] = level_duration_make( + false, (uint32_t)subghz_protocol_nord_ice_const.te_short * 25); + } else { + instance->encoder.upload[index++] = + level_duration_make(false, (uint32_t)subghz_protocol_nord_ice_const.te_long); + } + } + } + + instance->encoder.size_upload = index; + return; +} + +/** + * Analysis of received data + * @param instance Pointer to a SubGhzBlockGeneric* instance + */ +static void subghz_protocol_nord_ice_check_remote_controller(SubGhzBlockGeneric* instance) { + instance->serial = (instance->data >> 15) << 9 | + (instance->data & 0x1FF); // 26 bits for serial + instance->btn = (instance->data >> 9) & 0x3F; // 6 bits for button +} + +SubGhzProtocolStatus + subghz_protocol_encoder_nord_ice_deserialize(void* context, FlipperFormat* flipper_format) { + furi_assert(context); + SubGhzProtocolEncoderNord_Ice* instance = context; + SubGhzProtocolStatus ret = SubGhzProtocolStatusError; + do { + ret = subghz_block_generic_deserialize_check_count_bit( + &instance->generic, + flipper_format, + subghz_protocol_nord_ice_const.min_count_bit_for_found); + if(ret != SubGhzProtocolStatusOk) { + break; + } + // Optional value + flipper_format_read_uint32( + flipper_format, "Repeat", (uint32_t*)&instance->encoder.repeat, 1); + + subghz_protocol_nord_ice_check_remote_controller(&instance->generic); + subghz_protocol_encoder_nord_ice_get_upload(instance); + instance->encoder.is_running = true; + } while(false); + + return ret; +} + +void subghz_protocol_encoder_nord_ice_stop(void* context) { + SubGhzProtocolEncoderNord_Ice* instance = context; + instance->encoder.is_running = false; +} + +LevelDuration subghz_protocol_encoder_nord_ice_yield(void* context) { + SubGhzProtocolEncoderNord_Ice* instance = context; + + if(instance->encoder.repeat == 0 || !instance->encoder.is_running) { + instance->encoder.is_running = false; + return level_duration_reset(); + } + + LevelDuration ret = instance->encoder.upload[instance->encoder.front]; + + if(++instance->encoder.front == instance->encoder.size_upload) { + if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--; + instance->encoder.front = 0; + } + + return ret; +} + +void* subghz_protocol_decoder_nord_ice_alloc(SubGhzEnvironment* environment) { + UNUSED(environment); + SubGhzProtocolDecoderNord_Ice* instance = malloc(sizeof(SubGhzProtocolDecoderNord_Ice)); + instance->base.protocol = &subghz_protocol_nord_ice; + instance->generic.protocol_name = instance->base.protocol->name; + return instance; +} + +void subghz_protocol_decoder_nord_ice_free(void* context) { + furi_assert(context); + SubGhzProtocolDecoderNord_Ice* instance = context; + free(instance); +} + +void subghz_protocol_decoder_nord_ice_reset(void* context) { + furi_assert(context); + SubGhzProtocolDecoderNord_Ice* instance = context; + instance->decoder.parser_step = Nord_IceDecoderStepReset; +} + +void subghz_protocol_decoder_nord_ice_feed(void* context, bool level, volatile uint32_t duration) { + furi_assert(context); + SubGhzProtocolDecoderNord_Ice* instance = context; + + // Nord ICE Decoder + // 2026.03 - @xMasterX (MMX) + + // Key samples + // + // Serial Btn Serial + // 0x9467688A btn 1 = 10010100011001110 110100 010001010 + // 0x9467308A btn 2 = 10010100011001110 011000 010001010 + // 0x9467628A btn 3 = 10010100011001110 110001 010001010 + // 0x9467648A btn 4 = 10010100011001110 110010 010001010 + + switch(instance->decoder.parser_step) { + case Nord_IceDecoderStepReset: + if((!level) && (DURATION_DIFF(duration, subghz_protocol_nord_ice_const.te_short * 25) < + subghz_protocol_nord_ice_const.te_delta * 11)) { + //Found GAP + instance->decoder.decode_data = 0; + instance->decoder.decode_count_bit = 0; + instance->decoder.parser_step = Nord_IceDecoderStepSaveDuration; + } + break; + case Nord_IceDecoderStepSaveDuration: + if(level) { + instance->decoder.te_last = duration; + instance->decoder.parser_step = Nord_IceDecoderStepCheckDuration; + } else { + instance->decoder.parser_step = Nord_IceDecoderStepReset; + } + break; + case Nord_IceDecoderStepCheckDuration: + if(!level) { + // Bit 0 is short and long timing = 300us HIGH (te_last) and 800us LOW + if((DURATION_DIFF(instance->decoder.te_last, subghz_protocol_nord_ice_const.te_short) < + subghz_protocol_nord_ice_const.te_delta) && + (DURATION_DIFF(duration, subghz_protocol_nord_ice_const.te_long) < + subghz_protocol_nord_ice_const.te_delta)) { + subghz_protocol_blocks_add_bit(&instance->decoder, 0); + instance->decoder.parser_step = Nord_IceDecoderStepSaveDuration; + // Bit 1 is long and short timing = 800us HIGH (te_last) and 300us LOW + } else if( + (DURATION_DIFF(instance->decoder.te_last, subghz_protocol_nord_ice_const.te_long) < + subghz_protocol_nord_ice_const.te_delta) && + (DURATION_DIFF(duration, subghz_protocol_nord_ice_const.te_short) < + subghz_protocol_nord_ice_const.te_delta)) { + subghz_protocol_blocks_add_bit(&instance->decoder, 1); + instance->decoder.parser_step = Nord_IceDecoderStepSaveDuration; + } else if( + // End of the key + DURATION_DIFF(duration, subghz_protocol_nord_ice_const.te_short * 25) < + subghz_protocol_nord_ice_const.te_delta * 11) { + //Found next GAP and add bit 0 or 1 (only bit 0 was found on the remotes) + if((DURATION_DIFF( + instance->decoder.te_last, subghz_protocol_nord_ice_const.te_short) < + subghz_protocol_nord_ice_const.te_delta)) { + subghz_protocol_blocks_add_bit(&instance->decoder, 0); + } + if((DURATION_DIFF( + instance->decoder.te_last, subghz_protocol_nord_ice_const.te_long) < + subghz_protocol_nord_ice_const.te_delta)) { + subghz_protocol_blocks_add_bit(&instance->decoder, 1); + } + // If got 33 bits key reading is finished + if(instance->decoder.decode_count_bit == + subghz_protocol_nord_ice_const.min_count_bit_for_found) { + instance->generic.data = instance->decoder.decode_data; + instance->generic.data_count_bit = instance->decoder.decode_count_bit; + if(instance->base.callback) + instance->base.callback(&instance->base, instance->base.context); + } + instance->decoder.decode_data = 0; + instance->decoder.decode_count_bit = 0; + instance->decoder.parser_step = Nord_IceDecoderStepReset; + } else { + instance->decoder.parser_step = Nord_IceDecoderStepReset; + } + } else { + instance->decoder.parser_step = Nord_IceDecoderStepReset; + } + break; + } +} + +uint8_t subghz_protocol_decoder_nord_ice_get_hash_data(void* context) { + furi_assert(context); + SubGhzProtocolDecoderNord_Ice* instance = context; + return subghz_protocol_blocks_get_hash_data( + &instance->decoder, (instance->decoder.decode_count_bit / 8) + 1); +} + +SubGhzProtocolStatus subghz_protocol_decoder_nord_ice_serialize( + void* context, + FlipperFormat* flipper_format, + SubGhzRadioPreset* preset) { + furi_assert(context); + SubGhzProtocolDecoderNord_Ice* instance = context; + return subghz_block_generic_serialize(&instance->generic, flipper_format, preset); +} + +SubGhzProtocolStatus + subghz_protocol_decoder_nord_ice_deserialize(void* context, FlipperFormat* flipper_format) { + furi_assert(context); + SubGhzProtocolDecoderNord_Ice* instance = context; + return subghz_block_generic_deserialize_check_count_bit( + &instance->generic, + flipper_format, + subghz_protocol_nord_ice_const.min_count_bit_for_found); +} + +void subghz_protocol_decoder_nord_ice_get_string(void* context, FuriString* output) { + furi_assert(context); + SubGhzProtocolDecoderNord_Ice* instance = context; + + subghz_protocol_nord_ice_check_remote_controller(&instance->generic); + + uint64_t code_found_reverse = subghz_protocol_blocks_reverse_key( + instance->generic.data, instance->generic.data_count_bit); + + // for future use + // // push protocol data to global variable + // subghz_block_generic_global.btn_is_available = false; + // subghz_block_generic_global.current_btn = instance->generic.btn; + // subghz_block_generic_global.btn_length_bit = 4; + // // + + furi_string_cat_printf( + output, + "%s %db\r\n" + "Key: 0x%08llX\r\n" + "Yek: 0x%08llX\r\n" + "Serial: 0x%07lX\r\n" + "Btn: %02X", + instance->generic.protocol_name, + instance->generic.data_count_bit, + (uint64_t)(instance->generic.data & 0xFFFFFFFFF), + (code_found_reverse & 0xFFFFFFFFF), + instance->generic.serial, + instance->generic.btn); +} diff --git a/lib/subghz/protocols/nord_ice.h b/lib/subghz/protocols/nord_ice.h new file mode 100644 index 000000000..4149fba7d --- /dev/null +++ b/lib/subghz/protocols/nord_ice.h @@ -0,0 +1,109 @@ +#pragma once + +#include "base.h" + +#define SUBGHZ_PROTOCOL_NORD_ICE_NAME "Nord ICE" + +typedef struct SubGhzProtocolDecoderNord_Ice SubGhzProtocolDecoderNord_Ice; +typedef struct SubGhzProtocolEncoderNord_Ice SubGhzProtocolEncoderNord_Ice; + +extern const SubGhzProtocolDecoder subghz_protocol_nord_ice_decoder; +extern const SubGhzProtocolEncoder subghz_protocol_nord_ice_encoder; +extern const SubGhzProtocol subghz_protocol_nord_ice; + +/** + * Allocate SubGhzProtocolEncoderNord_Ice. + * @param environment Pointer to a SubGhzEnvironment instance + * @return SubGhzProtocolEncoderNord_Ice* pointer to a SubGhzProtocolEncoderNord_Ice instance + */ +void* subghz_protocol_encoder_nord_ice_alloc(SubGhzEnvironment* environment); + +/** + * Free SubGhzProtocolEncoderNord_Ice. + * @param context Pointer to a SubGhzProtocolEncoderNord_Ice instance + */ +void subghz_protocol_encoder_nord_ice_free(void* context); + +/** + * Deserialize and generating an upload to send. + * @param context Pointer to a SubGhzProtocolEncoderNord_Ice instance + * @param flipper_format Pointer to a FlipperFormat instance + * @return status + */ +SubGhzProtocolStatus + subghz_protocol_encoder_nord_ice_deserialize(void* context, FlipperFormat* flipper_format); + +/** + * Forced transmission stop. + * @param context Pointer to a SubGhzProtocolEncoderNord_Ice instance + */ +void subghz_protocol_encoder_nord_ice_stop(void* context); + +/** + * Getting the level and duration of the upload to be loaded into DMA. + * @param context Pointer to a SubGhzProtocolEncoderNord_Ice instance + * @return LevelDuration + */ +LevelDuration subghz_protocol_encoder_nord_ice_yield(void* context); + +/** + * Allocate SubGhzProtocolDecoderNord_Ice. + * @param environment Pointer to a SubGhzEnvironment instance + * @return SubGhzProtocolDecoderNord_Ice* pointer to a SubGhzProtocolDecoderNord_Ice instance + */ +void* subghz_protocol_decoder_nord_ice_alloc(SubGhzEnvironment* environment); + +/** + * Free SubGhzProtocolDecoderNord_Ice. + * @param context Pointer to a SubGhzProtocolDecoderNord_Ice instance + */ +void subghz_protocol_decoder_nord_ice_free(void* context); + +/** + * Reset decoder SubGhzProtocolDecoderNord_Ice. + * @param context Pointer to a SubGhzProtocolDecoderNord_Ice instance + */ +void subghz_protocol_decoder_nord_ice_reset(void* context); + +/** + * Parse a raw sequence of levels and durations received from the air. + * @param context Pointer to a SubGhzProtocolDecoderNord_Ice instance + * @param level Signal level true-high false-low + * @param duration Duration of this level in, us + */ +void subghz_protocol_decoder_nord_ice_feed(void* context, bool level, uint32_t duration); + +/** + * Getting the hash sum of the last randomly received parcel. + * @param context Pointer to a SubGhzProtocolDecoderNord_Ice instance + * @return hash Hash sum + */ +uint8_t subghz_protocol_decoder_nord_ice_get_hash_data(void* context); + +/** + * Serialize data SubGhzProtocolDecoderNord_Ice. + * @param context Pointer to a SubGhzProtocolDecoderNord_Ice instance + * @param flipper_format Pointer to a FlipperFormat instance + * @param preset The modulation on which the signal was received, SubGhzRadioPreset + * @return status + */ +SubGhzProtocolStatus subghz_protocol_decoder_nord_ice_serialize( + void* context, + FlipperFormat* flipper_format, + SubGhzRadioPreset* preset); + +/** + * Deserialize data SubGhzProtocolDecoderNord_Ice. + * @param context Pointer to a SubGhzProtocolDecoderNord_Ice instance + * @param flipper_format Pointer to a FlipperFormat instance + * @return status + */ +SubGhzProtocolStatus + subghz_protocol_decoder_nord_ice_deserialize(void* context, FlipperFormat* flipper_format); + +/** + * Getting a textual representation of the received data. + * @param context Pointer to a SubGhzProtocolDecoderNord_Ice instance + * @param output Resulting text + */ +void subghz_protocol_decoder_nord_ice_get_string(void* context, FuriString* output); diff --git a/lib/subghz/protocols/protocol_items.c b/lib/subghz/protocols/protocol_items.c index 8dfe647b1..416e3524f 100644 --- a/lib/subghz/protocols/protocol_items.c +++ b/lib/subghz/protocols/protocol_items.c @@ -29,6 +29,7 @@ const SubGhzProtocol* const subghz_protocol_registry_items[] = { &subghz_protocol_elplast, &subghz_protocol_treadmill37, &subghz_protocol_beninca_arc, &subghz_protocol_jarolift, &subghz_protocol_ditec_gol4, &subghz_protocol_keyfinder, + &subghz_protocol_nord_ice, }; const SubGhzProtocolRegistry subghz_protocol_registry = { diff --git a/lib/subghz/protocols/protocol_items.h b/lib/subghz/protocols/protocol_items.h index a0214b74f..d9e5f0cd0 100644 --- a/lib/subghz/protocols/protocol_items.h +++ b/lib/subghz/protocols/protocol_items.h @@ -58,3 +58,4 @@ #include "jarolift.h" #include "ditec_gol4.h" #include "keyfinder.h" +#include "nord_ice.h" From 648c031496a82ad447643815791fa1b891d3c2e8 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 10 Mar 2026 02:45:07 +0300 Subject: [PATCH 08/71] upd changelog sync apps --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e73a1a45b..40b8982d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ * SubGHz: Add **Nord ICE** protocol (33 bits, Static) * SubGHz: Add **CAME TOP44FGN** support in CAME TWEE protocol * NFC: Add Mifare Ultralight C Write Support (by @haw8411) -* Apps: Build tag (**9mar2026p3**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) +* Apps: Build tag (**10mar2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * Display: Extend lcd contrast range to full ST756x 6-bit range (by @ShaTie) (reduced to -10 to +18 to avoid fully unreadable state)

From 11cb99168ef62b35b5a35d3886b8e2098027f3eb Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 12 Mar 2026 21:24:10 +0300 Subject: [PATCH 09/71] sync apps --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40b8982d8..cda75a330 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ * SubGHz: Add **Nord ICE** protocol (33 bits, Static) * SubGHz: Add **CAME TOP44FGN** support in CAME TWEE protocol * NFC: Add Mifare Ultralight C Write Support (by @haw8411) -* Apps: Build tag (**10mar2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) +* Apps: Build tag (**12mar2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * Display: Extend lcd contrast range to full ST756x 6-bit range (by @ShaTie) (reduced to -10 to +18 to avoid fully unreadable state)

From 70c2b3c6270dbfc2f05f8b79887b506b279e0c9b Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sun, 15 Mar 2026 02:43:36 +0300 Subject: [PATCH 10/71] add zero and ffff mfkeys --- .../resources/subghz/assets/keeloq_mfcodes | 142 +++++++++--------- 1 file changed, 73 insertions(+), 69 deletions(-) diff --git a/applications/main/subghz/resources/subghz/assets/keeloq_mfcodes b/applications/main/subghz/resources/subghz/assets/keeloq_mfcodes index ae6315a2d..572bda5cc 100644 --- a/applications/main/subghz/resources/subghz/assets/keeloq_mfcodes +++ b/applications/main/subghz/resources/subghz/assets/keeloq_mfcodes @@ -1,72 +1,76 @@ Filetype: Flipper SubGhz Keystore File Version: 0 Encryption: 1 -IV: 6E 6F 74 69 63 65 73 20 62 75 6C 67 65 4F 77 4F -212F687B2D38E6E9066E95894E455A6AD3A8861CDDAFD09DBC2557506676332D -2CFF25E9743E4588820D24998B5047E8019C6E200922BCB66830C7F722822A79 -74AA0EF345C27F583042C4EDDBB83D5446387D4E5B31DFD3F7B9D09F0662A0AA95BEB40BF37678954CAE0200B2898D22 -6998443F71D17938425CD231B8500E769976601315D79D673AA37E4F9BDCE50B -3335691566FEDDDE5828BE0A5BBBD9AC013987A1134E68020D3F0F477E09BC71 -1B3A3CBB3D56783220BEAECEA15FFF5CD92AF44F63547F6F84F4533D2B3D820B -A1ECFE74E714D0E1514D0C18903598E7FB7E7AF5C0165DEDE85DFAE05B26C0EC -BA3F2EFBE7CB6671206EA90DA903FF48CCABEC9F393D55B9DB3B7E207A48C059 -CA91805C8ADF02501B41D6BF0C69D7C3DD6E218D4B4EB6BED2FDE1367256E544 -5465E78D877F2594AF40115723C8AC71D09F60ACCC8121CD3B6BA271BEA43B5C -BB1D3DE0A048694FC86CA8E79BB7BFBB1EF91515C44E917FD5017676EE5C4B13 -48C08D8B2B42D025D2557CC650EDA3C5CC301CD71169CFE0044C07A95224109B -3DAE6C5B94C237FF10CF82FAA58517DB4DD4E4D82FD81CFAAA4836C9B8460CE1 -645F8F5D213889BAB0F437497856A0CF402E3D4A3679258BB0BF2B768D1F1714 -AFC3C462698C795B1A407F27F499CD1757931BD676BA43E4B41E8EF2672EC4AE -5A19F0D1CD87C40883D7BE51377CDAD94C977B4F6CFCCECED91D56867C9EC211 -716645BD4F5B18002417A92D6A2BD29D18087F7ADB549A90A2661EB4163EF190 -54B83F7F7F50AD4C3DC3498822240AEB7D9A43728704F14691A39C79FAC9B6A3 -FFDDCEFAFF3ADA3616B388684B1AD4987B83E459BA5037E377FE17CC7EDAA430 -9CC98590A371E580B164A5F1369945A6DB653797331EC898755E84BE93EDF3EF -63697AD15064194A3986CC1F3111A09E3D86AC8D370A8EC4D22EDDA4B9661B63 -F7CE4FA63EA9D388AE4AB6A468768AE91E2E3CF6F1D9A5EA17845466A6801A8C -38B2DE32CF36D68842CB31FD97394892B76FB4E81FCFB33164676BAB354939DE -93192FA1D7E347AF9FC98EE49A8B02869527511D0B263FD3B08CCFE645A39F51 -3D5F832CF75D94B58B078B4EE0DFC1D987795CD49959517F391F9E9F09CA187C -6CBCCCE0F8E6A833D5CA18E09E821A7F93B9FD7D912632E277B1721985F4B701 -5896050E9F4950CD7061ED4DD3BA9FC6013CA52F5C1B0493110FA3934116AC66 -F0D3FF283FBD7CFBFE697DBE752EDD5A1F124DCCAE3B629A146E0E85A7ED4E06 -6B6C7C365A5CABA2F1023ED1F97D182F0B9D575C64393272E19B8B809DF3C329 -1AF30761D85AE1C6DD4D78D4ECE79983C7F58ECE859709570262811CFCA8C97F -FC86201D96F4AE36C63A1C00DA34AC14BFDE317383D19A534362E6149E860366 -54F55F34A27E1D690F56269D29C795105C8407A4E1A9ED431588F559948C115A -4DD62ECD2FE8CBB783E3D7F0B1D95496D585405DE5AEB07C8A4E053B3AAAD808 -EDB356AFA869EFB987F099239449D6B9F1469EA6D8C9F0A6B4171F685E2853A1 -03C058A2C0DB83179E520F035BBAAB01673EFE4FDE717A0F5D1AB6B81BC9D033 -D2F33C08136AB161EED6121E721F8C479CED5F26A1B7DEEDEA8580314C232C5E -51CB3275D6D7250CD88A446242590C3DFD36DF83F5E34379DE73601511CD24B9 -ED4EC928AC505CBEFA6ACB2901C8E2221FC5073624B0B78F8365DD0442EFBA10 -76F959BBF8A4EC4AC22F41F06719BCBA58F070CB65B282BFC2C99F6CC02E30DB -0A44A7C5D4ECF2F08CBDF3FE947D45C6AC586D4558DBFB36719927F58EBB525B -F096777DDC5BF671CBF1A9682EBEE8308AA1002F6CB6899D575A30CFACB30125C8378864C77922360ADD0AC486EFF7F4 -372863D5B6F6D7A671AB734A99403AF230DEBBBA435AB8F236B179960FAFDFC6 -6CC31C5E86CF6851D92FA973253B3B1FC9023B150980A216A63720CD74592EB7 -6643B2CA94415FE4A4E25A3086B8370172C53283360A7BD54B322BD889A5A831 -03209037BD7E96A16C69A4F0B6FEB3F2ABA1408353BE09C53485CB7C2362A490 -BE4619EC34F7FB8DFE53A8ABFDE6A831895B88D0AB1A9B4861BC7EC7F1121611 -41223F7AD8B0F28D08C1EEC2B96324F337F6428759032CAE58A04E8511BC4CBCE497EE1502F18BED5B5038E719F4F085 -1533D9B78F16803DFD8AC0C9C85ECEB6BABE9ADF83BF0B6692857BAA03843444064FCDBC89464D9E002AAFF53C65098B -466CC36F82D1AF9BA0704789B2F8B249E79D4964E7E5CB74917760E7D30AB7FA -E9673C0CF35137C179132DDCA57869E285FE6014BB32C5DD78E7549ED0076906 -A511A9999C777750FA348772A9E2881A75DEC94ED833AC282037B3E10913B150 -F20652B2CA0E0ADD34331D2462CD5AFACC4E01F24EF17C97CFF57089039AD7D8 -AFCB6518894626B9E8AFC7E7D9D0430D1D820969CE79B61B91E63D08D4995308D8403705AE4E25AB0F254D47FB03A1A2 -C43987548689A1DD9A65A3BE76F97A8470263ACAC0696B5882383AE97893B5D6 -7FB47AC9A16094C38D436F9932041A40F6B4644A633361B47C6F6E4A8F2E7C26 -BDD48C4689912872055999ECADCF93D152ACDEC34C6318F2FBBBA1A34C8CC166FCBA31AED11F95C973B21A646828CEEA -1582E265DF46C8A9A019AF338104C267873FD3C2C7AF290263FEF921CC9926E76F37FA31881307C675CF37F8A5A236D7 -790D8AF6D9F5B634F953F5751378BA5412425B950C696549902EB6A9383630D0 -6F44FF0DB55651218C81B41153C9D77B1911C5BFA70650FB9326DD6B8A1479EE -4B0891C1C24E004AB2869A858941C47346EF0789DFAEA1CD1B0E008A73AF2F7E -7E9F990652FD6632612866B6ECA726EAE2112234EBCA59D0C1BF220B94E1F472 -9C08E9DAE977DD30E61E6634F51BFD654330C34E9FB3AA2E8856B428141D7164 -7C0E756C593FFBBE2609F63D142A0D53F99772EB0722616DFC7B203BA70AA5AE -52C03D8984496DFF95503D843CA1980A6CA46AAE3390C6D4D38E277A0937429C185C118B98B6EBE0711F10F47F8D8F3F -DE8A5BFD2AB351BCAD1D7817737946EF2445E3C974C9E11BF7AD058463F0E437 -D09ABB320F59A75C4B2EE8B52E45D52C46226DD8BC8339F53229578733928F1F -C2E6F135E638C83363097EB79ED9A58E94A8E5F626E27083D1363E8BC61B55A4 -F06CC55446D0FCCCE5AD007949AE3FCA9C6FCB3C0CA1B5DE1BA65544B55326F1CF726E1F1E5CA90CA2B986930C9B9D83 +IV: 4E 6F 74 69 63 65 73 20 62 75 6C 67 65 55 77 55 +DD5D658073C2C257430FD30815480D300A4FAA72750B741431D90E7B74C6F94B +D30DBA9B63C4A5A47E6F1462819706F347B4524FB0342729B454791ECB491338 +4BF3E836BECF53BF8ECADC9E35EB94A6F2A424C4918226B7B2F3B0102BB28AFB61610BF188269855559DDEF49413635B +96BD4E3E3D02094DFE3C4E5CCBC8ED7BE679ACE3C83ADF1433BA4E08576E610D +99B4D5475099E999558BBBCFC5083B08BB99FF7F9E8EFEB222388750CBA464CD +DA94EF9FED763E6625C2FA839C8A288986664EFD12575D21575B6C77345EC6A2 +FD0A76366C8F84C2BF080A40EB6573CF41AFA51138320C187BDDB1E40CEA35E8 +5C375767AB4BE93E3A10875B4B006D4264FD9FB606381A8878AF4EC960EFC849 +01297BF2D4372C5FC376E79835D8D5A382F76E3D9367E2FA81F54C10BDB2FDA8 +415FE673D1CDAE0E7141EA78326573987E51353C91992E334FCD030E3172B8C6 +C6EA321FA06DBD86D328B5CBBCFE94628DA3D415463835229F1224EAB5F8B181 +6468B83FB26298BD496299D64DAE0F2D12B081CDE9B83AA5B19500162D7DA6E8 +2569E458EFFE0725581F7490CBF416B3E8E64C1D0F2C13DEA5BE926475F631DC +857A04B029277DA403276CADE88EAD4956A959AB9C652614F435DCE1B3B92215 +A08B60A323178B3A8644D0018C705E5E13F5F34DB41132348CEDBBCB6C75164F +A171F6CEEB240D1117DF8A5396B4E468EB03D8AA315321FA19F01E67145A861E +4255FAC9CD843A206D89D6D5204409193E594AE73D5E490F84F39DC76A0C6C23 +589E6270609485D8FEA3029A23C3084F48FB5384939ACF447CA289C4282F38BA +D1B349CF3C402BCF7F87FEF5A19E8C2748D52715092EC5668C3C61A15958F7F2 +34191142584299F22C0A007EC8E333646E57C6B406C103795ED4A2731CAF55A5 +158809D80C052D780C8AA05F88AF67FF4CF93234A7F7A78B5AA11F067623AD2D +81C22401A96674BB64D3512AEEB5F8A060537AFE1D4F2BB31ADB237E1BC61FC0 +C732F26FEC449F545CD379BF9AD2AC2769EDA98C0D7163A69E37EE17853E98AF +59375DCD3703B87223DE49174274CD28FF95BA077357A10304FC3B44E679CA91 +E9E9E87E1098CCB6C3799EB31D91E9C77EF243164FE98DAA688B22CD8ED7D944 +AC0319E2F9D0520357E61B88B17D293AE32B68661484890AEBCBADA0E7C47C40 +865F354CA28D9AABFFB3ABBD15CB22ECBDC937D292AA2D72B9D7457E870E9117 +7D2D90D3D583F77F60C8782E61011D52A452FC33F8B5E6A8AD6A2378F2819D3B +FC3C20DA524C0E1D43881F15605E18B58D9E804D15F01AB3DC9850D472C4C3A1 +25C03EAA292F03155ACD4C599030C159C2F085970B2BD905A6CF42E69D5E7BF4 +F84099879C1BD93BD77489CBEE9DB22F7F4688480A5DB9F1CF8359ADAA456B10 +21A4BE7E01B1848FB26E7CFEAD9FA841F4B80C4C2C84D07F6737F61E1FB4F4F3 +8094BD23328B5854C11FE696D21F1F58C280DC6D31F7F6B56F99E04D83949EFD +B87664A507982A6103904E81BF7D6AE1222E038D405EE84F3B5E84F4E3D6BFC7 +F102B622433FCBA3910DA43309D67FC46BE7A472BAAF8CDDEFFCD796D23DB122 +D3A015CE7E7D9CD23CED808CC72CBB42A09C5D718B58C04E1E25B3D7F46BCBB3 +02E1C8124D6B955689193369C87BDFCC66C0750E65EED7D7592E6EECA83BF1CA +4A37AC36B48DF89D664CC0390F2B1AAF553EBC2C661D14E5DDA2016C66946FF6 +3087AB22BF5A72376A888C26846B15DF842E70F8AE113E64BC855B2BA4801831 +562ED92C172D0B20799D0FDC565AED5B2A771249ACBA7D8F33176B99B3C4CD88 +448D6DEAC1C01D1B96EBB480D22A5AD011DC36655D8318BACCEE630F9BA5CC20 +2F9C204D0A2E7A5686F7684E438DC3DCEE400AE23306D6B604A5905DB33DBF0F +50A7E7773F4BCA75247E2E9ACF9025F5DF0370A2C20E2ECE8EDC00F25761D158 +2F2B0A5F9A71FD2B1E2AA55B3C6EA75789FC5A20CE3E8790E0260E59D10E7D96 +EBBD9E861DCC0A85A2828026C98D69F882F05CE2DE0CD58AB06FE2E778CBEABC40E591AE59EEA3A4012F9266E61D4971 +8716AE40A23BB9D23DC9EE0917D7E2D50891A4D8495F08174953A883B4EA61DA +F2B9B78FA7239BB61E4FB5DDA4286C5FA2CFF34A4456781C55AC4E23644E2A46 +D570A2BD25921E6E3409E0017BC9547F1BFF0992EDC35F0BC0C526B03738D8F0 +BDA88E42BAA3025E730AF8244F2F0DC5E8F9B531882A5E1EF48BF6F0B86DB186 +03E2FFCDF9243D7DB19DAEAF2563D2CF604D82E73E501071523EA1DD3E3A1313 +702A8F9F8F5D670D999D2D4F6388F22846B23FFC4EB53A71DAC92BED76A7D1111C0536FA5293B148B501E8BE70F84C3F +A605F5B456CD733EAD4E12432617936494C1ACA42B2CEE639151AD8846D02EFBF17575565A458B7E8D6E2929BEE70ADB +F5628A7A8602546DD4BAB166DF796B7011A3F66F896599FFC7467045B1B3EB4F +BE6B94C4A7C701FBF7D189FA3C371DC7D208CF84E1EEC00A4C2F4DC14BF8CC17 +AFF0D5E080A7DAD2AFE0C498D3DFD58EAA0AE4F4F96883B46C36537B66D344C2 +E0435324027761281E17E62CCDB964E7B87EA673205DC76109570DEEA25E9C86 +84EB0F58B6F3ED2B6D62BA21D0EBCA0A1C7E17C792EBB47A2EA596AB6E4E98A78908E5E31CCE35CD8091F8D39DA52EF5 +C4027456DE3BF67B080AC563B2F38318B04DB5ED968D33D7493F0FC5CC51B165 +7C59386D2F5AE8F14AA3C1FBD953F5B7B153A3B94B20BA51C18B18BF2F0DB037 +A8E7A91824FA42085CEEE47A80A9269779AE69AFADFA210E8A4C48585F827FCB99509686D7C62215E556DBE73BB476B5 +514F6030D39CC0DE60B0280EC55F32CE93CA5D9446E7DDF03949AAB7C1276CA036A65A5B58C0904158117768D0AA22A6 +F75FC0AA714F37B75E2FDAFB837B671CD7A349A8D77565C6C0F2943A4B0B88A8 +E8C43768DA5C853253C9FD9D2112A66C509DAA37601CBD83DBFA59329E015D40 +2006DA52D75063F76A99A726E0E7B21A737A7D759C470867FF7EECC61B6A5637 +5B474E6EC7E10EB5415B5593E2C03ED5C079B2CFE2B7BCBC8B3D758727043CD4 +1F3328F14AE9C0867143929D0020AF17F58D09C1B47DBD92FFEAFD2260779DFD +CB194E675F700497AA04686CB17CF77F4295D095A0500D41FD0DC2C4FD8BE294 +328E577B4A997108194BEE91E68190663C4AA3E142B8BEF2A99B70CB4D11897DAA990D5F1FC3D33098982D73F63A26DA +4D5DC5AEF974F0E15A0B06726AC0D2589B1C931B848BFD15BFFE01B8F3BF8E50 +A9B1734BCB160609DF5B02E3C9E459D2DE3297920CBAA7677692C27886527D82 +A161A69B3BE5975F8742AE5461A4983201D7B6B91978D87C0E0D71BF74F5A6D7 +FE3F0F9875D8D2C6E51923E610A812E6BAC4E65FCD4026AA965600A4235AF3EE03D27B793C4E629948BADA5C44EE8915 From 221efbca0c8ae734fd2204897ee3af0ac9f8a182 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sun, 15 Mar 2026 02:45:41 +0300 Subject: [PATCH 11/71] upd changelog --- CHANGELOG.md | 3 ++- documentation/SubGHzSupportedSystems.md | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cda75a330..12f30cf45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,9 @@ - Current API: 87.6 * SubGHz: Add **Nord ICE** protocol (33 bits, Static) * SubGHz: Add **CAME TOP44FGN** support in CAME TWEE protocol +* SubGHz: Add all 0x0s and all 0xFs KeeLoq MF codes for normal and simple learning * NFC: Add Mifare Ultralight C Write Support (by @haw8411) -* Apps: Build tag (**12mar2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) +* Apps: Build tag (**14mar2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * Display: Extend lcd contrast range to full ST756x 6-bit range (by @ShaTie) (reduced to -10 to +18 to avoid fully unreadable state)

diff --git a/documentation/SubGHzSupportedSystems.md b/documentation/SubGHzSupportedSystems.md index 8c552e9c7..87615f2b0 100644 --- a/documentation/SubGHzSupportedSystems.md +++ b/documentation/SubGHzSupportedSystems.md @@ -163,6 +163,10 @@ The following manufacturers have KeeLoq support in Unleashed firmware: - Tomahawk TZ-9030 (KeeLoq, 64 bits) - Tomahawk Z,X 3-5 (KeeLoq, 64 bits) - ZX-730-750-1055 (KeeLoq, 64 bits) +- Zero_Simple (KeeLoq, 64 bits) +- Zero_Normal (KeeLoq, 64 bits) +- FFFF_Simple (KeeLoq, 64 bits) +- FFFF_Normal (KeeLoq, 64 bits) *Note: Most KeeLoq manufacturers operate in the 433 MHz and 868 MHz frequency bands with AM650 modulation. Some operate at other frequencies or modulations. Not all KeeLoq systems are supported for full decoding or emulation.* From 6899ba9351ddf2326fe929366f5acba9a250402c Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 20 Mar 2026 03:09:20 +0300 Subject: [PATCH 12/71] sync apps --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12f30cf45..fb5a6123c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ * SubGHz: Add **CAME TOP44FGN** support in CAME TWEE protocol * SubGHz: Add all 0x0s and all 0xFs KeeLoq MF codes for normal and simple learning * NFC: Add Mifare Ultralight C Write Support (by @haw8411) -* Apps: Build tag (**14mar2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) +* Apps: Build tag (**20mar2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * Display: Extend lcd contrast range to full ST756x 6-bit range (by @ShaTie) (reduced to -10 to +18 to avoid fully unreadable state)

From b1796cc28d590deec2cd84bbbf794c574c80ab71 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:46:28 +0300 Subject: [PATCH 13/71] NFC: Fix BusFault in Write to Initial Card OFW PR 4362 by akrylysov --- .../protocols/mf_classic/mf_classic_poller.c | 37 +++++++++++-------- .../mf_classic/mf_classic_poller_i.h | 1 + 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller.c b/lib/nfc/protocols/mf_classic/mf_classic_poller.c index ae2f5467f..86a3ccddd 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller.c +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller.c @@ -65,24 +65,28 @@ void mf_classic_poller_free(MfClassicPoller* instance) { bit_buffer_free(instance->tx_encrypted_buffer); bit_buffer_free(instance->rx_encrypted_buffer); - // Clean up resources in MfClassicPollerDictAttackContext - MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; + // Clean up dict attack resources when the poller was in dict attack mode. + if(instance->mode == MfClassicPollerModeDictAttackStandard || + instance->mode == MfClassicPollerModeDictAttackEnhanced || + instance->mode == MfClassicPollerModeDictAttackCUID) { + MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; - // Free the dictionaries - if(dict_attack_ctx->mf_classic_system_dict) { - keys_dict_free(dict_attack_ctx->mf_classic_system_dict); - dict_attack_ctx->mf_classic_system_dict = NULL; - } - if(dict_attack_ctx->mf_classic_user_dict) { - keys_dict_free(dict_attack_ctx->mf_classic_user_dict); - dict_attack_ctx->mf_classic_user_dict = NULL; - } + // Free the dictionaries + if(dict_attack_ctx->mf_classic_system_dict) { + keys_dict_free(dict_attack_ctx->mf_classic_system_dict); + dict_attack_ctx->mf_classic_system_dict = NULL; + } + if(dict_attack_ctx->mf_classic_user_dict) { + keys_dict_free(dict_attack_ctx->mf_classic_user_dict); + dict_attack_ctx->mf_classic_user_dict = NULL; + } - // Free the nested nonce array if it exists - if(dict_attack_ctx->nested_nonce.nonces) { - free(dict_attack_ctx->nested_nonce.nonces); - dict_attack_ctx->nested_nonce.nonces = NULL; - dict_attack_ctx->nested_nonce.count = 0; + // Free the nested nonce array if it exists + if(dict_attack_ctx->nested_nonce.nonces) { + free(dict_attack_ctx->nested_nonce.nonces); + dict_attack_ctx->nested_nonce.nonces = NULL; + dict_attack_ctx->nested_nonce.count = 0; + } } free(instance); @@ -162,6 +166,7 @@ NfcCommand mf_classic_poller_handler_start(MfClassicPoller* instance) { instance->mfc_event.type = MfClassicPollerEventTypeRequestMode; command = instance->callback(instance->general_event, instance->context); + instance->mode = instance->mfc_event_data.poller_mode.mode; if(instance->mfc_event_data.poller_mode.mode == MfClassicPollerModeDictAttackStandard || instance->mfc_event_data.poller_mode.mode == MfClassicPollerModeDictAttackCUID) { diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller_i.h b/lib/nfc/protocols/mf_classic/mf_classic_poller_i.h index 607b126a0..3043deb15 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller_i.h +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller_i.h @@ -183,6 +183,7 @@ struct MfClassicPoller { MfClassicType current_type_check; uint8_t sectors_total; + MfClassicPollerMode mode; MfClassicPollerModeContext mode_ctx; Crypto1* crypto; From 49ac76f953867b1704c816b5ba79b659b61395b8 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:47:58 +0300 Subject: [PATCH 14/71] upd changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb5a6123c..7ef40c78b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ * SubGHz: Add **CAME TOP44FGN** support in CAME TWEE protocol * SubGHz: Add all 0x0s and all 0xFs KeeLoq MF codes for normal and simple learning * NFC: Add Mifare Ultralight C Write Support (by @haw8411) -* Apps: Build tag (**20mar2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) +* OFW PR 4362: NFC: Fix BusFault in Write to Initial Card (by @akrylysov) +* Apps: Build tag (**22mar2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * Display: Extend lcd contrast range to full ST756x 6-bit range (by @ShaTie) (reduced to -10 to +18 to avoid fully unreadable state)

From a5f6285e917240101bc86444ebf36a6b9f2b7e66 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 27 Mar 2026 12:26:36 +0300 Subject: [PATCH 15/71] fix came twee repeats value --- CHANGELOG.md | 3 ++- lib/subghz/protocols/came_twee.c | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ef40c78b..fdcb72b34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,10 @@ * SubGHz: Add **Nord ICE** protocol (33 bits, Static) * SubGHz: Add **CAME TOP44FGN** support in CAME TWEE protocol * SubGHz: Add all 0x0s and all 0xFs KeeLoq MF codes for normal and simple learning +* SubGHz: Fix CAME TWEE repeats count for button click * NFC: Add Mifare Ultralight C Write Support (by @haw8411) * OFW PR 4362: NFC: Fix BusFault in Write to Initial Card (by @akrylysov) -* Apps: Build tag (**22mar2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) +* Apps: Build tag (**27mar2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * Display: Extend lcd contrast range to full ST756x 6-bit range (by @ShaTie) (reduced to -10 to +18 to avoid fully unreadable state)

diff --git a/lib/subghz/protocols/came_twee.c b/lib/subghz/protocols/came_twee.c index 55947efbf..b23db0cc9 100644 --- a/lib/subghz/protocols/came_twee.c +++ b/lib/subghz/protocols/came_twee.c @@ -109,7 +109,7 @@ void* subghz_protocol_encoder_came_twee_alloc(SubGhzEnvironment* environment) { instance->base.protocol = &subghz_protocol_came_twee; instance->generic.protocol_name = instance->base.protocol->name; - instance->encoder.repeat = 3; + instance->encoder.repeat = 1; instance->encoder.size_upload = 1536; // 1308 instance->encoder.upload = malloc(instance->encoder.size_upload * sizeof(LevelDuration)); instance->encoder.is_running = false; From ecc3e78efa3b077ca4b37f67aff8ae6f8b1e9734 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sun, 29 Mar 2026 14:31:50 +0300 Subject: [PATCH 16/71] Fix EMV crash --- CHANGELOG.md | 4 ++-- lib/nfc/helpers/iso14443_4_layer.c | 5 ++++- lib/toolbox/bit_buffer.c | 1 - 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdcb72b34..c37572301 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ * SubGHz: Add **Nord ICE** protocol (33 bits, Static) * SubGHz: Add **CAME TOP44FGN** support in CAME TWEE protocol * SubGHz: Add all 0x0s and all 0xFs KeeLoq MF codes for normal and simple learning -* SubGHz: Fix CAME TWEE repeats count for button click +* SubGHz: **Fix CAME TWEE repeats count for button click** +* NFC: **Fix "MIR" and other EMV cards crash on Read** (by @Dmitry422) * NFC: Add Mifare Ultralight C Write Support (by @haw8411) * OFW PR 4362: NFC: Fix BusFault in Write to Initial Card (by @akrylysov) * Apps: Build tag (**27mar2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) @@ -12,7 +13,6 @@

#### Known NFC post-refactor regressions list: - Mifare Mini clones reading is broken (original mini working fine) (OFW) -- While reading some EMV capable cards via NFC->Read flipper may crash due to Desfire poller issue, read those cards via Extra actions->Read specific card type->EMV ---- diff --git a/lib/nfc/helpers/iso14443_4_layer.c b/lib/nfc/helpers/iso14443_4_layer.c index 4a92956a9..86bb25197 100644 --- a/lib/nfc/helpers/iso14443_4_layer.c +++ b/lib/nfc/helpers/iso14443_4_layer.c @@ -172,7 +172,10 @@ bool iso14443_4_layer_decode_response( bit_buffer_copy_right(output_data, block_data, 1); } else { if(!bit_buffer_starts_with_byte(block_data, instance->pcb_prev)) break; - bit_buffer_copy_right(output_data, block_data, 1); + // Fix for some EMV cards with strange response + if(bit_buffer_get_size_bytes(block_data) > 1) { + bit_buffer_copy_right(output_data, block_data, 1); + } ret = true; } } while(false); diff --git a/lib/toolbox/bit_buffer.c b/lib/toolbox/bit_buffer.c index 36869bac2..e261e80d4 100644 --- a/lib/toolbox/bit_buffer.c +++ b/lib/toolbox/bit_buffer.c @@ -58,7 +58,6 @@ void bit_buffer_copy_right(BitBuffer* buf, const BitBuffer* other, size_t start_ furi_check(buf); furi_check(other); furi_check(bit_buffer_get_size_bytes(other) > start_index); - // TODO: Fix crash furi_check(buf->capacity_bytes >= bit_buffer_get_size_bytes(other) - start_index); memcpy(buf->data, other->data + start_index, bit_buffer_get_size_bytes(other) - start_index); From 4cc5b089ff2e10dc28a23cbe36cd10a4029e6f14 Mon Sep 17 00:00:00 2001 From: mxcdoam <72457810+mxcdoam@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:15:35 +0300 Subject: [PATCH 17/71] applicatin.fam update --- applications/main/nfc/application.fam | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index 275da7417..9e5caf8df 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -305,6 +305,33 @@ App( sources=["plugins/supported_cards/plantain.c"], ) +App( + appid="szppk_so_parser", + apptype=FlipperAppType.PLUGIN, + entry_point="szppk_so_plugin_ep", + targets=["f7"], + requires=["nfc"], + sources=["plugins/supported_cards/szppk_so.c"], +) + +App( + appid="sk_tk_parser", + apptype=FlipperAppType.PLUGIN, + entry_point="sk_tk_plugin_ep", + targets=["f7"], + requires=["nfc"], + sources=["plugins/supported_cards/sk_tk.c"], +) + +App( + appid="sev_tk_parser", + apptype=FlipperAppType.PLUGIN, + entry_point="sev_tk_plugin_ep", + targets=["f7"], + requires=["nfc"], + sources=["plugins/supported_cards/sevppk_tk.c"], +) + App( appid="two_cities_parser", apptype=FlipperAppType.PLUGIN, From f75a1c69aa1fb26e394c15460e5f951e8e8afec5 Mon Sep 17 00:00:00 2001 From: mxcdoam <72457810+mxcdoam@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:22:20 +0300 Subject: [PATCH 18/71] updated parsers --- .../nfc/plugins/supported_cards/plantain.c | 839 +++++++++++++----- .../nfc/plugins/supported_cards/sevppk_tk.c | 399 +++++++++ .../main/nfc/plugins/supported_cards/sk_tk.c | 406 +++++++++ .../nfc/plugins/supported_cards/szppk_so.c | 447 ++++++++++ .../nfc/plugins/supported_cards/two_cities.c | 2 +- .../main/nfc/resources/nfc/assets/sev_id.nfc | 124 +++ .../main/nfc/resources/nfc/assets/sk_id.nfc | 85 ++ .../main/nfc/resources/nfc/assets/sz_id.nfc | 151 ++++ 8 files changed, 2211 insertions(+), 242 deletions(-) create mode 100644 applications/main/nfc/plugins/supported_cards/sevppk_tk.c create mode 100644 applications/main/nfc/plugins/supported_cards/sk_tk.c create mode 100644 applications/main/nfc/plugins/supported_cards/szppk_so.c create mode 100644 applications/main/nfc/resources/nfc/assets/sev_id.nfc create mode 100644 applications/main/nfc/resources/nfc/assets/sk_id.nfc create mode 100644 applications/main/nfc/resources/nfc/assets/sz_id.nfc diff --git a/applications/main/nfc/plugins/supported_cards/plantain.c b/applications/main/nfc/plugins/supported_cards/plantain.c index add7ab560..53bacfbf2 100644 --- a/applications/main/nfc/plugins/supported_cards/plantain.c +++ b/applications/main/nfc/plugins/supported_cards/plantain.c @@ -1,23 +1,21 @@ +//Based on parsers written by Leptoptilos and Assasinfil + #include "nfc_supported_card_plugin.h" - #include - #include #include #include #include - -#define TAG "Plantain" - -void from_minutes_to_datetime(uint32_t minutes, DateTime* datetime, uint16_t start_year) { - uint32_t timestamp = minutes * 60; - DateTime start_datetime = {0}; - start_datetime.year = start_year - 1; - start_datetime.month = 12; - start_datetime.day = 31; - timestamp += datetime_datetime_to_timestamp(&start_datetime); - datetime_timestamp_to_datetime(timestamp, datetime); -} +#include +#define PLANTAIN_EPOCH_START 1262304000 //2010-01-01 +#define PPK_WHOLE_EPOCH_START 946684800 //2000-01-01 +#define PPK_CURRENT_EPOCH_START 1388534400 //2014-01-01 +#define SECONDS_IN_A_DAY 86400 +#define SECONDS_IN_A_MINUTE 60 +#define FIRST_PPK_TICKET_OFFSET 101 +#define FIRST_TICKET_VALUE_BLOCK 104 +#define SECOND_PPK_TICKET_OFFSET 102 +#define SECOND_TICKET_VALUE_BLOCK 108 typedef struct { uint64_t a; @@ -30,79 +28,549 @@ typedef struct { } PlantainCardConfig; static const MfClassicKeyPair plantain_1k_keys[] = { - {.a = 0xffffffffffff, .b = 0xffffffffffff}, - {.a = 0xffffffffffff, .b = 0xffffffffffff}, - {.a = 0xffffffffffff, .b = 0xffffffffffff}, - {.a = 0xffffffffffff, .b = 0xffffffffffff}, - {.a = 0xe56ac127dd45, .b = 0x19fc84a3784b}, - {.a = 0x77dabc9825e1, .b = 0x9764fec3154a}, - {.a = 0xffffffffffff, .b = 0xffffffffffff}, - {.a = 0xffffffffffff, .b = 0xffffffffffff}, - {.a = 0x26973ea74321, .b = 0xd27058c6e2c7}, - {.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3}, - {.a = 0xea0fd73cb149, .b = 0x29c35fa068fb}, - {.a = 0xc76bf71a2509, .b = 0x9ba241db3f56}, - {.a = 0xacffffffffff, .b = 0x71f3a315ad26}, - {.a = 0xffffffffffff, .b = 0xffffffffffff}, - {.a = 0xffffffffffff, .b = 0xffffffffffff}, - {.a = 0xffffffffffff, .b = 0xffffffffffff}, + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //0 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //1 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //2 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //3 + {.a = 0xe56ac127dd45, .b = 0x19fc84a3784b}, //4 + {.a = 0x77dabc9825e1, .b = 0x9764fec3154a}, //5 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //6 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //7 + {.a = 0x26973ea74321, .b = 0xd27058c6e2c7}, //8 + {.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3}, //9 + {.a = 0xea0fd73cb149, .b = 0x29c35fa068fb}, //10 + {.a = 0xc76bf71a2509, .b = 0x9ba241db3f56}, //11 + {.a = 0xacffffffffff, .b = 0x71f3a315ad26}, //12 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //13 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //14 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //15 + {.a = 0x72f96bdd3714, .b = 0x462225cd34cf}, //16 + {.a = 0x044ce1872bc3, .b = 0x8c90c70cff4a}, //17 + {.a = 0xbc2d1791dec1, .b = 0xca96a487de0b}, //18 + {.a = 0x8791b2ccb5c4, .b = 0xc956c3b80da3}, //19 + {.a = 0x8e26e45e7d65, .b = 0x8e65b3af7d22}, //20 + {.a = 0x0f318130ed18, .b = 0x0c420a20e056}, //21 + {.a = 0x045ceca15535, .b = 0x31bec3d9e510}, //22 + {.a = 0x9d993c5d4ef4, .b = 0x86120e488abf}, //23 + {.a = 0xc65d4eaa645b, .b = 0xb69d40d1a439}, //24 + {.a = 0x3a8a139c20b4, .b = 0x8818a9c5d406}, //25 + {.a = 0xbaff3053b496, .b = 0x4b7cb25354d3}, //26 + {.a = 0x7413b599c4ea, .b = 0xb0a2aaf3a1ba}, //27 + {.a = 0x7413b599c4ea, .b = 0xb0a2aaf3a1ba}, //28 + {.a = 0x0ce7cd2cc72b, .b = 0xfa1fbb3f0f1f}, //29 + {.a = 0x0eb23cc8110b, .b = 0x04dc35277635}, //30 + {.a = 0xbc4580b7f20b, .b = 0xd0a4131fb290}, //31 + }; static const MfClassicKeyPair plantain_4k_keys[] = { - {.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff}, - {.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff}, - {.a = 0xe56ac127dd45, .b = 0x19fc84a3784b}, {.a = 0x77dabc9825e1, .b = 0x9764fec3154a}, - {.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff}, - {.a = 0x26973ea74321, .b = 0xd27058c6e2c7}, {.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3}, - {.a = 0xea0fd73cb149, .b = 0x29c35fa068fb}, {.a = 0xc76bf71a2509, .b = 0x9ba241db3f56}, - {.a = 0xacffffffffff, .b = 0x71f3a315ad26}, {.a = 0xffffffffffff, .b = 0xffffffffffff}, - {.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff}, - {.a = 0x72f96bdd3714, .b = 0x462225cd34cf}, {.a = 0x044ce1872bc3, .b = 0x8c90c70cff4a}, - {.a = 0xbc2d1791dec1, .b = 0xca96a487de0b}, {.a = 0x8791b2ccb5c4, .b = 0xc956c3b80da3}, - {.a = 0x8e26e45e7d65, .b = 0x8e65b3af7d22}, {.a = 0x0f318130ed18, .b = 0x0c420a20e056}, - {.a = 0x045ceca15535, .b = 0x31bec3d9e510}, {.a = 0x9d993c5d4ef4, .b = 0x86120e488abf}, - {.a = 0xc65d4eaa645b, .b = 0xb69d40d1a439}, {.a = 0x3a8a139c20b4, .b = 0x8818a9c5d406}, - {.a = 0xbaff3053b496, .b = 0x4b7cb25354d3}, {.a = 0x7413b599c4ea, .b = 0xb0a2AAF3A1BA}, - {.a = 0x0ce7cd2cc72b, .b = 0xfa1fbb3f0f1f}, {.a = 0x0be5fac8b06a, .b = 0x6f95887a4fd3}, - {.a = 0x0eb23cc8110b, .b = 0x04dc35277635}, {.a = 0xbc4580b7f20b, .b = 0xd0a4131fb290}, - {.a = 0x7a396f0d633d, .b = 0xad2bdc097023}, {.a = 0xa3faa6daff67, .b = 0x7600e889adf9}, - {.a = 0xfd8705e721b0, .b = 0x296fc317a513}, {.a = 0x22052b480d11, .b = 0xe19504c39461}, - {.a = 0xa7141147d430, .b = 0xff16014fefc7}, {.a = 0x8a8d88151a00, .b = 0x038b5f9b5a2a}, - {.a = 0xb27addfb64b0, .b = 0x152fd0c420a7}, {.a = 0x7259fa0197c6, .b = 0x5583698df085}, + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //0 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //1 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //2 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //3 + {.a = 0xe56ac127dd45, .b = 0x19fc84a3784b}, //4 + {.a = 0x77dabc9825e1, .b = 0x9764fec3154a}, //5 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //6 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //7 + {.a = 0x26973ea74321, .b = 0xd27058c6e2c7}, //8 + {.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3}, //9 + {.a = 0xea0fd73cb149, .b = 0x29c35fa068fb}, //10 + {.a = 0xc76bf71a2509, .b = 0x9ba241db3f56}, //11 + {.a = 0xacffffffffff, .b = 0x71f3a315ad26}, //12 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //13 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //14 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //15 + {.a = 0x72f96bdd3714, .b = 0x462225cd34cf}, //16 + {.a = 0x044ce1872bc3, .b = 0x8c90c70cff4a}, //17 + {.a = 0xbc2d1791dec1, .b = 0xca96a487de0b}, //18 + {.a = 0x8791b2ccb5c4, .b = 0xc956c3b80da3}, //19 + {.a = 0x8e26e45e7d65, .b = 0x8e65b3af7d22}, //20 + {.a = 0x0f318130ed18, .b = 0x0c420a20e056}, //21 + {.a = 0x045ceca15535, .b = 0x31bec3d9e510}, //22 + {.a = 0x9d993c5d4ef4, .b = 0x86120e488abf}, //23 + {.a = 0xc65d4eaa645b, .b = 0xb69d40d1a439}, //24 + {.a = 0x3a8a139c20b4, .b = 0x8818a9c5d406}, //25 + {.a = 0xbaff3053b496, .b = 0x4b7cb25354d3}, //26 + {.a = 0x7413b599c4ea, .b = 0xb0a2AAF3A1BA}, //27 + {.a = 0x0ce7cd2cc72b, .b = 0xfa1fbb3f0f1f}, //28 + {.a = 0x0be5fac8b06a, .b = 0x6f95887a4fd3}, //29 + {.a = 0x0eb23cc8110b, .b = 0x04dc35277635}, //30 + {.a = 0xbc4580b7f20b, .b = 0xd0a4131fb290}, //31 + {.a = 0x7a396f0d633d, .b = 0xad2bdc097023}, //32 + {.a = 0xa3faa6daff67, .b = 0x7600e889adf9}, //33 + {.a = 0xfd8705e721b0, .b = 0x296fc317a513}, //34 + {.a = 0x22052b480d11, .b = 0xe19504c39461}, //35 + {.a = 0xa7141147d430, .b = 0xff16014fefc7}, //36 + {.a = 0x8a8d88151a00, .b = 0x038b5f9b5a2a}, //37 + {.a = 0xb27addfb64b0, .b = 0x152fd0c420a7}, //38 + {.a = 0x7259fa0197c6, .b = 0x5583698df085}, //39 }; static const MfClassicKeyPair plantain_4k_keys_legacy[] = { - {.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff}, - {.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff}, - {.a = 0xe56ac127dd45, .b = 0x19fc84a3784b}, {.a = 0x77dabc9825e1, .b = 0x9764fec3154a}, - {.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff}, - {.a = 0x26973ea74321, .b = 0xd27058c6e2c7}, {.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3}, - {.a = 0xea0fd73cb149, .b = 0x29c35fa068fb}, {.a = 0xc76bf71a2509, .b = 0x9ba241db3f56}, - {.a = 0xacffffffffff, .b = 0x71f3a315ad26}, {.a = 0xffffffffffff, .b = 0xffffffffffff}, - {.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff}, - {.a = 0x72f96bdd3714, .b = 0x462225cd34cf}, {.a = 0x044ce1872bc3, .b = 0x8c90c70cff4a}, - {.a = 0xbc2d1791dec1, .b = 0xca96a487de0b}, {.a = 0x8791b2ccb5c4, .b = 0xc956c3b80da3}, - {.a = 0x8e26e45e7d65, .b = 0x8e65b3af7d22}, {.a = 0x0f318130ed18, .b = 0x0c420a20e056}, - {.a = 0x045ceca15535, .b = 0x31bec3d9e510}, {.a = 0x9d993c5d4ef4, .b = 0x86120e488abf}, - {.a = 0xc65d4eaa645b, .b = 0xb69d40d1a439}, {.a = 0x46d78e850a7e, .b = 0xa470f8130991}, - {.a = 0x42e9b54e51ab, .b = 0x0231b86df52e}, {.a = 0x0f01ceff2742, .b = 0x6fec74559ca7}, - {.a = 0xb81f2b0c2f66, .b = 0xa7e2d95f0003}, {.a = 0x9ea3387a63c1, .b = 0x437e59f57561}, - {.a = 0x0eb23cc8110b, .b = 0x04dc35277635}, {.a = 0xbc4580b7f20b, .b = 0xd0a4131fb290}, - {.a = 0x7a396f0d633d, .b = 0xad2bdc097023}, {.a = 0xa3faa6daff67, .b = 0x7600e889adf9}, - {.a = 0xfd8705e721b0, .b = 0x296fc317a513}, {.a = 0x22052b480d11, .b = 0xe19504c39461}, - {.a = 0xa7141147d430, .b = 0xff16014fefc7}, {.a = 0x8a8d88151a00, .b = 0x038b5f9b5a2a}, - {.a = 0xb27addfb64b0, .b = 0x152fd0c420a7}, {.a = 0x7259fa0197c6, .b = 0x5583698df085}, + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //0 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //1 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //2 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //3 + {.a = 0xe56ac127dd45, .b = 0x19fc84a3784b}, //4 + {.a = 0x77dabc9825e1, .b = 0x9764fec3154a}, //5 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //6 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //7 + {.a = 0x26973ea74321, .b = 0xd27058c6e2c7}, //8 + {.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3}, //9 + {.a = 0xea0fd73cb149, .b = 0x29c35fa068fb}, //10 + {.a = 0xc76bf71a2509, .b = 0x9ba241db3f56}, //11 + {.a = 0xacffffffffff, .b = 0x71f3a315ad26}, //12 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //13 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //14 + {.a = 0xffffffffffff, .b = 0xffffffffffff}, //15 + {.a = 0x72f96bdd3714, .b = 0x462225cd34cf}, //16 + {.a = 0x044ce1872bc3, .b = 0x8c90c70cff4a}, //17 + {.a = 0xbc2d1791dec1, .b = 0xca96a487de0b}, //18 + {.a = 0x8791b2ccb5c4, .b = 0xc956c3b80da3}, //19 + {.a = 0x8e26e45e7d65, .b = 0x8e65b3af7d22}, //20 + {.a = 0x0f318130ed18, .b = 0x0c420a20e056}, //21 + {.a = 0x045ceca15535, .b = 0x31bec3d9e510}, //22 + {.a = 0x9d993c5d4ef4, .b = 0x86120e488abf}, //23 + {.a = 0xc65d4eaa645b, .b = 0xb69d40d1a439}, //24 + {.a = 0x46d78e850a7e, .b = 0xa470f8130991}, //25 + {.a = 0x42e9b54e51ab, .b = 0x0231b86df52e}, //26 + {.a = 0x0f01ceff2742, .b = 0x6fec74559ca7}, //27 + {.a = 0xb81f2b0c2f66, .b = 0xa7e2d95f0003}, //28 + {.a = 0x9ea3387a63c1, .b = 0x437e59f57561}, //29 + {.a = 0x0eb23cc8110b, .b = 0x04dc35277635}, //30 + {.a = 0xbc4580b7f20b, .b = 0xd0a4131fb290}, //31 + {.a = 0x7a396f0d633d, .b = 0xad2bdc097023}, //32 + {.a = 0xa3faa6daff67, .b = 0x7600e889adf9}, //33 + {.a = 0xfd8705e721b0, .b = 0x296fc317a513}, //34 + {.a = 0x22052b480d11, .b = 0xe19504c39461}, //35 + {.a = 0xa7141147d430, .b = 0xff16014fefc7}, //36 + {.a = 0x8a8d88151a00, .b = 0x038b5f9b5a2a}, //37 + {.a = 0xb27addfb64b0, .b = 0x152fd0c420a7}, //38 + {.a = 0x7259fa0197c6, .b = 0x5583698df085}, //39 }; +typedef struct { + uint16_t departure_uic; + uint16_t destination_uic; + uint16_t trip_start_uic; + uint16_t trip_end_uic; + FuriString* departure_name; + FuriString* destination_name; + FuriString* trip_start_sta_name; + FuriString* trip_end_sta_name; + uint8_t value_data; + uint8_t direction; + uint8_t current_status; + uint16_t valid_from_data; + uint16_t valid_till_data; + DateTime valid_from; + DateTime valid_till; + uint32_t tap_data; + DateTime tap_time; + uint8_t first_ticket_marker; + uint8_t second_ticket_marker; + uint64_t sys_n; + uint8_t ppk_cnt; +} PPKData; + +typedef struct { + uint64_t card_number; + FuriString* card_number_str; + uint32_t balance; + uint8_t trips_metro; + uint8_t trips_ground; + uint32_t last_trip_data; + DateTime last_trip_time; + uint16_t validator; + uint16_t fare; + uint32_t last_payment_date_data; + DateTime last_payment_date; + uint16_t last_payment_amount; + uint8_t keyset; + +} PlantainData; +// Function to map UIC codes to station names +static inline void sz_uic_to_sta(Storage* storage, const char* file_name, PPKData* ticket) { + FlipperFormat* file = flipper_format_file_alloc(storage); + FuriString* departure_uic = furi_string_alloc_printf("%04X", ticket->departure_uic); + FuriString* destination_uic = furi_string_alloc_printf("%04X", ticket->destination_uic); + FuriString* trip_start_uic = furi_string_alloc_printf("%04X", ticket->trip_start_uic); + FuriString* trip_end_uic = furi_string_alloc_printf("%04X", ticket->trip_end_uic); + + if(flipper_format_file_open_existing(file, file_name)) { + flipper_format_read_string( + file, furi_string_get_cstr(departure_uic), ticket->departure_name); + flipper_format_rewind(file); + flipper_format_read_string( + file, furi_string_get_cstr(destination_uic), ticket->destination_name); + flipper_format_rewind(file); + flipper_format_read_string( + file, furi_string_get_cstr(trip_start_uic), ticket->trip_start_sta_name); + flipper_format_rewind(file); + flipper_format_read_string( + file, furi_string_get_cstr(trip_end_uic), ticket->trip_end_sta_name); + } + + flipper_format_free(file); + furi_string_free(departure_uic); + furi_string_free(destination_uic); + furi_string_free(trip_start_uic); + furi_string_free(trip_end_uic); +} +// Function to resolve station names for a ticket, and if not found, set to "1E" + UIC code +static void resolve_station_name(Storage* storage, PPKData* ticket) { + sz_uic_to_sta(storage, EXT_PATH("nfc/assets/sz_id.nfc"), ticket); + if(furi_string_utf8_length(ticket->departure_name) <= 2) { + furi_string_printf(ticket->departure_name, "1E%04X", ticket->departure_uic); + } + if(furi_string_utf8_length(ticket->destination_name) <= 2) { + furi_string_printf(ticket->destination_name, "1E%04X", ticket->destination_uic); + } + if(furi_string_utf8_length(ticket->trip_start_sta_name) <= 2) { + furi_string_printf(ticket->trip_start_sta_name, "1E%04X", ticket->trip_start_uic); + } + if(furi_string_utf8_length(ticket->trip_end_sta_name) <= 2) { + furi_string_printf(ticket->trip_end_sta_name, "1E%04X", ticket->trip_end_uic); + } +} +// Function to extract plantain purse data from the card data +static inline void extract_purse_data( + const MfClassicData* data, + PlantainData* purse, + PPKData* ticket, + uint8_t first_ppk_ticket_offset, + uint8_t second_ppk_ticket_offset) { + uint64_t card_number = 0; + size_t uid_len = 0; + const uint8_t* uid = mf_classic_get_uid(data, &uid_len); + purse->card_number_str = furi_string_alloc(); + const uint8_t* temp_ptr = &uid[0]; + uint8_t card_number_tmp[uid_len]; + + if(uid_len == 4) { + for(size_t i = 0; i < 4; i++) { + card_number_tmp[i] = temp_ptr[3 - i]; + } + } else if(uid_len == 7) { + for(size_t i = 0; i < 7; i++) { + card_number_tmp[i] = temp_ptr[6 - i]; + } + } else { + return; + } + + for(size_t i = 0; i < uid_len; i++) { + card_number = (card_number << 8) | card_number_tmp[i]; + } + FuriString* card_number_s = furi_string_alloc(); + furi_string_cat_printf(card_number_s, "%lld", card_number); + FuriString* tmp_s = furi_string_alloc_set_str("9643 3078 "); + for(uint8_t i = 0; i < 24; i += 4) { + for(uint8_t j = 0; j < 4; j++) { + furi_string_push_back(tmp_s, furi_string_get_char(card_number_s, i + j)); + } + furi_string_push_back(tmp_s, ' '); + } + + furi_string_set(purse->card_number_str, furi_string_get_cstr(tmp_s)); + furi_string_free(card_number_s); + furi_string_free(tmp_s); + + if(data->type == MfClassicType1k) { + uint32_t balance = 0; + for(uint8_t i = 0; i < 4; i++) + balance = (balance << 8) | data->block[16].data[3 - i]; + balance /= 100; + purse->balance = balance; + purse->trips_metro = data->block[21].data[0]; + purse->trips_ground = data->block[21].data[1]; + uint32_t last_trip_timestamp = 0; + for(uint8_t i = 0; i < 3; i++) { + last_trip_timestamp = (last_trip_timestamp << 8) | data->block[21].data[4 - i]; + } + + for(uint8_t i = 0; i < 3; i++) { + purse->last_trip_data = (purse->last_trip_data << 8) | data->block[21].data[4 - i]; + } + purse->validator = (data->block[20].data[5] << 8) | data->block[20].data[4]; + uint16_t fare = ((data->block[20].data[7] << 8) | data->block[20].data[6]) / 100; + purse->fare = fare; + + for(uint8_t i = 0; i < 3; i++) { + purse->last_payment_date_data = (purse->last_payment_date_data << 8) | + data->block[18].data[4 - i]; + } + purse->last_payment_amount = ((data->block[18].data[10] << 16) | + (data->block[18].data[9] << 8) | (data->block[18].data[8])) / + 100; + last_trip_timestamp = PLANTAIN_EPOCH_START + purse->last_trip_data * SECONDS_IN_A_MINUTE; + const uint32_t last_payment_timestamp = + PLANTAIN_EPOCH_START + purse->last_payment_date_data * SECONDS_IN_A_MINUTE; + datetime_timestamp_to_datetime(last_trip_timestamp, &purse->last_trip_time); + datetime_timestamp_to_datetime(last_payment_timestamp, &purse->last_payment_date); + + } else if(data->type == MfClassicType4k) { + uint32_t balance = 0; + for(uint8_t i = 0; i < 4; i++) + balance = (balance << 8) | data->block[16].data[3 - i]; + balance /= 100; + purse->balance = balance; + purse->trips_metro = data->block[21].data[0]; + purse->trips_ground = data->block[21].data[1]; + + for(uint8_t i = 0; i < 3; i++) { + purse->last_trip_data = (purse->last_trip_data << 8) | data->block[21].data[4 - i]; + } + purse->validator = (data->block[20].data[5] << 8) | data->block[20].data[4]; + uint16_t fare = ((data->block[20].data[7] << 8) | data->block[20].data[6]) / 100; + purse->fare = fare; + + for(uint8_t i = 0; i < 3; i++) { + purse->last_payment_date_data = (purse->last_payment_date_data << 8) | + data->block[18].data[4 - i]; + } + purse->last_payment_amount = ((data->block[18].data[10] << 16) | + (data->block[18].data[9] << 8) | (data->block[18].data[8])) / + 100; + uint32_t last_trip_timestamp = + PLANTAIN_EPOCH_START + purse->last_trip_data * SECONDS_IN_A_MINUTE; + const uint32_t last_payment_timestamp = + PLANTAIN_EPOCH_START + purse->last_payment_date_data * SECONDS_IN_A_MINUTE; + datetime_timestamp_to_datetime(last_trip_timestamp, &purse->last_trip_time); + datetime_timestamp_to_datetime(last_payment_timestamp, &purse->last_payment_date); + } + + ticket->first_ticket_marker = data->block[first_ppk_ticket_offset].data[0]; + ticket->second_ticket_marker = data->block[second_ppk_ticket_offset].data[0]; +} +// Function to extract PPK ticket data from the card data +static inline void extract_ppk_data( + Storage* storage, + const MfClassicData* data, + PPKData* ticket, + bool second_ticket) { + const uint8_t* temp_ptr = &data->block[SECOND_TICKET_VALUE_BLOCK + 2].data[0]; + uint8_t sys_n_arr[6] = {0}; + + if(second_ticket == 0) { + for(size_t i = 0; i < 6; i++) { + sys_n_arr[i] = temp_ptr[7 - i]; + } + ticket->departure_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[6] << 8) | + (data->block[FIRST_PPK_TICKET_OFFSET].data[5]); + ticket->destination_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[9] << 8) | + (data->block[FIRST_PPK_TICKET_OFFSET].data[8]); + ticket->value_data = data->block[FIRST_TICKET_VALUE_BLOCK].data[0]; + ticket->current_status = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[8]; + ticket->valid_from_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[2] << 8) | + (data->block[FIRST_PPK_TICKET_OFFSET].data[1]); + ticket->valid_till_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[4] << 8) | + (data->block[FIRST_PPK_TICKET_OFFSET].data[3]); + + ticket->direction = data->block[FIRST_PPK_TICKET_OFFSET].data[14]; + ticket->tap_data = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[2] << 16) | + (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[1] << 8) | + data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[0]; + ticket->ppk_cnt = data->block[105].data[10]; + ticket->trip_start_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[4] << 8) | + (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[3]); + ticket->trip_end_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[7] << 8) | + (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[6]); + + } else if(second_ticket == 1) { + for(size_t i = 0; i < 6; i++) { + sys_n_arr[i] = temp_ptr[13 - i]; + } + ticket->departure_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[6] << 8) | + (data->block[SECOND_PPK_TICKET_OFFSET].data[5]); + ticket->destination_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[9] << 8) | + (data->block[SECOND_PPK_TICKET_OFFSET].data[8]); + ticket->value_data = data->block[SECOND_TICKET_VALUE_BLOCK].data[0]; + ticket->current_status = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[8]; + ticket->valid_from_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[2] << 8) | + (data->block[SECOND_PPK_TICKET_OFFSET].data[1]); + ticket->valid_till_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[4] << 8) | + (data->block[SECOND_PPK_TICKET_OFFSET].data[3]); + + ticket->direction = data->block[SECOND_PPK_TICKET_OFFSET].data[14]; + ticket->tap_data = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[2] << 16) | + (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[1] << 8) | + data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[0]; + ticket->ppk_cnt = data->block[109].data[10]; + ticket->trip_start_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[4] << 8) | + (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[3]); + ticket->trip_end_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[7] << 8) | + (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[6]); + } + ticket->first_ticket_marker = data->block[FIRST_PPK_TICKET_OFFSET].data[0]; + ticket->second_ticket_marker = data->block[SECOND_PPK_TICKET_OFFSET].data[0]; + uint64_t sys_n = 0; + for(size_t i = 0; i < 6; i++) { + sys_n = (sys_n << 8) | sys_n_arr[i]; + } + ticket->sys_n = sys_n; + + const uint32_t valid_from_timestamp = + PPK_WHOLE_EPOCH_START + ticket->valid_from_data * SECONDS_IN_A_DAY; + const uint32_t valid_till_timestamp = + PPK_WHOLE_EPOCH_START + ticket->valid_till_data * SECONDS_IN_A_DAY; + const uint32_t tap_timestamp = + PPK_CURRENT_EPOCH_START + ticket->tap_data * SECONDS_IN_A_MINUTE; + datetime_timestamp_to_datetime(valid_from_timestamp, &ticket->valid_from); + datetime_timestamp_to_datetime(valid_till_timestamp, &ticket->valid_till); + datetime_timestamp_to_datetime(tap_timestamp, &ticket->tap_time); + resolve_station_name(storage, ticket); +} +// Function to format and print plantain purse data +static void printf_plantain_data(FuriString* parsed_data, PlantainData* purse) { + furi_string_printf(parsed_data, "\e#Plantain Card"); + furi_string_cat_printf( + parsed_data, + "\nNumber: %s\nBalance: %ld RUB\nMetro Trips: %d\nGround Trips: %d\nLast Trip: %02d.%02d.%04d %02d:%02d", + furi_string_get_cstr(purse->card_number_str), + purse->balance, + purse->trips_metro, + purse->trips_ground, + purse->last_trip_time.day, + purse->last_trip_time.month, + purse->last_trip_time.year, + purse->last_trip_time.hour, + purse->last_trip_time.minute); + + furi_string_cat_printf( + parsed_data, + "\nValidator: %d\nFare: %d RUB\nRefilled on: %02d.%02d.%04d %02d:%02d\nAmount: %d RUB", + purse->validator, + purse->fare, + purse->last_payment_date.day, + purse->last_payment_date.month, + purse->last_payment_date.year, + purse->last_payment_date.hour, + purse->last_payment_date.minute, + purse->last_payment_amount); + + if(purse->keyset == 1) + furi_string_cat_printf(parsed_data, "\nPPK keys installed:> YES"); + else + furi_string_cat_printf(parsed_data, "\nPPK keys installed:> NO"); + + furi_string_free(purse->card_number_str); +} + +// Function to format and print PPK ticket data +static void printf_ppk_data(FuriString* parsed_data, PPKData* ticket, bool ticket_number) { + if(ticket_number == 0) { + furi_string_cat_printf(parsed_data, "\n\n\e#PPK Ticket:\n"); + switch(ticket->first_ticket_marker) { + case 0x33: + furi_string_cat_printf(parsed_data, "Type:> 1 ride"); + break; + case 0x34: + furi_string_cat_printf(parsed_data, "Type:> 2 rides (Mon.-Fri.)"); + break; + case 0x35: + furi_string_cat_printf(parsed_data, "Type:> 2 rides (Fri.-Mon.)"); + break; + case 0x36: + furi_string_cat_printf(parsed_data, "Type:> 2 rides (Sat.-Mon."); + break; + default: + furi_string_cat_printf( + parsed_data, "Type:> Unknown, 0x%02X", ticket->first_ticket_marker); + } + } else if(ticket_number == 1) { + furi_string_cat_printf(parsed_data, "\n\nSecond PPK Ticket:\n"); + switch(ticket->second_ticket_marker) { + case 0x33: + furi_string_cat_printf(parsed_data, "Type:> 1 ride"); + break; + case 0x34: + furi_string_cat_printf(parsed_data, "Type:> 2 rides (Mon.-Fri.)"); + break; + case 0x35: + furi_string_cat_printf(parsed_data, "Type:> 2 rides (Fri.-Mon.)"); + break; + case 0x36: + furi_string_cat_printf(parsed_data, "Type:> 2 rides (Sat.-Mon.)"); + break; + default: + furi_string_cat_printf( + parsed_data, "Type:> Unknown, 0x%02X", ticket->second_ticket_marker); + } + } + + furi_string_cat_printf( + parsed_data, + "\nFrom:> %s\nTo:> %s", + furi_string_get_cstr(ticket->departure_name), + furi_string_get_cstr(ticket->destination_name)); + + if(ticket->valid_from.day == ticket->valid_till.day) { + furi_string_cat_printf( + parsed_data, + "\nValid On: %02d-%02d-%04d", + ticket->valid_from.day, + ticket->valid_from.month, + ticket->valid_from.year); + } else { + furi_string_cat_printf( + parsed_data, + "\nValid From: %02d-%02d-%04d\nValid thru: %02d-%02d-%04d", + ticket->valid_from.day, + ticket->valid_from.month, + ticket->valid_from.year, + ticket->valid_till.day, + ticket->valid_till.month, + ticket->valid_till.year); + } + + if(ticket->direction == 1) { + furi_string_cat_printf(parsed_data, "\nDirection: One-way ->>"); + } else if(ticket->direction == 2) { + furi_string_cat_printf(parsed_data, "\nDirection: Round-trip <<-->>"); + } + furi_string_cat_printf(parsed_data, "\nRides left:> %02d", ticket->value_data); + + if(ticket->current_status == 0) { + furi_string_cat_printf(parsed_data, "\nStatus:> TICKET IS READY\n"); + } else if(ticket->current_status == 0x80) + furi_string_cat_printf( + parsed_data, + "\nStatus:> ENTERED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\n", + furi_string_get_cstr(ticket->trip_start_sta_name), + ticket->tap_time.day, + ticket->tap_time.month, + ticket->tap_time.year, + ticket->tap_time.hour, + ticket->tap_time.minute); + else if(ticket->current_status == 0x1E) + furi_string_cat_printf( + parsed_data, + "\nStatus:> EXITED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\n", + furi_string_get_cstr(ticket->trip_end_sta_name), + ticket->tap_time.day, + ticket->tap_time.month, + ticket->tap_time.year, + ticket->tap_time.hour, + ticket->tap_time.minute); + else + furi_string_cat_printf(parsed_data, "\nStatus:> UNKNOWN (%02X)\n", ticket->current_status); + + furi_string_cat_printf( + parsed_data, "SYS N:> %lld\nPPK CNT:> %03d", ticket->sys_n, ticket->ppk_cnt); +} +//Function to select a keyset based on card type static bool plantain_get_card_config(PlantainCardConfig* config, MfClassicType type) { bool success = true; if(type == MfClassicType1k) { config->data_sector = 8; config->keys = plantain_1k_keys; + } else if(type == MfClassicType4k) { config->data_sector = 8; config->keys = plantain_4k_keys; + } else { success = false; } @@ -114,11 +582,10 @@ static bool plantain_verify_type(Nfc* nfc, MfClassicType type) { bool verified = false; do { - PlantainCardConfig cfg = {}; + PlantainCardConfig cfg = {0}; if(!plantain_get_card_config(&cfg, type)) break; const uint8_t block_num = mf_classic_get_first_block_num_of_sector(cfg.data_sector); - FURI_LOG_D(TAG, "Verifying sector %lu", cfg.data_sector); MfClassicKey key = {0}; bit_lib_num_to_bytes_be(cfg.keys[cfg.data_sector].a, COUNT_OF(key.data), key.data); @@ -127,7 +594,6 @@ static bool plantain_verify_type(Nfc* nfc, MfClassicType type) { MfClassicError error = mf_classic_poller_sync_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_context); if(error != MfClassicErrorNone) { - FURI_LOG_D(TAG, "Failed to read block %u: %d", block_num, error); break; } @@ -157,7 +623,7 @@ static bool plantain_read(Nfc* nfc, NfcDevice* device) { if(error != MfClassicErrorNone) break; data->type = type; - PlantainCardConfig cfg = {}; + PlantainCardConfig cfg = {0}; if(!plantain_get_card_config(&cfg, data->type)) break; const uint8_t legacy_check_sec_num = 26; @@ -171,21 +637,28 @@ static bool plantain_read(Nfc* nfc, NfcDevice* device) { error = mf_classic_poller_sync_auth( nfc, legacy_check_block_num, &key, MfClassicKeyTypeA, NULL); if(error == MfClassicErrorNone) { - FURI_LOG_D(TAG, "Legacy keys detected"); cfg.keys = plantain_4k_keys_legacy; } MfClassicDeviceKeys keys = {}; - for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) { - bit_lib_num_to_bytes_be(cfg.keys[i].a, sizeof(MfClassicKey), keys.key_a[i].data); - FURI_BIT_SET(keys.key_a_mask, i); - bit_lib_num_to_bytes_be(cfg.keys[i].b, sizeof(MfClassicKey), keys.key_b[i].data); - FURI_BIT_SET(keys.key_b_mask, i); + if(data->type == MfClassicType1k) { + for(size_t i = 0; i < 32; i++) { + bit_lib_num_to_bytes_be(cfg.keys[i].a, sizeof(MfClassicKey), keys.key_a[i].data); + FURI_BIT_SET(keys.key_a_mask, i); + bit_lib_num_to_bytes_be(cfg.keys[i].b, sizeof(MfClassicKey), keys.key_b[i].data); + FURI_BIT_SET(keys.key_b_mask, i); + } + } else { + for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) { + bit_lib_num_to_bytes_be(cfg.keys[i].a, sizeof(MfClassicKey), keys.key_a[i].data); + FURI_BIT_SET(keys.key_a_mask, i); + bit_lib_num_to_bytes_be(cfg.keys[i].b, sizeof(MfClassicKey), keys.key_b[i].data); + FURI_BIT_SET(keys.key_b_mask, i); + } } error = mf_classic_poller_sync_read(nfc, &keys, data); if(error == MfClassicErrorNotPresent) { - FURI_LOG_W(TAG, "Failed to read data"); break; } @@ -198,18 +671,22 @@ static bool plantain_read(Nfc* nfc, NfcDevice* device) { return is_read; } - +//Main parsing function static bool plantain_parse(const NfcDevice* device, FuriString* parsed_data) { furi_assert(device); - size_t uid_len = 0; const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic); - const uint8_t* uid = mf_classic_get_uid(data, &uid_len); - + PPKData ticket = {0}; + ticket.departure_name = furi_string_alloc(); + ticket.destination_name = furi_string_alloc(); + ticket.trip_start_sta_name = furi_string_alloc(); + ticket.trip_end_sta_name = furi_string_alloc(); + PlantainData purse = {0}; + Storage* storage = furi_record_open(RECORD_STORAGE); bool parsed = false; do { // Verify card type - PlantainCardConfig cfg = {}; + PlantainCardConfig cfg = {0}; if(!plantain_get_card_config(&cfg, data->type)) break; // Verify key @@ -220,167 +697,47 @@ static bool plantain_parse(const NfcDevice* device, FuriString* parsed_data) { bit_lib_bytes_to_num_be(sec_tr->key_a.data, COUNT_OF(sec_tr->key_a.data)); if(key != cfg.keys[cfg.data_sector].a) break; - furi_string_printf(parsed_data, "\e#Plantain card\n"); + if(data->block[107].data[10] == 0x02) + purse.keyset = 0; + else + purse.keyset = 1; - const uint8_t* temp_ptr = &uid[0]; - - // UID is read from last to first byte - uint8_t card_number_tmp[uid_len]; - - if(uid_len == 4) { - for(size_t i = 0; i < 4; i++) { - card_number_tmp[i] = temp_ptr[3 - i]; - } - } else if(uid_len == 7) { - for(size_t i = 0; i < 7; i++) { - card_number_tmp[i] = temp_ptr[6 - i]; - } - } else { - break; + // Extract plantain purse data and fill PPK tickets markers + extract_purse_data( + data, &purse, &ticket, FIRST_PPK_TICKET_OFFSET, SECOND_PPK_TICKET_OFFSET); + // Print plantain purse data + printf_plantain_data(parsed_data, &purse); + if(purse.card_number_str) furi_string_free(purse.card_number_str); + // Extract and print PPK ticket data if present + if(ticket.first_ticket_marker != 0) { + extract_ppk_data(storage, data, &ticket, 0); + printf_ppk_data(parsed_data, &ticket, false); } - //UID is converted to a card number - uint64_t card_number = 0; - for(size_t i = 0; i < uid_len; i++) { - card_number = (card_number << 8) | card_number_tmp[i]; + // Extract and print second PPK ticket data if present + if(ticket.second_ticket_marker != 0) { + PPKData second_ticket = {0}; + second_ticket.departure_name = furi_string_alloc(); + second_ticket.destination_name = furi_string_alloc(); + second_ticket.trip_start_sta_name = furi_string_alloc(); + second_ticket.trip_end_sta_name = furi_string_alloc(); + + extract_ppk_data(storage, data, &second_ticket, 1); + + printf_ppk_data(parsed_data, &second_ticket, true); + + furi_string_free(second_ticket.departure_name); + furi_string_free(second_ticket.destination_name); + furi_string_free(second_ticket.trip_start_sta_name); + furi_string_free(second_ticket.trip_end_sta_name); } - // Print card number with 4-digit groups. "3" in "3078" denotes a ticket type "3 - full ticket", will differ on discounted cards. - furi_string_cat_printf(parsed_data, "Number: "); - FuriString* card_number_s = furi_string_alloc(); - furi_string_cat_printf(card_number_s, "%lld", card_number); - FuriString* tmp_s = furi_string_alloc_set_str("9643 3078 "); - for(uint8_t i = 0; i < 24; i += 4) { - for(uint8_t j = 0; j < 4; j++) { - furi_string_push_back(tmp_s, furi_string_get_char(card_number_s, i + j)); - } - furi_string_push_back(tmp_s, ' '); - } - furi_string_cat_printf(parsed_data, "%s\n", furi_string_get_cstr(tmp_s)); - // this works for 2K Plantain - if(data->type == MfClassicType1k) { - //balance - uint32_t balance = 0; - for(uint8_t i = 0; i < 4; i++) { - balance = (balance << 8) | data->block[16].data[3 - i]; - } - furi_string_cat_printf(parsed_data, "Balance: %ld rub\n", balance / 100); - - //trips - uint8_t trips_metro = data->block[21].data[0]; - uint8_t trips_ground = data->block[21].data[1]; - furi_string_cat_printf(parsed_data, "Trips: %d\n", trips_metro + trips_ground); - //trip time - uint32_t last_trip_timestamp = 0; - for(uint8_t i = 0; i < 3; i++) { - last_trip_timestamp = (last_trip_timestamp << 8) | data->block[21].data[4 - i]; - } - DateTime last_trip = {0}; - from_minutes_to_datetime(last_trip_timestamp + 24 * 60, &last_trip, 2010); - furi_string_cat_printf( - parsed_data, - "Trip start: %02d.%02d.%04d %02d:%02d\n", - last_trip.day, - last_trip.month, - last_trip.year, - last_trip.hour, - last_trip.minute); - //validator - uint16_t validator = (data->block[20].data[5] << 8) | data->block[20].data[4]; - furi_string_cat_printf(parsed_data, "Validator: %d\n", validator); - //tariff - uint16_t fare = (data->block[20].data[7] << 8) | data->block[20].data[6]; - furi_string_cat_printf(parsed_data, "Tariff: %d rub\n", fare / 100); - //trips in metro - furi_string_cat_printf(parsed_data, "Trips (Metro): %d\n", trips_metro); - //trips on ground - furi_string_cat_printf(parsed_data, "Trips (Ground): %d\n", trips_ground); - //last payment - uint32_t last_payment_timestamp = 0; - for(uint8_t i = 0; i < 3; i++) { - last_payment_timestamp = (last_payment_timestamp << 8) | - data->block[18].data[4 - i]; - } - DateTime last_payment_date = {0}; - from_minutes_to_datetime(last_payment_timestamp + 24 * 60, &last_payment_date, 2010); - furi_string_cat_printf( - parsed_data, - "Last pay: %02d.%02d.%04d %02d:%02d\n", - last_payment_date.day, - last_payment_date.month, - last_payment_date.year, - last_payment_date.hour, - last_payment_date.minute); - //Last payment amount. - uint16_t last_payment = ((data->block[18].data[10] << 16) | - (data->block[18].data[9] << 8) | (data->block[18].data[8])) / - 100; - furi_string_cat_printf(parsed_data, "Amount: %d rub", last_payment); - furi_string_free(card_number_s); - furi_string_free(tmp_s); - //This is for 4K Plantains. - } else if(data->type == MfClassicType4k) { - //balance - uint32_t balance = 0; - for(uint8_t i = 0; i < 4; i++) { - balance = (balance << 8) | data->block[16].data[3 - i]; - } - furi_string_cat_printf(parsed_data, "Balance: %ld rub\n", balance / 100); - - //trips - uint8_t trips_metro = data->block[21].data[0]; - uint8_t trips_ground = data->block[21].data[1]; - furi_string_cat_printf(parsed_data, "Trips: %d\n", trips_metro + trips_ground); - //trip time - uint32_t last_trip_timestamp = 0; - for(uint8_t i = 0; i < 3; i++) { - last_trip_timestamp = (last_trip_timestamp << 8) | data->block[21].data[4 - i]; - } - DateTime last_trip = {0}; - from_minutes_to_datetime(last_trip_timestamp + 24 * 60, &last_trip, 2010); - furi_string_cat_printf( - parsed_data, - "Trip start: %02d.%02d.%04d %02d:%02d\n", - last_trip.day, - last_trip.month, - last_trip.year, - last_trip.hour, - last_trip.minute); - //validator - uint16_t validator = (data->block[20].data[5] << 8) | data->block[20].data[4]; - furi_string_cat_printf(parsed_data, "Validator: %d\n", validator); - //tariff - uint16_t fare = (data->block[20].data[7] << 8) | data->block[20].data[6]; - furi_string_cat_printf(parsed_data, "Tariff: %d rub\n", fare / 100); - //trips in metro - furi_string_cat_printf(parsed_data, "Trips (Metro): %d\n", trips_metro); - //trips on ground - furi_string_cat_printf(parsed_data, "Trips (Ground): %d\n", trips_ground); - //last payment - uint32_t last_payment_timestamp = 0; - for(uint8_t i = 0; i < 3; i++) { - last_payment_timestamp = (last_payment_timestamp << 8) | - data->block[18].data[4 - i]; - } - DateTime last_payment_date = {0}; - from_minutes_to_datetime(last_payment_timestamp + 24 * 60, &last_payment_date, 2010); - furi_string_cat_printf( - parsed_data, - "Last pay: %02d.%02d.%04d %02d:%02d\n", - last_payment_date.day, - last_payment_date.month, - last_payment_date.year, - last_payment_date.hour, - last_payment_date.minute); - //Last payment amount - uint16_t last_payment = ((data->block[18].data[10] << 16) | - (data->block[18].data[9] << 8) | (data->block[18].data[8])) / - 100; - furi_string_cat_printf(parsed_data, "Amount: %d rub", last_payment); - furi_string_free(card_number_s); - furi_string_free(tmp_s); - } parsed = true; } while(false); + furi_string_free(ticket.departure_name); + furi_string_free(ticket.destination_name); + furi_string_free(ticket.trip_start_sta_name); + furi_string_free(ticket.trip_end_sta_name); + furi_record_close(RECORD_STORAGE); return parsed; } diff --git a/applications/main/nfc/plugins/supported_cards/sevppk_tk.c b/applications/main/nfc/plugins/supported_cards/sevppk_tk.c new file mode 100644 index 000000000..ba8eef6a2 --- /dev/null +++ b/applications/main/nfc/plugins/supported_cards/sevppk_tk.c @@ -0,0 +1,399 @@ +//Based on parsers written by Leptoptilos and Assasinfil + +#include "nfc_supported_card_plugin.h" +#include +#include +#include +#include +#include +#include +#define PPK_WHOLE_EPOCH_START 946684800 //2000-01-01 +#define PPK_CURRENT_EPOCH_START 1388534400 //2014-01-01 +#define SECONDS_IN_A_DAY 86400 +#define SECONDS_IN_A_MINUTE 60 +#define FIRST_PPK_TICKET_OFFSET 76 +#define FIRST_TICKET_VALUE_BLOCK 77 +#define SECOND_PPK_TICKET_OFFSET 88 +#define SECOND_TICKET_VALUE_BLOCK 89 + +typedef struct { + uint64_t a; + uint64_t b; +} MfClassicKeyPair; + +typedef struct { + uint16_t departure_uic; + uint16_t destination_uic; + uint16_t trip_start_uic; + uint16_t trip_end_uic; + FuriString* departure_name; + FuriString* destination_name; + FuriString* trip_start_sta_name; + FuriString* trip_end_sta_name; + uint8_t value_data; + uint8_t current_status; + uint16_t valid_from_data; + uint16_t valid_till_data; + DateTime valid_from; + DateTime valid_till; + uint32_t tap_data; + DateTime tap_time; + uint8_t first_ticket_marker; + uint8_t second_ticket_marker; + uint8_t ppk_cnt; +} TicketData; + +static const MfClassicKeyPair t_card_2k[] = { + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //0 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //1 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //2 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //3 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //4 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //5 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //6 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //7 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //8 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //9 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //10 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //11 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //12 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //13 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //14 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //15 + + {.a = 0x061029A4A6A4, .b = 0xCF43E52FC23D}, //16 + {.a = 0xE09F2EC8229C, .b = 0x044D78B70F92}, //17 + {.a = 0x66843B4FF86E, .b = 0x4474EB131577}, //18 + {.a = 0x9AC9A9B5E809, .b = 0x558B8E45E568}, //19 + {.a = 0xC114DE1AD90F, .b = 0xA0A9033E2A09}, //20 + {.a = 0xF696EFFDD838, .b = 0xA38333EC3F72}, //21 + {.a = 0xAB50BDE9CD04, .b = 0x67991D678AED}, //22 + {.a = 0x9041DD7B236C, .b = 0x8A527C8CA237}, //23 + {.a = 0x7D40A5AA63D0, .b = 0x4A61C563DD8A}, //24 + {.a = 0x05D0520EC52B, .b = 0xB40BD14E6CB4}, //25 + {.a = 0x9354F1BFF80F, .b = 0xF99D40CBBA63}, //26 + {.a = 0x84132D9FD1AF, .b = 0x1BAA2563C14D}, //27 + {.a = 0x27A8E6436B01, .b = 0x4DB883715A5C}, //28 + {.a = 0x58B173568D26, .b = 0x2E4ADD66E35E}, //29 + {.a = 0x1BE71601E73C, .b = 0xB855F30C54FB}, //30 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //31 + +}; + +static inline void sz_uic_to_sta(Storage* storage, const char* file_name, TicketData* ticket) { + FlipperFormat* file = flipper_format_file_alloc(storage); + FuriString* departure_uic = furi_string_alloc_printf("%04X", ticket->departure_uic); + FuriString* destination_uic = furi_string_alloc_printf("%04X", ticket->destination_uic); + FuriString* trip_start_uic = furi_string_alloc_printf("%04X", ticket->trip_start_uic); + FuriString* trip_end_uic = furi_string_alloc_printf("%04X", ticket->trip_end_uic); + + if(flipper_format_file_open_existing(file, file_name)) { + flipper_format_read_string( + file, furi_string_get_cstr(departure_uic), ticket->departure_name); + flipper_format_rewind(file); + flipper_format_read_string( + file, furi_string_get_cstr(destination_uic), ticket->destination_name); + flipper_format_rewind(file); + flipper_format_read_string( + file, furi_string_get_cstr(trip_start_uic), ticket->trip_start_sta_name); + flipper_format_rewind(file); + flipper_format_read_string( + file, furi_string_get_cstr(trip_end_uic), ticket->trip_end_sta_name); + } + + flipper_format_free(file); + furi_string_free(departure_uic); + furi_string_free(destination_uic); + furi_string_free(trip_start_uic); + furi_string_free(trip_end_uic); +} + +static void resolve_station_name(Storage* storage, TicketData* ticket) { + sz_uic_to_sta(storage, EXT_PATH("nfc/assets/sev_id.nfc"), ticket); + if(furi_string_utf8_length(ticket->departure_name) <= 2) { + furi_string_printf(ticket->departure_name, "1f%04X", ticket->departure_uic); + } + if(furi_string_utf8_length(ticket->destination_name) <= 2) { + furi_string_printf(ticket->destination_name, "1F%04X", ticket->destination_uic); + } + if(furi_string_utf8_length(ticket->trip_start_sta_name) <= 2) { + furi_string_printf(ticket->trip_start_sta_name, "1F%04X", ticket->trip_start_uic); + } + if(furi_string_utf8_length(ticket->trip_end_sta_name) <= 2) { + furi_string_printf(ticket->trip_end_sta_name, "1F%04X", ticket->trip_end_uic); + } +} + +static inline void extract_ppk_data( + Storage* storage, + const MfClassicData* data, + TicketData* ticket, + bool ticket_number) { + if(ticket_number == 0) { + ticket->departure_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[6] << 8) | + (data->block[FIRST_PPK_TICKET_OFFSET].data[5]); + ticket->destination_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[9] << 8) | + (data->block[FIRST_PPK_TICKET_OFFSET].data[8]); + ticket->value_data = data->block[FIRST_TICKET_VALUE_BLOCK].data[0]; + ticket->current_status = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[8]; + ticket->valid_from_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[2] << 8) | + (data->block[FIRST_PPK_TICKET_OFFSET].data[1]); + ticket->valid_till_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[4] << 8) | + (data->block[FIRST_PPK_TICKET_OFFSET].data[3]); + ticket->tap_data = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[2] << 16) | + (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[1] << 8) | + data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[0]; + ticket->ppk_cnt = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[10]; + ticket->trip_start_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[4] << 8) | + (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[3]); + ticket->trip_end_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[7] << 8) | + (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[6]); + + } else { + ticket->departure_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[6] << 8) | + (data->block[SECOND_PPK_TICKET_OFFSET].data[5]); + ticket->destination_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[9] << 8) | + (data->block[SECOND_PPK_TICKET_OFFSET].data[8]); + ticket->value_data = data->block[SECOND_TICKET_VALUE_BLOCK].data[0]; + ticket->current_status = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[8]; + ticket->valid_from_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[2] << 8) | + (data->block[SECOND_PPK_TICKET_OFFSET].data[1]); + ticket->valid_till_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[4] << 8) | + (data->block[SECOND_PPK_TICKET_OFFSET].data[3]); + ticket->tap_data = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[2] << 16) | + (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[1] << 8) | + data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[0]; + ticket->ppk_cnt = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[10]; + ticket->trip_start_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[4] << 8) | + (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[3]); + ticket->trip_end_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[7] << 8) | + (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[6]); + } + ticket->first_ticket_marker = data->block[FIRST_PPK_TICKET_OFFSET].data[0]; + ticket->second_ticket_marker = data->block[SECOND_PPK_TICKET_OFFSET].data[0]; + const uint32_t valid_from_timestamp = + PPK_WHOLE_EPOCH_START + ticket->valid_from_data * SECONDS_IN_A_DAY; + const uint32_t valid_till_timestamp = + PPK_WHOLE_EPOCH_START + ticket->valid_till_data * SECONDS_IN_A_DAY; + const uint32_t tap_timestamp = + PPK_CURRENT_EPOCH_START + ticket->tap_data * SECONDS_IN_A_MINUTE; + datetime_timestamp_to_datetime(valid_from_timestamp, &ticket->valid_from); + datetime_timestamp_to_datetime(valid_till_timestamp, &ticket->valid_till); + datetime_timestamp_to_datetime(tap_timestamp, &ticket->tap_time); + resolve_station_name(storage, ticket); +} + +static void + printf_transport_card(FuriString* parsed_data, TicketData* ticket, bool ticket_number) { + if(ticket->departure_uic == 0x0000) { + furi_string_cat_printf( + parsed_data, + "\e#Unknown SevPPK Card\n NO TICKET DATA FOUND \n\nTHE TICKET IS NOT ISSUED\nOR LAYOUT IS UNKNOWN\n"); + return; + } else { + if(ticket_number == 0) { + furi_string_cat_printf(parsed_data, "\e#SevPPK Transport Card\n"); + switch(ticket->first_ticket_marker) { + case 0x02: + furi_string_cat_printf(parsed_data, "Type:> 5 days unlim."); + break; + case 0x06: + furi_string_cat_printf(parsed_data, "Type:> 10 rides"); + break; + case 0x18: + furi_string_cat_printf(parsed_data, "Type:> 30 days unlim."); + break; + case 0x1B: + furi_string_cat_printf(parsed_data, "Type:> 20 rides"); + break; + default: + furi_string_cat_printf( + parsed_data, "Type: Unknown, 0x%02X", ticket->first_ticket_marker); + } + } else { + furi_string_cat_printf(parsed_data, "\e#Second Ticket:"); + switch(ticket->second_ticket_marker) { + case 0x02: + furi_string_cat_printf(parsed_data, "Type:> 5 days unlim."); + break; + case 0x06: + furi_string_cat_printf(parsed_data, "Type:> 10 rides"); + break; + case 0x18: + furi_string_cat_printf(parsed_data, "Type:> 30 days unlim."); + break; + case 0x1B: + furi_string_cat_printf(parsed_data, "Type:> 20 rides"); + break; + default: + furi_string_cat_printf( + parsed_data, "Type: Unknown, 0x%02X", ticket->second_ticket_marker); + } + } + } + + furi_string_cat_printf( + parsed_data, + "\nFrom:>%s\nTo:>%s\nValid From: %02d-%02d-%04d\nValid thru: %02d-%02d-%04d\n", + furi_string_get_cstr(ticket->departure_name), + furi_string_get_cstr(ticket->destination_name), + ticket->valid_from.day, + ticket->valid_from.month, + ticket->valid_from.year, + ticket->valid_till.day, + ticket->valid_till.month, + ticket->valid_till.year); + + if(ticket->value_data > 0) { + furi_string_cat_printf(parsed_data, "Rides remain: %02d\n", ticket->value_data); + } + + switch(ticket->current_status) { + case 0x00: + furi_string_cat_printf(parsed_data, "Status:> NOT USED\n"); + break; + case 0x80: + furi_string_cat_printf( + parsed_data, + "Status:> ENTERED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n", + furi_string_get_cstr(ticket->trip_start_sta_name), + ticket->tap_time.day, + ticket->tap_time.month, + ticket->tap_time.year, + ticket->tap_time.hour, + ticket->tap_time.minute, + ticket->ppk_cnt); + break; + case 0x1F: + furi_string_cat_printf( + parsed_data, + "Status:> EXITED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n", + furi_string_get_cstr(ticket->trip_end_sta_name), + ticket->tap_time.day, + ticket->tap_time.month, + ticket->tap_time.year, + ticket->tap_time.hour, + ticket->tap_time.minute, + ticket->ppk_cnt); + break; + default: + furi_string_cat_printf( + parsed_data, + "Status:> UNKNOWN (%02X)\nPPK CNT: %03d", + ticket->current_status, + ticket->ppk_cnt); + break; + } +} + +bool sev_tk_verify(Nfc* nfc) { + const uint8_t verify_sector = 19; + const uint8_t block_num = mf_classic_get_first_block_num_of_sector(verify_sector); + + MfClassicKey key = {}; + bit_lib_num_to_bytes_be(t_card_2k[verify_sector].a, COUNT_OF(key.data), key.data); + + MfClassicAuthContext auth_ctx = {}; + MfClassicError error = + mf_classic_poller_sync_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_ctx); + + return error == MfClassicErrorNone; +} + +static bool sev_tk_read(Nfc* nfc, NfcDevice* device) { + furi_assert(nfc); + furi_assert(device); + + MfClassicData* data = mf_classic_alloc(); + nfc_device_copy_data(device, NfcProtocolMfClassic, data); + + bool is_read = false; + MfClassicType type = MfClassicType1k; + MfClassicError error = mf_classic_poller_sync_detect_type(nfc, &type); + + if(error == MfClassicErrorNone) { + data->type = type; + MfClassicDeviceKeys keys = {}; + + for(size_t i = 0; i < 32; i++) { + bit_lib_num_to_bytes_be(t_card_2k[i].a, sizeof(MfClassicKey), keys.key_a[i].data); + FURI_BIT_SET(keys.key_a_mask, i); + bit_lib_num_to_bytes_be(t_card_2k[i].b, sizeof(MfClassicKey), keys.key_b[i].data); + FURI_BIT_SET(keys.key_b_mask, i); + } + + error = mf_classic_poller_sync_read(nfc, &keys, data); + if(error != MfClassicErrorNotPresent) { + nfc_device_set_data(device, NfcProtocolMfClassic, data); + is_read = (error == MfClassicErrorNone); + } + } + + mf_classic_free(data); + return is_read; +} + +static bool sev_tk_parse(const NfcDevice* device, FuriString* parsed_data) { + furi_assert(device); + + const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic); + bool parsed = false; + + do { + MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, 19); + uint64_t key = bit_lib_bytes_to_num_be(sec_tr->key_a.data, 6); + if(key != t_card_2k[19].a) break; + + Storage* storage = furi_record_open(RECORD_STORAGE); + TicketData primary_ticket = {0}; + primary_ticket.departure_name = furi_string_alloc(); + primary_ticket.destination_name = furi_string_alloc(); + primary_ticket.trip_start_sta_name = furi_string_alloc(); + primary_ticket.trip_end_sta_name = furi_string_alloc(); + extract_ppk_data(storage, data, &primary_ticket, 0); + + printf_transport_card(parsed_data, &primary_ticket, false); + + furi_string_free(primary_ticket.departure_name); + furi_string_free(primary_ticket.destination_name); + furi_string_free(primary_ticket.trip_start_sta_name); + furi_string_free(primary_ticket.trip_end_sta_name); + + if(primary_ticket.second_ticket_marker != 0) { + TicketData secondary_ticket = {0}; + secondary_ticket.departure_name = furi_string_alloc(); + secondary_ticket.destination_name = furi_string_alloc(); + secondary_ticket.trip_start_sta_name = furi_string_alloc(); + secondary_ticket.trip_end_sta_name = furi_string_alloc(); + + extract_ppk_data(storage, data, &secondary_ticket, 1); + printf_transport_card(parsed_data, &secondary_ticket, true); + furi_string_free(primary_ticket.departure_name); + furi_string_free(primary_ticket.destination_name); + furi_string_free(primary_ticket.trip_start_sta_name); + furi_string_free(primary_ticket.trip_end_sta_name); + } + + furi_record_close(RECORD_STORAGE); + parsed = true; + } while(false); + + return parsed; +} + +static const NfcSupportedCardsPlugin sev_tk_plugin = { + .protocol = NfcProtocolMfClassic, + .verify = sev_tk_verify, + .read = sev_tk_read, + .parse = sev_tk_parse, +}; + +static const FlipperAppPluginDescriptor sev_tk_plugin_descriptor = { + .appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID, + .ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION, + .entry_point = &sev_tk_plugin, +}; + +const FlipperAppPluginDescriptor* sev_tk_plugin_ep(void) { + return &sev_tk_plugin_descriptor; +} diff --git a/applications/main/nfc/plugins/supported_cards/sk_tk.c b/applications/main/nfc/plugins/supported_cards/sk_tk.c new file mode 100644 index 000000000..42d4e7e96 --- /dev/null +++ b/applications/main/nfc/plugins/supported_cards/sk_tk.c @@ -0,0 +1,406 @@ +//Based on parsers written by Leptoptilos and Assasinfil + +#include "nfc_supported_card_plugin.h" +#include +#include +#include +#include +#include +#include +#define PPK_WHOLE_EPOCH_START 946684800 //2000-01-01 +#define PPK_CURRENT_EPOCH_START 1388534400 //2014-01-01 +#define SECONDS_IN_A_DAY 86400 +#define SECONDS_IN_A_MINUTE 60 +#define FIRST_PPK_TICKET_OFFSET 76 +#define FIRST_TICKET_VALUE_BLOCK 77 +#define SECOND_PPK_TICKET_OFFSET 88 +#define SECOND_TICKET_VALUE_BLOCK 89 + +typedef struct { + uint64_t a; + uint64_t b; +} MfClassicKeyPair; + +typedef struct { + uint16_t departure_uic; + uint16_t destination_uic; + uint16_t trip_start_uic; + uint16_t trip_end_uic; + FuriString* departure_name; + FuriString* destination_name; + FuriString* trip_start_sta_name; + FuriString* trip_end_sta_name; + uint8_t value_data; + uint8_t current_status; + uint16_t valid_from_data; + uint16_t valid_till_data; + DateTime valid_from; + DateTime valid_till; + uint32_t tap_data; + DateTime tap_time; + uint8_t first_ticket_marker; + uint8_t second_ticket_marker; + uint8_t ppk_cnt; +} TicketData; + +static const MfClassicKeyPair t_card_4k[] = { + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B4B2B1B3B6}, //0 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B5B2B4B9B0}, //1 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B8B4B6B1B3}, //2 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B3B8B5B7}, //3 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B6B4B2B8}, //4 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B4B5B3B8}, //5 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B8B1B7B8}, //6 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B5B1B8B1}, //7 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B6B4B8B7}, //8 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B9B2B9B2B4}, //9 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B1B8B0B6}, //10 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B7B8B9B5B5}, //11 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B7B1B0B0}, //12 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B9B2B8B3B2}, //13 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B4B1B0B4B6}, //14 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B4B2B5B4B8}, //15 + {.a = 0x684CE8377AB8, .b = 0x91888D728D9E}, //16 + {.a = 0xF462ED255B44, .b = 0x0D40620AC610}, //17 + {.a = 0xF1E8FD5B5C9F, .b = 0xABB5763550B0}, //18 + {.a = 0xE9E3265B45B1, .b = 0x11D848B26034}, //19 + {.a = 0xAEA3755DFA82, .b = 0x36D1BAC1395E}, //20 + {.a = 0x83F58B205854, .b = 0x967DAFCF2674}, //21 + {.a = 0x5CDF18E68A75, .b = 0x1689C175B14E}, //22 + {.a = 0x126DC25C5D53, .b = 0x346B03AF1FF3}, //23 + {.a = 0xF1B013C4495C, .b = 0x74CE14DBC71F}, //24 + {.a = 0xA5FE4FAD0269, .b = 0x0025FEA845E5}, //25 + {.a = 0x43080428049C, .b = 0x2E91BB6F511E}, //26 + {.a = 0x4F44A08C51BC, .b = 0xE44CC58DF833}, //27 + {.a = 0x3FEC92F652BE, .b = 0x942039006B83}, //28 + {.a = 0x3A3BE5B635FA, .b = 0xFC564425A9BA}, //29 + {.a = 0x78C9E1C688BB, .b = 0xA29E362B22F3}, //30 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //31 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //32 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //33 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //34 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //35 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //36 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //37 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //38 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //39 +}; + +static inline void sz_uic_to_sta(Storage* storage, const char* file_name, TicketData* ticket) { + FlipperFormat* file = flipper_format_file_alloc(storage); + FuriString* departure_uic = furi_string_alloc_printf("%04X", ticket->departure_uic); + FuriString* destination_uic = furi_string_alloc_printf("%04X", ticket->destination_uic); + FuriString* trip_start_uic = furi_string_alloc_printf("%04X", ticket->trip_start_uic); + FuriString* trip_end_uic = furi_string_alloc_printf("%04X", ticket->trip_end_uic); + + if(flipper_format_file_open_existing(file, file_name)) { + flipper_format_read_string( + file, furi_string_get_cstr(departure_uic), ticket->departure_name); + flipper_format_rewind(file); + flipper_format_read_string( + file, furi_string_get_cstr(destination_uic), ticket->destination_name); + flipper_format_rewind(file); + flipper_format_read_string( + file, furi_string_get_cstr(trip_start_uic), ticket->trip_start_sta_name); + flipper_format_rewind(file); + flipper_format_read_string( + file, furi_string_get_cstr(trip_end_uic), ticket->trip_end_sta_name); + } + + flipper_format_free(file); + furi_string_free(departure_uic); + furi_string_free(destination_uic); + furi_string_free(trip_start_uic); + furi_string_free(trip_end_uic); +} + +static void resolve_station_name(Storage* storage, TicketData* ticket) { + sz_uic_to_sta(storage, EXT_PATH("nfc/assets/sk_id.nfc"), ticket); + if(furi_string_utf8_length(ticket->departure_name) <= 2) { + furi_string_printf(ticket->departure_name, "1f%04X", ticket->departure_uic); + } + if(furi_string_utf8_length(ticket->destination_name) <= 2) { + furi_string_printf(ticket->destination_name, "1F%04X", ticket->destination_uic); + } + if(furi_string_utf8_length(ticket->trip_start_sta_name) <= 2) { + furi_string_printf(ticket->trip_start_sta_name, "1F%04X", ticket->trip_start_uic); + } + if(furi_string_utf8_length(ticket->trip_end_sta_name) <= 2) { + furi_string_printf(ticket->trip_end_sta_name, "1F%04X", ticket->trip_end_uic); + } +} + +static inline void extract_ppk_data( + Storage* storage, + const MfClassicData* data, + TicketData* ticket, + bool ticket_number) { + if(ticket_number == 0) { + ticket->departure_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[6] << 8) | + (data->block[FIRST_PPK_TICKET_OFFSET].data[5]); + ticket->destination_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[9] << 8) | + (data->block[FIRST_PPK_TICKET_OFFSET].data[8]); + ticket->value_data = data->block[FIRST_TICKET_VALUE_BLOCK].data[0]; + ticket->current_status = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[8]; + ticket->valid_from_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[2] << 8) | + (data->block[FIRST_PPK_TICKET_OFFSET].data[1]); + ticket->valid_till_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[4] << 8) | + (data->block[FIRST_PPK_TICKET_OFFSET].data[3]); + ticket->tap_data = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[2] << 16) | + (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[1] << 8) | + data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[0]; + ticket->ppk_cnt = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[10]; + ticket->trip_start_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[4] << 8) | + (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[3]); + ticket->trip_end_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[7] << 8) | + (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[6]); + + } else { + ticket->departure_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[6] << 8) | + (data->block[SECOND_PPK_TICKET_OFFSET].data[5]); + ticket->destination_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[9] << 8) | + (data->block[SECOND_PPK_TICKET_OFFSET].data[8]); + ticket->value_data = data->block[SECOND_TICKET_VALUE_BLOCK].data[0]; + ticket->current_status = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[8]; + ticket->valid_from_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[2] << 8) | + (data->block[SECOND_PPK_TICKET_OFFSET].data[1]); + ticket->valid_till_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[4] << 8) | + (data->block[SECOND_PPK_TICKET_OFFSET].data[3]); + ticket->tap_data = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[2] << 16) | + (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[1] << 8) | + data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[0]; + ticket->ppk_cnt = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[10]; + ticket->trip_start_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[4] << 8) | + (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[3]); + ticket->trip_end_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[7] << 8) | + (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[6]); + } + ticket->first_ticket_marker = data->block[FIRST_PPK_TICKET_OFFSET].data[0]; + ticket->second_ticket_marker = data->block[SECOND_PPK_TICKET_OFFSET].data[0]; + + const uint32_t valid_from_timestamp = + PPK_WHOLE_EPOCH_START + ticket->valid_from_data * SECONDS_IN_A_DAY; + const uint32_t valid_till_timestamp = + PPK_WHOLE_EPOCH_START + ticket->valid_till_data * SECONDS_IN_A_DAY; + const uint32_t tap_timestamp = + PPK_CURRENT_EPOCH_START + ticket->tap_data * SECONDS_IN_A_MINUTE; + datetime_timestamp_to_datetime(valid_from_timestamp, &ticket->valid_from); + datetime_timestamp_to_datetime(valid_till_timestamp, &ticket->valid_till); + datetime_timestamp_to_datetime(tap_timestamp, &ticket->tap_time); + resolve_station_name(storage, ticket); +} + +static void + printf_transport_card(FuriString* parsed_data, TicketData* ticket, bool ticket_number) { + if(ticket->departure_uic == 0x0000) { + furi_string_cat_printf( + parsed_data, + "\e#Unknown SKPPK Card\n NO TICKET DATA FOUND \n\nTHE TICKET IS NOT ISSUED\nOR LAYOUT IS UNKNOWN\n"); + return; + } else { + if(ticket_number == 0) { + furi_string_cat_printf(parsed_data, "\e#SKPPK Transport Card\n"); + switch(ticket->first_ticket_marker) { + case 0x02: + furi_string_cat_printf(parsed_data, "Type:> 5 days unlim."); + break; + case 0x06: + furi_string_cat_printf(parsed_data, "Type:> 10 rides"); + break; + case 0x18: + furi_string_cat_printf(parsed_data, "Type:> 30 days unlim."); + break; + case 0x1B: + furi_string_cat_printf(parsed_data, "Type:> 20 rides."); + break; + default: + furi_string_cat_printf( + parsed_data, "Type: Unknown, 0x%02X\n", ticket->first_ticket_marker); + } + } else { + furi_string_cat_printf(parsed_data, "\e#Second Ticket:\n"); + switch(ticket->second_ticket_marker) { + case 0x02: + furi_string_cat_printf(parsed_data, "Type:> 5 days unlim."); + break; + case 0x06: + furi_string_cat_printf(parsed_data, "Type:> 10 rides"); + break; + case 0x18: + furi_string_cat_printf(parsed_data, "Type:> 30 days unlim."); + break; + case 0x1B: + furi_string_cat_printf(parsed_data, "Type:> 20 rides."); + break; + default: + furi_string_cat_printf( + parsed_data, "Type: Unknown, 0x%02X", ticket->second_ticket_marker); + } + } + + furi_string_cat_printf( + parsed_data, + "\nFrom:>%s\nTo:>%s\nValid From: %02d-%02d-%04d\nValid thru: %02d-%02d-%04d\n", + furi_string_get_cstr(ticket->departure_name), + furi_string_get_cstr(ticket->destination_name), + ticket->valid_from.day, + ticket->valid_from.month, + ticket->valid_from.year, + ticket->valid_till.day, + ticket->valid_till.month, + ticket->valid_till.year); + + if(ticket->value_data > 0) { + furi_string_cat_printf(parsed_data, "Rides remain: %02d\n", ticket->value_data); + } + + switch(ticket->current_status) { + case 0x00: + furi_string_cat_printf(parsed_data, "Status:> NOT USED\n"); + break; + case 0x80: + furi_string_cat_printf( + parsed_data, + "Status:> ENTERED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n", + furi_string_get_cstr(ticket->trip_start_sta_name), + ticket->tap_time.day, + ticket->tap_time.month, + ticket->tap_time.year, + ticket->tap_time.hour, + ticket->tap_time.minute, + ticket->ppk_cnt); + break; + case 0x1F: + furi_string_cat_printf( + parsed_data, + "Status:> EXITED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n", + furi_string_get_cstr(ticket->trip_end_sta_name), + ticket->tap_time.day, + ticket->tap_time.month, + ticket->tap_time.year, + ticket->tap_time.hour, + ticket->tap_time.minute, + ticket->ppk_cnt); + break; + default: + furi_string_cat_printf( + parsed_data, + "Status:> UNKNOWN (%02X)\nPPK CNT: %03d", + ticket->current_status, + ticket->ppk_cnt); + break; + } + } +} + +bool sk_tk_verify(Nfc* nfc) { + const uint8_t verify_sector = 19; + const uint8_t block_num = mf_classic_get_first_block_num_of_sector(verify_sector); + + MfClassicKey key = {}; + bit_lib_num_to_bytes_be(t_card_4k[verify_sector].a, COUNT_OF(key.data), key.data); + + MfClassicAuthContext auth_ctx = {}; + MfClassicError error = + mf_classic_poller_sync_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_ctx); + + return error == MfClassicErrorNone; +} + +static bool sk_tk_read(Nfc* nfc, NfcDevice* device) { + furi_assert(nfc); + furi_assert(device); + + MfClassicData* data = mf_classic_alloc(); + nfc_device_copy_data(device, NfcProtocolMfClassic, data); + + bool is_read = false; + MfClassicType type = MfClassicType4k; + MfClassicError error = mf_classic_poller_sync_detect_type(nfc, &type); + + if(error == MfClassicErrorNone) { + data->type = type; + MfClassicDeviceKeys keys = {}; + + for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) { + bit_lib_num_to_bytes_be(t_card_4k[i].a, sizeof(MfClassicKey), keys.key_a[i].data); + FURI_BIT_SET(keys.key_a_mask, i); + bit_lib_num_to_bytes_be(t_card_4k[i].b, sizeof(MfClassicKey), keys.key_b[i].data); + FURI_BIT_SET(keys.key_b_mask, i); + } + + error = mf_classic_poller_sync_read(nfc, &keys, data); + if(error != MfClassicErrorNotPresent) { + nfc_device_set_data(device, NfcProtocolMfClassic, data); + is_read = (error == MfClassicErrorNone); + } + } + + mf_classic_free(data); + return is_read; +} + +static bool sk_tk_parse(const NfcDevice* device, FuriString* parsed_data) { + furi_assert(device); + + const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic); + bool parsed = false; + + do { + MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, 19); + uint64_t key = bit_lib_bytes_to_num_be(sec_tr->key_a.data, 6); + if(key != t_card_4k[19].a) break; + + Storage* storage = furi_record_open(RECORD_STORAGE); + TicketData primary_ticket = {0}; + primary_ticket.departure_name = furi_string_alloc(); + primary_ticket.destination_name = furi_string_alloc(); + primary_ticket.trip_start_sta_name = furi_string_alloc(); + primary_ticket.trip_end_sta_name = furi_string_alloc(); + extract_ppk_data(storage, data, &primary_ticket, 0); + + printf_transport_card(parsed_data, &primary_ticket, false); + + furi_string_free(primary_ticket.departure_name); + furi_string_free(primary_ticket.destination_name); + furi_string_free(primary_ticket.trip_start_sta_name); + furi_string_free(primary_ticket.trip_end_sta_name); + + if(primary_ticket.second_ticket_marker != 0) { + TicketData secondary_ticket = {0}; + secondary_ticket.departure_name = furi_string_alloc(); + secondary_ticket.destination_name = furi_string_alloc(); + secondary_ticket.trip_start_sta_name = furi_string_alloc(); + secondary_ticket.trip_end_sta_name = furi_string_alloc(); + + extract_ppk_data(storage, data, &secondary_ticket, 1); + printf_transport_card(parsed_data, &secondary_ticket, true); + furi_string_free(primary_ticket.departure_name); + furi_string_free(primary_ticket.destination_name); + furi_string_free(primary_ticket.trip_start_sta_name); + furi_string_free(primary_ticket.trip_end_sta_name); + } + + furi_record_close(RECORD_STORAGE); + parsed = true; + } while(false); + + return parsed; +} + +static const NfcSupportedCardsPlugin sk_tk_plugin = { + .protocol = NfcProtocolMfClassic, + .verify = sk_tk_verify, + .read = sk_tk_read, + .parse = sk_tk_parse, +}; + +static const FlipperAppPluginDescriptor sk_tk_plugin_descriptor = { + .appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID, + .ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION, + .entry_point = &sk_tk_plugin, +}; + +const FlipperAppPluginDescriptor* sk_tk_plugin_ep(void) { + return &sk_tk_plugin_descriptor; +} diff --git a/applications/main/nfc/plugins/supported_cards/szppk_so.c b/applications/main/nfc/plugins/supported_cards/szppk_so.c new file mode 100644 index 000000000..2683f0105 --- /dev/null +++ b/applications/main/nfc/plugins/supported_cards/szppk_so.c @@ -0,0 +1,447 @@ +#include "nfc_supported_card_plugin.h" +#include +#include +#include +#include +#include +#include +#define PPK_WHOLE_EPOCH_START 946684800 //2000-01-01 +#define PPK_CURRENT_EPOCH_START 1388534400 //1388530800 //2014-01-01 +#define SECONDS_IN_A_DAY 86400 +#define SECONDS_IN_A_MINUTE 60 +#define FIRST_PPK_TICKET_OFFSET 76 +#define FIRST_TICKET_VALUE_BLOCK 77 +#define SECOND_PPK_TICKET_OFFSET 88 +#define SECOND_TICKET_VALUE_BLOCK 89 + +typedef struct { + uint64_t a; + uint64_t b; +} MfClassicKeyPair; + +typedef struct { + uint16_t departure_uic; + uint16_t destination_uic; + uint16_t trip_start_uic; + uint16_t trip_end_uic; + FuriString* departure_name; + FuriString* destination_name; + FuriString* trip_start_sta_name; + FuriString* trip_end_sta_name; + uint8_t value_data; + uint8_t current_status; + uint16_t valid_from_data; + uint16_t valid_till_data; + DateTime valid_from; + DateTime valid_till; + uint32_t tap_data; + DateTime tap_time; + uint8_t first_ticket_marker; + uint8_t second_ticket_marker; + uint8_t ppk_cnt; +} TicketData; + +static const MfClassicKeyPair so_card_2k[] = { + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B7B5B8B4B5}, //0 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B6B2B9B8B7}, //1 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B2B4B4B4B0}, //2 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B3B5B2B2B2}, //3 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B8B4B8B0B9}, //4 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B3B7B9B8B6}, //5 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B6B5B5B5B6}, //6 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B7B0B0B2B0}, //7 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B1B2B8B0}, //8 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B1B8B3B2}, //9 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B0B3B1B8}, //10 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B2B6B7B5B2}, //11 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B9B2B0B2B0}, //12 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B7B8B8B7B3}, //13 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B3B4B9B2B0}, //14 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B7B2B0B0B9}, //15 + {.a = 0xA94CAB611187, .b = 0x389109BD1D82}, //16 + {.a = 0xBF4280329F11, .b = 0x28D9EDD2096D}, //17 + {.a = 0xDE6BD90BD6B0, .b = 0x94866C16E9A4}, //18 + {.a = 0x2EA9493CAA7C, .b = 0x5068BCE2BC1C}, //19 + {.a = 0x15A41BA53F6C, .b = 0x3BD3CF43571C}, //20 + {.a = 0x1290FFD80DB5, .b = 0xD821B7916B7E}, //21 + {.a = 0x68C1A07E96A9, .b = 0x2B3323E75750}, //22 + {.a = 0xC699831AB307, .b = 0xCD7F7E9111F1}, //23 + {.a = 0x4E5884BF23E9, .b = 0x2287812A6AEE}, //24 + {.a = 0xC55212F716DC, .b = 0x594E368CCEFF}, //25 + {.a = 0x6EF127E674B1, .b = 0xDD21C8D3E0B9}, //26 + {.a = 0xFB79FAF4B55C, .b = 0xFE52B3B2A93B}, //27 + {.a = 0x6CF85CDFF647, .b = 0xCCAD7C41FC8A}, //28 + {.a = 0x591F6C130F91, .b = 0x2D2B734ECF91}, //29 + {.a = 0xEEB83529B79B, .b = 0xCB14E70EBA38}, //30 + {.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //31 +}; + +static inline void sz_uic_to_sta(Storage* storage, const char* file_name, TicketData* ticket) { + FlipperFormat* file = flipper_format_file_alloc(storage); + FuriString* departure_uic = furi_string_alloc_printf("%04X", ticket->departure_uic); + FuriString* destination_uic = furi_string_alloc_printf("%04X", ticket->destination_uic); + FuriString* trip_start_uic = furi_string_alloc_printf("%04X", ticket->trip_start_uic); + FuriString* trip_end_uic = furi_string_alloc_printf("%04X", ticket->trip_end_uic); + + if(flipper_format_file_open_existing(file, file_name)) { + flipper_format_read_string( + file, furi_string_get_cstr(departure_uic), ticket->departure_name); + flipper_format_rewind(file); + flipper_format_read_string( + file, furi_string_get_cstr(destination_uic), ticket->destination_name); + flipper_format_rewind(file); + flipper_format_read_string( + file, furi_string_get_cstr(trip_start_uic), ticket->trip_start_sta_name); + flipper_format_rewind(file); + flipper_format_read_string( + file, furi_string_get_cstr(trip_end_uic), ticket->trip_end_sta_name); + } + + flipper_format_free(file); + furi_string_free(departure_uic); + furi_string_free(destination_uic); + furi_string_free(trip_start_uic); + furi_string_free(trip_end_uic); +} +// Function to resolve station names for a ticket, and if not found, set to "1E" + UIC code +static void resolve_station_name(Storage* storage, TicketData* ticket) { + sz_uic_to_sta(storage, EXT_PATH("nfc/assets/sz_id.nfc"), ticket); + if(furi_string_utf8_length(ticket->departure_name) <= 2) { + furi_string_printf(ticket->departure_name, "1E%04X", ticket->departure_uic); + } + if(furi_string_utf8_length(ticket->destination_name) <= 2) { + furi_string_printf(ticket->destination_name, "1E%04X", ticket->destination_uic); + } + if(furi_string_utf8_length(ticket->trip_start_sta_name) <= 2) { + furi_string_printf(ticket->trip_start_sta_name, "1E%04X", ticket->trip_start_uic); + } + if(furi_string_utf8_length(ticket->trip_end_sta_name) <= 2) { + furi_string_printf(ticket->trip_end_sta_name, "1E%04X", ticket->trip_end_uic); + } +} + +static inline void extract_ppk_data( + Storage* storage, + const MfClassicData* data, + TicketData* ticket, + bool ticket_number) { + if(ticket_number == 0) { + ticket->departure_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[6] << 8) | + (data->block[FIRST_PPK_TICKET_OFFSET].data[5]); + ticket->destination_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[9] << 8) | + (data->block[FIRST_PPK_TICKET_OFFSET].data[8]); + ticket->value_data = data->block[FIRST_TICKET_VALUE_BLOCK].data[0]; + ticket->current_status = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[8]; + ticket->valid_from_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[2] << 8) | + (data->block[FIRST_PPK_TICKET_OFFSET].data[1]); + ticket->valid_till_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[4] << 8) | + (data->block[FIRST_PPK_TICKET_OFFSET].data[3]); + ticket->tap_data = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[2] << 16) | + (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[1] << 8) | + data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[0]; + ticket->ppk_cnt = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[10]; + ticket->trip_start_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[4] << 8) | + (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[3]); + ticket->trip_end_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[7] << 8) | + (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[6]); + + } else { + ticket->departure_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[6] << 8) | + (data->block[SECOND_PPK_TICKET_OFFSET].data[5]); + ticket->destination_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[9] << 8) | + (data->block[SECOND_PPK_TICKET_OFFSET].data[8]); + ticket->value_data = data->block[SECOND_TICKET_VALUE_BLOCK].data[0]; + ticket->current_status = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[8]; + ticket->valid_from_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[2] << 8) | + (data->block[SECOND_PPK_TICKET_OFFSET].data[1]); + ticket->valid_till_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[4] << 8) | + (data->block[SECOND_PPK_TICKET_OFFSET].data[3]); + ticket->tap_data = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[2] << 16) | + (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[1] << 8) | + data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[0]; + ticket->ppk_cnt = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[10]; + ticket->trip_start_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[4] << 8) | + (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[3]); + ticket->trip_end_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[7] << 8) | + (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[6]); + } + ticket->first_ticket_marker = data->block[FIRST_PPK_TICKET_OFFSET].data[0]; + ticket->second_ticket_marker = data->block[SECOND_PPK_TICKET_OFFSET].data[0]; + const uint32_t valid_from_timestamp = + PPK_WHOLE_EPOCH_START + ticket->valid_from_data * SECONDS_IN_A_DAY; + const uint32_t valid_till_timestamp = + PPK_WHOLE_EPOCH_START + ticket->valid_till_data * SECONDS_IN_A_DAY; + const uint32_t tap_timestamp = + PPK_CURRENT_EPOCH_START + ticket->tap_data * SECONDS_IN_A_MINUTE; + datetime_timestamp_to_datetime(valid_from_timestamp, &ticket->valid_from); + datetime_timestamp_to_datetime(valid_till_timestamp, &ticket->valid_till); + datetime_timestamp_to_datetime(tap_timestamp, &ticket->tap_time); + resolve_station_name(storage, ticket); +} + +static inline bool is_accompany_card(uint16_t departure_uic, uint16_t destination_uic) { + return departure_uic == destination_uic; +} + +static void printf_accompany_card(TicketData* ticket, FuriString* parsed_data) { + if(ticket->departure_uic == 0x0000) { + furi_string_cat_printf( + parsed_data, + "\e#Unknown SZPPK Card\n NO TICKET DATA FOUND \n\nTHE TICKET IS NOT ISSUED\nOR LAYOUT IS UNKNOWN\n"); + return; + } else { + furi_string_cat_printf( + parsed_data, + "\e#SZPPK Accompany Card\nValid on: %02d-%02d-%04d\nStation: > %s\n", + ticket->valid_from.day, + ticket->valid_from.month, + ticket->valid_from.year, + furi_string_get_cstr(ticket->departure_name)); + + switch(ticket->current_status) { + case 0x00: + furi_string_cat_printf( + parsed_data, "Status:> NOT USED\nPPK CNT: %03d\n", ticket->ppk_cnt); + break; + case 0x80: + furi_string_cat_printf( + parsed_data, + "Status:> ENTERED STATION\nChecked in at:> %02d:%02d\nPPK CNT: %03d\n", + ticket->tap_time.hour, + ticket->tap_time.minute, + ticket->ppk_cnt); + break; + case 0x1E: + furi_string_cat_printf( + parsed_data, + "Status:> EXITED STATION\nChecked out at:> %02d:%02d\nPPK CNT: %03d\n", + ticket->tap_time.hour, + ticket->tap_time.minute, + ticket->ppk_cnt); + break; + default: + furi_string_cat_printf( + parsed_data, "Status:> UNKNOWN\nPPK CNT: %03d\n", ticket->ppk_cnt); + break; + } + } +} + +static void + printf_transport_card(FuriString* parsed_data, TicketData* ticket, bool ticket_number) { + if(ticket->departure_uic == 0x0000) { + furi_string_cat_printf( + parsed_data, + "\e#Unknown SZPPK Card\n NO TICKET DATA FOUND \n\nTHE TICKET IS NOT ISSUED\nOR LAYOUT IS UNKNOWN\n"); + return; + } else { + if(ticket_number == 0) { + furi_string_cat_printf(parsed_data, "\e#SZPPK Transport Card\n"); + switch(ticket->first_ticket_marker) { + case 0x02: + furi_string_cat_printf(parsed_data, "Type:> 5 days unlim."); + break; + case 0x06: + furi_string_cat_printf(parsed_data, "Type:> 10 rides"); + break; + case 0x18: + furi_string_cat_printf(parsed_data, "Type:> 30 days unlim."); + break; + case 0x1B: + furi_string_cat_printf(parsed_data, "Type:> 20 rides."); + break; + default: + furi_string_cat_printf( + parsed_data, "Type: Unknown, 0x%02X\n", ticket->first_ticket_marker); + } + } else { + furi_string_cat_printf(parsed_data, "\e#Second Ticket:\n"); + switch(ticket->second_ticket_marker) { + case 0x02: + furi_string_cat_printf(parsed_data, "Type:> 5 days unlim."); + break; + case 0x06: + furi_string_cat_printf(parsed_data, "Type:> 10 rides"); + break; + case 0x18: + furi_string_cat_printf(parsed_data, "Type:> 30 days unlim."); + break; + case 0x1B: + furi_string_cat_printf(parsed_data, "Type:> 20 rides."); + break; + default: + furi_string_cat_printf( + parsed_data, "Type: Unknown, 0x%02X", ticket->second_ticket_marker); + } + } + + furi_string_cat_printf( + parsed_data, + "\nFrom:>%s\nTo:>%s\nValid From: %02d-%02d-%04d\nValid thru: %02d-%02d-%04d", + furi_string_get_cstr(ticket->departure_name), + furi_string_get_cstr(ticket->destination_name), + ticket->valid_from.day, + ticket->valid_from.month, + ticket->valid_from.year, + ticket->valid_till.day, + ticket->valid_till.month, + ticket->valid_till.year); + + if(ticket->value_data > 0) { + furi_string_cat_printf(parsed_data, "Rides remain: %02d\n", ticket->value_data); + } + + switch(ticket->current_status) { + case 0x00: + furi_string_cat_printf( + parsed_data, "Status:> NOT USED\nPPK CNT: %03d\n", ticket->ppk_cnt); + break; + case 0x80: + furi_string_cat_printf( + parsed_data, + "\nStatus:> ENTERED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n", + furi_string_get_cstr(ticket->trip_start_sta_name), + ticket->tap_time.day, + ticket->tap_time.month, + ticket->tap_time.year, + ticket->tap_time.hour, + ticket->tap_time.minute, + ticket->ppk_cnt); + break; + case 0x1E: + furi_string_cat_printf( + parsed_data, + "\nStatus:> EXITED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n", + furi_string_get_cstr(ticket->trip_end_sta_name), + ticket->tap_time.day, + ticket->tap_time.month, + ticket->tap_time.year, + ticket->tap_time.hour, + ticket->tap_time.minute, + ticket->ppk_cnt); + break; + default: + furi_string_cat_printf( + parsed_data, + "Status:> UNKNOWN (%02X)\nPPK CNT: %03d", + ticket->current_status, + ticket->ppk_cnt); + break; + } + } +} + +bool szppk_so_verify(Nfc* nfc) { + const uint8_t verify_sector = 19; + const uint8_t block_num = mf_classic_get_first_block_num_of_sector(verify_sector); + + MfClassicKey key = {}; + bit_lib_num_to_bytes_be(so_card_2k[verify_sector].a, COUNT_OF(key.data), key.data); + + MfClassicAuthContext auth_ctx = {}; + MfClassicError error = + mf_classic_poller_sync_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_ctx); + + return error == MfClassicErrorNone; +} + +static bool szppk_so_read(Nfc* nfc, NfcDevice* device) { + furi_assert(nfc); + furi_assert(device); + + MfClassicData* data = mf_classic_alloc(); + nfc_device_copy_data(device, NfcProtocolMfClassic, data); + + bool is_read = false; + MfClassicType type = MfClassicType1k; + MfClassicError error = mf_classic_poller_sync_detect_type(nfc, &type); + + if(error == MfClassicErrorNone) { + data->type = type; + MfClassicDeviceKeys keys = {}; + + for(size_t i = 0; i < 32; i++) { + bit_lib_num_to_bytes_be(so_card_2k[i].a, sizeof(MfClassicKey), keys.key_a[i].data); + FURI_BIT_SET(keys.key_a_mask, i); + bit_lib_num_to_bytes_be(so_card_2k[i].b, sizeof(MfClassicKey), keys.key_b[i].data); + FURI_BIT_SET(keys.key_b_mask, i); + } + + error = mf_classic_poller_sync_read(nfc, &keys, data); + if(error != MfClassicErrorNotPresent) { + nfc_device_set_data(device, NfcProtocolMfClassic, data); + is_read = (error == MfClassicErrorNone); + } + } + + mf_classic_free(data); + return is_read; +} + +static bool szppk_so_parse(const NfcDevice* device, FuriString* parsed_data) { + furi_assert(device); + + const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic); + bool parsed = false; + + do { + MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, 19); + uint64_t key = bit_lib_bytes_to_num_be(sec_tr->key_a.data, 6); + if(key != so_card_2k[19].a) break; + + Storage* storage = furi_record_open(RECORD_STORAGE); + TicketData primary_ticket = {0}; + primary_ticket.departure_name = furi_string_alloc(); + primary_ticket.destination_name = furi_string_alloc(); + primary_ticket.trip_start_sta_name = furi_string_alloc(); + primary_ticket.trip_end_sta_name = furi_string_alloc(); + extract_ppk_data(storage, data, &primary_ticket, 0); + if(is_accompany_card(primary_ticket.departure_uic, primary_ticket.destination_uic)) { + printf_accompany_card(&primary_ticket, parsed_data); + } else { + printf_transport_card(parsed_data, &primary_ticket, false); + } + furi_string_free(primary_ticket.departure_name); + furi_string_free(primary_ticket.destination_name); + furi_string_free(primary_ticket.trip_start_sta_name); + furi_string_free(primary_ticket.trip_end_sta_name); + + if(primary_ticket.second_ticket_marker != 0) { + TicketData secondary_ticket = {0}; + secondary_ticket.departure_name = furi_string_alloc(); + secondary_ticket.destination_name = furi_string_alloc(); + secondary_ticket.trip_start_sta_name = furi_string_alloc(); + secondary_ticket.trip_end_sta_name = furi_string_alloc(); + + extract_ppk_data(storage, data, &secondary_ticket, 1); + printf_transport_card(parsed_data, &secondary_ticket, true); + furi_string_free(primary_ticket.departure_name); + furi_string_free(primary_ticket.destination_name); + furi_string_free(primary_ticket.trip_start_sta_name); + furi_string_free(primary_ticket.trip_end_sta_name); + } + + furi_record_close(RECORD_STORAGE); + parsed = true; + + } while(false); + + return parsed; +} + +static const NfcSupportedCardsPlugin szppk_so_plugin = { + .protocol = NfcProtocolMfClassic, + .verify = szppk_so_verify, + .read = szppk_so_read, + .parse = szppk_so_parse, +}; + +static const FlipperAppPluginDescriptor szppk_so_plugin_descriptor = { + .appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID, + .ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION, + .entry_point = &szppk_so_plugin, +}; + +const FlipperAppPluginDescriptor* szppk_so_plugin_ep(void) { + return &szppk_so_plugin_descriptor; +} diff --git a/applications/main/nfc/plugins/supported_cards/two_cities.c b/applications/main/nfc/plugins/supported_cards/two_cities.c index 240c6c585..1295b1c65 100644 --- a/applications/main/nfc/plugins/supported_cards/two_cities.c +++ b/applications/main/nfc/plugins/supported_cards/two_cities.c @@ -39,7 +39,7 @@ bool two_cities_verify(Nfc* nfc) { bool verified = false; do { - const uint8_t verify_sector = 4; + const uint8_t verify_sector = 9; uint8_t block_num = mf_classic_get_first_block_num_of_sector(verify_sector); FURI_LOG_D(TAG, "Verifying sector %u", verify_sector); diff --git a/applications/main/nfc/resources/nfc/assets/sev_id.nfc b/applications/main/nfc/resources/nfc/assets/sev_id.nfc new file mode 100644 index 000000000..35a2bc234 --- /dev/null +++ b/applications/main/nfc/resources/nfc/assets/sev_id.nfc @@ -0,0 +1,124 @@ +Filetype: Flipper NFC resources +Version: 1 +# staID: Name +9468: KULISHKI +9469: KRASNOE +968C: UGLICH +97A7: 40 KM +9C61: 29 KM +AB91: YAROSLAVL-GL. +AB92: YAROSLAVL-PASS +AB93: PRIVOLZHYE +AB94: FILINO +ABD5: 300 KM +ABFF: 4 KM +AC4D: SKALINO +AC4F: MARFINO +AC50: PRECHISTOE +AC51: MAKAROVO +AC53: PURSHEVO +AC54: RODIONOVO +AC55: MASLOVO +AC56: NEKOUZ +AC57: SHESTIKHINO +AC59: VOLGA +AC5A: KOBOSTOVO +AC5B: TIKHMENEVO +AC5C: LOM +AC5D: VAULOVO +AC5E: CHEBAKOVO +AC64: LYUTOVO +AC65: TOSCHIKHA +AC66: BURMAKINO +AC68: PANTELEEVO +AC69: PUTYATINO +AC6A: PUCHKOVSKIY +AC6B: UTKINO +AC6F: KOZMODEMNSK +AC70: KOROMYSLOVO +AC71: SEMIBRATOVO +AC72: ROSTOV-YARSLV. +AC73: PETROVSK +AC74: SIL'NITSY +AC75: ITLAR' +AC77: BEKLEMISHEVO +AC78: RYAZANTSEVO +AC79: SHUSHKOVO +AC7A: BERENDEEVO +AC8A: RYBINSK-PASS. +ACA6: SEKSHA +ACA7: LYUBIM +ACA9: ZHAROK +ACAA: SOT' +ACAB: LUNKA +ACD4: DANOLOV +ACE9: DUNAIKA +ACEA: LIPOVAYA GORA +AD03: 155 KM +AD04: 147 KM +AD06: 187 KM +AD07: 231 KM +AD08: 259 KM +AD09: 268 KM +AD0A: 274 KM +AD0B: DEPO +AD0C: 310 KM +AD0D: 322 KM +AD0E: 331 KM +AD0F: 343 KM +AD10: 351 KM +AD45: 361 KM +AD46: 370 KM +AD47: 378 KM +AD48: 388 KM +AD4A: 399 KM +AD4B: 409 KM +AD4C: 419 KM +AEB1: KOCHENYATINO +AEB3: BAGRIMOVO +AEB4: REKA +AEB5: SOSNOVTSY +AEB9: DOGADTSEVO +AEBD: KUDRYAVTSEVO +AEBF: PROSVET +AEC1: GAVROLOV YAM +AEC3: LEVTSOVO +AEC7: HOZHAEVO +AECA: TSYBRINO +AECB: ROKSHA +AEF4: 69 KM +AEF5: 56 KM +AEF6: 323 KM +AEFA: 287 KM +AEFB: 296 KM +AEFC: 305 KM +AEFD: 310 KM +AEFE: 318 KM +AF0E: 265 KM +AF0F: 285 KM +B057: MAKAROVSKAYA +B05A: USHAKOVO +B05F: VAREGOVO +B07A: 306 KM +B07B: 296 KM +B07C: YURINSKIY +B07D: TOROPOVO +B07E: PINYAGI +B07F: CHIZHOVO +B080: KLINTSEVO +B081: TENINO +B082: MOLOT +B083: TELISCHEVO +B084: SUHAREZH +B086: 324 KM +B087: POLYANKI +B08A: DEBOLOVSKAYA +B08B: 2 KM +B090: RUSHA +B0AC: KOTOROSL' +B0D1: 403 KM +B0D2: SHOLOSOVO +B0D3: MURINSKOE +B0D4: ONAN'JINO +B0D5: L'NOZAVOD +B0F3: 359 KM \ No newline at end of file diff --git a/applications/main/nfc/resources/nfc/assets/sk_id.nfc b/applications/main/nfc/resources/nfc/assets/sk_id.nfc new file mode 100644 index 000000000..db840ad21 --- /dev/null +++ b/applications/main/nfc/resources/nfc/assets/sk_id.nfc @@ -0,0 +1,85 @@ +Filetype: Flipper NFC resources +Version: 1 +# staID: Name +8377: OP 1725 KM +8350: STEKOL'NYI +834F: SELHOZTECHNIKA +834D: KANGLY +834B: OP 1861 KM +834A: OP 1856 KM +8349: OP 1842 KM +8348: OP 1832 KM +8347: OP 1806 KM +8344: OP 44 KM +8343: OP 129 KM +8288: SUVOROVSKAYA +8188: NESKUCHNYI +8183: OP 1741 KM +8182: KOCHUBEEVSKII +817E: SURKUL +817D: OP 1823 KM +8177: TERSKII +8176: KRASNAYA +8175: TEPLAYA RECHKA +8165: DVORTSOVYI +8164: PIONER +8161: ORBEL'YANOVO +815F: DZHEMUHA +815D: ETOKA +8158: OBIL'NYI +8157: NINY +813C: STAVROPOL +8104: KARMALINOVSKII +8084: KRASN.DEREVNIA +807E: MIHAILOVSKAIA +7FF8: ZMEIKA +7FF6: MASHUK +7FF5: LERMONTOVSKII +7FF4: NOVOPIATIGORSK +7FF3: SKACHKI +7FF2: ZOLOTUSHKA +7FF1: BELYI UGOL' +7FF0: PODKUMOK +7FEF: MINUTKA +7FBA: PLAKSEIKA +7FB9: MASLOV KUT +7FB8: ZELENOKUMSK +7FB7: KUMA +7FB6: GEORGIEVSK +7FA6: VIAZNIKI +7FA4: YAGODKA +7FA1: MAIAK +7F9B: BEDENNOVSK +7F6A: PALAGIADA +7F69: RYZDVIANAIA +7F68: PEREDOVAIA +7F65: GRIGOROPOLISSK. +7F5C: NEVINNOMYSSK. +7F2B: INOZEMTSEVO +7F2A: MIN.VODY +7F1B: ZHELEZNOVODSK +7F17: BESHTAU +7EFD: RASSHEVATKA +7EBC: PIATIGORSK +7EB2: KISLOVODSK +7EA5: ZOLSKII +7EA4: VINOGRADNAIA +7EA2: KUMAGORSK +7E9F: NAGUTSKAIA +7E9D: KURSAVKA +7E9B: KIAN +7E9A: ZELENCHUK +7E96: BOGLOVSKAIA +7E95: OVECHKA +8162: KURSHAVA +7F97: CHUKOTSKII +83AE: DEBRI +8306: DEGTIAREVSKI +8185: NIVA +8160: OP 1814 KM +815B: CHISTAIA +7FA5: BERMEDSKII +7F93: IZOBILNAIA +7F64: KRASNOKUBANSK. +7ED0: ESSENTUKI +7EA6: APOLLONSKAIA diff --git a/applications/main/nfc/resources/nfc/assets/sz_id.nfc b/applications/main/nfc/resources/nfc/assets/sz_id.nfc new file mode 100644 index 000000000..98c6f287b --- /dev/null +++ b/applications/main/nfc/resources/nfc/assets/sz_id.nfc @@ -0,0 +1,151 @@ +Filetype: Flipper NFC resources +Version: 1 +# staID: Name +9426: LADOZH.VOKZ. +9421: MOSKOV.VOKZ. +9423: VITEBS.VOKZ +9424: FINLND.VOKZ +9425: BALT.VOKZ +948B: IM.MOROZOVA +94B4: TOSNO +94B5: SABLINO +94BE: GORY +94C0: IZHORY +94C1: RYBATSKOE +24C2: OBUKHOVO +94C3: SLAVYANKA +94C4: KOLPINO +94C9: MSHINSKAYA +94CC: STROGANOVO +94CD: SUI'DA +94CE: GATCHINA BLT. +94D4: VYRITSA +94D5: PAVLOVSK +94D6: TSARSKOE SELO +94E2: KALISHCHE +94E3: LEBYAZHYE +94E4: BOLSH.IZHORA +9507: BELOOSTROV +9508: ZELENOGORSK +9511: RUCH'I +9512: TOKSOVO +9513: OSEL'KI +9514: PERI +9515: GRUZINO +9516: VASKELOVO +9517: OREKHOVO +9518: PETYAJARVI +9519: LOSEVO +951A: GROMOVO +951F: KUZNECHNOE +95C3: KAPITOLOVO +969A: MGA +969C: LUGA +969D: TOLMACHEVO +969E: SIVERSKAYA +969F: GATCHINA VRSH. +96A1: OREDEZH +96A3: SLANTSY +96A5: ORANIENBAUM 1 +96A8: VOLOSOVO +96BD: TIKHVIN +96C0: VOLKHOVSTROY 1 +96C1: VOLKHOVSTROY 2 +96CA: VYBORG +96CB: SOSNOVO +96CC: PRIOZYORSK +970A: BOLOTISTOE +971D: STAR.DEREVNYA +9721: 54 KM +9723: KOLOSKOVO(79KM) +972C: PARAVOZNY MUZEI +9741: BOROVAYA +97BB: DETSKOSELSKAYA +9809: KUPCHINO +9810: PETROKREPOST' +9811: ALEKSANDROVSK.F +9817: BERNGARDOVKA +981C: PROSPEKT SLAVY +9826: SOSNOVAYA POL. +9829: PUDOST' +982B: LENINSKI' PR. +982D: PAVLOVO-NA-NEVE +9835: LANSKAYA +9838: KANNELYARVI +983A: MELN.RUCHEI +983F: PISKARYOVKA +9849: UDEL'NAYA +9852: BRONKA +9853: VSEVOLOZHSK. +985D: FARFOROVSKAYA +9867: RZHEVKA +9868: KOBRALOVO +986B: AEROPORT +9871: BRONEVAYA +987B: SESTRORETSK +9880: TATIANINO +988A: KUSHELEVKA +9890: RAKH'YA +9894: UL'YANKA +9899: PESOCHNAYA +989A: NAVALOCHNAYA +989E: STREL'NA +98A3: ST.PETERGOF +98A5: KOLTUSHI +98A8: KUZ'MOLOVO +98AD: KRASN. SELO +98AE: SHUSHARY +98BC: GORELOVO +98C1: ROSHCHINO +98CB: LISII' NOS +98D5: DACHNOYE +98D6: PREDPORTOVAYA +98E4: NOVAYA OKHTA +98E9: REPINO +98F3: LEVASHOVO +98FD: NOV.DEREVNYA +98FE: IZHORSK.ZAVOD +9908: VAGANOVO +990C: SHUVALOVO +9911: DUNAI' +9916: IRINOVKA +991C: TARKHOVKA +9923: PUPYSHEVO +9925: TAI'TSY +9926: LAVRIKI +992A: LADOZHSK.OZ. +992D: ROMANOVKA +9936: RAZLIV +9938: KIRILLOVSKOE +9939: OL'GINO +993D: METALLOSTROY +993E: KAVGOLOVO +9943: VOZD.PARK +9944: POST KOVALEVO +9949: LAPPELOVO +994D: KORNEVO +9952: SOLNECHNOYE +9956: MYAGLOVO +995B: OZERKI +995C: KOMAROVO +9969: PROBA +996A: KIRPICH.ZAVOD +9970: KURORT +9975: 67 KM +9976: USHKOVO +9981: DUDERGOF +9989: UNIVERSITETSK. +9992: YAKHTENNAYA +9996: KOVALEVO +9997: DIBUNY +99AF: MANUSHKINO +99CF: PARGOLOVO +99D4: LIGOVO +9C09: DEVYATKINO +9C58: NEV.DUBROVKA +9C5A: ALEKSANDROVSK. +9C5B: GORSKAYA +9D1C: NOV.PETERGOF +9FDC: BORISOVA GRIVA +98B7: SERGIEVO +9957: LAKHTA \ No newline at end of file From bdff6c7bc8bbe49cf3a1d5a2eb05afb1055b5353 Mon Sep 17 00:00:00 2001 From: mxcdoam <72457810+mxcdoam@users.noreply.github.com> Date: Sun, 29 Mar 2026 23:03:17 +0300 Subject: [PATCH 19/71] Credits --- applications/main/nfc/plugins/supported_cards/plantain.c | 2 +- applications/main/nfc/plugins/supported_cards/sevppk_tk.c | 2 +- applications/main/nfc/plugins/supported_cards/sk_tk.c | 2 +- applications/main/nfc/plugins/supported_cards/szppk_so.c | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/applications/main/nfc/plugins/supported_cards/plantain.c b/applications/main/nfc/plugins/supported_cards/plantain.c index 53bacfbf2..a9cf2c946 100644 --- a/applications/main/nfc/plugins/supported_cards/plantain.c +++ b/applications/main/nfc/plugins/supported_cards/plantain.c @@ -1,4 +1,4 @@ -//Based on parsers written by Leptoptilos and Assasinfil +//Based on parsers written by Leptoptilos and Assasinfil. Also, thanks to WillyJL () for help! #include "nfc_supported_card_plugin.h" #include diff --git a/applications/main/nfc/plugins/supported_cards/sevppk_tk.c b/applications/main/nfc/plugins/supported_cards/sevppk_tk.c index ba8eef6a2..e069c8c9a 100644 --- a/applications/main/nfc/plugins/supported_cards/sevppk_tk.c +++ b/applications/main/nfc/plugins/supported_cards/sevppk_tk.c @@ -1,4 +1,4 @@ -//Based on parsers written by Leptoptilos and Assasinfil +//Based on parsers written by Leptoptilos and Assasinfil. Also, thanks to WillyJL () for help! #include "nfc_supported_card_plugin.h" #include diff --git a/applications/main/nfc/plugins/supported_cards/sk_tk.c b/applications/main/nfc/plugins/supported_cards/sk_tk.c index 42d4e7e96..a294ed45d 100644 --- a/applications/main/nfc/plugins/supported_cards/sk_tk.c +++ b/applications/main/nfc/plugins/supported_cards/sk_tk.c @@ -1,4 +1,4 @@ -//Based on parsers written by Leptoptilos and Assasinfil +//Based on parsers written by Leptoptilos and Assasinfil. Also, thanks to WillyJL () for help! #include "nfc_supported_card_plugin.h" #include diff --git a/applications/main/nfc/plugins/supported_cards/szppk_so.c b/applications/main/nfc/plugins/supported_cards/szppk_so.c index 2683f0105..16bbbd807 100644 --- a/applications/main/nfc/plugins/supported_cards/szppk_so.c +++ b/applications/main/nfc/plugins/supported_cards/szppk_so.c @@ -1,3 +1,5 @@ +//Based on parsers written by Leptoptilos and Assasinfil. Also, thanks to WillyJL () for help! + #include "nfc_supported_card_plugin.h" #include #include From a044de9cd3dcf55174541a27d5330976d0ddc8ec Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 30 Mar 2026 00:07:22 +0300 Subject: [PATCH 20/71] Address utf8 support when uploading JavaScript application OFW PR 4364 by bekindpleaserewind --- CHANGELOG.md | 1 + applications/system/js_app/packages/fz-sdk/sdk.js | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c37572301..815ada806 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * OFW PR 4362: NFC: Fix BusFault in Write to Initial Card (by @akrylysov) * Apps: Build tag (**27mar2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes +* OFW PR 4364: JS: Address utf8 support when uploading JavaScript application (by @bekindpleaserewind) * Display: Extend lcd contrast range to full ST756x 6-bit range (by @ShaTie) (reduced to -10 to +18 to avoid fully unreadable state)

#### Known NFC post-refactor regressions list: diff --git a/applications/system/js_app/packages/fz-sdk/sdk.js b/applications/system/js_app/packages/fz-sdk/sdk.js index 2cd1c6ab3..1e9496c30 100644 --- a/applications/system/js_app/packages/fz-sdk/sdk.js +++ b/applications/system/js_app/packages/fz-sdk/sdk.js @@ -153,10 +153,11 @@ async function upload(config) { port.write(`storage remove ${config.output}\x0d`); port.drain(); await waitFor(">: ", 1000); - port.write(`storage write_chunk ${config.output} ${appFile.length}\x0d`); + const appFileBuffer = Buffer.from(appFile, "utf8"); + port.write(`storage write_chunk ${config.output} ${appFileBuffer.length}\x0d`); await waitFor("Ready", 1000); - port.write(appFile); - port.drain(); + port.write(appFileBuffer); + await new Promise(resolve => port.drain(resolve)); await waitFor(">: ", 1000); console.log("Launching application"); From cb3dc8952a23862b28db9304f1b785040d65d346 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 30 Mar 2026 00:09:19 +0300 Subject: [PATCH 21/71] upd changelog --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 815ada806..b6821e4f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,9 @@ * SubGHz: Add all 0x0s and all 0xFs KeeLoq MF codes for normal and simple learning * SubGHz: **Fix CAME TWEE repeats count for button click** * NFC: **Fix "MIR" and other EMV cards crash on Read** (by @Dmitry422) -* NFC: Add Mifare Ultralight C Write Support (by @haw8411) -* OFW PR 4362: NFC: Fix BusFault in Write to Initial Card (by @akrylysov) +* NFC: Add **Mifare Ultralight C Write Support** (by @haw8411) +* NFC: Add **new parsers SZPPK, SKPPK and SevPPK** (PR #981 | by @mxcdoam) +* OFW PR 4362: NFC: **Fix BusFault** in Write to Initial Card (by @akrylysov) * Apps: Build tag (**27mar2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * OFW PR 4364: JS: Address utf8 support when uploading JavaScript application (by @bekindpleaserewind) From 5d3f1a6ad24f050ae92be8765b73598954aa8676 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 30 Mar 2026 00:15:05 +0300 Subject: [PATCH 22/71] add missing part --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6821e4f4..4f9db5911 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ * SubGHz: **Fix CAME TWEE repeats count for button click** * NFC: **Fix "MIR" and other EMV cards crash on Read** (by @Dmitry422) * NFC: Add **Mifare Ultralight C Write Support** (by @haw8411) -* NFC: Add **new parsers SZPPK, SKPPK and SevPPK** (PR #981 | by @mxcdoam) +* NFC: Add **new parsers SZPPK, SKPPK and SevPPK**, upgrade Plantain parser, fix TwoCities parser (PR #981 | by @mxcdoam) * OFW PR 4362: NFC: **Fix BusFault** in Write to Initial Card (by @akrylysov) * Apps: Build tag (**27mar2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes From b9fa2943ed782e29e7c0cd748bcdf865d9d920a7 Mon Sep 17 00:00:00 2001 From: DoniyorI Date: Tue, 31 Mar 2026 22:05:59 -0400 Subject: [PATCH 23/71] nfc: add ISO15693-3 and SLIX write-back support Add Write Single Block poller functions for ISO 15693-3 and SLIX/SLIX2, enable the Write scene for both protocols, and skip blocks that already contain the correct data to minimise RF session time and avoid timeouts on large tags (e.g. 80-block ICODE SLIX2). Tested on NXP ICODE SLIX2 (UID E0 04 01 09 05 05 09 37, 80 x 4 byte blocks). --- .../protocol_support/iso15693_3/iso15693_3.c | 70 +++++++++++- .../nfc/helpers/protocol_support/slix/slix.c | 105 +++++++++++++++++- lib/nfc/protocols/iso15693_3/iso15693_3.h | 5 + lib/nfc/protocols/iso15693_3/iso15693_3_i.c | 19 ++++ lib/nfc/protocols/iso15693_3/iso15693_3_i.h | 2 + .../protocols/iso15693_3/iso15693_3_poller.h | 42 +++++++ .../iso15693_3/iso15693_3_poller_i.c | 51 +++++++++ .../iso15693_3/iso15693_3_poller_i.h | 2 - lib/nfc/protocols/slix/slix_poller.h | 34 ++++++ lib/nfc/protocols/slix/slix_poller_i.c | 28 +++++ targets/f7/api_symbols.csv | 7 +- 11 files changed, 358 insertions(+), 7 deletions(-) diff --git a/applications/main/nfc/helpers/protocol_support/iso15693_3/iso15693_3.c b/applications/main/nfc/helpers/protocol_support/iso15693_3/iso15693_3.c index 4c69fbe81..d68b97dc0 100644 --- a/applications/main/nfc/helpers/protocol_support/iso15693_3/iso15693_3.c +++ b/applications/main/nfc/helpers/protocol_support/iso15693_3/iso15693_3.c @@ -109,9 +109,75 @@ static void nfc_scene_emulate_on_enter_iso15693_3(NfcApp* instance) { instance->listener, nfc_scene_emulate_listener_callback_iso15693_3, instance); } +static NfcCommand + nfc_scene_write_poller_callback_iso15693_3(NfcGenericEvent event, void* context) { + furi_assert(event.protocol == NfcProtocolIso15693_3); + + NfcApp* instance = context; + Iso15693_3Poller* poller = event.instance; + const Iso15693_3PollerEvent* iso15693_3_event = event.event_data; + NfcCommand command = NfcCommandContinue; + + if(iso15693_3_event->type == Iso15693_3PollerEventTypeReady) { + const Iso15693_3Data* write_data = + nfc_device_get_data(instance->nfc_device, NfcProtocolIso15693_3); + const Iso15693_3Data* card_data = iso15693_3_poller_get_data(poller); + + view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventCardDetected); + + if(card_data->system_info.block_count != write_data->system_info.block_count || + card_data->system_info.block_size != write_data->system_info.block_size) { + furi_string_set(instance->text_box_store, "Incompatible card\n(block count/size\nmismatch)"); + view_dispatcher_send_custom_event( + instance->view_dispatcher, NfcCustomEventPollerFailure); + } else { + Iso15693_3Error error = Iso15693_3ErrorNone; + const uint8_t block_size = write_data->system_info.block_size; + const uint8_t* write_block_data = + (const uint8_t*)simple_array_cget_data(write_data->block_data); + const uint8_t* card_block_data = + (const uint8_t*)simple_array_cget_data(card_data->block_data); + for(uint16_t i = 0; i < write_data->system_info.block_count; i++) { + if(iso15693_3_is_block_locked(write_data, i)) continue; + // Skip blocks that already contain the correct data + if(memcmp( + write_block_data + i * block_size, + card_block_data + i * block_size, + block_size) == 0) + continue; + error = iso15693_3_poller_write_block( + poller, write_block_data + i * block_size, i, block_size); + if(error == Iso15693_3ErrorInternal) { + // Block is locked on the target card, skip it + error = Iso15693_3ErrorNone; + continue; + } + if(error != Iso15693_3ErrorNone) break; + } + + if(error == Iso15693_3ErrorNone) { + view_dispatcher_send_custom_event( + instance->view_dispatcher, NfcCustomEventPollerSuccess); + } else { + view_dispatcher_send_custom_event( + instance->view_dispatcher, NfcCustomEventPollerFailure); + } + } + command = NfcCommandStop; + } + + return command; +} + +static void nfc_scene_write_on_enter_iso15693_3(NfcApp* instance) { + furi_string_set(instance->text_box_store, "Apply the\ntarget card now"); + instance->poller = nfc_poller_alloc(instance->nfc, NfcProtocolIso15693_3); + nfc_poller_start(instance->poller, nfc_scene_write_poller_callback_iso15693_3, instance); +} + const NfcProtocolSupportBase nfc_protocol_support_iso15693_3 = { .features = NfcProtocolFeatureEmulateFull | NfcProtocolFeatureEditUid | - NfcProtocolFeatureMoreInfo, + NfcProtocolFeatureMoreInfo | NfcProtocolFeatureWrite, .scene_info = { @@ -155,7 +221,7 @@ const NfcProtocolSupportBase nfc_protocol_support_iso15693_3 = { }, .scene_write = { - .on_enter = nfc_protocol_support_common_on_enter_empty, + .on_enter = nfc_scene_write_on_enter_iso15693_3, .on_event = nfc_protocol_support_common_on_event_empty, }, }; diff --git a/applications/main/nfc/helpers/protocol_support/slix/slix.c b/applications/main/nfc/helpers/protocol_support/slix/slix.c index 32094140d..895c1f2b5 100644 --- a/applications/main/nfc/helpers/protocol_support/slix/slix.c +++ b/applications/main/nfc/helpers/protocol_support/slix/slix.c @@ -9,6 +9,10 @@ #include "../nfc_protocol_support_common.h" #include "../nfc_protocol_support_gui_common.h" +#include + +#define TAG "SlixWrite" + static void nfc_scene_info_on_enter_slix(NfcApp* instance) { const NfcDevice* device = instance->nfc_device; const SlixData* data = nfc_device_get_data(device, NfcProtocolSlix); @@ -106,8 +110,105 @@ static void nfc_scene_emulate_on_enter_slix(NfcApp* instance) { nfc_listener_start(instance->listener, nfc_scene_emulate_listener_callback_slix, instance); } +static NfcCommand nfc_scene_write_poller_callback_slix(NfcGenericEvent event, void* context) { + furi_assert(event.protocol == NfcProtocolSlix); + + NfcApp* instance = context; + SlixPoller* poller = event.instance; + const SlixPollerEvent* slix_event = event.event_data; + NfcCommand command = NfcCommandContinue; + + if(slix_event->type == SlixPollerEventTypeReady) { + const SlixData* write_data = nfc_device_get_data(instance->nfc_device, NfcProtocolSlix); + const Iso15693_3Data* write_iso_data = slix_get_base_data(write_data); + const Iso15693_3Data* card_iso_data = + slix_get_base_data((const SlixData*)nfc_poller_get_data(instance->poller)); + + view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventCardDetected); + + if(card_iso_data->system_info.block_count != write_iso_data->system_info.block_count || + card_iso_data->system_info.block_size != write_iso_data->system_info.block_size) { + furi_string_set( + instance->text_box_store, + "Incompatible card\n(block count/size\nmismatch)"); + view_dispatcher_send_custom_event( + instance->view_dispatcher, NfcCustomEventPollerFailure); + } else { + SlixError error = SlixErrorNone; + uint16_t failed_block = 0; + const uint8_t block_size = write_iso_data->system_info.block_size; + const uint8_t* write_data_ptr = + (const uint8_t*)simple_array_cget_data(write_iso_data->block_data); + const uint8_t* card_data_ptr = + (const uint8_t*)simple_array_cget_data(card_iso_data->block_data); + for(uint16_t i = 0; i < write_iso_data->system_info.block_count; i++) { + if(iso15693_3_is_block_locked(write_iso_data, i)) continue; + // Skip blocks that already contain the correct data + if(memcmp( + write_data_ptr + i * block_size, + card_data_ptr + i * block_size, + block_size) == 0) + continue; + error = slix_poller_write_block( + poller, write_data_ptr + i * block_size, i, block_size); + if(error == SlixErrorInternal) { + // Block is locked on the target card, skip it + error = SlixErrorNone; + continue; + } + if(error != SlixErrorNone) { + failed_block = i; + FURI_LOG_E(TAG, "Write failed: block %u, error %d", failed_block, (int)error); + break; + } + } + + if(error == SlixErrorNone) { + view_dispatcher_send_custom_event( + instance->view_dispatcher, NfcCustomEventPollerSuccess); + } else { + const char* err_name; + switch(error) { + case SlixErrorTimeout: + err_name = "Timeout"; + break; + case SlixErrorFormat: + err_name = "BadCRC"; + break; + case SlixErrorNotSupported: + err_name = "NotSupported"; + break; + case SlixErrorInternal: + err_name = "Locked"; + break; + case SlixErrorWrongPassword: + err_name = "WrongPwd"; + break; + default: + err_name = "Unknown"; + break; + } + furi_string_printf( + instance->text_box_store, "Block %u: %s", failed_block, err_name); + view_dispatcher_send_custom_event( + instance->view_dispatcher, NfcCustomEventPollerFailure); + } + } + command = NfcCommandStop; + } + + return command; +} + +static void nfc_scene_write_on_enter_slix(NfcApp* instance) { + furi_string_set(instance->text_box_store, "Apply the\ntarget card now"); + instance->poller = nfc_poller_alloc(instance->nfc, NfcProtocolSlix); + nfc_poller_start(instance->poller, nfc_scene_write_poller_callback_slix, instance); +} + const NfcProtocolSupportBase nfc_protocol_support_slix = { - .features = NfcProtocolFeatureEmulateFull | NfcProtocolFeatureMoreInfo, + .features = NfcProtocolFeatureEmulateFull | NfcProtocolFeatureMoreInfo | + NfcProtocolFeatureWrite, .scene_info = { @@ -151,7 +252,7 @@ const NfcProtocolSupportBase nfc_protocol_support_slix = { }, .scene_write = { - .on_enter = nfc_protocol_support_common_on_enter_empty, + .on_enter = nfc_scene_write_on_enter_slix, .on_event = nfc_protocol_support_common_on_event_empty, }, }; diff --git a/lib/nfc/protocols/iso15693_3/iso15693_3.h b/lib/nfc/protocols/iso15693_3/iso15693_3.h index fdff7112b..d727429bd 100644 --- a/lib/nfc/protocols/iso15693_3/iso15693_3.h +++ b/lib/nfc/protocols/iso15693_3/iso15693_3.h @@ -15,6 +15,11 @@ extern "C" { #define ISO15693_3_FDT_LISTEN_FC (4320U) #define ISO15693_3_POLL_POLL_MIN_US (1500U) +/* NVM write time varies by tag type (ICODE SLIX: 9.6ms, generic: up to 20ms). + * ISO 15693-3 write commands require a longer FWT than reads to account for + * EEPROM programming delays before the tag responds. 20ms covers all known tags. */ +#define ISO15693_3_FDT_WRITE_POLL_FC (271200U) + #define ISO15693_3_REQ_FLAG_SUBCARRIER_1 (0U << 0) #define ISO15693_3_REQ_FLAG_SUBCARRIER_2 (1U << 0) #define ISO15693_3_REQ_FLAG_DATA_RATE_LO (0U << 1) diff --git a/lib/nfc/protocols/iso15693_3/iso15693_3_i.c b/lib/nfc/protocols/iso15693_3/iso15693_3_i.c index 3d8d95c3a..b3f68e0b5 100644 --- a/lib/nfc/protocols/iso15693_3/iso15693_3_i.c +++ b/lib/nfc/protocols/iso15693_3/iso15693_3_i.c @@ -176,6 +176,25 @@ Iso15693_3Error return ret; } +Iso15693_3Error iso15693_3_write_block_response_parse(const BitBuffer* buf) { + Iso15693_3Error ret = Iso15693_3ErrorNone; + + do { + if(iso15693_3_error_response_parse(&ret, buf)) break; + + typedef struct { + uint8_t flags; + } WriteBlockResponseLayout; + + if(bit_buffer_get_size_bytes(buf) != sizeof(WriteBlockResponseLayout)) { + ret = Iso15693_3ErrorUnexpectedResponse; + break; + } + } while(false); + + return ret; +} + Iso15693_3Error iso15693_3_get_block_security_response_parse( uint8_t* data, uint16_t block_count, diff --git a/lib/nfc/protocols/iso15693_3/iso15693_3_i.h b/lib/nfc/protocols/iso15693_3/iso15693_3_i.h index 253bda7f5..955b67409 100644 --- a/lib/nfc/protocols/iso15693_3/iso15693_3_i.h +++ b/lib/nfc/protocols/iso15693_3/iso15693_3_i.h @@ -28,6 +28,8 @@ Iso15693_3Error Iso15693_3Error iso15693_3_read_block_response_parse(uint8_t* data, uint8_t block_size, const BitBuffer* buf); +Iso15693_3Error iso15693_3_write_block_response_parse(const BitBuffer* buf); + Iso15693_3Error iso15693_3_get_block_security_response_parse( uint8_t* data, uint16_t block_count, diff --git a/lib/nfc/protocols/iso15693_3/iso15693_3_poller.h b/lib/nfc/protocols/iso15693_3/iso15693_3_poller.h index efe8337af..0f52012e9 100644 --- a/lib/nfc/protocols/iso15693_3/iso15693_3_poller.h +++ b/lib/nfc/protocols/iso15693_3/iso15693_3_poller.h @@ -13,6 +13,14 @@ extern "C" { */ typedef struct Iso15693_3Poller Iso15693_3Poller; +/** + * @brief Get the Iso15693_3 data accumulated by the poller. + * + * @param[in] instance pointer to the Iso15693_3Poller instance. + * @return pointer to the Iso15693_3Data structure filled during activation. + */ +const Iso15693_3Data* iso15693_3_poller_get_data(Iso15693_3Poller* instance); + /** * @brief Enumeration of possible Iso15693_3 poller event types. */ @@ -144,6 +152,40 @@ Iso15693_3Error iso15693_3_poller_get_blocks_security( uint8_t* data, uint16_t block_count); +/** + * @brief Write a single Iso15693_3 block. + * + * Must ONLY be used inside the callback function. + * + * @param[in, out] instance pointer to the instance to be used in the transaction. + * @param[in] data pointer to the buffer containing the data to write. + * @param[in] block_number block number to write. + * @param[in] block_size size of the block in bytes. + * @return Iso15693_3ErrorNone on success, an error code on failure. + */ +Iso15693_3Error iso15693_3_poller_write_block( + Iso15693_3Poller* instance, + const uint8_t* data, + uint8_t block_number, + uint8_t block_size); + +/** + * @brief Write multiple consecutive Iso15693_3 blocks. + * + * Must ONLY be used inside the callback function. + * + * @param[in, out] instance pointer to the instance to be used in the transaction. + * @param[in] data pointer to the buffer containing the data to write. + * @param[in] block_count number of blocks to write. + * @param[in] block_size size of each block in bytes. + * @return Iso15693_3ErrorNone on success, an error code on failure. + */ +Iso15693_3Error iso15693_3_poller_write_blocks( + Iso15693_3Poller* instance, + const uint8_t* data, + uint16_t block_count, + uint8_t block_size); + #ifdef __cplusplus } #endif diff --git a/lib/nfc/protocols/iso15693_3/iso15693_3_poller_i.c b/lib/nfc/protocols/iso15693_3/iso15693_3_poller_i.c index bc677ce67..a8a8dba87 100644 --- a/lib/nfc/protocols/iso15693_3/iso15693_3_poller_i.c +++ b/lib/nfc/protocols/iso15693_3/iso15693_3_poller_i.c @@ -234,6 +234,57 @@ Iso15693_3Error iso15693_3_poller_read_blocks( return ret; } +Iso15693_3Error iso15693_3_poller_write_block( + Iso15693_3Poller* instance, + const uint8_t* data, + uint8_t block_number, + uint8_t block_size) { + furi_assert(instance); + furi_assert(data); + + bit_buffer_reset(instance->tx_buffer); + bit_buffer_reset(instance->rx_buffer); + + bit_buffer_append_byte( + instance->tx_buffer, ISO15693_3_REQ_FLAG_SUBCARRIER_1 | ISO15693_3_REQ_FLAG_DATA_RATE_HI); + bit_buffer_append_byte(instance->tx_buffer, ISO15693_3_CMD_WRITE_BLOCK); + bit_buffer_append_byte(instance->tx_buffer, block_number); + bit_buffer_append_bytes(instance->tx_buffer, data, block_size); + + Iso15693_3Error ret; + + do { + ret = iso15693_3_poller_send_frame( + instance, instance->tx_buffer, instance->rx_buffer, ISO15693_3_FDT_WRITE_POLL_FC); + if(ret != Iso15693_3ErrorNone) break; + + ret = iso15693_3_write_block_response_parse(instance->rx_buffer); + } while(false); + + return ret; +} + +Iso15693_3Error iso15693_3_poller_write_blocks( + Iso15693_3Poller* instance, + const uint8_t* data, + uint16_t block_count, + uint8_t block_size) { + furi_assert(instance); + furi_assert(data); + furi_assert(block_count); + furi_assert(block_size); + + Iso15693_3Error ret = Iso15693_3ErrorNone; + + for(uint32_t i = 0; i < block_count; ++i) { + ret = iso15693_3_poller_write_block( + instance, &data[block_size * i], (uint8_t)i, block_size); + if(ret != Iso15693_3ErrorNone) break; + } + + return ret; +} + Iso15693_3Error iso15693_3_poller_get_blocks_security( Iso15693_3Poller* instance, uint8_t* data, diff --git a/lib/nfc/protocols/iso15693_3/iso15693_3_poller_i.h b/lib/nfc/protocols/iso15693_3/iso15693_3_poller_i.h index 346d0d724..1a0f48139 100644 --- a/lib/nfc/protocols/iso15693_3/iso15693_3_poller_i.h +++ b/lib/nfc/protocols/iso15693_3/iso15693_3_poller_i.h @@ -33,8 +33,6 @@ struct Iso15693_3Poller { void* context; }; -const Iso15693_3Data* iso15693_3_poller_get_data(Iso15693_3Poller* instance); - #ifdef __cplusplus } #endif diff --git a/lib/nfc/protocols/slix/slix_poller.h b/lib/nfc/protocols/slix/slix_poller.h index e78f7882a..e0fee1a6a 100644 --- a/lib/nfc/protocols/slix/slix_poller.h +++ b/lib/nfc/protocols/slix/slix_poller.h @@ -118,6 +118,40 @@ SlixError slix_poller_set_password( SlixPassword password, SlixRandomNumber random_number); +/** + * @brief Write a single block to a Slix card. + * + * Must ONLY be used inside the callback function. + * + * @param[in, out] instance pointer to the instance to be used in the transaction. + * @param[in] data pointer to the buffer containing the data to write. + * @param[in] block_number block number to write. + * @param[in] block_size size of the block in bytes. + * @return SlixErrorNone on success, an error code on failure. + */ +SlixError slix_poller_write_block( + SlixPoller* instance, + const uint8_t* data, + uint8_t block_number, + uint8_t block_size); + +/** + * @brief Write multiple consecutive blocks to a Slix card. + * + * Must ONLY be used inside the callback function. + * + * @param[in, out] instance pointer to the instance to be used in the transaction. + * @param[in] data pointer to the buffer containing the data to write. + * @param[in] block_count number of blocks to write. + * @param[in] block_size size of each block in bytes. + * @return SlixErrorNone on success, an error code on failure. + */ +SlixError slix_poller_write_blocks( + SlixPoller* instance, + const uint8_t* data, + uint16_t block_count, + uint8_t block_size); + #ifdef __cplusplus } #endif diff --git a/lib/nfc/protocols/slix/slix_poller_i.c b/lib/nfc/protocols/slix/slix_poller_i.c index ee6912cc4..7759e1591 100644 --- a/lib/nfc/protocols/slix/slix_poller_i.c +++ b/lib/nfc/protocols/slix/slix_poller_i.c @@ -128,3 +128,31 @@ SlixError slix_poller_set_password( return error; } + +SlixError slix_poller_write_block( + SlixPoller* instance, + const uint8_t* data, + uint8_t block_number, + uint8_t block_size) { + furi_assert(instance); + furi_assert(data); + + const Iso15693_3Error error = + iso15693_3_poller_write_block(instance->iso15693_3_poller, data, block_number, block_size); + return slix_process_iso15693_3_error(error); +} + +SlixError slix_poller_write_blocks( + SlixPoller* instance, + const uint8_t* data, + uint16_t block_count, + uint8_t block_size) { + furi_assert(instance); + furi_assert(data); + furi_assert(block_count); + furi_assert(block_size); + + const Iso15693_3Error error = + iso15693_3_poller_write_blocks(instance->iso15693_3_poller, data, block_count, block_size); + return slix_process_iso15693_3_error(error); +} diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index bb03dcbb6..bccd2c381 100755 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,87.6,, +Version,+,87.7,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/applications.h,, Header,+,applications/services/bt/bt_service/bt.h,, @@ -2360,11 +2360,14 @@ Function,+,iso15693_3_is_equal,_Bool,"const Iso15693_3Data*, const Iso15693_3Dat Function,+,iso15693_3_load,_Bool,"Iso15693_3Data*, FlipperFormat*, uint32_t" Function,+,iso15693_3_poller_activate,Iso15693_3Error,"Iso15693_3Poller*, Iso15693_3Data*" Function,+,iso15693_3_poller_get_blocks_security,Iso15693_3Error,"Iso15693_3Poller*, uint8_t*, uint16_t" +Function,+,iso15693_3_poller_get_data,const Iso15693_3Data*,Iso15693_3Poller* Function,+,iso15693_3_poller_get_system_info,Iso15693_3Error,"Iso15693_3Poller*, Iso15693_3SystemInfo*" Function,+,iso15693_3_poller_inventory,Iso15693_3Error,"Iso15693_3Poller*, uint8_t*" Function,+,iso15693_3_poller_read_block,Iso15693_3Error,"Iso15693_3Poller*, uint8_t*, uint8_t, uint8_t" Function,+,iso15693_3_poller_read_blocks,Iso15693_3Error,"Iso15693_3Poller*, uint8_t*, uint16_t, uint8_t" Function,+,iso15693_3_poller_send_frame,Iso15693_3Error,"Iso15693_3Poller*, const BitBuffer*, BitBuffer*, uint32_t" +Function,+,iso15693_3_poller_write_block,Iso15693_3Error,"Iso15693_3Poller*, const uint8_t*, uint8_t, uint8_t" +Function,+,iso15693_3_poller_write_blocks,Iso15693_3Error,"Iso15693_3Poller*, const uint8_t*, uint16_t, uint8_t" Function,+,iso15693_3_reset,void,Iso15693_3Data* Function,+,iso15693_3_save,_Bool,"const Iso15693_3Data*, FlipperFormat*" Function,+,iso15693_3_set_uid,_Bool,"Iso15693_3Data*, const uint8_t*, size_t" @@ -3397,6 +3400,8 @@ Function,+,slix_poller_get_random_number,SlixError,"SlixPoller*, SlixRandomNumbe Function,+,slix_poller_read_signature,SlixError,"SlixPoller*, SlixSignature*" Function,+,slix_poller_send_frame,SlixError,"SlixPoller*, const BitBuffer*, BitBuffer*, uint32_t" Function,+,slix_poller_set_password,SlixError,"SlixPoller*, SlixPasswordType, SlixPassword, SlixRandomNumber" +Function,+,slix_poller_write_block,SlixError,"SlixPoller*, const uint8_t*, uint8_t, uint8_t" +Function,+,slix_poller_write_blocks,SlixError,"SlixPoller*, const uint8_t*, uint16_t, uint8_t" Function,+,slix_reset,void,SlixData* Function,+,slix_save,_Bool,"const SlixData*, FlipperFormat*" Function,+,slix_set_uid,_Bool,"SlixData*, const uint8_t*, size_t" From 17748be7f78108f0a262cdd718358dba1951d03c Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:34:26 +0300 Subject: [PATCH 24/71] upd changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f9db5911..d432473eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,12 @@ * SubGHz: Add **CAME TOP44FGN** support in CAME TWEE protocol * SubGHz: Add all 0x0s and all 0xFs KeeLoq MF codes for normal and simple learning * SubGHz: **Fix CAME TWEE repeats count for button click** +* NFC: Add **ISO15693-3 and SLIX write-back support** (PR #984 | by @DoniyorI) * NFC: **Fix "MIR" and other EMV cards crash on Read** (by @Dmitry422) * NFC: Add **Mifare Ultralight C Write Support** (by @haw8411) * NFC: Add **new parsers SZPPK, SKPPK and SevPPK**, upgrade Plantain parser, fix TwoCities parser (PR #981 | by @mxcdoam) * OFW PR 4362: NFC: **Fix BusFault** in Write to Initial Card (by @akrylysov) -* Apps: Build tag (**27mar2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) +* Apps: Build tag (**1apr2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * OFW PR 4364: JS: Address utf8 support when uploading JavaScript application (by @bekindpleaserewind) * Display: Extend lcd contrast range to full ST756x 6-bit range (by @ShaTie) (reduced to -10 to +18 to avoid fully unreadable state) From 9cc61f16bfc16105f8b02ab693f1c20c6a412d3b Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:34:41 +0300 Subject: [PATCH 25/71] fmt --- .../main/nfc/helpers/protocol_support/iso15693_3/iso15693_3.c | 3 ++- applications/main/nfc/helpers/protocol_support/slix/slix.c | 3 +-- lib/nfc/protocols/iso15693_3/iso15693_3_poller_i.c | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/applications/main/nfc/helpers/protocol_support/iso15693_3/iso15693_3.c b/applications/main/nfc/helpers/protocol_support/iso15693_3/iso15693_3.c index d68b97dc0..4451751f9 100644 --- a/applications/main/nfc/helpers/protocol_support/iso15693_3/iso15693_3.c +++ b/applications/main/nfc/helpers/protocol_support/iso15693_3/iso15693_3.c @@ -127,7 +127,8 @@ static NfcCommand if(card_data->system_info.block_count != write_data->system_info.block_count || card_data->system_info.block_size != write_data->system_info.block_size) { - furi_string_set(instance->text_box_store, "Incompatible card\n(block count/size\nmismatch)"); + furi_string_set( + instance->text_box_store, "Incompatible card\n(block count/size\nmismatch)"); view_dispatcher_send_custom_event( instance->view_dispatcher, NfcCustomEventPollerFailure); } else { diff --git a/applications/main/nfc/helpers/protocol_support/slix/slix.c b/applications/main/nfc/helpers/protocol_support/slix/slix.c index 895c1f2b5..020323bc2 100644 --- a/applications/main/nfc/helpers/protocol_support/slix/slix.c +++ b/applications/main/nfc/helpers/protocol_support/slix/slix.c @@ -129,8 +129,7 @@ static NfcCommand nfc_scene_write_poller_callback_slix(NfcGenericEvent event, vo if(card_iso_data->system_info.block_count != write_iso_data->system_info.block_count || card_iso_data->system_info.block_size != write_iso_data->system_info.block_size) { furi_string_set( - instance->text_box_store, - "Incompatible card\n(block count/size\nmismatch)"); + instance->text_box_store, "Incompatible card\n(block count/size\nmismatch)"); view_dispatcher_send_custom_event( instance->view_dispatcher, NfcCustomEventPollerFailure); } else { diff --git a/lib/nfc/protocols/iso15693_3/iso15693_3_poller_i.c b/lib/nfc/protocols/iso15693_3/iso15693_3_poller_i.c index a8a8dba87..f738f2087 100644 --- a/lib/nfc/protocols/iso15693_3/iso15693_3_poller_i.c +++ b/lib/nfc/protocols/iso15693_3/iso15693_3_poller_i.c @@ -277,8 +277,8 @@ Iso15693_3Error iso15693_3_poller_write_blocks( Iso15693_3Error ret = Iso15693_3ErrorNone; for(uint32_t i = 0; i < block_count; ++i) { - ret = iso15693_3_poller_write_block( - instance, &data[block_size * i], (uint8_t)i, block_size); + ret = + iso15693_3_poller_write_block(instance, &data[block_size * i], (uint8_t)i, block_size); if(ret != Iso15693_3ErrorNone) break; } From e17ea14ac874b45d1bd07b65d313a6c01a9783f8 Mon Sep 17 00:00:00 2001 From: jlaughter <53415135+jlaughter@users.noreply.github.com> Date: Sun, 19 Apr 2026 01:30:37 -0400 Subject: [PATCH 26/71] Create allstar_firefly.h --- lib/subghz/protocols/allstar_firefly.h | 61 ++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 lib/subghz/protocols/allstar_firefly.h diff --git a/lib/subghz/protocols/allstar_firefly.h b/lib/subghz/protocols/allstar_firefly.h new file mode 100644 index 000000000..6ebf0dda6 --- /dev/null +++ b/lib/subghz/protocols/allstar_firefly.h @@ -0,0 +1,61 @@ +#pragma once + +/** + * allstar_firefly.h - Allstar Firefly 318ALD31K native SubGHz protocol + * + * Registers the Allstar Firefly gate remote as a first-class SubGHz protocol, + * supporting decode from air, save/load of .sub files, and retransmit. + * + * Protocol summary (measured from captured remotes): + * Carrier : 318 MHz OOK (FuriHalSubGhzPresetOok650Async) + * Code type : Static 9-bit trinary (Supertex ED-9 encoder IC) + * Frame : 18 symbols (2 per bit), no separate sync pulse + * Repeats : 20 frames per keypress + * Inter-frame gap: ~30 440 us + * + * Symbol encoding - each bit = two (pulse, gap) pairs: + * '+' ON : H H = [4045us HIGH, 607us LOW] x2 + * '-' OFF : L L = [530us HIGH, 4139us LOW] x2 + * '0' FLOAT : H L = [4045us HIGH, 607us LOW, 530us HIGH, 4139us LOW] + * + * Save file format: + * Filetype: Flipper SubGhz Key File + * Version: 1 + * Frequency: 318000000 + * Preset: FuriHalSubGhzPresetOok650Async + * Protocol: Allstar Firefly + * Key: +--000++- + * + * To register with the SubGHz app, in protocol_list.c add: + * #include "allstar_firefly.h" + * &subghz_protocol_allstar_firefly, (in the protocol array) + */ + +#include + +/* Protocol name (must match what is written to .sub files) */ +#define SUBGHZ_PROTOCOL_ALLSTAR_FIREFLY_NAME "Allstar Firefly" + +/* Timing constants (from 37-frame analysis of two captures) */ +#define AF_FREQ 318000000UL +#define AF_BIT_COUNT 9u +#define AF_SYM_COUNT 18u + +#define AF_LONG_PULSE_US 4045u +#define AF_SHORT_PULSE_US 530u +#define AF_SHORT_GAP_US 607u +#define AF_LONG_GAP_US 4139u +#define AF_INTERFRAME_US 30440u + +#define AF_SHORT_PULSE_MIN 300u +#define AF_SHORT_PULSE_MAX 1200u +#define AF_LONG_PULSE_MIN 2500u +#define AF_LONG_PULSE_MAX 5500u +#define AF_FRAME_THRESH_US 20000u +#define AF_TX_REPEAT 20u +#define AF_TX_BUF_SIZE (AF_TX_REPEAT * AF_SYM_COUNT * 2u + 8u) + +/* Protocol vtable entries */ +extern const SubGhzProtocolDecoder subghz_protocol_decoder_allstar_firefly; +extern const SubGhzProtocolEncoder subghz_protocol_encoder_allstar_firefly; +extern const SubGhzProtocol subghz_protocol_allstar_firefly; From 1f2022b87c0416e5e67709f9f62de34511997790 Mon Sep 17 00:00:00 2001 From: jlaughter <53415135+jlaughter@users.noreply.github.com> Date: Sun, 19 Apr 2026 01:35:55 -0400 Subject: [PATCH 27/71] subghz: add Allstar Firefly 318ALD31K protocol implementation --- lib/subghz/protocols/allstar_firefly.c | 327 +++++++++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 lib/subghz/protocols/allstar_firefly.c diff --git a/lib/subghz/protocols/allstar_firefly.c b/lib/subghz/protocols/allstar_firefly.c new file mode 100644 index 000000000..cbec4a36e --- /dev/null +++ b/lib/subghz/protocols/allstar_firefly.c @@ -0,0 +1,327 @@ +/** + * allstar_firefly.c -- Allstar Firefly 318ALD31K native SubGHz protocol + * + * Implements the SubGhzProtocol vtable for the Supertex ED-9 based gate remote. + * Both the decoder state machine and encoder TX buffer are ported directly from + * the proven FAP implementation; only the framing (alloc/free/serialize/etc.) + * changes to match the native SubGHz plugin interface. + * + * See allstar_firefly.h for full protocol documentation. + */ + +#include "allstar_firefly.h" + +#include + +#include +#include +#include +#include + +#define TAG "AllstarFirefly" + +/* --- Decoder instance ------------------------------------------------------ */ + +typedef enum { + AfRxState_WaitGap, + AfRxState_Receiving, +} AfRxState; + +typedef struct { + SubGhzProtocolDecoderBase base; + AfRxState rx_state; + uint8_t rx_syms[AF_SYM_COUNT]; + uint8_t rx_count; + char dip[AF_BIT_COUNT + 1]; +} SubGhzProtocolDecoderAllstarFirefly; + +typedef struct { + SubGhzProtocolEncoderBase base; + char dip[AF_BIT_COUNT + 1]; + uint32_t tx_buf[AF_TX_BUF_SIZE]; + uint32_t tx_size; + uint32_t tx_pos; +} SubGhzProtocolEncoderAllstarFirefly; + +static bool af_decode_symbols(const uint8_t* syms, char* dip) { + for(uint8_t i = 0; i < AF_BIT_COUNT; i++) { + uint8_t a = syms[i * 2]; + uint8_t b = syms[i * 2 + 1]; + if (a == 1 && b == 1) dip[i] = '+'; + else if (a == 0 && b == 0) dip[i] = '-'; + else if (a == 1 && b == 0) dip[i] = '0'; + else return false; + } + dip[AF_BIT_COUNT] = '\0'; + return true; +} + +static bool af_dip_valid(const char* dip) { + if(!dip) return false; + for(uint8_t i = 0; i < AF_BIT_COUNT; i++) { + if(dip[i] != '+' && dip[i] != '-' && dip[i] != '0') return false; + } + return (dip[AF_BIT_COUNT] == '\0'); +} + +static uint32_t af_dip_to_uint32(const char* dip) { + uint32_t val = 0; + for(uint8_t i = 0; i < AF_BIT_COUNT; i++) { + val *= 3; + if (dip[i] == '+') val += 2; + else if(dip[i] == '0') val += 1; + } + return val; +} + +static uint32_t af_build_tx_buf(const char* dip, uint32_t* buf) { + uint32_t pos = 0; + for(uint32_t rep = 0; rep < AF_TX_REPEAT; rep++) { + for(uint32_t bit = 0; bit < AF_BIT_COUNT; bit++) { + bool last = (bit == AF_BIT_COUNT - 1); + char c = dip[bit]; + uint32_t p0, g0, p1, g1; + if(c == '+') { + p0 = AF_LONG_PULSE_US; g0 = AF_SHORT_GAP_US; + p1 = AF_LONG_PULSE_US; g1 = AF_SHORT_GAP_US; + } else if(c == '-') { + p0 = AF_SHORT_PULSE_US; g0 = AF_LONG_GAP_US; + p1 = AF_SHORT_PULSE_US; g1 = AF_LONG_GAP_US; + } else { + p0 = AF_LONG_PULSE_US; g0 = AF_SHORT_GAP_US; + p1 = AF_SHORT_PULSE_US; g1 = AF_LONG_GAP_US; + } + if(last) g1 = AF_INTERFRAME_US; + buf[pos++] = p0; buf[pos++] = g0; + buf[pos++] = p1; buf[pos++] = g1; + } + } + return pos; +} +static void* subghz_protocol_decoder_allstar_firefly_alloc(SubGhzEnvironment* environment) { + UNUSED(environment); + SubGhzProtocolDecoderAllstarFirefly* instance = + malloc(sizeof(SubGhzProtocolDecoderAllstarFirefly)); + instance->base.protocol = &subghz_protocol_allstar_firefly; + instance->rx_state = AfRxState_WaitGap; + instance->rx_count = 0; + memset(instance->dip, '-', AF_BIT_COUNT); + instance->dip[AF_BIT_COUNT] = '\0'; + return instance; +} + +static void subghz_protocol_decoder_allstar_firefly_free(void* context) { + furi_assert(context); free(context); +} + +static void subghz_protocol_decoder_allstar_firefly_reset(void* context) { + furi_assert(context); + SubGhzProtocolDecoderAllstarFirefly* instance = context; + instance->rx_state = AfRxState_WaitGap; + instance->rx_count = 0; +} + +static void subghz_protocol_decoder_allstar_firefly_feed( + void* context, bool level, uint32_t duration) { + + furi_assert(context); + SubGhzProtocolDecoderAllstarFirefly* instance = context; + + if(level) { + if(instance->rx_state == AfRxState_Receiving) { + uint8_t sym; + if(duration >= AF_LONG_PULSE_MIN && duration <= AF_LONG_PULSE_MAX) { + sym = 1u; + } else if(duration >= AF_SHORT_PULSE_MIN && duration <= AF_SHORT_PULSE_MAX) { + sym = 0u; + } else { + instance->rx_state = AfRxState_WaitGap; + instance->rx_count = 0; + return; + } + if(instance->rx_count < AF_SYM_COUNT) { + instance->rx_syms[instance->rx_count++] = sym; + } + } + } else { + if(duration >= AF_FRAME_THRESH_US) { + if(instance->rx_state == AfRxState_Receiving && + instance->rx_count == AF_SYM_COUNT) { + char decoded[AF_BIT_COUNT + 1]; + if(af_decode_symbols(instance->rx_syms, decoded)) { + memcpy(instance->dip, decoded, AF_BIT_COUNT + 1); + if(instance->base.callback) { + instance->base.callback( + &instance->base, + instance->base.context); + } + } + instance->rx_state = AfRxState_WaitGap; + instance->rx_count = 0; + } else if(instance->rx_state == AfRxState_WaitGap) { + instance->rx_state = AfRxState_Receiving; + instance->rx_count = 0; + } else { + instance->rx_state = AfRxState_WaitGap; + instance->rx_count = 0; + } + } + } +} + +static uint8_t subghz_protocol_decoder_allstar_firefly_get_hash_data(void* context) { + furi_assert(context); + SubGhzProtocolDecoderAllstarFirefly* instance = context; + uint8_t hash = 0; + for(uint8_t i = 0; i < AF_BIT_COUNT; i++) hash ^= (uint8_t)instance->dip[i]; + return hash; +} +static SubGhzProtocolStatus subghz_protocol_decoder_allstar_firefly_serialize( + void* context, FlipperFormat* flipper_format, SubGhzRadioPreset* preset) { + + furi_assert(context); + SubGhzProtocolDecoderAllstarFirefly* instance = context; + if(!flipper_format_write_uint32(flipper_format, "Frequency", &preset->frequency, 1)) { + FURI_LOG_E(TAG, "Failed to write Frequency"); + return SubGhzProtocolStatusErrorParserOthers; + } + if(!flipper_format_write_string(flipper_format, "Preset", preset->name)) { + FURI_LOG_E(TAG, "Failed to write Preset"); + return SubGhzProtocolStatusErrorParserOthers; + } + if(!flipper_format_write_string_cstr( + flipper_format, "Protocol", SUBGHZ_PROTOCOL_ALLSTAR_FIREFLY_NAME)) { + FURI_LOG_E(TAG, "Failed to write Protocol"); + return SubGhzProtocolStatusErrorParserOthers; + } + if(!flipper_format_write_string_cstr(flipper_format, "Key", instance->dip)) { + FURI_LOG_E(TAG, "Failed to write Key"); + return SubGhzProtocolStatusErrorParserOthers; + } + return SubGhzProtocolStatusOk; +} + +static SubGhzProtocolStatus subghz_protocol_decoder_allstar_firefly_deserialize( + void* context, FlipperFormat* flipper_format) { + + furi_assert(context); + SubGhzProtocolDecoderAllstarFirefly* instance = context; + FuriString* key_str = furi_string_alloc(); + if(!flipper_format_read_string(flipper_format, "Key", key_str)) { + FURI_LOG_E(TAG, "Missing Key field"); + furi_string_free(key_str); + return SubGhzProtocolStatusErrorParserOthers; + } + const char* key_cstr = furi_string_get_cstr(key_str); + if(strlen(key_cstr) != AF_BIT_COUNT || !af_dip_valid(key_cstr)) { + FURI_LOG_E(TAG, "Invalid Key value: %s", key_cstr); + furi_string_free(key_str); + return SubGhzProtocolStatusErrorParserOthers; + } + memcpy(instance->dip, key_cstr, AF_BIT_COUNT + 1); + furi_string_free(key_str); + return SubGhzProtocolStatusOk; +} + +static void subghz_protocol_decoder_allstar_firefly_get_string( + void* context, FuriString* output) { + + furi_assert(context); + SubGhzProtocolDecoderAllstarFirefly* instance = context; + uint32_t code = af_dip_to_uint32(instance->dip); + furi_string_cat_printf( + output, + "%s\r\n0x%04lX\r\n" + "Freq: 318MHz OOK\r\n" + "1 2 3 4 5 6 7 8 9\r\n" + "%c %c %c %c %c %c %c %c %c", + SUBGHZ_PROTOCOL_ALLSTAR_FIREFLY_NAME, + (unsigned long)code, + instance->dip[0], instance->dip[1], instance->dip[2], + instance->dip[3], instance->dip[4], instance->dip[5], + instance->dip[6], instance->dip[7], instance->dip[8]); +} +static void* subghz_protocol_encoder_allstar_firefly_alloc(SubGhzEnvironment* environment) { + UNUSED(environment); + SubGhzProtocolEncoderAllstarFirefly* instance = + malloc(sizeof(SubGhzProtocolEncoderAllstarFirefly)); + instance->base.protocol = &subghz_protocol_allstar_firefly; + memset(instance->dip, '-', AF_BIT_COUNT); + instance->dip[AF_BIT_COUNT] = '\0'; + instance->tx_size = 0; + instance->tx_pos = 0; + return instance; +} + +static void subghz_protocol_encoder_allstar_firefly_free(void* context) { + furi_assert(context); free(context); +} + +static void subghz_protocol_encoder_allstar_firefly_stop(void* context) { + UNUSED(context); +} + +static SubGhzProtocolStatus subghz_protocol_encoder_allstar_firefly_deserialize( + void* context, FlipperFormat* flipper_format) { + + furi_assert(context); + SubGhzProtocolEncoderAllstarFirefly* instance = context; + FuriString* key_str = furi_string_alloc(); + if(!flipper_format_read_string(flipper_format, "Key", key_str)) { + FURI_LOG_E(TAG, "Encoder: missing Key field"); + furi_string_free(key_str); + return SubGhzProtocolStatusErrorParserOthers; + } + const char* key_cstr = furi_string_get_cstr(key_str); + if(strlen(key_cstr) != AF_BIT_COUNT || !af_dip_valid(key_cstr)) { + FURI_LOG_E(TAG, "Encoder: invalid Key: %s", key_cstr); + furi_string_free(key_str); + return SubGhzProtocolStatusErrorParserOthers; + } + memcpy(instance->dip, key_cstr, AF_BIT_COUNT + 1); + furi_string_free(key_str); + instance->tx_size = af_build_tx_buf(instance->dip, instance->tx_buf); + instance->tx_pos = 0; + return SubGhzProtocolStatusOk; +} + +static LevelDuration subghz_protocol_encoder_allstar_firefly_yield(void* context) { + furi_assert(context); + SubGhzProtocolEncoderAllstarFirefly* instance = context; + if(instance->tx_pos >= instance->tx_size) { + return level_duration_reset(); + } + bool lv = (instance->tx_pos % 2 == 0); + uint32_t dur = instance->tx_buf[instance->tx_pos++]; + return level_duration_make(lv, dur); +} +const SubGhzProtocolDecoder subghz_protocol_decoder_allstar_firefly = { + .alloc = subghz_protocol_decoder_allstar_firefly_alloc, + .free = subghz_protocol_decoder_allstar_firefly_free, + .feed = subghz_protocol_decoder_allstar_firefly_feed, + .reset = subghz_protocol_decoder_allstar_firefly_reset, + .get_hash_data = subghz_protocol_decoder_allstar_firefly_get_hash_data, + .serialize = subghz_protocol_decoder_allstar_firefly_serialize, + .deserialize = subghz_protocol_decoder_allstar_firefly_deserialize, + .get_string = subghz_protocol_decoder_allstar_firefly_get_string, +}; + +const SubGhzProtocolEncoder subghz_protocol_encoder_allstar_firefly = { + .alloc = subghz_protocol_encoder_allstar_firefly_alloc, + .free = subghz_protocol_encoder_allstar_firefly_free, + .deserialize = subghz_protocol_encoder_allstar_firefly_deserialize, + .stop = subghz_protocol_encoder_allstar_firefly_stop, + .yield = subghz_protocol_encoder_allstar_firefly_yield, +}; + +const SubGhzProtocol subghz_protocol_allstar_firefly = { + .name = SUBGHZ_PROTOCOL_ALLSTAR_FIREFLY_NAME, + .type = SubGhzProtocolTypeStatic, + .flag = SubGhzProtocolFlag_AM | + SubGhzProtocolFlag_Decodable | + SubGhzProtocolFlag_Load | + SubGhzProtocolFlag_Save | + SubGhzProtocolFlag_Send, + .decoder = &subghz_protocol_decoder_allstar_firefly, + .encoder = &subghz_protocol_encoder_allstar_firefly, +}; From e7098821ca062d462e186dcc7aace32231c3c0f5 Mon Sep 17 00:00:00 2001 From: jlaughter <53415135+jlaughter@users.noreply.github.com> Date: Sun, 19 Apr 2026 01:39:04 -0400 Subject: [PATCH 28/71] Update protocol_items.h --- lib/subghz/protocols/protocol_items.h | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/subghz/protocols/protocol_items.h b/lib/subghz/protocols/protocol_items.h index d9e5f0cd0..57930bdf5 100644 --- a/lib/subghz/protocols/protocol_items.h +++ b/lib/subghz/protocols/protocol_items.h @@ -2,6 +2,7 @@ #include "../registry.h" #include "../subghz_protocol_registry.h" +#include "allstar_firefly.h" #include "princeton.h" #include "keeloq.h" #include "nice_flo.h" From fa1d01d70f1c1bb40d15d54c3e6420e7fa920e62 Mon Sep 17 00:00:00 2001 From: jlaughter <53415135+jlaughter@users.noreply.github.com> Date: Sun, 19 Apr 2026 01:40:49 -0400 Subject: [PATCH 29/71] subghz: register Allstar Firefly in protocol_items.c --- lib/subghz/protocols/protocol_items.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/subghz/protocols/protocol_items.c b/lib/subghz/protocols/protocol_items.c index 416e3524f..25a0420ee 100644 --- a/lib/subghz/protocols/protocol_items.c +++ b/lib/subghz/protocols/protocol_items.c @@ -29,7 +29,7 @@ const SubGhzProtocol* const subghz_protocol_registry_items[] = { &subghz_protocol_elplast, &subghz_protocol_treadmill37, &subghz_protocol_beninca_arc, &subghz_protocol_jarolift, &subghz_protocol_ditec_gol4, &subghz_protocol_keyfinder, - &subghz_protocol_nord_ice, + &subghz_protocol_nord_ice, &subghz_protocol_allstar_firefly, }; const SubGhzProtocolRegistry subghz_protocol_registry = { From fd4e552c703891899587458797c9816b31b18614 Mon Sep 17 00:00:00 2001 From: jlaughter <53415135+jlaughter@users.noreply.github.com> Date: Sun, 19 Apr 2026 01:41:44 -0400 Subject: [PATCH 30/71] subghz: register Allstar Firefly in protocol_items.c --- lib/subghz/protocols/protocol_items.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/subghz/protocols/protocol_items.h b/lib/subghz/protocols/protocol_items.h index 57930bdf5..3a2b763f1 100644 --- a/lib/subghz/protocols/protocol_items.h +++ b/lib/subghz/protocols/protocol_items.h @@ -1,8 +1,6 @@ #pragma once #include "../registry.h" #include "../subghz_protocol_registry.h" - -#include "allstar_firefly.h" #include "princeton.h" #include "keeloq.h" #include "nice_flo.h" @@ -60,3 +58,4 @@ #include "ditec_gol4.h" #include "keyfinder.h" #include "nord_ice.h" +#include "allstar_firefly.h" From b5fff36498205fc4a82e4dd08c6f6bbec953220e Mon Sep 17 00:00:00 2001 From: jlaughter <53415135+jlaughter@users.noreply.github.com> Date: Sun, 19 Apr 2026 23:19:02 -0400 Subject: [PATCH 31/71] subghz: fix Allstar Firefly serialize/deserialize to use standard block_generic format --- lib/subghz/protocols/allstar_firefly.c | 163 ++++++++++++------------- 1 file changed, 75 insertions(+), 88 deletions(-) diff --git a/lib/subghz/protocols/allstar_firefly.c b/lib/subghz/protocols/allstar_firefly.c index cbec4a36e..29fa86d15 100644 --- a/lib/subghz/protocols/allstar_firefly.c +++ b/lib/subghz/protocols/allstar_firefly.c @@ -2,9 +2,18 @@ * allstar_firefly.c -- Allstar Firefly 318ALD31K native SubGHz protocol * * Implements the SubGhzProtocol vtable for the Supertex ED-9 based gate remote. - * Both the decoder state machine and encoder TX buffer are ported directly from - * the proven FAP implementation; only the framing (alloc/free/serialize/etc.) - * changes to match the native SubGHz plugin interface. + * Uses subghz_block_generic_serialize / deserialize for standard-format .sub + * files, encoding the 9-position trinary DIP code as a uint64 (base-3, MSB + * first: '+' = 2, '0' = 1, '-' = 0). + * + * Saved file format: + * Filetype: Flipper SubGhz Key File + * Version: 1 + * Frequency: 318000000 + * Preset: FuriHalSubGhzPresetOok650Async + * Protocol: Allstar Firefly + * Key: 00 00 00 00 00 00 34 B9 + * Bit: 18 * * See allstar_firefly.h for full protocol documentation. */ @@ -12,6 +21,7 @@ #include "allstar_firefly.h" #include +#include #include #include @@ -20,8 +30,6 @@ #define TAG "AllstarFirefly" -/* --- Decoder instance ------------------------------------------------------ */ - typedef enum { AfRxState_WaitGap, AfRxState_Receiving, @@ -29,6 +37,7 @@ typedef enum { typedef struct { SubGhzProtocolDecoderBase base; + SubGhzBlockGeneric generic; AfRxState rx_state; uint8_t rx_syms[AF_SYM_COUNT]; uint8_t rx_count; @@ -37,6 +46,7 @@ typedef struct { typedef struct { SubGhzProtocolEncoderBase base; + SubGhzBlockGeneric generic; char dip[AF_BIT_COUNT + 1]; uint32_t tx_buf[AF_TX_BUF_SIZE]; uint32_t tx_size; @@ -64,8 +74,8 @@ static bool af_dip_valid(const char* dip) { return (dip[AF_BIT_COUNT] == '\0'); } -static uint32_t af_dip_to_uint32(const char* dip) { - uint32_t val = 0; +static uint64_t af_dip_to_uint64(const char* dip) { + uint64_t val = 0; for(uint8_t i = 0; i < AF_BIT_COUNT; i++) { val *= 3; if (dip[i] == '+') val += 2; @@ -74,6 +84,17 @@ static uint32_t af_dip_to_uint32(const char* dip) { return val; } +static void af_uint64_to_dip(uint64_t val, char* dip) { + for(int8_t i = AF_BIT_COUNT - 1; i >= 0; i--) { + uint8_t rem = (uint8_t)(val % 3); + val /= 3; + if (rem == 2) dip[i] = '+'; + else if(rem == 1) dip[i] = '0'; + else dip[i] = '-'; + } + dip[AF_BIT_COUNT] = '\0'; +} + static uint32_t af_build_tx_buf(const char* dip, uint32_t* buf) { uint32_t pos = 0; for(uint32_t rep = 0; rep < AF_TX_REPEAT; rep++) { @@ -98,15 +119,19 @@ static uint32_t af_build_tx_buf(const char* dip, uint32_t* buf) { } return pos; } + static void* subghz_protocol_decoder_allstar_firefly_alloc(SubGhzEnvironment* environment) { UNUSED(environment); SubGhzProtocolDecoderAllstarFirefly* instance = malloc(sizeof(SubGhzProtocolDecoderAllstarFirefly)); - instance->base.protocol = &subghz_protocol_allstar_firefly; - instance->rx_state = AfRxState_WaitGap; - instance->rx_count = 0; + instance->base.protocol = &subghz_protocol_allstar_firefly; + instance->generic.protocol_name = SUBGHZ_PROTOCOL_ALLSTAR_FIREFLY_NAME; + instance->generic.data = 0; + instance->generic.data_count_bit = AF_SYM_COUNT; + instance->rx_state = AfRxState_WaitGap; + instance->rx_count = 0; memset(instance->dip, '-', AF_BIT_COUNT); - instance->dip[AF_BIT_COUNT] = '\0'; + instance->dip[AF_BIT_COUNT] = '\0'; return instance; } @@ -139,9 +164,8 @@ static void subghz_protocol_decoder_allstar_firefly_feed( instance->rx_count = 0; return; } - if(instance->rx_count < AF_SYM_COUNT) { + if(instance->rx_count < AF_SYM_COUNT) instance->rx_syms[instance->rx_count++] = sym; - } } } else { if(duration >= AF_FRAME_THRESH_US) { @@ -150,11 +174,10 @@ static void subghz_protocol_decoder_allstar_firefly_feed( char decoded[AF_BIT_COUNT + 1]; if(af_decode_symbols(instance->rx_syms, decoded)) { memcpy(instance->dip, decoded, AF_BIT_COUNT + 1); - if(instance->base.callback) { - instance->base.callback( - &instance->base, - instance->base.context); - } + instance->generic.data = af_dip_to_uint64(decoded); + instance->generic.data_count_bit = AF_SYM_COUNT; + if(instance->base.callback) + instance->base.callback(&instance->base, instance->base.context); } instance->rx_state = AfRxState_WaitGap; instance->rx_count = 0; @@ -176,59 +199,30 @@ static uint8_t subghz_protocol_decoder_allstar_firefly_get_hash_data(void* conte for(uint8_t i = 0; i < AF_BIT_COUNT; i++) hash ^= (uint8_t)instance->dip[i]; return hash; } + static SubGhzProtocolStatus subghz_protocol_decoder_allstar_firefly_serialize( void* context, FlipperFormat* flipper_format, SubGhzRadioPreset* preset) { - furi_assert(context); SubGhzProtocolDecoderAllstarFirefly* instance = context; - if(!flipper_format_write_uint32(flipper_format, "Frequency", &preset->frequency, 1)) { - FURI_LOG_E(TAG, "Failed to write Frequency"); - return SubGhzProtocolStatusErrorParserOthers; - } - if(!flipper_format_write_string(flipper_format, "Preset", preset->name)) { - FURI_LOG_E(TAG, "Failed to write Preset"); - return SubGhzProtocolStatusErrorParserOthers; - } - if(!flipper_format_write_string_cstr( - flipper_format, "Protocol", SUBGHZ_PROTOCOL_ALLSTAR_FIREFLY_NAME)) { - FURI_LOG_E(TAG, "Failed to write Protocol"); - return SubGhzProtocolStatusErrorParserOthers; - } - if(!flipper_format_write_string_cstr(flipper_format, "Key", instance->dip)) { - FURI_LOG_E(TAG, "Failed to write Key"); - return SubGhzProtocolStatusErrorParserOthers; - } - return SubGhzProtocolStatusOk; + return subghz_block_generic_serialize(&instance->generic, flipper_format, preset); } static SubGhzProtocolStatus subghz_protocol_decoder_allstar_firefly_deserialize( void* context, FlipperFormat* flipper_format) { - furi_assert(context); SubGhzProtocolDecoderAllstarFirefly* instance = context; - FuriString* key_str = furi_string_alloc(); - if(!flipper_format_read_string(flipper_format, "Key", key_str)) { - FURI_LOG_E(TAG, "Missing Key field"); - furi_string_free(key_str); - return SubGhzProtocolStatusErrorParserOthers; - } - const char* key_cstr = furi_string_get_cstr(key_str); - if(strlen(key_cstr) != AF_BIT_COUNT || !af_dip_valid(key_cstr)) { - FURI_LOG_E(TAG, "Invalid Key value: %s", key_cstr); - furi_string_free(key_str); - return SubGhzProtocolStatusErrorParserOthers; - } - memcpy(instance->dip, key_cstr, AF_BIT_COUNT + 1); - furi_string_free(key_str); + SubGhzProtocolStatus status = subghz_block_generic_deserialize_check_count_bit( + &instance->generic, flipper_format, AF_SYM_COUNT); + if(status != SubGhzProtocolStatusOk) return status; + af_uint64_to_dip(instance->generic.data, instance->dip); + if(!af_dip_valid(instance->dip)) return SubGhzProtocolStatusErrorParserOthers; return SubGhzProtocolStatusOk; } static void subghz_protocol_decoder_allstar_firefly_get_string( void* context, FuriString* output) { - furi_assert(context); SubGhzProtocolDecoderAllstarFirefly* instance = context; - uint32_t code = af_dip_to_uint32(instance->dip); furi_string_cat_printf( output, "%s\r\n0x%04lX\r\n" @@ -236,20 +230,24 @@ static void subghz_protocol_decoder_allstar_firefly_get_string( "1 2 3 4 5 6 7 8 9\r\n" "%c %c %c %c %c %c %c %c %c", SUBGHZ_PROTOCOL_ALLSTAR_FIREFLY_NAME, - (unsigned long)code, + (unsigned long)(instance->generic.data), instance->dip[0], instance->dip[1], instance->dip[2], instance->dip[3], instance->dip[4], instance->dip[5], instance->dip[6], instance->dip[7], instance->dip[8]); } + static void* subghz_protocol_encoder_allstar_firefly_alloc(SubGhzEnvironment* environment) { UNUSED(environment); SubGhzProtocolEncoderAllstarFirefly* instance = malloc(sizeof(SubGhzProtocolEncoderAllstarFirefly)); - instance->base.protocol = &subghz_protocol_allstar_firefly; + instance->base.protocol = &subghz_protocol_allstar_firefly; + instance->generic.protocol_name = SUBGHZ_PROTOCOL_ALLSTAR_FIREFLY_NAME; + instance->generic.data = 0; + instance->generic.data_count_bit = AF_SYM_COUNT; memset(instance->dip, '-', AF_BIT_COUNT); - instance->dip[AF_BIT_COUNT] = '\0'; - instance->tx_size = 0; - instance->tx_pos = 0; + instance->dip[AF_BIT_COUNT] = '\0'; + instance->tx_size = 0; + instance->tx_pos = 0; return instance; } @@ -263,23 +261,13 @@ static void subghz_protocol_encoder_allstar_firefly_stop(void* context) { static SubGhzProtocolStatus subghz_protocol_encoder_allstar_firefly_deserialize( void* context, FlipperFormat* flipper_format) { - furi_assert(context); SubGhzProtocolEncoderAllstarFirefly* instance = context; - FuriString* key_str = furi_string_alloc(); - if(!flipper_format_read_string(flipper_format, "Key", key_str)) { - FURI_LOG_E(TAG, "Encoder: missing Key field"); - furi_string_free(key_str); - return SubGhzProtocolStatusErrorParserOthers; - } - const char* key_cstr = furi_string_get_cstr(key_str); - if(strlen(key_cstr) != AF_BIT_COUNT || !af_dip_valid(key_cstr)) { - FURI_LOG_E(TAG, "Encoder: invalid Key: %s", key_cstr); - furi_string_free(key_str); - return SubGhzProtocolStatusErrorParserOthers; - } - memcpy(instance->dip, key_cstr, AF_BIT_COUNT + 1); - furi_string_free(key_str); + SubGhzProtocolStatus status = subghz_block_generic_deserialize_check_count_bit( + &instance->generic, flipper_format, AF_SYM_COUNT); + if(status != SubGhzProtocolStatusOk) return status; + af_uint64_to_dip(instance->generic.data, instance->dip); + if(!af_dip_valid(instance->dip)) return SubGhzProtocolStatusErrorParserOthers; instance->tx_size = af_build_tx_buf(instance->dip, instance->tx_buf); instance->tx_pos = 0; return SubGhzProtocolStatusOk; @@ -288,22 +276,21 @@ static SubGhzProtocolStatus subghz_protocol_encoder_allstar_firefly_deserialize( static LevelDuration subghz_protocol_encoder_allstar_firefly_yield(void* context) { furi_assert(context); SubGhzProtocolEncoderAllstarFirefly* instance = context; - if(instance->tx_pos >= instance->tx_size) { - return level_duration_reset(); - } + if(instance->tx_pos >= instance->tx_size) return level_duration_reset(); bool lv = (instance->tx_pos % 2 == 0); uint32_t dur = instance->tx_buf[instance->tx_pos++]; return level_duration_make(lv, dur); } + const SubGhzProtocolDecoder subghz_protocol_decoder_allstar_firefly = { - .alloc = subghz_protocol_decoder_allstar_firefly_alloc, - .free = subghz_protocol_decoder_allstar_firefly_free, - .feed = subghz_protocol_decoder_allstar_firefly_feed, - .reset = subghz_protocol_decoder_allstar_firefly_reset, - .get_hash_data = subghz_protocol_decoder_allstar_firefly_get_hash_data, - .serialize = subghz_protocol_decoder_allstar_firefly_serialize, - .deserialize = subghz_protocol_decoder_allstar_firefly_deserialize, - .get_string = subghz_protocol_decoder_allstar_firefly_get_string, + .alloc = subghz_protocol_decoder_allstar_firefly_alloc, + .free = subghz_protocol_decoder_allstar_firefly_free, + .feed = subghz_protocol_decoder_allstar_firefly_feed, + .reset = subghz_protocol_decoder_allstar_firefly_reset, + .get_hash_data = subghz_protocol_decoder_allstar_firefly_get_hash_data, + .serialize = subghz_protocol_decoder_allstar_firefly_serialize, + .deserialize = subghz_protocol_decoder_allstar_firefly_deserialize, + .get_string = subghz_protocol_decoder_allstar_firefly_get_string, }; const SubGhzProtocolEncoder subghz_protocol_encoder_allstar_firefly = { @@ -317,10 +304,10 @@ const SubGhzProtocolEncoder subghz_protocol_encoder_allstar_firefly = { const SubGhzProtocol subghz_protocol_allstar_firefly = { .name = SUBGHZ_PROTOCOL_ALLSTAR_FIREFLY_NAME, .type = SubGhzProtocolTypeStatic, - .flag = SubGhzProtocolFlag_AM | + .flag = SubGhzProtocolFlag_AM | SubGhzProtocolFlag_Decodable | - SubGhzProtocolFlag_Load | - SubGhzProtocolFlag_Save | + SubGhzProtocolFlag_Load | + SubGhzProtocolFlag_Save | SubGhzProtocolFlag_Send, .decoder = &subghz_protocol_decoder_allstar_firefly, .encoder = &subghz_protocol_encoder_allstar_firefly, From d5fad4f38b1ef0f5774e6ef0b95dcf71392a91dc Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 21 Apr 2026 01:56:55 +0300 Subject: [PATCH 32/71] Remake to use other protocols codebase Also remake DIP UI and funcs --- lib/subghz/protocols/allstar_firefly.c | 554 ++++++++++++++----------- lib/subghz/protocols/allstar_firefly.h | 126 +++++- 2 files changed, 412 insertions(+), 268 deletions(-) diff --git a/lib/subghz/protocols/allstar_firefly.c b/lib/subghz/protocols/allstar_firefly.c index 29fa86d15..8f85a397f 100644 --- a/lib/subghz/protocols/allstar_firefly.c +++ b/lib/subghz/protocols/allstar_firefly.c @@ -4,7 +4,7 @@ * Implements the SubGhzProtocol vtable for the Supertex ED-9 based gate remote. * Uses subghz_block_generic_serialize / deserialize for standard-format .sub * files, encoding the 9-position trinary DIP code as a uint64 (base-3, MSB - * first: '+' = 2, '0' = 1, '-' = 0). + * first: '+' = 2, '0' = 3, '-' = 0). * * Saved file format: * Filetype: Flipper SubGhz Key File @@ -20,295 +20,357 @@ #include "allstar_firefly.h" -#include -#include - -#include -#include -#include -#include +#include "../blocks/const.h" +#include "../blocks/decoder.h" +#include "../blocks/encoder.h" +#include "../blocks/generic.h" +#include "../blocks/math.h" #define TAG "AllstarFirefly" -typedef enum { - AfRxState_WaitGap, - AfRxState_Receiving, -} AfRxState; +#define DIP_P 0b11 //(+) +#define DIP_O 0b10 //(0) +#define DIP_N 0b00 //(-) -typedef struct { +#define DIP_PATTERN "%c%c%c%c%c%c%c%c%c" + +#define SHOW_DIP_P(dip, check_dip) \ + ((((dip >> 0x10) & 0x3) == check_dip) ? '*' : '_'), \ + ((((dip >> 0xE) & 0x3) == check_dip) ? '*' : '_'), \ + ((((dip >> 0xC) & 0x3) == check_dip) ? '*' : '_'), \ + ((((dip >> 0xA) & 0x3) == check_dip) ? '*' : '_'), \ + ((((dip >> 0x8) & 0x3) == check_dip) ? '*' : '_'), \ + ((((dip >> 0x6) & 0x3) == check_dip) ? '*' : '_'), \ + ((((dip >> 0x4) & 0x3) == check_dip) ? '*' : '_'), \ + ((((dip >> 0x2) & 0x3) == check_dip) ? '*' : '_'), \ + ((((dip >> 0x0) & 0x3) == check_dip) ? '*' : '_') + +static const SubGhzBlockConst subghz_protocol_allstar_firefly_const = { + .te_short = 600, + .te_long = 4000, + .te_delta = 300, + .min_count_bit_for_found = 18, +}; + +struct SubGhzProtocolDecoderAllstarFirefly { SubGhzProtocolDecoderBase base; - SubGhzBlockGeneric generic; - AfRxState rx_state; - uint8_t rx_syms[AF_SYM_COUNT]; - uint8_t rx_count; - char dip[AF_BIT_COUNT + 1]; -} SubGhzProtocolDecoderAllstarFirefly; -typedef struct { + SubGhzBlockDecoder decoder; + SubGhzBlockGeneric generic; +}; + +struct SubGhzProtocolEncoderAllstarFirefly { SubGhzProtocolEncoderBase base; + + SubGhzProtocolBlockEncoder encoder; SubGhzBlockGeneric generic; - char dip[AF_BIT_COUNT + 1]; - uint32_t tx_buf[AF_TX_BUF_SIZE]; - uint32_t tx_size; - uint32_t tx_pos; -} SubGhzProtocolEncoderAllstarFirefly; +}; -static bool af_decode_symbols(const uint8_t* syms, char* dip) { - for(uint8_t i = 0; i < AF_BIT_COUNT; i++) { - uint8_t a = syms[i * 2]; - uint8_t b = syms[i * 2 + 1]; - if (a == 1 && b == 1) dip[i] = '+'; - else if (a == 0 && b == 0) dip[i] = '-'; - else if (a == 1 && b == 0) dip[i] = '0'; - else return false; - } - dip[AF_BIT_COUNT] = '\0'; - return true; -} +typedef enum { + AllstarFireflyDecoderStepReset = 0, + AllstarFireflyDecoderStepSaveDuration, + AllstarFireflyDecoderStepCheckDuration, +} AllstarFireflyDecoderStep; -static bool af_dip_valid(const char* dip) { - if(!dip) return false; - for(uint8_t i = 0; i < AF_BIT_COUNT; i++) { - if(dip[i] != '+' && dip[i] != '-' && dip[i] != '0') return false; - } - return (dip[AF_BIT_COUNT] == '\0'); -} +const SubGhzProtocolDecoder subghz_protocol_allstar_firefly_decoder = { + .alloc = subghz_protocol_decoder_allstar_firefly_alloc, + .free = subghz_protocol_decoder_allstar_firefly_free, -static uint64_t af_dip_to_uint64(const char* dip) { - uint64_t val = 0; - for(uint8_t i = 0; i < AF_BIT_COUNT; i++) { - val *= 3; - if (dip[i] == '+') val += 2; - else if(dip[i] == '0') val += 1; - } - return val; -} + .feed = subghz_protocol_decoder_allstar_firefly_feed, + .reset = subghz_protocol_decoder_allstar_firefly_reset, -static void af_uint64_to_dip(uint64_t val, char* dip) { - for(int8_t i = AF_BIT_COUNT - 1; i >= 0; i--) { - uint8_t rem = (uint8_t)(val % 3); - val /= 3; - if (rem == 2) dip[i] = '+'; - else if(rem == 1) dip[i] = '0'; - else dip[i] = '-'; - } - dip[AF_BIT_COUNT] = '\0'; -} + .get_hash_data = subghz_protocol_decoder_allstar_firefly_get_hash_data, + .serialize = subghz_protocol_decoder_allstar_firefly_serialize, + .deserialize = subghz_protocol_decoder_allstar_firefly_deserialize, + .get_string = subghz_protocol_decoder_allstar_firefly_get_string, +}; -static uint32_t af_build_tx_buf(const char* dip, uint32_t* buf) { - uint32_t pos = 0; - for(uint32_t rep = 0; rep < AF_TX_REPEAT; rep++) { - for(uint32_t bit = 0; bit < AF_BIT_COUNT; bit++) { - bool last = (bit == AF_BIT_COUNT - 1); - char c = dip[bit]; - uint32_t p0, g0, p1, g1; - if(c == '+') { - p0 = AF_LONG_PULSE_US; g0 = AF_SHORT_GAP_US; - p1 = AF_LONG_PULSE_US; g1 = AF_SHORT_GAP_US; - } else if(c == '-') { - p0 = AF_SHORT_PULSE_US; g0 = AF_LONG_GAP_US; - p1 = AF_SHORT_PULSE_US; g1 = AF_LONG_GAP_US; - } else { - p0 = AF_LONG_PULSE_US; g0 = AF_SHORT_GAP_US; - p1 = AF_SHORT_PULSE_US; g1 = AF_LONG_GAP_US; - } - if(last) g1 = AF_INTERFRAME_US; - buf[pos++] = p0; buf[pos++] = g0; - buf[pos++] = p1; buf[pos++] = g1; - } - } - return pos; -} +const SubGhzProtocolEncoder subghz_protocol_allstar_firefly_encoder = { + .alloc = subghz_protocol_encoder_allstar_firefly_alloc, + .free = subghz_protocol_encoder_allstar_firefly_free, -static void* subghz_protocol_decoder_allstar_firefly_alloc(SubGhzEnvironment* environment) { + .deserialize = subghz_protocol_encoder_allstar_firefly_deserialize, + .stop = subghz_protocol_encoder_allstar_firefly_stop, + .yield = subghz_protocol_encoder_allstar_firefly_yield, +}; + +const SubGhzProtocol subghz_protocol_allstar_firefly = { + .name = SUBGHZ_PROTOCOL_ALLSTAR_FIREFLY_NAME, + .type = SubGhzProtocolTypeStatic, + .flag = SubGhzProtocolFlag_315 | SubGhzProtocolFlag_AM | SubGhzProtocolFlag_Decodable | + SubGhzProtocolFlag_Load | SubGhzProtocolFlag_Save | SubGhzProtocolFlag_Send, + + .decoder = &subghz_protocol_allstar_firefly_decoder, + .encoder = &subghz_protocol_allstar_firefly_encoder, +}; + +void* subghz_protocol_encoder_allstar_firefly_alloc(SubGhzEnvironment* environment) { UNUSED(environment); - SubGhzProtocolDecoderAllstarFirefly* instance = - malloc(sizeof(SubGhzProtocolDecoderAllstarFirefly)); - instance->base.protocol = &subghz_protocol_allstar_firefly; - instance->generic.protocol_name = SUBGHZ_PROTOCOL_ALLSTAR_FIREFLY_NAME; - instance->generic.data = 0; - instance->generic.data_count_bit = AF_SYM_COUNT; - instance->rx_state = AfRxState_WaitGap; - instance->rx_count = 0; - memset(instance->dip, '-', AF_BIT_COUNT); - instance->dip[AF_BIT_COUNT] = '\0'; + SubGhzProtocolEncoderAllstarFirefly* instance = + malloc(sizeof(SubGhzProtocolEncoderAllstarFirefly)); + + instance->base.protocol = &subghz_protocol_allstar_firefly; + instance->generic.protocol_name = instance->base.protocol->name; + + instance->encoder.repeat = 5; + instance->encoder.size_upload = 256; + instance->encoder.upload = malloc(instance->encoder.size_upload * sizeof(LevelDuration)); + instance->encoder.is_running = false; return instance; } -static void subghz_protocol_decoder_allstar_firefly_free(void* context) { - furi_assert(context); free(context); +void subghz_protocol_encoder_allstar_firefly_free(void* context) { + furi_assert(context); + SubGhzProtocolEncoderAllstarFirefly* instance = context; + free(instance->encoder.upload); + free(instance); } -static void subghz_protocol_decoder_allstar_firefly_reset(void* context) { - furi_assert(context); - SubGhzProtocolDecoderAllstarFirefly* instance = context; - instance->rx_state = AfRxState_WaitGap; - instance->rx_count = 0; -} +/** + * Generating an upload from data. + * @param instance Pointer to a SubGhzProtocolEncoderAllstarFirefly instance + */ +static void subghz_protocol_encoder_allstar_firefly_get_upload( + SubGhzProtocolEncoderAllstarFirefly* instance) { + furi_assert(instance); + size_t index = 0; -static void subghz_protocol_decoder_allstar_firefly_feed( - void* context, bool level, uint32_t duration) { - - furi_assert(context); - SubGhzProtocolDecoderAllstarFirefly* instance = context; - - if(level) { - if(instance->rx_state == AfRxState_Receiving) { - uint8_t sym; - if(duration >= AF_LONG_PULSE_MIN && duration <= AF_LONG_PULSE_MAX) { - sym = 1u; - } else if(duration >= AF_SHORT_PULSE_MIN && duration <= AF_SHORT_PULSE_MAX) { - sym = 0u; + // Send key and GAP + for(uint8_t i = instance->generic.data_count_bit; i > 0; i--) { + if(bit_read(instance->generic.data, i - 1)) { + // Send bit 1 + instance->encoder.upload[index++] = + level_duration_make(true, (uint32_t)subghz_protocol_allstar_firefly_const.te_long); + if(i == 1) { + //Send gap if bit was last + instance->encoder.upload[index++] = level_duration_make( + false, (uint32_t)(subghz_protocol_allstar_firefly_const.te_short * 50) + 400); } else { - instance->rx_state = AfRxState_WaitGap; - instance->rx_count = 0; - return; + instance->encoder.upload[index++] = level_duration_make( + false, (uint32_t)subghz_protocol_allstar_firefly_const.te_short); } - if(instance->rx_count < AF_SYM_COUNT) - instance->rx_syms[instance->rx_count++] = sym; - } - } else { - if(duration >= AF_FRAME_THRESH_US) { - if(instance->rx_state == AfRxState_Receiving && - instance->rx_count == AF_SYM_COUNT) { - char decoded[AF_BIT_COUNT + 1]; - if(af_decode_symbols(instance->rx_syms, decoded)) { - memcpy(instance->dip, decoded, AF_BIT_COUNT + 1); - instance->generic.data = af_dip_to_uint64(decoded); - instance->generic.data_count_bit = AF_SYM_COUNT; - if(instance->base.callback) - instance->base.callback(&instance->base, instance->base.context); - } - instance->rx_state = AfRxState_WaitGap; - instance->rx_count = 0; - } else if(instance->rx_state == AfRxState_WaitGap) { - instance->rx_state = AfRxState_Receiving; - instance->rx_count = 0; + } else { + // Send bit 0 + instance->encoder.upload[index++] = level_duration_make( + true, (uint32_t)subghz_protocol_allstar_firefly_const.te_short); + if(i == 1) { + //Send gap if bit was last + instance->encoder.upload[index++] = level_duration_make( + false, (uint32_t)(subghz_protocol_allstar_firefly_const.te_short * 50) + 400); } else { - instance->rx_state = AfRxState_WaitGap; - instance->rx_count = 0; + instance->encoder.upload[index++] = level_duration_make( + false, (uint32_t)subghz_protocol_allstar_firefly_const.te_long); } } } + + instance->encoder.size_upload = index; + return; } -static uint8_t subghz_protocol_decoder_allstar_firefly_get_hash_data(void* context) { +SubGhzProtocolStatus subghz_protocol_encoder_allstar_firefly_deserialize( + void* context, + FlipperFormat* flipper_format) { + furi_assert(context); + SubGhzProtocolEncoderAllstarFirefly* instance = context; + SubGhzProtocolStatus ret = SubGhzProtocolStatusError; + do { + ret = subghz_block_generic_deserialize_check_count_bit( + &instance->generic, + flipper_format, + subghz_protocol_allstar_firefly_const.min_count_bit_for_found); + if(ret != SubGhzProtocolStatusOk) { + break; + } + // Optional value + flipper_format_read_uint32( + flipper_format, "Repeat", (uint32_t*)&instance->encoder.repeat, 1); + + subghz_protocol_encoder_allstar_firefly_get_upload(instance); + instance->encoder.is_running = true; + } while(false); + + return ret; +} + +void subghz_protocol_encoder_allstar_firefly_stop(void* context) { + SubGhzProtocolEncoderAllstarFirefly* instance = context; + instance->encoder.is_running = false; +} + +LevelDuration subghz_protocol_encoder_allstar_firefly_yield(void* context) { + SubGhzProtocolEncoderAllstarFirefly* instance = context; + + if(instance->encoder.repeat == 0 || !instance->encoder.is_running) { + instance->encoder.is_running = false; + return level_duration_reset(); + } + + LevelDuration ret = instance->encoder.upload[instance->encoder.front]; + + if(++instance->encoder.front == instance->encoder.size_upload) { + if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--; + instance->encoder.front = 0; + } + + return ret; +} + +void* subghz_protocol_decoder_allstar_firefly_alloc(SubGhzEnvironment* environment) { + UNUSED(environment); + SubGhzProtocolDecoderAllstarFirefly* instance = + malloc(sizeof(SubGhzProtocolDecoderAllstarFirefly)); + instance->base.protocol = &subghz_protocol_allstar_firefly; + instance->generic.protocol_name = instance->base.protocol->name; + return instance; +} + +void subghz_protocol_decoder_allstar_firefly_free(void* context) { furi_assert(context); SubGhzProtocolDecoderAllstarFirefly* instance = context; - uint8_t hash = 0; - for(uint8_t i = 0; i < AF_BIT_COUNT; i++) hash ^= (uint8_t)instance->dip[i]; - return hash; + free(instance); } -static SubGhzProtocolStatus subghz_protocol_decoder_allstar_firefly_serialize( - void* context, FlipperFormat* flipper_format, SubGhzRadioPreset* preset) { +void subghz_protocol_decoder_allstar_firefly_reset(void* context) { + furi_assert(context); + SubGhzProtocolDecoderAllstarFirefly* instance = context; + instance->decoder.parser_step = AllstarFireflyDecoderStepReset; +} + +void subghz_protocol_decoder_allstar_firefly_feed( + void* context, + bool level, + volatile uint32_t duration) { + furi_assert(context); + SubGhzProtocolDecoderAllstarFirefly* instance = context; + + // Allstar Firefly Decoder + // 2026 - by @jlaughter + // Remake to match other protocols code base - @xMasterX (MMX) + + switch(instance->decoder.parser_step) { + case AllstarFireflyDecoderStepReset: + if((!level) && + (DURATION_DIFF(duration, subghz_protocol_allstar_firefly_const.te_short * 50) < + subghz_protocol_allstar_firefly_const.te_delta * 5)) { + // 30k us + // Found GAP + instance->decoder.decode_data = 0; + instance->decoder.decode_count_bit = 0; + instance->decoder.parser_step = AllstarFireflyDecoderStepSaveDuration; + } + break; + case AllstarFireflyDecoderStepSaveDuration: + if(level) { + instance->decoder.te_last = duration; + instance->decoder.parser_step = AllstarFireflyDecoderStepCheckDuration; + } else { + instance->decoder.parser_step = AllstarFireflyDecoderStepReset; + } + break; + case AllstarFireflyDecoderStepCheckDuration: + if(!level) { + // Bit 1 is long and short timing = 4000us HIGH (te_last) and 600us LOW + if((DURATION_DIFF( + instance->decoder.te_last, subghz_protocol_allstar_firefly_const.te_long) < + subghz_protocol_allstar_firefly_const.te_delta) && + (DURATION_DIFF(duration, subghz_protocol_allstar_firefly_const.te_short) < + subghz_protocol_allstar_firefly_const.te_delta)) { + subghz_protocol_blocks_add_bit(&instance->decoder, 1); + instance->decoder.parser_step = AllstarFireflyDecoderStepSaveDuration; + // Bit 0 is short and long timing = 600us HIGH (te_last) and 4000us LOW + } else if( + (DURATION_DIFF( + instance->decoder.te_last, subghz_protocol_allstar_firefly_const.te_short) < + subghz_protocol_allstar_firefly_const.te_delta) && + (DURATION_DIFF(duration, subghz_protocol_allstar_firefly_const.te_long) < + subghz_protocol_allstar_firefly_const.te_delta)) { + subghz_protocol_blocks_add_bit(&instance->decoder, 0); + instance->decoder.parser_step = AllstarFireflyDecoderStepSaveDuration; + } else if( + // End of the key + DURATION_DIFF(duration, subghz_protocol_allstar_firefly_const.te_short * 50) < + subghz_protocol_allstar_firefly_const.te_delta * 5) { + // Found next GAP and add bit 0 or 1 + if((DURATION_DIFF( + instance->decoder.te_last, subghz_protocol_allstar_firefly_const.te_long) < + subghz_protocol_allstar_firefly_const.te_delta)) { + subghz_protocol_blocks_add_bit(&instance->decoder, 1); + } + if((DURATION_DIFF( + instance->decoder.te_last, + subghz_protocol_allstar_firefly_const.te_short) < + subghz_protocol_allstar_firefly_const.te_delta)) { + subghz_protocol_blocks_add_bit(&instance->decoder, 0); + } + // If got 18 bits key reading is finished + if(instance->decoder.decode_count_bit == + subghz_protocol_allstar_firefly_const.min_count_bit_for_found) { + instance->generic.data = instance->decoder.decode_data; + instance->generic.data_count_bit = instance->decoder.decode_count_bit; + + if(instance->base.callback) + instance->base.callback(&instance->base, instance->base.context); + } + instance->decoder.decode_data = 0; + instance->decoder.decode_count_bit = 0; + instance->decoder.parser_step = AllstarFireflyDecoderStepReset; + } else { + instance->decoder.parser_step = AllstarFireflyDecoderStepReset; + } + } else { + instance->decoder.parser_step = AllstarFireflyDecoderStepReset; + } + break; + } +} + +uint8_t subghz_protocol_decoder_allstar_firefly_get_hash_data(void* context) { + furi_assert(context); + SubGhzProtocolDecoderAllstarFirefly* instance = context; + return subghz_protocol_blocks_get_hash_data( + &instance->decoder, (instance->decoder.decode_count_bit / 8) + 1); +} + +SubGhzProtocolStatus subghz_protocol_decoder_allstar_firefly_serialize( + void* context, + FlipperFormat* flipper_format, + SubGhzRadioPreset* preset) { furi_assert(context); SubGhzProtocolDecoderAllstarFirefly* instance = context; return subghz_block_generic_serialize(&instance->generic, flipper_format, preset); } -static SubGhzProtocolStatus subghz_protocol_decoder_allstar_firefly_deserialize( - void* context, FlipperFormat* flipper_format) { +SubGhzProtocolStatus subghz_protocol_decoder_allstar_firefly_deserialize( + void* context, + FlipperFormat* flipper_format) { furi_assert(context); SubGhzProtocolDecoderAllstarFirefly* instance = context; - SubGhzProtocolStatus status = subghz_block_generic_deserialize_check_count_bit( - &instance->generic, flipper_format, AF_SYM_COUNT); - if(status != SubGhzProtocolStatusOk) return status; - af_uint64_to_dip(instance->generic.data, instance->dip); - if(!af_dip_valid(instance->dip)) return SubGhzProtocolStatusErrorParserOthers; - return SubGhzProtocolStatusOk; + return subghz_block_generic_deserialize_check_count_bit( + &instance->generic, + flipper_format, + subghz_protocol_allstar_firefly_const.min_count_bit_for_found); } -static void subghz_protocol_decoder_allstar_firefly_get_string( - void* context, FuriString* output) { +void subghz_protocol_decoder_allstar_firefly_get_string(void* context, FuriString* output) { furi_assert(context); SubGhzProtocolDecoderAllstarFirefly* instance = context; + + uint64_t code_found_reverse = subghz_protocol_blocks_reverse_key( + instance->generic.data, instance->generic.data_count_bit); + furi_string_cat_printf( output, - "%s\r\n0x%04lX\r\n" - "Freq: 318MHz OOK\r\n" - "1 2 3 4 5 6 7 8 9\r\n" - "%c %c %c %c %c %c %c %c %c", - SUBGHZ_PROTOCOL_ALLSTAR_FIREFLY_NAME, - (unsigned long)(instance->generic.data), - instance->dip[0], instance->dip[1], instance->dip[2], - instance->dip[3], instance->dip[4], instance->dip[5], - instance->dip[6], instance->dip[7], instance->dip[8]); + "%s %db\r\n" + "Key:0x%05lX Yek:0x%05lX\r\n" + " +: " DIP_PATTERN "\r\n" + " o: " DIP_PATTERN "\r\n" + " -: " DIP_PATTERN "\r\n", + instance->generic.protocol_name, + instance->generic.data_count_bit, + (uint32_t)(instance->generic.data & 0xFFFFF), + (uint32_t)(code_found_reverse & 0xFFFFF), + SHOW_DIP_P(instance->generic.data, DIP_P), + SHOW_DIP_P(instance->generic.data, DIP_O), + SHOW_DIP_P(instance->generic.data, DIP_N)); } - -static void* subghz_protocol_encoder_allstar_firefly_alloc(SubGhzEnvironment* environment) { - UNUSED(environment); - SubGhzProtocolEncoderAllstarFirefly* instance = - malloc(sizeof(SubGhzProtocolEncoderAllstarFirefly)); - instance->base.protocol = &subghz_protocol_allstar_firefly; - instance->generic.protocol_name = SUBGHZ_PROTOCOL_ALLSTAR_FIREFLY_NAME; - instance->generic.data = 0; - instance->generic.data_count_bit = AF_SYM_COUNT; - memset(instance->dip, '-', AF_BIT_COUNT); - instance->dip[AF_BIT_COUNT] = '\0'; - instance->tx_size = 0; - instance->tx_pos = 0; - return instance; -} - -static void subghz_protocol_encoder_allstar_firefly_free(void* context) { - furi_assert(context); free(context); -} - -static void subghz_protocol_encoder_allstar_firefly_stop(void* context) { - UNUSED(context); -} - -static SubGhzProtocolStatus subghz_protocol_encoder_allstar_firefly_deserialize( - void* context, FlipperFormat* flipper_format) { - furi_assert(context); - SubGhzProtocolEncoderAllstarFirefly* instance = context; - SubGhzProtocolStatus status = subghz_block_generic_deserialize_check_count_bit( - &instance->generic, flipper_format, AF_SYM_COUNT); - if(status != SubGhzProtocolStatusOk) return status; - af_uint64_to_dip(instance->generic.data, instance->dip); - if(!af_dip_valid(instance->dip)) return SubGhzProtocolStatusErrorParserOthers; - instance->tx_size = af_build_tx_buf(instance->dip, instance->tx_buf); - instance->tx_pos = 0; - return SubGhzProtocolStatusOk; -} - -static LevelDuration subghz_protocol_encoder_allstar_firefly_yield(void* context) { - furi_assert(context); - SubGhzProtocolEncoderAllstarFirefly* instance = context; - if(instance->tx_pos >= instance->tx_size) return level_duration_reset(); - bool lv = (instance->tx_pos % 2 == 0); - uint32_t dur = instance->tx_buf[instance->tx_pos++]; - return level_duration_make(lv, dur); -} - -const SubGhzProtocolDecoder subghz_protocol_decoder_allstar_firefly = { - .alloc = subghz_protocol_decoder_allstar_firefly_alloc, - .free = subghz_protocol_decoder_allstar_firefly_free, - .feed = subghz_protocol_decoder_allstar_firefly_feed, - .reset = subghz_protocol_decoder_allstar_firefly_reset, - .get_hash_data = subghz_protocol_decoder_allstar_firefly_get_hash_data, - .serialize = subghz_protocol_decoder_allstar_firefly_serialize, - .deserialize = subghz_protocol_decoder_allstar_firefly_deserialize, - .get_string = subghz_protocol_decoder_allstar_firefly_get_string, -}; - -const SubGhzProtocolEncoder subghz_protocol_encoder_allstar_firefly = { - .alloc = subghz_protocol_encoder_allstar_firefly_alloc, - .free = subghz_protocol_encoder_allstar_firefly_free, - .deserialize = subghz_protocol_encoder_allstar_firefly_deserialize, - .stop = subghz_protocol_encoder_allstar_firefly_stop, - .yield = subghz_protocol_encoder_allstar_firefly_yield, -}; - -const SubGhzProtocol subghz_protocol_allstar_firefly = { - .name = SUBGHZ_PROTOCOL_ALLSTAR_FIREFLY_NAME, - .type = SubGhzProtocolTypeStatic, - .flag = SubGhzProtocolFlag_AM | - SubGhzProtocolFlag_Decodable | - SubGhzProtocolFlag_Load | - SubGhzProtocolFlag_Save | - SubGhzProtocolFlag_Send, - .decoder = &subghz_protocol_decoder_allstar_firefly, - .encoder = &subghz_protocol_encoder_allstar_firefly, -}; diff --git a/lib/subghz/protocols/allstar_firefly.h b/lib/subghz/protocols/allstar_firefly.h index 6ebf0dda6..96fc718d8 100644 --- a/lib/subghz/protocols/allstar_firefly.h +++ b/lib/subghz/protocols/allstar_firefly.h @@ -31,31 +31,113 @@ * &subghz_protocol_allstar_firefly, (in the protocol array) */ -#include +#include "base.h" /* Protocol name (must match what is written to .sub files) */ -#define SUBGHZ_PROTOCOL_ALLSTAR_FIREFLY_NAME "Allstar Firefly" +#define SUBGHZ_PROTOCOL_ALLSTAR_FIREFLY_NAME "Allstar Firefly" -/* Timing constants (from 37-frame analysis of two captures) */ -#define AF_FREQ 318000000UL -#define AF_BIT_COUNT 9u -#define AF_SYM_COUNT 18u +typedef struct SubGhzProtocolDecoderAllstarFirefly SubGhzProtocolDecoderAllstarFirefly; +typedef struct SubGhzProtocolEncoderAllstarFirefly SubGhzProtocolEncoderAllstarFirefly; -#define AF_LONG_PULSE_US 4045u -#define AF_SHORT_PULSE_US 530u -#define AF_SHORT_GAP_US 607u -#define AF_LONG_GAP_US 4139u -#define AF_INTERFRAME_US 30440u +extern const SubGhzProtocolDecoder subghz_protocol_allstar_firefly_decoder; +extern const SubGhzProtocolEncoder subghz_protocol_allstar_firefly_encoder; +extern const SubGhzProtocol subghz_protocol_allstar_firefly; -#define AF_SHORT_PULSE_MIN 300u -#define AF_SHORT_PULSE_MAX 1200u -#define AF_LONG_PULSE_MIN 2500u -#define AF_LONG_PULSE_MAX 5500u -#define AF_FRAME_THRESH_US 20000u -#define AF_TX_REPEAT 20u -#define AF_TX_BUF_SIZE (AF_TX_REPEAT * AF_SYM_COUNT * 2u + 8u) +/** + * Allocate SubGhzProtocolEncoderAllstarFirefly. + * @param environment Pointer to a SubGhzEnvironment instance + * @return SubGhzProtocolEncoderAllstarFirefly* pointer to a SubGhzProtocolEncoderAllstarFirefly instance + */ +void* subghz_protocol_encoder_allstar_firefly_alloc(SubGhzEnvironment* environment); -/* Protocol vtable entries */ -extern const SubGhzProtocolDecoder subghz_protocol_decoder_allstar_firefly; -extern const SubGhzProtocolEncoder subghz_protocol_encoder_allstar_firefly; -extern const SubGhzProtocol subghz_protocol_allstar_firefly; +/** + * Free SubGhzProtocolEncoderAllstarFirefly. + * @param context Pointer to a SubGhzProtocolEncoderAllstarFirefly instance + */ +void subghz_protocol_encoder_allstar_firefly_free(void* context); + +/** + * Deserialize and generating an upload to send. + * @param context Pointer to a SubGhzProtocolEncoderAllstarFirefly instance + * @param flipper_format Pointer to a FlipperFormat instance + * @return status + */ +SubGhzProtocolStatus subghz_protocol_encoder_allstar_firefly_deserialize( + void* context, + FlipperFormat* flipper_format); + +/** + * Forced transmission stop. + * @param context Pointer to a SubGhzProtocolEncoderAllstarFirefly instance + */ +void subghz_protocol_encoder_allstar_firefly_stop(void* context); + +/** + * Getting the level and duration of the upload to be loaded into DMA. + * @param context Pointer to a SubGhzProtocolEncoderAllstarFirefly instance + * @return LevelDuration + */ +LevelDuration subghz_protocol_encoder_allstar_firefly_yield(void* context); + +/** + * Allocate SubGhzProtocolDecoderAllstarFirefly. + * @param environment Pointer to a SubGhzEnvironment instance + * @return SubGhzProtocolDecoderAllstarFirefly* pointer to a SubGhzProtocolDecoderAllstarFirefly instance + */ +void* subghz_protocol_decoder_allstar_firefly_alloc(SubGhzEnvironment* environment); + +/** + * Free SubGhzProtocolDecoderAllstarFirefly. + * @param context Pointer to a SubGhzProtocolDecoderAllstarFirefly instance + */ +void subghz_protocol_decoder_allstar_firefly_free(void* context); + +/** + * Reset decoder SubGhzProtocolDecoderAllstarFirefly. + * @param context Pointer to a SubGhzProtocolDecoderAllstarFirefly instance + */ +void subghz_protocol_decoder_allstar_firefly_reset(void* context); + +/** + * Parse a raw sequence of levels and durations received from the air. + * @param context Pointer to a SubGhzProtocolDecoderAllstarFirefly instance + * @param level Signal level true-high false-low + * @param duration Duration of this level in, us + */ +void subghz_protocol_decoder_allstar_firefly_feed(void* context, bool level, uint32_t duration); + +/** + * Getting the hash sum of the last randomly received parcel. + * @param context Pointer to a SubGhzProtocolDecoderAllstarFirefly instance + * @return hash Hash sum + */ +uint8_t subghz_protocol_decoder_allstar_firefly_get_hash_data(void* context); + +/** + * Serialize data SubGhzProtocolDecoderAllstarFirefly. + * @param context Pointer to a SubGhzProtocolDecoderAllstarFirefly instance + * @param flipper_format Pointer to a FlipperFormat instance + * @param preset The modulation on which the signal was received, SubGhzRadioPreset + * @return status + */ +SubGhzProtocolStatus subghz_protocol_decoder_allstar_firefly_serialize( + void* context, + FlipperFormat* flipper_format, + SubGhzRadioPreset* preset); + +/** + * Deserialize data SubGhzProtocolDecoderAllstarFirefly. + * @param context Pointer to a SubGhzProtocolDecoderAllstarFirefly instance + * @param flipper_format Pointer to a FlipperFormat instance + * @return status + */ +SubGhzProtocolStatus subghz_protocol_decoder_allstar_firefly_deserialize( + void* context, + FlipperFormat* flipper_format); + +/** + * Getting a textual representation of the received data. + * @param context Pointer to a SubGhzProtocolDecoderAllstarFirefly instance + * @param output Resulting text + */ +void subghz_protocol_decoder_allstar_firefly_get_string(void* context, FuriString* output); From 63d49b6e48533c8a182f3d0af97c59e629f07706 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 21 Apr 2026 03:31:11 +0300 Subject: [PATCH 33/71] subghz upgrades [ci skip] docs update pr template update changelog update --- .github/pull_request_template.md | 20 +- CHANGELOG.md | 7 +- .../main/subghz/helpers/subghz_custom_event.h | 14 ++ .../main/subghz/helpers/subghz_gen_info.c | 153 +++++++++++++- .../main/subghz/helpers/subghz_gen_info.h | 4 +- .../helpers/subghz_txrx_create_protocol_key.c | 6 +- .../helpers/subghz_txrx_create_protocol_key.h | 2 +- .../resources/subghz/assets/keeloq_mfcodes | 190 +++++++++++------- .../subghz/scenes/subghz_scene_set_button.c | 8 +- .../subghz/scenes/subghz_scene_set_counter.c | 12 +- .../subghz/scenes/subghz_scene_set_seed.c | 22 +- .../subghz/scenes/subghz_scene_set_serial.c | 14 +- .../subghz/scenes/subghz_scene_set_type.c | 30 ++- documentation/SubGHzSupportedSystems.md | 56 +++++- lib/nfc/protocols/mf_desfire/mf_desfire_i.c | 4 +- .../mf_ultralight/mf_ultralight_listener.c | 4 + lib/subghz/protocols/came_atomo.c | 56 +++++- lib/subghz/protocols/keeloq.c | 100 ++++++++- lib/subghz/protocols/keeloq_common.c | 94 +++++++++ lib/subghz/protocols/keeloq_common.h | 20 ++ lib/subghz/protocols/public_api.h | 2 +- targets/f7/api_symbols.csv | 2 +- 22 files changed, 673 insertions(+), 147 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 4bc69d8e6..234fbe7c5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,8 +6,22 @@ - [ Describe how to verify changes ] -# Checklist (For Reviewer) +# Author Checklist (Fill this out): -- [ ] PR has description of feature/bug -- [ ] Description contains actions to verify feature/bugfix +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation (if it exists) + +# AI usage disclosure (Fill this out): + +- [ ] Partially AI assisted (clarify below which code was AI assisted and briefly explain what it does). +- [ ] Fully AI generated (explain what all the generated code does in moderate detail). + +- [ Describe how AI was used in this PR if it was used ] + +# Checklist (For Reviewer) (Don't fill this out!): + +- [ ] PR has proper description of new feature/bugfix +- [ ] Description contains actions to verify feature/bugfix on the hardware +- [ ] No obvious issues with the code was found - [ ] I've built this code, uploaded it to the device and verified feature/bugfix diff --git a/CHANGELOG.md b/CHANGELOG.md index d432473eb..428c8e8f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## Main changes -- Current API: 87.6 +- Current API: 87.7 +* SubGHz: Add support for **42+ Keeloq based systems** (with partial Add Manually support) (see [Full list](/documentation/SubGHzSupportedSystems.md)) (by @zero-mega, @xMasterX, ARF Team) * SubGHz: Add **Nord ICE** protocol (33 bits, Static) +* SubGHz: **Better support for CAME Atomo** type remotes (TOPD4REN) (decode + button codes) (thx to Roman for raw recordings) * SubGHz: Add **CAME TOP44FGN** support in CAME TWEE protocol * SubGHz: Add all 0x0s and all 0xFs KeeLoq MF codes for normal and simple learning * SubGHz: **Fix CAME TWEE repeats count for button click** @@ -9,7 +11,8 @@ * NFC: Add **Mifare Ultralight C Write Support** (by @haw8411) * NFC: Add **new parsers SZPPK, SKPPK and SevPPK**, upgrade Plantain parser, fix TwoCities parser (PR #981 | by @mxcdoam) * OFW PR 4362: NFC: **Fix BusFault** in Write to Initial Card (by @akrylysov) -* Apps: Build tag (**1apr2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) +* OFW PR 4369: NFC: Fix stack buffer overflows in MFUL FAST_READ and DESFire file settings parsers (by @qp-x-qp) +* Apps: Build tag (**21apr2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * OFW PR 4364: JS: Address utf8 support when uploading JavaScript application (by @bekindpleaserewind) * Display: Extend lcd contrast range to full ST756x 6-bit range (by @ShaTie) (reduced to -10 to +18 to avoid fully unreadable state) diff --git a/applications/main/subghz/helpers/subghz_custom_event.h b/applications/main/subghz/helpers/subghz_custom_event.h index ebb120929..63b590bfc 100644 --- a/applications/main/subghz/helpers/subghz_custom_event.h +++ b/applications/main/subghz/helpers/subghz_custom_event.h @@ -69,6 +69,7 @@ typedef enum { SetTypeFaacSLH_868, SetTypeFaacSLH_433, SetTypeBFTMitto, + SetTypeErreka433, SetTypeSomfyTelis, SetTypeSomfyKeytis, SetTypeKingGatesStylo4k, @@ -109,6 +110,19 @@ typedef enum { SetTypeNovoferm_433_92, SetTypeHormannEcoStar_433_92, SetTypeCardinS449_433FM, + SetTypePujol433, + SetTypePujol_Vario433, + SetTypeET_Blue433, + SetTypeET_Blue_Mix433, + SetTypeATA_PTX4_433, + SetTypeSeav433, + SetTypeWisniowski433, + SetTypeFadini433, + SetTypeMc_Garcia433, + SetTypeClemsa_Mutancode433, + SetTypeDoormatic433, + SetTypeElvox433, + SetTypeVerex433, SetTypeFAACRCXT_433_92, SetTypeFAACRCXT_868, SetTypeGeniusBravo433, diff --git a/applications/main/subghz/helpers/subghz_gen_info.c b/applications/main/subghz/helpers/subghz_gen_info.c index db3f2926a..7feaea887 100644 --- a/applications/main/subghz/helpers/subghz_gen_info.c +++ b/applications/main/subghz/helpers/subghz_gen_info.c @@ -496,14 +496,25 @@ void subghz_scene_set_type_fill_generation_infos(GenInfo* infos_dest, SetType ty break; case SetTypeBFTMitto: gen_info = (GenInfo){ - .type = GenKeeloqBFT, + .type = GenKeeloqSeed, .mod = "AM650", .freq = 433920000, - .keeloq_bft.serial = key & 0x000FFFFF, - .keeloq_bft.btn = 0x02, - .keeloq_bft.cnt = 0x02, - .keeloq_bft.seed = key & 0x000FFFFF, - .keeloq_bft.manuf = "BFT"}; + .keeloq_seed.serial = key & 0x000FFFFF, + .keeloq_seed.btn = 0x02, + .keeloq_seed.cnt = 0x02, + .keeloq_seed.seed = key & 0x000FFFFF, + .keeloq_seed.manuf = "BFT"}; + break; + case SetTypeErreka433: + gen_info = (GenInfo){ + .type = GenKeeloqSeed, + .mod = "AM650", + .freq = 433920000, + .keeloq_seed.serial = key & 0x000FFFFF, + .keeloq_seed.btn = 0x02, + .keeloq_seed.cnt = 0x02, + .keeloq_seed.seed = key & 0x000FFFFF, + .keeloq_seed.manuf = "Erreka"}; break; case SetTypeAlutechAT4N: gen_info = (GenInfo){ @@ -698,6 +709,136 @@ void subghz_scene_set_type_fill_generation_infos(GenInfo* infos_dest, SetType ty .keeloq.cnt = 0x03, .keeloq.manuf = "Cardin_S449"}; break; + case SetTypePujol433: + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Pujol"}; + break; + case SetTypeET_Blue433: + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x04, + .keeloq.cnt = 0x03, + .keeloq.manuf = "ET_Blue"}; + break; + case SetTypeET_Blue_Mix433: + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x04, + .keeloq.cnt = 0x03, + .keeloq.manuf = "ET_Blue_Mix"}; + break; + case SetTypeATA_PTX4_433: + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "ATA_PTX4"}; + break; + case SetTypePujol_Vario433: + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Pujol_Vario"}; + break; + case SetTypeSeav433: + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Seav"}; + break; + case SetTypeWisniowski433: + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Wisniowski"}; + break; + case SetTypeFadini433: + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Fadini"}; + break; + case SetTypeMc_Garcia433: + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Mc_Garcia"}; + break; + case SetTypeClemsa_Mutancode433: + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Clemsa_Mutancode"}; + break; + case SetTypeDoormatic433: + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Doormatic"}; + break; + case SetTypeElvox433: + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Elvox"}; + break; + case SetTypeVerex433: + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Verex"}; + break; case SetTypeFAACRCXT_433_92: gen_info = (GenInfo){ .type = GenKeeloq, diff --git a/applications/main/subghz/helpers/subghz_gen_info.h b/applications/main/subghz/helpers/subghz_gen_info.h index 60212801c..8fcfb92fc 100644 --- a/applications/main/subghz/helpers/subghz_gen_info.h +++ b/applications/main/subghz/helpers/subghz_gen_info.h @@ -7,7 +7,7 @@ typedef enum { GenFaacSLH, GenKeeloq, GenCameAtomo, - GenKeeloqBFT, + GenKeeloqSeed, GenAlutechAt4n, GenSomfyTelis, GenSomfyKeytis, @@ -55,7 +55,7 @@ typedef struct { uint16_t cnt; uint32_t seed; const char* manuf; - } keeloq_bft; + } keeloq_seed; struct { uint32_t serial; uint8_t btn; diff --git a/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.c b/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.c index 11bb70ed9..4601cbc28 100644 --- a/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.c +++ b/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.c @@ -125,7 +125,7 @@ bool subghz_txrx_gen_keeloq_protocol( //TODO lead to a general appearance return res; } -bool subghz_txrx_gen_keeloq_bft_protocol( +bool subghz_txrx_gen_keeloq_seed_protocol( void* context, const char* preset_name, uint32_t frequency, @@ -142,7 +142,7 @@ bool subghz_txrx_gen_keeloq_bft_protocol( subghz_transmitter_alloc_init(txrx->environment, SUBGHZ_PROTOCOL_KEELOQ_NAME); subghz_txrx_set_preset(txrx, preset_name, frequency, NULL, 0); - if(txrx->transmitter && subghz_protocol_keeloq_bft_create_data( + if(txrx->transmitter && subghz_protocol_keeloq_seed_create_data( subghz_transmitter_get_protocol_instance(txrx->transmitter), txrx->fff_data, serial, @@ -162,7 +162,7 @@ bool subghz_txrx_gen_keeloq_bft_protocol( flipper_format_write_hex(txrx->fff_data, "Seed", seed_data, sizeof(uint32_t)); - flipper_format_write_string_cstr(txrx->fff_data, "Manufacture", "BFT"); + flipper_format_write_string_cstr(txrx->fff_data, "Manufacture", manufacture_name); } subghz_transmitter_free(txrx->transmitter); diff --git a/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.h b/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.h index 811724584..d9d8bbb0a 100644 --- a/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.h +++ b/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.h @@ -63,7 +63,7 @@ bool subghz_txrx_gen_keeloq_protocol( uint16_t cnt, const char* manufacture_name); -bool subghz_txrx_gen_keeloq_bft_protocol( +bool subghz_txrx_gen_keeloq_seed_protocol( void* context, const char* preset_name, uint32_t frequency, diff --git a/applications/main/subghz/resources/subghz/assets/keeloq_mfcodes b/applications/main/subghz/resources/subghz/assets/keeloq_mfcodes index 572bda5cc..2772b0e39 100644 --- a/applications/main/subghz/resources/subghz/assets/keeloq_mfcodes +++ b/applications/main/subghz/resources/subghz/assets/keeloq_mfcodes @@ -1,76 +1,120 @@ Filetype: Flipper SubGhz Keystore File Version: 0 Encryption: 1 -IV: 4E 6F 74 69 63 65 73 20 62 75 6C 67 65 55 77 55 -DD5D658073C2C257430FD30815480D300A4FAA72750B741431D90E7B74C6F94B -D30DBA9B63C4A5A47E6F1462819706F347B4524FB0342729B454791ECB491338 -4BF3E836BECF53BF8ECADC9E35EB94A6F2A424C4918226B7B2F3B0102BB28AFB61610BF188269855559DDEF49413635B -96BD4E3E3D02094DFE3C4E5CCBC8ED7BE679ACE3C83ADF1433BA4E08576E610D -99B4D5475099E999558BBBCFC5083B08BB99FF7F9E8EFEB222388750CBA464CD -DA94EF9FED763E6625C2FA839C8A288986664EFD12575D21575B6C77345EC6A2 -FD0A76366C8F84C2BF080A40EB6573CF41AFA51138320C187BDDB1E40CEA35E8 -5C375767AB4BE93E3A10875B4B006D4264FD9FB606381A8878AF4EC960EFC849 -01297BF2D4372C5FC376E79835D8D5A382F76E3D9367E2FA81F54C10BDB2FDA8 -415FE673D1CDAE0E7141EA78326573987E51353C91992E334FCD030E3172B8C6 -C6EA321FA06DBD86D328B5CBBCFE94628DA3D415463835229F1224EAB5F8B181 -6468B83FB26298BD496299D64DAE0F2D12B081CDE9B83AA5B19500162D7DA6E8 -2569E458EFFE0725581F7490CBF416B3E8E64C1D0F2C13DEA5BE926475F631DC -857A04B029277DA403276CADE88EAD4956A959AB9C652614F435DCE1B3B92215 -A08B60A323178B3A8644D0018C705E5E13F5F34DB41132348CEDBBCB6C75164F -A171F6CEEB240D1117DF8A5396B4E468EB03D8AA315321FA19F01E67145A861E -4255FAC9CD843A206D89D6D5204409193E594AE73D5E490F84F39DC76A0C6C23 -589E6270609485D8FEA3029A23C3084F48FB5384939ACF447CA289C4282F38BA -D1B349CF3C402BCF7F87FEF5A19E8C2748D52715092EC5668C3C61A15958F7F2 -34191142584299F22C0A007EC8E333646E57C6B406C103795ED4A2731CAF55A5 -158809D80C052D780C8AA05F88AF67FF4CF93234A7F7A78B5AA11F067623AD2D -81C22401A96674BB64D3512AEEB5F8A060537AFE1D4F2BB31ADB237E1BC61FC0 -C732F26FEC449F545CD379BF9AD2AC2769EDA98C0D7163A69E37EE17853E98AF -59375DCD3703B87223DE49174274CD28FF95BA077357A10304FC3B44E679CA91 -E9E9E87E1098CCB6C3799EB31D91E9C77EF243164FE98DAA688B22CD8ED7D944 -AC0319E2F9D0520357E61B88B17D293AE32B68661484890AEBCBADA0E7C47C40 -865F354CA28D9AABFFB3ABBD15CB22ECBDC937D292AA2D72B9D7457E870E9117 -7D2D90D3D583F77F60C8782E61011D52A452FC33F8B5E6A8AD6A2378F2819D3B -FC3C20DA524C0E1D43881F15605E18B58D9E804D15F01AB3DC9850D472C4C3A1 -25C03EAA292F03155ACD4C599030C159C2F085970B2BD905A6CF42E69D5E7BF4 -F84099879C1BD93BD77489CBEE9DB22F7F4688480A5DB9F1CF8359ADAA456B10 -21A4BE7E01B1848FB26E7CFEAD9FA841F4B80C4C2C84D07F6737F61E1FB4F4F3 -8094BD23328B5854C11FE696D21F1F58C280DC6D31F7F6B56F99E04D83949EFD -B87664A507982A6103904E81BF7D6AE1222E038D405EE84F3B5E84F4E3D6BFC7 -F102B622433FCBA3910DA43309D67FC46BE7A472BAAF8CDDEFFCD796D23DB122 -D3A015CE7E7D9CD23CED808CC72CBB42A09C5D718B58C04E1E25B3D7F46BCBB3 -02E1C8124D6B955689193369C87BDFCC66C0750E65EED7D7592E6EECA83BF1CA -4A37AC36B48DF89D664CC0390F2B1AAF553EBC2C661D14E5DDA2016C66946FF6 -3087AB22BF5A72376A888C26846B15DF842E70F8AE113E64BC855B2BA4801831 -562ED92C172D0B20799D0FDC565AED5B2A771249ACBA7D8F33176B99B3C4CD88 -448D6DEAC1C01D1B96EBB480D22A5AD011DC36655D8318BACCEE630F9BA5CC20 -2F9C204D0A2E7A5686F7684E438DC3DCEE400AE23306D6B604A5905DB33DBF0F -50A7E7773F4BCA75247E2E9ACF9025F5DF0370A2C20E2ECE8EDC00F25761D158 -2F2B0A5F9A71FD2B1E2AA55B3C6EA75789FC5A20CE3E8790E0260E59D10E7D96 -EBBD9E861DCC0A85A2828026C98D69F882F05CE2DE0CD58AB06FE2E778CBEABC40E591AE59EEA3A4012F9266E61D4971 -8716AE40A23BB9D23DC9EE0917D7E2D50891A4D8495F08174953A883B4EA61DA -F2B9B78FA7239BB61E4FB5DDA4286C5FA2CFF34A4456781C55AC4E23644E2A46 -D570A2BD25921E6E3409E0017BC9547F1BFF0992EDC35F0BC0C526B03738D8F0 -BDA88E42BAA3025E730AF8244F2F0DC5E8F9B531882A5E1EF48BF6F0B86DB186 -03E2FFCDF9243D7DB19DAEAF2563D2CF604D82E73E501071523EA1DD3E3A1313 -702A8F9F8F5D670D999D2D4F6388F22846B23FFC4EB53A71DAC92BED76A7D1111C0536FA5293B148B501E8BE70F84C3F -A605F5B456CD733EAD4E12432617936494C1ACA42B2CEE639151AD8846D02EFBF17575565A458B7E8D6E2929BEE70ADB -F5628A7A8602546DD4BAB166DF796B7011A3F66F896599FFC7467045B1B3EB4F -BE6B94C4A7C701FBF7D189FA3C371DC7D208CF84E1EEC00A4C2F4DC14BF8CC17 -AFF0D5E080A7DAD2AFE0C498D3DFD58EAA0AE4F4F96883B46C36537B66D344C2 -E0435324027761281E17E62CCDB964E7B87EA673205DC76109570DEEA25E9C86 -84EB0F58B6F3ED2B6D62BA21D0EBCA0A1C7E17C792EBB47A2EA596AB6E4E98A78908E5E31CCE35CD8091F8D39DA52EF5 -C4027456DE3BF67B080AC563B2F38318B04DB5ED968D33D7493F0FC5CC51B165 -7C59386D2F5AE8F14AA3C1FBD953F5B7B153A3B94B20BA51C18B18BF2F0DB037 -A8E7A91824FA42085CEEE47A80A9269779AE69AFADFA210E8A4C48585F827FCB99509686D7C62215E556DBE73BB476B5 -514F6030D39CC0DE60B0280EC55F32CE93CA5D9446E7DDF03949AAB7C1276CA036A65A5B58C0904158117768D0AA22A6 -F75FC0AA714F37B75E2FDAFB837B671CD7A349A8D77565C6C0F2943A4B0B88A8 -E8C43768DA5C853253C9FD9D2112A66C509DAA37601CBD83DBFA59329E015D40 -2006DA52D75063F76A99A726E0E7B21A737A7D759C470867FF7EECC61B6A5637 -5B474E6EC7E10EB5415B5593E2C03ED5C079B2CFE2B7BCBC8B3D758727043CD4 -1F3328F14AE9C0867143929D0020AF17F58D09C1B47DBD92FFEAFD2260779DFD -CB194E675F700497AA04686CB17CF77F4295D095A0500D41FD0DC2C4FD8BE294 -328E577B4A997108194BEE91E68190663C4AA3E142B8BEF2A99B70CB4D11897DAA990D5F1FC3D33098982D73F63A26DA -4D5DC5AEF974F0E15A0B06726AC0D2589B1C931B848BFD15BFFE01B8F3BF8E50 -A9B1734BCB160609DF5B02E3C9E459D2DE3297920CBAA7677692C27886527D82 -A161A69B3BE5975F8742AE5461A4983201D7B6B91978D87C0E0D71BF74F5A6D7 -FE3F0F9875D8D2C6E51923E610A812E6BAC4E65FCD4026AA965600A4235AF3EE03D27B793C4E629948BADA5C44EE8915 +IV: 2E 2E 2E 4D 65 6F 77 20 4D 65 6F 77 20 4F 77 4F +411F604B4F4487BEFB5285C7023822BE66D08DD0AAB0B659D58A35E68850BD85 +12BB80D35A9750D218BDE6D4BE4E5FD3B5CE20EDA464E1FA80A000D49FF567B4 +81B613E6CFD99287BCBF0957733704E61B4FFE9CBEED2DC721FBABFEB1A249CE91EEEAE285FC10E0A83A6E1BBEA7EB56 +421C18DF4D8280560EFADDBF0BB02F90942CC5B5B2986327D96F5446CC531BD0 +8BB08D9758943404ACE935B58640E78478C91140E5871D7D070B104DE470C3F2 +1E45CCF03C56DDA4403214A430330647D58D443F665794F5F04D9701C6F32AA2 +4429DDE801D7EB69ADD042A70F83AD6160E8504D6C28A9A4A3D7531140549343 +64BC31C7E98E5223E7D74B844CF4BA26594119D3E468188123D6056C52AF11E7 +F7133D37B1A05BCFB3267D7659B85598E3B2B23502C090C042F0AAEB84E4326D +97FA5E05D70495342BACADA35918C339518DC3EC4AC94F94BC73A08BC539A80F +45F22E4ED7559C875725F2DFCF2DFC0631DF1565A12F455F13C4A101A54E607B +95C78F6417110955813DA8E4AFDCF118A1E19006B4D627FCD1B1688F16CE1933 +7B19D2DA8DEA82083E9F52CED668A0CD6B997AED26F40F6BED30EE7D0E3BD117 +A9B4AECEB0C40BFE38F06302769702C89CF530C16308D0DB7A74AF9B70D3034B +D97B2D17062FD5264C88A2369ACA8BBA064B9B99D064C8831B12B1A23E51BEC1 +A26401DADE20E5D99F53703D17B6E247D169FE0073512A23F6EDB8B3FD9AA73E +FFA8F754EC999FA789C333D7C2EEE62CE7ABEFEF9EDA191DDF9C0BEE59E30927 +82FB6C3FB0AD818834B4335855599741728466E3418EBB0C03382C01F2CFE517 +2D284BC94C8C3EAE99FB8060524540C1FA07866A648CD49B82B874DF2073691E +5EFE887394C60DADCDBE4E25E323CAFDC8E0E5A435FE4941CE29274A319E2F4B +8B3CD3C3310933B59C328892998E434CF34F5230B219B9F00EE888154F71D234 +FF176D5DD6D6C296498E40E30FFF2F8498760ED5609B2E3080661A29B1F3C7E7 +CFFD9A9B1210EE4B49849E00B8513400B5399BEDCC98D8C1CEFB5F062BA0F2D7 +99DACFB462669C3F2C87C09F2B3FB0AC8F9F26DEDF6D04CF0C306F7F91615FCF +47A99EB39C8655BD8CB93D349478514E0ECA46C6463011B6B1F0A9B807DEF300 +CC2EDDB4A96311B0A3D4A55F485292CF7585E1CAE9D3D46ADBD40A3391396DFF +82DE652C1008367C54C8C431DADC5026CCF1B300BB112D2F4B3366B319F61284 +71BBA17FDC6261801D17434538524D3B6EE42355B8ABF99C07DBD69F3C1031CE +974DC30EDA9E1CA01C0532E82593D7D84E7FE282659EDAA8FC3474DA422D57B2 +6975A600597A196A21D8432881D95D977C168027D9FFAB273DE00A5AEE3BC5E1 +0B7EC247D17B8C80E151E9DD60B94D2D6CD4E7A03E6404FA044D6BC2A15CF214 +73D12A173C2B97A77231901E94B1C81EA1AF79BD1EA01F8F8E2A3B38177A84EC +9D309CE4B225FC251019B1354E8AEB47EE1D6FA792FF0E14FAFB0C0944D79350 +816B8EB8CB5ADEAC80C9359F9BF538A6641A20AB8BE422AD65E50802DFF38CED +1F41BBFFA9338DB4143D4D3CBA84637FB397EEA36D5D89D4A5D08B3FB8181540 +295359A1E73F5F94186DBFC13175A6DA08771483043408C145B0FB4A10716C86 +990E0B2ABE53757378385879D4D5BBCEF45A69F659963DB6813931973B69ABD6 +6509E69551E5D3E38349DC16CBABBD059DB80852ECA56F7902DEE11F280B14B6 +C8D22F8E7592087432BC4FF32D1810697406948F2C69C54BC5CA8F59BE65278F +E53840FE4593AB7708C14E3E1A33D689160C018CF2CEC3A6FB309929BDA2A66F +3CE77B617C5E133046704DAE55B5328C861CE02F223CE75E6CDFF296927D0012 +0CD25E6B29860621BED286C97675EC6515D9A93B7BCC5E8571460DF90A73F748 +C387DAC55E4B8F15BCB228CF58F857419C91BAF619D38D2700E7149BE56C9D37 +E1AEB1E36C5C0D30B858101BE459E29CE2A49470022495A8D4869D44826F7928 +173D2BB833DCB4291D25E43C9E85AA7900198BF92D6E97EE861BBF6CBE1D2147ADABA5DAEA976F6D2A2716B7E3E75DEC +A388246FAB037912C9B1D2D25C144795B16E917BD2653693346E1A076718FC03 +4EA223311825F64F88AB509D69B708A2143A76A180ED1AB46C8F1AA1C4A9E5AF +0CBBC26D7C458E415DCEECC9F73008B556BED1B77A3AA6BE8C557B46058180D9 +9DE0FFD924900E5B3FD4CCE2B69D7B20FF52EE1E747FFACC6A631056D7D2B4CE +2C05D81B20DA46ED217BE759EFD95919D434FAC98B08405FB9604B61982E4141 +4EEBB881DBE7A97C9C043221B2038F8B919C144D33ACDCE0F9AB5A2D63363E07 +F19CDBD22DCC70D2A11AD7D6210FEA7601BA2093F1E9766C4F8452F1AD14046B +AA165E848FB25FAF3AC74D1AD809BA2A45FE2E79090256BF9E77BDDCD7A4A4E2 +C63B40C559AE339DB491E1E5B39C0EB321EC3A3BD2441763A5B2DF7C2DF534F4 +DA1CEA4B84C64D744AC3BE1D29B1E752CCFB3CFAA56AEB85C1BED37293BEAF93 +C67E0C619482F59D63C6B097347A4C777EBC60D800DD586C10A0BCEC67628618 +0EDE32F4FB6ED09526381B638A7CA5869C7B20FE6E003311065398E3ED3E3474 +43714DC1C28BC0FD3DDD348E93169523D6BC4C8AC1F62545C44D405ED1CA4927 +B49A42DCCC1A76C1643AD96CAE981BE430BEC9CA372A115C02E87BECE4FFFAA6 +E66E1F05704B9BE33AC1A52D33B3CE1D373106B61C10252E56618F64FB2E665B +A78DD00AF303F280CACF7DC5C91778096D77989AAE29EDDC3D772EA32A6A5CB1038196BB1C2394C3E09A706EB421447F +7EC357770322CFB212E77C7A1557C46AF1048A87CB14AE0339FF18E031927DD9 +698F99BEC7BBDA2F06BA8F393235783C05F1E324CB89A2F9239A93C63A96D657 +5955B7E969ECD7E5DDAB6AC8C103CD1C03504A8DA71346452302887FD72ED9D4 +A6856ADA5AD21EAC6255C1A3166CBAFC8763DD6BB4F2AB42BCCB1B71D02B722C +DB86290A20BD5A1CAFED7D06511096B32BAA7CAEDC49A53488542F3944B72531 +83B8E69B9A50EB7D3F51D4221C4002110AB129A5E32A5CF07763BF5A198EBC68978CCF097D4396AFD920DD5A23C4DE14 +95C4CA690A9214E66520F67150A42E5ADE143E9C8D0C5B9CFABDFC93D193FC3D +A3B3E5E7B57F758FDB2062613DC7CF3990DAEF6F2F1EABCD169EFA85C1237F8D +2FD52B5A55EA063680E6CD164F6CD50148E6CBF0DAD1250494CF0E536D39687D46876E1E2C0A9902F785C60273BB5026 +38F372E8F07F16FB50942FFAC6FC4D49601CF09A65C714AC0998EB1FE0DD8F5A +94B863DA82DD1BD27D083690AB4B59D9B6236FCB1172B9480F2E11BC420FC46C3D554B78B2476C964379E028DC89BA29 +4D52AAF64319E435FDD046276964E7A819A3CCF9EEC576A0993FE6F8E29095BB +F44CE71DE07CFAD16BEE0041832A532EA266557C640B4FAE6245252B47C6B8C05AFD9C6F305596ECF68BFE8829B9032D +8520769CE953F5F02C132DE95141E8F78FF2C846B9F672970771BBFCB51386BF +FAA8C71F036FD5A1BDCFCEC05E890BCCA8EB92E7AABF4C7D64D1EA3D25730DD0 +CDB89D5B8B73DB2013EDBB34D8444B37EBAA308345A9626E153FF0A9574FC4D6A2B9588BC20EC0D9B1498228F116D3F6 +13351D444888E92AD03EFFE3466EFD5719DA41FB5BA0805C09CF3E79B188223B +B36B0913A714AB0DD7C0005426127B3A2C02DD3112E745064D7A5899DE26A128 +C3AB7BBA577D74FD581FDA6BA17C1D2E1EBD4A4AA5CB42852E59CEE91D81B3E263549755171B22A5FBF4A1156C8C0A7B +18F579227253BAB1E3CA2E71331C6090286480D19BB1FEF242793D27BCA157D3 +BF613CE44B9BDA278D884D2F756FBE164A9736D28A52B0101611D61B643EB978 +9DE9087BD023D6F6ABDF4A345E85A1110D6B205CCC31DC7372126CCC5EC618EF +764602226E608E5371E693185D57B7C4D6FE2CBDA379D986A251CC7286B283FEE35E07E5B32B93649D52D9CE4FFC90A7 +3AA3162A345891481F09C9231238AD644B6804643C5EED165B3B3F078E0FC216435AE2385B3AF4EB44C029D9A60FC5D9 +867DD29F637DDFBE24CA6AFDA94366D865D701CBCAA9E7074B6FB29592AE31933F230C6471C5A513D19560F5E0820BCB +EDB4BC2F025C11311527B227E42726C3BB4A6CFF49898E2FFEB2097F0924E8EE +D54BFE2E7C21D84AD967433C7329BBBD257A026BFC7866071C80396B45E764D6 +ED13A30EC29B22AB34D5EFE27C74C112ACD42CFFFD0695CE743FE2D0ECCCB35EF58222519E54289E7AD0624A11698986 +84AF1F78D666058274DE1896D706E6196B55B782C395216915C1023E1E44F4A2 +648F762C38B32B930029E5730609A46CF0BCB4117BEF41D36ED47BE8EB8DB2AC +745FAFAFC79AD2DFBE3B03387EE5C3FC05EBF1A61A371E439B7FA977A1ACA3B0 +8C4FE20E0F71A9822FD32A866571D9ED6F8A1F393430AF5DB5FDBF4F7030165A +CC54F712C0C63A6B207C9ACAD7943BDCCC0F8126449E82CA5681F88A0491625B +9F1654268308E44DD6A9128AB1BF9B01D521D5BA9A98E5A77D35564C3678D9EAF2C8A2EC09FC3E5CD1A831FDB95486D0 +58E094157F881A156E0D4EBB79ABFED8D855A9BE1589471A8BDA6AA9F9C58F4B8BF6B326653B6CE28C45DDC3DE4CC4BE +B3BE094F6E487FD211792E424BB5EB95FC11AC78832FE9EAFDB076867A9B4AA2 +9FC6BE151041C150003E5BA940ADABDD02A080A9F4EE61215A6162BB1C13B7D9 +7CFE8E8FE7E2218425F581F70AC6E043E9253774A80450113C836EBAF4EAF4A0 +8610A9694E586306A4078E5DD3C757870C21130D6F67593994568FA1E7B7F4CA +774DDBA8E8AA78BFF82B1E9C8CC58873C73AD0B977ED4EDD465513F9D238AA7DB2BE64536F4950BDAA00F971551F0922 +DB103B1AB06E9115193F5DC6F0C2D806AE9518C91D7D72945B27BE86B2D16351 +86C4A3F2ACBFA942CF4FF3DC062A4786D9D005C05671E232D93DEBCF5C1D7B96 +909EB49451C1D7A1B6E01FFC55EED8DA3C5DB4AFEA5C347DD4E79642746C963E64113CEDECF951947C095B12390BA10F +AD7CFDF6D5C93806F8E5321EC2B4E2C6C7B750874E046154EC76EC2678B6F0C938D2D99A6646663ABC730190C7108C1F +F3DC34F33197A7D2DCBA4D25ACC6EAA0612723C92BFF226E7B3A73BD07B2EBEA +B7074A0B79FE44FA94C3EC55C2C694ADD593C021DED43085CB5D69DBA7F6F6F1 +9C7B26FC47E16E41E8F1F560EBEE3B580E5F44C1A2DA0BA415C279530416F231 +EBED9733677546781E77DC9E6668AE3CA6D1B6C0E1446EE10EBEC8F8ABF5228C +D21EB73FE8C693DD4C580A51DB46D5DF26D798A8A67095FF087B8F14907993C2 +CB9D919298CB3EF9458A962BDD79736F6FC00E209B389C0543D06687AB16ADE0 +EED58F3D2003F1F288718544B95F8F7728AB6D0A7D10E3BEFED3C7F4A55A6B7CCBF8EACE17F0FADCDA6B19DBA74A2324 +D4E83675B9B5BB81D1E5C17CB88225C514C6DD2852D22A8C4681F0683248B21E +0949495177B2C21F42679921EA4C83CD7A9700FB34B13DDF3E61CE26162DE0AF +2C38210508654DE730157BA138118793DDA2BCD7958EDD21B02796B689D0990C +47C78252386879DB6F9F3516FBCA29BAD3EB5F837B881422D7D421A41CCB851EFEA209D371E6E81EF957441D31D71842 diff --git a/applications/main/subghz/scenes/subghz_scene_set_button.c b/applications/main/subghz/scenes/subghz_scene_set_button.c index 2006fdfa6..efec4e4a3 100644 --- a/applications/main/subghz/scenes/subghz_scene_set_button.c +++ b/applications/main/subghz/scenes/subghz_scene_set_button.c @@ -24,9 +24,9 @@ void subghz_scene_set_button_on_enter(void* context) { byte_ptr = &subghz->gen_info->keeloq.btn; byte_count = sizeof(subghz->gen_info->keeloq.btn); break; - case GenKeeloqBFT: - byte_ptr = &subghz->gen_info->keeloq_bft.btn; - byte_count = sizeof(subghz->gen_info->keeloq_bft.btn); + case GenKeeloqSeed: + byte_ptr = &subghz->gen_info->keeloq_seed.btn; + byte_count = sizeof(subghz->gen_info->keeloq_seed.btn); break; case GenAlutechAt4n: byte_ptr = &subghz->gen_info->alutech_at_4n.btn; @@ -99,7 +99,7 @@ bool subghz_scene_set_button_on_event(void* context, SceneManagerEvent event) { switch(subghz->gen_info->type) { case GenFaacSLH: case GenKeeloq: - case GenKeeloqBFT: + case GenKeeloqSeed: case GenAlutechAt4n: case GenSomfyTelis: case GenKingGatesStylo4k: diff --git a/applications/main/subghz/scenes/subghz_scene_set_counter.c b/applications/main/subghz/scenes/subghz_scene_set_counter.c index be13499b8..7a5da91c3 100644 --- a/applications/main/subghz/scenes/subghz_scene_set_counter.c +++ b/applications/main/subghz/scenes/subghz_scene_set_counter.c @@ -30,9 +30,9 @@ void subghz_scene_set_counter_on_enter(void* context) { byte_ptr = (uint8_t*)&subghz->gen_info->came_atomo.cnt; byte_count = sizeof(subghz->gen_info->came_atomo.cnt); break; - case GenKeeloqBFT: - byte_ptr = (uint8_t*)&subghz->gen_info->keeloq_bft.cnt; - byte_count = sizeof(subghz->gen_info->keeloq_bft.cnt); + case GenKeeloqSeed: + byte_ptr = (uint8_t*)&subghz->gen_info->keeloq_seed.cnt; + byte_count = sizeof(subghz->gen_info->keeloq_seed.cnt); break; case GenAlutechAt4n: byte_ptr = (uint8_t*)&subghz->gen_info->alutech_at_4n.cnt; @@ -123,8 +123,8 @@ bool subghz_scene_set_counter_on_event(void* context, SceneManagerEvent event) { case GenCameAtomo: subghz->gen_info->came_atomo.cnt = __bswap16(subghz->gen_info->came_atomo.cnt); break; - case GenKeeloqBFT: - subghz->gen_info->keeloq_bft.cnt = __bswap16(subghz->gen_info->keeloq_bft.cnt); + case GenKeeloqSeed: + subghz->gen_info->keeloq_seed.cnt = __bswap16(subghz->gen_info->keeloq_seed.cnt); break; case GenAlutechAt4n: subghz->gen_info->alutech_at_4n.cnt = @@ -168,7 +168,7 @@ bool subghz_scene_set_counter_on_event(void* context, SceneManagerEvent event) { switch(subghz->gen_info->type) { case GenFaacSLH: - case GenKeeloqBFT: + case GenKeeloqSeed: scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSetSeed); return true; case GenKeeloq: diff --git a/applications/main/subghz/scenes/subghz_scene_set_seed.c b/applications/main/subghz/scenes/subghz_scene_set_seed.c index b3bd447e6..482ad07e4 100644 --- a/applications/main/subghz/scenes/subghz_scene_set_seed.c +++ b/applications/main/subghz/scenes/subghz_scene_set_seed.c @@ -22,9 +22,9 @@ void subghz_scene_set_seed_on_enter(void* context) { byte_ptr = (uint8_t*)&subghz->gen_info->faac_slh.seed; byte_count = sizeof(subghz->gen_info->faac_slh.seed); break; - case GenKeeloqBFT: - byte_ptr = (uint8_t*)&subghz->gen_info->keeloq_bft.seed; - byte_count = sizeof(subghz->gen_info->keeloq_bft.seed); + case GenKeeloqSeed: + byte_ptr = (uint8_t*)&subghz->gen_info->keeloq_seed.seed; + byte_count = sizeof(subghz->gen_info->keeloq_seed.seed); break; // Not needed for these types case GenKeeloq: @@ -78,17 +78,17 @@ bool subghz_scene_set_seed_on_event(void* context, SceneManagerEvent event) { subghz->gen_info->faac_slh.seed, subghz->gen_info->faac_slh.manuf); break; - case GenKeeloqBFT: - subghz->gen_info->keeloq_bft.seed = __bswap32(subghz->gen_info->keeloq_bft.seed); - generated_protocol = subghz_txrx_gen_keeloq_bft_protocol( + case GenKeeloqSeed: + subghz->gen_info->keeloq_seed.seed = __bswap32(subghz->gen_info->keeloq_seed.seed); + generated_protocol = subghz_txrx_gen_keeloq_seed_protocol( subghz->txrx, subghz->gen_info->mod, subghz->gen_info->freq, - subghz->gen_info->keeloq_bft.serial, - subghz->gen_info->keeloq_bft.btn, - subghz->gen_info->keeloq_bft.cnt, - subghz->gen_info->keeloq_bft.seed, - subghz->gen_info->keeloq_bft.manuf); + subghz->gen_info->keeloq_seed.serial, + subghz->gen_info->keeloq_seed.btn, + subghz->gen_info->keeloq_seed.cnt, + subghz->gen_info->keeloq_seed.seed, + subghz->gen_info->keeloq_seed.manuf); break; // Not needed for these types case GenKeeloq: diff --git a/applications/main/subghz/scenes/subghz_scene_set_serial.c b/applications/main/subghz/scenes/subghz_scene_set_serial.c index ba105a804..9d4640d00 100644 --- a/applications/main/subghz/scenes/subghz_scene_set_serial.c +++ b/applications/main/subghz/scenes/subghz_scene_set_serial.c @@ -30,9 +30,9 @@ void subghz_scene_set_serial_on_enter(void* context) { byte_ptr = (uint8_t*)&subghz->gen_info->came_atomo.serial; byte_count = sizeof(subghz->gen_info->came_atomo.serial); break; - case GenKeeloqBFT: - byte_ptr = (uint8_t*)&subghz->gen_info->keeloq_bft.serial; - byte_count = sizeof(subghz->gen_info->keeloq_bft.serial); + case GenKeeloqSeed: + byte_ptr = (uint8_t*)&subghz->gen_info->keeloq_seed.serial; + byte_count = sizeof(subghz->gen_info->keeloq_seed.serial); break; case GenAlutechAt4n: byte_ptr = (uint8_t*)&subghz->gen_info->alutech_at_4n.serial; @@ -118,9 +118,9 @@ bool subghz_scene_set_serial_on_event(void* context, SceneManagerEvent event) { subghz->gen_info->came_atomo.serial = __bswap32(subghz->gen_info->came_atomo.serial); break; - case GenKeeloqBFT: - subghz->gen_info->keeloq_bft.serial = - __bswap32(subghz->gen_info->keeloq_bft.serial); + case GenKeeloqSeed: + subghz->gen_info->keeloq_seed.serial = + __bswap32(subghz->gen_info->keeloq_seed.serial); break; case GenAlutechAt4n: subghz->gen_info->alutech_at_4n.serial = @@ -172,7 +172,7 @@ bool subghz_scene_set_serial_on_event(void* context, SceneManagerEvent event) { switch(subghz->gen_info->type) { case GenFaacSLH: case GenKeeloq: - case GenKeeloqBFT: + case GenKeeloqSeed: case GenAlutechAt4n: case GenSomfyTelis: case GenSomfyKeytis: diff --git a/applications/main/subghz/scenes/subghz_scene_set_type.c b/applications/main/subghz/scenes/subghz_scene_set_type.c index 7ee72ea21..f144947d7 100644 --- a/applications/main/subghz/scenes/subghz_scene_set_type.c +++ b/applications/main/subghz/scenes/subghz_scene_set_type.c @@ -15,6 +15,7 @@ static const char* submenu_names[SetTypeMAX] = { [SetTypeFaacSLH_868] = "FAAC SLH 868MHz", [SetTypeFaacSLH_433] = "FAAC SLH 433MHz", [SetTypeBFTMitto] = "BFT Mitto 433MHz", + [SetTypeErreka433] = "Erreka 433MHz", [SetTypeSomfyTelis] = "Somfy Telis 433MHz", [SetTypeSomfyKeytis] = "Somfy Keytis 433MHz", [SetTypeANMotorsAT4] = "AN-Motors AT4 433MHz", @@ -56,6 +57,19 @@ static const char* submenu_names[SetTypeMAX] = { [SetTypeNovoferm_433_92] = "KL: Novoferm 433MHz", [SetTypeHormannEcoStar_433_92] = "KL: Hor. EcoStar 433MHz", [SetTypeCardinS449_433FM] = "KL: Cardin S449 433MHz", + [SetTypePujol433] = "KL: Pujol 433MHz", + [SetTypePujol_Vario433] = "KL: Pujol Vario 433MHz", + [SetTypeET_Blue433] = "KL: ET Blue 433MHz", + [SetTypeET_Blue_Mix433] = "KL: ET Blue Mix 433MHz", + [SetTypeATA_PTX4_433] = "KL: ATA PTX4 433MHz", + [SetTypeSeav433] = "KL: Seav 433MHz", + [SetTypeWisniowski433] = "KL: Wisniowski 433MHz", + [SetTypeFadini433] = "KL: Fadini 433MHz", + [SetTypeMc_Garcia433] = "KL: Mc Garcia 433MHz", + [SetTypeClemsa_Mutancode433] = "KL: Clemsa Mutancode 433MHz", + [SetTypeDoormatic433] = "KL: Doormatic 433MHz", + [SetTypeElvox433] = "KL: Elvox 433MHz", + [SetTypeVerex433] = "KL: Verex 433MHz", [SetTypeFAACRCXT_433_92] = "KL: FAAC RC,XT 433MHz", [SetTypeFAACRCXT_868] = "KL: FAAC RC,XT 868MHz", [SetTypeGeniusBravo433] = "KL: Genius TX4RC 433MHz", @@ -163,16 +177,16 @@ bool subghz_scene_set_type_generate_protocol_from_infos(SubGhz* subghz) { gen_info.came_atomo.serial, gen_info.came_atomo.cnt); break; - case GenKeeloqBFT: - generated_protocol = subghz_txrx_gen_keeloq_bft_protocol( + case GenKeeloqSeed: + generated_protocol = subghz_txrx_gen_keeloq_seed_protocol( subghz->txrx, gen_info.mod, gen_info.freq, - gen_info.keeloq_bft.serial, - gen_info.keeloq_bft.btn, - gen_info.keeloq_bft.cnt, - gen_info.keeloq_bft.seed, - gen_info.keeloq_bft.manuf); + gen_info.keeloq_seed.serial, + gen_info.keeloq_seed.btn, + gen_info.keeloq_seed.cnt, + gen_info.keeloq_seed.seed, + gen_info.keeloq_seed.manuf); break; case GenAlutechAt4n: generated_protocol = subghz_txrx_gen_alutech_at_4n_protocol( @@ -313,7 +327,7 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) { case GenFaacSLH: // Serial (u32), Button (u8), Counter (u32), Seed (u32) case GenKeeloq: // Serial (u32), Button (u8), Counter (u16) case GenCameAtomo: // Serial (u32), Counter (u16) - case GenKeeloqBFT: // Serial (u32), Button (u8), Counter (u16), Seed (u32) + case GenKeeloqSeed: // Serial (u32), Button (u8), Counter (u16), Seed (u32) case GenAlutechAt4n: // Serial (u32), Button (u8), Counter (u16) case GenSomfyTelis: // Serial (u32), Button (u8), Counter (u16) case GenSomfyKeytis: // Serial (u32), Button (u8), Counter (u16) diff --git a/documentation/SubGHzSupportedSystems.md b/documentation/SubGHzSupportedSystems.md index 87615f2b0..b6f1b34f0 100644 --- a/documentation/SubGHzSupportedSystems.md +++ b/documentation/SubGHzSupportedSystems.md @@ -22,7 +22,7 @@ That list is only for default SubGHz app, apps like *Weather Station* have their - BETT `433.92MHz` `AM650` (18 bits, Static) - Beninca ARC (TOGO2VA) `433.92MHz` `AM650` (128 bits, Dynamic AES128) (button code `0` emulates `hidden button` option on the remote) - BFT Mitto `433.92MHz` `AM650` (64 bits, Dynamic, KeeLoq based with Seed taken from serial) -- CAME Atomo `433.92MHz, 868MHz` `AM650` (62 bits, Dynamic) +- CAME Atomo `433.92MHz, 868MHz` `AM650` (62 bits, Dynamic) (TOPD4REN, TOP44RBN, TOP42R, TOP44R) - CAME TWEE `433.92MHz` `AM650` (54 bits, Pseudo-Dynamic) (+ TOP44FGN) (aka New Fixed Code) - CAME `433.92MHz, 868MHz` `AM650` (12, 24 bits, Static) - Ditec GOL4 `433.92MHz` `AM650` (54 bits, Dynamic) (should be compatible with BIXLG4, BIXLS2, BIXLP2) - (right arrow emulates button `0` (hidden button)) @@ -33,7 +33,7 @@ That list is only for default SubGHz app, apps like *Weather Station* have their - Dickert MAHS `AM650` (36 bits, Static) - Doitrand `AM650` (37 bits, Dynamic) - Elplast/P-11B/3BK/E.C.A `433MHz` `AM650` (18 bits, Static) -- FAAC SLH `433.92MHz, 868.35MHz` `AM650` (64 bits, Dynamic) (+ Genius KILO TX2/4 JLC) +- FAAC SLH `433.92MHz, 868.35MHz` `AM650` (64 bits, Dynamic) (+ Genius KILO TX2/4 JLC, other Genius models) - Gate TX `433.92MHz` `AM650` (64 bits, Static) - Hormann `868MHz` `AM650` (44 bits, Static) - HCS101 `AM650` (64 bits, Simple Dynamic, KeeLoq-like) @@ -120,7 +120,7 @@ The following manufacturers have KeeLoq support in Unleashed firmware: - KEY (KeeLoq, 64 bits) - Monarch - `433.92MHz` `AM650` (KeeLoq, 64 bits) (no serial in Hop, uses fixed value 0x100 - normal learning) - Motorline - `433.92MHz` `AM650` (KeeLoq, 64 bits) (normal learning) -- Mutanco/Mutancode (KeeLoq, 64 bits) (12bit serial part in Hop - normal learning) +- Clemsa Mutancode (KeeLoq, 64 bits) (12bit serial part in Hop - normal learning) - Mhouse - `433.92MHz` `AM650` (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) - Nice Smilo - `433.92MHz` `AM650` (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) - Normstahl - `433.92MHz` `AM650` (KeeLoq, 64 bits) @@ -128,6 +128,47 @@ The following manufacturers have KeeLoq support in Unleashed firmware: - Sommer `434.42MHz, 868.80MHz` `FSK12K (or FSK476)` (KeeLoq, 64 bits) (normal learning) (TX03-868-4, Pearl, and maybe other models are supported (SOMloq)) - Steelmate - `433.92MHz` `AM650` (KeeLoq, 64 bits) (12bit serial part in Hop - normal learning) - Stilmatic (R-Tech) - `433.92MHz` `AM650` (KeeLoq, 64 bits) (12bit serial part in Hop - normal learning) (receiver checks for 10bit only (unverified)) +- Erreka - `433.92MHz` `AM650` (KeeLoq, 64 bits) (8bit serial part in Hop - secure learning with Seed) +- Pujol - `433.92MHz` `AM650` (KeeLoq, 64 bits) (8bit serial part in Hop - special learning) (Pujol Vario 4, other models) +- Pujol Vario - `433.92MHz` `AM650` (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) (older models) +- Wisniowski (KeeLoq, 64 bits) (12bit serial part in Hop - normal learning) +- Doormatic (KeeLoq, 64 bits) (normal learning) +- Elvox (KeeLoq, 64 bits) (normal learning) +- ET Blue (KeeLoq, 64 bits) (10bit serial part in Hop - normal learning) +- ET Blue Mix (KeeLoq, 64 bits) (10bit serial part in Hop - normal learning) +- ATA PTX4 (KeeLoq, 64 bits) (12bit serial part in Hop - normal learning) +- Verex (KeeLoq, 64 bits) (normal learning) +- Mc Garcia (KeeLoq, 64 bits) (10bit serial part in Hop - simple learning) +- Fadini (KeeLoq, 64 bits) (12bit serial part in Hop - simple learning) +- Seav (KeeLoq, 64 bits) (12bit serial part in Hop - normal learning) +- AERF Otros (KeeLoq, 64 bits) (10bit serial part in Hop - special learning) +- AERF Collbaix (KeeLoq, 64 bits) (10bit serial part in Hop - special learning) +- AERF Hy dom (KeeLoq, 64 bits) (10bit serial part in Hop - special learning) +- AERF Medva (KeeLoq, 64 bits) (10bit serial part in Hop - special learning) +- AERF VDS (KeeLoq, 64 bits) (10bit serial part in Hop - special learning) +- AERF Temp (KeeLoq, 64 bits) (10bit serial part in Hop - special learning) +- AERF Sabutom (KeeLoq, 64 bits) (10bit serial part in Hop - special learning) +- AERF Motorgate (KeeLoq, 64 bits) (10bit serial part in Hop - special learning) +- JCM1G EMFA (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) +- JCM1G DMIL (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) +- JCM1G Antonio Meca (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) +- JCM1G Hy dom (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) +- JCM1G Baleato (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) +- JCM1G Cas (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) +- JCM1G Cubells (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) +- JCM1G Cyacsa (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) +- JCM1G Forsa (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) +- JCM1G Gandara (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) +- JCM1G Pujol (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) +- JCM1G Gibidi (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) +- JCM1G Gibidi2 (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) +- JCM1G Hybrid (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) +- JCM1G Tech (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) +- JCM1G Norton (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) +- JCM1G Nueva Castilla (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) +- JCM1G Puertas Lorenzo (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) +- JCM1G Serviparking (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) +- JCM1G Zibor (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) ### Alarms, unknown origin, etc. - APS-1100/APS-2550 (KeeLoq, 64 bits) @@ -163,10 +204,11 @@ The following manufacturers have KeeLoq support in Unleashed firmware: - Tomahawk TZ-9030 (KeeLoq, 64 bits) - Tomahawk Z,X 3-5 (KeeLoq, 64 bits) - ZX-730-750-1055 (KeeLoq, 64 bits) -- Zero_Simple (KeeLoq, 64 bits) -- Zero_Normal (KeeLoq, 64 bits) -- FFFF_Simple (KeeLoq, 64 bits) -- FFFF_Normal (KeeLoq, 64 bits) +- Zero Simple (KeeLoq, 64 bits) +- Zero Normal (KeeLoq, 64 bits) +- FFFF Simple (KeeLoq, 64 bits) +- FFFF Normal (KeeLoq, 64 bits) +- Miserere (KeeLoq, 64 bits) (normal learning) *Note: Most KeeLoq manufacturers operate in the 433 MHz and 868 MHz frequency bands with AM650 modulation. Some operate at other frequencies or modulations. Not all KeeLoq systems are supported for full decoding or emulation.* diff --git a/lib/nfc/protocols/mf_desfire/mf_desfire_i.c b/lib/nfc/protocols/mf_desfire/mf_desfire_i.c index b90ef8ccf..886049685 100644 --- a/lib/nfc/protocols/mf_desfire/mf_desfire_i.c +++ b/lib/nfc/protocols/mf_desfire/mf_desfire_i.c @@ -290,9 +290,7 @@ bool mf_desfire_file_settings_parse(MfDesfireFileSettings* data, const BitBuffer printf("\r\n"); break; } - if(additional_access_rights_len > - MF_DESFIRE_MAX_KEYS * sizeof(MfDesfireFileAccessRights)) - break; + if(additional_access_rights_len > MF_DESFIRE_MAX_KEYS - 1) break; memcpy( &file_settings_temp.access_rights[1], diff --git a/lib/nfc/protocols/mf_ultralight/mf_ultralight_listener.c b/lib/nfc/protocols/mf_ultralight/mf_ultralight_listener.c index d8c5fb1e6..9a25d81b6 100644 --- a/lib/nfc/protocols/mf_ultralight/mf_ultralight_listener.c +++ b/lib/nfc/protocols/mf_ultralight/mf_ultralight_listener.c @@ -169,6 +169,10 @@ static MfUltralightCommand MfUltralightPage pages[64] = {}; uint8_t page_cnt = (end_page - start_page) + 1; + if(page_cnt > COUNT_OF(pages)) { + command = MfUltralightCommandNotProcessedNAK; + break; + } mf_ultralight_listener_perform_read(pages, instance, start_page, page_cnt, do_i2c_check); bit_buffer_copy_bytes(instance->tx_buffer, (uint8_t*)pages, page_cnt * 4); diff --git a/lib/subghz/protocols/came_atomo.c b/lib/subghz/protocols/came_atomo.c index bd21dafd1..ddab59ff3 100644 --- a/lib/subghz/protocols/came_atomo.c +++ b/lib/subghz/protocols/came_atomo.c @@ -258,6 +258,10 @@ static void subghz_protocol_encoder_came_atomo_get_upload( btn = 0x4; } else if(btn == 0x4) { btn = 0x6; + } else if(btn == 0x5) { + btn = 0x0C; + } else if(btn == 0x6) { + btn = 0x0E; } // override button if we change it with signal settings button editor @@ -482,9 +486,11 @@ void subghz_protocol_decoder_came_atomo_feed(void* context, bool level, uint32_t ManchesterEvent event = ManchesterEventReset; switch(instance->decoder.parser_step) { case CameAtomoDecoderStepReset: - // There are two known options for the header: 72K us (TOP42R, TOP44R) or 12k us (found on TOP44RBN) + // There are two known options for the header: 72K us (TOP42R, TOP44R) or 12k us (found on TOP44RBN) / 19k us (TOPD4REN) if((!level) && ((DURATION_DIFF(duration, subghz_protocol_came_atomo_const.te_long * 10) < subghz_protocol_came_atomo_const.te_delta * 20) || + (DURATION_DIFF(duration, subghz_protocol_came_atomo_const.te_long * 16) < + subghz_protocol_came_atomo_const.te_delta * 10) || (DURATION_DIFF(duration, subghz_protocol_came_atomo_const.te_long * 60) < subghz_protocol_came_atomo_const.te_delta * 40))) { //Found header CAME @@ -661,6 +667,10 @@ static void subghz_protocol_came_atomo_remote_controller(SubGhzBlockGeneric* ins instance->btn = 0x3; } else if(btn_decode == 0x6) { instance->btn = 0x4; + } else if(btn_decode == 0x0C) { + instance->btn = 0x5; + } else if(btn_decode == 0x0E) { + instance->btn = 0x6; } uint32_t hi = pack[0] << 24 | pack[1] << 16 | pack[2] << 8 | pack[3]; @@ -671,7 +681,7 @@ static void subghz_protocol_came_atomo_remote_controller(SubGhzBlockGeneric* ins if(subghz_custom_btn_get_original() == 0) { subghz_custom_btn_set_original(instance->btn); } - subghz_custom_btn_set_max(3); + subghz_custom_btn_set_max(4); } void atomo_encrypt(uint8_t* buff) { @@ -738,6 +748,12 @@ static uint8_t subghz_protocol_came_atomo_get_btn_code(void) { case 0x4: btn = 0x1; break; + case 0x5: + btn = 0x1; + break; + case 0x6: + btn = 0x1; + break; default: break; @@ -756,6 +772,12 @@ static uint8_t subghz_protocol_came_atomo_get_btn_code(void) { case 0x4: btn = 0x2; break; + case 0x5: + btn = 0x2; + break; + case 0x6: + btn = 0x2; + break; default: break; @@ -774,6 +796,36 @@ static uint8_t subghz_protocol_came_atomo_get_btn_code(void) { case 0x4: btn = 0x3; break; + case 0x5: + btn = 0x4; + break; + case 0x6: + btn = 0x4; + break; + + default: + break; + } + } else if(custom_btn_id == SUBGHZ_CUSTOM_BTN_RIGHT) { + switch(original_btn_code) { + case 0x1: + btn = 0x5; + break; + case 0x2: + btn = 0x5; + break; + case 0x3: + btn = 0x5; + break; + case 0x4: + btn = 0x5; + break; + case 0x5: + btn = 0x6; + break; + case 0x6: + btn = 0x5; + break; default: break; diff --git a/lib/subghz/protocols/keeloq.c b/lib/subghz/protocols/keeloq.c index 2bf173994..ac04610f9 100644 --- a/lib/subghz/protocols/keeloq.c +++ b/lib/subghz/protocols/keeloq.c @@ -367,7 +367,7 @@ static bool subghz_protocol_keeloq_gen_data( } else if( (strcmp(instance->manufacture_name, "DTM_Neo") == 0) || (strcmp(instance->manufacture_name, "FAAC_RC,XT") == 0) || - (strcmp(instance->manufacture_name, "Mutanco_Mutancode") == 0) || + (strcmp(instance->manufacture_name, "Clemsa_Mutancode") == 0) || (strcmp(instance->manufacture_name, "Came_Space") == 0) || (strcmp(instance->manufacture_name, "Genius_Bravo") == 0) || (strcmp(instance->manufacture_name, "GSN") == 0) || @@ -376,20 +376,33 @@ static bool subghz_protocol_keeloq_gen_data( (strcmp(instance->manufacture_name, "Pecinin") == 0) || (strcmp(instance->manufacture_name, "Steelmate") == 0) || (strcmp(instance->manufacture_name, "Cardin_S449") == 0) || - (strcmp(instance->manufacture_name, "Stilmatic") == 0)) { + (strcmp(instance->manufacture_name, "Stilmatic") == 0) || + (strcmp(instance->manufacture_name, "Wisniowski") == 0) || + (strcmp(instance->manufacture_name, "ATA_PTX4") == 0) || + (strcmp(instance->manufacture_name, "Fadini") == 0) || + (strcmp(instance->manufacture_name, "Seav") == 0)) { // DTM Neo, Came_Space uses 12bit serial -> simple learning - // FAAC_RC,XT , Mutanco_Mutancode, Genius_Bravo, GSN 12bit serial -> normal learning + // FAAC_RC,XT , Clemsa_Mutancode, Genius_Bravo, GSN 12bit serial -> normal learning // Rosh, Rossi, Pecinin -> 12bit serial - simple learning // Steelmate -> 12bit serial - normal learning // Cardin_S449 -> 12bit serial - normal learning // Stilmatic (r-tech) -> 12bit serial - normal learning + // Wisniowski -> 12bit serial - normal learning + // ATA_PTX4 -> 12bit serial - normal learning + // Fadini -> 12bit serial - simple learning + // Seav -> 12bit serial - normal learning decrypt = btn << 28 | (instance->generic.serial & 0xFFF) << 16 | instance->generic.cnt; } else if( (strcmp(instance->manufacture_name, "NICE_Smilo") == 0) || (strcmp(instance->manufacture_name, "NICE_MHOUSE") == 0) || - (strcmp(instance->manufacture_name, "JCM_Tech") == 0)) { - // Nice Smilo, MHouse, JCM -> 8bit serial - simple learning + (strcmp(instance->manufacture_name, "JCM_Tech") == 0) || + (strcmp(instance->manufacture_name, "Pujol_Vario") == 0) || + (strcmp(instance->manufacture_name, "Pujol") == 0) || + (strcmp(instance->manufacture_name, "Erreka") == 0)) { + // Nice Smilo, MHouse, JCM, Pujol_Vario -> 8bit serial - simple learning + // Pujol -> 8bit serial - special learning + // Erreka -> 8bit serial - secure learning with seed decrypt = btn << 28 | (instance->generic.serial & 0xFF) << 16 | instance->generic.cnt; } else if( @@ -453,6 +466,28 @@ static bool subghz_protocol_keeloq_gen_data( fix, manufacture_code->key); hop = subghz_protocol_keeloq_common_encrypt(decrypt, man); break; + case KEELOQ_LEARNING_AERF: + man = subghz_protocol_keeloq_common_learning_aerf( + fix, manufacture_code->key); + hop = subghz_protocol_keeloq_common_encrypt(decrypt, man); + break; + case KEELOQ_LEARNING_ERREKA: + man = subghz_protocol_keeloq_common_learning_erreka( + fix, instance->generic.seed, manufacture_code->key); + hop = subghz_protocol_keeloq_common_encrypt(decrypt, man); + break; + case KEELOQ_LEARNING_PUJOL: + man = subghz_protocol_keeloq_common_learning_pujol( + fix, manufacture_code->key); + hop = subghz_protocol_keeloq_common_encrypt(decrypt, man); + break; + case KEELOQ_LEARNING_SIMPLE_JCM: + //Simple Learning 8 bit serial + decrypt = btn << 28 | (instance->generic.serial & 0xFF) << 16 | + instance->generic.cnt; + hop = subghz_protocol_keeloq_common_encrypt( + decrypt, manufacture_code->key); + break; case KEELOQ_LEARNING_UNKNOWN: if(kl_type_en == 1) { hop = subghz_protocol_keeloq_common_encrypt( @@ -511,7 +546,7 @@ bool subghz_protocol_keeloq_create_data( return false; } -bool subghz_protocol_keeloq_bft_create_data( +bool subghz_protocol_keeloq_seed_create_data( void* context, FlipperFormat* flipper_format, uint32_t serial, @@ -1101,6 +1136,55 @@ static uint32_t subghz_protocol_keeloq_check_remote_controller_selector( return decrypt; } break; + case KEELOQ_LEARNING_AERF: + man = subghz_protocol_keeloq_common_learning_aerf(fix, manufacture_code->key); + decrypt = subghz_protocol_keeloq_common_decrypt_derived(hop, man, 0x240u); + if(subghz_protocol_keeloq_check_decrypt(instance, decrypt, btn, end_serial)) { + *manufacture_name = furi_string_get_cstr(manufacture_code->name); + keystore->mfname = *manufacture_name; + return decrypt; + } + decrypt = subghz_protocol_keeloq_common_decrypt_derived(hop, man, 0x210u); + if(subghz_protocol_keeloq_check_decrypt(instance, decrypt, btn, end_serial)) { + *manufacture_name = furi_string_get_cstr(manufacture_code->name); + keystore->mfname = *manufacture_name; + return decrypt; + } + decrypt = subghz_protocol_keeloq_common_decrypt(hop, man); + if(subghz_protocol_keeloq_check_decrypt(instance, decrypt, btn, end_serial)) { + *manufacture_name = furi_string_get_cstr(manufacture_code->name); + keystore->mfname = *manufacture_name; + return decrypt; + } + break; + case KEELOQ_LEARNING_ERREKA: + man = subghz_protocol_keeloq_common_learning_erreka( + fix, instance->seed, manufacture_code->key); + decrypt = subghz_protocol_keeloq_common_decrypt(hop, man); + if(subghz_protocol_keeloq_check_decrypt(instance, decrypt, btn, end_serial)) { + *manufacture_name = furi_string_get_cstr(manufacture_code->name); + keystore->mfname = *manufacture_name; + return decrypt; + } + break; + case KEELOQ_LEARNING_PUJOL: + man = subghz_protocol_keeloq_common_learning_pujol(fix, manufacture_code->key); + decrypt = subghz_protocol_keeloq_common_decrypt(hop, man); + if(subghz_protocol_keeloq_check_decrypt(instance, decrypt, btn, end_serial)) { + *manufacture_name = furi_string_get_cstr(manufacture_code->name); + keystore->mfname = *manufacture_name; + return decrypt; + } + break; + case KEELOQ_LEARNING_SIMPLE_JCM: + // Simple Learning 8 bit serial + decrypt = subghz_protocol_keeloq_common_decrypt(hop, manufacture_code->key); + if(subghz_protocol_keeloq_check_decrypt(instance, decrypt, btn, end_serial)) { + *manufacture_name = furi_string_get_cstr(manufacture_code->name); + keystore->mfname = *manufacture_name; + return decrypt; + } + break; case KEELOQ_LEARNING_UNKNOWN: // Simple Learning decrypt = subghz_protocol_keeloq_common_decrypt(hop, manufacture_code->key); @@ -1625,7 +1709,9 @@ void subghz_protocol_decoder_keeloq_get_string(void* context, FuriString* output code_found_reverse_lo, instance->generic.btn, instance->manufacture_name); - } else if(strcmp(instance->manufacture_name, "BFT") == 0) { + } else if( + (strcmp(instance->manufacture_name, "BFT") == 0) || + (strcmp(instance->manufacture_name, "Erreka") == 0)) { // Allow counter edit subghz_block_generic_global.cnt_is_available = true; subghz_block_generic_global.cnt_length_bit = 16; diff --git a/lib/subghz/protocols/keeloq_common.c b/lib/subghz/protocols/keeloq_common.c index a9a2238dd..2486ec45f 100644 --- a/lib/subghz/protocols/keeloq_common.c +++ b/lib/subghz/protocols/keeloq_common.c @@ -140,3 +140,97 @@ inline uint64_t subghz_protocol_keeloq_common_magic_serial_type3_learning(uint32_t data, uint64_t man) { return (man & 0xFFFFFFFFFF000000) | (data & 0xFFFFFF); } + +// Key utils + +static inline uint32_t subghz_protocol_keeloq_common_manufacturer_nl_extend( + uint32_t x, + uint32_t k_lo, + uint32_t k_hi, + uint32_t outer_limit) { + uint32_t r4 = outer_limit; + uint32_t r5 = 0u; + const uint32_t r6 = KEELOQ_NLF; + + while(r5 != r4) { + if(r5 < 0x210u) { + uint32_t r1 = (x >> 15) & 1u; + uint32_t r7 = r1 ^ ((x >> 1) | (x << 31)); + r1 = (15u - r5) & 0x3Fu; + uint32_t lr = 32u - r1; + uint32_t ip = r1 - 32u; + lr = k_hi << lr; + r1 = k_lo >> r1; + ip = (r1 < 32u) ? (k_hi >> ip) : 0u; + r1 = (r1 | lr | ip) & 1u; + ip = (x >> 30) & 1u; + r1 ^= r7; + r7 = (x >> 25) & 1u; + r7 += ip << 1; + ip = (x >> 19) & 1u; + ip += r7 << 1; + r7 = (x >> 8) & 1u; + r7 += ip << 1; + x &= 1u; + x += r7 << 1; + x = (int32_t)r6 >> (x & 31u); + x &= 1u; + x ^= r1; + } + r5 += 1u; + } + return x; +} + +static inline uint32_t subghz_protocol_keeloq_common_word_rotate16(uint32_t v) { + return (v >> 16) | (v << 16); +} + +inline uint32_t subghz_protocol_keeloq_common_decrypt_derived( + uint32_t hop_encrypted, + uint64_t derived_manufacturing_key, + uint32_t outer_limit) { + return subghz_protocol_keeloq_common_manufacturer_nl_extend( + hop_encrypted, + (uint32_t)derived_manufacturing_key, + (uint32_t)(derived_manufacturing_key >> 32u), + outer_limit); +} + +// Protocol (Manufacturer) specific learning +// TODO: Better documentation for these functions + +inline uint64_t subghz_protocol_keeloq_common_learning_aerf(uint32_t data, const uint64_t key) { + uint32_t k_lo = (uint32_t)key; + uint32_t k_hi = (uint32_t)(key >> 32); + uint32_t d = data & 0x0FFFFFFFu; + uint32_t x = d | 0x20000000u; + x = subghz_protocol_keeloq_common_manufacturer_nl_extend(x, k_lo, k_hi, 0x40u); + uint32_t k1 = x; + x = d | 0x60000000u; + x = subghz_protocol_keeloq_common_manufacturer_nl_extend(x, k_lo, k_hi, 0x40u); + return ((uint64_t)x << 32) | k1; +} + +inline uint64_t + subghz_protocol_keeloq_common_learning_erreka(uint32_t data, uint32_t mix, const uint64_t key) { + uint32_t d = data & 0x0FFFFFFFu; + uint32_t k1 = subghz_protocol_keeloq_common_decrypt(d | 0x20000000u, key); + uint32_t r4 = mix >> 4; + uint32_t r1 = (mix << 4) & 0xF000F000u; + r4 = (r4 & 0x0F000F00u) | r1; + uint32_t r5 = mix & 0x00FF00FFu; + uint32_t x = r4 | r5; + x |= 0x60000000u; + uint32_t k2 = subghz_protocol_keeloq_common_decrypt(x, key); + return ((uint64_t)k2 << 32) | k1; +} + +inline uint64_t subghz_protocol_keeloq_common_learning_pujol(uint32_t data, const uint64_t key) { + uint32_t d = data & 0x0FFFFFFFu; + uint32_t w1 = subghz_protocol_keeloq_common_decrypt(d | 0x20000000u, key); + uint32_t w2 = subghz_protocol_keeloq_common_decrypt(d | 0x60000000u, key); + uint32_t k1 = subghz_protocol_keeloq_common_word_rotate16(w1); + uint32_t k2 = subghz_protocol_keeloq_common_word_rotate16(w2); + return ((uint64_t)k2 << 32) | k1; +} diff --git a/lib/subghz/protocols/keeloq_common.h b/lib/subghz/protocols/keeloq_common.h index 1250cd2fc..93904f194 100644 --- a/lib/subghz/protocols/keeloq_common.h +++ b/lib/subghz/protocols/keeloq_common.h @@ -28,6 +28,10 @@ // #define BENINCA_ARC_KEY_TYPE 9u -- RESERVED #define KEELOQ_LEARNING_SIMPLE_KINGGATES 10u #define KEELOQ_LEARNING_NORMAL_JAROLIFT 11u +#define KEELOQ_LEARNING_ERREKA 12u +#define KEELOQ_LEARNING_PUJOL 13u +#define KEELOQ_LEARNING_AERF 14u +#define KEELOQ_LEARNING_SIMPLE_JCM 15u /** * Simple Learning Encrypt @@ -101,3 +105,19 @@ uint64_t subghz_protocol_keeloq_common_magic_serial_type2_learning(uint32_t data */ uint64_t subghz_protocol_keeloq_common_magic_serial_type3_learning(uint32_t data, uint64_t man); + +// Protocol (Manufacturer) specific learning +// TODO: Better documentation for these functions + +uint64_t subghz_protocol_keeloq_common_learning_aerf(uint32_t data, const uint64_t key); + +uint64_t + subghz_protocol_keeloq_common_learning_erreka(uint32_t data, uint32_t mix, const uint64_t key); + +uint64_t subghz_protocol_keeloq_common_learning_pujol(uint32_t data, const uint64_t key); + +// Utils +uint32_t subghz_protocol_keeloq_common_decrypt_derived( + uint32_t hop_encrypted, + uint64_t derived_manufacturing_key, + uint32_t outer_limit); diff --git a/lib/subghz/protocols/public_api.h b/lib/subghz/protocols/public_api.h index 43bef118b..aa5e1965a 100644 --- a/lib/subghz/protocols/public_api.h +++ b/lib/subghz/protocols/public_api.h @@ -57,7 +57,7 @@ bool subghz_protocol_keeloq_create_data( * @param preset Modulation, SubGhzRadioPreset * @return true On success */ -bool subghz_protocol_keeloq_bft_create_data( +bool subghz_protocol_keeloq_seed_create_data( void* context, FlipperFormat* flipper_format, uint32_t serial, diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index bccd2c381..c1c6cbcef 100755 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -3734,7 +3734,7 @@ Function,+,subghz_protocol_encoder_raw_stop,void,void* Function,+,subghz_protocol_encoder_raw_yield,LevelDuration,void* Function,+,subghz_protocol_faac_slh_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint32_t, uint32_t, const char*, SubGhzRadioPreset*" Function,+,subghz_protocol_jarolift_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, SubGhzRadioPreset*" -Function,+,subghz_protocol_keeloq_bft_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, uint32_t, const char*, SubGhzRadioPreset*" +Function,+,subghz_protocol_keeloq_seed_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, uint32_t, const char*, SubGhzRadioPreset*" Function,+,subghz_protocol_keeloq_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, const char*, SubGhzRadioPreset*" Function,+,subghz_protocol_kinggates_stylo_4k_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, SubGhzRadioPreset*" Function,+,subghz_protocol_nice_flor_s_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, SubGhzRadioPreset*, _Bool" From ad2568cd12cd256817c55ccf164dbb5ff0fc6331 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 21 Apr 2026 03:57:01 +0300 Subject: [PATCH 34/71] upd changelog --- CHANGELOG.md | 1 + documentation/SubGHzSupportedSystems.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 428c8e8f4..520aefaa3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Main changes - Current API: 87.7 * SubGHz: Add support for **42+ Keeloq based systems** (with partial Add Manually support) (see [Full list](/documentation/SubGHzSupportedSystems.md)) (by @zero-mega, @xMasterX, ARF Team) +* SubGHz: Add **Allstar Firefly 318ALD31K** protocol (18 bits, Static) (PR #989 | by @jlaughter) * SubGHz: Add **Nord ICE** protocol (33 bits, Static) * SubGHz: **Better support for CAME Atomo** type remotes (TOPD4REN) (decode + button codes) (thx to Roman for raw recordings) * SubGHz: Add **CAME TOP44FGN** support in CAME TWEE protocol diff --git a/documentation/SubGHzSupportedSystems.md b/documentation/SubGHzSupportedSystems.md index b6f1b34f0..8bb37d9f9 100644 --- a/documentation/SubGHzSupportedSystems.md +++ b/documentation/SubGHzSupportedSystems.md @@ -58,6 +58,7 @@ That list is only for default SubGHz app, apps like *Weather Station* have their - Nero Radio `434.42MHz` `AM650` (56 bits, Static mode only, Dynamic is unsupported) - Security+1.0 `315MHz, 433.92MHz, 390MHz` `AM650` (42 bits, Dynamic) - Security+2.0 `310MHz, 390MHz, 868MHz` `AM650` (62 bits, Dynamic) +- Allstar Firefly 318ALD31K `318MHz` `AM650` (18 bits, Static) ### Sensors & Smart home - Intertechno V3 `AM650` (32 bits, Static) - Lights, sockets, other. From 1ed18e469c2199f66d6efe8ad913a07094424fd3 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 23 Apr 2026 04:51:24 +0300 Subject: [PATCH 35/71] upd changelog, fix small logic issue --- CHANGELOG.md | 3 ++- lib/subghz/protocols/kinggates_stylo_4k.c | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 520aefaa3..ce976953e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,9 @@ * NFC: Add **new parsers SZPPK, SKPPK and SevPPK**, upgrade Plantain parser, fix TwoCities parser (PR #981 | by @mxcdoam) * OFW PR 4362: NFC: **Fix BusFault** in Write to Initial Card (by @akrylysov) * OFW PR 4369: NFC: Fix stack buffer overflows in MFUL FAST_READ and DESFire file settings parsers (by @qp-x-qp) -* Apps: Build tag (**21apr2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) +* Apps: Build tag (**23apr2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes +* SubGHz: Fix small logic error in KingGatesStylo4k protocol (found by @X-Stuff) * OFW PR 4364: JS: Address utf8 support when uploading JavaScript application (by @bekindpleaserewind) * Display: Extend lcd contrast range to full ST756x 6-bit range (by @ShaTie) (reduced to -10 to +18 to avoid fully unreadable state)

diff --git a/lib/subghz/protocols/kinggates_stylo_4k.c b/lib/subghz/protocols/kinggates_stylo_4k.c index 29922c10e..ef2bd5dc8 100644 --- a/lib/subghz/protocols/kinggates_stylo_4k.c +++ b/lib/subghz/protocols/kinggates_stylo_4k.c @@ -568,8 +568,8 @@ static void subghz_protocol_kinggates_stylo_4k_remote_controller( if(((decrypt >> 28) == instance->btn) && (((decrypt >> 24) & 0x0F) == 0x0C) && (((decrypt >> 16) & 0xFF) == (instance->serial & 0xFF))) { ret = true; - break; } + break; } } if(ret) { From c4ef54c71afa5fb892b0f84d1022333b70eafce2 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 24 Apr 2026 01:03:47 +0300 Subject: [PATCH 36/71] Fix CLI with NTAG4xx and Type 4 Tag support by WillyJL --- applications/main/nfc/application.fam | 4 ++- .../cli/commands/dump/nfc_cli_command_dump.c | 24 ++++++++------ .../protocols/ntag4xx/nfc_cli_dump_ntag4xx.c | 31 +++++++++++++++++++ .../protocols/ntag4xx/nfc_cli_dump_ntag4xx.h | 5 +++ .../type_4_tag/nfc_cli_dump_type_4_tag.c | 31 +++++++++++++++++++ .../type_4_tag/nfc_cli_dump_type_4_tag.h | 5 +++ .../nfc/cli/commands/helpers/nfc_cli_format.c | 4 ++- .../cli/commands/raw/nfc_cli_command_raw.c | 3 ++ 8 files changed, 96 insertions(+), 11 deletions(-) create mode 100644 applications/main/nfc/cli/commands/dump/protocols/ntag4xx/nfc_cli_dump_ntag4xx.c create mode 100644 applications/main/nfc/cli/commands/dump/protocols/ntag4xx/nfc_cli_dump_ntag4xx.h create mode 100644 applications/main/nfc/cli/commands/dump/protocols/type_4_tag/nfc_cli_dump_type_4_tag.c create mode 100644 applications/main/nfc/cli/commands/dump/protocols/type_4_tag/nfc_cli_dump_type_4_tag.h diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index 9e5caf8df..7da31a971 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -631,13 +631,15 @@ App( "cli/commands/dump/protocols/iso14443_4a/nfc_cli_dump_iso14443_4a.c", "cli/commands/dump/protocols/iso14443_4b/nfc_cli_dump_iso14443_4b.c", "cli/commands/dump/protocols/iso15693_3/nfc_cli_dump_iso15693_3.c", + "cli/commands/dump/protocols/felica/nfc_cli_dump_felica.c", "cli/commands/dump/protocols/mf_ultralight/nfc_cli_dump_mf_ultralight.c", "cli/commands/dump/protocols/mf_classic/nfc_cli_dump_mf_classic.c", "cli/commands/dump/protocols/mf_plus/nfc_cli_dump_mf_plus.c", "cli/commands/dump/protocols/mf_desfire/nfc_cli_dump_mf_desfire.c", "cli/commands/dump/protocols/slix/nfc_cli_dump_slix.c", "cli/commands/dump/protocols/st25tb/nfc_cli_dump_st25tb.c", - "cli/commands/dump/protocols/felica/nfc_cli_dump_felica.c", + "cli/commands/dump/protocols/ntag4xx/nfc_cli_dump_ntag4xx.c", + "cli/commands/dump/protocols/type_4_tag/nfc_cli_dump_type_4_tag.c", "cli/commands/mfu/nfc_cli_command_mfu.c", "cli/commands/mfu/nfc_cli_action_info.c", "cli/commands/mfu/nfc_cli_action_rdbl.c", diff --git a/applications/main/nfc/cli/commands/dump/nfc_cli_command_dump.c b/applications/main/nfc/cli/commands/dump/nfc_cli_command_dump.c index 2f968b58e..3976dfe1b 100644 --- a/applications/main/nfc/cli/commands/dump/nfc_cli_command_dump.c +++ b/applications/main/nfc/cli/commands/dump/nfc_cli_command_dump.c @@ -9,13 +9,15 @@ #include "protocols/iso14443_4a/nfc_cli_dump_iso14443_4a.h" #include "protocols/iso14443_4b/nfc_cli_dump_iso14443_4b.h" #include "protocols/iso15693_3/nfc_cli_dump_iso15693_3.h" -#include "protocols/mf_classic/nfc_cli_dump_mf_classic.h" -#include "protocols/mf_desfire/nfc_cli_dump_mf_desfire.h" -#include "protocols/mf_plus/nfc_cli_dump_mf_plus.h" +#include "protocols/felica/nfc_cli_dump_felica.h" #include "protocols/mf_ultralight/nfc_cli_dump_mf_ultralight.h" +#include "protocols/mf_classic/nfc_cli_dump_mf_classic.h" +#include "protocols/mf_plus/nfc_cli_dump_mf_plus.h" +#include "protocols/mf_desfire/nfc_cli_dump_mf_desfire.h" #include "protocols/slix/nfc_cli_dump_slix.h" #include "protocols/st25tb/nfc_cli_dump_st25tb.h" -#include "protocols/felica/nfc_cli_dump_felica.h" +#include "protocols/ntag4xx/nfc_cli_dump_ntag4xx.h" +#include "protocols/type_4_tag/nfc_cli_dump_type_4_tag.h" #include #include @@ -81,18 +83,20 @@ static bool nfc_cli_dump_parse_filename_key(FuriString* value, void* output) { } NfcGenericCallback protocol_poller_callbacks[NfcProtocolNum] = { - [NfcProtocolMfUltralight] = nfc_cli_dump_poller_callback_mf_ultralight, - [NfcProtocolMfClassic] = nfc_cli_dump_poller_callback_mf_classic, - [NfcProtocolFelica] = nfc_cli_dump_poller_callback_felica, [NfcProtocolIso14443_3a] = nfc_cli_dump_poller_callback_iso14443_3a, [NfcProtocolIso14443_3b] = nfc_cli_dump_poller_callback_iso14443_3b, [NfcProtocolIso14443_4a] = nfc_cli_dump_poller_callback_iso14443_4a, [NfcProtocolIso14443_4b] = nfc_cli_dump_poller_callback_iso14443_4b, [NfcProtocolIso15693_3] = nfc_cli_dump_poller_callback_iso15693_3, - [NfcProtocolSlix] = nfc_cli_dump_poller_callback_slix, - [NfcProtocolMfDesfire] = nfc_cli_dump_poller_callback_mf_desfire, + [NfcProtocolFelica] = nfc_cli_dump_poller_callback_felica, + [NfcProtocolMfUltralight] = nfc_cli_dump_poller_callback_mf_ultralight, + [NfcProtocolMfClassic] = nfc_cli_dump_poller_callback_mf_classic, [NfcProtocolMfPlus] = nfc_cli_dump_poller_callback_mf_plus, + [NfcProtocolMfDesfire] = nfc_cli_dump_poller_callback_mf_desfire, + [NfcProtocolSlix] = nfc_cli_dump_poller_callback_slix, [NfcProtocolSt25tb] = nfc_cli_dump_poller_callback_st25tb, + [NfcProtocolNtag4xx] = nfc_cli_dump_poller_callback_ntag4xx, + [NfcProtocolType4Tag] = nfc_cli_dump_poller_callback_type_4_tag, }; static void nfc_cli_dump_generate_filename(FuriString* file_path) { @@ -219,6 +223,8 @@ static const NfcProtocolNameValuePair supported_protocols[] = { {.name = "des", .value = NfcProtocolMfDesfire}, {.name = "slix", .value = NfcProtocolSlix}, {.name = "st25", .value = NfcProtocolSt25tb}, + {.name = "ntag4", .value = NfcProtocolNtag4xx}, + {.name = "t4t", .value = NfcProtocolType4Tag}, }; static bool nfc_cli_dump_parse_protocol(FuriString* value, void* output) { diff --git a/applications/main/nfc/cli/commands/dump/protocols/ntag4xx/nfc_cli_dump_ntag4xx.c b/applications/main/nfc/cli/commands/dump/protocols/ntag4xx/nfc_cli_dump_ntag4xx.c new file mode 100644 index 000000000..04a29e79a --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/ntag4xx/nfc_cli_dump_ntag4xx.c @@ -0,0 +1,31 @@ +#include "nfc_cli_dump_ntag4xx.h" +#include + +#define TAG "NTAG4XX" + +NfcCommand nfc_cli_dump_poller_callback_ntag4xx(NfcGenericEvent event, void* context) { + furi_assert(context); + furi_assert(event.protocol == NfcProtocolNtag4xx); + furi_assert(event.event_data); + + NfcCliDumpContext* instance = context; + const Ntag4xxPollerEvent* ntag4xx_event = event.event_data; + + NfcCommand command = NfcCommandContinue; + + if(ntag4xx_event->type == Ntag4xxPollerEventTypeReadSuccess) { + nfc_device_set_data( + instance->nfc_device, NfcProtocolNtag4xx, nfc_poller_get_data(instance->poller)); + instance->result = NfcCliDumpErrorNone; + command = NfcCommandStop; + } else if(ntag4xx_event->type == Ntag4xxPollerEventTypeReadFailed) { + instance->result = NfcCliDumpErrorFailedToRead; + command = NfcCommandReset; + } + + if(command == NfcCommandStop) { + furi_semaphore_release(instance->sem_done); + } + + return command; +} diff --git a/applications/main/nfc/cli/commands/dump/protocols/ntag4xx/nfc_cli_dump_ntag4xx.h b/applications/main/nfc/cli/commands/dump/protocols/ntag4xx/nfc_cli_dump_ntag4xx.h new file mode 100644 index 000000000..3f0738452 --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/ntag4xx/nfc_cli_dump_ntag4xx.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../nfc_cli_dump_common_types.h" + +NfcCommand nfc_cli_dump_poller_callback_ntag4xx(NfcGenericEvent event, void* context); diff --git a/applications/main/nfc/cli/commands/dump/protocols/type_4_tag/nfc_cli_dump_type_4_tag.c b/applications/main/nfc/cli/commands/dump/protocols/type_4_tag/nfc_cli_dump_type_4_tag.c new file mode 100644 index 000000000..fd955f69e --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/type_4_tag/nfc_cli_dump_type_4_tag.c @@ -0,0 +1,31 @@ +#include "nfc_cli_dump_type_4_tag.h" +#include + +#define TAG "TYPE4TAG" + +NfcCommand nfc_cli_dump_poller_callback_type_4_tag(NfcGenericEvent event, void* context) { + furi_assert(context); + furi_assert(event.protocol == NfcProtocolType4Tag); + furi_assert(event.event_data); + + NfcCliDumpContext* instance = context; + const Type4TagPollerEvent* type_4_tag_event = event.event_data; + + NfcCommand command = NfcCommandContinue; + + if(type_4_tag_event->type == Type4TagPollerEventTypeReadSuccess) { + nfc_device_set_data( + instance->nfc_device, NfcProtocolType4Tag, nfc_poller_get_data(instance->poller)); + instance->result = NfcCliDumpErrorNone; + command = NfcCommandStop; + } else if(type_4_tag_event->type == Type4TagPollerEventTypeReadFailed) { + instance->result = NfcCliDumpErrorFailedToRead; + command = NfcCommandReset; + } + + if(command == NfcCommandStop) { + furi_semaphore_release(instance->sem_done); + } + + return command; +} diff --git a/applications/main/nfc/cli/commands/dump/protocols/type_4_tag/nfc_cli_dump_type_4_tag.h b/applications/main/nfc/cli/commands/dump/protocols/type_4_tag/nfc_cli_dump_type_4_tag.h new file mode 100644 index 000000000..2492ad5f5 --- /dev/null +++ b/applications/main/nfc/cli/commands/dump/protocols/type_4_tag/nfc_cli_dump_type_4_tag.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../nfc_cli_dump_common_types.h" + +NfcCommand nfc_cli_dump_poller_callback_type_4_tag(NfcGenericEvent event, void* context); diff --git a/applications/main/nfc/cli/commands/helpers/nfc_cli_format.c b/applications/main/nfc/cli/commands/helpers/nfc_cli_format.c index abe153938..c761c3d6e 100644 --- a/applications/main/nfc/cli/commands/helpers/nfc_cli_format.c +++ b/applications/main/nfc/cli/commands/helpers/nfc_cli_format.c @@ -9,10 +9,12 @@ static const char* protocol_names[NfcProtocolNum] = { [NfcProtocolFelica] = "FeliCa", [NfcProtocolMfUltralight] = "Mifare Ultralight", [NfcProtocolMfClassic] = "Mifare Classic", - [NfcProtocolMfDesfire] = "Mifare DESFire", [NfcProtocolMfPlus] = "Mifare Plus", + [NfcProtocolMfDesfire] = "Mifare DESFire", [NfcProtocolSlix] = "Slix", [NfcProtocolSt25tb] = "St25tb", + [NfcProtocolNtag4xx] = "Ntag4xx", + [NfcProtocolType4Tag] = "Type 4 Tag", }; const char* nfc_cli_get_protocol_name(NfcProtocol protocol) { diff --git a/applications/main/nfc/cli/commands/raw/nfc_cli_command_raw.c b/applications/main/nfc/cli/commands/raw/nfc_cli_command_raw.c index f90adb4c6..03fc9eec8 100644 --- a/applications/main/nfc/cli/commands/raw/nfc_cli_command_raw.c +++ b/applications/main/nfc/cli/commands/raw/nfc_cli_command_raw.c @@ -117,9 +117,12 @@ const NfcCliRawProtocolSpecificHandler nfc_cli_raw_protocol_handlers[] = { [NfcProtocolFelica] = nfc_cli_raw_felica_handler, [NfcProtocolMfUltralight] = NULL, [NfcProtocolMfClassic] = NULL, + [NfcProtocolMfPlus] = NULL, [NfcProtocolMfDesfire] = NULL, [NfcProtocolSlix] = NULL, [NfcProtocolSt25tb] = NULL, + [NfcProtocolNtag4xx] = NULL, + [NfcProtocolType4Tag] = NULL, }; static NfcCommand nfc_cli_raw_poller_callback(NfcGenericEventEx event, void* context) { From cc84f535ed667370860e03a42b1a83c5cd05fe19 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 24 Apr 2026 01:04:41 +0300 Subject: [PATCH 37/71] remove unused subghzhistory code --- applications/main/subghz/subghz_history.c | 7 ------- 1 file changed, 7 deletions(-) diff --git a/applications/main/subghz/subghz_history.c b/applications/main/subghz/subghz_history.c index 20d73feb0..ba116cc26 100644 --- a/applications/main/subghz/subghz_history.c +++ b/applications/main/subghz/subghz_history.c @@ -231,13 +231,6 @@ bool subghz_history_add_to_history( break; } furi_string_cat(instance->tmp_string, text); - } else if(!strcmp(furi_string_get_cstr(instance->tmp_string), "Star Line")) { - furi_string_set(instance->tmp_string, "SL "); - if(!flipper_format_read_string(item->flipper_string, "Manufacture", text)) { - FURI_LOG_E(TAG, "Missing Protocol"); - break; - } - furi_string_cat(instance->tmp_string, text); } if(!flipper_format_rewind(item->flipper_string)) { FURI_LOG_E(TAG, "Rewind error"); From 893feaff20d608e3706e3b20137fc702c61b984a Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 24 Apr 2026 01:28:49 +0300 Subject: [PATCH 38/71] upd changelog --- CHANGELOG.md | 3 ++- documentation/SubGHzSupportedSystems.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce976953e..5af7fc2f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,10 @@ * NFC: **Fix "MIR" and other EMV cards crash on Read** (by @Dmitry422) * NFC: Add **Mifare Ultralight C Write Support** (by @haw8411) * NFC: Add **new parsers SZPPK, SKPPK and SevPPK**, upgrade Plantain parser, fix TwoCities parser (PR #981 | by @mxcdoam) +* NFC: Fix CLI with NTAG4xx and Type 4 Tag support (by @WillyJL) * OFW PR 4362: NFC: **Fix BusFault** in Write to Initial Card (by @akrylysov) * OFW PR 4369: NFC: Fix stack buffer overflows in MFUL FAST_READ and DESFire file settings parsers (by @qp-x-qp) -* Apps: Build tag (**23apr2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) +* Apps: Build tag (**24apr2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * SubGHz: Fix small logic error in KingGatesStylo4k protocol (found by @X-Stuff) * OFW PR 4364: JS: Address utf8 support when uploading JavaScript application (by @bekindpleaserewind) diff --git a/documentation/SubGHzSupportedSystems.md b/documentation/SubGHzSupportedSystems.md index 8bb37d9f9..7bef46f65 100644 --- a/documentation/SubGHzSupportedSystems.md +++ b/documentation/SubGHzSupportedSystems.md @@ -22,6 +22,7 @@ That list is only for default SubGHz app, apps like *Weather Station* have their - BETT `433.92MHz` `AM650` (18 bits, Static) - Beninca ARC (TOGO2VA) `433.92MHz` `AM650` (128 bits, Dynamic AES128) (button code `0` emulates `hidden button` option on the remote) - BFT Mitto `433.92MHz` `AM650` (64 bits, Dynamic, KeeLoq based with Seed taken from serial) +- Erreka - `433.92MHz` `AM650` (KeeLoq, 64 bits) (8bit serial part in Hop - secure learning with Seed) - CAME Atomo `433.92MHz, 868MHz` `AM650` (62 bits, Dynamic) (TOPD4REN, TOP44RBN, TOP42R, TOP44R) - CAME TWEE `433.92MHz` `AM650` (54 bits, Pseudo-Dynamic) (+ TOP44FGN) (aka New Fixed Code) - CAME `433.92MHz, 868MHz` `AM650` (12, 24 bits, Static) @@ -129,7 +130,6 @@ The following manufacturers have KeeLoq support in Unleashed firmware: - Sommer `434.42MHz, 868.80MHz` `FSK12K (or FSK476)` (KeeLoq, 64 bits) (normal learning) (TX03-868-4, Pearl, and maybe other models are supported (SOMloq)) - Steelmate - `433.92MHz` `AM650` (KeeLoq, 64 bits) (12bit serial part in Hop - normal learning) - Stilmatic (R-Tech) - `433.92MHz` `AM650` (KeeLoq, 64 bits) (12bit serial part in Hop - normal learning) (receiver checks for 10bit only (unverified)) -- Erreka - `433.92MHz` `AM650` (KeeLoq, 64 bits) (8bit serial part in Hop - secure learning with Seed) - Pujol - `433.92MHz` `AM650` (KeeLoq, 64 bits) (8bit serial part in Hop - special learning) (Pujol Vario 4, other models) - Pujol Vario - `433.92MHz` `AM650` (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) (older models) - Wisniowski (KeeLoq, 64 bits) (12bit serial part in Hop - normal learning) From fb1a80fe894be736dbc4a17e827cb9c76d369a63 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 24 Apr 2026 01:54:33 +0300 Subject: [PATCH 39/71] small ui fixes to fit text --- applications/main/subghz/scenes/subghz_scene_set_type.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/main/subghz/scenes/subghz_scene_set_type.c b/applications/main/subghz/scenes/subghz_scene_set_type.c index f144947d7..4f0bd3685 100644 --- a/applications/main/subghz/scenes/subghz_scene_set_type.c +++ b/applications/main/subghz/scenes/subghz_scene_set_type.c @@ -66,13 +66,13 @@ static const char* submenu_names[SetTypeMAX] = { [SetTypeWisniowski433] = "KL: Wisniowski 433MHz", [SetTypeFadini433] = "KL: Fadini 433MHz", [SetTypeMc_Garcia433] = "KL: Mc Garcia 433MHz", - [SetTypeClemsa_Mutancode433] = "KL: Clemsa Mutancode 433MHz", + [SetTypeClemsa_Mutancode433] = "KL: Clm.Mutancode 433M.", [SetTypeDoormatic433] = "KL: Doormatic 433MHz", [SetTypeElvox433] = "KL: Elvox 433MHz", [SetTypeVerex433] = "KL: Verex 433MHz", [SetTypeFAACRCXT_433_92] = "KL: FAAC RC,XT 433MHz", [SetTypeFAACRCXT_868] = "KL: FAAC RC,XT 868MHz", - [SetTypeGeniusBravo433] = "KL: Genius TX4RC 433MHz", + [SetTypeGeniusBravo433] = "KL: Genius TX4RC 433M.", [SetTypeNiceMHouse_433_92] = "KL: Mhouse 433MHz", [SetTypeNiceSmilo_433_92] = "KL: Nice Smilo 433MHz", [SetTypeNiceFlorS_433_92] = "Nice FloR-S 433MHz", From a9f5bace13c396b20098c3ba977c09fe6fa8c4a2 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 25 Apr 2026 00:01:00 +0300 Subject: [PATCH 40/71] fix nice flo --- CHANGELOG.md | 1 + lib/subghz/protocols/nice_flo.c | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5af7fc2f4..03ddaa69d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * SubGHz: Add **CAME TOP44FGN** support in CAME TWEE protocol * SubGHz: Add all 0x0s and all 0xFs KeeLoq MF codes for normal and simple learning * SubGHz: **Fix CAME TWEE repeats count for button click** +* SubGHz: Improve Nice FLO decoding (thx to Roman for raw recordings) * NFC: Add **ISO15693-3 and SLIX write-back support** (PR #984 | by @DoniyorI) * NFC: **Fix "MIR" and other EMV cards crash on Read** (by @Dmitry422) * NFC: Add **Mifare Ultralight C Write Support** (by @haw8411) diff --git a/lib/subghz/protocols/nice_flo.c b/lib/subghz/protocols/nice_flo.c index f7c8fe757..b0acfd1eb 100644 --- a/lib/subghz/protocols/nice_flo.c +++ b/lib/subghz/protocols/nice_flo.c @@ -10,7 +10,7 @@ static const SubGhzBlockConst subghz_protocol_nice_flo_const = { .te_short = 700, .te_long = 1400, - .te_delta = 200, + .te_delta = 250, .min_count_bit_for_found = 12, }; @@ -211,8 +211,8 @@ void subghz_protocol_decoder_nice_flo_feed(void* context, bool level, uint32_t d switch(instance->decoder.parser_step) { case NiceFloDecoderStepReset: if((!level) && (DURATION_DIFF(duration, subghz_protocol_nice_flo_const.te_short * 36) < - subghz_protocol_nice_flo_const.te_delta * 36)) { - //Found header Nice Flo + subghz_protocol_nice_flo_const.te_delta * 29)) { + //Found header Nice Flo (25200us +- 7250us) instance->decoder.parser_step = NiceFloDecoderStepFoundStartBit; } break; @@ -326,8 +326,8 @@ void subghz_protocol_decoder_nice_flo_get_string(void* context, FuriString* outp furi_string_cat_printf( output, "%s %dbit\r\n" - "Key:0x%08lX\r\n" - "Yek:0x%08lX\r\n", + "Key:0x%06lX\r\n" + "Yek:0x%06lX\r\n", instance->generic.protocol_name, instance->generic.data_count_bit, code_found_lo, From 44f025d119df744ea662f6db80b0294721683410 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 25 Apr 2026 04:15:31 +0300 Subject: [PATCH 41/71] upd changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03ddaa69d..ebddaf63c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ * NFC: Fix CLI with NTAG4xx and Type 4 Tag support (by @WillyJL) * OFW PR 4362: NFC: **Fix BusFault** in Write to Initial Card (by @akrylysov) * OFW PR 4369: NFC: Fix stack buffer overflows in MFUL FAST_READ and DESFire file settings parsers (by @qp-x-qp) -* Apps: Build tag (**24apr2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) +* Apps: Build tag (**25apr2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * SubGHz: Fix small logic error in KingGatesStylo4k protocol (found by @X-Stuff) * OFW PR 4364: JS: Address utf8 support when uploading JavaScript application (by @bekindpleaserewind) From f1e1d0131385c029ba17a5dfeb9251083a38f115 Mon Sep 17 00:00:00 2001 From: SkeletonMan03 Date: Sun, 26 Apr 2026 14:11:14 -0500 Subject: [PATCH 42/71] Make toggle more obvious --- applications/services/desktop/views/desktop_view_lock_menu.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/services/desktop/views/desktop_view_lock_menu.c b/applications/services/desktop/views/desktop_view_lock_menu.c index 995a47f95..1dac8fe7a 100644 --- a/applications/services/desktop/views/desktop_view_lock_menu.c +++ b/applications/services/desktop/views/desktop_view_lock_menu.c @@ -65,9 +65,9 @@ void desktop_lock_menu_draw_callback(Canvas* canvas, void* model) { //if(i == DesktopLockMenuIndexLock) { if(i == DesktopLockMenuIndexBt) { if(m->bt_mode) { - str = "Bluetooth Off"; + str = "Turn Bluetooth Off"; } else { - str = "Bluetooth On"; + str = "Turn Bluetooth On"; } } else if(i == DesktopLockMenuIndexStealth) { if(m->stealth_mode) { From ceddafdc690b2a12e1204edf7a37c557f9e63682 Mon Sep 17 00:00:00 2001 From: SkeletonMan03 Date: Sun, 26 Apr 2026 14:51:28 -0500 Subject: [PATCH 43/71] Change power off menu to say "Battery" instead of "Settings" --- .../power_settings_app/scenes/power_settings_scene_power_off.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/settings/power_settings_app/scenes/power_settings_scene_power_off.c b/applications/settings/power_settings_app/scenes/power_settings_scene_power_off.c index b89c2d0b8..eda825d6d 100644 --- a/applications/settings/power_settings_app/scenes/power_settings_scene_power_off.c +++ b/applications/settings/power_settings_app/scenes/power_settings_scene_power_off.c @@ -27,7 +27,7 @@ void power_settings_scene_power_off_on_enter(void* context) { dialog, " I will be\nwaiting for\n you here...", 78, 14, AlignLeft, AlignTop); dialog_ex_set_icon(dialog, 24, 10, &I_dolph_cry_49x54); } - dialog_ex_set_left_button_text(dialog, "Settings"); + dialog_ex_set_left_button_text(dialog, "Battery"); dialog_ex_set_right_button_text(dialog, "Power Off"); dialog_ex_set_result_callback(dialog, power_settings_scene_power_off_dialog_callback); dialog_ex_set_context(dialog, app); From ef1a4495183872b8e934c86e2e35729046cb4c87 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 27 Apr 2026 00:43:06 +0300 Subject: [PATCH 44/71] upd changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebddaf63c..c15357ce8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ * OFW PR 4369: NFC: Fix stack buffer overflows in MFUL FAST_READ and DESFire file settings parsers (by @qp-x-qp) * Apps: Build tag (**25apr2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes +* UI: Wording changes to be more clear (PR #994 | by @SkeletonMan03) * SubGHz: Fix small logic error in KingGatesStylo4k protocol (found by @X-Stuff) * OFW PR 4364: JS: Address utf8 support when uploading JavaScript application (by @bekindpleaserewind) * Display: Extend lcd contrast range to full ST756x 6-bit range (by @ShaTie) (reduced to -10 to +18 to avoid fully unreadable state) From 637258ffbb08801f1a7675b8a81ad2a3c26b17fb Mon Sep 17 00:00:00 2001 From: Roman Belyakovsky Date: Tue, 28 Apr 2026 21:03:51 +0300 Subject: [PATCH 45/71] Changing zoom global mic shortcut to avoid conflict with terminal --- applications/system/hid_app/views/hid_ptt.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/system/hid_app/views/hid_ptt.c b/applications/system/hid_app/views/hid_ptt.c index 8a28cd292..e9b1849a0 100644 --- a/applications/system/hid_app/views/hid_ptt.c +++ b/applications/system/hid_app/views/hid_ptt.c @@ -128,9 +128,9 @@ static void hid_ptt_trigger_hand_zoom(HidPushToTalk* hid_ptt) { // zoom global macos static void hid_ptt_trigger_mute_macos_zoom_global(HidPushToTalk* hid_ptt) { hid_hal_keyboard_press( - hid_ptt->hid, KEY_MOD_LEFT_GUI | KEY_MOD_RIGHT_ALT | KEY_MOD_LEFT_SHIFT | HID_KEYBOARD_M); + hid_ptt->hid, KEY_MOD_LEFT_GUI | KEY_MOD_RIGHT_ALT | KEY_MOD_LEFT_SHIFT | HID_KEYBOARD_N); hid_hal_keyboard_release( - hid_ptt->hid, KEY_MOD_LEFT_GUI | KEY_MOD_RIGHT_ALT | KEY_MOD_LEFT_SHIFT | HID_KEYBOARD_M); + hid_ptt->hid, KEY_MOD_LEFT_GUI | KEY_MOD_RIGHT_ALT | KEY_MOD_LEFT_SHIFT | HID_KEYBOARD_N); } static void hid_ptt_trigger_camera_macos_zoom_global(HidPushToTalk* hid_ptt) { From fe66188541eb56a1a6d283b52c0e1d35413e7b07 Mon Sep 17 00:00:00 2001 From: Roman Belyakovsky Date: Tue, 28 Apr 2026 21:38:23 +0300 Subject: [PATCH 46/71] Adding status bar --- applications/system/hid_app/views/hid_ptt.c | 115 +++++++++++++++----- 1 file changed, 85 insertions(+), 30 deletions(-) diff --git a/applications/system/hid_app/views/hid_ptt.c b/applications/system/hid_app/views/hid_ptt.c index e9b1849a0..b688de89c 100644 --- a/applications/system/hid_app/views/hid_ptt.c +++ b/applications/system/hid_app/views/hid_ptt.c @@ -1,8 +1,11 @@ #include "hid_ptt.h" #include "hid_ptt_menu.h" #include +#include #include #include +#include +#include #include "../hid.h" #include "../views.h" @@ -673,43 +676,94 @@ static void hid_ptt_draw_text_centered(Canvas* canvas, uint8_t y, FuriString* st furi_string_free(disp_str); } +static void hid_ptt_draw_status_bar(Canvas* canvas, bool connected) { + char time_str[16]; + DateTime dt; + furi_hal_rtc_get_datetime(&dt); + + uint8_t hour = dt.hour; + if(locale_get_time_format() == LocaleTimeFormat12h) { + if(hour > 12) { + hour -= 12; + } + if(hour == 0) { + hour = 12; + } + } + snprintf(time_str, sizeof(time_str), "%02u:%02u", hour, dt.minute); + + uint8_t battery = furi_hal_power_get_pct(); + if(battery > 100) { + battery = 100; + } + + canvas_set_color(canvas, ColorWhite); + canvas_draw_box(canvas, 0, 0, 64, 11); + canvas_set_color(canvas, ColorBlack); + canvas_draw_rframe(canvas, 0, 0, 64, 11, 1); + canvas_draw_line(canvas, 1, 9, 62, 9); + + if(connected) { + canvas_draw_icon(canvas, 1, 0, &I_Ble_connected_15x15); + } else { + canvas_draw_icon(canvas, 1, 0, &I_Ble_disconnected_15x15); + } + + const uint8_t battery_x = 47; + const uint8_t battery_y = 2; + const uint8_t battery_w = 13; + const uint8_t battery_h = 6; + canvas_draw_frame(canvas, battery_x, battery_y, battery_w, battery_h); + canvas_draw_box(canvas, battery_x + battery_w, battery_y + 2, 1, 2); + canvas_draw_box(canvas, battery_x + 1, battery_y + 1, ((battery_w - 2) * battery) / 100, 4); + + canvas_set_font(canvas, FontPrimary); + canvas_draw_str_aligned(canvas, 31, 8, AlignCenter, AlignBottom, time_str); +} + static void hid_ptt_draw_callback(Canvas* canvas, void* context) { furi_assert(context); HidPushToTalkModel* model = context; - // Header - canvas_set_font(canvas, FontPrimary); -#ifdef HID_TRANSPORT_BLE - if(model->connected) { - canvas_draw_icon(canvas, 0, 0, &I_Ble_connected_15x15); - } else { - canvas_draw_icon(canvas, 0, 0, &I_Ble_disconnected_15x15); - } -#endif - - // OS and App labels - canvas_set_font(canvas, FontSecondary); - hid_ptt_draw_text_centered(canvas, 73, model->app); - hid_ptt_draw_text_centered(canvas, 84, model->os); - - // Help label - canvas_draw_icon(canvas, 0, 88, &I_Help_top_64x17); - canvas_draw_line(canvas, 4, 105, 4, 114); - canvas_draw_line(canvas, 63, 105, 63, 114); - canvas_draw_icon(canvas, 7, 107, &I_Hold_15x5); - canvas_draw_icon(canvas, 24, 105, &I_BtnLeft_9x9); - canvas_draw_icon(canvas, 34, 108, &I_for_help_27x5); - canvas_draw_icon(canvas, 0, 115, &I_Help_exit_64x9); - canvas_draw_icon(canvas, 24, 115, &I_BtnBackV_9x9); + const uint8_t top_offset = 11; + const uint8_t helper_top_y = 92; const uint8_t x_1 = 0; const uint8_t x_2 = x_1 + 19 + 4; const uint8_t x_3 = x_1 + 19 * 2 + 8; - const uint8_t y_1 = 3; + const uint8_t y_1 = top_offset + 3; const uint8_t y_2 = y_1 + 19; const uint8_t y_3 = y_2 + 19; + const uint8_t controls_bottom_y = y_3 + 18; + const uint8_t labels_center_y = (controls_bottom_y + helper_top_y) / 2; + const uint8_t app_label_y = labels_center_y - 5; + const uint8_t os_label_y = app_label_y + 11; + + // Header + canvas_set_font(canvas, FontPrimary); +#ifdef HID_TRANSPORT_BLE + hid_ptt_draw_status_bar(canvas, model->connected); +#else + hid_ptt_draw_status_bar(canvas, false); +#endif + + // OS and App labels + canvas_set_font(canvas, FontSecondary); + hid_ptt_draw_text_centered(canvas, app_label_y, model->app); + hid_ptt_draw_text_centered(canvas, os_label_y, model->os); + + // Help label + canvas_draw_icon(canvas, 0, helper_top_y, &I_Help_top_64x17); + canvas_draw_line(canvas, 4, 109, 4, 118); + canvas_draw_line(canvas, 63, 109, 63, 118); + canvas_draw_icon(canvas, 7, 111, &I_Hold_15x5); + canvas_draw_icon(canvas, 24, 109, &I_BtnLeft_9x9); + canvas_draw_icon(canvas, 34, 112, &I_for_help_27x5); + canvas_draw_icon(canvas, 0, 119, &I_Help_exit_64x9); + canvas_draw_icon(canvas, 24, 119, &I_BtnBackV_9x9); + // Up canvas_draw_icon(canvas, x_2, y_1, &I_Button_18x18); if(model->up_pressed) { @@ -756,23 +810,24 @@ static void hid_ptt_draw_callback(Canvas* canvas, void* context) { // Back / Mic const uint8_t x_mic = x_3; - canvas_draw_icon(canvas, x_mic, 0, &I_RoundButtonUnpressed_16x16); + const uint8_t y_mic = top_offset; + canvas_draw_icon(canvas, x_mic, y_mic, &I_RoundButtonUnpressed_16x16); if(!(!model->muted || (model->ptt_pressed))) { // show muted if(model->mic_pressed) { // canvas_draw_icon(canvas, x_mic + 1, 0, &I_MicrophonePressedCrossed_15x15); - canvas_draw_icon(canvas, x_mic, 0, &I_MicrophonePressedCrossedBtn_16x16); + canvas_draw_icon(canvas, x_mic, y_mic, &I_MicrophonePressedCrossedBtn_16x16); } else { - canvas_draw_icon(canvas, x_mic, 0, &I_MicrophoneCrossed_16x16); + canvas_draw_icon(canvas, x_mic, y_mic, &I_MicrophoneCrossed_16x16); } } else { // show unmuted if(model->mic_pressed) { // canvas_draw_icon(canvas, x_mic + 1, 0, &I_MicrophonePressed_15x15); - canvas_draw_icon(canvas, x_mic, 0, &I_MicrophonePressedBtn_16x16); + canvas_draw_icon(canvas, x_mic, y_mic, &I_MicrophonePressedBtn_16x16); } else { - canvas_draw_icon(canvas, x_mic + 5, 2, &I_Mic_7x11); + canvas_draw_icon(canvas, x_mic + 5, y_mic + 2, &I_Mic_7x11); } } From 74c69f74eabcd658f05771f576543bcb96155af8 Mon Sep 17 00:00:00 2001 From: Roman Belyakovsky Date: Tue, 28 Apr 2026 21:50:59 +0300 Subject: [PATCH 47/71] Fixing text position --- applications/system/hid_app/views/hid_ptt.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/applications/system/hid_app/views/hid_ptt.c b/applications/system/hid_app/views/hid_ptt.c index b688de89c..0c9ed837b 100644 --- a/applications/system/hid_app/views/hid_ptt.c +++ b/applications/system/hid_app/views/hid_ptt.c @@ -726,6 +726,7 @@ static void hid_ptt_draw_callback(Canvas* canvas, void* context) { HidPushToTalkModel* model = context; const uint8_t top_offset = 11; + const uint8_t status_bar_bottom_y = top_offset; const uint8_t helper_top_y = 92; const uint8_t x_1 = 0; @@ -738,7 +739,7 @@ static void hid_ptt_draw_callback(Canvas* canvas, void* context) { const uint8_t controls_bottom_y = y_3 + 18; const uint8_t labels_center_y = (controls_bottom_y + helper_top_y) / 2; - const uint8_t app_label_y = labels_center_y - 5; + const uint8_t app_label_y = labels_center_y - 1; const uint8_t os_label_y = app_label_y + 11; // Header @@ -810,7 +811,7 @@ static void hid_ptt_draw_callback(Canvas* canvas, void* context) { // Back / Mic const uint8_t x_mic = x_3; - const uint8_t y_mic = top_offset; + const uint8_t y_mic = status_bar_bottom_y + ((y_2 - status_bar_bottom_y - 16) / 2); canvas_draw_icon(canvas, x_mic, y_mic, &I_RoundButtonUnpressed_16x16); if(!(!model->muted || (model->ptt_pressed))) { From a9438319d60d11b852eb595961d3b0c26b0938c2 Mon Sep 17 00:00:00 2001 From: Roman Belyakovsky Date: Tue, 28 Apr 2026 22:08:10 +0300 Subject: [PATCH 48/71] Moving BT icon to status bar --- applications/system/hid_app/views/hid_ptt.c | 24 +++++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/applications/system/hid_app/views/hid_ptt.c b/applications/system/hid_app/views/hid_ptt.c index 0c9ed837b..f4f476e31 100644 --- a/applications/system/hid_app/views/hid_ptt.c +++ b/applications/system/hid_app/views/hid_ptt.c @@ -676,7 +676,19 @@ static void hid_ptt_draw_text_centered(Canvas* canvas, uint8_t y, FuriString* st furi_string_free(disp_str); } -static void hid_ptt_draw_status_bar(Canvas* canvas, bool connected) { +static void hid_ptt_draw_bt_connected(Canvas* canvas, uint8_t x, uint8_t y) { + // Home-like compact Bluetooth glyph with activity line and end dot. + canvas_draw_line(canvas, x + 1, y + 0, x + 1, y + 7); + canvas_draw_line(canvas, x + 1, y + 4, x + 4, y + 1); + canvas_draw_line(canvas, x + 1, y + 4, x + 4, y + 7); + canvas_draw_line(canvas, x + 1, y + 0, x + 4, y + 2); + canvas_draw_line(canvas, x + 1, y + 7, x + 4, y + 5); + + canvas_draw_line(canvas, x + 6, y + 4, x + 12, y + 4); + canvas_draw_disc(canvas, x + 14, y + 4, 1); +} + +static void hid_ptt_draw_status_bar(Canvas* canvas, bool show_bt, bool connected) { char time_str[16]; DateTime dt; furi_hal_rtc_get_datetime(&dt); @@ -703,10 +715,8 @@ static void hid_ptt_draw_status_bar(Canvas* canvas, bool connected) { canvas_draw_rframe(canvas, 0, 0, 64, 11, 1); canvas_draw_line(canvas, 1, 9, 62, 9); - if(connected) { - canvas_draw_icon(canvas, 1, 0, &I_Ble_connected_15x15); - } else { - canvas_draw_icon(canvas, 1, 0, &I_Ble_disconnected_15x15); + if(show_bt && connected) { + hid_ptt_draw_bt_connected(canvas, 3, 1); } const uint8_t battery_x = 47; @@ -745,9 +755,9 @@ static void hid_ptt_draw_callback(Canvas* canvas, void* context) { // Header canvas_set_font(canvas, FontPrimary); #ifdef HID_TRANSPORT_BLE - hid_ptt_draw_status_bar(canvas, model->connected); + hid_ptt_draw_status_bar(canvas, true, model->connected); #else - hid_ptt_draw_status_bar(canvas, false); + hid_ptt_draw_status_bar(canvas, false, false); #endif // OS and App labels From 3457a32621ef05aa5edd961fed789cd7a9423690 Mon Sep 17 00:00:00 2001 From: Roman Belyakovsky Date: Tue, 28 Apr 2026 22:23:53 +0300 Subject: [PATCH 49/71] Fixing bt status --- applications/system/hid_app/views/hid_ptt.c | 35 ++++++++++++++------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/applications/system/hid_app/views/hid_ptt.c b/applications/system/hid_app/views/hid_ptt.c index f4f476e31..999888641 100644 --- a/applications/system/hid_app/views/hid_ptt.c +++ b/applications/system/hid_app/views/hid_ptt.c @@ -679,15 +679,24 @@ static void hid_ptt_draw_text_centered(Canvas* canvas, uint8_t y, FuriString* st static void hid_ptt_draw_bt_connected(Canvas* canvas, uint8_t x, uint8_t y) { // Home-like compact Bluetooth glyph with activity line and end dot. canvas_draw_line(canvas, x + 1, y + 0, x + 1, y + 7); - canvas_draw_line(canvas, x + 1, y + 4, x + 4, y + 1); - canvas_draw_line(canvas, x + 1, y + 4, x + 4, y + 7); canvas_draw_line(canvas, x + 1, y + 0, x + 4, y + 2); canvas_draw_line(canvas, x + 1, y + 7, x + 4, y + 5); + canvas_draw_line(canvas, x + 1, y + 4, x + 4, y + 1); + canvas_draw_line(canvas, x + 1, y + 4, x + 4, y + 7); - canvas_draw_line(canvas, x + 6, y + 4, x + 12, y + 4); + canvas_draw_line(canvas, x + 7, y + 4, x + 12, y + 4); canvas_draw_disc(canvas, x + 14, y + 4, 1); } +static void hid_ptt_draw_bt_idle(Canvas* canvas, uint8_t x, uint8_t y) { + // Home-like disconnected icon: only Bluetooth glyph. + canvas_draw_line(canvas, x + 1, y + 0, x + 1, y + 7); + canvas_draw_line(canvas, x + 1, y + 0, x + 4, y + 2); + canvas_draw_line(canvas, x + 1, y + 7, x + 4, y + 5); + canvas_draw_line(canvas, x + 1, y + 4, x + 4, y + 1); + canvas_draw_line(canvas, x + 1, y + 4, x + 4, y + 7); +} + static void hid_ptt_draw_status_bar(Canvas* canvas, bool show_bt, bool connected) { char time_str[16]; DateTime dt; @@ -710,17 +719,21 @@ static void hid_ptt_draw_status_bar(Canvas* canvas, bool show_bt, bool connected } canvas_set_color(canvas, ColorWhite); - canvas_draw_box(canvas, 0, 0, 64, 11); + canvas_draw_box(canvas, 0, 0, 64, 13); canvas_set_color(canvas, ColorBlack); - canvas_draw_rframe(canvas, 0, 0, 64, 11, 1); - canvas_draw_line(canvas, 1, 9, 62, 9); + canvas_draw_rframe(canvas, 0, 0, 64, 13, 1); + canvas_draw_line(canvas, 1, 11, 62, 11); - if(show_bt && connected) { - hid_ptt_draw_bt_connected(canvas, 3, 1); + if(show_bt) { + if(connected) { + hid_ptt_draw_bt_connected(canvas, 3, 2); + } else { + hid_ptt_draw_bt_idle(canvas, 3, 2); + } } const uint8_t battery_x = 47; - const uint8_t battery_y = 2; + const uint8_t battery_y = 3; const uint8_t battery_w = 13; const uint8_t battery_h = 6; canvas_draw_frame(canvas, battery_x, battery_y, battery_w, battery_h); @@ -728,14 +741,14 @@ static void hid_ptt_draw_status_bar(Canvas* canvas, bool show_bt, bool connected canvas_draw_box(canvas, battery_x + 1, battery_y + 1, ((battery_w - 2) * battery) / 100, 4); canvas_set_font(canvas, FontPrimary); - canvas_draw_str_aligned(canvas, 31, 8, AlignCenter, AlignBottom, time_str); + canvas_draw_str_aligned(canvas, 31, 9, AlignCenter, AlignBottom, time_str); } static void hid_ptt_draw_callback(Canvas* canvas, void* context) { furi_assert(context); HidPushToTalkModel* model = context; - const uint8_t top_offset = 11; + const uint8_t top_offset = 13; const uint8_t status_bar_bottom_y = top_offset; const uint8_t helper_top_y = 92; From 0f6a6d1a52659d0eec9f2f9b258a83255745110c Mon Sep 17 00:00:00 2001 From: Roman Belyakovsky Date: Tue, 28 Apr 2026 22:27:41 +0300 Subject: [PATCH 50/71] Fixing clock in status bar --- applications/system/hid_app/views/hid_ptt.c | 25 ++++++++++++--------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/applications/system/hid_app/views/hid_ptt.c b/applications/system/hid_app/views/hid_ptt.c index 999888641..b465d7848 100644 --- a/applications/system/hid_app/views/hid_ptt.c +++ b/applications/system/hid_app/views/hid_ptt.c @@ -676,25 +676,28 @@ static void hid_ptt_draw_text_centered(Canvas* canvas, uint8_t y, FuriString* st furi_string_free(disp_str); } -static void hid_ptt_draw_bt_connected(Canvas* canvas, uint8_t x, uint8_t y) { - // Home-like compact Bluetooth glyph with activity line and end dot. +static void hid_ptt_draw_bt_glyph(Canvas* canvas, uint8_t x, uint8_t y) { + // Compact 5x8 Bluetooth rune. canvas_draw_line(canvas, x + 1, y + 0, x + 1, y + 7); canvas_draw_line(canvas, x + 1, y + 0, x + 4, y + 2); canvas_draw_line(canvas, x + 1, y + 7, x + 4, y + 5); canvas_draw_line(canvas, x + 1, y + 4, x + 4, y + 1); canvas_draw_line(canvas, x + 1, y + 4, x + 4, y + 7); +} - canvas_draw_line(canvas, x + 7, y + 4, x + 12, y + 4); - canvas_draw_disc(canvas, x + 14, y + 4, 1); +static void hid_ptt_draw_bt_connected(Canvas* canvas, uint8_t x, uint8_t y) { + // Connected icon: rune + activity pixels/line + end circle. + hid_ptt_draw_bt_glyph(canvas, x, y); + + canvas_draw_dot(canvas, x + 6, y + 4); + canvas_draw_dot(canvas, x + 8, y + 4); + canvas_draw_line(canvas, x + 10, y + 4, x + 12, y + 4); + canvas_draw_circle(canvas, x + 14, y + 4, 1); } static void hid_ptt_draw_bt_idle(Canvas* canvas, uint8_t x, uint8_t y) { - // Home-like disconnected icon: only Bluetooth glyph. - canvas_draw_line(canvas, x + 1, y + 0, x + 1, y + 7); - canvas_draw_line(canvas, x + 1, y + 0, x + 4, y + 2); - canvas_draw_line(canvas, x + 1, y + 7, x + 4, y + 5); - canvas_draw_line(canvas, x + 1, y + 4, x + 4, y + 1); - canvas_draw_line(canvas, x + 1, y + 4, x + 4, y + 7); + // Disconnected icon: only Bluetooth rune. + hid_ptt_draw_bt_glyph(canvas, x, y); } static void hid_ptt_draw_status_bar(Canvas* canvas, bool show_bt, bool connected) { @@ -741,7 +744,7 @@ static void hid_ptt_draw_status_bar(Canvas* canvas, bool show_bt, bool connected canvas_draw_box(canvas, battery_x + 1, battery_y + 1, ((battery_w - 2) * battery) / 100, 4); canvas_set_font(canvas, FontPrimary); - canvas_draw_str_aligned(canvas, 31, 9, AlignCenter, AlignBottom, time_str); + canvas_draw_str_aligned(canvas, 31, 10, AlignCenter, AlignBottom, time_str); } static void hid_ptt_draw_callback(Canvas* canvas, void* context) { From a8de70b08d5ecbc8705173e6fd4dfe139444836c Mon Sep 17 00:00:00 2001 From: Roman Belyakovsky Date: Tue, 28 Apr 2026 22:40:02 +0300 Subject: [PATCH 51/71] Fixing bt icon and crash on bt disconnect --- applications/system/hid_app/views/hid_ptt.c | 40 ++++++++------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/applications/system/hid_app/views/hid_ptt.c b/applications/system/hid_app/views/hid_ptt.c index b465d7848..92e2afc9c 100644 --- a/applications/system/hid_app/views/hid_ptt.c +++ b/applications/system/hid_app/views/hid_ptt.c @@ -13,6 +13,18 @@ #define TAG "HidPushToTalk" +// Exact home status-bar Bluetooth icon pixels (from assets/icons/StatusBar). +// Bitmap format for canvas_draw_bitmap(): row-major, 1-bit. +// Full compressed frame data including leading 0x00 heatshrink marker, +// copied verbatim from build/f7-firmware-D/assets/compiled/assets_icons.c +static const uint8_t hid_ptt_bluetooth_connected_16x8_bits[] = { + 0x00, 0x04, 0x00, 0x0d, 0x00, 0x16, 0x60, 0x4c, + 0x97, 0x4c, 0x97, 0x16, 0x60, 0x0d, 0x00, 0x04, 0x00, +}; +static const uint8_t hid_ptt_bluetooth_idle_5x8_bits[] = { + 0x00, 0x04, 0x0d, 0x16, 0x0c, 0x0c, 0x16, 0x0d, 0x04, +}; + struct HidPushToTalk { View* view; Hid* hid; @@ -676,30 +688,6 @@ static void hid_ptt_draw_text_centered(Canvas* canvas, uint8_t y, FuriString* st furi_string_free(disp_str); } -static void hid_ptt_draw_bt_glyph(Canvas* canvas, uint8_t x, uint8_t y) { - // Compact 5x8 Bluetooth rune. - canvas_draw_line(canvas, x + 1, y + 0, x + 1, y + 7); - canvas_draw_line(canvas, x + 1, y + 0, x + 4, y + 2); - canvas_draw_line(canvas, x + 1, y + 7, x + 4, y + 5); - canvas_draw_line(canvas, x + 1, y + 4, x + 4, y + 1); - canvas_draw_line(canvas, x + 1, y + 4, x + 4, y + 7); -} - -static void hid_ptt_draw_bt_connected(Canvas* canvas, uint8_t x, uint8_t y) { - // Connected icon: rune + activity pixels/line + end circle. - hid_ptt_draw_bt_glyph(canvas, x, y); - - canvas_draw_dot(canvas, x + 6, y + 4); - canvas_draw_dot(canvas, x + 8, y + 4); - canvas_draw_line(canvas, x + 10, y + 4, x + 12, y + 4); - canvas_draw_circle(canvas, x + 14, y + 4, 1); -} - -static void hid_ptt_draw_bt_idle(Canvas* canvas, uint8_t x, uint8_t y) { - // Disconnected icon: only Bluetooth rune. - hid_ptt_draw_bt_glyph(canvas, x, y); -} - static void hid_ptt_draw_status_bar(Canvas* canvas, bool show_bt, bool connected) { char time_str[16]; DateTime dt; @@ -729,9 +717,9 @@ static void hid_ptt_draw_status_bar(Canvas* canvas, bool show_bt, bool connected if(show_bt) { if(connected) { - hid_ptt_draw_bt_connected(canvas, 3, 2); + canvas_draw_bitmap(canvas, 3, 2, 16, 8, hid_ptt_bluetooth_connected_16x8_bits); } else { - hid_ptt_draw_bt_idle(canvas, 3, 2); + canvas_draw_bitmap(canvas, 3, 2, 5, 8, hid_ptt_bluetooth_idle_5x8_bits); } } From 3a6d7173f4909b9fc8f0bde024dcac3c1657bf1d Mon Sep 17 00:00:00 2001 From: Roman Belyakovsky Date: Tue, 28 Apr 2026 22:45:13 +0300 Subject: [PATCH 52/71] Fixing overlapping icons in the statusbar --- applications/system/hid_app/views/hid_ptt.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/applications/system/hid_app/views/hid_ptt.c b/applications/system/hid_app/views/hid_ptt.c index 92e2afc9c..a72a2420a 100644 --- a/applications/system/hid_app/views/hid_ptt.c +++ b/applications/system/hid_app/views/hid_ptt.c @@ -717,13 +717,13 @@ static void hid_ptt_draw_status_bar(Canvas* canvas, bool show_bt, bool connected if(show_bt) { if(connected) { - canvas_draw_bitmap(canvas, 3, 2, 16, 8, hid_ptt_bluetooth_connected_16x8_bits); + canvas_draw_bitmap(canvas, 2, 2, 16, 8, hid_ptt_bluetooth_connected_16x8_bits); } else { - canvas_draw_bitmap(canvas, 3, 2, 5, 8, hid_ptt_bluetooth_idle_5x8_bits); + canvas_draw_bitmap(canvas, 2, 2, 5, 8, hid_ptt_bluetooth_idle_5x8_bits); } } - const uint8_t battery_x = 47; + const uint8_t battery_x = 48; const uint8_t battery_y = 3; const uint8_t battery_w = 13; const uint8_t battery_h = 6; @@ -732,7 +732,7 @@ static void hid_ptt_draw_status_bar(Canvas* canvas, bool show_bt, bool connected canvas_draw_box(canvas, battery_x + 1, battery_y + 1, ((battery_w - 2) * battery) / 100, 4); canvas_set_font(canvas, FontPrimary); - canvas_draw_str_aligned(canvas, 31, 10, AlignCenter, AlignBottom, time_str); + canvas_draw_str_aligned(canvas, 33, 10, AlignCenter, AlignBottom, time_str); } static void hid_ptt_draw_callback(Canvas* canvas, void* context) { From 02d8a12924f5bfa0145412cd41d386b2ceffe90e Mon Sep 17 00:00:00 2001 From: Roman Belyakovsky Date: Tue, 28 Apr 2026 23:03:16 +0300 Subject: [PATCH 53/71] Moving help to app selection menu --- applications/system/hid_app/views/hid_ptt.c | 154 ++++++++++-------- .../system/hid_app/views/hid_ptt_menu.c | 40 ++++- .../system/hid_app/views/hid_ptt_menu.h | 12 ++ 3 files changed, 133 insertions(+), 73 deletions(-) diff --git a/applications/system/hid_app/views/hid_ptt.c b/applications/system/hid_app/views/hid_ptt.c index a72a2420a..27b1d211f 100644 --- a/applications/system/hid_app/views/hid_ptt.c +++ b/applications/system/hid_app/views/hid_ptt.c @@ -402,6 +402,83 @@ static void hid_ptt_trigger_mute_linux_gather(HidPushToTalk* hid_ptt) { hid_ptt->hid, KEY_MOD_LEFT_CTRL | KEY_MOD_LEFT_SHIFT | HID_KEYBOARD_A); } + static void hid_ptt_populate_help(HidPushToTalk* hid_ptt, uint32_t appIndex) { + widget_reset(hid_ptt->help); + char* app_specific_help = ""; + switch(appIndex) { + case HidPushToTalkAppIndexGoogleMeet: + app_specific_help = + "Google Meet:\n" + "This feature is off by default in your audio settings " + "and may not work for Windows users who use their screen " + "reader. In this situation, the spacebar performs a different action.\n\n"; + break; + case HidPushToTalkAppIndexGoogleMeetGlobal: + app_specific_help = "Google Meet (Global):\n" + "1. Install \"Google Meet - Global Shortcuts\" extension.\n" + "2. Open chrome://extensions/shortcuts.\n" + "3. Set 'Toggle microphone' to Cmd+Ctrl+7 and enable Global.\n" + "4. Set 'Toggle camera' to Cmd+Ctrl+8 and enable Global.\n" + "5. Set 'Raise hand' to Cmd+Ctrl+9 and enable Global.\n\n"; + break; + case HidPushToTalkAppIndexDiscord: + app_specific_help = + "Discord:\n" + "1. Under App Settings, click Voice & Video. Under Input Mode, " + "check the box next to Push to Talk.\n" + "2. Scroll down to SHORTCUT, click Record Keybinder.\n" + "3. Press PTT in the app to bind it." + "4. Go to Keybinds and assign mute button.\n\n"; + break; + case HidPushToTalkAppIndexTeamSpeak: + app_specific_help = "TeamSpeak:\n" + "To make keys working bind them in TeamSpeak settings.\n\n"; + break; + case HidPushToTalkAppIndexTeams: + app_specific_help = + "Teams:\n" + "Go to Settings > Privacy. Make sure Keyboard shortcut to unmute is toggled on.\n\n"; + break; + case HidPushToTalkAppIndexZoomGlobal: + app_specific_help = "Zoom (Global):\n" + "1. Go to Settings > Keyboard Shortcuts.\n" + "2. Find the 'Mute/Unmute' shortcut and click 'Edit'.\n" + "3. Press the Mute button in the app to bind it.\n" + "4. Check global checkbox.\n" + "5. Repeat for video and hand shortcuts.\n\n"; + break; + } + FuriString* msg = furi_string_alloc(); + furi_string_cat_printf( + msg, + "%sGeneral:\n" + "To operate properly flipper microphone " + "status must be in sync with your computer.\n" + "Hold > to change mic status.\n" + "Long-press OK in menu to open this help.\n" + "Press BACK to switch mic on/off.\n" + "Hold 'o' for PTT mode (mic will be off once you release 'o')\n" + "Hold BACK to exit.", + app_specific_help); + widget_add_text_scroll_element(hid_ptt->help, 0, 0, 128, 64, furi_string_get_cstr(msg)); + furi_string_free(msg); + } + + static void hid_ptt_menu_help_callback( + void* context, + uint32_t osIndex, + FuriString* osLabel, + uint32_t appIndex, + FuriString* appLabel) { + UNUSED(osIndex); + UNUSED(osLabel); + UNUSED(appLabel); + furi_assert(context); + HidPushToTalk* hid_ptt = context; + hid_ptt_populate_help(hid_ptt, appIndex); + view_dispatcher_switch_to_view(hid_ptt->hid->view_dispatcher, HidViewPushToTalkHelp); + } + static void hid_ptt_menu_callback( void* context, uint32_t osIndex, @@ -609,67 +686,9 @@ static void hid_ptt_menu_callback( } } - char* app_specific_help = ""; - switch(appIndex) { - case HidPushToTalkAppIndexGoogleMeet: - app_specific_help = - "Google Meet:\n" - "This feature is off by default in your audio settings " - "and may not work for Windows users who use their screen " - "reader. In this situation, the spacebar performs a different action.\n\n"; - break; - case HidPushToTalkAppIndexGoogleMeetGlobal: - app_specific_help = "Google Meet (Global):\n" - "1. Install \"Google Meet - Global Shortcuts\" extension.\n" - "2. Open chrome://extensions/shortcuts.\n" - "3. Set 'Toggle microphone' to Cmd+Ctrl+7 and enable Global.\n" - "4. Set 'Toggle camera' to Cmd+Ctrl+8 and enable Global.\n" - "5. Set 'Raise hand' to Cmd+Ctrl+9 and enable Global.\n\n"; - break; - case HidPushToTalkAppIndexDiscord: - app_specific_help = - "Discord:\n" - "1. Under App Settings, click Voice & Video. Under Input Mode, " - "check the box next to Push to Talk.\n" - "2. Scroll down to SHORTCUT, click Record Keybinder.\n" - "3. Press PTT in the app to bind it." - "4. Go to Keybinds and assign mute button.\n\n"; - break; - case HidPushToTalkAppIndexTeamSpeak: - app_specific_help = "TeamSpeak:\n" - "To make keys working bind them in TeamSpeak settings.\n\n"; - break; - case HidPushToTalkAppIndexTeams: - app_specific_help = - "Teams:\n" - "Go to Settings > Privacy. Make sure Keyboard shortcut to unmute is toggled on.\n\n"; - break; - case HidPushToTalkAppIndexZoomGlobal: - app_specific_help = "Zoom (Global):\n" - "1. Go to Settings > Keyboard Shortcuts.\n" - "2. Find the 'Mute/Unmute' shortcut and click 'Edit'.\n" - "3. Press the Mute button in the app to bind it.\n" - "4. Check global checkbox.\n" - "5. Repeat for video and hand shortcuts.\n\n"; - } - - FuriString* msg = furi_string_alloc(); - furi_string_cat_printf( - msg, - "%sGeneral:\n" - "To operate properly flipper microphone " - "status must be in sync with your computer.\n" - "Hold > to change mic status.\n" - "Hold < to open this help.\n" - "Press BACK to switch mic on/off.\n" - "Hold 'o' for PTT mode (mic will be off once you release 'o')\n" - "Hold BACK to exit.", - app_specific_help); - widget_add_text_scroll_element( - hid_ptt->help, 0, 0, 128, 64, furi_string_get_cstr(msg)); - furi_string_free(msg); }, true); + hid_ptt_populate_help(hid_ptt, appIndex); view_dispatcher_switch_to_view(hid_ptt->hid->view_dispatcher, HidViewPushToTalk); } @@ -771,11 +790,6 @@ static void hid_ptt_draw_callback(Canvas* canvas, void* context) { // Help label canvas_draw_icon(canvas, 0, helper_top_y, &I_Help_top_64x17); - canvas_draw_line(canvas, 4, 109, 4, 118); - canvas_draw_line(canvas, 63, 109, 63, 118); - canvas_draw_icon(canvas, 7, 111, &I_Hold_15x5); - canvas_draw_icon(canvas, 24, 109, &I_BtnLeft_9x9); - canvas_draw_icon(canvas, 34, 112, &I_for_help_27x5); canvas_draw_icon(canvas, 0, 119, &I_Help_exit_64x9); canvas_draw_icon(canvas, 24, 119, &I_BtnBackV_9x9); @@ -935,11 +949,6 @@ static void hid_ptt_process(HidPushToTalk* hid_ptt, InputEvent* event) { } else if(event->type == InputTypeLong && event->key == InputKeyRight) { model->muted = !model->muted; notification_message(hid_ptt->hid->notifications, &sequence_single_vibro); - } else if(event->type == InputTypeLong && event->key == InputKeyLeft) { - notification_message(hid_ptt->hid->notifications, &sequence_single_vibro); - model->left_pressed = false; - view_dispatcher_switch_to_view( - hid_ptt->hid->view_dispatcher, HidViewPushToTalkHelp); } //LED if(!model->muted || (model->ptt_pressed)) { @@ -971,9 +980,9 @@ View* hid_ptt_get_view(HidPushToTalk* hid_ptt) { return hid_ptt->view; } -static uint32_t hid_ptt_view(void* context) { +static uint32_t hid_ptt_menu_view(void* context) { UNUSED(context); - return HidViewPushToTalk; + return HidViewPushToTalkMenu; } HidPushToTalk* hid_ptt_alloc(Hid* hid) { @@ -1204,9 +1213,10 @@ HidPushToTalk* hid_ptt_alloc(Hid* hid) { hid_ptt); hid_ptt->help = widget_alloc(); - view_set_previous_callback(widget_get_view(hid_ptt->help), hid_ptt_view); + view_set_previous_callback(widget_get_view(hid_ptt->help), hid_ptt_menu_view); view_dispatcher_add_view( hid->view_dispatcher, HidViewPushToTalkHelp, widget_get_view(hid_ptt->help)); + ptt_menu_set_long_ok_callback(hid->hid_ptt_menu, hid_ptt_menu_help_callback, hid_ptt); return hid_ptt; } diff --git a/applications/system/hid_app/views/hid_ptt_menu.c b/applications/system/hid_app/views/hid_ptt_menu.c index 074c85ba7..439ca4732 100644 --- a/applications/system/hid_app/views/hid_ptt_menu.c +++ b/applications/system/hid_app/views/hid_ptt_menu.c @@ -10,6 +10,8 @@ struct HidPushToTalkMenu { View* view; Hid* hid; + PushToTalkMenuLongOkCallback long_ok_callback; + void* long_ok_callback_context; }; typedef struct { @@ -338,6 +340,39 @@ void ptt_menu_process_ok(HidPushToTalkMenu* hid_ptt_menu) { } } + void ptt_menu_process_long_ok(HidPushToTalkMenu* hid_ptt_menu) { + PushToTalkMenuList* list = NULL; + PushToTalkMenuItem* item = NULL; + with_view_model( + hid_ptt_menu->view, + HidPushToTalkMenuModel * model, + { + list = &model->lists[model->list_position]; + const size_t items_size = PushToTalkMenuItemArray_size(list->items); + if(model->position < items_size) { + item = PushToTalkMenuItemArray_get(list->items, model->position); + } + }, + false); + if(item && list && hid_ptt_menu->long_ok_callback) { + hid_ptt_menu->long_ok_callback( + hid_ptt_menu->long_ok_callback_context, + list->index, + list->label, + item->index, + item->label); + } + } + + void ptt_menu_set_long_ok_callback( + HidPushToTalkMenu* hid_ptt_menu, + PushToTalkMenuLongOkCallback callback, + void* callback_context) { + furi_assert(hid_ptt_menu); + hid_ptt_menu->long_ok_callback = callback; + hid_ptt_menu->long_ok_callback_context = callback_context; + } + static bool hid_ptt_menu_input_callback(InputEvent* event, void* context) { furi_assert(context); HidPushToTalkMenu* hid_ptt_menu = context; @@ -375,7 +410,10 @@ static bool hid_ptt_menu_input_callback(InputEvent* event, void* context) { consumed = true; ptt_menu_process_down(hid_ptt_menu); } - } + } else if(event->type == InputTypeLong && event->key == InputKeyOk) { + consumed = true; + ptt_menu_process_long_ok(hid_ptt_menu); + } return consumed; } diff --git a/applications/system/hid_app/views/hid_ptt_menu.h b/applications/system/hid_app/views/hid_ptt_menu.h index 2defd0569..df6fcb015 100644 --- a/applications/system/hid_app/views/hid_ptt_menu.h +++ b/applications/system/hid_app/views/hid_ptt_menu.h @@ -12,6 +12,13 @@ typedef void (*PushToTalkMenuItemCallback)( uint32_t itemIndex, FuriString* itemLabel); + typedef void (*PushToTalkMenuLongOkCallback)( + void* context, + uint32_t listIndex, + FuriString* listLabel, + uint32_t itemIndex, + FuriString* itemLabel); + HidPushToTalkMenu* hid_ptt_menu_alloc(Hid* bt_hid); void hid_ptt_menu_free(HidPushToTalkMenu* hid_ptt_menu); @@ -27,3 +34,8 @@ void ptt_menu_add_item_to_list( void* callback_context); void ptt_menu_add_list(HidPushToTalkMenu* hid_ptt_menu, const char* label, uint32_t index); + +void ptt_menu_set_long_ok_callback( + HidPushToTalkMenu* hid_ptt_menu, + PushToTalkMenuLongOkCallback callback, + void* callback_context); From 093d611091caef18ba35b765ff1c5817523f21b2 Mon Sep 17 00:00:00 2001 From: Roman Belyakovsky Date: Tue, 28 Apr 2026 23:17:24 +0300 Subject: [PATCH 54/71] Adding long press left to send enter for zoom --- applications/system/hid_app/views/hid_ptt.c | 27 +++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/applications/system/hid_app/views/hid_ptt.c b/applications/system/hid_app/views/hid_ptt.c index 27b1d211f..9e566fb6e 100644 --- a/applications/system/hid_app/views/hid_ptt.c +++ b/applications/system/hid_app/views/hid_ptt.c @@ -445,8 +445,21 @@ static void hid_ptt_trigger_mute_linux_gather(HidPushToTalk* hid_ptt) { "2. Find the 'Mute/Unmute' shortcut and click 'Edit'.\n" "3. Press the Mute button in the app to bind it.\n" "4. Check global checkbox.\n" - "5. Repeat for video and hand shortcuts.\n\n"; + "5. Repeat for video and hand shortcuts.\n" + "6. Long-press < to send Enter key.\n\n"; break; + case HidPushToTalkAppIndexZoom: + app_specific_help = "Zoom:\n" + "1. Go to Settings > Keyboard Shortcuts.\n" + "2. Find the 'Mute/Unmute' shortcut and click 'Edit'.\n" + "3. Press the Mute button in the app to bind it.\n" + "4. Repeat for video and hand shortcuts.\n" + "5. Long-press < to send Enter key.\n\n"; + break; + } + char* left_button_help = ""; + if(appIndex == HidPushToTalkAppIndexZoom || appIndex == HidPushToTalkAppIndexZoomGlobal) { + left_button_help = "Long-press < sends Enter.\n"; } FuriString* msg = furi_string_alloc(); furi_string_cat_printf( @@ -455,11 +468,13 @@ static void hid_ptt_trigger_mute_linux_gather(HidPushToTalk* hid_ptt) { "To operate properly flipper microphone " "status must be in sync with your computer.\n" "Hold > to change mic status.\n" + "%s" "Long-press OK in menu to open this help.\n" "Press BACK to switch mic on/off.\n" "Hold 'o' for PTT mode (mic will be off once you release 'o')\n" "Hold BACK to exit.", - app_specific_help); + app_specific_help, + left_button_help); widget_add_text_scroll_element(hid_ptt->help, 0, 0, 128, 64, furi_string_get_cstr(msg)); furi_string_free(msg); } @@ -949,6 +964,14 @@ static void hid_ptt_process(HidPushToTalk* hid_ptt, InputEvent* event) { } else if(event->type == InputTypeLong && event->key == InputKeyRight) { model->muted = !model->muted; notification_message(hid_ptt->hid->notifications, &sequence_single_vibro); + } else if(event->type == InputTypeLong && event->key == InputKeyLeft) { + if( + model->appIndex == HidPushToTalkAppIndexZoom || + model->appIndex == HidPushToTalkAppIndexZoomGlobal) { + hid_hal_keyboard_press(hid_ptt->hid, HID_KEYBOARD_RETURN); + hid_hal_keyboard_release(hid_ptt->hid, HID_KEYBOARD_RETURN); + notification_message(hid_ptt->hid->notifications, &sequence_single_vibro); + } } //LED if(!model->muted || (model->ptt_pressed)) { From dfd5867ed4a731ac0ef26e86e155267b95e9d0e4 Mon Sep 17 00:00:00 2001 From: Roman Belyakovsky Date: Tue, 28 Apr 2026 23:29:19 +0300 Subject: [PATCH 55/71] Fixing lower helper for non-zoom apps --- applications/system/hid_app/views/hid_ptt.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/applications/system/hid_app/views/hid_ptt.c b/applications/system/hid_app/views/hid_ptt.c index 9e566fb6e..d26819aad 100644 --- a/applications/system/hid_app/views/hid_ptt.c +++ b/applications/system/hid_app/views/hid_ptt.c @@ -775,7 +775,14 @@ static void hid_ptt_draw_callback(Canvas* canvas, void* context) { const uint8_t top_offset = 13; const uint8_t status_bar_bottom_y = top_offset; - const uint8_t helper_top_y = 92; + + // For Zoom/Zoom Global, keep helper banner higher to show Enter key hint space + // For other apps, move it down to close the gap + uint8_t helper_top_y = 102; + if(model->appIndex == HidPushToTalkAppIndexZoom || + model->appIndex == HidPushToTalkAppIndexZoomGlobal) { + helper_top_y = 92; + } const uint8_t x_1 = 0; const uint8_t x_2 = x_1 + 19 + 4; From 0f877005e389b4ef1e43e39ffaab18bac31f5380 Mon Sep 17 00:00:00 2001 From: Roman Belyakovsky Date: Tue, 28 Apr 2026 23:44:26 +0300 Subject: [PATCH 56/71] Fixing helper view for zoom --- applications/system/hid_app/assets/for_11x5.png | Bin 0 -> 335 bytes applications/system/hid_app/views/hid_ptt.c | 11 +++++++++++ 2 files changed, 11 insertions(+) create mode 100644 applications/system/hid_app/assets/for_11x5.png diff --git a/applications/system/hid_app/assets/for_11x5.png b/applications/system/hid_app/assets/for_11x5.png new file mode 100644 index 0000000000000000000000000000000000000000..3ebc4f23c723185c368b7be348c653ef75427955 GIT binary patch literal 335 zcmeAS@N?(olHy`uVBq!ia0vp^+(69A2qYMG-_`yNq!f}pf_xbms?-=58d?|_egTCV zUNA6}8Za=tN?>5Hn!&&zUNC1@pbb!hDaqU2g@N&Im+%rGkG;gx*OmPhi;Re>^xMDZ zSb#zT$%%etKw27zS%DY~yz;AO0U6?+E{-7_*OPbrZ%8a>K75UZ?IKItT}}prjjRjA zbDC!Zm8h1uMwFx^mZVxG7o`Fz1|tI_GhG7{T_cMSBLgdA6Dwm2Z36=<1A{-hrCul+ za`RI%(<(t4Omz(nbPX&+jEt-djjasKAsQZ3nC}H@(16=el9`)YT#}eufNqJol@U}A T_XhP?pdJQKS3j3^P6appIndex == HidPushToTalkAppIndexZoom || + model->appIndex == HidPushToTalkAppIndexZoomGlobal) { + canvas_draw_icon(canvas, 7, 111, &I_Hold_15x5); + canvas_draw_icon(canvas, 24, 109, &I_BtnLeft_9x9); + canvas_draw_icon(canvas, 35, 112, &I_for_11x5); + canvas_draw_icon(canvas, 48, 112, &I_Return_10x7); + } + // Up canvas_draw_icon(canvas, x_2, y_1, &I_Button_18x18); if(model->up_pressed) { From b05dad33863b84339e96b2fce38bbeff4193b3d9 Mon Sep 17 00:00:00 2001 From: Roman Belyakovsky Date: Tue, 28 Apr 2026 23:51:12 +0300 Subject: [PATCH 57/71] Adding helper info to the menu view --- .../system/hid_app/views/hid_ptt_menu.c | 197 ++++++++++++++---- 1 file changed, 160 insertions(+), 37 deletions(-) diff --git a/applications/system/hid_app/views/hid_ptt_menu.c b/applications/system/hid_app/views/hid_ptt_menu.c index 439ca4732..6c8b1fa65 100644 --- a/applications/system/hid_app/views/hid_ptt_menu.c +++ b/applications/system/hid_app/views/hid_ptt_menu.c @@ -2,16 +2,22 @@ #include "hid_ptt.h" #include #include +#include #include "../hid.h" #include "../views.h" +#include "hid_icons.h" + #define TAG "HidPushToTalkMenu" +#define PTT_MENU_HELP_HINT_DELAY_MS 5000U +#define PTT_MENU_HINT_TIMER_PERIOD_MS 150U struct HidPushToTalkMenu { View* view; Hid* hid; - PushToTalkMenuLongOkCallback long_ok_callback; - void* long_ok_callback_context; + PushToTalkMenuLongOkCallback long_ok_callback; + void* long_ok_callback_context; + FuriTimer* hint_timer; }; typedef struct { @@ -62,8 +68,71 @@ typedef struct { size_t window_position; PushToTalkMenuList* lists; int lists_count; + uint32_t last_interaction_tick; + size_t hint_list_position; + size_t hint_item_position; + bool hint_visible; + uint16_t hint_scroll_tick; } HidPushToTalkMenuModel; +static void hid_ptt_menu_mark_interaction(HidPushToTalkMenu* hid_ptt_menu) { + with_view_model( + hid_ptt_menu->view, + HidPushToTalkMenuModel * model, + { + model->last_interaction_tick = furi_get_tick(); + model->hint_list_position = model->list_position; + model->hint_item_position = model->position; + model->hint_visible = false; + model->hint_scroll_tick = 0; + }, + true); +} + +static void hid_ptt_menu_hint_timer_callback(void* context) { + furi_assert(context); + HidPushToTalkMenu* hid_ptt_menu = context; + with_view_model( + hid_ptt_menu->view, + HidPushToTalkMenuModel * model, + { + const uint32_t now = furi_get_tick(); + const bool selection_changed = + (model->list_position != model->hint_list_position) || + (model->position != model->hint_item_position); + + if(selection_changed) { + model->hint_list_position = model->list_position; + model->hint_item_position = model->position; + model->last_interaction_tick = now; + model->hint_visible = false; + model->hint_scroll_tick = 0; + } else if(!model->hint_visible) { + if((now - model->last_interaction_tick) >= PTT_MENU_HELP_HINT_DELAY_MS) { + model->hint_visible = true; + model->hint_scroll_tick = 0; + } + } else { + model->hint_scroll_tick++; + } + }, + true); +} + +static void hid_ptt_menu_enter_callback(void* context) { + furi_assert(context); + HidPushToTalkMenu* hid_ptt_menu = context; + hid_ptt_menu_mark_interaction(hid_ptt_menu); + furi_timer_start( + hid_ptt_menu->hint_timer, furi_ms_to_ticks(PTT_MENU_HINT_TIMER_PERIOD_MS)); +} + +static void hid_ptt_menu_exit_callback(void* context) { + furi_assert(context); + HidPushToTalkMenu* hid_ptt_menu = context; + furi_timer_stop(hid_ptt_menu->hint_timer); +} + static void hid_ptt_menu_draw_list(Canvas* canvas, void* context, const PushToTalkMenuItemArray_t items) { furi_assert(context); @@ -96,7 +165,12 @@ static void FuriString* disp_str; disp_str = furi_string_alloc_set(PushToTalkMenuItemArray_cref(it)->label); - elements_string_fit_width(canvas, disp_str, item_width - (6 * 2)); + size_t item_text_width = item_width - (6 * 2); + if((position == model->position) && model->hint_visible) { + // Reserve space for hint near selected item. + item_text_width = item_width / 2; + } + elements_string_fit_width(canvas, disp_str, item_text_width); canvas_draw_str( canvas, @@ -104,6 +178,38 @@ static void y_offset + (item_position * item_height) + item_height - 4, furi_string_get_cstr(disp_str)); + if((position == model->position) && model->hint_visible) { + const char* hint_prefix = "Long press"; + const char* hint_suffix = "for help"; + const int32_t text_y = y_offset + (item_position * item_height) + item_height - 4; + const int32_t icon_y = text_y - 8; + const size_t selected_text_w = canvas_string_width(canvas, furi_string_get_cstr(disp_str)); + const int32_t hint_start_x = 6 + selected_text_w + 3; + const int32_t row_right_x = item_width - 2; + + if(hint_start_x < row_right_x) { + const size_t prefix_w = canvas_string_width(canvas, hint_prefix); + const size_t icon_w = 9; + const size_t suffix_w = canvas_string_width(canvas, hint_suffix); + const size_t hint_w = prefix_w + 2 + icon_w + 2 + suffix_w; + const size_t available = row_right_x - hint_start_x; + + int32_t scroll_offset = 0; + if(hint_w > available) { + const size_t overflow = hint_w - available; + const size_t cycle = overflow * 2; + const size_t step = cycle ? (model->hint_scroll_tick % cycle) : 0; + scroll_offset = (step <= overflow) ? step : (cycle - step); + } + + const int32_t draw_x = hint_start_x - scroll_offset; + canvas_draw_str(canvas, draw_x, text_y, hint_prefix); + const int32_t icon_x = draw_x + prefix_w + 2; + canvas_draw_icon(canvas, icon_x, icon_y, &I_Ok_btn_9x9); + canvas_draw_str(canvas, icon_x + icon_w + 2, text_y, hint_suffix); + } + } + furi_string_free(disp_str); } @@ -340,38 +446,38 @@ void ptt_menu_process_ok(HidPushToTalkMenu* hid_ptt_menu) { } } - void ptt_menu_process_long_ok(HidPushToTalkMenu* hid_ptt_menu) { - PushToTalkMenuList* list = NULL; - PushToTalkMenuItem* item = NULL; - with_view_model( - hid_ptt_menu->view, - HidPushToTalkMenuModel * model, - { - list = &model->lists[model->list_position]; - const size_t items_size = PushToTalkMenuItemArray_size(list->items); - if(model->position < items_size) { - item = PushToTalkMenuItemArray_get(list->items, model->position); - } - }, - false); - if(item && list && hid_ptt_menu->long_ok_callback) { - hid_ptt_menu->long_ok_callback( - hid_ptt_menu->long_ok_callback_context, - list->index, - list->label, - item->index, - item->label); - } +void ptt_menu_process_long_ok(HidPushToTalkMenu* hid_ptt_menu) { + PushToTalkMenuList* list = NULL; + PushToTalkMenuItem* item = NULL; + with_view_model( + hid_ptt_menu->view, + HidPushToTalkMenuModel * model, + { + list = &model->lists[model->list_position]; + const size_t items_size = PushToTalkMenuItemArray_size(list->items); + if(model->position < items_size) { + item = PushToTalkMenuItemArray_get(list->items, model->position); + } + }, + false); + if(item && list && hid_ptt_menu->long_ok_callback) { + hid_ptt_menu->long_ok_callback( + hid_ptt_menu->long_ok_callback_context, + list->index, + list->label, + item->index, + item->label); } +} - void ptt_menu_set_long_ok_callback( - HidPushToTalkMenu* hid_ptt_menu, - PushToTalkMenuLongOkCallback callback, - void* callback_context) { - furi_assert(hid_ptt_menu); - hid_ptt_menu->long_ok_callback = callback; - hid_ptt_menu->long_ok_callback_context = callback_context; - } +void ptt_menu_set_long_ok_callback( + HidPushToTalkMenu* hid_ptt_menu, + PushToTalkMenuLongOkCallback callback, + void* callback_context) { + furi_assert(hid_ptt_menu); + hid_ptt_menu->long_ok_callback = callback; + hid_ptt_menu->long_ok_callback_context = callback_context; +} static bool hid_ptt_menu_input_callback(InputEvent* event, void* context) { furi_assert(context); @@ -410,10 +516,15 @@ static bool hid_ptt_menu_input_callback(InputEvent* event, void* context) { consumed = true; ptt_menu_process_down(hid_ptt_menu); } - } else if(event->type == InputTypeLong && event->key == InputKeyOk) { - consumed = true; - ptt_menu_process_long_ok(hid_ptt_menu); - } + } else if(event->type == InputTypeLong && event->key == InputKeyOk) { + consumed = true; + ptt_menu_process_long_ok(hid_ptt_menu); + } + + if(event->type != InputTypeRelease) { + hid_ptt_menu_mark_interaction(hid_ptt_menu); + } + return consumed; } @@ -430,6 +541,11 @@ HidPushToTalkMenu* hid_ptt_menu_alloc(Hid* hid) { view_allocate_model(hid_ptt_menu->view, ViewModelTypeLocking, sizeof(HidPushToTalkMenuModel)); view_set_draw_callback(hid_ptt_menu->view, hid_ptt_menu_draw_callback); view_set_input_callback(hid_ptt_menu->view, hid_ptt_menu_input_callback); + view_set_enter_callback(hid_ptt_menu->view, hid_ptt_menu_enter_callback); + view_set_exit_callback(hid_ptt_menu->view, hid_ptt_menu_exit_callback); + + hid_ptt_menu->hint_timer = + furi_timer_alloc(hid_ptt_menu_hint_timer_callback, FuriTimerTypePeriodic, hid_ptt_menu); with_view_model( hid_ptt_menu->view, @@ -438,6 +554,11 @@ HidPushToTalkMenu* hid_ptt_menu_alloc(Hid* hid) { model->lists_count = 0; model->position = 0; model->window_position = 0; + model->last_interaction_tick = furi_get_tick(); + model->hint_list_position = 0; + model->hint_item_position = 0; + model->hint_visible = false; + model->hint_scroll_tick = 0; }, true); return hid_ptt_menu; @@ -445,6 +566,8 @@ HidPushToTalkMenu* hid_ptt_menu_alloc(Hid* hid) { void hid_ptt_menu_free(HidPushToTalkMenu* hid_ptt_menu) { furi_assert(hid_ptt_menu); + furi_timer_stop(hid_ptt_menu->hint_timer); + furi_timer_free(hid_ptt_menu->hint_timer); with_view_model( hid_ptt_menu->view, HidPushToTalkMenuModel * model, From 65a12073685a7d32e33b8ab0d349821690278ff6 Mon Sep 17 00:00:00 2001 From: Roman Belyakovsky Date: Wed, 29 Apr 2026 00:00:02 +0300 Subject: [PATCH 58/71] Improving helper display in menu --- .../system/hid_app/views/hid_ptt_menu.c | 64 +++++++++---------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/applications/system/hid_app/views/hid_ptt_menu.c b/applications/system/hid_app/views/hid_ptt_menu.c index 6c8b1fa65..7072eff57 100644 --- a/applications/system/hid_app/views/hid_ptt_menu.c +++ b/applications/system/hid_app/views/hid_ptt_menu.c @@ -165,49 +165,43 @@ static void FuriString* disp_str; disp_str = furi_string_alloc_set(PushToTalkMenuItemArray_cref(it)->label); - size_t item_text_width = item_width - (6 * 2); - if((position == model->position) && model->hint_visible) { - // Reserve space for hint near selected item. - item_text_width = item_width / 2; - } - elements_string_fit_width(canvas, disp_str, item_text_width); - - canvas_draw_str( - canvas, - 6, - y_offset + (item_position * item_height) + item_height - 4, - furi_string_get_cstr(disp_str)); + const int32_t text_y = y_offset + (item_position * item_height) + item_height - 4; + const int32_t row_left_x = 6; + const int32_t row_right_x = item_width - 2; + const size_t row_width = row_right_x - row_left_x; if((position == model->position) && model->hint_visible) { - const char* hint_prefix = "Long press"; + const char* hint_prefix = " | Long press"; const char* hint_suffix = "for help"; - const int32_t text_y = y_offset + (item_position * item_height) + item_height - 4; const int32_t icon_y = text_y - 8; - const size_t selected_text_w = canvas_string_width(canvas, furi_string_get_cstr(disp_str)); - const int32_t hint_start_x = 6 + selected_text_w + 3; - const int32_t row_right_x = item_width - 2; - if(hint_start_x < row_right_x) { - const size_t prefix_w = canvas_string_width(canvas, hint_prefix); - const size_t icon_w = 9; - const size_t suffix_w = canvas_string_width(canvas, hint_suffix); - const size_t hint_w = prefix_w + 2 + icon_w + 2 + suffix_w; - const size_t available = row_right_x - hint_start_x; + const size_t label_w = canvas_string_width(canvas, furi_string_get_cstr(disp_str)); + const size_t prefix_w = canvas_string_width(canvas, hint_prefix); + const size_t icon_w = 9; + const size_t suffix_w = canvas_string_width(canvas, hint_suffix); - int32_t scroll_offset = 0; - if(hint_w > available) { - const size_t overflow = hint_w - available; - const size_t cycle = overflow * 2; - const size_t step = cycle ? (model->hint_scroll_tick % cycle) : 0; - scroll_offset = (step <= overflow) ? step : (cycle - step); - } + const size_t group_w = label_w + 2 + prefix_w + 2 + icon_w + 2 + suffix_w; - const int32_t draw_x = hint_start_x - scroll_offset; - canvas_draw_str(canvas, draw_x, text_y, hint_prefix); - const int32_t icon_x = draw_x + prefix_w + 2; - canvas_draw_icon(canvas, icon_x, icon_y, &I_Ok_btn_9x9); - canvas_draw_str(canvas, icon_x + icon_w + 2, text_y, hint_suffix); + int32_t scroll_offset = 0; + if(group_w > row_width) { + const size_t overflow = group_w - row_width; + const size_t cycle = overflow * 2; + const size_t step = cycle ? (model->hint_scroll_tick % cycle) : 0; + scroll_offset = (step <= overflow) ? step : (cycle - step); } + + const int32_t draw_x = row_left_x - scroll_offset; + canvas_draw_str(canvas, draw_x, text_y, furi_string_get_cstr(disp_str)); + + int32_t hint_x = draw_x + label_w + 2; + canvas_draw_str(canvas, hint_x, text_y, hint_prefix); + hint_x += prefix_w + 2; + canvas_draw_icon(canvas, hint_x, icon_y, &I_Ok_btn_9x9); + hint_x += icon_w + 2; + canvas_draw_str(canvas, hint_x, text_y, hint_suffix); + } else { + elements_string_fit_width(canvas, disp_str, item_width - (6 * 2)); + canvas_draw_str(canvas, row_left_x, text_y, furi_string_get_cstr(disp_str)); } furi_string_free(disp_str); From 2c1d9bd9e80e5b2c0eb970b3a7cd31821e34be8a Mon Sep 17 00:00:00 2001 From: Roman Belyakovsky Date: Wed, 29 Apr 2026 00:12:01 +0300 Subject: [PATCH 59/71] Visialazing long left press for zoom --- applications/system/hid_app/views/hid_ptt.c | 85 +++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/applications/system/hid_app/views/hid_ptt.c b/applications/system/hid_app/views/hid_ptt.c index 045b82f1b..9ae4eed0e 100644 --- a/applications/system/hid_app/views/hid_ptt.c +++ b/applications/system/hid_app/views/hid_ptt.c @@ -6,12 +6,15 @@ #include #include #include +#include #include "../hid.h" #include "../views.h" #include "hid_icons.h" #define TAG "HidPushToTalk" +#define HID_PTT_LEFT_HOLD_ANIM_STEP_MS 100U +#define HID_PTT_LEFT_HOLD_ANIM_STEPS 5U // Exact home status-bar Bluetooth icon pixels (from assets/icons/StatusBar). // Bitmap format for canvas_draw_bitmap(): row-major, 1-bit. @@ -29,6 +32,7 @@ struct HidPushToTalk { View* view; Hid* hid; Widget* help; + FuriTimer* left_hold_timer; }; typedef void (*PushToTalkActionCallback)(HidPushToTalk* hid_ptt); @@ -47,6 +51,7 @@ typedef struct { size_t osIndex; size_t appIndex; size_t window_position; + uint8_t left_hold_progress; PushToTalkActionCallback callback_trigger_mute; PushToTalkActionCallback callback_trigger_camera; PushToTalkActionCallback callback_trigger_hand; @@ -74,6 +79,66 @@ enum HidPushToTalkAppIndex { HidPushToTalkAppIndexSize, }; +static bool hid_ptt_is_zoom_app(size_t app_index) { + return (app_index == HidPushToTalkAppIndexZoom) || + (app_index == HidPushToTalkAppIndexZoomGlobal); +} + +static void hid_ptt_left_hold_timer_callback(void* context) { + furi_assert(context); + HidPushToTalk* hid_ptt = context; + with_view_model( + hid_ptt->view, + HidPushToTalkModel * model, + { + if(model->left_pressed && hid_ptt_is_zoom_app(model->appIndex)) { + if(model->left_hold_progress < HID_PTT_LEFT_HOLD_ANIM_STEPS) { + model->left_hold_progress++; + } + } else { + model->left_hold_progress = 0; + furi_timer_stop(hid_ptt->left_hold_timer); + } + }, + true); +} + +static void hid_ptt_draw_zoom_enter_hint( + Canvas* canvas, + uint8_t x, + uint8_t y, + uint8_t progress, + bool pressed) { + const uint8_t width = 18; + const uint8_t height = 10; + const uint8_t inner_x = x + 1; + const uint8_t inner_y = y + 1; + const uint8_t inner_w = width - 2; + const uint8_t inner_h = height - 1; + + if(progress > 0) { + const uint8_t fill_w = (inner_w * progress) / HID_PTT_LEFT_HOLD_ANIM_STEPS; + if(fill_w > 0) { + canvas_draw_box(canvas, inner_x, inner_y, fill_w, inner_h - 1); + } + } + + for(uint8_t dot_x = x; dot_x < x + width; dot_x += 2) { + canvas_draw_dot(canvas, dot_x, y + height - 1); + } + for(uint8_t dot_y = y + 1; dot_y < y + height; dot_y += 2) { + canvas_draw_dot(canvas, x, dot_y); + canvas_draw_dot(canvas, x + width - 1, dot_y); + } + + UNUSED(pressed); + if(progress > 0) { + canvas_set_color(canvas, ColorWhite); + } + canvas_draw_icon(canvas, x + 4, y + 1, &I_Enter_11x7); + canvas_set_color(canvas, ColorBlack); +} + // meet, zoom static void hid_ptt_start_ptt_meet_zoom(HidPushToTalk* hid_ptt) { hid_hal_keyboard_press(hid_ptt->hid, HID_KEYBOARD_SPACEBAR); @@ -857,6 +922,10 @@ static void hid_ptt_draw_callback(Canvas* canvas, void* context) { } canvas_set_color(canvas, ColorBlack); + if(hid_ptt_is_zoom_app(model->appIndex)) { + hid_ptt_draw_zoom_enter_hint(canvas, x_1, y_2 + 18, model->left_hold_progress, model->left_pressed); + } + // Right / Camera canvas_draw_icon(canvas, x_3, y_2, &I_Button_18x18); if(model->right_pressed) { @@ -930,6 +999,12 @@ static void hid_ptt_process(HidPushToTalk* hid_ptt, InputEvent* event) { hid_hal_consumer_key_press(hid_ptt->hid, HID_CONSUMER_VOLUME_DECREMENT); } else if(event->key == InputKeyLeft) { model->left_pressed = true; + if(hid_ptt_is_zoom_app(model->appIndex)) { + model->left_hold_progress = 0; + furi_timer_start( + hid_ptt->left_hold_timer, + furi_ms_to_ticks(HID_PTT_LEFT_HOLD_ANIM_STEP_MS)); + } } else if(event->key == InputKeyRight) { model->right_pressed = true; } else if(event->key == InputKeyOk) { @@ -953,6 +1028,8 @@ static void hid_ptt_process(HidPushToTalk* hid_ptt, InputEvent* event) { } } else if(event->key == InputKeyLeft) { model->left_pressed = false; + model->left_hold_progress = 0; + furi_timer_stop(hid_ptt->left_hold_timer); } else if(event->key == InputKeyRight) { model->right_pressed = false; @@ -986,6 +1063,8 @@ static void hid_ptt_process(HidPushToTalk* hid_ptt, InputEvent* event) { if( model->appIndex == HidPushToTalkAppIndexZoom || model->appIndex == HidPushToTalkAppIndexZoomGlobal) { + model->left_hold_progress = HID_PTT_LEFT_HOLD_ANIM_STEPS; + furi_timer_stop(hid_ptt->left_hold_timer); hid_hal_keyboard_press(hid_ptt->hid, HID_KEYBOARD_RETURN); hid_hal_keyboard_release(hid_ptt->hid, HID_KEYBOARD_RETURN); notification_message(hid_ptt->hid->notifications, &sequence_single_vibro); @@ -1006,6 +1085,7 @@ static bool hid_ptt_input_callback(InputEvent* event, void* context) { HidPushToTalk* hid_ptt = context; bool consumed = false; if(event->type == InputTypeLong && event->key == InputKeyBack) { + furi_timer_stop(hid_ptt->left_hold_timer); hid_hal_keyboard_release_all(hid_ptt->hid); notification_message(hid_ptt->hid->notifications, &sequence_double_vibro); widget_reset(hid_ptt->help); @@ -1029,6 +1109,8 @@ static uint32_t hid_ptt_menu_view(void* context) { HidPushToTalk* hid_ptt_alloc(Hid* hid) { HidPushToTalk* hid_ptt = malloc(sizeof(HidPushToTalk)); hid_ptt->hid = hid; + hid_ptt->left_hold_timer = + furi_timer_alloc(hid_ptt_left_hold_timer_callback, FuriTimerTypePeriodic, hid_ptt); hid_ptt->view = view_alloc(); view_set_context(hid_ptt->view, hid_ptt); view_allocate_model(hid_ptt->view, ViewModelTypeLocking, sizeof(HidPushToTalkModel)); @@ -1041,6 +1123,7 @@ HidPushToTalk* hid_ptt_alloc(Hid* hid) { HidPushToTalkModel * model, { model->muted = true; // assume we're muted + model->left_hold_progress = 0; model->os = furi_string_alloc(); model->app = furi_string_alloc(); }, @@ -1263,6 +1346,8 @@ HidPushToTalk* hid_ptt_alloc(Hid* hid) { void hid_ptt_free(HidPushToTalk* hid_ptt) { furi_assert(hid_ptt); + furi_timer_stop(hid_ptt->left_hold_timer); + furi_timer_free(hid_ptt->left_hold_timer); notification_message(hid_ptt->hid->notifications, &sequence_reset_red); with_view_model( hid_ptt->view, From 2a7e048cd8cb8874c7f265d3f06b554a9fc92af4 Mon Sep 17 00:00:00 2001 From: Roman Belyakovsky Date: Wed, 29 Apr 2026 00:29:30 +0300 Subject: [PATCH 60/71] Fixing google meet global screen --- applications/system/hid_app/views/hid_ptt.c | 49 ++++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/applications/system/hid_app/views/hid_ptt.c b/applications/system/hid_app/views/hid_ptt.c index 9ae4eed0e..2696c22c4 100644 --- a/applications/system/hid_app/views/hid_ptt.c +++ b/applications/system/hid_app/views/hid_ptt.c @@ -787,6 +787,35 @@ static void hid_ptt_draw_text_centered(Canvas* canvas, uint8_t y, FuriString* st furi_string_free(disp_str); } +static void hid_ptt_draw_app_label(Canvas* canvas, uint8_t first_line_y, FuriString* app) { + FuriString* first_line = furi_string_alloc_set(app); + if(canvas_string_width(canvas, furi_string_get_cstr(first_line)) <= canvas_width(canvas)) { + hid_ptt_draw_text_centered(canvas, first_line_y, first_line); + furi_string_free(first_line); + return; + } + + const char* app_cstr = furi_string_get_cstr(app); + const char* split = strrchr(app_cstr, ' '); + if(!split) { + hid_ptt_draw_text_centered(canvas, first_line_y, first_line); + furi_string_free(first_line); + return; + } + + FuriString* second_line = furi_string_alloc(); + furi_string_set_strn(first_line, app_cstr, split - app_cstr); + furi_string_set_str(second_line, split + 1); + + elements_string_fit_width(canvas, first_line, canvas_width(canvas)); + elements_string_fit_width(canvas, second_line, canvas_width(canvas)); + hid_ptt_draw_text_centered(canvas, first_line_y, first_line); + hid_ptt_draw_text_centered(canvas, first_line_y + 10, second_line); + + furi_string_free(second_line); + furi_string_free(first_line); +} + static void hid_ptt_draw_status_bar(Canvas* canvas, bool show_bt, bool connected) { char time_str[16]; DateTime dt; @@ -838,14 +867,18 @@ static void hid_ptt_draw_callback(Canvas* canvas, void* context) { furi_assert(context); HidPushToTalkModel* model = context; + const bool app_label_needs_two_lines = + canvas_string_width(canvas, furi_string_get_cstr(model->app)) > canvas_width(canvas); + const uint8_t top_offset = 13; const uint8_t status_bar_bottom_y = top_offset; - + // For Zoom/Zoom Global, keep helper banner higher to show Enter key hint space // For other apps, move it down to close the gap uint8_t helper_top_y = 102; - if(model->appIndex == HidPushToTalkAppIndexZoom || - model->appIndex == HidPushToTalkAppIndexZoomGlobal) { + if((model->appIndex == HidPushToTalkAppIndexZoom || + model->appIndex == HidPushToTalkAppIndexZoomGlobal) && + !app_label_needs_two_lines) { helper_top_y = 92; } @@ -859,8 +892,8 @@ static void hid_ptt_draw_callback(Canvas* canvas, void* context) { const uint8_t controls_bottom_y = y_3 + 18; const uint8_t labels_center_y = (controls_bottom_y + helper_top_y) / 2; - const uint8_t app_label_y = labels_center_y - 1; - const uint8_t os_label_y = app_label_y + 11; + const uint8_t app_label_y = app_label_needs_two_lines ? (controls_bottom_y + 10) : (labels_center_y - 1); + const uint8_t os_label_y = app_label_needs_two_lines ? (app_label_y + 20) : (app_label_y + 11); // Header canvas_set_font(canvas, FontPrimary); @@ -872,7 +905,11 @@ static void hid_ptt_draw_callback(Canvas* canvas, void* context) { // OS and App labels canvas_set_font(canvas, FontSecondary); - hid_ptt_draw_text_centered(canvas, app_label_y, model->app); + if(app_label_needs_two_lines) { + hid_ptt_draw_app_label(canvas, app_label_y, model->app); + } else { + hid_ptt_draw_text_centered(canvas, app_label_y, model->app); + } hid_ptt_draw_text_centered(canvas, os_label_y, model->os); // Help label From 58afca1dbfb910c5f27418fb563db3770ed9c76e Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Wed, 29 Apr 2026 03:10:15 +0300 Subject: [PATCH 61/71] upd changelog, fbt format --- CHANGELOG.md | 3 +- applications/system/hid_app/views/hid_ptt.c | 225 ++++++++++-------- .../system/hid_app/views/hid_ptt_menu.c | 13 +- .../system/hid_app/views/hid_ptt_menu.h | 12 +- 4 files changed, 136 insertions(+), 117 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c15357ce8..c2a3df2d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,9 @@ * NFC: Fix CLI with NTAG4xx and Type 4 Tag support (by @WillyJL) * OFW PR 4362: NFC: **Fix BusFault** in Write to Initial Card (by @akrylysov) * OFW PR 4369: NFC: Fix stack buffer overflows in MFUL FAST_READ and DESFire file settings parsers (by @qp-x-qp) -* Apps: Build tag (**25apr2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) +* Apps: Build tag (**29apr2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes +* HID App: PTT improvements (PR #996 | by @hryamzik) * UI: Wording changes to be more clear (PR #994 | by @SkeletonMan03) * SubGHz: Fix small logic error in KingGatesStylo4k protocol (found by @X-Stuff) * OFW PR 4364: JS: Address utf8 support when uploading JavaScript application (by @bekindpleaserewind) diff --git a/applications/system/hid_app/views/hid_ptt.c b/applications/system/hid_app/views/hid_ptt.c index 2696c22c4..9670b7c2a 100644 --- a/applications/system/hid_app/views/hid_ptt.c +++ b/applications/system/hid_app/views/hid_ptt.c @@ -6,26 +6,48 @@ #include #include #include -#include #include "../hid.h" #include "../views.h" #include "hid_icons.h" -#define TAG "HidPushToTalk" +#define TAG "HidPushToTalk" #define HID_PTT_LEFT_HOLD_ANIM_STEP_MS 100U -#define HID_PTT_LEFT_HOLD_ANIM_STEPS 5U +#define HID_PTT_LEFT_HOLD_ANIM_STEPS 5U // Exact home status-bar Bluetooth icon pixels (from assets/icons/StatusBar). // Bitmap format for canvas_draw_bitmap(): row-major, 1-bit. // Full compressed frame data including leading 0x00 heatshrink marker, // copied verbatim from build/f7-firmware-D/assets/compiled/assets_icons.c static const uint8_t hid_ptt_bluetooth_connected_16x8_bits[] = { - 0x00, 0x04, 0x00, 0x0d, 0x00, 0x16, 0x60, 0x4c, - 0x97, 0x4c, 0x97, 0x16, 0x60, 0x0d, 0x00, 0x04, 0x00, + 0x00, + 0x04, + 0x00, + 0x0d, + 0x00, + 0x16, + 0x60, + 0x4c, + 0x97, + 0x4c, + 0x97, + 0x16, + 0x60, + 0x0d, + 0x00, + 0x04, + 0x00, }; static const uint8_t hid_ptt_bluetooth_idle_5x8_bits[] = { - 0x00, 0x04, 0x0d, 0x16, 0x0c, 0x0c, 0x16, 0x0d, 0x04, + 0x00, + 0x04, + 0x0d, + 0x16, + 0x0c, + 0x0c, + 0x16, + 0x0d, + 0x04, }; struct HidPushToTalk { @@ -467,97 +489,96 @@ static void hid_ptt_trigger_mute_linux_gather(HidPushToTalk* hid_ptt) { hid_ptt->hid, KEY_MOD_LEFT_CTRL | KEY_MOD_LEFT_SHIFT | HID_KEYBOARD_A); } - static void hid_ptt_populate_help(HidPushToTalk* hid_ptt, uint32_t appIndex) { - widget_reset(hid_ptt->help); - char* app_specific_help = ""; - switch(appIndex) { - case HidPushToTalkAppIndexGoogleMeet: - app_specific_help = - "Google Meet:\n" - "This feature is off by default in your audio settings " - "and may not work for Windows users who use their screen " - "reader. In this situation, the spacebar performs a different action.\n\n"; - break; - case HidPushToTalkAppIndexGoogleMeetGlobal: - app_specific_help = "Google Meet (Global):\n" - "1. Install \"Google Meet - Global Shortcuts\" extension.\n" - "2. Open chrome://extensions/shortcuts.\n" - "3. Set 'Toggle microphone' to Cmd+Ctrl+7 and enable Global.\n" - "4. Set 'Toggle camera' to Cmd+Ctrl+8 and enable Global.\n" - "5. Set 'Raise hand' to Cmd+Ctrl+9 and enable Global.\n\n"; - break; - case HidPushToTalkAppIndexDiscord: - app_specific_help = - "Discord:\n" - "1. Under App Settings, click Voice & Video. Under Input Mode, " - "check the box next to Push to Talk.\n" - "2. Scroll down to SHORTCUT, click Record Keybinder.\n" - "3. Press PTT in the app to bind it." - "4. Go to Keybinds and assign mute button.\n\n"; - break; - case HidPushToTalkAppIndexTeamSpeak: - app_specific_help = "TeamSpeak:\n" - "To make keys working bind them in TeamSpeak settings.\n\n"; - break; - case HidPushToTalkAppIndexTeams: - app_specific_help = - "Teams:\n" - "Go to Settings > Privacy. Make sure Keyboard shortcut to unmute is toggled on.\n\n"; - break; - case HidPushToTalkAppIndexZoomGlobal: - app_specific_help = "Zoom (Global):\n" - "1. Go to Settings > Keyboard Shortcuts.\n" - "2. Find the 'Mute/Unmute' shortcut and click 'Edit'.\n" - "3. Press the Mute button in the app to bind it.\n" - "4. Check global checkbox.\n" - "5. Repeat for video and hand shortcuts.\n" - "6. Long-press < to send Enter key.\n\n"; - break; - case HidPushToTalkAppIndexZoom: - app_specific_help = "Zoom:\n" - "1. Go to Settings > Keyboard Shortcuts.\n" - "2. Find the 'Mute/Unmute' shortcut and click 'Edit'.\n" - "3. Press the Mute button in the app to bind it.\n" - "4. Repeat for video and hand shortcuts.\n" - "5. Long-press < to send Enter key.\n\n"; - break; - } - char* left_button_help = ""; - if(appIndex == HidPushToTalkAppIndexZoom || appIndex == HidPushToTalkAppIndexZoomGlobal) { - left_button_help = "Long-press < sends Enter.\n"; - } - FuriString* msg = furi_string_alloc(); - furi_string_cat_printf( - msg, - "%sGeneral:\n" - "To operate properly flipper microphone " - "status must be in sync with your computer.\n" - "Hold > to change mic status.\n" - "%s" - "Long-press OK in menu to open this help.\n" - "Press BACK to switch mic on/off.\n" - "Hold 'o' for PTT mode (mic will be off once you release 'o')\n" - "Hold BACK to exit.", - app_specific_help, - left_button_help); - widget_add_text_scroll_element(hid_ptt->help, 0, 0, 128, 64, furi_string_get_cstr(msg)); - furi_string_free(msg); +static void hid_ptt_populate_help(HidPushToTalk* hid_ptt, uint32_t appIndex) { + widget_reset(hid_ptt->help); + char* app_specific_help = ""; + switch(appIndex) { + case HidPushToTalkAppIndexGoogleMeet: + app_specific_help = + "Google Meet:\n" + "This feature is off by default in your audio settings " + "and may not work for Windows users who use their screen " + "reader. In this situation, the spacebar performs a different action.\n\n"; + break; + case HidPushToTalkAppIndexGoogleMeetGlobal: + app_specific_help = "Google Meet (Global):\n" + "1. Install \"Google Meet - Global Shortcuts\" extension.\n" + "2. Open chrome://extensions/shortcuts.\n" + "3. Set 'Toggle microphone' to Cmd+Ctrl+7 and enable Global.\n" + "4. Set 'Toggle camera' to Cmd+Ctrl+8 and enable Global.\n" + "5. Set 'Raise hand' to Cmd+Ctrl+9 and enable Global.\n\n"; + break; + case HidPushToTalkAppIndexDiscord: + app_specific_help = "Discord:\n" + "1. Under App Settings, click Voice & Video. Under Input Mode, " + "check the box next to Push to Talk.\n" + "2. Scroll down to SHORTCUT, click Record Keybinder.\n" + "3. Press PTT in the app to bind it." + "4. Go to Keybinds and assign mute button.\n\n"; + break; + case HidPushToTalkAppIndexTeamSpeak: + app_specific_help = "TeamSpeak:\n" + "To make keys working bind them in TeamSpeak settings.\n\n"; + break; + case HidPushToTalkAppIndexTeams: + app_specific_help = + "Teams:\n" + "Go to Settings > Privacy. Make sure Keyboard shortcut to unmute is toggled on.\n\n"; + break; + case HidPushToTalkAppIndexZoomGlobal: + app_specific_help = "Zoom (Global):\n" + "1. Go to Settings > Keyboard Shortcuts.\n" + "2. Find the 'Mute/Unmute' shortcut and click 'Edit'.\n" + "3. Press the Mute button in the app to bind it.\n" + "4. Check global checkbox.\n" + "5. Repeat for video and hand shortcuts.\n" + "6. Long-press < to send Enter key.\n\n"; + break; + case HidPushToTalkAppIndexZoom: + app_specific_help = "Zoom:\n" + "1. Go to Settings > Keyboard Shortcuts.\n" + "2. Find the 'Mute/Unmute' shortcut and click 'Edit'.\n" + "3. Press the Mute button in the app to bind it.\n" + "4. Repeat for video and hand shortcuts.\n" + "5. Long-press < to send Enter key.\n\n"; + break; } + char* left_button_help = ""; + if(appIndex == HidPushToTalkAppIndexZoom || appIndex == HidPushToTalkAppIndexZoomGlobal) { + left_button_help = "Long-press < sends Enter.\n"; + } + FuriString* msg = furi_string_alloc(); + furi_string_cat_printf( + msg, + "%sGeneral:\n" + "To operate properly flipper microphone " + "status must be in sync with your computer.\n" + "Hold > to change mic status.\n" + "%s" + "Long-press OK in menu to open this help.\n" + "Press BACK to switch mic on/off.\n" + "Hold 'o' for PTT mode (mic will be off once you release 'o')\n" + "Hold BACK to exit.", + app_specific_help, + left_button_help); + widget_add_text_scroll_element(hid_ptt->help, 0, 0, 128, 64, furi_string_get_cstr(msg)); + furi_string_free(msg); +} - static void hid_ptt_menu_help_callback( - void* context, - uint32_t osIndex, - FuriString* osLabel, - uint32_t appIndex, - FuriString* appLabel) { - UNUSED(osIndex); - UNUSED(osLabel); - UNUSED(appLabel); - furi_assert(context); - HidPushToTalk* hid_ptt = context; - hid_ptt_populate_help(hid_ptt, appIndex); - view_dispatcher_switch_to_view(hid_ptt->hid->view_dispatcher, HidViewPushToTalkHelp); - } +static void hid_ptt_menu_help_callback( + void* context, + uint32_t osIndex, + FuriString* osLabel, + uint32_t appIndex, + FuriString* appLabel) { + UNUSED(osIndex); + UNUSED(osLabel); + UNUSED(appLabel); + furi_assert(context); + HidPushToTalk* hid_ptt = context; + hid_ptt_populate_help(hid_ptt, appIndex); + view_dispatcher_switch_to_view(hid_ptt->hid->view_dispatcher, HidViewPushToTalkHelp); +} static void hid_ptt_menu_callback( void* context, @@ -765,10 +786,9 @@ static void hid_ptt_menu_callback( break; } } - }, true); - hid_ptt_populate_help(hid_ptt, appIndex); + hid_ptt_populate_help(hid_ptt, appIndex); view_dispatcher_switch_to_view(hid_ptt->hid->view_dispatcher, HidViewPushToTalk); } @@ -892,7 +912,8 @@ static void hid_ptt_draw_callback(Canvas* canvas, void* context) { const uint8_t controls_bottom_y = y_3 + 18; const uint8_t labels_center_y = (controls_bottom_y + helper_top_y) / 2; - const uint8_t app_label_y = app_label_needs_two_lines ? (controls_bottom_y + 10) : (labels_center_y - 1); + const uint8_t app_label_y = app_label_needs_two_lines ? (controls_bottom_y + 10) : + (labels_center_y - 1); const uint8_t os_label_y = app_label_needs_two_lines ? (app_label_y + 20) : (app_label_y + 11); // Header @@ -960,7 +981,8 @@ static void hid_ptt_draw_callback(Canvas* canvas, void* context) { canvas_set_color(canvas, ColorBlack); if(hid_ptt_is_zoom_app(model->appIndex)) { - hid_ptt_draw_zoom_enter_hint(canvas, x_1, y_2 + 18, model->left_hold_progress, model->left_pressed); + hid_ptt_draw_zoom_enter_hint( + canvas, x_1, y_2 + 18, model->left_hold_progress, model->left_pressed); } // Right / Camera @@ -1097,9 +1119,8 @@ static void hid_ptt_process(HidPushToTalk* hid_ptt, InputEvent* event) { model->muted = !model->muted; notification_message(hid_ptt->hid->notifications, &sequence_single_vibro); } else if(event->type == InputTypeLong && event->key == InputKeyLeft) { - if( - model->appIndex == HidPushToTalkAppIndexZoom || - model->appIndex == HidPushToTalkAppIndexZoomGlobal) { + if(model->appIndex == HidPushToTalkAppIndexZoom || + model->appIndex == HidPushToTalkAppIndexZoomGlobal) { model->left_hold_progress = HID_PTT_LEFT_HOLD_ANIM_STEPS; furi_timer_stop(hid_ptt->left_hold_timer); hid_hal_keyboard_press(hid_ptt->hid, HID_KEYBOARD_RETURN); diff --git a/applications/system/hid_app/views/hid_ptt_menu.c b/applications/system/hid_app/views/hid_ptt_menu.c index 7072eff57..ca293160c 100644 --- a/applications/system/hid_app/views/hid_ptt_menu.c +++ b/applications/system/hid_app/views/hid_ptt_menu.c @@ -2,14 +2,13 @@ #include "hid_ptt.h" #include #include -#include #include "../hid.h" #include "../views.h" #include "hid_icons.h" -#define TAG "HidPushToTalkMenu" -#define PTT_MENU_HELP_HINT_DELAY_MS 5000U +#define TAG "HidPushToTalkMenu" +#define PTT_MENU_HELP_HINT_DELAY_MS 5000U #define PTT_MENU_HINT_TIMER_PERIOD_MS 150U struct HidPushToTalkMenu { @@ -97,9 +96,8 @@ static void hid_ptt_menu_hint_timer_callback(void* context) { HidPushToTalkMenuModel * model, { const uint32_t now = furi_get_tick(); - const bool selection_changed = - (model->list_position != model->hint_list_position) || - (model->position != model->hint_item_position); + const bool selection_changed = (model->list_position != model->hint_list_position) || + (model->position != model->hint_item_position); if(selection_changed) { model->hint_list_position = model->list_position; @@ -123,8 +121,7 @@ static void hid_ptt_menu_enter_callback(void* context) { furi_assert(context); HidPushToTalkMenu* hid_ptt_menu = context; hid_ptt_menu_mark_interaction(hid_ptt_menu); - furi_timer_start( - hid_ptt_menu->hint_timer, furi_ms_to_ticks(PTT_MENU_HINT_TIMER_PERIOD_MS)); + furi_timer_start(hid_ptt_menu->hint_timer, furi_ms_to_ticks(PTT_MENU_HINT_TIMER_PERIOD_MS)); } static void hid_ptt_menu_exit_callback(void* context) { diff --git a/applications/system/hid_app/views/hid_ptt_menu.h b/applications/system/hid_app/views/hid_ptt_menu.h index df6fcb015..ee61a9a50 100644 --- a/applications/system/hid_app/views/hid_ptt_menu.h +++ b/applications/system/hid_app/views/hid_ptt_menu.h @@ -12,12 +12,12 @@ typedef void (*PushToTalkMenuItemCallback)( uint32_t itemIndex, FuriString* itemLabel); - typedef void (*PushToTalkMenuLongOkCallback)( - void* context, - uint32_t listIndex, - FuriString* listLabel, - uint32_t itemIndex, - FuriString* itemLabel); +typedef void (*PushToTalkMenuLongOkCallback)( + void* context, + uint32_t listIndex, + FuriString* listLabel, + uint32_t itemIndex, + FuriString* itemLabel); HidPushToTalkMenu* hid_ptt_menu_alloc(Hid* bt_hid); From bcbb1b5e048923fe1ce139503d80c76ff6aede2f Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:12:59 +0300 Subject: [PATCH 62/71] remove duplicate code from raw protocol --- CHANGELOG.md | 1 + lib/subghz/protocols/raw.c | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2a3df2d5..9a194a1f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ ## Other changes * HID App: PTT improvements (PR #996 | by @hryamzik) * UI: Wording changes to be more clear (PR #994 | by @SkeletonMan03) +* SubGHz: Fix duplicate code in RAW protocol * SubGHz: Fix small logic error in KingGatesStylo4k protocol (found by @X-Stuff) * OFW PR 4364: JS: Address utf8 support when uploading JavaScript application (by @bekindpleaserewind) * Display: Extend lcd contrast range to full ST756x 6-bit range (by @ShaTie) (reduced to -10 to +18 to avoid fully unreadable state) diff --git a/lib/subghz/protocols/raw.c b/lib/subghz/protocols/raw.c index 326576bde..483435124 100644 --- a/lib/subghz/protocols/raw.c +++ b/lib/subghz/protocols/raw.c @@ -99,10 +99,6 @@ bool subghz_protocol_raw_save_to_file_init( if(!storage_simply_mkdir(instance->storage, SUBGHZ_RAW_FOLDER)) { break; } - // Create saved directory if necessary - if(!storage_simply_mkdir(instance->storage, SUBGHZ_RAW_FOLDER)) { - break; - } furi_string_set(instance->file_name, dev_name); // First remove subghz device file if it was saved From daec03bd352db1654c3bf456bbeeed2eb9f87d03 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sun, 3 May 2026 23:08:56 +0300 Subject: [PATCH 63/71] add canvas_buffer to api --- CHANGELOG.md | 3 ++- applications/services/gui/canvas.c | 1 + applications/services/gui/canvas.h | 16 ++++++++++++++++ applications/services/gui/canvas_i.h | 16 ---------------- targets/f7/api_symbols.csv | 6 ++++-- 5 files changed, 23 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a194a1f4..3d26747d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## Main changes -- Current API: 87.7 +- Current API: 87.8 * SubGHz: Add support for **42+ Keeloq based systems** (with partial Add Manually support) (see [Full list](/documentation/SubGHzSupportedSystems.md)) (by @zero-mega, @xMasterX, ARF Team) * SubGHz: Add **Allstar Firefly 318ALD31K** protocol (18 bits, Static) (PR #989 | by @jlaughter) * SubGHz: Add **Nord ICE** protocol (33 bits, Static) @@ -17,6 +17,7 @@ * OFW PR 4369: NFC: Fix stack buffer overflows in MFUL FAST_READ and DESFire file settings parsers (by @qp-x-qp) * Apps: Build tag (**29apr2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes +* UI: Add `canvas_get_buffer`, `canvas_get_buffer_size` to public API * HID App: PTT improvements (PR #996 | by @hryamzik) * UI: Wording changes to be more clear (PR #994 | by @SkeletonMan03) * SubGHz: Fix duplicate code in RAW protocol diff --git a/applications/services/gui/canvas.c b/applications/services/gui/canvas.c index 337789dd3..581bc9312 100644 --- a/applications/services/gui/canvas.c +++ b/applications/services/gui/canvas.c @@ -1,4 +1,5 @@ #include "canvas_i.h" +#include "canvas.h" #include "icon_animation_i.h" #include diff --git a/applications/services/gui/canvas.h b/applications/services/gui/canvas.h index efd314687..3bbdde83d 100644 --- a/applications/services/gui/canvas.h +++ b/applications/services/gui/canvas.h @@ -85,6 +85,22 @@ typedef enum { /** Canvas anonymous structure */ typedef struct Canvas Canvas; +/** Get canvas buffer. + * + * @param canvas Canvas instance + * + * @return pointer to buffer + */ +uint8_t* canvas_get_buffer(Canvas* canvas); + +/** Get canvas buffer size. + * + * @param canvas Canvas instance + * + * @return size of canvas in bytes + */ +size_t canvas_get_buffer_size(const Canvas* canvas); + /** Reset canvas drawing tools configuration * * @param canvas Canvas instance diff --git a/applications/services/gui/canvas_i.h b/applications/services/gui/canvas_i.h index cc918f8df..30c96b45d 100644 --- a/applications/services/gui/canvas_i.h +++ b/applications/services/gui/canvas_i.h @@ -61,22 +61,6 @@ Canvas* canvas_init(void); */ void canvas_free(Canvas* canvas); -/** Get canvas buffer. - * - * @param canvas Canvas instance - * - * @return pointer to buffer - */ -uint8_t* canvas_get_buffer(Canvas* canvas); - -/** Get canvas buffer size. - * - * @param canvas Canvas instance - * - * @return size of canvas in bytes - */ -size_t canvas_get_buffer_size(const Canvas* canvas); - /** Set drawing region relative to real screen buffer * * @param canvas Canvas instance diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index c1c6cbcef..3b6b8417d 100755 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,87.7,, +Version,+,87.8,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/applications.h,, Header,+,applications/services/bt/bt_service/bt.h,, @@ -880,6 +880,8 @@ Function,+,canvas_draw_str_aligned,void,"Canvas*, int32_t, int32_t, Align, Align Function,+,canvas_draw_triangle,void,"Canvas*, int32_t, int32_t, size_t, size_t, CanvasDirection" Function,+,canvas_draw_xbm,void,"Canvas*, int32_t, int32_t, size_t, size_t, const uint8_t*" Function,+,canvas_draw_xbm_ex,void,"Canvas*, int32_t, int32_t, size_t, size_t, IconRotation, const uint8_t*" +Function,+,canvas_get_buffer,uint8_t*,Canvas* +Function,+,canvas_get_buffer_size,size_t,const Canvas* Function,+,canvas_get_font_params,const CanvasFontParameters*,"const Canvas*, Font" Function,+,canvas_glyph_width,size_t,"Canvas*, uint16_t" Function,+,canvas_height,size_t,const Canvas* @@ -3734,8 +3736,8 @@ Function,+,subghz_protocol_encoder_raw_stop,void,void* Function,+,subghz_protocol_encoder_raw_yield,LevelDuration,void* Function,+,subghz_protocol_faac_slh_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint32_t, uint32_t, const char*, SubGhzRadioPreset*" Function,+,subghz_protocol_jarolift_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, SubGhzRadioPreset*" -Function,+,subghz_protocol_keeloq_seed_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, uint32_t, const char*, SubGhzRadioPreset*" Function,+,subghz_protocol_keeloq_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, const char*, SubGhzRadioPreset*" +Function,+,subghz_protocol_keeloq_seed_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, uint32_t, const char*, SubGhzRadioPreset*" Function,+,subghz_protocol_kinggates_stylo_4k_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, SubGhzRadioPreset*" Function,+,subghz_protocol_nice_flor_s_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, SubGhzRadioPreset*, _Bool" Function,+,subghz_protocol_phoenix_v2_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint16_t, SubGhzRadioPreset*" From 09fc86415a15e8f24b4b5820fdf6552526eb74d9 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 4 May 2026 22:59:24 +0300 Subject: [PATCH 64/71] force build params --- site_scons/commandline.scons | 4 ++-- site_scons/firmwareopts.scons | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/site_scons/commandline.scons b/site_scons/commandline.scons index c4c2fb5ab..21a02414a 100644 --- a/site_scons/commandline.scons +++ b/site_scons/commandline.scons @@ -69,7 +69,7 @@ vars.AddVariables( BoolVariable( "DEBUG", help="Enable debug build", - default=True, + default=False, ), BoolVariable( "LIB_DEBUG", @@ -79,7 +79,7 @@ vars.AddVariables( BoolVariable( "COMPACT", help="Optimize for size", - default=False, + default=True, ), EnumVariable( "TARGET_HW", diff --git a/site_scons/firmwareopts.scons b/site_scons/firmwareopts.scons index 6af861324..4ef210f89 100644 --- a/site_scons/firmwareopts.scons +++ b/site_scons/firmwareopts.scons @@ -28,7 +28,7 @@ else: "NDEBUG", ], CCFLAGS=[ - "-Og", + "-Os", ], ) From 91f4ef973edc50a71c54cdce63fb7b084b84bec4 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 5 May 2026 01:14:46 +0300 Subject: [PATCH 65/71] upd changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d26747d8..3f136db43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ * NFC: Fix CLI with NTAG4xx and Type 4 Tag support (by @WillyJL) * OFW PR 4362: NFC: **Fix BusFault** in Write to Initial Card (by @akrylysov) * OFW PR 4369: NFC: Fix stack buffer overflows in MFUL FAST_READ and DESFire file settings parsers (by @qp-x-qp) -* Apps: Build tag (**29apr2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) +* Apps: Build tag (**5may2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * UI: Add `canvas_get_buffer`, `canvas_get_buffer_size` to public API * HID App: PTT improvements (PR #996 | by @hryamzik) From 76c067f66a145b56b88a146a3276cdeb535b7666 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Wed, 6 May 2026 13:48:49 +0300 Subject: [PATCH 66/71] upd changelog --- CHANGELOG.md | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f136db43..16e83b7e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,29 +1,8 @@ ## Main changes - Current API: 87.8 -* SubGHz: Add support for **42+ Keeloq based systems** (with partial Add Manually support) (see [Full list](/documentation/SubGHzSupportedSystems.md)) (by @zero-mega, @xMasterX, ARF Team) -* SubGHz: Add **Allstar Firefly 318ALD31K** protocol (18 bits, Static) (PR #989 | by @jlaughter) -* SubGHz: Add **Nord ICE** protocol (33 bits, Static) -* SubGHz: **Better support for CAME Atomo** type remotes (TOPD4REN) (decode + button codes) (thx to Roman for raw recordings) -* SubGHz: Add **CAME TOP44FGN** support in CAME TWEE protocol -* SubGHz: Add all 0x0s and all 0xFs KeeLoq MF codes for normal and simple learning -* SubGHz: **Fix CAME TWEE repeats count for button click** -* SubGHz: Improve Nice FLO decoding (thx to Roman for raw recordings) -* NFC: Add **ISO15693-3 and SLIX write-back support** (PR #984 | by @DoniyorI) -* NFC: **Fix "MIR" and other EMV cards crash on Read** (by @Dmitry422) -* NFC: Add **Mifare Ultralight C Write Support** (by @haw8411) -* NFC: Add **new parsers SZPPK, SKPPK and SevPPK**, upgrade Plantain parser, fix TwoCities parser (PR #981 | by @mxcdoam) -* NFC: Fix CLI with NTAG4xx and Type 4 Tag support (by @WillyJL) -* OFW PR 4362: NFC: **Fix BusFault** in Write to Initial Card (by @akrylysov) -* OFW PR 4369: NFC: Fix stack buffer overflows in MFUL FAST_READ and DESFire file settings parsers (by @qp-x-qp) -* Apps: Build tag (**5may2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) +* Apps: Build tag (**6may2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes -* UI: Add `canvas_get_buffer`, `canvas_get_buffer_size` to public API -* HID App: PTT improvements (PR #996 | by @hryamzik) -* UI: Wording changes to be more clear (PR #994 | by @SkeletonMan03) -* SubGHz: Fix duplicate code in RAW protocol -* SubGHz: Fix small logic error in KingGatesStylo4k protocol (found by @X-Stuff) -* OFW PR 4364: JS: Address utf8 support when uploading JavaScript application (by @bekindpleaserewind) -* Display: Extend lcd contrast range to full ST756x 6-bit range (by @ShaTie) (reduced to -10 to +18 to avoid fully unreadable state) +* Apps: Fixed crash in SubGHz Wardriving when changing GPS baudrate with GPS module connected

#### Known NFC post-refactor regressions list: - Mifare Mini clones reading is broken (original mini working fine) (OFW) From 466c923dd14a586727143c0c82470e35da80210a Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 8 May 2026 19:05:42 +0300 Subject: [PATCH 67/71] upd changelog --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16e83b7e4..53e1f711f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,11 @@ ## Main changes - Current API: 87.8 -* Apps: Build tag (**6may2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) +* This release is the hotfix and upgrade for 2 apps below, other changes are same as in version 087 +* Apps: Moved **Nearby Files** to Base pack and **added option to set baudrate** and **add/update GPS position to extisting files** (SubGHz, NFC, RFID, iButton) +* Apps: **Fixed crash in SubGHz Wardriving** when changing GPS baudrate with GPS module connected +* Apps: Build tag (**8may2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes -* Apps: Fixed crash in SubGHz Wardriving when changing GPS baudrate with GPS module connected +* Apps: Fixed Lat/Lon placement in SubGHz Wardriving when file is saved (by @WillyJL)

#### Known NFC post-refactor regressions list: - Mifare Mini clones reading is broken (original mini working fine) (OFW) From 41628a4ce9df1289c6d34a6b7a969cee9109c032 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 9 May 2026 15:58:53 +0300 Subject: [PATCH 68/71] hotfix faac slh --- lib/subghz/protocols/faac_slh.c | 77 ++++++++++++++++----------------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/lib/subghz/protocols/faac_slh.c b/lib/subghz/protocols/faac_slh.c index fb82b1379..3e853ec02 100644 --- a/lib/subghz/protocols/faac_slh.c +++ b/lib/subghz/protocols/faac_slh.c @@ -123,6 +123,41 @@ void subghz_protocol_encoder_faac_slh_free(void* context) { free(instance); } +static bool subghz_protocol_faac_slh_encrypt(SubGhzProtocolEncoderFaacSLH* instance) { + uint32_t fix = instance->generic.serial << 4 | instance->generic.btn; + uint32_t hop = 0; + uint32_t decrypt = 0; + uint64_t man = 0; + char fixx[8] = {}; + int shiftby = 32; + + for(int i = 0; i < 8; i++) { + fixx[i] = (fix >> (shiftby -= 4)) & 0xF; + } + + if((instance->generic.cnt % 2) == 0) { + decrypt = fixx[6] << 28 | fixx[7] << 24 | fixx[5] << 20 | + (instance->generic.cnt & 0xFFFFF); + } else { + decrypt = fixx[2] << 28 | fixx[3] << 24 | fixx[4] << 20 | + (instance->generic.cnt & 0xFFFFF); + } + for + M_EACH(manufacture_code, *subghz_keystore_get_data(instance->keystore), SubGhzKeyArray_t) { + if(strcmp(furi_string_get_cstr(manufacture_code->name), "FAAC_SLH") == 0) { + //FAAC Learning + man = subghz_protocol_keeloq_common_faac_learning( + instance->generic.seed, manufacture_code->key); + hop = subghz_protocol_keeloq_common_encrypt(decrypt, man); + break; + } + } + if(hop) { + instance->generic.data = (uint64_t)fix << 32 | hop; + } + return true; +} + static bool subghz_protocol_faac_slh_gen_data(SubGhzProtocolEncoderFaacSLH* instance) { // override button if we change it with signal settings button editor // else work as standart @@ -235,16 +270,6 @@ static bool subghz_protocol_faac_slh_gen_data(SubGhzProtocolEncoderFaacSLH* inst } } } - uint32_t fix = instance->generic.serial << 4 | instance->generic.btn; - uint32_t hop = 0; - uint32_t decrypt = 0; - uint64_t man = 0; - int res = 0; - char fixx[8] = {}; - int shiftby = 32; - for(int i = 0; i < 8; i++) { - fixx[i] = (fix >> (shiftby -= 4)) & 0xF; - } if(allow_zero_seed || (instance->generic.seed != 0x0)) { // check OFEX mode @@ -275,32 +300,7 @@ static bool subghz_protocol_faac_slh_gen_data(SubGhzProtocolEncoderFaacSLH* inst } } - if((instance->generic.cnt % 2) == 0) { - decrypt = fixx[6] << 28 | fixx[7] << 24 | fixx[5] << 20 | - (instance->generic.cnt & 0xFFFFF); - } else { - decrypt = fixx[2] << 28 | fixx[3] << 24 | fixx[4] << 20 | - (instance->generic.cnt & 0xFFFFF); - } - for - M_EACH(manufacture_code, *subghz_keystore_get_data(instance->keystore), SubGhzKeyArray_t) { - res = strcmp(furi_string_get_cstr(manufacture_code->name), instance->manufacture_name); - if(res == 0) { - switch(manufacture_code->type) { - case KEELOQ_LEARNING_FAAC: - //FAAC Learning - man = subghz_protocol_keeloq_common_faac_learning( - instance->generic.seed, manufacture_code->key); - hop = subghz_protocol_keeloq_common_encrypt(decrypt, man); - break; - } - break; - } - } - if(hop) { - instance->generic.data = (uint64_t)fix << 32 | hop; - } - return true; + return subghz_protocol_faac_slh_encrypt(instance); } bool subghz_protocol_faac_slh_create_data( @@ -322,7 +322,7 @@ bool subghz_protocol_faac_slh_create_data( instance->manufacture_name = manufacture_name; instance->generic.data_count_bit = 64; allow_zero_seed = true; - bool res = subghz_protocol_faac_slh_gen_data(instance); + bool res = subghz_protocol_faac_slh_encrypt(instance); if(res) { return SubGhzProtocolStatusOk == subghz_block_generic_serialize(&instance->generic, flipper_format, preset); @@ -618,8 +618,7 @@ static void subghz_protocol_faac_slh_check_remote_controller( for M_EACH(manufacture_code, *subghz_keystore_get_data(keystore), SubGhzKeyArray_t) { - switch(manufacture_code->type) { - case KEELOQ_LEARNING_FAAC: + if(strcmp(furi_string_get_cstr(manufacture_code->name), "FAAC_SLH") == 0) { // FAAC Learning man = subghz_protocol_keeloq_common_faac_learning( instance->seed, manufacture_code->key); From 44ff715a3e2247db8864c76fc6c256be0571d355 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 9 May 2026 16:00:55 +0300 Subject: [PATCH 69/71] upd changelog --- CHANGELOG.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53e1f711f..73edec75d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,10 @@ ## Main changes - Current API: 87.8 -* This release is the hotfix and upgrade for 2 apps below, other changes are same as in version 087 -* Apps: Moved **Nearby Files** to Base pack and **added option to set baudrate** and **add/update GPS position to extisting files** (SubGHz, NFC, RFID, iButton) -* Apps: **Fixed crash in SubGHz Wardriving** when changing GPS baudrate with GPS module connected +* This release is the hotfix for the SubGHz protocol below, other changes are same as in version 088/087 +* SubGHz: **Fix FAAC SLH wrong decode/encode**, apply little code cleanup * Apps: Build tag (**8may2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes -* Apps: Fixed Lat/Lon placement in SubGHz Wardriving when file is saved (by @WillyJL) +* None

#### Known NFC post-refactor regressions list: - Mifare Mini clones reading is broken (original mini working fine) (OFW) From c5bcab305032cc0848b7223f972e193ebd3a78e8 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sun, 24 May 2026 02:12:59 +0300 Subject: [PATCH 70/71] upd changelog --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73edec75d..14f7949e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,6 @@ ## Main changes - Current API: 87.8 -* This release is the hotfix for the SubGHz protocol below, other changes are same as in version 088/087 -* SubGHz: **Fix FAAC SLH wrong decode/encode**, apply little code cleanup -* Apps: Build tag (**8may2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) +* Apps: Build tag (**24may2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * None

From 78cbcab365895e9efc5304253d7feb7e472500ec Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sun, 31 May 2026 00:56:45 +0300 Subject: [PATCH 71/71] bump apps pack --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14f7949e7..5673ef3ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## Main changes - Current API: 87.8 -* Apps: Build tag (**24may2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) +* Apps: Build tag (**30may2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * None