From 8ef9a07608949513e4c30fb990da4b3195a8f89b Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Wed, 9 Jul 2025 04:54:34 +0300 Subject: [PATCH] Subghz V2 Phoenix fully supported now With big thanks to all contributors 2022.08 - @Skorpionm 2025.07 - @xMasterX & @RocketGod-git --- .../main/subghz/helpers/subghz_custom_event.h | 1 + .../helpers/subghz_txrx_create_protocol_key.c | 28 ++ .../helpers/subghz_txrx_create_protocol_key.h | 7 + .../subghz/scenes/subghz_scene_set_type.c | 30 +- lib/subghz/protocols/phoenix_v2.c | 280 +++++++++++++++++- lib/subghz/protocols/public_api.h | 16 + targets/f7/api_symbols.csv | 1 + 7 files changed, 352 insertions(+), 11 deletions(-) diff --git a/applications/main/subghz/helpers/subghz_custom_event.h b/applications/main/subghz/helpers/subghz_custom_event.h index a3dae60b8..e971bd056 100644 --- a/applications/main/subghz/helpers/subghz_custom_event.h +++ b/applications/main/subghz/helpers/subghz_custom_event.h @@ -74,6 +74,7 @@ typedef enum { SetTypeSomfyTelis, SetTypeANMotorsAT4, SetTypeAlutechAT4N, + SetTypePhoenix_V2_433, SetTypeHCS101_433_92, SetTypeDoorHan_315_00, SetTypeDoorHan_433_92, 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 813b706b6..5a7e07e0e 100644 --- a/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.c +++ b/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.c @@ -384,6 +384,34 @@ bool subghz_txrx_gen_secplus_v1_protocol( return ret; } +bool subghz_txrx_gen_phoenix_v2_protocol( + void* context, + const char* preset_name, + uint32_t frequency, + uint32_t serial, + uint16_t cnt) { + SubGhzTxRx* txrx = context; + + bool res = false; + + txrx->transmitter = + subghz_transmitter_alloc_init(txrx->environment, SUBGHZ_PROTOCOL_PHOENIX_V2_NAME); + subghz_txrx_set_preset(txrx, preset_name, frequency, NULL, 0); + + if(txrx->transmitter && subghz_protocol_phoenix_v2_create_data( + subghz_transmitter_get_protocol_instance(txrx->transmitter), + txrx->fff_data, + serial, + cnt, + txrx->preset)) { + res = true; + } + + subghz_transmitter_free(txrx->transmitter); + + return res; +} + void subghz_txrx_gen_serial_gangqi(uint64_t* result_key) { uint64_t randkey = (uint64_t)rand(); uint16_t serial = (uint16_t)((randkey) & 0xFFFF); 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 fba7acb6f..7daa61b31 100644 --- a/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.h +++ b/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.h @@ -115,6 +115,13 @@ bool subghz_txrx_gen_came_atomo_protocol( uint32_t serial, uint16_t cnt); +bool subghz_txrx_gen_phoenix_v2_protocol( + void* context, + const char* preset_name, + uint32_t frequency, + uint32_t serial, + uint16_t cnt); + /** * Generate data SecPlus v2 protocol * diff --git a/applications/main/subghz/scenes/subghz_scene_set_type.c b/applications/main/subghz/scenes/subghz_scene_set_type.c index 134d2e1d1..3f2fd9eff 100644 --- a/applications/main/subghz/scenes/subghz_scene_set_type.c +++ b/applications/main/subghz/scenes/subghz_scene_set_type.c @@ -20,6 +20,7 @@ static const char* submenu_names[SetTypeMAX] = { [SetTypeSomfyTelis] = "Somfy Telis 433MHz", [SetTypeANMotorsAT4] = "AN-Motors AT4 433MHz", [SetTypeAlutechAT4N] = "Alutech AT4N 433MHz", + [SetTypePhoenix_V2_433] = "V2 Phoenix 433MHz", [SetTypeHCS101_433_92] = "KL: HCS101 433MHz", [SetTypeDoorHan_315_00] = "KL: DoorHan 315MHz", [SetTypeDoorHan_433_92] = "KL: DoorHan 433MHz", @@ -112,6 +113,7 @@ typedef enum { GenNiceFlorS, GenSecPlus1, GenSecPlus2, + GenPhoenixV2, } GenType; typedef struct { @@ -170,6 +172,10 @@ typedef struct { uint8_t btn; uint32_t cnt; } sec_plus_2; + struct { + uint32_t serial; + uint16_t cnt; + } phoenix_v2; }; } GenInfo; @@ -634,16 +640,16 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) { .type = GenCameAtomo, .mod = "AM650", .freq = 433920000, - .keeloq.serial = (key & 0x0FFFFFFF) | 0x10000000, - .keeloq.cnt = 0x03}; + .came_atomo.serial = (key & 0x0FFFFFFF) | 0x10000000, + .came_atomo.cnt = 0x03}; break; case SetTypeCameAtomo868: gen_info = (GenInfo){ .type = GenCameAtomo, .mod = "AM650", .freq = 868350000, - .keeloq.serial = (key & 0x0FFFFFFF) | 0x10000000, - .keeloq.cnt = 0x03}; + .came_atomo.serial = (key & 0x0FFFFFFF) | 0x10000000, + .came_atomo.cnt = 0x03}; break; case SetTypeBFTMitto: gen_info = (GenInfo){ @@ -869,6 +875,14 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) { .sec_plus_2.btn = 0x68, .sec_plus_2.cnt = 0xE500000}; break; + case SetTypePhoenix_V2_433: + gen_info = (GenInfo){ + .type = GenPhoenixV2, + .mod = "AM650", + .freq = 433920000, + .phoenix_v2.serial = (key & 0x0FFFFFFF) | 0xB0000000, + .phoenix_v2.cnt = 0x025D}; + break; default: furi_crash("Not implemented"); break; @@ -976,6 +990,14 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) { gen_info.sec_plus_2.btn, gen_info.sec_plus_2.cnt); break; + case GenPhoenixV2: + generated_protocol = subghz_txrx_gen_phoenix_v2_protocol( + subghz->txrx, + gen_info.mod, + gen_info.freq, + gen_info.phoenix_v2.serial, + gen_info.phoenix_v2.cnt); + break; default: furi_crash("Not implemented"); break; diff --git a/lib/subghz/protocols/phoenix_v2.c b/lib/subghz/protocols/phoenix_v2.c index 9e88324c4..0229aeaeb 100644 --- a/lib/subghz/protocols/phoenix_v2.c +++ b/lib/subghz/protocols/phoenix_v2.c @@ -6,9 +6,9 @@ #include "../blocks/generic.h" #include "../blocks/math.h" -#define TAG "SubGhzProtocolPhoenixV2" +#include "../blocks/custom_btn_i.h" -//transmission only static mode +#define TAG "SubGhzProtocolPhoenixV2" static const SubGhzBlockConst subghz_protocol_phoenix_v2_const = { .te_short = 427, @@ -62,7 +62,7 @@ const SubGhzProtocolEncoder subghz_protocol_phoenix_v2_encoder = { const SubGhzProtocol subghz_protocol_phoenix_v2 = { .name = SUBGHZ_PROTOCOL_PHOENIX_V2_NAME, - .type = SubGhzProtocolTypeStatic, + .type = SubGhzProtocolTypeDynamic, .flag = SubGhzProtocolFlag_433 | SubGhzProtocolFlag_AM | SubGhzProtocolFlag_Decodable | SubGhzProtocolFlag_Load | SubGhzProtocolFlag_Save | SubGhzProtocolFlag_Send, @@ -91,6 +91,138 @@ void subghz_protocol_encoder_phoenix_v2_free(void* context) { free(instance); } +// Pre define functions +static uint16_t subghz_protocol_phoenix_v2_encrypt_counter(uint64_t full_key, uint16_t counter); +static void subghz_protocol_phoenix_v2_check_remote_controller(SubGhzBlockGeneric* instance); + +bool subghz_protocol_phoenix_v2_create_data( + void* context, + FlipperFormat* flipper_format, + uint32_t serial, + uint16_t cnt, + SubGhzRadioPreset* preset) { + furi_assert(context); + SubGhzProtocolEncoderPhoenix_V2* instance = context; + instance->generic.btn = 0x1; + instance->generic.serial = serial; + instance->generic.cnt = cnt; + instance->generic.data_count_bit = 52; + + uint64_t local_data_rev = + (uint64_t)(((uint64_t)instance->generic.cnt << 40) | + ((uint64_t)instance->generic.btn << 32) | (uint64_t)instance->generic.serial); + + uint16_t encrypted_counter = (uint16_t)subghz_protocol_phoenix_v2_encrypt_counter( + local_data_rev, instance->generic.cnt); + + instance->generic.data = subghz_protocol_blocks_reverse_key( + (uint64_t)(((uint64_t)encrypted_counter << 40) | ((uint64_t)instance->generic.btn << 32) | + (uint64_t)instance->generic.serial), + instance->generic.data_count_bit + 4); + + return SubGhzProtocolStatusOk == + subghz_block_generic_serialize(&instance->generic, flipper_format, preset); +} + +// Get custom button code +static uint8_t subghz_protocol_phoenix_v2_get_btn_code(void) { + uint8_t custom_btn_id = subghz_custom_btn_get(); + uint8_t original_btn_code = subghz_custom_btn_get_original(); + uint8_t btn = original_btn_code; + + // Set custom button + if((custom_btn_id == SUBGHZ_CUSTOM_BTN_OK) && (original_btn_code != 0)) { + // Restore original button code + btn = original_btn_code; + } else if(custom_btn_id == SUBGHZ_CUSTOM_BTN_UP) { + switch(original_btn_code) { + case 0x1: + btn = 0x2; + break; + case 0x2: + btn = 0x1; + break; + case 0x4: + btn = 0x1; + break; + case 0x8: + btn = 0x1; + break; + case 0x3: + btn = 0x1; + break; + + default: + break; + } + } else if(custom_btn_id == SUBGHZ_CUSTOM_BTN_DOWN) { + switch(original_btn_code) { + case 0x1: + btn = 0x4; + break; + case 0x2: + btn = 0x4; + break; + case 0x4: + btn = 0x2; + break; + case 0x8: + btn = 0x4; + break; + case 0x3: + btn = 0x4; + break; + + default: + break; + } + } else if(custom_btn_id == SUBGHZ_CUSTOM_BTN_LEFT) { + switch(original_btn_code) { + case 0x1: + btn = 0x8; + break; + case 0x2: + btn = 0x8; + break; + case 0x4: + btn = 0x8; + break; + case 0x8: + btn = 0x2; + break; + case 0x3: + btn = 0x8; + break; + + default: + break; + } + } else if(custom_btn_id == SUBGHZ_CUSTOM_BTN_RIGHT) { + switch(original_btn_code) { + case 0x1: + btn = 0x3; + break; + case 0x2: + btn = 0x3; + break; + case 0x4: + btn = 0x3; + break; + case 0x8: + btn = 0x3; + break; + case 0x3: + btn = 0x2; + break; + + default: + break; + } + } + + return btn; +} + /** * Generating an upload from data. * @param instance Pointer to a SubGhzProtocolEncoderPhoenix_V2 instance @@ -107,6 +239,40 @@ static bool } else { instance->encoder.size_upload = size_upload; } + + uint8_t btn = instance->generic.btn; + + // Save original button for later use + if(subghz_custom_btn_get_original() == 0) { + subghz_custom_btn_set_original(btn); + } + + // Get custom button code + // This will override the btn variable if a custom button is set + btn = subghz_protocol_phoenix_v2_get_btn_code(); + + // Reconstruction of the data + if(instance->generic.cnt < 0xFFFF) { + if((instance->generic.cnt + furi_hal_subghz_get_rolling_counter_mult()) > 0xFFFF) { + instance->generic.cnt = 0; + } else { + instance->generic.cnt += furi_hal_subghz_get_rolling_counter_mult(); + } + } else if((instance->generic.cnt >= 0xFFFF) && (furi_hal_subghz_get_rolling_counter_mult() != 0)) { + instance->generic.cnt = 0; + } + + uint64_t local_data_rev = subghz_protocol_blocks_reverse_key( + instance->generic.data, instance->generic.data_count_bit + 4); + + uint16_t encrypted_counter = (uint16_t)subghz_protocol_phoenix_v2_encrypt_counter( + local_data_rev, instance->generic.cnt); + + instance->generic.data = subghz_protocol_blocks_reverse_key( + (uint64_t)(((uint64_t)encrypted_counter << 40) | ((uint64_t)btn << 32) | + (uint64_t)instance->generic.serial), + instance->generic.data_count_bit + 4); + //Send header instance->encoder.upload[index++] = level_duration_make(false, (uint32_t)subghz_protocol_phoenix_v2_const.te_short * 60); @@ -149,10 +315,22 @@ SubGhzProtocolStatus flipper_format_read_uint32( flipper_format, "Repeat", (uint32_t*)&instance->encoder.repeat, 1); + subghz_protocol_phoenix_v2_check_remote_controller(&instance->generic); + if(!subghz_protocol_encoder_phoenix_v2_get_upload(instance)) { ret = SubGhzProtocolStatusErrorEncoderGetUpload; break; } + + uint8_t key_data[sizeof(uint64_t)] = {0}; + for(size_t i = 0; i < sizeof(uint64_t); i++) { + key_data[sizeof(uint64_t) - i - 1] = (instance->generic.data >> i * 8) & 0xFF; + } + if(!flipper_format_update_hex(flipper_format, "Key", key_data, sizeof(uint64_t))) { + FURI_LOG_E(TAG, "Unable to add Key"); + break; + } + instance->encoder.is_running = true; } while(false); @@ -274,16 +452,103 @@ void subghz_protocol_decoder_phoenix_v2_feed(void* context, bool level, uint32_t } } +static uint16_t subghz_protocol_phoenix_v2_encrypt_counter(uint64_t full_key, uint16_t counter) { + uint8_t xor_key1 = (uint8_t)(full_key >> 24); // First byte of serial + uint8_t xor_key2 = (uint8_t)((full_key >> 16) & 0xFF); // Second byte of serial + + uint8_t byte2 = (uint8_t)(counter >> 8); // First counter byte + uint8_t byte1 = (uint8_t)(counter & 0xFF); // Second counter byte + + // See decrypt function before reading these comments + for(int i = 0; i < 16; i++) { + // The key to reversing the process is that the MSB of the *current* byte2 + // tells us what the MSB of the *previous* byte1 was. This allows us to + // determine if the conditional XOR was applied before?. + uint8_t msb_of_prev_byte1 = byte2 & 0x80; + + if(msb_of_prev_byte1 == 0) { + // reverse the XOR. + byte2 ^= xor_key2; + byte1 ^= xor_key1; + } + + // Perform the bit shuffle in reverse + // Store the least significant bit (LSB) of the current byte1. + uint8_t lsb_of_current_byte1 = byte1 & 1; + + byte2 = (byte2 << 1) | lsb_of_current_byte1; + byte1 = (byte1 >> 1) | msb_of_prev_byte1; + } + + return (uint16_t)byte1 << 8 | byte2; +} + +static uint16_t subghz_protocol_phoenix_v2_decrypt_counter(uint64_t full_key) { + uint16_t encrypted_value = (uint16_t)((full_key >> 40) & 0xFFFF); + + uint8_t byte1 = (uint8_t)(encrypted_value >> 8); // First encrypted counter byte + uint8_t byte2 = (uint8_t)(encrypted_value & 0xFF); // Second encrypted counter byte + + uint8_t xor_key1 = (uint8_t)(full_key >> 24); // First byte of serial + uint8_t xor_key2 = (uint8_t)((full_key >> 16) & 0xFF); // Second byte of serial + + for(int i = 0; i < 16; i++) { + // Store the most significant bit (MSB) of byte1. + // The check `(msb_of_byte1 == 0)` will determine if we apply the XOR keys. + uint8_t msb_of_byte1 = byte1 & 0x80; + + // Store the least significant bit (LSB) of byte2. + uint8_t lsb_of_byte2 = byte2 & 1; + + // Perform a bit shuffle between the two bytes + byte2 = (byte2 >> 1) | msb_of_byte1; + byte1 = (byte1 << 1) | lsb_of_byte2; + + // Conditionally apply the XOR keys based on the original MSB of byte1. + if(msb_of_byte1 == 0) { + byte1 ^= xor_key1; + // The mask `& 0x7F` clears the MSB of byte2 after the XOR. + byte2 = (byte2 ^ xor_key2) & 0x7F; + } + } + + return (uint16_t)byte2 << 8 | byte1; +} + /** * Analysis of received data * @param instance Pointer to a SubGhzBlockGeneric* instance */ static void subghz_protocol_phoenix_v2_check_remote_controller(SubGhzBlockGeneric* instance) { + // 2022.08 - @Skorpionm + // 2025.07 - @xMasterX & @RocketGod-git + // Fully supported now, with button switch and add manually + // + // Key samples + // Full key example: 0xC63E01B9615720 - after subghz_protocol_blocks_reverse_key was applied + // Serial - B9615720 + // Button - 01 + // Encrypted -> Decrypted counters + // C63E - 025C + // BCC1 - 025D + // 3341 - 025E + // 49BE - 025F + // 99D3 - 0260 + // E32C - 0261 + uint64_t data_rev = subghz_protocol_blocks_reverse_key(instance->data, instance->data_count_bit + 4); + instance->serial = data_rev & 0xFFFFFFFF; - instance->cnt = (data_rev >> 40) & 0xFFFF; + instance->cnt = subghz_protocol_phoenix_v2_decrypt_counter(data_rev); instance->btn = (data_rev >> 32) & 0xF; + // encrypted cnt is (data_rev >> 40) & 0xFFFF + + // Save original button for later use + if(subghz_custom_btn_get_original() == 0) { + subghz_custom_btn_set_original(instance->btn); + } + subghz_custom_btn_set_max(4); } uint8_t subghz_protocol_decoder_phoenix_v2_get_hash_data(void* context) { @@ -321,12 +586,13 @@ void subghz_protocol_decoder_phoenix_v2_get_string(void* context, FuriString* ou "%s %dbit\r\n" "Key:%05lX%08lX\r\n" "Sn:0x%07lX \r\n" - "Btn:%X Cnt: 0x%04lX\r\n", + "Cnt: 0x%04lX\r\n" + "Btn: %X\r\n", instance->generic.protocol_name, instance->generic.data_count_bit, (uint32_t)(instance->generic.data >> 32) & 0xFFFFFFFF, (uint32_t)(instance->generic.data & 0xFFFFFFFF), instance->generic.serial, - instance->generic.btn, - instance->generic.cnt); + instance->generic.cnt, + instance->generic.btn); } diff --git a/lib/subghz/protocols/public_api.h b/lib/subghz/protocols/public_api.h index d7ae21c2a..39c08e6aa 100644 --- a/lib/subghz/protocols/public_api.h +++ b/lib/subghz/protocols/public_api.h @@ -123,6 +123,22 @@ bool subghz_protocol_came_atomo_create_data( uint16_t cnt, SubGhzRadioPreset* preset); +/** + * Key generation from simple data. + * @param context Pointer to a SubGhzProtocolEncoderPhoenix_V2 instance + * @param flipper_format Pointer to a FlipperFormat instance + * @param serial Serial number + * @param cnt Counter value, 16 bit + * @param preset Modulation, SubGhzRadioPreset + * @return true On success + */ +bool subghz_protocol_phoenix_v2_create_data( + void* context, + FlipperFormat* flipper_format, + uint32_t serial, + uint16_t cnt, + SubGhzRadioPreset* preset); + /** * New remote generation. * @param context Pointer to a SubGhzProtocolEncoderNiceFlorS instance diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index d0cea83e2..7696a05bc 100755 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -3621,6 +3621,7 @@ Function,+,subghz_protocol_faac_slh_create_data,_Bool,"void*, FlipperFormat*, ui 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_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, const char*, 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*" Function,+,subghz_protocol_raw_file_encoder_worker_set_callback_end,void,"SubGhzProtocolEncoderRAW*, SubGhzProtocolEncoderRAWCallbackEnd, void*" Function,+,subghz_protocol_raw_gen_fff_data,void,"FlipperFormat*, const char*, const char*" Function,+,subghz_protocol_raw_get_sample_write,size_t,SubGhzProtocolDecoderRAW*