Compare commits

..

8 Commits

Author SHA1 Message Date
d4rks1d33 86f5aae002 Remove spanish comments
Build Dev Firmware / build (push) Failing after 14m41s
2026-05-18 22:54:40 -03:00
d4rks1d33 46f3a5c993 More updates :D 2026-05-18 22:53:07 -03:00
D4rk$1d3 52015fb289 Update README
Build Dev Firmware / build (push) Failing after 16s
Added images for Custom Emulation Settings and Scene.
2026-05-18 20:52:27 -03:00
D4rk$1d3 23ba62cd69 Add files via upload 2026-05-18 20:49:21 -03:00
d4rks1d33 cd1e9d6945 Stupid chatGPT that add utm_source=chatgpt.com on links
Build Dev Firmware / build (push) Failing after 18s
2026-05-18 20:39:55 -03:00
d4rks1d33 c49b843096 Stupid chatGPT that add utm_source=chatgpt.com on links 2026-05-18 20:37:13 -03:00
d4rks1d33 0c35337bb7 Some updates 2026-05-18 20:33:56 -03:00
d4rks1d33 e419b9865a Rollback
Build Dev Firmware / build (push) Failing after 16s
2026-05-08 16:21:24 +00:00
26 changed files with 2372 additions and 63 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

+9 -10
View File
@@ -1,48 +1,47 @@
name: Build Dev Firmware
on:
push:
branches:
- main
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build firmware
run: |
export DIST_SUFFIX=Flipper-ARF
chmod +x fbt
./fbt COMPACT=1 DEBUG=0 updater_package
- name: Generate tag name
id: tag
run: echo "TAG=dev-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Detect firmware updater
id: firmware
run: |
DIR=$(ls -d dist/f7-* | head -n 1)
FILE="$DIR/flipper-z-f7-update-Flipper-ARF.tgz"
if [ ! -f "$FILE" ]; then
echo "Firmware file not found!"
exit 1
fi
echo "FILE=$FILE" >> $GITHUB_OUTPUT
- name: Read changelog
id: changelog
run: |
{
echo 'CHANGELOG<<EOF'
cat CHANGELOG.md
echo 'EOF'
} >> "$GITHUB_OUTPUT"
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.tag.outputs.TAG }}
name: Dev Build ${{ steps.tag.outputs.TAG }}
body: ${{ steps.changelog.outputs.CHANGELOG }}
files: ${{ steps.firmware.outputs.FILE }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+19
View File
@@ -0,0 +1,19 @@
# Changelog
---
### Added
- Protocol name allowlist filter: in Receiver Config, a new "Proto Filter"
field accepts a comma-separated list of protocol names (e.g. "Ford V2,VAG").
When set, the receiver ignores all decoded signals that are not in the list,
reducing RAM usage and increasing the chance of capturing the target protocol.
Leave empty to disable (default behavior, all protocols accepted).
Setting is persisted in last_subghz.settings under the ProtocolFilter key.
### Changed
- Protocol Filter: replaced free-text input with a dedicated protocol list
scene (Proto Filter in Receiver Config). All registered protocols are shown
as toggleable items (--- / ONLY). Selecting one or more protocols restricts
the receiver to only show those; leaving all as --- disables the filter.
The active count is shown inline in Receiver Config ("N set" or "All").
Filter is persisted across sessions and cleared by Reset to default.
+2
View File
@@ -36,6 +36,8 @@ This project may incorporate, adapt, or build upon **other open-source projects*
| Keeloq Key Manager | Mod Hopping Config |
| ![PSA Decrypt](.arf_pictures/psa_decrypt_builtin.png) | ![Counter BruteForce](.arf_pictures/counter_bruteforce.png) |
| PSA XTEA Decrypt | Counter BruteForce |
| ![Custom Emulation Settings](.arf_pictures/custom_emulation_settings.png) | ![Custom Emulation Scene](.arf_pictures/custom_emulation_scene.png) |
| Custom Emulation Settings | Custom Emulation Scene |
---
@@ -64,6 +64,10 @@ typedef enum {
SubGhzCustomEventViewFreqAnalOkLong,
SubGhzCustomEventByteInputDone,
SubGhzCustomEventCarEmulateTransmit,
SubGhzCustomEventCarEmulateStop,
SubGhzCustomEventCarEmulateExit,
} SubGhzCustomEvent;
typedef enum {
@@ -94,6 +94,7 @@ typedef enum {
SubGhzViewIdReadRAW,
SubGhzViewIdPsaDecrypt,
SubGhzViewIdKeeloqDecrypt,
SubGhzViewIdCarEmulate,
} SubGhzViewId;
@@ -0,0 +1,499 @@
/**
* Scene: CarEmulate
* Custom automotive-key emulation GUI ported from ProtoPirate.
* Activated when SubGhzLastSettings::custom_car_emulate == true and the
* user presses "Emulate" on a saved dynamic protocol.
*
* Flow:
* SavedMenu → Emulate → (custom_car_emulate?) CarEmulate : Transmitter
*/
#include "../subghz_i.h"
#include "../views/subghz_car_emulate.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>
#define TAG "SubGhzSceneCarEmulate"
#define MIN_TX_TICKS 66U /* ~666 ms at 100 ms tick */
/* ── Per-session state (heap, freed on exit) ─────────────────────────────── */
typedef struct {
/* Signal metadata read from fff_data */
char protocol_name[48];
uint32_t serial;
uint8_t original_button;
uint32_t original_counter;
uint32_t current_counter;
uint32_t freq;
char preset_short[12]; /* "AM650", "FM476", … */
/* 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 */
uint8_t pending_button;
} CarEmulateState;
static CarEmulateState* s_state = NULL;
/* ═══════════════════════════════════════════════════════════════════════════
* Button mapping (protocol-name → InputKey → button byte)
* Ported verbatim from protopirate_scene_emulate.c
* ═════════════════════════════════════════════════════════════════════════*/
//static uint8_t car_emulate_map_button(
// const char* protocol,
// InputKey key,
// uint8_t original) {
/* Land Rover V0 */
// if(strstr(protocol, "Land Rover")) {
// switch(key) {
// case InputKeyUp: return 0x02; /* Lock */
// case InputKeyOk: return 0x04; /* Unlock */
// default: return original;
// }
// }
/* Mazda */
// if(strstr(protocol, "Mazda")) {
// switch(key) {
// case InputKeyUp: return 0x01;
// case InputKeyOk: return 0x02;
// case InputKeyDown: return 0x04;
// case InputKeyRight: return 0x08;
// default: return original;
// }
// }
/* PSA */
// if(strstr(protocol, "PSA")) {
// switch(key) {
// case InputKeyUp: return 0x1;
// case InputKeyOk: return 0x2;
// case InputKeyDown: return 0x4;
// case InputKeyLeft: return 0x8;
// default: return original;
// }
// }
/* VAG */
// if(strstr(protocol, "VAG")) {
// if(original == 0x10 || original == 0x20 || original == 0x40) {
// switch(key) {
// case InputKeyUp: return 0x20;
// case InputKeyOk: return 0x10;
// case InputKeyDown: return 0x40;
// default: return original;
// }
// }
// switch(key) {
// case InputKeyUp: return 0x2;
// case InputKeyOk: return 0x1;
// case InputKeyDown: return 0x4;
// case InputKeyLeft: return 0x8;
// case InputKeyRight: return 0x3;
// default: return original;
// }
// }
/* Honda Static */
// if(strstr(protocol, "Honda Static")) {
// switch(key) {
// case InputKeyUp: return 0x1;
// case InputKeyOk: return 0x2;
// case InputKeyDown: return 0x4;
// case InputKeyRight: return 0x5;
// case InputKeyLeft: return 0x8;
// default: return original;
// }
// }
/* Ford */
// if(strstr(protocol, "Ford")) {
// switch(key) {
// case InputKeyLeft: return 0x1;
// case InputKeyUp: return 0x2;
// case InputKeyOk: return 0x4;
// case InputKeyDown: return 0x8;
// case InputKeyRight: return 0x10;
// default: return original;
// }
// }
/* Chrysler */
// if(strstr(protocol, "Chrysler")) {
// switch(key) {
// case InputKeyUp: return 0x1;
// case InputKeyOk: return 0x2;
// default: return original;
// }
// }
/* Subaru */
// if(strstr(protocol, "Subaru")) {
// switch(key) {
// case InputKeyUp: return 0x1;
// case InputKeyOk: return 0x2;
// case InputKeyDown: return 0x3;
// case InputKeyLeft: return 0x4;
// case InputKeyRight: return 0x8;
// default: return original;
// }
// }
/* Fiat V1 */
// if(strstr(protocol, "Fiat V1")) {
// switch(key) {
// case InputKeyUp: return 0x8;
// case InputKeyOk: return 0x0;
// case InputKeyDown: return 0xD;
// default: return original;
// }
// }
/* Generic KeeLoq / KIA etc. simple 4-button layout */
// if(strstr(protocol, "Kia") || strstr(protocol, "KIA") ||
// strstr(protocol, "KeeLoq") || strstr(protocol, "Keeloq")) {
// switch(key) {
// case InputKeyUp: return 0x1;
// case InputKeyOk: return 0x2;
// case InputKeyDown: return 0x3;
// case InputKeyLeft: return 0x4;
// case InputKeyRight: return 0x8;
// default: return original;
// }
// }
// return original;
//}
/* ═══════════════════════════════════════════════════════════════════════════
* TX helpers
* ═════════════════════════════════════════════════════════════════════════*/
/**
* Read frequency and short preset name from fff_data.
* Falls back to 433.92 MHz / "AM650" on failure.
*/
static void car_emulate_read_freq_preset(SubGhz* subghz, CarEmulateState* st) {
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
st->freq = 433920000UL;
strncpy(st->preset_short, "AM650", sizeof(st->preset_short) - 1);
if(!fff) return;
uint32_t freq = 0;
flipper_format_rewind(fff);
if(flipper_format_read_uint32(fff, "Frequency", &freq, 1) && freq > 0) {
st->freq = freq;
}
FuriString* preset_str = furi_string_alloc();
flipper_format_rewind(fff);
if(flipper_format_read_string(fff, "Preset", preset_str)) {
/* Convert long FuriHal name → short token used by the setting */
const char* raw = furi_string_get_cstr(preset_str);
const char* short_name = "AM650";
if(strstr(raw, "Ook270")) short_name = "AM270";
else if(strstr(raw, "Ook650")) short_name = "AM650";
else if(strstr(raw, "238")) short_name = "FM238";
else if(strstr(raw, "12K")) short_name = "FM12K";
else if(strstr(raw, "476")) short_name = "FM476";
else if(strstr(raw, "Custom")) short_name = "CUST";
strncpy(st->preset_short, short_name, sizeof(st->preset_short) - 1);
}
furi_string_free(preset_str);
}
/** Update Btn and Cnt fields in fff_data so the transmitter re-serialises them. */
static void car_emulate_apply_button(SubGhz* subghz, InputKey key) {
UNUSED(subghz);
uint8_t custom_btn_id;
switch(key) {
case InputKeyUp: custom_btn_id = SUBGHZ_CUSTOM_BTN_UP; break;
case InputKeyDown: custom_btn_id = SUBGHZ_CUSTOM_BTN_DOWN; break;
case InputKeyLeft: custom_btn_id = SUBGHZ_CUSTOM_BTN_LEFT; break;
case InputKeyRight: custom_btn_id = SUBGHZ_CUSTOM_BTN_RIGHT; break;
case InputKeyOk:
default: custom_btn_id = SUBGHZ_CUSTOM_BTN_OK; break;
}
subghz_custom_btn_set(custom_btn_id);
}
/** Update Cnt in fff_data (Btn is handled by the protocol via custom_btn). */
static void car_emulate_update_fff(SubGhz* subghz, uint32_t counter) {
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
if(!fff) return;
flipper_format_rewind(fff);
flipper_format_insert_or_update_uint32(fff, "Cnt", &counter, 1);
}
/** Apply tx_power to the current preset and start a single transmission burst. */
static bool car_emulate_start_tx(SubGhz* subghz, uint8_t custom_btn_id) {
SubGhzRadioPreset preset = subghz_txrx_get_preset(subghz->txrx);
if(preset.data && preset.data_size > 0 && subghz->tx_power > 0) {
subghz_txrx_set_tx_power(preset.data, preset.data_size, subghz->tx_power);
FURI_LOG_I(TAG, "TX power index applied: %u", subghz->tx_power);
}
subghz_custom_btn_set(custom_btn_id);
bool ok = subghz_tx_start(subghz, subghz_txrx_get_fff_data(subghz->txrx));
if(ok) {
subghz->state_notifications = SubGhzNotificationStateTx;
notification_message(subghz->notifications, &sequence_blink_magenta_10);
FURI_LOG_I(TAG, "TX started");
} else {
FURI_LOG_E(TAG, "subghz_tx_start failed");
}
return ok;
}
/** Stop an active transmission. */
static void car_emulate_stop_tx(SubGhz* subghz) {
subghz_txrx_stop(subghz->txrx);
subghz->state_notifications = SubGhzNotificationStateIDLE;
notification_message(subghz->notifications, &sequence_blink_stop);
FURI_LOG_I(TAG, "TX stopped");
}
/* ═══════════════════════════════════════════════════════════════════════════
* View callback (fired from the View's input handler)
* ═════════════════════════════════════════════════════════════════════════*/
static void subghz_scene_car_emulate_view_callback(uint32_t event, void* context) {
SubGhz* subghz = context;
view_dispatcher_send_custom_event(subghz->view_dispatcher, event);
}
/* ═══════════════════════════════════════════════════════════════════════════
* Helpers to keep the view in sync
* ═════════════════════════════════════════════════════════════════════════*/
static void car_emulate_refresh_view(SubGhz* subghz) {
furi_assert(s_state);
subghz_car_emulate_view_set_data(
subghz->car_emulate_view,
s_state->protocol_name,
s_state->serial,
s_state->current_counter,
s_state->original_counter,
s_state->freq,
s_state->preset_short,
s_state->is_transmitting);
}
/* ═══════════════════════════════════════════════════════════════════════════
* Scene on_enter
* ═════════════════════════════════════════════════════════════════════════*/
void subghz_scene_car_emulate_on_enter(void* context) {
SubGhz* subghz = context;
furi_assert(subghz);
/* Allocate per-session state */
s_state = malloc(sizeof(CarEmulateState));
furi_check(s_state);
memset(s_state, 0, sizeof(CarEmulateState));
/* ── Read metadata from the loaded fff_data ── */
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
if(fff) {
FuriString* tmp = furi_string_alloc();
flipper_format_rewind(fff);
if(flipper_format_read_string(fff, "Protocol", tmp)) {
strncpy(
s_state->protocol_name,
furi_string_get_cstr(tmp),
sizeof(s_state->protocol_name) - 1);
}
flipper_format_rewind(fff);
flipper_format_read_uint32(fff, "Serial", &s_state->serial, 1);
flipper_format_rewind(fff);
uint32_t btn_tmp = 0;
if(flipper_format_read_uint32(fff, "Btn", &btn_tmp, 1)) {
s_state->original_button = (uint8_t)btn_tmp;
}
flipper_format_rewind(fff);
flipper_format_read_uint32(fff, "Cnt", &s_state->original_counter, 1);
s_state->current_counter = s_state->original_counter;
furi_string_free(tmp);
}
/* ── Initialize the custom_btn system ──────────────────────────────────
* Reset first so any leftover state from a previous session is cleared.
* Then deserialize the decoder once: this causes the protocol's own
* deserialize() to call subghz_custom_btn_set_original() and
* subghz_custom_btn_set_max(), which is exactly what the standard
* Transmitter scene does via subghz_scene_transmitter_update_data_show().
* After this call:
* - 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_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);
/* 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(
subghz->car_emulate_view,
"UNLOCK", /* OK */
"LOCK", /* Up */
"TRUNK", /* Down */
"PANIC", /* Left */
"START" /* Right */
);
car_emulate_read_freq_preset(subghz, s_state);
/* ── Configure the view ── */
subghz_car_emulate_view_set_callback(
subghz->car_emulate_view, subghz_scene_car_emulate_view_callback, subghz);
car_emulate_refresh_view(subghz);
subghz->state_notifications = SubGhzNotificationStateIDLE;
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdCarEmulate);
}
/* ═══════════════════════════════════════════════════════════════════════════
* Scene on_event
* ═════════════════════════════════════════════════════════════════════════*/
bool subghz_scene_car_emulate_on_event(void* context, SceneManagerEvent event) {
SubGhz* subghz = context;
furi_assert(s_state);
bool consumed = false;
if(event.type == SceneManagerEventTypeCustom) {
/* ── Transmit ── */
if((event.event & 0xFFFFU) == SubGhzCustomEventCarEmulateTransmit) {
InputKey key = (InputKey)((event.event >> 16) & 0xFFU);
/* Stop any ongoing TX first */
if(subghz->state_notifications == SubGhzNotificationStateTx) {
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);
/* Only update the counter in fff_data; the protocol handles Btn. */
car_emulate_update_fff(subghz, s_state->current_counter);
s_state->is_transmitting = true;
s_state->stop_pending = false;
s_state->tx_start_tick = (uint32_t)furi_get_tick();
uint8_t cur_btn = subghz_custom_btn_get();
if(!car_emulate_start_tx(subghz, cur_btn)) {
s_state->is_transmitting = false;
notification_message(subghz->notifications, &sequence_error);
}
car_emulate_refresh_view(subghz);
consumed = true;
/* ── Stop ── */
} else if(event.event == SubGhzCustomEventCarEmulateStop) {
if(s_state->is_transmitting &&
subghz->state_notifications == SubGhzNotificationStateTx) {
uint32_t elapsed = (uint32_t)furi_get_tick() - s_state->tx_start_tick;
if(elapsed >= MIN_TX_TICKS) {
car_emulate_stop_tx(subghz);
s_state->is_transmitting = false;
s_state->stop_pending = false;
} else {
s_state->stop_pending = true;
}
}
car_emulate_refresh_view(subghz);
consumed = true;
/* ── Exit ── */
} else if(event.event == SubGhzCustomEventCarEmulateExit) {
if(subghz->state_notifications == SubGhzNotificationStateTx) {
car_emulate_stop_tx(subghz);
}
scene_manager_search_and_switch_to_previous_scene(
subghz->scene_manager, SubGhzSceneSavedMenu);
consumed = true;
}
} else if(event.type == SceneManagerEventTypeTick) {
if(s_state->is_transmitting &&
subghz->state_notifications == SubGhzNotificationStateTx) {
/* Check if hardware is done */
if(subghz_devices_is_async_complete_tx(subghz->txrx->radio_device)) {
subghz->state_notifications = SubGhzNotificationStateIDLE;
subghz_txrx_stop(subghz->txrx);
if(s_state->stop_pending) {
s_state->is_transmitting = false;
s_state->stop_pending = false;
notification_message(subghz->notifications, &sequence_blink_stop);
}
} else {
/* Still transmitting blink LED */
notification_message(subghz->notifications, &sequence_blink_magenta_10);
}
/* Enforce MIN_TX_TICKS stop gate */
if(s_state->stop_pending) {
uint32_t elapsed = (uint32_t)furi_get_tick() - s_state->tx_start_tick;
if(elapsed >= MIN_TX_TICKS) {
car_emulate_stop_tx(subghz);
s_state->is_transmitting = false;
s_state->stop_pending = false;
}
}
}
/* Refresh view every tick for animation */
car_emulate_refresh_view(subghz);
consumed = true;
}
return consumed;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Scene on_exit
* ═════════════════════════════════════════════════════════════════════════*/
void subghz_scene_car_emulate_on_exit(void* context) {
SubGhz* subghz = context;
if(subghz->state_notifications == SubGhzNotificationStateTx) {
car_emulate_stop_tx(subghz);
}
subghz->state_notifications = SubGhzNotificationStateIDLE;
notification_message(subghz->notifications, &sequence_blink_stop);
/* Clear view callbacks */
subghz_car_emulate_view_set_callback(subghz->car_emulate_view, NULL, NULL);
/* Free per-session state */
if(s_state) {
free(s_state);
s_state = NULL;
}
}
@@ -0,0 +1,109 @@
/**
* Scene: CarEmulateSettings
* Toggle: Custom Emulate Off / On
* Selector: TX Power (reuses the same table as Radio Settings)
* Both settings are persisted in SubGhzLastSettings.
*/
#include "../subghz_i.h"
#include <lib/toolbox/value_index.h>
#define TAG "SubGhzCarEmulateSettings"
/* ── Toggle ──────────────────────────────────────────────────────────────── */
static const char* const toggle_text[] = {"Off", "On"};
static void subghz_scene_car_emulate_settings_toggle_changed(VariableItem* item) {
SubGhz* subghz = variable_item_get_context(item);
furi_assert(subghz);
uint8_t index = variable_item_get_current_value_index(item);
variable_item_set_current_value_text(item, toggle_text[index]);
subghz->last_settings->custom_car_emulate = (index == 1);
subghz_last_settings_save(subghz->last_settings);
}
/* ── TX Power ────────────────────────────────────────────────────────────── */
/* Must match the table in subghz_scene_radio_settings.c exactly */
#define CE_TX_POWER_COUNT 9
static const char* const ce_tx_power_text[CE_TX_POWER_COUNT] = {
"Preset", /* index 0 → use whatever the preset has baked in */
"10dBm +",
"7dBm",
"5dBm",
"0dBm",
"-10dBm",
"-15dBm",
"-20dBm",
"-30dBm",
};
static void subghz_scene_car_emulate_settings_power_changed(VariableItem* item) {
SubGhz* subghz = variable_item_get_context(item);
furi_assert(subghz);
uint8_t index = variable_item_get_current_value_index(item);
variable_item_set_current_value_text(item, ce_tx_power_text[index]);
/* Mirror the same fields that Radio Settings touches so the value is
* visible everywhere and survives app restart. */
subghz->tx_power = index;
subghz->last_settings->tx_power = index;
subghz_last_settings_save(subghz->last_settings);
/* Patch the live preset buffer immediately so any subsequent TX in this
* session uses the new power without needing a restart. */
SubGhzRadioPreset preset = subghz_txrx_get_preset(subghz->txrx);
if(preset.data && preset.data_size > 0) {
subghz_txrx_set_tx_power(preset.data, preset.data_size, index);
}
}
/* ── Scene callbacks ─────────────────────────────────────────────────────── */
void subghz_scene_car_emulate_settings_on_enter(void* context) {
SubGhz* subghz = context;
furi_assert(subghz);
VariableItemList* list = subghz->variable_item_list;
variable_item_list_reset(list);
/* ── Row 1: Custom Emulate toggle ── */
VariableItem* item = variable_item_list_add(
list,
"Custom Emulate",
2,
subghz_scene_car_emulate_settings_toggle_changed,
subghz);
uint8_t toggle_idx = subghz->last_settings->custom_car_emulate ? 1 : 0;
variable_item_set_current_value_index(item, toggle_idx);
variable_item_set_current_value_text(item, toggle_text[toggle_idx]);
/* ── Row 2: TX Power ── */
item = variable_item_list_add(
list,
"TX Power",
CE_TX_POWER_COUNT,
subghz_scene_car_emulate_settings_power_changed,
subghz);
/* Clamp stored value to valid range in case settings file is corrupt */
uint8_t power_idx = subghz->tx_power;
if(power_idx >= CE_TX_POWER_COUNT) power_idx = 0;
variable_item_set_current_value_index(item, power_idx);
variable_item_set_current_value_text(item, ce_tx_power_text[power_idx]);
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdVariableItemList);
}
bool subghz_scene_car_emulate_settings_on_event(void* context, SceneManagerEvent event) {
UNUSED(context);
UNUSED(event);
return false;
}
void subghz_scene_car_emulate_settings_on_exit(void* context) {
SubGhz* subghz = context;
variable_item_list_reset(subghz->variable_item_list);
}
@@ -34,3 +34,5 @@ ADD_SCENE(subghz, keeloq_decrypt, KeeloqDecrypt)
ADD_SCENE(subghz, keeloq_bf2, KeeloqBf2)
ADD_SCENE(subghz, kl_bf_cleanup, KlBfCleanup)
ADD_SCENE(subghz, counter_bf, CounterBf)
ADD_SCENE(subghz, car_emulate, CarEmulate)
ADD_SCENE(subghz, car_emulate_settings, CarEmulateSettings)
@@ -1,53 +1,135 @@
#include "../subghz_i.h"
#include <lib/subghz/subghz_protocol_registry.h>
void subghz_scene_protocol_list_submenu_callback(void* context, uint32_t index) {
SubGhz* subghz = context;
view_dispatcher_send_custom_event(subghz->view_dispatcher, index);
}
#define TAG "SubGhzSceneProtocolList"
void subghz_scene_protocol_list_on_enter(void* context) {
SubGhz* subghz = context;
/* ── helpers ──────────────────────────────────────────────────────────────── */
submenu_reset(subghz->submenu);
size_t protocol_count = subghz_protocol_registry_count(&subghz_protocol_registry);
char header_str[32];
snprintf(header_str, sizeof(header_str), "Protocols: %zu", protocol_count);
submenu_set_header(subghz->submenu, header_str);
for(size_t i = 0; i < protocol_count; i++) {
const SubGhzProtocol* protocol =
subghz_protocol_registry_get_by_index(&subghz_protocol_registry, i);
if(protocol) {
submenu_add_item(
subghz->submenu,
protocol->name,
i,
subghz_scene_protocol_list_submenu_callback,
subghz);
}
}
submenu_set_selected_item(
subghz->submenu,
scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneProtocolList));
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdMenu);
}
bool subghz_scene_protocol_list_on_event(void* context, SceneManagerEvent event) {
SubGhz* subghz = context;
if(event.type == SceneManagerEventTypeCustom) {
scene_manager_set_scene_state(subghz->scene_manager, SubGhzSceneProtocolList, event.event);
return true;
static bool proto_filter_contains(const char* filter, const char* name) {
const char* p = filter;
while(*p) {
const char* comma = strchr(p, ',');
size_t len = comma ? (size_t)(comma - p) : strlen(p);
if(len == strlen(name) && strncmp(p, name, len) == 0) return true;
if(!comma) break;
p = comma + 1;
}
return false;
}
static void proto_filter_toggle(char* filter, size_t filter_size, const char* name) {
if(proto_filter_contains(filter, name)) {
/* remove it */
char tmp[256] = {0};
const char* p = filter;
bool first = true;
while(*p) {
const char* comma = strchr(p, ',');
size_t len = comma ? (size_t)(comma - p) : strlen(p);
if(!(len == strlen(name) && strncmp(p, name, len) == 0)) {
if(!first) strncat(tmp, ",", sizeof(tmp) - strlen(tmp) - 1);
strncat(tmp, p, len < sizeof(tmp) - strlen(tmp) - 1 ? len : 0);
first = false;
}
if(!comma) break;
p = comma + 1;
}
strncpy(filter, tmp, filter_size - 1);
filter[filter_size - 1] = '\0';
} else {
/* add it */
if(filter[0] != '\0') strncat(filter, ",", filter_size - strlen(filter) - 1);
strncat(filter, name, filter_size - strlen(filter) - 1);
}
}
/* ── callbacks ────────────────────────────────────────────────────────────── */
static void subghz_scene_protocol_list_item_changed(VariableItem* item) {
SubGhz* subghz = variable_item_get_context(item);
uint8_t value_index = variable_item_get_current_value_index(item);
uint32_t proto_idx =
scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneProtocolList);
size_t selected =
variable_item_list_get_selected_item_index(subghz->variable_item_list);
UNUSED(proto_idx);
const SubGhzProtocol* protocol =
subghz_protocol_registry_get_by_index(&subghz_protocol_registry, selected);
if(!protocol) return;
const char* name = protocol->name;
bool currently_in =
proto_filter_contains(subghz->last_settings->protocol_filter, name);
if((value_index == 1) != currently_in) {
proto_filter_toggle(
subghz->last_settings->protocol_filter,
sizeof(subghz->last_settings->protocol_filter),
name);
}
variable_item_set_current_value_text(item, value_index ? "ONLY" : "---");
subghz_last_settings_save(subghz->last_settings);
}
/* ── scene callbacks ──────────────────────────────────────────────────────── */
void subghz_scene_protocol_list_on_enter(void* context) {
SubGhz* subghz = context;
VariableItemList* list = subghz->variable_item_list;
variable_item_list_reset(list);
size_t protocol_count = subghz_protocol_registry_count(&subghz_protocol_registry);
for(size_t i = 0; i < protocol_count; i++) {
const SubGhzProtocol* protocol =
subghz_protocol_registry_get_by_index(&subghz_protocol_registry, i);
if(!protocol) continue;
VariableItem* item = variable_item_list_add(
list,
protocol->name,
2,
subghz_scene_protocol_list_item_changed,
subghz);
bool enabled = proto_filter_contains(
subghz->last_settings->protocol_filter, protocol->name);
variable_item_set_current_value_index(item, enabled ? 1 : 0);
variable_item_set_current_value_text(item, enabled ? "ONLY" : "---");
}
variable_item_list_set_selected_item(
list,
scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneProtocolList));
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdVariableItemList);
}
bool subghz_scene_protocol_list_on_event(void* context, SceneManagerEvent event) {
SubGhz* subghz = context;
bool consumed = false;
if(event.type == SceneManagerEventTypeCustom) {
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneProtocolList, event.event);
consumed = true;
} else if(event.type == SceneManagerEventTypeBack) {
scene_manager_previous_scene(subghz->scene_manager);
consumed = true;
}
return consumed;
}
void subghz_scene_protocol_list_on_exit(void* context) {
SubGhz* subghz = context;
submenu_reset(subghz->submenu);
scene_manager_set_scene_state(
subghz->scene_manager,
SubGhzSceneProtocolList,
variable_item_list_get_selected_item_index(subghz->variable_item_list));
variable_item_list_reset(subghz->variable_item_list);
}
@@ -105,6 +105,29 @@ static void subghz_scene_add_to_history_callback(
SubGhz* subghz = context;
// The check can be moved to /lib/subghz/receiver.c, but may result in false positives
/* Protocol name allowlist filter — if non-empty, drop anything not in the list */
if(subghz->last_settings->protocol_filter[0] != '\0') {
const char* proto_name = decoder_base->protocol->name;
const char* filter = subghz->last_settings->protocol_filter;
bool allowed = false;
/* Walk the comma-separated list */
const char* p = filter;
while(*p) {
const char* comma = strchr(p, ',');
size_t len = comma ? (size_t)(comma - p) : strlen(p);
if(len == strlen(proto_name) && strncmp(p, proto_name, len) == 0) {
allowed = true;
break;
}
if(!comma) break;
p = comma + 1;
}
if(!allowed) {
FURI_LOG_D(TAG, "%s filtered by allowlist", proto_name);
return;
}
}
if((decoder_base->protocol->flag & subghz->ignore_filter) == 0) {
SubGhzHistory* history = subghz->history;
FuriString* item_name = furi_string_alloc();
@@ -17,6 +17,7 @@ enum SubGhzSettingIndex {
SubGhzSettingIndexIgnoreNiceFlorS,
SubGhzSettingIndexDeleteOldSignals,
SubGhzSettingIndexSound,
SubGhzSettingIndexProtoFilter,
SubGhzSettingIndexResetToDefault,
SubGhzSettingIndexLock,
SubGhzSettingIndexRAWThresholdRSSI,
@@ -445,7 +446,9 @@ static void subghz_scene_receiver_config_set_delete_old_signals(VariableItem* it
static void subghz_scene_receiver_config_var_list_enter_callback(void* context, uint32_t index) {
furi_assert(context);
SubGhz* subghz = context;
if(index == SubGhzSettingIndexLock) {
if(index == SubGhzSettingIndexProtoFilter) {
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneProtocolList);
} else if(index == SubGhzSettingIndexLock) {
view_dispatcher_send_custom_event(
subghz->view_dispatcher, SubGhzCustomEventSceneSettingLock);
} else if(index == SubGhzSettingIndexResetToDefault) {
@@ -473,6 +476,7 @@ static void subghz_scene_receiver_config_var_list_enter_callback(void* context,
subghz->last_settings->filter = subghz->filter;
subghz->last_settings->delete_old_signals = false;
subghz->last_settings->tx_power = subghz->tx_power = 0;
subghz->last_settings->protocol_filter[0] = '\0';
subghz_txrx_speaker_set_state(subghz->txrx, speaker_value[default_index]);
subghz_txrx_hopper_set_state(subghz->txrx, hopping_value[default_index]);
@@ -668,6 +672,25 @@ void subghz_scene_receiver_config_on_enter(void* context) {
if(scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneReadRAW) !=
SubGhzCustomEventManagerSet) {
/* Protocol filter */
item = variable_item_list_add(
subghz->variable_item_list,
"Proto Filter",
1,
NULL,
subghz);
if(subghz->last_settings->protocol_filter[0] == '\0') {
variable_item_set_current_value_text(item, "All");
} else {
uint8_t count = 1;
for(const char* p = subghz->last_settings->protocol_filter; *p; p++) {
if(*p == ',') count++;
}
static char filter_count_str[8];
snprintf(filter_count_str, sizeof(filter_count_str), "%u set", count);
variable_item_set_current_value_text(item, filter_count_str);
}
// Reset to default
variable_item_list_add(subghz->variable_item_list, "Reset to default", 1, NULL, NULL);
@@ -6,7 +6,8 @@ enum SubmenuIndex {
SubmenuIndexEdit,
SubmenuIndexDelete,
SubmenuIndexSignalSettings,
SubmenuIndexCounterBf
SubmenuIndexCounterBf, /* <-- comma was missing here */
SubmenuIndexCarEmulateSettings,
};
void subghz_scene_saved_menu_submenu_callback(void* context, uint32_t index) {
@@ -77,6 +78,13 @@ void subghz_scene_saved_menu_on_enter(void* context) {
subghz_scene_saved_menu_submenu_callback,
subghz);
submenu_add_item(
subghz->submenu,
"Custom Emulate Settings",
SubmenuIndexCarEmulateSettings,
subghz_scene_saved_menu_submenu_callback,
subghz);
if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) {
submenu_add_item(
subghz->submenu,
@@ -109,7 +117,22 @@ bool subghz_scene_saved_menu_on_event(void* context, SceneManagerEvent event) {
if(event.event == SubmenuIndexEmulate) {
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexEmulate);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneTransmitter);
bool use_custom = subghz->last_settings->custom_car_emulate;
if(use_custom) {
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
uint32_t cnt_tmp = 0;
flipper_format_rewind(fff);
if(!flipper_format_read_uint32(fff, "Cnt", &cnt_tmp, 1)) {
use_custom = false;
}
}
if(use_custom) {
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneCarEmulate);
} else {
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneTransmitter);
}
return true;
} else if(event.event == SubmenuIndexPsaDecrypt) {
scene_manager_set_scene_state(
@@ -136,6 +159,14 @@ bool subghz_scene_saved_menu_on_event(void* context, SceneManagerEvent event) {
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexCounterBf);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneCounterBf);
return true;
} else if(event.event == SubmenuIndexCarEmulateSettings) {
/* <-- was outside the if block due to misplaced brace, now fixed */
scene_manager_set_scene_state(
subghz->scene_manager,
SubGhzSceneSavedMenu,
SubmenuIndexCarEmulateSettings);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneCarEmulateSettings);
return true;
}
}
return false;
+10
View File
@@ -206,6 +206,12 @@ SubGhz* subghz_alloc(bool alloc_for_tx_only) {
SubGhzViewIdKeeloqDecrypt,
subghz_view_keeloq_decrypt_get_view(subghz->subghz_keeloq_decrypt));
subghz->car_emulate_view = subghz_car_emulate_view_alloc();
view_dispatcher_add_view(
subghz->view_dispatcher,
SubGhzViewIdCarEmulate,
subghz_car_emulate_view_get_view(subghz->car_emulate_view));
//init threshold rssi
subghz->threshold_rssi = subghz_threshold_rssi_alloc();
@@ -321,6 +327,10 @@ void subghz_free(SubGhz* subghz, bool alloc_for_tx_only) {
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdKeeloqDecrypt);
subghz_view_keeloq_decrypt_free(subghz->subghz_keeloq_decrypt);
// Custom car-emulate view
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdCarEmulate);
subghz_car_emulate_view_free(subghz->car_emulate_view);
// Read RAW
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdReadRAW);
subghz_read_raw_free(subghz->subghz_read_raw);
+3
View File
@@ -43,6 +43,8 @@
#include "helpers/subghz_txrx.h"
#include "helpers/subghz_keeloq_keys.h"
#include "views/subghz_car_emulate.h"
#define SUBGHZ_MAX_LEN_NAME 64
#define SUBGHZ_EXT_PRESET_NAME true
#define SUBGHZ_RAW_THRESHOLD_MIN (-90.0f)
@@ -76,6 +78,7 @@ struct SubGhz {
SubGhzReadRAW* subghz_read_raw;
SubGhzViewPsaDecrypt* subghz_psa_decrypt;
SubGhzViewKeeloqDecrypt* subghz_keeloq_decrypt;
SubGhzCarEmulateView* car_emulate_view;
bool raw_send_only;
bool save_datetime_set;
@@ -22,6 +22,8 @@
#define SUBGHZ_LAST_SETTING_FIELD_HOPPING_THRESHOLD "HoppingThreshold"
#define SUBGHZ_LAST_SETTING_FIELD_LED_AND_POWER_AMP "LedAndPowerAmp"
#define SUBGHZ_LAST_SETTING_FIELD_TX_POWER "TXPower"
#define SUBGHZ_LAST_SETTING_FIELD_CUSTOM_CAR_EMULATE "CustomCarEmulate"
#define SUBGHZ_LAST_SETTING_FIELD_PROTOCOL_FILTER "ProtocolFilter"
SubGhzLastSettings* subghz_last_settings_alloc(void) {
SubGhzLastSettings* instance = malloc(sizeof(SubGhzLastSettings));
@@ -50,6 +52,7 @@ void subghz_last_settings_load(SubGhzLastSettings* instance, size_t preset_count
instance->enable_preset_hopping = false;
instance->preset_hopping_threshold = SUBGHZ_LAST_SETTING_DEFAULT_PRESET_HOPPING_THRESHOLD;
instance->leds_and_amp = true;
instance->protocol_filter[0] = '\0';
Storage* storage = furi_record_open(RECORD_STORAGE);
FlipperFormat* fff_data_file = flipper_format_file_alloc(storage);
@@ -163,6 +166,27 @@ void subghz_last_settings_load(SubGhzLastSettings* instance, size_t preset_count
1)) {
flipper_format_rewind(fff_data_file);
}
if(!flipper_format_read_bool(
fff_data_file,
SUBGHZ_LAST_SETTING_FIELD_CUSTOM_CAR_EMULATE,
&instance->custom_car_emulate,
1)) {
instance->custom_car_emulate = false;
flipper_format_rewind(fff_data_file);
}
FuriString* filter_str = furi_string_alloc();
if(flipper_format_read_string(
fff_data_file, SUBGHZ_LAST_SETTING_FIELD_PROTOCOL_FILTER, filter_str)) {
strncpy(
instance->protocol_filter,
furi_string_get_cstr(filter_str),
sizeof(instance->protocol_filter) - 1);
instance->protocol_filter[sizeof(instance->protocol_filter) - 1] = '\0';
} else {
instance->protocol_filter[0] = '\0';
flipper_format_rewind(fff_data_file);
}
furi_string_free(filter_str);
} while(0);
} else {
@@ -281,6 +305,19 @@ bool subghz_last_settings_save(SubGhzLastSettings* instance) {
file, SUBGHZ_LAST_SETTING_FIELD_LED_AND_POWER_AMP, &instance->leds_and_amp, 1)) {
break;
}
if(!flipper_format_write_bool(
file,
SUBGHZ_LAST_SETTING_FIELD_CUSTOM_CAR_EMULATE,
&instance->custom_car_emulate,
1)) {
break;
}
if(!flipper_format_write_string_cstr(
file,
SUBGHZ_LAST_SETTING_FIELD_PROTOCOL_FILTER,
instance->protocol_filter)) {
break;
}
saved = true;
} while(0);
@@ -30,6 +30,8 @@ typedef struct {
float preset_hopping_threshold;
bool leds_and_amp;
uint8_t tx_power;
bool custom_car_emulate;
char protocol_filter[256]; /* comma-separated allowlist, empty = disabled */
} SubGhzLastSettings;
SubGhzLastSettings* subghz_last_settings_alloc(void);
@@ -0,0 +1,264 @@
#include "subghz_car_emulate.h"
#include "../helpers/subghz_custom_event.h"
#include <gui/elements.h>
#include <input/input.h>
#include <furi.h>
#define TAG "SubGhzCarEmulateView"
/* ── Model ──────────────────────────────────────────────────────────────── */
typedef struct {
char protocol_name[32];
uint32_t serial;
uint32_t counter;
uint32_t original_counter;
uint32_t freq;
char preset[12];
bool is_transmitting;
uint8_t anim_frame;
char label_ok[12];
char label_up[12];
char label_down[12];
char label_left[12];
char label_right[12];
} SubGhzCarEmulateViewModel;
/* ── Handle ─────────────────────────────────────────────────────────────── */
struct SubGhzCarEmulateView {
View* view;
SubGhzCarEmulateViewCallback callback;
void* context;
};
/* ── Draw ───────────────────────────────────────────────────────────────── */
static void subghz_car_emulate_view_draw(Canvas* canvas, void* model_ptr) {
SubGhzCarEmulateViewModel* m = model_ptr;
m->anim_frame = (m->anim_frame + 1) % 8;
canvas_clear(canvas);
/* Header bar */
canvas_draw_box(canvas, 0, 0, 128, 11);
canvas_invert_color(canvas);
canvas_set_font(canvas, FontSecondary);
canvas_draw_str_aligned(canvas, 64, 2, AlignCenter, AlignTop, m->protocol_name);
canvas_invert_color(canvas);
/* Info row 1: serial + counter */
canvas_set_font(canvas, FontSecondary);
char buf[32];
if(m->serial <= 0xFFFFFFUL) {
snprintf(buf, sizeof(buf), "SN:%06lX", (unsigned long)(m->serial & 0xFFFFFFUL));
} else {
snprintf(buf, sizeof(buf), "SN:%08lX", (unsigned long)m->serial);
}
canvas_draw_str(canvas, 2, 20, buf);
snprintf(buf, sizeof(buf), "CNT:%04lX", (unsigned long)m->counter);
canvas_draw_str(canvas, 68, 20, buf);
if(m->counter > m->original_counter) {
snprintf(buf, sizeof(buf), "+%ld", (long)(m->counter - m->original_counter));
canvas_draw_str(canvas, 112, 20, buf);
}
/* Info row 2: frequency + preset */
snprintf(
buf,
sizeof(buf),
"F:%lu.%02lu",
(unsigned long)(m->freq / 1000000UL),
(unsigned long)((m->freq % 1000000UL) / 10000UL));
canvas_draw_str(canvas, 2, 30, buf);
canvas_draw_str(canvas, 95, 30, m->preset);
/* ── Button labels ── */
const uint8_t font_h = canvas_current_font_height(canvas);
/* Centre → UNLOCK (OK button) */
{
const char* lbl = m->label_ok;
uint8_t w = (uint8_t)(canvas_string_width(canvas, lbl) + 8U);
canvas_draw_rbox(canvas, 64 - w / 2, 45 - font_h / 2, w, font_h, 3);
canvas_invert_color(canvas);
canvas_draw_str_aligned(canvas, 64, 49, AlignCenter, AlignBottom, lbl);
canvas_invert_color(canvas);
}
/* Up → LOCK */
{
const char* lbl = m->label_up;
uint8_t w = (uint8_t)(canvas_string_width(canvas, lbl) + 8U);
canvas_draw_rbox(canvas, 64 - w / 2, 33 - font_h / 2, w, font_h, 3);
canvas_invert_color(canvas);
canvas_draw_str_aligned(canvas, 64, 37, AlignCenter, AlignBottom, lbl);
canvas_invert_color(canvas);
}
/* Left → PANIC */
{
const char* lbl = m->label_left;
uint8_t w = (uint8_t)(canvas_string_width(canvas, lbl) + 8U);
canvas_draw_rbox(canvas, 0, 46 - font_h / 2, w, font_h, 3);
canvas_invert_color(canvas);
canvas_draw_str_aligned(canvas, w / 2, 50, AlignCenter, AlignBottom, lbl);
canvas_invert_color(canvas);
}
/* Right → generic extra */
{
const char* lbl = m->label_right;
uint8_t w = (uint8_t)(canvas_string_width(canvas, lbl) + 8U);
canvas_draw_rbox(canvas, 127 - w, 46 - font_h / 2, w, font_h, 3);
canvas_invert_color(canvas);
canvas_draw_str_aligned(canvas, 127 - w / 2, 50, AlignCenter, AlignBottom, lbl);
canvas_invert_color(canvas);
}
/* Down → BOOT */
{
const char* lbl = m->label_down;
uint8_t w = (uint8_t)(canvas_string_width(canvas, lbl) + 8U);
canvas_draw_rbox(canvas, 64 - w / 2, 57 - font_h / 2, w, font_h, 3);
canvas_invert_color(canvas);
canvas_draw_str_aligned(canvas, 64, 61, AlignCenter, AlignBottom, lbl);
canvas_invert_color(canvas);
}
/* TX overlay */
if(m->is_transmitting) {
canvas_draw_rbox(canvas, 24, 18, 80, 18, 3);
canvas_invert_color(canvas);
int wave = m->anim_frame % 3;
canvas_draw_str(canvas, 28 + wave * 2, 25, ")))");
canvas_set_font(canvas, FontPrimary);
canvas_draw_str_aligned(canvas, 64, 24, AlignCenter, AlignCenter, "TX");
canvas_invert_color(canvas);
}
}
/* ── Input ──────────────────────────────────────────────────────────────── */
static bool subghz_car_emulate_view_input(InputEvent* event, void* context) {
SubGhzCarEmulateView* instance = context;
furi_assert(instance);
if(event->type == InputTypePress) {
if(event->key == InputKeyBack) {
if(instance->callback) {
instance->callback(SubGhzCustomEventCarEmulateExit, instance->context);
}
return true;
}
/* Any directional / OK key → start TX */
if(instance->callback) {
/* Pack the raw InputKey into the upper bits of the event so the
scene can read which button was pressed.
Lower 16 bits = SubGhzCustomEventCarEmulateTransmit marker,
upper 16 bits = InputKey value. */
uint32_t ev = ((uint32_t)event->key << 16) |
(uint32_t)SubGhzCustomEventCarEmulateTransmit;
instance->callback(ev, instance->context);
}
return true;
} else if(event->type == InputTypeRelease) {
if(event->key != InputKeyBack) {
if(instance->callback) {
instance->callback(SubGhzCustomEventCarEmulateStop, instance->context);
}
return true;
}
}
return false;
}
/* ── Alloc / Free ───────────────────────────────────────────────────────── */
SubGhzCarEmulateView* subghz_car_emulate_view_alloc(void) {
SubGhzCarEmulateView* instance = malloc(sizeof(SubGhzCarEmulateView));
furi_check(instance);
instance->view = view_alloc();
instance->callback = NULL;
instance->context = NULL;
view_set_context(instance->view, instance);
view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(SubGhzCarEmulateViewModel));
view_set_draw_callback(instance->view, subghz_car_emulate_view_draw);
view_set_input_callback(instance->view, subghz_car_emulate_view_input);
return instance;
}
void subghz_car_emulate_view_free(SubGhzCarEmulateView* instance) {
furi_check(instance);
view_free(instance->view);
free(instance);
}
View* subghz_car_emulate_view_get_view(SubGhzCarEmulateView* instance) {
furi_check(instance);
return instance->view;
}
void subghz_car_emulate_view_set_callback(
SubGhzCarEmulateView* instance,
SubGhzCarEmulateViewCallback callback,
void* context) {
furi_check(instance);
instance->callback = callback;
instance->context = context;
}
void subghz_car_emulate_view_set_data(
SubGhzCarEmulateView* instance,
const char* protocol_name,
uint32_t serial,
uint32_t counter,
uint32_t original_counter,
uint32_t freq,
const char* preset,
bool is_transmitting) {
furi_check(instance);
with_view_model(
instance->view,
SubGhzCarEmulateViewModel * m,
{
strncpy(m->protocol_name, protocol_name, sizeof(m->protocol_name) - 1);
m->protocol_name[sizeof(m->protocol_name) - 1] = '\0';
m->serial = serial;
m->counter = counter;
m->original_counter = original_counter;
m->freq = freq;
strncpy(m->preset, preset, sizeof(m->preset) - 1);
m->preset[sizeof(m->preset) - 1] = '\0';
m->is_transmitting = is_transmitting;
},
true);
}
void subghz_car_emulate_view_set_labels(
SubGhzCarEmulateView* instance,
const char* ok,
const char* up,
const char* down,
const char* left,
const char* right) {
furi_check(instance);
with_view_model(
instance->view,
SubGhzCarEmulateViewModel * m,
{
strncpy(m->label_ok, ok ? ok : "", sizeof(m->label_ok) - 1);
strncpy(m->label_up, up ? up : "", sizeof(m->label_up) - 1);
strncpy(m->label_down, down ? down : "", sizeof(m->label_down) - 1);
strncpy(m->label_left, left ? left : "", sizeof(m->label_left) - 1);
strncpy(m->label_right, right ? right : "", sizeof(m->label_right) - 1);
},
true);
}
@@ -0,0 +1,45 @@
#pragma once
#include <gui/view.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct SubGhzCarEmulateView SubGhzCarEmulateView;
typedef void (*SubGhzCarEmulateViewCallback)(uint32_t event, void* context);
SubGhzCarEmulateView* subghz_car_emulate_view_alloc(void);
void subghz_car_emulate_view_free(SubGhzCarEmulateView* instance);
View* subghz_car_emulate_view_get_view(SubGhzCarEmulateView* instance);
void subghz_car_emulate_view_set_callback(
SubGhzCarEmulateView* instance,
SubGhzCarEmulateViewCallback callback,
void* context);
/** Update the fields shown on the view.
* All strings are copied internally so the caller can free them after the call.
*/
void subghz_car_emulate_view_set_labels(
SubGhzCarEmulateView* instance,
const char* ok,
const char* up,
const char* down,
const char* left,
const char* right);
void subghz_car_emulate_view_set_data(
SubGhzCarEmulateView* instance,
const char* protocol_name,
uint32_t serial,
uint32_t counter,
uint32_t original_counter,
uint32_t freq,
const char* preset,
bool is_transmitting);
#ifdef __cplusplus
}
#endif
+56
View File
@@ -16,3 +16,59 @@ void subghz_custom_btn_set_max(uint8_t b);
void subghz_custom_btn_set_prog_mode(ProgMode prog_mode);
ProgMode subghz_custom_btn_get_prog_mode(void);
/**
* Helper macro: declare a static button-map table and the two
* conversion functions that every protocol with custom buttons needs.
*
* Usage in your protocol .c file:
*
* SUBGHZ_CUSTOM_BTN_DEFINE_MAP(my_proto,
* {SUBGHZ_CUSTOM_BTN_OK, 0x01}, // OK → Lock
* {SUBGHZ_CUSTOM_BTN_UP, 0x01}, // Up → Lock
* {SUBGHZ_CUSTOM_BTN_DOWN, 0x02}, // Down → Unlock
* {SUBGHZ_CUSTOM_BTN_LEFT, 0x04}, // Left → Boot
* {SUBGHZ_CUSTOM_BTN_RIGHT, 0x08}, // Right → Panic
* )
*
* This generates:
* static uint8_t my_proto_custom_btn_to_code(uint8_t custom_btn);
* static uint8_t my_proto_code_to_custom_btn(uint8_t code);
* static const uint8_t my_proto_custom_btn_max;
*/
typedef struct {
uint8_t custom_btn_id; /* SUBGHZ_CUSTOM_BTN_OK / UP / DOWN / LEFT / RIGHT */
uint8_t protocol_code; /* the actual byte the protocol puts in the frame */
} SubGhzCustomBtnEntry;
#define SUBGHZ_CUSTOM_BTN_DEFINE_MAP(prefix_, ...) \
static const SubGhzCustomBtnEntry prefix_##_btn_map[] = {__VA_ARGS__}; \
static const uint8_t prefix_##_custom_btn_max = \
(sizeof(prefix_##_btn_map) / sizeof(SubGhzCustomBtnEntry)) - 1U; \
\
static uint8_t prefix_##_custom_btn_to_code(uint8_t custom_btn) { \
for(size_t i = 0; i < sizeof(prefix_##_btn_map) / \
sizeof(SubGhzCustomBtnEntry); i++) { \
if(prefix_##_btn_map[i].custom_btn_id == custom_btn) \
return prefix_##_btn_map[i].protocol_code; \
} \
/* fallback: return whatever OK maps to */ \
return prefix_##_btn_map[0].protocol_code; \
} \
\
static uint8_t prefix_##_code_to_custom_btn(uint8_t code) { \
for(size_t i = 0; i < sizeof(prefix_##_btn_map) / \
sizeof(SubGhzCustomBtnEntry); i++) { \
if(prefix_##_btn_map[i].protocol_code == code) \
return prefix_##_btn_map[i].custom_btn_id; \
} \
return SUBGHZ_CUSTOM_BTN_OK; \
} \
\
static void prefix_##_custom_btn_init(uint8_t current_code) { \
uint8_t original = prefix_##_code_to_custom_btn(current_code); \
if(subghz_custom_btn_get_original() == 0) \
subghz_custom_btn_set_original(original); \
subghz_custom_btn_set_max(prefix_##_custom_btn_max); \
}
+85 -9
View File
@@ -3,6 +3,7 @@
#include <string.h>
#include <lib/toolbox/manchester_decoder.h>
#include <lib/toolbox/manchester_encoder.h>
#include <lib/subghz/blocks/custom_btn_i.h>
#define FORD_V2_TE_SHORT 200U
#define FORD_V2_TE_LONG 400U
@@ -34,6 +35,16 @@
#define FORD_V2_PREAMBLE_COUNT_MAX 0xFFFFU
#define FORD_V2_ENCODER_DEFAULT_REPEAT 10U
SUBGHZ_CUSTOM_BTN_DEFINE_MAP(
ford_v2,
{SUBGHZ_CUSTOM_BTN_OK, 0x11}, /* OK → Unlock */
{SUBGHZ_CUSTOM_BTN_UP, 0x10}, /* Up → Lock */
{SUBGHZ_CUSTOM_BTN_DOWN, 0x13}, /* Down → Trunk */
{SUBGHZ_CUSTOM_BTN_LEFT, 0x14}, /* Left → Panic */
{SUBGHZ_CUSTOM_BTN_RIGHT, 0x15}, /* Right → RemoteStart */
)
static const uint16_t ford_v2_sync_shift16_inv =
(uint16_t)(~(((uint16_t)FORD_V2_SYNC_0 << 8) | (uint16_t)FORD_V2_SYNC_1));
@@ -208,6 +219,10 @@ static bool ford_v2_decoder_commit_frame(SubGhzProtocolDecoderFordV2* instance)
return false;
}
/* Register this protocol's button map with the custom_btn system so the
* standard transmitter view can show UP/DOWN/LEFT/RIGHT cycling. */
ford_v2_custom_btn_init(instance->generic.btn);
if(instance->base.callback) {
instance->base.callback(&instance->base, instance->base.context);
}
@@ -461,26 +476,44 @@ static SubGhzProtocolStatus ford_v2_encoder_deserialize_read_header(
return SubGhzProtocolStatusOk;
}
static SubGhzProtocolStatus ford_v2_encoder_deserialize_validate_and_pack(SubGhzProtocolEncoderFordV2* instance) {
static SubGhzProtocolStatus ford_v2_encoder_deserialize_validate_and_pack(
SubGhzProtocolEncoderFordV2* instance) {
ford_v2_encoder_rebuild_raw_from_payload(instance);
if(!ford_v2_button_is_valid(instance->raw_bytes[6])) {
return SubGhzProtocolStatusErrorParserOthers;
}
uint16_t cnt = (uint16_t)(
(((uint16_t)(instance->raw_bytes[7] & 0x7FU)) << 9) |
(((uint16_t)instance->raw_bytes[8]) << 1) |
((uint16_t)(instance->raw_bytes[9] >> 7)));
cnt = (cnt + 1U) & 0x7FFFU;
// raw_bytes[7] bits [6:0] = cnt[14:8], bit[7] = parity(btn)
instance->raw_bytes[7] = (instance->raw_bytes[7] & 0x80U) |
(uint8_t)((cnt >> 9) & 0x7FU);
instance->raw_bytes[8] = (uint8_t)((cnt >> 1) & 0xFFU);
// raw_bytes[9] bit[7] = cnt[0], bits[6:0] = tail
instance->raw_bytes[9] = (instance->raw_bytes[9] & 0x7FU) |
(uint8_t)((cnt & 1U) << 7);
ford_v2_encoder_refresh_data_from_raw(instance);
instance->generic.btn = instance->raw_bytes[6];
instance->generic.serial = ((uint32_t)instance->raw_bytes[2] << 24) |
((uint32_t)instance->raw_bytes[3] << 16) |
((uint32_t)instance->raw_bytes[4] << 8) |
(uint32_t)instance->raw_bytes[5];
instance->generic.cnt = (uint16_t)((((uint16_t)(instance->raw_bytes[7] & 0x7FU)) << 9) |
(((uint16_t)instance->raw_bytes[8]) << 1) |
((uint16_t)(instance->raw_bytes[9] >> 7)));
instance->generic.serial =
((uint32_t)instance->raw_bytes[2] << 24) |
((uint32_t)instance->raw_bytes[3] << 16) |
((uint32_t)instance->raw_bytes[4] << 8) |
(uint32_t)instance->raw_bytes[5];
instance->generic.cnt = cnt;
return SubGhzProtocolStatusOk;
}
static void ford_v2_encoder_deserialize_apply_repeat(SubGhzProtocolEncoderFordV2* instance, FlipperFormat* flipper_format) {
flipper_format_rewind(flipper_format);
uint32_t repeat = FORD_V2_ENCODER_DEFAULT_REPEAT;
@@ -523,13 +556,36 @@ SubGhzProtocolStatus
FuriString* temp_str = furi_string_alloc();
furi_check(temp_str);
SubGhzProtocolStatus ret = ford_v2_encoder_deserialize_read_header(instance, flipper_format, temp_str);
SubGhzProtocolStatus ret =
ford_v2_encoder_deserialize_read_header(instance, flipper_format, temp_str);
if(ret == SubGhzProtocolStatusOk) {
ret = ford_v2_encoder_deserialize_validate_and_pack(instance);
}
if(ret == SubGhzProtocolStatusOk) {
ford_v2_custom_btn_init(instance->raw_bytes[6]);
uint8_t btn_sel = subghz_custom_btn_get();
if(btn_sel != SUBGHZ_CUSTOM_BTN_OK) {
uint8_t new_code = ford_v2_custom_btn_to_code(btn_sel);
if(ford_v2_button_is_valid(new_code)) {
instance->raw_bytes[6] = new_code;
const uint8_t k7_msb =
(uint8_t)(ford_v2_uint8_parity(new_code) << 7);
instance->raw_bytes[7] =
(instance->raw_bytes[7] & 0x7FU) | k7_msb;
ford_v2_encoder_refresh_data_from_raw(instance);
instance->generic.btn = new_code;
}
}
instance->extra_data = 0;
for(uint8_t i = 0; i < FORD_V2_TAIL_RAW_BYTE_COUNT; i++) {
instance->extra_data =
(instance->extra_data << 8) | (uint64_t)instance->raw_bytes[8U + i];
}
ford_v2_encoder_deserialize_apply_repeat(instance, flipper_format);
ford_v2_encoder_build_upload(instance);
instance->encoder.is_running = true;
@@ -539,6 +595,7 @@ SubGhzProtocolStatus
return ret;
}
void subghz_protocol_encoder_ford_v2_stop(void* context) {
furi_check(context);
SubGhzProtocolEncoderFordV2* instance = context;
@@ -740,6 +797,25 @@ SubGhzProtocolStatus subghz_protocol_decoder_ford_v2_deserialize(
return SubGhzProtocolStatusErrorParserOthers;
}
/* Keep custom_btn in sync when loading from file. */
ford_v2_custom_btn_init(instance->generic.btn);
uint8_t btn_sel = subghz_custom_btn_get();
if(btn_sel != SUBGHZ_CUSTOM_BTN_OK) {
uint8_t new_code = ford_v2_custom_btn_to_code(btn_sel);
if(ford_v2_button_is_valid(new_code)) {
instance->generic.btn = new_code;
instance->raw_bytes[6] = new_code;
instance->raw_bytes[7] = (instance->raw_bytes[7] & 0x7FU) |
(uint8_t)(ford_v2_uint8_parity(new_code) << 7);
instance->generic.data = 0;
for(uint8_t i = 0; i < FORD_V2_KEY_BYTE_COUNT; i++) {
instance->generic.data =
(instance->generic.data << 8) | (uint64_t)instance->raw_bytes[i];
}
}
}
return ret;
}
+984
View File
@@ -0,0 +1,984 @@
#include "land_rover_v0.h"
#include <string.h>
#define TAG "LandRoverV0"
static const SubGhzBlockConst subghz_protocol_land_rover_v0_const = {
.te_short = 250,
.te_long = 500,
.te_delta = 100,
.min_count_bit_for_found = 81,
};
#define LAND_ROVER_V0_PREAMBLE_PAIRS 319U
#define LAND_ROVER_V0_MIN_PREAMBLE_PAIRS 64U
#define LAND_ROVER_V0_SYNC_US 750U
#define LAND_ROVER_V0_SYNC_DELTA_US 120U
#define LAND_ROVER_V0_UPLOAD_CAPACITY 1024U
#define LAND_ROVER_V0_GAP_US 50000U
#define LAND_ROVER_V0_BTN_UNKNOWN 0x00U
#define LAND_ROVER_V0_BTN_LOCK 0x02U
#define LAND_ROVER_V0_BTN_UNLOCK 0x04U
#define LAND_ROVER_V0_SIG_UNLOCK 0xA285E3UL
#define LAND_ROVER_V0_SIG_LOCK 0xC20363UL
/* Extra FlipperFormat field names specific to this protocol */
#define LAND_ROVER_V0_FF_BTNSIG "BtnSig"
#define LAND_ROVER_V0_FF_CHECK "Check"
#define LAND_ROVER_V0_FF_TAIL "Tail"
#define LAND_ROVER_V0_FF_EXTRA_BIT "ExtraBit"
/* FlipperFormat field name aliases (replacing the external pp library's FF_* macros) */
#define LR_FF_KEY "Key"
#define LR_FF_SERIAL "Serial"
#define LR_FF_BTN "Btn"
#define LR_FF_CNT "Cnt"
/* ── Decoder struct ──────────────────────────────────────────────────────── */
typedef struct SubGhzProtocolDecoderLandRoverV0 {
SubGhzProtocolDecoderBase base;
SubGhzBlockDecoder decoder;
SubGhzBlockGeneric generic;
uint16_t preamble_count;
uint8_t raw[10];
uint8_t bit_count;
bool extra_bit;
bool previous_bit;
bool boundary_pad_skipped;
bool pending_short;
uint64_t key;
uint16_t tail;
uint32_t command_signature;
uint32_t serial;
uint32_t count;
uint8_t button;
uint8_t check;
bool check_ok;
bool tail_ok;
} SubGhzProtocolDecoderLandRoverV0;
/* ── Encoder struct ──────────────────────────────────────────────────────── */
typedef struct SubGhzProtocolEncoderLandRoverV0 {
SubGhzProtocolEncoderBase base;
SubGhzProtocolBlockEncoder encoder;
SubGhzBlockGeneric generic;
uint64_t key;
uint16_t tail;
uint32_t command_signature;
uint32_t serial;
uint32_t count;
uint8_t button;
uint8_t check;
} SubGhzProtocolEncoderLandRoverV0;
/* ── Decoder state machine steps ─────────────────────────────────────────── */
typedef enum {
LandRoverV0DecoderStepReset = 0,
LandRoverV0DecoderStepPreambleLow,
LandRoverV0DecoderStepPreambleHigh,
LandRoverV0DecoderStepSyncLow,
LandRoverV0DecoderStepData,
} LandRoverV0DecoderStep;
/* ═══════════════════════════════════════════════════════════════════════════
* Internal helpers replacing pp_* functions from the external app library
* ═════════════════════════════════════════════════════════════════════════*/
/** Write a uint64_t into 8 bytes in big-endian order. */
static inline void lr_u64_to_bytes_be(uint64_t val, uint8_t out[8]) {
for(int i = 7; i >= 0; i--) {
out[i] = (uint8_t)(val & 0xFFU);
val >>= 8;
}
}
/** Read 8 big-endian bytes and return a uint64_t. */
static inline uint64_t lr_bytes_to_u64_be(const uint8_t in[8]) {
uint64_t val = 0;
for(int i = 0; i < 8; i++) {
val = (val << 8) | in[i];
}
return val;
}
/** Returns true when duration matches te_short within te_delta. */
static inline bool lr_is_short(uint32_t duration) {
return DURATION_DIFF(duration, subghz_protocol_land_rover_v0_const.te_short) <
subghz_protocol_land_rover_v0_const.te_delta;
}
/** Returns true when duration matches te_long within te_delta. */
static inline bool lr_is_long(uint32_t duration) {
return DURATION_DIFF(duration, subghz_protocol_land_rover_v0_const.te_long) <
subghz_protocol_land_rover_v0_const.te_delta;
}
/** Insert-or-update a single uint32 field in a FlipperFormat file. */
static void lr_ff_write_u32(FlipperFormat* ff, const char* key, uint32_t val) {
flipper_format_insert_or_update_uint32(ff, key, &val, 1);
}
/** Read a single uint32 field from a FlipperFormat file. */
static bool lr_ff_read_u32(FlipperFormat* ff, const char* key, uint32_t* out) {
return flipper_format_read_uint32(ff, key, out, 1);
}
/**
* Verify that the "Protocol" field in the FlipperFormat file matches the
* expected protocol name. Returns SubGhzProtocolStatusOk on match.
*/
static SubGhzProtocolStatus lr_verify_protocol_name(
FlipperFormat* ff,
const char* expected_name) {
FuriString* name = furi_string_alloc();
bool ok = false;
if(flipper_format_read_string(ff, "Protocol", name)) {
ok = furi_string_equal_str(name, expected_name);
}
furi_string_free(name);
return ok ? SubGhzProtocolStatusOk : SubGhzProtocolStatusErrorProtocolNotFound;
}
/**
* Read the "Repeat" field from a FlipperFormat file.
* Falls back to default_val when the field is absent.
*/
static uint16_t lr_encoder_read_repeat(FlipperFormat* ff, uint16_t default_val) {
uint32_t repeat = default_val;
flipper_format_rewind(ff);
if(!flipper_format_read_uint32(ff, "Repeat", &repeat, 1)) {
repeat = default_val;
}
return (uint16_t)repeat;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Forward declarations for internal (static) helpers
* ═════════════════════════════════════════════════════════════════════════*/
static uint8_t land_rover_v0_button_from_signature(uint32_t signature);
static const char* land_rover_v0_button_name(uint8_t button);
static uint8_t land_rover_v0_calculate_check(uint32_t count);
static bool land_rover_v0_calculate_tail_msb(uint32_t count);
static uint16_t land_rover_v0_calculate_tail(uint32_t count);
static void land_rover_v0_parse_key_fields(
uint64_t key,
uint32_t* signature,
uint32_t* serial,
uint32_t* count,
uint8_t* button,
uint8_t* check);
static bool land_rover_v0_validate_frame(
uint64_t key,
uint16_t tail,
bool extra_bit,
bool* check_ok,
bool* tail_ok);
static bool land_rover_v0_add_decoded_bit(
SubGhzProtocolDecoderLandRoverV0* instance,
bool bit);
static bool land_rover_v0_process_transition(
SubGhzProtocolDecoderLandRoverV0* instance,
bool level,
uint32_t duration);
static bool land_rover_v0_finish_frame(SubGhzProtocolDecoderLandRoverV0* instance);
static bool land_rover_v0_encoder_add_level(
SubGhzProtocolEncoderLandRoverV0* instance,
size_t* index,
bool level,
uint32_t duration);
static bool land_rover_v0_encoder_add_bit(
SubGhzProtocolEncoderLandRoverV0* instance,
size_t* index,
bool* previous_bit,
bool bit);
static bool land_rover_v0_build_upload(SubGhzProtocolEncoderLandRoverV0* instance);
/* ═══════════════════════════════════════════════════════════════════════════
* Protocol descriptor tables
* ═════════════════════════════════════════════════════════════════════════*/
const SubGhzProtocolDecoder subghz_protocol_land_rover_v0_decoder = {
.alloc = subghz_protocol_decoder_land_rover_v0_alloc,
.free = subghz_protocol_decoder_land_rover_v0_free,
.feed = subghz_protocol_decoder_land_rover_v0_feed,
.reset = subghz_protocol_decoder_land_rover_v0_reset,
.get_hash_data = subghz_protocol_decoder_land_rover_v0_get_hash_data,
.serialize = subghz_protocol_decoder_land_rover_v0_serialize,
.deserialize = subghz_protocol_decoder_land_rover_v0_deserialize,
.get_string = subghz_protocol_decoder_land_rover_v0_get_string,
};
const SubGhzProtocolEncoder subghz_protocol_land_rover_v0_encoder = {
.alloc = subghz_protocol_encoder_land_rover_v0_alloc,
.free = subghz_protocol_encoder_land_rover_v0_free,
.deserialize = subghz_protocol_encoder_land_rover_v0_deserialize,
.stop = subghz_protocol_encoder_land_rover_v0_stop,
.yield = subghz_protocol_encoder_land_rover_v0_yield,
};
const SubGhzProtocol subghz_protocol_land_rover_v0 = {
.name = LAND_ROVER_PROTOCOL_V0_NAME,
.type = SubGhzProtocolTypeDynamic,
.flag = SubGhzProtocolFlag_433 | SubGhzProtocolFlag_FM |
SubGhzProtocolFlag_Decodable | SubGhzProtocolFlag_Load |
SubGhzProtocolFlag_Save | SubGhzProtocolFlag_Send,
.decoder = &subghz_protocol_land_rover_v0_decoder,
.encoder = &subghz_protocol_land_rover_v0_encoder,
};
/* ═══════════════════════════════════════════════════════════════════════════
* Protocol logic helpers
* ═════════════════════════════════════════════════════════════════════════*/
static bool land_rover_v0_is_sync(uint32_t duration) {
return DURATION_DIFF(duration, LAND_ROVER_V0_SYNC_US) < LAND_ROVER_V0_SYNC_DELTA_US;
}
static uint8_t land_rover_v0_button_from_signature(uint32_t signature) {
if(signature == LAND_ROVER_V0_SIG_UNLOCK) return LAND_ROVER_V0_BTN_UNLOCK;
if(signature == LAND_ROVER_V0_SIG_LOCK) return LAND_ROVER_V0_BTN_LOCK;
return LAND_ROVER_V0_BTN_UNKNOWN;
}
static const char* land_rover_v0_button_name(uint8_t button) {
switch(button) {
case LAND_ROVER_V0_BTN_LOCK: return "Lock";
case LAND_ROVER_V0_BTN_UNLOCK: return "Unlock";
default: return "Unknown";
}
}
static uint8_t land_rover_v0_calculate_check(uint32_t count) {
const uint8_t c0 =
((count >> 1) ^ (count >> 2) ^ (count >> 3) ^ (count >> 4) ^ (count >> 6)) & 1U;
const uint8_t c1 =
((count >> 0) ^ (count >> 2) ^ (count >> 3) ^ (count >> 4) ^ (count >> 5) ^
(count >> 6) ^ 1U) & 1U;
const uint8_t c2 =
((count >> 1) ^ (count >> 3) ^ (count >> 4) ^ (count >> 5) ^ (count >> 6)) & 1U;
return (uint8_t)(c0 | (c1 << 1) | (c2 << 2));
}
static bool land_rover_v0_calculate_tail_msb(uint32_t count) {
const uint8_t tail =
((count >> 0) ^ (count >> 2) ^ (count >> 4) ^ (count >> 5)) & 1U;
return tail != 0U;
}
static uint16_t land_rover_v0_calculate_tail(uint32_t count) {
return land_rover_v0_calculate_tail_msb(count) ? 0xFFFFU : 0x7FFFU;
}
static void land_rover_v0_parse_key_fields(
uint64_t key,
uint32_t* signature,
uint32_t* serial,
uint32_t* count,
uint8_t* button,
uint8_t* check) {
uint8_t key_bytes[8];
lr_u64_to_bytes_be(key, key_bytes);
const uint32_t sig =
((uint32_t)key_bytes[0] << 16) | ((uint32_t)key_bytes[1] << 8) | key_bytes[2];
const uint32_t sn =
((uint32_t)key_bytes[3] << 16) | ((uint32_t)key_bytes[4] << 8) | key_bytes[5];
const uint32_t cnt =
((uint32_t)key_bytes[6] << 1) | ((key_bytes[7] >> 7) & 1U);
if(signature) *signature = sig;
if(serial) *serial = sn;
if(count) *count = cnt;
if(button) *button = land_rover_v0_button_from_signature(sig);
if(check) *check = key_bytes[7] & 0x07U;
}
static bool land_rover_v0_validate_frame(
uint64_t key,
uint16_t tail,
bool extra_bit,
bool* check_ok,
bool* tail_ok) {
uint8_t key_bytes[8];
lr_u64_to_bytes_be(key, key_bytes);
const uint32_t count = ((uint32_t)key_bytes[6] << 1) | ((key_bytes[7] >> 7) & 1U);
const uint8_t expected_check = land_rover_v0_calculate_check(count);
const uint16_t expected_tail = land_rover_v0_calculate_tail(count);
const bool local_check_ok =
((key_bytes[7] & 0x78U) == 0U) && ((key_bytes[7] & 0x07U) == expected_check);
const bool local_tail_ok = (tail == expected_tail) && extra_bit;
if(check_ok) *check_ok = local_check_ok;
if(tail_ok) *tail_ok = local_tail_ok;
return local_check_ok && local_tail_ok;
}
static bool land_rover_v0_add_decoded_bit(
SubGhzProtocolDecoderLandRoverV0* instance,
bool bit) {
if(instance->bit_count < 80U) {
const uint8_t byte_index = instance->bit_count / 8U;
const uint8_t bit_index = 7U - (instance->bit_count % 8U);
if(bit) instance->raw[byte_index] |= (uint8_t)(1U << bit_index);
} else if(instance->bit_count == 80U) {
instance->extra_bit = bit;
} else {
return false;
}
instance->bit_count++;
return true;
}
static bool land_rover_v0_finish_frame(SubGhzProtocolDecoderLandRoverV0* instance) {
const uint64_t key = lr_bytes_to_u64_be(instance->raw);
const uint16_t tail =
((uint16_t)instance->raw[8] << 8) | instance->raw[9];
if(!land_rover_v0_validate_frame(
key, tail, instance->extra_bit,
&instance->check_ok, &instance->tail_ok)) {
return false;
}
instance->key = key;
instance->tail = tail;
land_rover_v0_parse_key_fields(
key,
&instance->command_signature,
&instance->serial,
&instance->count,
&instance->button,
&instance->check);
instance->generic.data = instance->key;
instance->generic.data_count_bit =
subghz_protocol_land_rover_v0_const.min_count_bit_for_found;
instance->generic.serial = instance->serial;
instance->generic.btn = instance->button;
instance->generic.cnt = instance->count;
return true;
}
static bool land_rover_v0_process_transition(
SubGhzProtocolDecoderLandRoverV0* instance,
bool level,
uint32_t duration) {
if(!instance->boundary_pad_skipped) {
if(level && lr_is_short(duration)) {
instance->boundary_pad_skipped = true;
return true;
}
instance->boundary_pad_skipped = true;
}
if(instance->pending_short) {
if(!instance->previous_bit && !level && lr_is_short(duration)) {
instance->pending_short = false;
return land_rover_v0_add_decoded_bit(instance, false);
} else if(instance->previous_bit && level && lr_is_short(duration)) {
instance->pending_short = false;
return land_rover_v0_add_decoded_bit(instance, true);
}
return false;
}
if(!instance->previous_bit) {
if(level && lr_is_long(duration)) {
instance->previous_bit = true;
return land_rover_v0_add_decoded_bit(instance, true);
} else if(level && lr_is_short(duration)) {
instance->pending_short = true;
return true;
}
return false;
}
if(!level && lr_is_long(duration)) {
instance->previous_bit = false;
return land_rover_v0_add_decoded_bit(instance, false);
} else if(!level && lr_is_short(duration)) {
instance->pending_short = true;
return true;
}
return false;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Encoder waveform helpers
* ═════════════════════════════════════════════════════════════════════════*/
static bool land_rover_v0_encoder_add_level(
SubGhzProtocolEncoderLandRoverV0* instance,
size_t* index,
bool level,
uint32_t duration) {
if(*index >= LAND_ROVER_V0_UPLOAD_CAPACITY) return false;
instance->encoder.upload[(*index)++] = level_duration_make(level, duration);
return true;
}
static bool land_rover_v0_encoder_add_bit(
SubGhzProtocolEncoderLandRoverV0* instance,
size_t* index,
bool* previous_bit,
bool bit) {
const uint32_t te_short = subghz_protocol_land_rover_v0_const.te_short;
const uint32_t te_long = subghz_protocol_land_rover_v0_const.te_long;
if(!*previous_bit && !bit) {
/* 0→0: two short pulses low-high */
if(!land_rover_v0_encoder_add_level(instance, index, true, te_short) ||
!land_rover_v0_encoder_add_level(instance, index, false, te_short))
return false;
} else if(!*previous_bit && bit) {
/* 0→1: one long high */
if(!land_rover_v0_encoder_add_level(instance, index, true, te_long))
return false;
} else if(*previous_bit && !bit) {
/* 1→0: one long low */
if(!land_rover_v0_encoder_add_level(instance, index, false, te_long))
return false;
} else {
/* 1→1: two short pulses high-low */
if(!land_rover_v0_encoder_add_level(instance, index, false, te_short) ||
!land_rover_v0_encoder_add_level(instance, index, true, te_short))
return false;
}
*previous_bit = bit;
return true;
}
static bool land_rover_v0_build_upload(SubGhzProtocolEncoderLandRoverV0* instance) {
furi_check(instance);
size_t index = 0;
const uint32_t te_short = subghz_protocol_land_rover_v0_const.te_short;
uint8_t key_bytes[8];
lr_u64_to_bytes_be(instance->key, key_bytes);
/* Preamble: alternating short high/low pairs */
for(uint16_t i = 0; i < LAND_ROVER_V0_PREAMBLE_PAIRS; i++) {
if(!land_rover_v0_encoder_add_level(instance, &index, true, te_short) ||
!land_rover_v0_encoder_add_level(instance, &index, false, te_short))
return false;
}
/* Sync: long high, long low, short high (boundary pulse) */
if(!land_rover_v0_encoder_add_level(instance, &index, true, LAND_ROVER_V0_SYNC_US) ||
!land_rover_v0_encoder_add_level(instance, &index, false, LAND_ROVER_V0_SYNC_US) ||
!land_rover_v0_encoder_add_level(instance, &index, true, te_short))
return false;
/* First encoded bit: always 0, previous state is 1 (the boundary pulse) */
bool previous_bit = true;
if(!land_rover_v0_encoder_add_bit(instance, &index, &previous_bit, false))
return false;
/* Data bits 2..63 from the 64-bit key */
for(uint8_t bit_index = 2; bit_index < 64; bit_index++) {
const uint8_t byte_index = bit_index / 8U;
const uint8_t bit_in_byte = 7U - (bit_index % 8U);
const bool bit = (key_bytes[byte_index] >> bit_in_byte) & 1U;
if(!land_rover_v0_encoder_add_bit(instance, &index, &previous_bit, bit))
return false;
}
/* 16-bit tail */
instance->tail = land_rover_v0_calculate_tail(instance->count);
for(uint8_t bit_index = 0; bit_index < 16; bit_index++) {
const bool bit = (instance->tail >> (15U - bit_index)) & 1U;
if(!land_rover_v0_encoder_add_bit(instance, &index, &previous_bit, bit))
return false;
}
/* Extra bit (always 1) */
if(!land_rover_v0_encoder_add_bit(instance, &index, &previous_bit, true))
return false;
/* Inter-frame gap */
if(!land_rover_v0_encoder_add_level(instance, &index, false, LAND_ROVER_V0_GAP_US))
return false;
instance->encoder.front = 0;
instance->encoder.size_upload = index;
return true;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Decoder public API
* ═════════════════════════════════════════════════════════════════════════*/
void* subghz_protocol_decoder_land_rover_v0_alloc(SubGhzEnvironment* environment) {
UNUSED(environment);
SubGhzProtocolDecoderLandRoverV0* instance =
calloc(1, sizeof(SubGhzProtocolDecoderLandRoverV0));
furi_check(instance);
instance->base.protocol = &subghz_protocol_land_rover_v0;
instance->generic.protocol_name = instance->base.protocol->name;
return instance;
}
void subghz_protocol_decoder_land_rover_v0_free(void* context) {
furi_check(context);
free(context);
}
void subghz_protocol_decoder_land_rover_v0_reset(void* context) {
furi_check(context);
SubGhzProtocolDecoderLandRoverV0* instance = context;
instance->decoder.parser_step = LandRoverV0DecoderStepReset;
instance->decoder.te_last = 0;
instance->preamble_count = 0;
memset(instance->raw, 0, sizeof(instance->raw));
instance->bit_count = 0;
instance->extra_bit = false;
instance->previous_bit = true;
instance->boundary_pad_skipped = false;
instance->pending_short = false;
}
void subghz_protocol_decoder_land_rover_v0_feed(
void* context,
bool level,
uint32_t duration) {
furi_check(context);
SubGhzProtocolDecoderLandRoverV0* instance = context;
switch(instance->decoder.parser_step) {
case LandRoverV0DecoderStepReset:
if(level && lr_is_short(duration)) {
instance->preamble_count = 0;
instance->decoder.parser_step = LandRoverV0DecoderStepPreambleLow;
}
break;
case LandRoverV0DecoderStepPreambleLow:
if(!level && lr_is_short(duration)) {
instance->preamble_count++;
instance->decoder.parser_step = LandRoverV0DecoderStepPreambleHigh;
} else {
instance->decoder.parser_step = LandRoverV0DecoderStepReset;
}
break;
case LandRoverV0DecoderStepPreambleHigh:
if(level && lr_is_short(duration)) {
instance->decoder.parser_step = LandRoverV0DecoderStepPreambleLow;
} else if(level && land_rover_v0_is_sync(duration) &&
instance->preamble_count >= LAND_ROVER_V0_MIN_PREAMBLE_PAIRS) {
instance->decoder.parser_step = LandRoverV0DecoderStepSyncLow;
} else {
instance->decoder.parser_step = LandRoverV0DecoderStepReset;
}
break;
case LandRoverV0DecoderStepSyncLow:
if(!level && land_rover_v0_is_sync(duration)) {
memset(instance->raw, 0, sizeof(instance->raw));
instance->bit_count = 0;
instance->extra_bit = false;
instance->previous_bit = true;
instance->boundary_pad_skipped = false;
instance->pending_short = false;
land_rover_v0_add_decoded_bit(instance, true);
instance->decoder.parser_step = LandRoverV0DecoderStepData;
} else {
instance->decoder.parser_step = LandRoverV0DecoderStepReset;
}
break;
case LandRoverV0DecoderStepData:
if(!land_rover_v0_process_transition(instance, level, duration)) {
instance->decoder.parser_step = LandRoverV0DecoderStepReset;
break;
}
if(instance->bit_count ==
subghz_protocol_land_rover_v0_const.min_count_bit_for_found) {
if(land_rover_v0_finish_frame(instance) && instance->base.callback) {
instance->base.callback(&instance->base, instance->base.context);
}
instance->decoder.parser_step = LandRoverV0DecoderStepReset;
}
break;
}
instance->decoder.te_last = duration;
}
uint8_t subghz_protocol_decoder_land_rover_v0_get_hash_data(void* context) {
furi_check(context);
SubGhzProtocolDecoderLandRoverV0* instance = context;
SubGhzBlockDecoder decoder = {
.decode_data = instance->key,
.decode_count_bit = 64,
};
uint8_t hash = subghz_protocol_blocks_get_hash_data(&decoder, 9);
hash ^= (uint8_t)(instance->tail >> 8);
hash ^= (uint8_t)instance->tail;
hash ^= instance->extra_bit ? 1U : 0U;
return hash;
}
SubGhzProtocolStatus subghz_protocol_decoder_land_rover_v0_serialize(
void* context,
FlipperFormat* flipper_format,
SubGhzRadioPreset* preset) {
furi_check(context);
SubGhzProtocolDecoderLandRoverV0* instance = context;
SubGhzProtocolStatus ret =
subghz_block_generic_serialize(&instance->generic, flipper_format, preset);
if(ret == SubGhzProtocolStatusOk) {
uint8_t key_bytes[8];
lr_u64_to_bytes_be(instance->key, key_bytes);
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_hex(
flipper_format, LR_FF_KEY, key_bytes, sizeof(key_bytes));
lr_ff_write_u32(flipper_format, LR_FF_SERIAL, instance->serial);
lr_ff_write_u32(flipper_format, LR_FF_BTN, instance->button);
lr_ff_write_u32(flipper_format, LAND_ROVER_V0_FF_BTNSIG, instance->command_signature);
lr_ff_write_u32(flipper_format, LR_FF_CNT, instance->count);
lr_ff_write_u32(flipper_format, LAND_ROVER_V0_FF_CHECK, instance->check);
lr_ff_write_u32(flipper_format, LAND_ROVER_V0_FF_TAIL, instance->tail);
lr_ff_write_u32(flipper_format, LAND_ROVER_V0_FF_EXTRA_BIT,
instance->extra_bit ? 1U : 0U);
}
return ret;
}
SubGhzProtocolStatus subghz_protocol_decoder_land_rover_v0_deserialize(
void* context,
FlipperFormat* flipper_format) {
furi_check(context);
SubGhzProtocolDecoderLandRoverV0* instance = context;
SubGhzProtocolStatus ret = subghz_block_generic_deserialize_check_count_bit(
&instance->generic,
flipper_format,
subghz_protocol_land_rover_v0_const.min_count_bit_for_found);
if(ret == SubGhzProtocolStatusOk) {
uint8_t key_bytes[8] = {0};
bool have_key = false;
flipper_format_rewind(flipper_format);
if(flipper_format_read_hex(
flipper_format, LR_FF_KEY, key_bytes, sizeof(key_bytes))) {
instance->key = lr_bytes_to_u64_be(key_bytes);
have_key = true;
}
if(!have_key) {
instance->key = instance->generic.data;
lr_u64_to_bytes_be(instance->key, key_bytes);
}
uint32_t temp = 0;
flipper_format_rewind(flipper_format);
if(lr_ff_read_u32(flipper_format, LAND_ROVER_V0_FF_TAIL, &temp)) {
instance->tail = (uint16_t)(temp & 0xFFFFU);
} else {
const uint32_t count =
((uint32_t)key_bytes[6] << 1) | ((key_bytes[7] >> 7) & 1U);
instance->tail = land_rover_v0_calculate_tail(count);
}
flipper_format_rewind(flipper_format);
if(lr_ff_read_u32(flipper_format, LAND_ROVER_V0_FF_EXTRA_BIT, &temp)) {
instance->extra_bit = (temp & 1U) != 0;
} else {
instance->extra_bit = true;
}
land_rover_v0_validate_frame(
instance->key, instance->tail, instance->extra_bit,
&instance->check_ok, &instance->tail_ok);
land_rover_v0_parse_key_fields(
instance->key,
&instance->command_signature,
&instance->serial,
&instance->count,
&instance->button,
&instance->check);
instance->generic.data = instance->key;
instance->generic.data_count_bit =
subghz_protocol_land_rover_v0_const.min_count_bit_for_found;
instance->generic.serial = instance->serial;
instance->generic.btn = instance->button;
instance->generic.cnt = instance->count;
}
return ret;
}
void subghz_protocol_decoder_land_rover_v0_get_string(void* context, FuriString* output) {
furi_check(context);
SubGhzProtocolDecoderLandRoverV0* instance = context;
furi_string_cat_printf(
output,
"%s %dbit\r\n"
"Key:%016llX\r\n"
"Sn:%06lX Btn:%02X - %s\r\n"
"BtnSig:%06lX\r\n"
"Cnt:%05lX Chk:%02X [%s] Tail:%05lX [%s]\r\n",
instance->generic.protocol_name,
instance->generic.data_count_bit,
(unsigned long long)instance->key,
(unsigned long)instance->serial,
instance->button,
land_rover_v0_button_name(instance->button),
(unsigned long)instance->command_signature,
(unsigned long)instance->count,
instance->check,
instance->check_ok ? "OK" : "BAD",
(unsigned long)(((instance->tail >> 15) & 1U) ? 0x1FFFFUL : 0x0FFFFUL),
instance->tail_ok ? "OK" : "BAD");
}
/* ═══════════════════════════════════════════════════════════════════════════
* Encoder helpers
* ═════════════════════════════════════════════════════════════════════════*/
static uint32_t land_rover_v0_signature_from_button(uint8_t button) {
switch(button) {
case LAND_ROVER_V0_BTN_LOCK: return LAND_ROVER_V0_SIG_LOCK;
case LAND_ROVER_V0_BTN_UNLOCK: return LAND_ROVER_V0_SIG_UNLOCK;
default: return 0;
}
}
static uint64_t land_rover_v0_build_key(
uint32_t signature,
uint32_t serial,
uint32_t count) {
uint8_t key_bytes[8] = {0};
key_bytes[0] = (uint8_t)((signature >> 16) & 0xFFU);
key_bytes[1] = (uint8_t)((signature >> 8) & 0xFFU);
key_bytes[2] = (uint8_t)( signature & 0xFFU);
key_bytes[3] = (uint8_t)((serial >> 16) & 0xFFU);
key_bytes[4] = (uint8_t)((serial >> 8) & 0xFFU);
key_bytes[5] = (uint8_t)( serial & 0xFFU);
key_bytes[6] = (uint8_t)((count >> 1) & 0xFFU);
const bool counter_lsb = (count & 1U) != 0;
const uint8_t check = land_rover_v0_calculate_check(count);
key_bytes[7] = (counter_lsb ? 0x80U : 0x00U) | check;
return lr_bytes_to_u64_be(key_bytes);
}
/* ═══════════════════════════════════════════════════════════════════════════
* Encoder public API
* ═════════════════════════════════════════════════════════════════════════*/
void* subghz_protocol_encoder_land_rover_v0_alloc(SubGhzEnvironment* environment) {
UNUSED(environment);
SubGhzProtocolEncoderLandRoverV0* instance =
calloc(1, sizeof(SubGhzProtocolEncoderLandRoverV0));
furi_check(instance);
instance->base.protocol = &subghz_protocol_land_rover_v0;
instance->generic.protocol_name = instance->base.protocol->name;
/* Allocate the waveform upload buffer */
instance->encoder.upload =
malloc(LAND_ROVER_V0_UPLOAD_CAPACITY * sizeof(LevelDuration));
furi_check(instance->encoder.upload);
instance->encoder.repeat = 10;
instance->encoder.size_upload = 0;
instance->encoder.front = 0;
instance->encoder.is_running = false;
return instance;
}
void subghz_protocol_encoder_land_rover_v0_free(void* context) {
furi_check(context);
SubGhzProtocolEncoderLandRoverV0* instance = context;
free(instance->encoder.upload);
free(instance);
}
SubGhzProtocolStatus subghz_protocol_encoder_land_rover_v0_deserialize(
void* context,
FlipperFormat* flipper_format) {
furi_check(context);
SubGhzProtocolEncoderLandRoverV0* instance = context;
SubGhzProtocolStatus ret = SubGhzProtocolStatusError;
instance->encoder.is_running = false;
instance->encoder.front = 0;
instance->encoder.repeat = 10;
do {
if(lr_verify_protocol_name(flipper_format, instance->base.protocol->name) !=
SubGhzProtocolStatusOk)
break;
SubGhzProtocolStatus load_status =
subghz_block_generic_deserialize_check_count_bit(
&instance->generic,
flipper_format,
subghz_protocol_land_rover_v0_const.min_count_bit_for_found);
if(load_status != SubGhzProtocolStatusOk) break;
instance->serial = instance->generic.serial & 0xFFFFFFU;
instance->button = instance->generic.btn;
instance->count = instance->generic.cnt & 0x1FFU;
uint8_t key_bytes[8] = {0};
bool have_key = false;
flipper_format_rewind(flipper_format);
if(flipper_format_read_hex(
flipper_format, LR_FF_KEY, key_bytes, sizeof(key_bytes))) {
instance->key = lr_bytes_to_u64_be(key_bytes);
have_key = true;
}
if(have_key) {
land_rover_v0_parse_key_fields(
instance->key,
&instance->command_signature,
&instance->serial,
&instance->count,
&instance->button,
&instance->check);
}
uint32_t u32 = 0;
bool have_button = false;
flipper_format_rewind(flipper_format);
if(lr_ff_read_u32(flipper_format, LR_FF_SERIAL, &u32))
instance->serial = u32 & 0xFFFFFFU;
flipper_format_rewind(flipper_format);
if(lr_ff_read_u32(flipper_format, LR_FF_CNT, &u32))
instance->count = u32 & 0x1FFU;
flipper_format_rewind(flipper_format);
if(lr_ff_read_u32(flipper_format, LR_FF_BTN, &u32)) {
instance->button = (uint8_t)u32;
have_button = true;
}
flipper_format_rewind(flipper_format);
if(lr_ff_read_u32(flipper_format, LAND_ROVER_V0_FF_BTNSIG, &u32))
instance->command_signature = u32 & 0xFFFFFFU;
if(have_button) {
const uint32_t sig = land_rover_v0_signature_from_button(instance->button);
if(sig != 0U) instance->command_signature = sig;
}
if(instance->command_signature == 0U) break;
instance->key = land_rover_v0_build_key(
instance->command_signature, instance->serial, instance->count);
lr_u64_to_bytes_be(instance->key, key_bytes);
instance->tail = land_rover_v0_calculate_tail(instance->count);
land_rover_v0_parse_key_fields(
instance->key,
&instance->command_signature,
&instance->serial,
&instance->count,
&instance->button,
&instance->check);
instance->generic.data = instance->key;
instance->generic.data_count_bit =
subghz_protocol_land_rover_v0_const.min_count_bit_for_found;
instance->generic.serial = instance->serial;
instance->generic.btn = instance->button;
instance->generic.cnt = instance->count;
flipper_format_rewind(flipper_format);
instance->encoder.repeat = lr_encoder_read_repeat(flipper_format, 10);
if(!land_rover_v0_build_upload(instance) ||
instance->encoder.size_upload == 0U)
break;
/* Update the file with all recalculated fields */
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_hex(
flipper_format, LR_FF_KEY, key_bytes, sizeof(key_bytes));
lr_ff_write_u32(flipper_format, LR_FF_SERIAL, instance->serial);
lr_ff_write_u32(flipper_format, LR_FF_BTN, instance->button);
lr_ff_write_u32(flipper_format, LAND_ROVER_V0_FF_BTNSIG, instance->command_signature);
lr_ff_write_u32(flipper_format, LR_FF_CNT, instance->count);
lr_ff_write_u32(flipper_format, LAND_ROVER_V0_FF_CHECK, instance->check);
lr_ff_write_u32(flipper_format, LAND_ROVER_V0_FF_TAIL, instance->tail);
lr_ff_write_u32(flipper_format, LAND_ROVER_V0_FF_EXTRA_BIT, 1U);
instance->encoder.is_running = true;
ret = SubGhzProtocolStatusOk;
} while(false);
return ret;
}
void subghz_protocol_encoder_land_rover_v0_stop(void* context) {
furi_check(context);
SubGhzProtocolEncoderLandRoverV0* instance = context;
instance->encoder.is_running = false;
}
LevelDuration subghz_protocol_encoder_land_rover_v0_yield(void* context) {
furi_check(context);
SubGhzProtocolEncoderLandRoverV0* instance = context;
if(instance->encoder.front >= instance->encoder.size_upload) {
/* One full repetition done; count it down */
if(instance->encoder.repeat > 0) {
instance->encoder.repeat--;
}
instance->encoder.front = 0;
if(instance->encoder.repeat == 0) {
instance->encoder.is_running = false;
return level_duration_reset();
}
}
return instance->encoder.upload[instance->encoder.front++];
}
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <furi.h>
#include <lib/subghz/protocols/base.h>
#include <lib/subghz/types.h>
#include <lib/subghz/blocks/const.h>
#include <lib/subghz/blocks/decoder.h>
#include <lib/subghz/blocks/encoder.h>
#include <lib/subghz/blocks/generic.h>
#include <lib/subghz/blocks/math.h>
#include <flipper_format/flipper_format.h>
#include <lib/toolbox/level_duration.h>
#define LAND_ROVER_PROTOCOL_V0_NAME "Land Rover V0"
extern const SubGhzProtocol subghz_protocol_land_rover_v0;
void* subghz_protocol_decoder_land_rover_v0_alloc(SubGhzEnvironment* environment);
void subghz_protocol_decoder_land_rover_v0_free(void* context);
void subghz_protocol_decoder_land_rover_v0_reset(void* context);
void subghz_protocol_decoder_land_rover_v0_feed(void* context, bool level, uint32_t duration);
uint8_t subghz_protocol_decoder_land_rover_v0_get_hash_data(void* context);
SubGhzProtocolStatus subghz_protocol_decoder_land_rover_v0_serialize(
void* context,
FlipperFormat* flipper_format,
SubGhzRadioPreset* preset);
SubGhzProtocolStatus
subghz_protocol_decoder_land_rover_v0_deserialize(void* context, FlipperFormat* flipper_format);
void subghz_protocol_decoder_land_rover_v0_get_string(void* context, FuriString* output);
void* subghz_protocol_encoder_land_rover_v0_alloc(SubGhzEnvironment* environment);
void subghz_protocol_encoder_land_rover_v0_free(void* context);
SubGhzProtocolStatus
subghz_protocol_encoder_land_rover_v0_deserialize(void* context, FlipperFormat* flipper_format);
void subghz_protocol_encoder_land_rover_v0_stop(void* context);
LevelDuration subghz_protocol_encoder_land_rover_v0_yield(void* context);
+1
View File
@@ -84,6 +84,7 @@ const SubGhzProtocol* const subghz_protocol_registry_items[] = {
&ford_protocol_v1,
&ford_protocol_v2,
&ford_protocol_v3,
&subghz_protocol_land_rover_v0,
};
+1
View File
@@ -85,3 +85,4 @@
#include "ford_v1.h"
#include "ford_v2.h"
#include "ford_v3.h"
#include "land_rover_v0.h"