mirror of
https://github.com/D4C1-Labs/Flipper-ARF.git
synced 2026-07-03 17:43:02 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bf9ca01621 | |||
| 86f5aae002 | |||
| 46f3a5c993 | |||
| 52015fb289 | |||
| 23ba62cd69 | |||
| cd1e9d6945 | |||
| c49b843096 | |||
| 0c35337bb7 | |||
| e419b9865a | |||
| a89cb55529 |
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
@@ -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.
|
||||||
@@ -22,6 +22,7 @@ This project may incorporate, adapt, or build upon **other open-source projects*
|
|||||||
- [Contribution Policy](#contribution-policy)
|
- [Contribution Policy](#contribution-policy)
|
||||||
- [Citations & References](#citations--references)
|
- [Citations & References](#citations--references)
|
||||||
- [Disclaimer](#disclaimer)
|
- [Disclaimer](#disclaimer)
|
||||||
|
- [Special Thanks](#special-thanks-to-everyone-who-contributes-to-this-project)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -35,6 +36,8 @@ This project may incorporate, adapt, or build upon **other open-source projects*
|
|||||||
| Keeloq Key Manager | Mod Hopping Config |
|
| Keeloq Key Manager | Mod Hopping Config |
|
||||||
|  |  |
|
|  |  |
|
||||||
| PSA XTEA Decrypt | Counter BruteForce |
|
| PSA XTEA Decrypt | Counter BruteForce |
|
||||||
|
|  |  |
|
||||||
|
| Custom Emulation Settings | Custom Emulation Scene |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -362,13 +365,22 @@ IN NO EVENT SHALL THE AUTHORS, COPYRIGHT HOLDERS, OR CONTRIBUTORS BE LIABLE FOR
|
|||||||
|
|
||||||
## Special Thanks
|
## Special Thanks
|
||||||
|
|
||||||
- 47LeCoste
|
<table align="center">
|
||||||
- Ash
|
<tr>
|
||||||
- D4c1
|
<td align="center">
|
||||||
- D4rks1d3
|
<a href="https://github.com/whatthefxck">
|
||||||
- LTX74
|
<img src="https://avatars.githubusercontent.com/whatthefxck?s=80" width="80" height="80" alt="whatthefxck"/>
|
||||||
- Leeroy
|
</a>
|
||||||
- lupettohf
|
</td>
|
||||||
- MMX
|
<td align="center">
|
||||||
- RalphWiggum
|
<a href="https://github.com/zero-mega">
|
||||||
- zero-mega
|
<img src="https://avatars.githubusercontent.com/zero-mega?s=80" width="80" height="80" alt="zero-mega"/>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
Special thanks to everyone who contributed code, testing, reversing,
|
||||||
|
research, ideas, captures and documentation.
|
||||||
|
</p>
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ typedef enum {
|
|||||||
SubGhzCustomEventViewFreqAnalOkLong,
|
SubGhzCustomEventViewFreqAnalOkLong,
|
||||||
|
|
||||||
SubGhzCustomEventByteInputDone,
|
SubGhzCustomEventByteInputDone,
|
||||||
|
SubGhzCustomEventCarEmulateTransmit,
|
||||||
|
SubGhzCustomEventCarEmulateStop,
|
||||||
|
SubGhzCustomEventCarEmulateExit,
|
||||||
|
|
||||||
} SubGhzCustomEvent;
|
} SubGhzCustomEvent;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ typedef enum {
|
|||||||
SubGhzViewIdReadRAW,
|
SubGhzViewIdReadRAW,
|
||||||
SubGhzViewIdPsaDecrypt,
|
SubGhzViewIdPsaDecrypt,
|
||||||
SubGhzViewIdKeeloqDecrypt,
|
SubGhzViewIdKeeloqDecrypt,
|
||||||
|
SubGhzViewIdCarEmulate,
|
||||||
|
|
||||||
} SubGhzViewId;
|
} 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, keeloq_bf2, KeeloqBf2)
|
||||||
ADD_SCENE(subghz, kl_bf_cleanup, KlBfCleanup)
|
ADD_SCENE(subghz, kl_bf_cleanup, KlBfCleanup)
|
||||||
ADD_SCENE(subghz, counter_bf, CounterBf)
|
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 "../subghz_i.h"
|
||||||
#include <lib/subghz/subghz_protocol_registry.h>
|
#include <lib/subghz/subghz_protocol_registry.h>
|
||||||
|
|
||||||
void subghz_scene_protocol_list_submenu_callback(void* context, uint32_t index) {
|
#define TAG "SubGhzSceneProtocolList"
|
||||||
SubGhz* subghz = context;
|
|
||||||
view_dispatcher_send_custom_event(subghz->view_dispatcher, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
void subghz_scene_protocol_list_on_enter(void* context) {
|
/* ── helpers ──────────────────────────────────────────────────────────────── */
|
||||||
SubGhz* subghz = context;
|
|
||||||
|
|
||||||
submenu_reset(subghz->submenu);
|
static bool proto_filter_contains(const char* filter, const char* name) {
|
||||||
|
const char* p = filter;
|
||||||
size_t protocol_count = subghz_protocol_registry_count(&subghz_protocol_registry);
|
while(*p) {
|
||||||
|
const char* comma = strchr(p, ',');
|
||||||
char header_str[32];
|
size_t len = comma ? (size_t)(comma - p) : strlen(p);
|
||||||
snprintf(header_str, sizeof(header_str), "Protocols: %zu", protocol_count);
|
if(len == strlen(name) && strncmp(p, name, len) == 0) return true;
|
||||||
submenu_set_header(subghz->submenu, header_str);
|
if(!comma) break;
|
||||||
|
p = comma + 1;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
return false;
|
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) {
|
void subghz_scene_protocol_list_on_exit(void* context) {
|
||||||
SubGhz* subghz = 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;
|
SubGhz* subghz = context;
|
||||||
|
|
||||||
// The check can be moved to /lib/subghz/receiver.c, but may result in false positives
|
// 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) {
|
if((decoder_base->protocol->flag & subghz->ignore_filter) == 0) {
|
||||||
SubGhzHistory* history = subghz->history;
|
SubGhzHistory* history = subghz->history;
|
||||||
FuriString* item_name = furi_string_alloc();
|
FuriString* item_name = furi_string_alloc();
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ enum SubGhzSettingIndex {
|
|||||||
SubGhzSettingIndexIgnoreNiceFlorS,
|
SubGhzSettingIndexIgnoreNiceFlorS,
|
||||||
SubGhzSettingIndexDeleteOldSignals,
|
SubGhzSettingIndexDeleteOldSignals,
|
||||||
SubGhzSettingIndexSound,
|
SubGhzSettingIndexSound,
|
||||||
|
SubGhzSettingIndexProtoFilter,
|
||||||
SubGhzSettingIndexResetToDefault,
|
SubGhzSettingIndexResetToDefault,
|
||||||
SubGhzSettingIndexLock,
|
SubGhzSettingIndexLock,
|
||||||
SubGhzSettingIndexRAWThresholdRSSI,
|
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) {
|
static void subghz_scene_receiver_config_var_list_enter_callback(void* context, uint32_t index) {
|
||||||
furi_assert(context);
|
furi_assert(context);
|
||||||
SubGhz* subghz = 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(
|
view_dispatcher_send_custom_event(
|
||||||
subghz->view_dispatcher, SubGhzCustomEventSceneSettingLock);
|
subghz->view_dispatcher, SubGhzCustomEventSceneSettingLock);
|
||||||
} else if(index == SubGhzSettingIndexResetToDefault) {
|
} 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->filter = subghz->filter;
|
||||||
subghz->last_settings->delete_old_signals = false;
|
subghz->last_settings->delete_old_signals = false;
|
||||||
subghz->last_settings->tx_power = subghz->tx_power = 0;
|
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_speaker_set_state(subghz->txrx, speaker_value[default_index]);
|
||||||
|
|
||||||
subghz_txrx_hopper_set_state(subghz->txrx, hopping_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) !=
|
if(scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneReadRAW) !=
|
||||||
SubGhzCustomEventManagerSet) {
|
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
|
// Reset to default
|
||||||
variable_item_list_add(subghz->variable_item_list, "Reset to default", 1, NULL, NULL);
|
variable_item_list_add(subghz->variable_item_list, "Reset to default", 1, NULL, NULL);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ enum SubmenuIndex {
|
|||||||
SubmenuIndexEdit,
|
SubmenuIndexEdit,
|
||||||
SubmenuIndexDelete,
|
SubmenuIndexDelete,
|
||||||
SubmenuIndexSignalSettings,
|
SubmenuIndexSignalSettings,
|
||||||
SubmenuIndexCounterBf
|
SubmenuIndexCounterBf, /* <-- comma was missing here */
|
||||||
|
SubmenuIndexCarEmulateSettings,
|
||||||
};
|
};
|
||||||
|
|
||||||
void subghz_scene_saved_menu_submenu_callback(void* context, uint32_t index) {
|
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_scene_saved_menu_submenu_callback,
|
||||||
subghz);
|
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)) {
|
if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) {
|
||||||
submenu_add_item(
|
submenu_add_item(
|
||||||
subghz->submenu,
|
subghz->submenu,
|
||||||
@@ -109,7 +117,22 @@ bool subghz_scene_saved_menu_on_event(void* context, SceneManagerEvent event) {
|
|||||||
if(event.event == SubmenuIndexEmulate) {
|
if(event.event == SubmenuIndexEmulate) {
|
||||||
scene_manager_set_scene_state(
|
scene_manager_set_scene_state(
|
||||||
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexEmulate);
|
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;
|
return true;
|
||||||
} else if(event.event == SubmenuIndexPsaDecrypt) {
|
} else if(event.event == SubmenuIndexPsaDecrypt) {
|
||||||
scene_manager_set_scene_state(
|
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);
|
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexCounterBf);
|
||||||
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneCounterBf);
|
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneCounterBf);
|
||||||
return true;
|
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;
|
return false;
|
||||||
|
|||||||
@@ -206,6 +206,12 @@ SubGhz* subghz_alloc(bool alloc_for_tx_only) {
|
|||||||
SubGhzViewIdKeeloqDecrypt,
|
SubGhzViewIdKeeloqDecrypt,
|
||||||
subghz_view_keeloq_decrypt_get_view(subghz->subghz_keeloq_decrypt));
|
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
|
//init threshold rssi
|
||||||
subghz->threshold_rssi = subghz_threshold_rssi_alloc();
|
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);
|
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdKeeloqDecrypt);
|
||||||
subghz_view_keeloq_decrypt_free(subghz->subghz_keeloq_decrypt);
|
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
|
// Read RAW
|
||||||
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdReadRAW);
|
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdReadRAW);
|
||||||
subghz_read_raw_free(subghz->subghz_read_raw);
|
subghz_read_raw_free(subghz->subghz_read_raw);
|
||||||
|
|||||||
@@ -43,6 +43,8 @@
|
|||||||
#include "helpers/subghz_txrx.h"
|
#include "helpers/subghz_txrx.h"
|
||||||
#include "helpers/subghz_keeloq_keys.h"
|
#include "helpers/subghz_keeloq_keys.h"
|
||||||
|
|
||||||
|
#include "views/subghz_car_emulate.h"
|
||||||
|
|
||||||
#define SUBGHZ_MAX_LEN_NAME 64
|
#define SUBGHZ_MAX_LEN_NAME 64
|
||||||
#define SUBGHZ_EXT_PRESET_NAME true
|
#define SUBGHZ_EXT_PRESET_NAME true
|
||||||
#define SUBGHZ_RAW_THRESHOLD_MIN (-90.0f)
|
#define SUBGHZ_RAW_THRESHOLD_MIN (-90.0f)
|
||||||
@@ -76,6 +78,7 @@ struct SubGhz {
|
|||||||
SubGhzReadRAW* subghz_read_raw;
|
SubGhzReadRAW* subghz_read_raw;
|
||||||
SubGhzViewPsaDecrypt* subghz_psa_decrypt;
|
SubGhzViewPsaDecrypt* subghz_psa_decrypt;
|
||||||
SubGhzViewKeeloqDecrypt* subghz_keeloq_decrypt;
|
SubGhzViewKeeloqDecrypt* subghz_keeloq_decrypt;
|
||||||
|
SubGhzCarEmulateView* car_emulate_view;
|
||||||
bool raw_send_only;
|
bool raw_send_only;
|
||||||
|
|
||||||
bool save_datetime_set;
|
bool save_datetime_set;
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
#define SUBGHZ_LAST_SETTING_FIELD_HOPPING_THRESHOLD "HoppingThreshold"
|
#define SUBGHZ_LAST_SETTING_FIELD_HOPPING_THRESHOLD "HoppingThreshold"
|
||||||
#define SUBGHZ_LAST_SETTING_FIELD_LED_AND_POWER_AMP "LedAndPowerAmp"
|
#define SUBGHZ_LAST_SETTING_FIELD_LED_AND_POWER_AMP "LedAndPowerAmp"
|
||||||
#define SUBGHZ_LAST_SETTING_FIELD_TX_POWER "TXPower"
|
#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* subghz_last_settings_alloc(void) {
|
||||||
SubGhzLastSettings* instance = malloc(sizeof(SubGhzLastSettings));
|
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->enable_preset_hopping = false;
|
||||||
instance->preset_hopping_threshold = SUBGHZ_LAST_SETTING_DEFAULT_PRESET_HOPPING_THRESHOLD;
|
instance->preset_hopping_threshold = SUBGHZ_LAST_SETTING_DEFAULT_PRESET_HOPPING_THRESHOLD;
|
||||||
instance->leds_and_amp = true;
|
instance->leds_and_amp = true;
|
||||||
|
instance->protocol_filter[0] = '\0';
|
||||||
|
|
||||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||||
FlipperFormat* fff_data_file = flipper_format_file_alloc(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)) {
|
1)) {
|
||||||
flipper_format_rewind(fff_data_file);
|
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);
|
} while(0);
|
||||||
} else {
|
} 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)) {
|
file, SUBGHZ_LAST_SETTING_FIELD_LED_AND_POWER_AMP, &instance->leds_and_amp, 1)) {
|
||||||
break;
|
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;
|
saved = true;
|
||||||
} while(0);
|
} while(0);
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ typedef struct {
|
|||||||
float preset_hopping_threshold;
|
float preset_hopping_threshold;
|
||||||
bool leds_and_amp;
|
bool leds_and_amp;
|
||||||
uint8_t tx_power;
|
uint8_t tx_power;
|
||||||
|
bool custom_car_emulate;
|
||||||
|
char protocol_filter[256]; /* comma-separated allowlist, empty = disabled */
|
||||||
} SubGhzLastSettings;
|
} SubGhzLastSettings;
|
||||||
|
|
||||||
SubGhzLastSettings* subghz_last_settings_alloc(void);
|
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
|
||||||
@@ -16,3 +16,59 @@ void subghz_custom_btn_set_max(uint8_t b);
|
|||||||
void subghz_custom_btn_set_prog_mode(ProgMode prog_mode);
|
void subghz_custom_btn_set_prog_mode(ProgMode prog_mode);
|
||||||
|
|
||||||
ProgMode subghz_custom_btn_get_prog_mode(void);
|
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); \
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <lib/toolbox/manchester_decoder.h>
|
#include <lib/toolbox/manchester_decoder.h>
|
||||||
#include <lib/toolbox/manchester_encoder.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_SHORT 200U
|
||||||
#define FORD_V2_TE_LONG 400U
|
#define FORD_V2_TE_LONG 400U
|
||||||
@@ -34,6 +35,16 @@
|
|||||||
#define FORD_V2_PREAMBLE_COUNT_MAX 0xFFFFU
|
#define FORD_V2_PREAMBLE_COUNT_MAX 0xFFFFU
|
||||||
#define FORD_V2_ENCODER_DEFAULT_REPEAT 10U
|
#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 =
|
static const uint16_t ford_v2_sync_shift16_inv =
|
||||||
(uint16_t)(~(((uint16_t)FORD_V2_SYNC_0 << 8) | (uint16_t)FORD_V2_SYNC_1));
|
(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;
|
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) {
|
if(instance->base.callback) {
|
||||||
instance->base.callback(&instance->base, instance->base.context);
|
instance->base.callback(&instance->base, instance->base.context);
|
||||||
}
|
}
|
||||||
@@ -461,26 +476,44 @@ static SubGhzProtocolStatus ford_v2_encoder_deserialize_read_header(
|
|||||||
return SubGhzProtocolStatusOk;
|
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);
|
ford_v2_encoder_rebuild_raw_from_payload(instance);
|
||||||
|
|
||||||
if(!ford_v2_button_is_valid(instance->raw_bytes[6])) {
|
if(!ford_v2_button_is_valid(instance->raw_bytes[6])) {
|
||||||
return SubGhzProtocolStatusErrorParserOthers;
|
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);
|
ford_v2_encoder_refresh_data_from_raw(instance);
|
||||||
|
|
||||||
instance->generic.btn = instance->raw_bytes[6];
|
instance->generic.btn = instance->raw_bytes[6];
|
||||||
instance->generic.serial = ((uint32_t)instance->raw_bytes[2] << 24) |
|
instance->generic.serial =
|
||||||
((uint32_t)instance->raw_bytes[3] << 16) |
|
((uint32_t)instance->raw_bytes[2] << 24) |
|
||||||
((uint32_t)instance->raw_bytes[4] << 8) |
|
((uint32_t)instance->raw_bytes[3] << 16) |
|
||||||
(uint32_t)instance->raw_bytes[5];
|
((uint32_t)instance->raw_bytes[4] << 8) |
|
||||||
instance->generic.cnt = (uint16_t)((((uint16_t)(instance->raw_bytes[7] & 0x7FU)) << 9) |
|
(uint32_t)instance->raw_bytes[5];
|
||||||
(((uint16_t)instance->raw_bytes[8]) << 1) |
|
instance->generic.cnt = cnt;
|
||||||
((uint16_t)(instance->raw_bytes[9] >> 7)));
|
|
||||||
|
|
||||||
return SubGhzProtocolStatusOk;
|
return SubGhzProtocolStatusOk;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void ford_v2_encoder_deserialize_apply_repeat(SubGhzProtocolEncoderFordV2* instance, FlipperFormat* flipper_format) {
|
static void ford_v2_encoder_deserialize_apply_repeat(SubGhzProtocolEncoderFordV2* instance, FlipperFormat* flipper_format) {
|
||||||
flipper_format_rewind(flipper_format);
|
flipper_format_rewind(flipper_format);
|
||||||
uint32_t repeat = FORD_V2_ENCODER_DEFAULT_REPEAT;
|
uint32_t repeat = FORD_V2_ENCODER_DEFAULT_REPEAT;
|
||||||
@@ -523,13 +556,36 @@ SubGhzProtocolStatus
|
|||||||
FuriString* temp_str = furi_string_alloc();
|
FuriString* temp_str = furi_string_alloc();
|
||||||
furi_check(temp_str);
|
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) {
|
if(ret == SubGhzProtocolStatusOk) {
|
||||||
ret = ford_v2_encoder_deserialize_validate_and_pack(instance);
|
ret = ford_v2_encoder_deserialize_validate_and_pack(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(ret == SubGhzProtocolStatusOk) {
|
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_deserialize_apply_repeat(instance, flipper_format);
|
||||||
ford_v2_encoder_build_upload(instance);
|
ford_v2_encoder_build_upload(instance);
|
||||||
instance->encoder.is_running = true;
|
instance->encoder.is_running = true;
|
||||||
@@ -539,6 +595,7 @@ SubGhzProtocolStatus
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void subghz_protocol_encoder_ford_v2_stop(void* context) {
|
void subghz_protocol_encoder_ford_v2_stop(void* context) {
|
||||||
furi_check(context);
|
furi_check(context);
|
||||||
SubGhzProtocolEncoderFordV2* instance = context;
|
SubGhzProtocolEncoderFordV2* instance = context;
|
||||||
@@ -740,6 +797,25 @@ SubGhzProtocolStatus subghz_protocol_decoder_ford_v2_deserialize(
|
|||||||
return SubGhzProtocolStatusErrorParserOthers;
|
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;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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++];
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -84,6 +84,7 @@ const SubGhzProtocol* const subghz_protocol_registry_items[] = {
|
|||||||
&ford_protocol_v1,
|
&ford_protocol_v1,
|
||||||
&ford_protocol_v2,
|
&ford_protocol_v2,
|
||||||
&ford_protocol_v3,
|
&ford_protocol_v3,
|
||||||
|
&subghz_protocol_land_rover_v0,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -85,3 +85,4 @@
|
|||||||
#include "ford_v1.h"
|
#include "ford_v1.h"
|
||||||
#include "ford_v2.h"
|
#include "ford_v2.h"
|
||||||
#include "ford_v3.h"
|
#include "ford_v3.h"
|
||||||
|
#include "land_rover_v0.h"
|
||||||
|
|||||||
Reference in New Issue
Block a user