Compare commits

..
Author SHA1 Message Date
d4rks1d33 833c9ad29b Custom emulate on receiver when using full dpad without saving the signal
Build Dev Firmware / build (push) Waiting to run
2026-07-23 22:11:38 -03:00
d4rks1d33 8117e422d0 fixes
Build Dev Firmware / build (push) Waiting to run
2026-07-23 21:42:29 -03:00
2 changed files with 194 additions and 24 deletions
@@ -9,11 +9,13 @@
*/
#include "../subghz_i.h"
#include "../views/subghz_car_emulate.h"
#include "../helpers/subghz_button_labels.h"
#include "../helpers/subghz_custom_event.h"
#include <lib/subghz/blocks/generic.h>
#include <notification/notification_messages.h>
#include "../helpers/subghz_txrx_i.h"
#include <lib/subghz/blocks/custom_btn_i.h>
#include <string.h>
#define TAG "SubGhzSceneCarEmulate"
#define MIN_TX_TICKS 66U /* ~666 ms at 100 ms tick */
@@ -29,13 +31,17 @@ typedef struct {
uint32_t freq;
char preset_short[12]; /* "AM650", "FM476", … */
bool has_serial;
bool has_counter;
/* TX state */
bool is_transmitting;
bool stop_pending; /* stop requested before MIN_TX_TICKS elapsed */
uint32_t tx_start_tick;
/* Pending button key (InputKey) decoded from the packed custom event */
/* Resolved custom button repeated while the physical key is held */
uint8_t pending_button;
uint8_t max_custom_button;
} CarEmulateState;
static CarEmulateState* s_state = NULL;
@@ -201,8 +207,101 @@ static void car_emulate_read_freq_preset(SubGhz* subghz, CarEmulateState* st) {
furi_string_free(preset_str);
}
static bool car_emulate_hex_digit(char c, uint8_t* value) {
if(c >= '0' && c <= '9') {
*value = c - '0';
return true;
} else if(c >= 'a' && c <= 'f') {
*value = c - 'a' + 10;
return true;
} else if(c >= 'A' && c <= 'F') {
*value = c - 'A' + 10;
return true;
}
return false;
}
static bool car_emulate_parse_hex_after(
const char* text,
const char* marker,
uint32_t* value) {
const char* field = strstr(text, marker);
if(!field) return false;
field += strlen(marker);
while(*field == ' ' || *field == '\t' || *field == '[') {
field++;
}
if(field[0] == '0' && (field[1] == 'x' || field[1] == 'X')) {
field += 2;
}
uint32_t parsed = 0;
uint8_t digits = 0;
uint8_t digit_value = 0;
while((digits < 8) && car_emulate_hex_digit(*field, &digit_value)) {
parsed = (parsed << 4) | digit_value;
digits++;
field++;
}
if(digits == 0) return false;
*value = parsed;
return true;
}
static bool car_emulate_serial_is_placeholder(void) {
return !s_state->has_serial || (s_state->serial <= 1);
}
static bool car_emulate_counter_is_placeholder(void) {
return !s_state->has_counter || (s_state->current_counter == 0);
}
static void car_emulate_set_counter(uint32_t value) {
s_state->original_counter = value;
s_state->current_counter = value;
s_state->has_counter = true;
}
static void car_emulate_apply_global_metadata(void) {
furi_assert(s_state);
if(subghz_block_generic_global.cnt_is_available) {
car_emulate_set_counter(subghz_block_generic_global.current_cnt);
}
}
static void car_emulate_apply_decoded_metadata(FuriString* decoded_text) {
furi_assert(s_state);
if(!decoded_text) return;
const char* text = furi_string_get_cstr(decoded_text);
uint32_t value = 0;
if((car_emulate_parse_hex_after(text, "Sn:", &value) ||
car_emulate_parse_hex_after(text, "SN:", &value) ||
car_emulate_parse_hex_after(text, "Ser:", &value) ||
car_emulate_parse_hex_after(text, "Serial:", &value)) &&
(value != 0)) {
s_state->serial = value;
s_state->has_serial = true;
} else if(
car_emulate_serial_is_placeholder() &&
car_emulate_parse_hex_after(text, "Fix:", &value) &&
(value != 0)) {
s_state->serial = value;
s_state->has_serial = true;
}
if(car_emulate_parse_hex_after(text, "Cnt:", &value) &&
((value != 0) || car_emulate_counter_is_placeholder())) {
car_emulate_set_counter(value);
}
}
/** Update Btn and Cnt fields in fff_data so the transmitter re-serialises them. */
static void car_emulate_apply_button(SubGhz* subghz, InputKey key) {
static bool car_emulate_apply_button(SubGhz* subghz, InputKey key) {
UNUSED(subghz);
uint8_t custom_btn_id;
@@ -215,7 +314,11 @@ static void car_emulate_apply_button(SubGhz* subghz, InputKey key) {
default: custom_btn_id = SUBGHZ_CUSTOM_BTN_OK; break;
}
subghz_custom_btn_set(custom_btn_id);
if(custom_btn_id > s_state->max_custom_button) {
return false;
}
return subghz_custom_btn_set(custom_btn_id);
}
@@ -252,12 +355,34 @@ static bool car_emulate_start_tx(SubGhz* subghz, uint8_t custom_btn_id) {
/** Stop an active transmission. */
static void car_emulate_stop_tx(SubGhz* subghz) {
subghz_block_generic_global.endless_tx = false;
subghz_txrx_stop(subghz->txrx);
subghz->state_notifications = SubGhzNotificationStateIDLE;
notification_message(subghz->notifications, &sequence_blink_stop);
FURI_LOG_I(TAG, "TX stopped");
}
static bool car_emulate_restart_tx(SubGhz* subghz) {
furi_assert(s_state);
car_emulate_update_fff(subghz, s_state->current_counter);
subghz_block_generic_global.endless_tx = true;
if(car_emulate_start_tx(subghz, s_state->pending_button)) {
s_state->is_transmitting = true;
subghz->state_notifications = SubGhzNotificationStateTx;
notification_message(subghz->notifications, &sequence_blink_magenta_10);
FURI_LOG_D(TAG, "TX restarted while button is held");
return true;
}
subghz_block_generic_global.endless_tx = false;
s_state->is_transmitting = false;
s_state->stop_pending = false;
notification_message(subghz->notifications, &sequence_error);
return false;
}
/* ═══════════════════════════════════════════════════════════════════════════
* View callback (fired from the View's input handler)
* ═════════════════════════════════════════════════════════════════════════*/
@@ -293,6 +418,7 @@ void subghz_scene_car_emulate_on_enter(void* context) {
s_state = malloc(sizeof(CarEmulateState));
furi_check(s_state);
memset(s_state, 0, sizeof(CarEmulateState));
subghz_block_generic_global.endless_tx = false;
/* ── Read metadata from the loaded fff_data ── */
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
@@ -308,7 +434,8 @@ void subghz_scene_car_emulate_on_enter(void* context) {
}
flipper_format_rewind(fff);
flipper_format_read_uint32(fff, "Serial", &s_state->serial, 1);
s_state->has_serial =
flipper_format_read_uint32(fff, "Serial", &s_state->serial, 1);
flipper_format_rewind(fff);
uint32_t btn_tmp = 0;
@@ -317,7 +444,8 @@ void subghz_scene_car_emulate_on_enter(void* context) {
}
flipper_format_rewind(fff);
flipper_format_read_uint32(fff, "Cnt", &s_state->original_counter, 1);
s_state->has_counter =
flipper_format_read_uint32(fff, "Cnt", &s_state->original_counter, 1);
s_state->current_counter = s_state->original_counter;
furi_string_free(tmp);
@@ -333,24 +461,58 @@ void subghz_scene_car_emulate_on_enter(void* context) {
* - subghz_custom_btn_get_original() → the button that was in the file
* - subghz_custom_btn_is_allowed() → true if protocol supports it
* - subghz_custom_btn_get_max() → number of buttons available */
subghz_block_generic_global_reset(NULL);
subghz_custom_btns_reset();
SubGhzProtocolDecoderBase* decoder = subghz_txrx_get_decoder(subghz->txrx);
if(decoder && fff) {
flipper_format_rewind(fff);
subghz_protocol_decoder_base_deserialize(decoder, fff);
if(subghz_protocol_decoder_base_deserialize(decoder, fff) == SubGhzProtocolStatusOk) {
FuriString* decoded_text = furi_string_alloc();
subghz_protocol_decoder_base_get_string(decoder, decoded_text);
car_emulate_apply_global_metadata();
car_emulate_apply_decoded_metadata(decoded_text);
furi_string_free(decoded_text);
}
/* Rewind again so subsequent reads in car_emulate_read_freq_preset()
* start from the beginning of the file. */
flipper_format_rewind(fff);
}
subghz_car_emulate_view_set_labels(
s_state->max_custom_button =
subghz_custom_btn_is_allowed() ? subghz_custom_btn_get_max() : SUBGHZ_CUSTOM_BTN_OK;
if(s_state->max_custom_button == SUBGHZ_CUSTOM_BTN_OK) {
s_state->max_custom_button =
subghz_button_labels_get_max_custom_btn(s_state->protocol_name);
if(s_state->max_custom_button != SUBGHZ_CUSTOM_BTN_OK) {
subghz_custom_btn_set_max(s_state->max_custom_button);
}
}
const char* button_labels[SUBGHZ_BUTTON_LABEL_COUNT];
subghz_button_labels_reset(button_labels);
subghz_button_labels_apply_protocol(s_state->protocol_name, button_labels);
subghz_car_emulate_view_set_labels(
subghz->car_emulate_view,
"UNLOCK", /* OK */
"LOCK", /* Up */
"TRUNK", /* Down */
"PANIC", /* Left */
"START" /* Right */
subghz_button_labels_get(
button_labels, SUBGHZ_CUSTOM_BTN_OK, subghz_custom_btn_get_original()),
s_state->max_custom_button >= SUBGHZ_CUSTOM_BTN_UP ?
subghz_button_labels_get(
button_labels, SUBGHZ_CUSTOM_BTN_UP, subghz_custom_btn_get_original()) :
"",
s_state->max_custom_button >= SUBGHZ_CUSTOM_BTN_DOWN ?
subghz_button_labels_get(
button_labels, SUBGHZ_CUSTOM_BTN_DOWN, subghz_custom_btn_get_original()) :
"",
s_state->max_custom_button >= SUBGHZ_CUSTOM_BTN_LEFT ?
subghz_button_labels_get(
button_labels, SUBGHZ_CUSTOM_BTN_LEFT, subghz_custom_btn_get_original()) :
"",
s_state->max_custom_button >= SUBGHZ_CUSTOM_BTN_RIGHT ?
subghz_button_labels_get(
button_labels, SUBGHZ_CUSTOM_BTN_RIGHT, subghz_custom_btn_get_original()) :
""
);
car_emulate_read_freq_preset(subghz, s_state);
@@ -384,14 +546,18 @@ bool subghz_scene_car_emulate_on_event(void* context, SceneManagerEvent event) {
car_emulate_stop_tx(subghz);
}
/* Bump counter */
s_state->current_counter++;
/* Set the custom button BEFORE deserialize() is called inside
* subghz_tx_start() → subghz_txrx_tx_start().
* The protocol's deserialize() will call subghz_custom_btn_get()
* to pick the right button code. */
car_emulate_apply_button(subghz, key);
if(!car_emulate_apply_button(subghz, key)) {
notification_message(subghz->notifications, &sequence_error);
car_emulate_refresh_view(subghz);
return true;
}
/* Bump counter */
s_state->current_counter++;
/* Only update the counter in fff_data; the protocol handles Btn. */
car_emulate_update_fff(subghz, s_state->current_counter);
@@ -400,9 +566,9 @@ bool subghz_scene_car_emulate_on_event(void* context, SceneManagerEvent event) {
s_state->stop_pending = false;
s_state->tx_start_tick = (uint32_t)furi_get_tick();
subghz_block_generic_global.endless_tx = true;
uint8_t cur_btn = subghz_custom_btn_get();
s_state->pending_button = cur_btn;
subghz_block_generic_global.endless_tx = true;
if(!car_emulate_start_tx(subghz, cur_btn)) {
s_state->is_transmitting = false;
subghz_block_generic_global.endless_tx = false;
@@ -414,13 +580,13 @@ bool subghz_scene_car_emulate_on_event(void* context, SceneManagerEvent event) {
/* ── Stop ── */
} else if(event.event == SubGhzCustomEventCarEmulateStop) {
subghz_block_generic_global.endless_tx = false;
if(s_state->is_transmitting &&
subghz->state_notifications == SubGhzNotificationStateTx) {
if(s_state->is_transmitting) {
subghz_block_generic_global.endless_tx = false;
uint32_t elapsed = (uint32_t)furi_get_tick() - s_state->tx_start_tick;
if(elapsed >= MIN_TX_TICKS) {
if(
elapsed >= MIN_TX_TICKS &&
subghz->state_notifications == SubGhzNotificationStateTx) {
car_emulate_stop_tx(subghz);
s_state->is_transmitting = false;
s_state->stop_pending = false;
@@ -436,6 +602,7 @@ bool subghz_scene_car_emulate_on_event(void* context, SceneManagerEvent event) {
if(subghz->state_notifications == SubGhzNotificationStateTx) {
car_emulate_stop_tx(subghz);
}
subghz_block_generic_global.endless_tx = false;
scene_manager_search_and_switch_to_previous_scene(
subghz->scene_manager, SubGhzSceneSavedMenu);
consumed = true;
@@ -455,6 +622,8 @@ bool subghz_scene_car_emulate_on_event(void* context, SceneManagerEvent event) {
s_state->is_transmitting = false;
s_state->stop_pending = false;
notification_message(subghz->notifications, &sequence_blink_stop);
} else {
car_emulate_restart_tx(subghz);
}
} else {
/* Still transmitting blink LED */
@@ -490,6 +659,7 @@ void subghz_scene_car_emulate_on_exit(void* context) {
car_emulate_stop_tx(subghz);
}
subghz_block_generic_global.endless_tx = false;
subghz->state_notifications = SubGhzNotificationStateIDLE;
notification_message(subghz->notifications, &sequence_blink_stop);
@@ -189,7 +189,7 @@ bool subghz_scene_receiver_info_on_event(void* context, SceneManagerEvent event)
stream_copy_full(src, dst);
stream_seek(dst, 0, StreamOffsetFromStart);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneTransmitter);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneCarEmulate);
return true;
}