Compare commits

...

6 Commits

Author SHA1 Message Date
D4rk$1d3 23ba62cd69 Add files via upload 2026-05-18 20:49:21 -03:00
d4rks1d33 cd1e9d6945 Stupid chatGPT that add utm_source=chatgpt.com on links
Build Dev Firmware / build (push) Failing after 18s
2026-05-18 20:39:55 -03:00
d4rks1d33 c49b843096 Stupid chatGPT that add utm_source=chatgpt.com on links 2026-05-18 20:37:13 -03:00
d4rks1d33 0c35337bb7 Some updates 2026-05-18 20:33:56 -03:00
d4rks1d33 e419b9865a Rollback
Build Dev Firmware / build (push) Failing after 16s
2026-05-08 16:21:24 +00:00
D4rk$1d3 a89cb55529 Update README.md
Build Dev Firmware / build (push) Successful in 7m23s
Updated the Special Thanks section to include a table format with contributor images and a message of appreciation.
2026-05-06 21:46:57 -03:00
23 changed files with 2354 additions and 30 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

+61 -9
View File
@@ -8,25 +8,62 @@ on:
permissions:
contents: write
concurrency:
group: release
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get latest semantic version tag
id: version
shell: bash
run: |
git fetch --tags
LAST_TAG=$(git tag --sort=-v:refname | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || true)
if [ -z "$LAST_TAG" ]; then
NEW_TAG="1.0.0"
else
IFS='.' read -r MAJOR MINOR PATCH <<< "$LAST_TAG"
PATCH=$((PATCH + 1))
NEW_TAG="$MAJOR.$MINOR.$PATCH"
fi
echo "Latest tag: $LAST_TAG"
echo "New tag: $NEW_TAG"
echo "NEW_TAG=$NEW_TAG" >> $GITHUB_OUTPUT
- name: Create Git tag
shell: bash
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag ${{ steps.version.outputs.NEW_TAG }}
git push origin ${{ steps.version.outputs.NEW_TAG }}
- name: Build firmware
shell: bash
run: |
export DIST_SUFFIX=Flipper-ARF
chmod +x fbt
./fbt COMPACT=1 DEBUG=0 updater_package
- name: Generate tag name
id: tag
run: echo "TAG=dev-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Detect firmware updater
id: firmware
shell: bash
run: |
DIR=$(ls -d dist/f7-* | head -n 1)
FILE="$DIR/flipper-z-f7-update-Flipper-ARF.tgz"
@@ -36,13 +73,28 @@ jobs:
exit 1
fi
echo "Found firmware: $FILE"
echo "FILE=$FILE" >> $GITHUB_OUTPUT
- name: Create Release
- name: Read changelog
id: changelog
shell: bash
run: |
{
echo 'CHANGELOG<<EOF'
cat CHANGELOG.md
echo 'EOF'
} >> "$GITHUB_OUTPUT"
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.tag.outputs.TAG }}
name: Dev Build ${{ steps.tag.outputs.TAG }}
files: ${{ steps.firmware.outputs.FILE }}
tag_name: ${{ steps.version.outputs.NEW_TAG }}
name: Release ${{ steps.version.outputs.NEW_TAG }}
body: ${{ steps.changelog.outputs.CHANGELOG }}
make_latest: true
files: |
${{ steps.firmware.outputs.FILE }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+123
View File
@@ -0,0 +1,123 @@
# Changelog
---
# Added
## CarEmulate Scene
* Added custom automotive key emulation view with:
* D-pad button layout
* TX overlay animation
* Counter delta display
* TX power control integration
* Added `CarEmulateSettings` scene with:
* Enable/disable toggle for the custom emulate UI
* TX power selector
* Persistent storage through `SubGhzLastSettings`
### Special Thanks
Special thanks to [RocketGod-git](https://github.com/RocketGod-git) for inspiration from the Proto Pirate application and related UI/interaction concepts that helped shape the CarEmulate workflow.
---
## Generic Custom Button API (`custom_btn_i.h`)
Protocols can now expose UP/DOWN/LEFT/RIGHT button cycling in the transmitter view using a single macro instead of manually wiring switch/case handlers and scattered `set_original()` / `set_max()` calls.
### New macro
```
SUBGHZ_CUSTOM_BTN_DEFINE_MAP(
my_proto,
{SUBGHZ_CUSTOM_BTN_OK, 0xAA},
{SUBGHZ_CUSTOM_BTN_UP, 0xBB},
{SUBGHZ_CUSTOM_BTN_DOWN, 0xCC},
{SUBGHZ_CUSTOM_BTN_LEFT, 0xDD},
{SUBGHZ_CUSTOM_BTN_RIGHT, 0xEE},
)
```
The macro automatically generates:
* `my_proto_custom_btn_to_code()`
* `my_proto_code_to_custom_btn()`
* `my_proto_custom_btn_init()`
---
## New Protocols
### Land Rover
* Added support for the Land Rover Sub-GHz protocol.
### Special Thanks
Thanks to [Zero-Mega](https://github.com/Zero-Mega) for research, references, and contributions related to Land Rover protocol support.
---
# Fixed
## CarEmulate
* Fixed `car_emulate_apply_button()` being fully commented out.
* D-pad input never reached the `custom_btn` system.
* Protocols always transmitted the originally captured button regardless of the selected key.
* Fixed `custom_btn_id` potentially being lost if `subghz_tx_start()` internally reset SubGhz state before encoder `deserialize()` execution.
* The selected button is now passed into `car_emulate_start_tx()`
* The button is re-applied immediately before TX begins.
* Fixed hardcoded button labels in the CarEmulate view.
* Previous labels:
* `LOCK`
* `UNLOCK`
* `PANIC`
* `BOOT`
* `XXX`
* Labels are now dynamically stored in the view model and configured by the scene through:
* `subghz_car_emulate_view_set_labels()`
---
# Internal Changes
## `custom_btn_i.h`
* Added `SubGhzCustomBtnEntry`
* Added `SUBGHZ_CUSTOM_BTN_DEFINE_MAP`
* Added automatic conversion helpers
* Added automatic `_init()` helper generation
---
# Ongoing Work
* Continued improvements and cleanup across additional automotive protocols.
* Ongoing protocol analysis, decoder refinement, encoder validation, and transmit reliability improvements.
* Additional protocol integrations and UI enhancements are still actively being developed.
---
# Notes
The old approach required:
* Manual switch/case handlers
* Manual button registration
* Repeated `subghz_custom_btn_set_original()`
* Repeated `subghz_custom_btn_set_max()`
* Protocol-specific duplicated logic
The new API centralizes all button mapping behavior into a single reusable macro system.
+20 -10
View File
@@ -22,6 +22,7 @@ This project may incorporate, adapt, or build upon **other open-source projects*
- [Contribution Policy](#contribution-policy)
- [Citations & References](#citations--references)
- [Disclaimer](#disclaimer)
- [Special Thanks](#special-thanks-to-everyone-who-contributes-to-this-project)
---
@@ -362,13 +363,22 @@ IN NO EVENT SHALL THE AUTHORS, COPYRIGHT HOLDERS, OR CONTRIBUTORS BE LIABLE FOR
## Special Thanks
- 47LeCoste
- Ash
- D4c1
- D4rks1d3
- LTX74
- Leeroy
- lupettohf
- MMX
- RalphWiggum
- zero-mega
<table align="center">
<tr>
<td align="center">
<a href="https://github.com/whatthefxck">
<img src="https://avatars.githubusercontent.com/whatthefxck?s=80" width="80" height="80" alt="whatthefxck"/>
</a>
</td>
<td align="center">
<a href="https://github.com/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,
SubGhzCustomEventByteInputDone,
SubGhzCustomEventCarEmulateTransmit,
SubGhzCustomEventCarEmulateStop,
SubGhzCustomEventCarEmulateExit,
} SubGhzCustomEvent;
typedef enum {
@@ -94,6 +94,7 @@ typedef enum {
SubGhzViewIdReadRAW,
SubGhzViewIdPsaDecrypt,
SubGhzViewIdKeeloqDecrypt,
SubGhzViewIdCarEmulate,
} SubGhzViewId;
@@ -0,0 +1,499 @@
/**
* Scene: CarEmulate
* Custom automotive-key emulation GUI ported from ProtoPirate.
* Activated when SubGhzLastSettings::custom_car_emulate == true and the
* user presses "Emulate" on a saved dynamic protocol.
*
* Flow:
* SavedMenu → Emulate → (custom_car_emulate?) CarEmulate : Transmitter
*/
#include "../subghz_i.h"
#include "../views/subghz_car_emulate.h"
#include "../helpers/subghz_custom_event.h"
#include <lib/subghz/blocks/generic.h>
#include <notification/notification_messages.h>
#include "../helpers/subghz_txrx_i.h"
#include <lib/subghz/blocks/custom_btn_i.h>
#define TAG "SubGhzSceneCarEmulate"
#define MIN_TX_TICKS 66U /* ~666 ms at 100 ms tick */
/* ── Per-session state (heap, freed on exit) ─────────────────────────────── */
typedef struct {
/* Signal metadata read from fff_data */
char protocol_name[48];
uint32_t serial;
uint8_t original_button;
uint32_t original_counter;
uint32_t current_counter;
uint32_t freq;
char preset_short[12]; /* "AM650", "FM476", … */
/* TX state */
bool is_transmitting;
bool stop_pending; /* stop requested before MIN_TX_TICKS elapsed */
uint32_t tx_start_tick;
/* Pending button key (InputKey) decoded from the packed custom event */
uint8_t pending_button;
} CarEmulateState;
static CarEmulateState* s_state = NULL;
/* ═══════════════════════════════════════════════════════════════════════════
* Button mapping (protocol-name → InputKey → button byte)
* Ported verbatim from protopirate_scene_emulate.c
* ═════════════════════════════════════════════════════════════════════════*/
//static uint8_t car_emulate_map_button(
// const char* protocol,
// InputKey key,
// uint8_t original) {
/* Land Rover V0 */
// if(strstr(protocol, "Land Rover")) {
// switch(key) {
// case InputKeyUp: return 0x02; /* Lock */
// case InputKeyOk: return 0x04; /* Unlock */
// default: return original;
// }
// }
/* Mazda */
// if(strstr(protocol, "Mazda")) {
// switch(key) {
// case InputKeyUp: return 0x01;
// case InputKeyOk: return 0x02;
// case InputKeyDown: return 0x04;
// case InputKeyRight: return 0x08;
// default: return original;
// }
// }
/* PSA */
// if(strstr(protocol, "PSA")) {
// switch(key) {
// case InputKeyUp: return 0x1;
// case InputKeyOk: return 0x2;
// case InputKeyDown: return 0x4;
// case InputKeyLeft: return 0x8;
// default: return original;
// }
// }
/* VAG */
// if(strstr(protocol, "VAG")) {
// if(original == 0x10 || original == 0x20 || original == 0x40) {
// switch(key) {
// case InputKeyUp: return 0x20;
// case InputKeyOk: return 0x10;
// case InputKeyDown: return 0x40;
// default: return original;
// }
// }
// switch(key) {
// case InputKeyUp: return 0x2;
// case InputKeyOk: return 0x1;
// case InputKeyDown: return 0x4;
// case InputKeyLeft: return 0x8;
// case InputKeyRight: return 0x3;
// default: return original;
// }
// }
/* Honda Static */
// if(strstr(protocol, "Honda Static")) {
// switch(key) {
// case InputKeyUp: return 0x1;
// case InputKeyOk: return 0x2;
// case InputKeyDown: return 0x4;
// case InputKeyRight: return 0x5;
// case InputKeyLeft: return 0x8;
// default: return original;
// }
// }
/* Ford */
// if(strstr(protocol, "Ford")) {
// switch(key) {
// case InputKeyLeft: return 0x1;
// case InputKeyUp: return 0x2;
// case InputKeyOk: return 0x4;
// case InputKeyDown: return 0x8;
// case InputKeyRight: return 0x10;
// default: return original;
// }
// }
/* Chrysler */
// if(strstr(protocol, "Chrysler")) {
// switch(key) {
// case InputKeyUp: return 0x1;
// case InputKeyOk: return 0x2;
// default: return original;
// }
// }
/* Subaru */
// if(strstr(protocol, "Subaru")) {
// switch(key) {
// case InputKeyUp: return 0x1;
// case InputKeyOk: return 0x2;
// case InputKeyDown: return 0x3;
// case InputKeyLeft: return 0x4;
// case InputKeyRight: return 0x8;
// default: return original;
// }
// }
/* Fiat V1 */
// if(strstr(protocol, "Fiat V1")) {
// switch(key) {
// case InputKeyUp: return 0x8;
// case InputKeyOk: return 0x0;
// case InputKeyDown: return 0xD;
// default: return original;
// }
// }
/* Generic KeeLoq / KIA etc. simple 4-button layout */
// if(strstr(protocol, "Kia") || strstr(protocol, "KIA") ||
// strstr(protocol, "KeeLoq") || strstr(protocol, "Keeloq")) {
// switch(key) {
// case InputKeyUp: return 0x1;
// case InputKeyOk: return 0x2;
// case InputKeyDown: return 0x3;
// case InputKeyLeft: return 0x4;
// case InputKeyRight: return 0x8;
// default: return original;
// }
// }
// return original;
//}
/* ═══════════════════════════════════════════════════════════════════════════
* TX helpers
* ═════════════════════════════════════════════════════════════════════════*/
/**
* Read frequency and short preset name from fff_data.
* Falls back to 433.92 MHz / "AM650" on failure.
*/
static void car_emulate_read_freq_preset(SubGhz* subghz, CarEmulateState* st) {
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
st->freq = 433920000UL;
strncpy(st->preset_short, "AM650", sizeof(st->preset_short) - 1);
if(!fff) return;
uint32_t freq = 0;
flipper_format_rewind(fff);
if(flipper_format_read_uint32(fff, "Frequency", &freq, 1) && freq > 0) {
st->freq = freq;
}
FuriString* preset_str = furi_string_alloc();
flipper_format_rewind(fff);
if(flipper_format_read_string(fff, "Preset", preset_str)) {
/* Convert long FuriHal name → short token used by the setting */
const char* raw = furi_string_get_cstr(preset_str);
const char* short_name = "AM650";
if(strstr(raw, "Ook270")) short_name = "AM270";
else if(strstr(raw, "Ook650")) short_name = "AM650";
else if(strstr(raw, "238")) short_name = "FM238";
else if(strstr(raw, "12K")) short_name = "FM12K";
else if(strstr(raw, "476")) short_name = "FM476";
else if(strstr(raw, "Custom")) short_name = "CUST";
strncpy(st->preset_short, short_name, sizeof(st->preset_short) - 1);
}
furi_string_free(preset_str);
}
/** Update Btn and Cnt fields in fff_data so the transmitter re-serialises them. */
static void car_emulate_apply_button(SubGhz* subghz, InputKey key) {
UNUSED(subghz);
uint8_t custom_btn_id;
switch(key) {
case InputKeyUp: custom_btn_id = SUBGHZ_CUSTOM_BTN_UP; break;
case InputKeyDown: custom_btn_id = SUBGHZ_CUSTOM_BTN_DOWN; break;
case InputKeyLeft: custom_btn_id = SUBGHZ_CUSTOM_BTN_LEFT; break;
case InputKeyRight: custom_btn_id = SUBGHZ_CUSTOM_BTN_RIGHT; break;
case InputKeyOk:
default: custom_btn_id = SUBGHZ_CUSTOM_BTN_OK; break;
}
subghz_custom_btn_set(custom_btn_id);
}
/** Update Cnt in fff_data (Btn is handled by the protocol via custom_btn). */
static void car_emulate_update_fff(SubGhz* subghz, uint32_t counter) {
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
if(!fff) return;
flipper_format_rewind(fff);
flipper_format_insert_or_update_uint32(fff, "Cnt", &counter, 1);
}
/** Apply tx_power to the current preset and start a single transmission burst. */
static bool car_emulate_start_tx(SubGhz* subghz, uint8_t custom_btn_id) {
SubGhzRadioPreset preset = subghz_txrx_get_preset(subghz->txrx);
if(preset.data && preset.data_size > 0 && subghz->tx_power > 0) {
subghz_txrx_set_tx_power(preset.data, preset.data_size, subghz->tx_power);
FURI_LOG_I(TAG, "TX power index applied: %u", subghz->tx_power);
}
subghz_custom_btn_set(custom_btn_id);
bool ok = subghz_tx_start(subghz, subghz_txrx_get_fff_data(subghz->txrx));
if(ok) {
subghz->state_notifications = SubGhzNotificationStateTx;
notification_message(subghz->notifications, &sequence_blink_magenta_10);
FURI_LOG_I(TAG, "TX started");
} else {
FURI_LOG_E(TAG, "subghz_tx_start failed");
}
return ok;
}
/** Stop an active transmission. */
static void car_emulate_stop_tx(SubGhz* subghz) {
subghz_txrx_stop(subghz->txrx);
subghz->state_notifications = SubGhzNotificationStateIDLE;
notification_message(subghz->notifications, &sequence_blink_stop);
FURI_LOG_I(TAG, "TX stopped");
}
/* ═══════════════════════════════════════════════════════════════════════════
* View callback (fired from the View's input handler)
* ═════════════════════════════════════════════════════════════════════════*/
static void subghz_scene_car_emulate_view_callback(uint32_t event, void* context) {
SubGhz* subghz = context;
view_dispatcher_send_custom_event(subghz->view_dispatcher, event);
}
/* ═══════════════════════════════════════════════════════════════════════════
* Helpers to keep the view in sync
* ═════════════════════════════════════════════════════════════════════════*/
static void car_emulate_refresh_view(SubGhz* subghz) {
furi_assert(s_state);
subghz_car_emulate_view_set_data(
subghz->car_emulate_view,
s_state->protocol_name,
s_state->serial,
s_state->current_counter,
s_state->original_counter,
s_state->freq,
s_state->preset_short,
s_state->is_transmitting);
}
/* ═══════════════════════════════════════════════════════════════════════════
* Scene on_enter
* ═════════════════════════════════════════════════════════════════════════*/
void subghz_scene_car_emulate_on_enter(void* context) {
SubGhz* subghz = context;
furi_assert(subghz);
/* Allocate per-session state */
s_state = malloc(sizeof(CarEmulateState));
furi_check(s_state);
memset(s_state, 0, sizeof(CarEmulateState));
/* ── Read metadata from the loaded fff_data ── */
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
if(fff) {
FuriString* tmp = furi_string_alloc();
flipper_format_rewind(fff);
if(flipper_format_read_string(fff, "Protocol", tmp)) {
strncpy(
s_state->protocol_name,
furi_string_get_cstr(tmp),
sizeof(s_state->protocol_name) - 1);
}
flipper_format_rewind(fff);
flipper_format_read_uint32(fff, "Serial", &s_state->serial, 1);
flipper_format_rewind(fff);
uint32_t btn_tmp = 0;
if(flipper_format_read_uint32(fff, "Btn", &btn_tmp, 1)) {
s_state->original_button = (uint8_t)btn_tmp;
}
flipper_format_rewind(fff);
flipper_format_read_uint32(fff, "Cnt", &s_state->original_counter, 1);
s_state->current_counter = s_state->original_counter;
furi_string_free(tmp);
}
/* ── Initialize the custom_btn system ──────────────────────────────────
* Reset first so any leftover state from a previous session is cleared.
* Then deserialize the decoder once: this causes the protocol's own
* deserialize() to call subghz_custom_btn_set_original() and
* subghz_custom_btn_set_max(), which is exactly what the standard
* Transmitter scene does via subghz_scene_transmitter_update_data_show().
* After this call:
* - subghz_custom_btn_get_original() → the button that was in the file
* - subghz_custom_btn_is_allowed() → true if protocol supports it
* - subghz_custom_btn_get_max() → number of buttons available */
subghz_custom_btns_reset();
SubGhzProtocolDecoderBase* decoder = subghz_txrx_get_decoder(subghz->txrx);
if(decoder && fff) {
flipper_format_rewind(fff);
subghz_protocol_decoder_base_deserialize(decoder, fff);
/* Rewind again so subsequent reads in car_emulate_read_freq_preset()
* start from the beginning of the file. */
flipper_format_rewind(fff);
}
subghz_car_emulate_view_set_labels(
subghz->car_emulate_view,
"UNLOCK", /* OK */
"LOCK", /* Up */
"TRUNK", /* Down */
"PANIC", /* Left */
"START" /* Right */
);
car_emulate_read_freq_preset(subghz, s_state);
/* ── Configure the view ── */
subghz_car_emulate_view_set_callback(
subghz->car_emulate_view, subghz_scene_car_emulate_view_callback, subghz);
car_emulate_refresh_view(subghz);
subghz->state_notifications = SubGhzNotificationStateIDLE;
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdCarEmulate);
}
/* ═══════════════════════════════════════════════════════════════════════════
* Scene on_event
* ═════════════════════════════════════════════════════════════════════════*/
bool subghz_scene_car_emulate_on_event(void* context, SceneManagerEvent event) {
SubGhz* subghz = context;
furi_assert(s_state);
bool consumed = false;
if(event.type == SceneManagerEventTypeCustom) {
/* ── Transmit ── */
if((event.event & 0xFFFFU) == SubGhzCustomEventCarEmulateTransmit) {
InputKey key = (InputKey)((event.event >> 16) & 0xFFU);
/* Stop any ongoing TX first */
if(subghz->state_notifications == SubGhzNotificationStateTx) {
car_emulate_stop_tx(subghz);
}
/* Bump counter */
s_state->current_counter++;
/* Set the custom button BEFORE deserialize() is called inside
* subghz_tx_start() → subghz_txrx_tx_start().
* The protocol's deserialize() will call subghz_custom_btn_get()
* to pick the right button code. */
car_emulate_apply_button(subghz, key);
/* Only update the counter in fff_data; the protocol handles Btn. */
car_emulate_update_fff(subghz, s_state->current_counter);
s_state->is_transmitting = true;
s_state->stop_pending = false;
s_state->tx_start_tick = (uint32_t)furi_get_tick();
uint8_t cur_btn = subghz_custom_btn_get();
if(!car_emulate_start_tx(subghz, cur_btn)) {
s_state->is_transmitting = false;
notification_message(subghz->notifications, &sequence_error);
}
car_emulate_refresh_view(subghz);
consumed = true;
/* ── Stop ── */
} else if(event.event == SubGhzCustomEventCarEmulateStop) {
if(s_state->is_transmitting &&
subghz->state_notifications == SubGhzNotificationStateTx) {
uint32_t elapsed = (uint32_t)furi_get_tick() - s_state->tx_start_tick;
if(elapsed >= MIN_TX_TICKS) {
car_emulate_stop_tx(subghz);
s_state->is_transmitting = false;
s_state->stop_pending = false;
} else {
s_state->stop_pending = true;
}
}
car_emulate_refresh_view(subghz);
consumed = true;
/* ── Exit ── */
} else if(event.event == SubGhzCustomEventCarEmulateExit) {
if(subghz->state_notifications == SubGhzNotificationStateTx) {
car_emulate_stop_tx(subghz);
}
scene_manager_search_and_switch_to_previous_scene(
subghz->scene_manager, SubGhzSceneSavedMenu);
consumed = true;
}
} else if(event.type == SceneManagerEventTypeTick) {
if(s_state->is_transmitting &&
subghz->state_notifications == SubGhzNotificationStateTx) {
/* Check if hardware is done */
if(subghz_devices_is_async_complete_tx(subghz->txrx->radio_device)) {
subghz->state_notifications = SubGhzNotificationStateIDLE;
subghz_txrx_stop(subghz->txrx);
if(s_state->stop_pending) {
s_state->is_transmitting = false;
s_state->stop_pending = false;
notification_message(subghz->notifications, &sequence_blink_stop);
}
} else {
/* Still transmitting blink LED */
notification_message(subghz->notifications, &sequence_blink_magenta_10);
}
/* Enforce MIN_TX_TICKS stop gate */
if(s_state->stop_pending) {
uint32_t elapsed = (uint32_t)furi_get_tick() - s_state->tx_start_tick;
if(elapsed >= MIN_TX_TICKS) {
car_emulate_stop_tx(subghz);
s_state->is_transmitting = false;
s_state->stop_pending = false;
}
}
}
/* Refresh view every tick for animation */
car_emulate_refresh_view(subghz);
consumed = true;
}
return consumed;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Scene on_exit
* ═════════════════════════════════════════════════════════════════════════*/
void subghz_scene_car_emulate_on_exit(void* context) {
SubGhz* subghz = context;
if(subghz->state_notifications == SubGhzNotificationStateTx) {
car_emulate_stop_tx(subghz);
}
subghz->state_notifications = SubGhzNotificationStateIDLE;
notification_message(subghz->notifications, &sequence_blink_stop);
/* Clear view callbacks */
subghz_car_emulate_view_set_callback(subghz->car_emulate_view, NULL, NULL);
/* Free per-session state */
if(s_state) {
free(s_state);
s_state = NULL;
}
}
@@ -0,0 +1,109 @@
/**
* Scene: CarEmulateSettings
* Toggle: Custom Emulate Off / On
* Selector: TX Power (reuses the same table as Radio Settings)
* Both settings are persisted in SubGhzLastSettings.
*/
#include "../subghz_i.h"
#include <lib/toolbox/value_index.h>
#define TAG "SubGhzCarEmulateSettings"
/* ── Toggle ──────────────────────────────────────────────────────────────── */
static const char* const toggle_text[] = {"Off", "On"};
static void subghz_scene_car_emulate_settings_toggle_changed(VariableItem* item) {
SubGhz* subghz = variable_item_get_context(item);
furi_assert(subghz);
uint8_t index = variable_item_get_current_value_index(item);
variable_item_set_current_value_text(item, toggle_text[index]);
subghz->last_settings->custom_car_emulate = (index == 1);
subghz_last_settings_save(subghz->last_settings);
}
/* ── TX Power ────────────────────────────────────────────────────────────── */
/* Must match the table in subghz_scene_radio_settings.c exactly */
#define CE_TX_POWER_COUNT 9
static const char* const ce_tx_power_text[CE_TX_POWER_COUNT] = {
"Preset", /* index 0 → use whatever the preset has baked in */
"10dBm +",
"7dBm",
"5dBm",
"0dBm",
"-10dBm",
"-15dBm",
"-20dBm",
"-30dBm",
};
static void subghz_scene_car_emulate_settings_power_changed(VariableItem* item) {
SubGhz* subghz = variable_item_get_context(item);
furi_assert(subghz);
uint8_t index = variable_item_get_current_value_index(item);
variable_item_set_current_value_text(item, ce_tx_power_text[index]);
/* Mirror the same fields that Radio Settings touches so the value is
* visible everywhere and survives app restart. */
subghz->tx_power = index;
subghz->last_settings->tx_power = index;
subghz_last_settings_save(subghz->last_settings);
/* Patch the live preset buffer immediately so any subsequent TX in this
* session uses the new power without needing a restart. */
SubGhzRadioPreset preset = subghz_txrx_get_preset(subghz->txrx);
if(preset.data && preset.data_size > 0) {
subghz_txrx_set_tx_power(preset.data, preset.data_size, index);
}
}
/* ── Scene callbacks ─────────────────────────────────────────────────────── */
void subghz_scene_car_emulate_settings_on_enter(void* context) {
SubGhz* subghz = context;
furi_assert(subghz);
VariableItemList* list = subghz->variable_item_list;
variable_item_list_reset(list);
/* ── Row 1: Custom Emulate toggle ── */
VariableItem* item = variable_item_list_add(
list,
"Custom Emulate",
2,
subghz_scene_car_emulate_settings_toggle_changed,
subghz);
uint8_t toggle_idx = subghz->last_settings->custom_car_emulate ? 1 : 0;
variable_item_set_current_value_index(item, toggle_idx);
variable_item_set_current_value_text(item, toggle_text[toggle_idx]);
/* ── Row 2: TX Power ── */
item = variable_item_list_add(
list,
"TX Power",
CE_TX_POWER_COUNT,
subghz_scene_car_emulate_settings_power_changed,
subghz);
/* Clamp stored value to valid range in case settings file is corrupt */
uint8_t power_idx = subghz->tx_power;
if(power_idx >= CE_TX_POWER_COUNT) power_idx = 0;
variable_item_set_current_value_index(item, power_idx);
variable_item_set_current_value_text(item, ce_tx_power_text[power_idx]);
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdVariableItemList);
}
bool subghz_scene_car_emulate_settings_on_event(void* context, SceneManagerEvent event) {
UNUSED(context);
UNUSED(event);
return false;
}
void subghz_scene_car_emulate_settings_on_exit(void* context) {
SubGhz* subghz = context;
variable_item_list_reset(subghz->variable_item_list);
}
@@ -34,3 +34,5 @@ ADD_SCENE(subghz, keeloq_decrypt, KeeloqDecrypt)
ADD_SCENE(subghz, keeloq_bf2, KeeloqBf2)
ADD_SCENE(subghz, kl_bf_cleanup, KlBfCleanup)
ADD_SCENE(subghz, counter_bf, CounterBf)
ADD_SCENE(subghz, car_emulate, CarEmulate)
ADD_SCENE(subghz, car_emulate_settings, CarEmulateSettings)
@@ -6,7 +6,8 @@ enum SubmenuIndex {
SubmenuIndexEdit,
SubmenuIndexDelete,
SubmenuIndexSignalSettings,
SubmenuIndexCounterBf
SubmenuIndexCounterBf, /* <-- comma was missing here */
SubmenuIndexCarEmulateSettings,
};
void subghz_scene_saved_menu_submenu_callback(void* context, uint32_t index) {
@@ -77,6 +78,13 @@ void subghz_scene_saved_menu_on_enter(void* context) {
subghz_scene_saved_menu_submenu_callback,
subghz);
submenu_add_item(
subghz->submenu,
"Custom Emulate Settings",
SubmenuIndexCarEmulateSettings,
subghz_scene_saved_menu_submenu_callback,
subghz);
if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) {
submenu_add_item(
subghz->submenu,
@@ -109,7 +117,22 @@ bool subghz_scene_saved_menu_on_event(void* context, SceneManagerEvent event) {
if(event.event == SubmenuIndexEmulate) {
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexEmulate);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneTransmitter);
bool use_custom = subghz->last_settings->custom_car_emulate;
if(use_custom) {
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
uint32_t cnt_tmp = 0;
flipper_format_rewind(fff);
if(!flipper_format_read_uint32(fff, "Cnt", &cnt_tmp, 1)) {
use_custom = false;
}
}
if(use_custom) {
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneCarEmulate);
} else {
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneTransmitter);
}
return true;
} else if(event.event == SubmenuIndexPsaDecrypt) {
scene_manager_set_scene_state(
@@ -136,6 +159,14 @@ bool subghz_scene_saved_menu_on_event(void* context, SceneManagerEvent event) {
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexCounterBf);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneCounterBf);
return true;
} else if(event.event == SubmenuIndexCarEmulateSettings) {
/* <-- was outside the if block due to misplaced brace, now fixed */
scene_manager_set_scene_state(
subghz->scene_manager,
SubGhzSceneSavedMenu,
SubmenuIndexCarEmulateSettings);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneCarEmulateSettings);
return true;
}
}
return false;
+10
View File
@@ -206,6 +206,12 @@ SubGhz* subghz_alloc(bool alloc_for_tx_only) {
SubGhzViewIdKeeloqDecrypt,
subghz_view_keeloq_decrypt_get_view(subghz->subghz_keeloq_decrypt));
subghz->car_emulate_view = subghz_car_emulate_view_alloc();
view_dispatcher_add_view(
subghz->view_dispatcher,
SubGhzViewIdCarEmulate,
subghz_car_emulate_view_get_view(subghz->car_emulate_view));
//init threshold rssi
subghz->threshold_rssi = subghz_threshold_rssi_alloc();
@@ -321,6 +327,10 @@ void subghz_free(SubGhz* subghz, bool alloc_for_tx_only) {
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdKeeloqDecrypt);
subghz_view_keeloq_decrypt_free(subghz->subghz_keeloq_decrypt);
// Custom car-emulate view
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdCarEmulate);
subghz_car_emulate_view_free(subghz->car_emulate_view);
// Read RAW
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdReadRAW);
subghz_read_raw_free(subghz->subghz_read_raw);
+3
View File
@@ -43,6 +43,8 @@
#include "helpers/subghz_txrx.h"
#include "helpers/subghz_keeloq_keys.h"
#include "views/subghz_car_emulate.h"
#define SUBGHZ_MAX_LEN_NAME 64
#define SUBGHZ_EXT_PRESET_NAME true
#define SUBGHZ_RAW_THRESHOLD_MIN (-90.0f)
@@ -76,6 +78,7 @@ struct SubGhz {
SubGhzReadRAW* subghz_read_raw;
SubGhzViewPsaDecrypt* subghz_psa_decrypt;
SubGhzViewKeeloqDecrypt* subghz_keeloq_decrypt;
SubGhzCarEmulateView* car_emulate_view;
bool raw_send_only;
bool save_datetime_set;
@@ -22,6 +22,7 @@
#define SUBGHZ_LAST_SETTING_FIELD_HOPPING_THRESHOLD "HoppingThreshold"
#define SUBGHZ_LAST_SETTING_FIELD_LED_AND_POWER_AMP "LedAndPowerAmp"
#define SUBGHZ_LAST_SETTING_FIELD_TX_POWER "TXPower"
#define SUBGHZ_LAST_SETTING_FIELD_CUSTOM_CAR_EMULATE "CustomCarEmulate"
SubGhzLastSettings* subghz_last_settings_alloc(void) {
SubGhzLastSettings* instance = malloc(sizeof(SubGhzLastSettings));
@@ -163,6 +164,14 @@ void subghz_last_settings_load(SubGhzLastSettings* instance, size_t preset_count
1)) {
flipper_format_rewind(fff_data_file);
}
if(!flipper_format_read_bool(
fff_data_file,
SUBGHZ_LAST_SETTING_FIELD_CUSTOM_CAR_EMULATE,
&instance->custom_car_emulate,
1)) {
instance->custom_car_emulate = false;
flipper_format_rewind(fff_data_file);
}
} while(0);
} else {
@@ -281,6 +290,13 @@ bool subghz_last_settings_save(SubGhzLastSettings* instance) {
file, SUBGHZ_LAST_SETTING_FIELD_LED_AND_POWER_AMP, &instance->leds_and_amp, 1)) {
break;
}
if(!flipper_format_write_bool(
file,
SUBGHZ_LAST_SETTING_FIELD_CUSTOM_CAR_EMULATE,
&instance->custom_car_emulate,
1)) {
break;
}
saved = true;
} while(0);
@@ -30,6 +30,7 @@ typedef struct {
float preset_hopping_threshold;
bool leds_and_amp;
uint8_t tx_power;
bool custom_car_emulate;
} SubGhzLastSettings;
SubGhzLastSettings* subghz_last_settings_alloc(void);
@@ -0,0 +1,264 @@
#include "subghz_car_emulate.h"
#include "../helpers/subghz_custom_event.h"
#include <gui/elements.h>
#include <input/input.h>
#include <furi.h>
#define TAG "SubGhzCarEmulateView"
/* ── Model ──────────────────────────────────────────────────────────────── */
typedef struct {
char protocol_name[32];
uint32_t serial;
uint32_t counter;
uint32_t original_counter;
uint32_t freq;
char preset[12];
bool is_transmitting;
uint8_t anim_frame;
char label_ok[12];
char label_up[12];
char label_down[12];
char label_left[12];
char label_right[12];
} SubGhzCarEmulateViewModel;
/* ── Handle ─────────────────────────────────────────────────────────────── */
struct SubGhzCarEmulateView {
View* view;
SubGhzCarEmulateViewCallback callback;
void* context;
};
/* ── Draw ───────────────────────────────────────────────────────────────── */
static void subghz_car_emulate_view_draw(Canvas* canvas, void* model_ptr) {
SubGhzCarEmulateViewModel* m = model_ptr;
m->anim_frame = (m->anim_frame + 1) % 8;
canvas_clear(canvas);
/* Header bar */
canvas_draw_box(canvas, 0, 0, 128, 11);
canvas_invert_color(canvas);
canvas_set_font(canvas, FontSecondary);
canvas_draw_str_aligned(canvas, 64, 2, AlignCenter, AlignTop, m->protocol_name);
canvas_invert_color(canvas);
/* Info row 1: serial + counter */
canvas_set_font(canvas, FontSecondary);
char buf[32];
if(m->serial <= 0xFFFFFFUL) {
snprintf(buf, sizeof(buf), "SN:%06lX", (unsigned long)(m->serial & 0xFFFFFFUL));
} else {
snprintf(buf, sizeof(buf), "SN:%08lX", (unsigned long)m->serial);
}
canvas_draw_str(canvas, 2, 20, buf);
snprintf(buf, sizeof(buf), "CNT:%04lX", (unsigned long)m->counter);
canvas_draw_str(canvas, 68, 20, buf);
if(m->counter > m->original_counter) {
snprintf(buf, sizeof(buf), "+%ld", (long)(m->counter - m->original_counter));
canvas_draw_str(canvas, 112, 20, buf);
}
/* Info row 2: frequency + preset */
snprintf(
buf,
sizeof(buf),
"F:%lu.%02lu",
(unsigned long)(m->freq / 1000000UL),
(unsigned long)((m->freq % 1000000UL) / 10000UL));
canvas_draw_str(canvas, 2, 30, buf);
canvas_draw_str(canvas, 95, 30, m->preset);
/* ── Button labels ── */
const uint8_t font_h = canvas_current_font_height(canvas);
/* Centre → UNLOCK (OK button) */
{
const char* lbl = m->label_ok;
uint8_t w = (uint8_t)(canvas_string_width(canvas, lbl) + 8U);
canvas_draw_rbox(canvas, 64 - w / 2, 45 - font_h / 2, w, font_h, 3);
canvas_invert_color(canvas);
canvas_draw_str_aligned(canvas, 64, 49, AlignCenter, AlignBottom, lbl);
canvas_invert_color(canvas);
}
/* Up → LOCK */
{
const char* lbl = m->label_up;
uint8_t w = (uint8_t)(canvas_string_width(canvas, lbl) + 8U);
canvas_draw_rbox(canvas, 64 - w / 2, 33 - font_h / 2, w, font_h, 3);
canvas_invert_color(canvas);
canvas_draw_str_aligned(canvas, 64, 37, AlignCenter, AlignBottom, lbl);
canvas_invert_color(canvas);
}
/* Left → PANIC */
{
const char* lbl = m->label_left;
uint8_t w = (uint8_t)(canvas_string_width(canvas, lbl) + 8U);
canvas_draw_rbox(canvas, 0, 46 - font_h / 2, w, font_h, 3);
canvas_invert_color(canvas);
canvas_draw_str_aligned(canvas, w / 2, 50, AlignCenter, AlignBottom, lbl);
canvas_invert_color(canvas);
}
/* Right → generic extra */
{
const char* lbl = m->label_right;
uint8_t w = (uint8_t)(canvas_string_width(canvas, lbl) + 8U);
canvas_draw_rbox(canvas, 127 - w, 46 - font_h / 2, w, font_h, 3);
canvas_invert_color(canvas);
canvas_draw_str_aligned(canvas, 127 - w / 2, 50, AlignCenter, AlignBottom, lbl);
canvas_invert_color(canvas);
}
/* Down → BOOT */
{
const char* lbl = m->label_down;
uint8_t w = (uint8_t)(canvas_string_width(canvas, lbl) + 8U);
canvas_draw_rbox(canvas, 64 - w / 2, 57 - font_h / 2, w, font_h, 3);
canvas_invert_color(canvas);
canvas_draw_str_aligned(canvas, 64, 61, AlignCenter, AlignBottom, lbl);
canvas_invert_color(canvas);
}
/* TX overlay */
if(m->is_transmitting) {
canvas_draw_rbox(canvas, 24, 18, 80, 18, 3);
canvas_invert_color(canvas);
int wave = m->anim_frame % 3;
canvas_draw_str(canvas, 28 + wave * 2, 25, ")))");
canvas_set_font(canvas, FontPrimary);
canvas_draw_str_aligned(canvas, 64, 24, AlignCenter, AlignCenter, "TX");
canvas_invert_color(canvas);
}
}
/* ── Input ──────────────────────────────────────────────────────────────── */
static bool subghz_car_emulate_view_input(InputEvent* event, void* context) {
SubGhzCarEmulateView* instance = context;
furi_assert(instance);
if(event->type == InputTypePress) {
if(event->key == InputKeyBack) {
if(instance->callback) {
instance->callback(SubGhzCustomEventCarEmulateExit, instance->context);
}
return true;
}
/* Any directional / OK key → start TX */
if(instance->callback) {
/* Pack the raw InputKey into the upper bits of the event so the
scene can read which button was pressed.
Lower 16 bits = SubGhzCustomEventCarEmulateTransmit marker,
upper 16 bits = InputKey value. */
uint32_t ev = ((uint32_t)event->key << 16) |
(uint32_t)SubGhzCustomEventCarEmulateTransmit;
instance->callback(ev, instance->context);
}
return true;
} else if(event->type == InputTypeRelease) {
if(event->key != InputKeyBack) {
if(instance->callback) {
instance->callback(SubGhzCustomEventCarEmulateStop, instance->context);
}
return true;
}
}
return false;
}
/* ── Alloc / Free ───────────────────────────────────────────────────────── */
SubGhzCarEmulateView* subghz_car_emulate_view_alloc(void) {
SubGhzCarEmulateView* instance = malloc(sizeof(SubGhzCarEmulateView));
furi_check(instance);
instance->view = view_alloc();
instance->callback = NULL;
instance->context = NULL;
view_set_context(instance->view, instance);
view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(SubGhzCarEmulateViewModel));
view_set_draw_callback(instance->view, subghz_car_emulate_view_draw);
view_set_input_callback(instance->view, subghz_car_emulate_view_input);
return instance;
}
void subghz_car_emulate_view_free(SubGhzCarEmulateView* instance) {
furi_check(instance);
view_free(instance->view);
free(instance);
}
View* subghz_car_emulate_view_get_view(SubGhzCarEmulateView* instance) {
furi_check(instance);
return instance->view;
}
void subghz_car_emulate_view_set_callback(
SubGhzCarEmulateView* instance,
SubGhzCarEmulateViewCallback callback,
void* context) {
furi_check(instance);
instance->callback = callback;
instance->context = context;
}
void subghz_car_emulate_view_set_data(
SubGhzCarEmulateView* instance,
const char* protocol_name,
uint32_t serial,
uint32_t counter,
uint32_t original_counter,
uint32_t freq,
const char* preset,
bool is_transmitting) {
furi_check(instance);
with_view_model(
instance->view,
SubGhzCarEmulateViewModel * m,
{
strncpy(m->protocol_name, protocol_name, sizeof(m->protocol_name) - 1);
m->protocol_name[sizeof(m->protocol_name) - 1] = '\0';
m->serial = serial;
m->counter = counter;
m->original_counter = original_counter;
m->freq = freq;
strncpy(m->preset, preset, sizeof(m->preset) - 1);
m->preset[sizeof(m->preset) - 1] = '\0';
m->is_transmitting = is_transmitting;
},
true);
}
void subghz_car_emulate_view_set_labels(
SubGhzCarEmulateView* instance,
const char* ok,
const char* up,
const char* down,
const char* left,
const char* right) {
furi_check(instance);
with_view_model(
instance->view,
SubGhzCarEmulateViewModel * m,
{
strncpy(m->label_ok, ok ? ok : "", sizeof(m->label_ok) - 1);
strncpy(m->label_up, up ? up : "", sizeof(m->label_up) - 1);
strncpy(m->label_down, down ? down : "", sizeof(m->label_down) - 1);
strncpy(m->label_left, left ? left : "", sizeof(m->label_left) - 1);
strncpy(m->label_right, right ? right : "", sizeof(m->label_right) - 1);
},
true);
}
@@ -0,0 +1,45 @@
#pragma once
#include <gui/view.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct SubGhzCarEmulateView SubGhzCarEmulateView;
typedef void (*SubGhzCarEmulateViewCallback)(uint32_t event, void* context);
SubGhzCarEmulateView* subghz_car_emulate_view_alloc(void);
void subghz_car_emulate_view_free(SubGhzCarEmulateView* instance);
View* subghz_car_emulate_view_get_view(SubGhzCarEmulateView* instance);
void subghz_car_emulate_view_set_callback(
SubGhzCarEmulateView* instance,
SubGhzCarEmulateViewCallback callback,
void* context);
/** Update the fields shown on the view.
* All strings are copied internally so the caller can free them after the call.
*/
void subghz_car_emulate_view_set_labels(
SubGhzCarEmulateView* instance,
const char* ok,
const char* up,
const char* down,
const char* left,
const char* right);
void subghz_car_emulate_view_set_data(
SubGhzCarEmulateView* instance,
const char* protocol_name,
uint32_t serial,
uint32_t counter,
uint32_t original_counter,
uint32_t freq,
const char* preset,
bool is_transmitting);
#ifdef __cplusplus
}
#endif
+56
View File
@@ -16,3 +16,59 @@ void subghz_custom_btn_set_max(uint8_t b);
void subghz_custom_btn_set_prog_mode(ProgMode prog_mode);
ProgMode subghz_custom_btn_get_prog_mode(void);
/**
* Helper macro: declare a static button-map table and the two
* conversion functions that every protocol with custom buttons needs.
*
* Usage in your protocol .c file:
*
* SUBGHZ_CUSTOM_BTN_DEFINE_MAP(my_proto,
* {SUBGHZ_CUSTOM_BTN_OK, 0x01}, // OK → Lock
* {SUBGHZ_CUSTOM_BTN_UP, 0x01}, // Up → Lock
* {SUBGHZ_CUSTOM_BTN_DOWN, 0x02}, // Down → Unlock
* {SUBGHZ_CUSTOM_BTN_LEFT, 0x04}, // Left → Boot
* {SUBGHZ_CUSTOM_BTN_RIGHT, 0x08}, // Right → Panic
* )
*
* This generates:
* static uint8_t my_proto_custom_btn_to_code(uint8_t custom_btn);
* static uint8_t my_proto_code_to_custom_btn(uint8_t code);
* static const uint8_t my_proto_custom_btn_max;
*/
typedef struct {
uint8_t custom_btn_id; /* SUBGHZ_CUSTOM_BTN_OK / UP / DOWN / LEFT / RIGHT */
uint8_t protocol_code; /* the actual byte the protocol puts in the frame */
} SubGhzCustomBtnEntry;
#define SUBGHZ_CUSTOM_BTN_DEFINE_MAP(prefix_, ...) \
static const SubGhzCustomBtnEntry prefix_##_btn_map[] = {__VA_ARGS__}; \
static const uint8_t prefix_##_custom_btn_max = \
(sizeof(prefix_##_btn_map) / sizeof(SubGhzCustomBtnEntry)) - 1U; \
\
static uint8_t prefix_##_custom_btn_to_code(uint8_t custom_btn) { \
for(size_t i = 0; i < sizeof(prefix_##_btn_map) / \
sizeof(SubGhzCustomBtnEntry); i++) { \
if(prefix_##_btn_map[i].custom_btn_id == custom_btn) \
return prefix_##_btn_map[i].protocol_code; \
} \
/* fallback: return whatever OK maps to */ \
return prefix_##_btn_map[0].protocol_code; \
} \
\
static uint8_t prefix_##_code_to_custom_btn(uint8_t code) { \
for(size_t i = 0; i < sizeof(prefix_##_btn_map) / \
sizeof(SubGhzCustomBtnEntry); i++) { \
if(prefix_##_btn_map[i].protocol_code == code) \
return prefix_##_btn_map[i].custom_btn_id; \
} \
return SUBGHZ_CUSTOM_BTN_OK; \
} \
\
static void prefix_##_custom_btn_init(uint8_t current_code) { \
uint8_t original = prefix_##_code_to_custom_btn(current_code); \
if(subghz_custom_btn_get_original() == 0) \
subghz_custom_btn_set_original(original); \
subghz_custom_btn_set_max(prefix_##_custom_btn_max); \
}
+85 -9
View File
@@ -3,6 +3,7 @@
#include <string.h>
#include <lib/toolbox/manchester_decoder.h>
#include <lib/toolbox/manchester_encoder.h>
#include <lib/subghz/blocks/custom_btn_i.h>
#define FORD_V2_TE_SHORT 200U
#define FORD_V2_TE_LONG 400U
@@ -34,6 +35,16 @@
#define FORD_V2_PREAMBLE_COUNT_MAX 0xFFFFU
#define FORD_V2_ENCODER_DEFAULT_REPEAT 10U
SUBGHZ_CUSTOM_BTN_DEFINE_MAP(
ford_v2,
{SUBGHZ_CUSTOM_BTN_OK, 0x11}, /* OK → Unlock */
{SUBGHZ_CUSTOM_BTN_UP, 0x10}, /* Up → Lock */
{SUBGHZ_CUSTOM_BTN_DOWN, 0x13}, /* Down → Trunk */
{SUBGHZ_CUSTOM_BTN_LEFT, 0x14}, /* Left → Panic */
{SUBGHZ_CUSTOM_BTN_RIGHT, 0x15}, /* Right → RemoteStart */
)
static const uint16_t ford_v2_sync_shift16_inv =
(uint16_t)(~(((uint16_t)FORD_V2_SYNC_0 << 8) | (uint16_t)FORD_V2_SYNC_1));
@@ -208,6 +219,10 @@ static bool ford_v2_decoder_commit_frame(SubGhzProtocolDecoderFordV2* instance)
return false;
}
/* Register this protocol's button map with the custom_btn system so the
* standard transmitter view can show UP/DOWN/LEFT/RIGHT cycling. */
ford_v2_custom_btn_init(instance->generic.btn);
if(instance->base.callback) {
instance->base.callback(&instance->base, instance->base.context);
}
@@ -461,26 +476,44 @@ static SubGhzProtocolStatus ford_v2_encoder_deserialize_read_header(
return SubGhzProtocolStatusOk;
}
static SubGhzProtocolStatus ford_v2_encoder_deserialize_validate_and_pack(SubGhzProtocolEncoderFordV2* instance) {
static SubGhzProtocolStatus ford_v2_encoder_deserialize_validate_and_pack(
SubGhzProtocolEncoderFordV2* instance) {
ford_v2_encoder_rebuild_raw_from_payload(instance);
if(!ford_v2_button_is_valid(instance->raw_bytes[6])) {
return SubGhzProtocolStatusErrorParserOthers;
}
uint16_t cnt = (uint16_t)(
(((uint16_t)(instance->raw_bytes[7] & 0x7FU)) << 9) |
(((uint16_t)instance->raw_bytes[8]) << 1) |
((uint16_t)(instance->raw_bytes[9] >> 7)));
cnt = (cnt + 1U) & 0x7FFFU;
// raw_bytes[7] bits [6:0] = cnt[14:8], bit[7] = parity(btn)
instance->raw_bytes[7] = (instance->raw_bytes[7] & 0x80U) |
(uint8_t)((cnt >> 9) & 0x7FU);
instance->raw_bytes[8] = (uint8_t)((cnt >> 1) & 0xFFU);
// raw_bytes[9] bit[7] = cnt[0], bits[6:0] = tail
instance->raw_bytes[9] = (instance->raw_bytes[9] & 0x7FU) |
(uint8_t)((cnt & 1U) << 7);
ford_v2_encoder_refresh_data_from_raw(instance);
instance->generic.btn = instance->raw_bytes[6];
instance->generic.serial = ((uint32_t)instance->raw_bytes[2] << 24) |
((uint32_t)instance->raw_bytes[3] << 16) |
((uint32_t)instance->raw_bytes[4] << 8) |
(uint32_t)instance->raw_bytes[5];
instance->generic.cnt = (uint16_t)((((uint16_t)(instance->raw_bytes[7] & 0x7FU)) << 9) |
(((uint16_t)instance->raw_bytes[8]) << 1) |
((uint16_t)(instance->raw_bytes[9] >> 7)));
instance->generic.serial =
((uint32_t)instance->raw_bytes[2] << 24) |
((uint32_t)instance->raw_bytes[3] << 16) |
((uint32_t)instance->raw_bytes[4] << 8) |
(uint32_t)instance->raw_bytes[5];
instance->generic.cnt = cnt;
return SubGhzProtocolStatusOk;
}
static void ford_v2_encoder_deserialize_apply_repeat(SubGhzProtocolEncoderFordV2* instance, FlipperFormat* flipper_format) {
flipper_format_rewind(flipper_format);
uint32_t repeat = FORD_V2_ENCODER_DEFAULT_REPEAT;
@@ -523,13 +556,36 @@ SubGhzProtocolStatus
FuriString* temp_str = furi_string_alloc();
furi_check(temp_str);
SubGhzProtocolStatus ret = ford_v2_encoder_deserialize_read_header(instance, flipper_format, temp_str);
SubGhzProtocolStatus ret =
ford_v2_encoder_deserialize_read_header(instance, flipper_format, temp_str);
if(ret == SubGhzProtocolStatusOk) {
ret = ford_v2_encoder_deserialize_validate_and_pack(instance);
}
if(ret == SubGhzProtocolStatusOk) {
ford_v2_custom_btn_init(instance->raw_bytes[6]);
uint8_t btn_sel = subghz_custom_btn_get();
if(btn_sel != SUBGHZ_CUSTOM_BTN_OK) {
uint8_t new_code = ford_v2_custom_btn_to_code(btn_sel);
if(ford_v2_button_is_valid(new_code)) {
instance->raw_bytes[6] = new_code;
const uint8_t k7_msb =
(uint8_t)(ford_v2_uint8_parity(new_code) << 7);
instance->raw_bytes[7] =
(instance->raw_bytes[7] & 0x7FU) | k7_msb;
ford_v2_encoder_refresh_data_from_raw(instance);
instance->generic.btn = new_code;
}
}
instance->extra_data = 0;
for(uint8_t i = 0; i < FORD_V2_TAIL_RAW_BYTE_COUNT; i++) {
instance->extra_data =
(instance->extra_data << 8) | (uint64_t)instance->raw_bytes[8U + i];
}
ford_v2_encoder_deserialize_apply_repeat(instance, flipper_format);
ford_v2_encoder_build_upload(instance);
instance->encoder.is_running = true;
@@ -539,6 +595,7 @@ SubGhzProtocolStatus
return ret;
}
void subghz_protocol_encoder_ford_v2_stop(void* context) {
furi_check(context);
SubGhzProtocolEncoderFordV2* instance = context;
@@ -740,6 +797,25 @@ SubGhzProtocolStatus subghz_protocol_decoder_ford_v2_deserialize(
return SubGhzProtocolStatusErrorParserOthers;
}
/* Keep custom_btn in sync when loading from file. */
ford_v2_custom_btn_init(instance->generic.btn);
uint8_t btn_sel = subghz_custom_btn_get();
if(btn_sel != SUBGHZ_CUSTOM_BTN_OK) {
uint8_t new_code = ford_v2_custom_btn_to_code(btn_sel);
if(ford_v2_button_is_valid(new_code)) {
instance->generic.btn = new_code;
instance->raw_bytes[6] = new_code;
instance->raw_bytes[7] = (instance->raw_bytes[7] & 0x7FU) |
(uint8_t)(ford_v2_uint8_parity(new_code) << 7);
instance->generic.data = 0;
for(uint8_t i = 0; i < FORD_V2_KEY_BYTE_COUNT; i++) {
instance->generic.data =
(instance->generic.data << 8) | (uint64_t)instance->raw_bytes[i];
}
}
}
return ret;
}
+984
View File
@@ -0,0 +1,984 @@
#include "land_rover_v0.h"
#include <string.h>
#define TAG "LandRoverV0"
static const SubGhzBlockConst subghz_protocol_land_rover_v0_const = {
.te_short = 250,
.te_long = 500,
.te_delta = 100,
.min_count_bit_for_found = 81,
};
#define LAND_ROVER_V0_PREAMBLE_PAIRS 319U
#define LAND_ROVER_V0_MIN_PREAMBLE_PAIRS 64U
#define LAND_ROVER_V0_SYNC_US 750U
#define LAND_ROVER_V0_SYNC_DELTA_US 120U
#define LAND_ROVER_V0_UPLOAD_CAPACITY 1024U
#define LAND_ROVER_V0_GAP_US 50000U
#define LAND_ROVER_V0_BTN_UNKNOWN 0x00U
#define LAND_ROVER_V0_BTN_LOCK 0x02U
#define LAND_ROVER_V0_BTN_UNLOCK 0x04U
#define LAND_ROVER_V0_SIG_UNLOCK 0xA285E3UL
#define LAND_ROVER_V0_SIG_LOCK 0xC20363UL
/* Extra FlipperFormat field names specific to this protocol */
#define LAND_ROVER_V0_FF_BTNSIG "BtnSig"
#define LAND_ROVER_V0_FF_CHECK "Check"
#define LAND_ROVER_V0_FF_TAIL "Tail"
#define LAND_ROVER_V0_FF_EXTRA_BIT "ExtraBit"
/* FlipperFormat field name aliases (replacing the external pp library's FF_* macros) */
#define LR_FF_KEY "Key"
#define LR_FF_SERIAL "Serial"
#define LR_FF_BTN "Btn"
#define LR_FF_CNT "Cnt"
/* ── Decoder struct ──────────────────────────────────────────────────────── */
typedef struct SubGhzProtocolDecoderLandRoverV0 {
SubGhzProtocolDecoderBase base;
SubGhzBlockDecoder decoder;
SubGhzBlockGeneric generic;
uint16_t preamble_count;
uint8_t raw[10];
uint8_t bit_count;
bool extra_bit;
bool previous_bit;
bool boundary_pad_skipped;
bool pending_short;
uint64_t key;
uint16_t tail;
uint32_t command_signature;
uint32_t serial;
uint32_t count;
uint8_t button;
uint8_t check;
bool check_ok;
bool tail_ok;
} SubGhzProtocolDecoderLandRoverV0;
/* ── Encoder struct ──────────────────────────────────────────────────────── */
typedef struct SubGhzProtocolEncoderLandRoverV0 {
SubGhzProtocolEncoderBase base;
SubGhzProtocolBlockEncoder encoder;
SubGhzBlockGeneric generic;
uint64_t key;
uint16_t tail;
uint32_t command_signature;
uint32_t serial;
uint32_t count;
uint8_t button;
uint8_t check;
} SubGhzProtocolEncoderLandRoverV0;
/* ── Decoder state machine steps ─────────────────────────────────────────── */
typedef enum {
LandRoverV0DecoderStepReset = 0,
LandRoverV0DecoderStepPreambleLow,
LandRoverV0DecoderStepPreambleHigh,
LandRoverV0DecoderStepSyncLow,
LandRoverV0DecoderStepData,
} LandRoverV0DecoderStep;
/* ═══════════════════════════════════════════════════════════════════════════
* Internal helpers replacing pp_* functions from the external app library
* ═════════════════════════════════════════════════════════════════════════*/
/** Write a uint64_t into 8 bytes in big-endian order. */
static inline void lr_u64_to_bytes_be(uint64_t val, uint8_t out[8]) {
for(int i = 7; i >= 0; i--) {
out[i] = (uint8_t)(val & 0xFFU);
val >>= 8;
}
}
/** Read 8 big-endian bytes and return a uint64_t. */
static inline uint64_t lr_bytes_to_u64_be(const uint8_t in[8]) {
uint64_t val = 0;
for(int i = 0; i < 8; i++) {
val = (val << 8) | in[i];
}
return val;
}
/** Returns true when duration matches te_short within te_delta. */
static inline bool lr_is_short(uint32_t duration) {
return DURATION_DIFF(duration, subghz_protocol_land_rover_v0_const.te_short) <
subghz_protocol_land_rover_v0_const.te_delta;
}
/** Returns true when duration matches te_long within te_delta. */
static inline bool lr_is_long(uint32_t duration) {
return DURATION_DIFF(duration, subghz_protocol_land_rover_v0_const.te_long) <
subghz_protocol_land_rover_v0_const.te_delta;
}
/** Insert-or-update a single uint32 field in a FlipperFormat file. */
static void lr_ff_write_u32(FlipperFormat* ff, const char* key, uint32_t val) {
flipper_format_insert_or_update_uint32(ff, key, &val, 1);
}
/** Read a single uint32 field from a FlipperFormat file. */
static bool lr_ff_read_u32(FlipperFormat* ff, const char* key, uint32_t* out) {
return flipper_format_read_uint32(ff, key, out, 1);
}
/**
* Verify that the "Protocol" field in the FlipperFormat file matches the
* expected protocol name. Returns SubGhzProtocolStatusOk on match.
*/
static SubGhzProtocolStatus lr_verify_protocol_name(
FlipperFormat* ff,
const char* expected_name) {
FuriString* name = furi_string_alloc();
bool ok = false;
if(flipper_format_read_string(ff, "Protocol", name)) {
ok = furi_string_equal_str(name, expected_name);
}
furi_string_free(name);
return ok ? SubGhzProtocolStatusOk : SubGhzProtocolStatusErrorProtocolNotFound;
}
/**
* Read the "Repeat" field from a FlipperFormat file.
* Falls back to default_val when the field is absent.
*/
static uint16_t lr_encoder_read_repeat(FlipperFormat* ff, uint16_t default_val) {
uint32_t repeat = default_val;
flipper_format_rewind(ff);
if(!flipper_format_read_uint32(ff, "Repeat", &repeat, 1)) {
repeat = default_val;
}
return (uint16_t)repeat;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Forward declarations for internal (static) helpers
* ═════════════════════════════════════════════════════════════════════════*/
static uint8_t land_rover_v0_button_from_signature(uint32_t signature);
static const char* land_rover_v0_button_name(uint8_t button);
static uint8_t land_rover_v0_calculate_check(uint32_t count);
static bool land_rover_v0_calculate_tail_msb(uint32_t count);
static uint16_t land_rover_v0_calculate_tail(uint32_t count);
static void land_rover_v0_parse_key_fields(
uint64_t key,
uint32_t* signature,
uint32_t* serial,
uint32_t* count,
uint8_t* button,
uint8_t* check);
static bool land_rover_v0_validate_frame(
uint64_t key,
uint16_t tail,
bool extra_bit,
bool* check_ok,
bool* tail_ok);
static bool land_rover_v0_add_decoded_bit(
SubGhzProtocolDecoderLandRoverV0* instance,
bool bit);
static bool land_rover_v0_process_transition(
SubGhzProtocolDecoderLandRoverV0* instance,
bool level,
uint32_t duration);
static bool land_rover_v0_finish_frame(SubGhzProtocolDecoderLandRoverV0* instance);
static bool land_rover_v0_encoder_add_level(
SubGhzProtocolEncoderLandRoverV0* instance,
size_t* index,
bool level,
uint32_t duration);
static bool land_rover_v0_encoder_add_bit(
SubGhzProtocolEncoderLandRoverV0* instance,
size_t* index,
bool* previous_bit,
bool bit);
static bool land_rover_v0_build_upload(SubGhzProtocolEncoderLandRoverV0* instance);
/* ═══════════════════════════════════════════════════════════════════════════
* Protocol descriptor tables
* ═════════════════════════════════════════════════════════════════════════*/
const SubGhzProtocolDecoder subghz_protocol_land_rover_v0_decoder = {
.alloc = subghz_protocol_decoder_land_rover_v0_alloc,
.free = subghz_protocol_decoder_land_rover_v0_free,
.feed = subghz_protocol_decoder_land_rover_v0_feed,
.reset = subghz_protocol_decoder_land_rover_v0_reset,
.get_hash_data = subghz_protocol_decoder_land_rover_v0_get_hash_data,
.serialize = subghz_protocol_decoder_land_rover_v0_serialize,
.deserialize = subghz_protocol_decoder_land_rover_v0_deserialize,
.get_string = subghz_protocol_decoder_land_rover_v0_get_string,
};
const SubGhzProtocolEncoder subghz_protocol_land_rover_v0_encoder = {
.alloc = subghz_protocol_encoder_land_rover_v0_alloc,
.free = subghz_protocol_encoder_land_rover_v0_free,
.deserialize = subghz_protocol_encoder_land_rover_v0_deserialize,
.stop = subghz_protocol_encoder_land_rover_v0_stop,
.yield = subghz_protocol_encoder_land_rover_v0_yield,
};
const SubGhzProtocol subghz_protocol_land_rover_v0 = {
.name = LAND_ROVER_PROTOCOL_V0_NAME,
.type = SubGhzProtocolTypeDynamic,
.flag = SubGhzProtocolFlag_433 | SubGhzProtocolFlag_FM |
SubGhzProtocolFlag_Decodable | SubGhzProtocolFlag_Load |
SubGhzProtocolFlag_Save | SubGhzProtocolFlag_Send,
.decoder = &subghz_protocol_land_rover_v0_decoder,
.encoder = &subghz_protocol_land_rover_v0_encoder,
};
/* ═══════════════════════════════════════════════════════════════════════════
* Protocol logic helpers
* ═════════════════════════════════════════════════════════════════════════*/
static bool land_rover_v0_is_sync(uint32_t duration) {
return DURATION_DIFF(duration, LAND_ROVER_V0_SYNC_US) < LAND_ROVER_V0_SYNC_DELTA_US;
}
static uint8_t land_rover_v0_button_from_signature(uint32_t signature) {
if(signature == LAND_ROVER_V0_SIG_UNLOCK) return LAND_ROVER_V0_BTN_UNLOCK;
if(signature == LAND_ROVER_V0_SIG_LOCK) return LAND_ROVER_V0_BTN_LOCK;
return LAND_ROVER_V0_BTN_UNKNOWN;
}
static const char* land_rover_v0_button_name(uint8_t button) {
switch(button) {
case LAND_ROVER_V0_BTN_LOCK: return "Lock";
case LAND_ROVER_V0_BTN_UNLOCK: return "Unlock";
default: return "Unknown";
}
}
static uint8_t land_rover_v0_calculate_check(uint32_t count) {
const uint8_t c0 =
((count >> 1) ^ (count >> 2) ^ (count >> 3) ^ (count >> 4) ^ (count >> 6)) & 1U;
const uint8_t c1 =
((count >> 0) ^ (count >> 2) ^ (count >> 3) ^ (count >> 4) ^ (count >> 5) ^
(count >> 6) ^ 1U) & 1U;
const uint8_t c2 =
((count >> 1) ^ (count >> 3) ^ (count >> 4) ^ (count >> 5) ^ (count >> 6)) & 1U;
return (uint8_t)(c0 | (c1 << 1) | (c2 << 2));
}
static bool land_rover_v0_calculate_tail_msb(uint32_t count) {
const uint8_t tail =
((count >> 0) ^ (count >> 2) ^ (count >> 4) ^ (count >> 5)) & 1U;
return tail != 0U;
}
static uint16_t land_rover_v0_calculate_tail(uint32_t count) {
return land_rover_v0_calculate_tail_msb(count) ? 0xFFFFU : 0x7FFFU;
}
static void land_rover_v0_parse_key_fields(
uint64_t key,
uint32_t* signature,
uint32_t* serial,
uint32_t* count,
uint8_t* button,
uint8_t* check) {
uint8_t key_bytes[8];
lr_u64_to_bytes_be(key, key_bytes);
const uint32_t sig =
((uint32_t)key_bytes[0] << 16) | ((uint32_t)key_bytes[1] << 8) | key_bytes[2];
const uint32_t sn =
((uint32_t)key_bytes[3] << 16) | ((uint32_t)key_bytes[4] << 8) | key_bytes[5];
const uint32_t cnt =
((uint32_t)key_bytes[6] << 1) | ((key_bytes[7] >> 7) & 1U);
if(signature) *signature = sig;
if(serial) *serial = sn;
if(count) *count = cnt;
if(button) *button = land_rover_v0_button_from_signature(sig);
if(check) *check = key_bytes[7] & 0x07U;
}
static bool land_rover_v0_validate_frame(
uint64_t key,
uint16_t tail,
bool extra_bit,
bool* check_ok,
bool* tail_ok) {
uint8_t key_bytes[8];
lr_u64_to_bytes_be(key, key_bytes);
const uint32_t count = ((uint32_t)key_bytes[6] << 1) | ((key_bytes[7] >> 7) & 1U);
const uint8_t expected_check = land_rover_v0_calculate_check(count);
const uint16_t expected_tail = land_rover_v0_calculate_tail(count);
const bool local_check_ok =
((key_bytes[7] & 0x78U) == 0U) && ((key_bytes[7] & 0x07U) == expected_check);
const bool local_tail_ok = (tail == expected_tail) && extra_bit;
if(check_ok) *check_ok = local_check_ok;
if(tail_ok) *tail_ok = local_tail_ok;
return local_check_ok && local_tail_ok;
}
static bool land_rover_v0_add_decoded_bit(
SubGhzProtocolDecoderLandRoverV0* instance,
bool bit) {
if(instance->bit_count < 80U) {
const uint8_t byte_index = instance->bit_count / 8U;
const uint8_t bit_index = 7U - (instance->bit_count % 8U);
if(bit) instance->raw[byte_index] |= (uint8_t)(1U << bit_index);
} else if(instance->bit_count == 80U) {
instance->extra_bit = bit;
} else {
return false;
}
instance->bit_count++;
return true;
}
static bool land_rover_v0_finish_frame(SubGhzProtocolDecoderLandRoverV0* instance) {
const uint64_t key = lr_bytes_to_u64_be(instance->raw);
const uint16_t tail =
((uint16_t)instance->raw[8] << 8) | instance->raw[9];
if(!land_rover_v0_validate_frame(
key, tail, instance->extra_bit,
&instance->check_ok, &instance->tail_ok)) {
return false;
}
instance->key = key;
instance->tail = tail;
land_rover_v0_parse_key_fields(
key,
&instance->command_signature,
&instance->serial,
&instance->count,
&instance->button,
&instance->check);
instance->generic.data = instance->key;
instance->generic.data_count_bit =
subghz_protocol_land_rover_v0_const.min_count_bit_for_found;
instance->generic.serial = instance->serial;
instance->generic.btn = instance->button;
instance->generic.cnt = instance->count;
return true;
}
static bool land_rover_v0_process_transition(
SubGhzProtocolDecoderLandRoverV0* instance,
bool level,
uint32_t duration) {
if(!instance->boundary_pad_skipped) {
if(level && lr_is_short(duration)) {
instance->boundary_pad_skipped = true;
return true;
}
instance->boundary_pad_skipped = true;
}
if(instance->pending_short) {
if(!instance->previous_bit && !level && lr_is_short(duration)) {
instance->pending_short = false;
return land_rover_v0_add_decoded_bit(instance, false);
} else if(instance->previous_bit && level && lr_is_short(duration)) {
instance->pending_short = false;
return land_rover_v0_add_decoded_bit(instance, true);
}
return false;
}
if(!instance->previous_bit) {
if(level && lr_is_long(duration)) {
instance->previous_bit = true;
return land_rover_v0_add_decoded_bit(instance, true);
} else if(level && lr_is_short(duration)) {
instance->pending_short = true;
return true;
}
return false;
}
if(!level && lr_is_long(duration)) {
instance->previous_bit = false;
return land_rover_v0_add_decoded_bit(instance, false);
} else if(!level && lr_is_short(duration)) {
instance->pending_short = true;
return true;
}
return false;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Encoder waveform helpers
* ═════════════════════════════════════════════════════════════════════════*/
static bool land_rover_v0_encoder_add_level(
SubGhzProtocolEncoderLandRoverV0* instance,
size_t* index,
bool level,
uint32_t duration) {
if(*index >= LAND_ROVER_V0_UPLOAD_CAPACITY) return false;
instance->encoder.upload[(*index)++] = level_duration_make(level, duration);
return true;
}
static bool land_rover_v0_encoder_add_bit(
SubGhzProtocolEncoderLandRoverV0* instance,
size_t* index,
bool* previous_bit,
bool bit) {
const uint32_t te_short = subghz_protocol_land_rover_v0_const.te_short;
const uint32_t te_long = subghz_protocol_land_rover_v0_const.te_long;
if(!*previous_bit && !bit) {
/* 0→0: two short pulses low-high */
if(!land_rover_v0_encoder_add_level(instance, index, true, te_short) ||
!land_rover_v0_encoder_add_level(instance, index, false, te_short))
return false;
} else if(!*previous_bit && bit) {
/* 0→1: one long high */
if(!land_rover_v0_encoder_add_level(instance, index, true, te_long))
return false;
} else if(*previous_bit && !bit) {
/* 1→0: one long low */
if(!land_rover_v0_encoder_add_level(instance, index, false, te_long))
return false;
} else {
/* 1→1: two short pulses high-low */
if(!land_rover_v0_encoder_add_level(instance, index, false, te_short) ||
!land_rover_v0_encoder_add_level(instance, index, true, te_short))
return false;
}
*previous_bit = bit;
return true;
}
static bool land_rover_v0_build_upload(SubGhzProtocolEncoderLandRoverV0* instance) {
furi_check(instance);
size_t index = 0;
const uint32_t te_short = subghz_protocol_land_rover_v0_const.te_short;
uint8_t key_bytes[8];
lr_u64_to_bytes_be(instance->key, key_bytes);
/* Preamble: alternating short high/low pairs */
for(uint16_t i = 0; i < LAND_ROVER_V0_PREAMBLE_PAIRS; i++) {
if(!land_rover_v0_encoder_add_level(instance, &index, true, te_short) ||
!land_rover_v0_encoder_add_level(instance, &index, false, te_short))
return false;
}
/* Sync: long high, long low, short high (boundary pulse) */
if(!land_rover_v0_encoder_add_level(instance, &index, true, LAND_ROVER_V0_SYNC_US) ||
!land_rover_v0_encoder_add_level(instance, &index, false, LAND_ROVER_V0_SYNC_US) ||
!land_rover_v0_encoder_add_level(instance, &index, true, te_short))
return false;
/* First encoded bit: always 0, previous state is 1 (the boundary pulse) */
bool previous_bit = true;
if(!land_rover_v0_encoder_add_bit(instance, &index, &previous_bit, false))
return false;
/* Data bits 2..63 from the 64-bit key */
for(uint8_t bit_index = 2; bit_index < 64; bit_index++) {
const uint8_t byte_index = bit_index / 8U;
const uint8_t bit_in_byte = 7U - (bit_index % 8U);
const bool bit = (key_bytes[byte_index] >> bit_in_byte) & 1U;
if(!land_rover_v0_encoder_add_bit(instance, &index, &previous_bit, bit))
return false;
}
/* 16-bit tail */
instance->tail = land_rover_v0_calculate_tail(instance->count);
for(uint8_t bit_index = 0; bit_index < 16; bit_index++) {
const bool bit = (instance->tail >> (15U - bit_index)) & 1U;
if(!land_rover_v0_encoder_add_bit(instance, &index, &previous_bit, bit))
return false;
}
/* Extra bit (always 1) */
if(!land_rover_v0_encoder_add_bit(instance, &index, &previous_bit, true))
return false;
/* Inter-frame gap */
if(!land_rover_v0_encoder_add_level(instance, &index, false, LAND_ROVER_V0_GAP_US))
return false;
instance->encoder.front = 0;
instance->encoder.size_upload = index;
return true;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Decoder public API
* ═════════════════════════════════════════════════════════════════════════*/
void* subghz_protocol_decoder_land_rover_v0_alloc(SubGhzEnvironment* environment) {
UNUSED(environment);
SubGhzProtocolDecoderLandRoverV0* instance =
calloc(1, sizeof(SubGhzProtocolDecoderLandRoverV0));
furi_check(instance);
instance->base.protocol = &subghz_protocol_land_rover_v0;
instance->generic.protocol_name = instance->base.protocol->name;
return instance;
}
void subghz_protocol_decoder_land_rover_v0_free(void* context) {
furi_check(context);
free(context);
}
void subghz_protocol_decoder_land_rover_v0_reset(void* context) {
furi_check(context);
SubGhzProtocolDecoderLandRoverV0* instance = context;
instance->decoder.parser_step = LandRoverV0DecoderStepReset;
instance->decoder.te_last = 0;
instance->preamble_count = 0;
memset(instance->raw, 0, sizeof(instance->raw));
instance->bit_count = 0;
instance->extra_bit = false;
instance->previous_bit = true;
instance->boundary_pad_skipped = false;
instance->pending_short = false;
}
void subghz_protocol_decoder_land_rover_v0_feed(
void* context,
bool level,
uint32_t duration) {
furi_check(context);
SubGhzProtocolDecoderLandRoverV0* instance = context;
switch(instance->decoder.parser_step) {
case LandRoverV0DecoderStepReset:
if(level && lr_is_short(duration)) {
instance->preamble_count = 0;
instance->decoder.parser_step = LandRoverV0DecoderStepPreambleLow;
}
break;
case LandRoverV0DecoderStepPreambleLow:
if(!level && lr_is_short(duration)) {
instance->preamble_count++;
instance->decoder.parser_step = LandRoverV0DecoderStepPreambleHigh;
} else {
instance->decoder.parser_step = LandRoverV0DecoderStepReset;
}
break;
case LandRoverV0DecoderStepPreambleHigh:
if(level && lr_is_short(duration)) {
instance->decoder.parser_step = LandRoverV0DecoderStepPreambleLow;
} else if(level && land_rover_v0_is_sync(duration) &&
instance->preamble_count >= LAND_ROVER_V0_MIN_PREAMBLE_PAIRS) {
instance->decoder.parser_step = LandRoverV0DecoderStepSyncLow;
} else {
instance->decoder.parser_step = LandRoverV0DecoderStepReset;
}
break;
case LandRoverV0DecoderStepSyncLow:
if(!level && land_rover_v0_is_sync(duration)) {
memset(instance->raw, 0, sizeof(instance->raw));
instance->bit_count = 0;
instance->extra_bit = false;
instance->previous_bit = true;
instance->boundary_pad_skipped = false;
instance->pending_short = false;
land_rover_v0_add_decoded_bit(instance, true);
instance->decoder.parser_step = LandRoverV0DecoderStepData;
} else {
instance->decoder.parser_step = LandRoverV0DecoderStepReset;
}
break;
case LandRoverV0DecoderStepData:
if(!land_rover_v0_process_transition(instance, level, duration)) {
instance->decoder.parser_step = LandRoverV0DecoderStepReset;
break;
}
if(instance->bit_count ==
subghz_protocol_land_rover_v0_const.min_count_bit_for_found) {
if(land_rover_v0_finish_frame(instance) && instance->base.callback) {
instance->base.callback(&instance->base, instance->base.context);
}
instance->decoder.parser_step = LandRoverV0DecoderStepReset;
}
break;
}
instance->decoder.te_last = duration;
}
uint8_t subghz_protocol_decoder_land_rover_v0_get_hash_data(void* context) {
furi_check(context);
SubGhzProtocolDecoderLandRoverV0* instance = context;
SubGhzBlockDecoder decoder = {
.decode_data = instance->key,
.decode_count_bit = 64,
};
uint8_t hash = subghz_protocol_blocks_get_hash_data(&decoder, 9);
hash ^= (uint8_t)(instance->tail >> 8);
hash ^= (uint8_t)instance->tail;
hash ^= instance->extra_bit ? 1U : 0U;
return hash;
}
SubGhzProtocolStatus subghz_protocol_decoder_land_rover_v0_serialize(
void* context,
FlipperFormat* flipper_format,
SubGhzRadioPreset* preset) {
furi_check(context);
SubGhzProtocolDecoderLandRoverV0* instance = context;
SubGhzProtocolStatus ret =
subghz_block_generic_serialize(&instance->generic, flipper_format, preset);
if(ret == SubGhzProtocolStatusOk) {
uint8_t key_bytes[8];
lr_u64_to_bytes_be(instance->key, key_bytes);
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_hex(
flipper_format, LR_FF_KEY, key_bytes, sizeof(key_bytes));
lr_ff_write_u32(flipper_format, LR_FF_SERIAL, instance->serial);
lr_ff_write_u32(flipper_format, LR_FF_BTN, instance->button);
lr_ff_write_u32(flipper_format, LAND_ROVER_V0_FF_BTNSIG, instance->command_signature);
lr_ff_write_u32(flipper_format, LR_FF_CNT, instance->count);
lr_ff_write_u32(flipper_format, LAND_ROVER_V0_FF_CHECK, instance->check);
lr_ff_write_u32(flipper_format, LAND_ROVER_V0_FF_TAIL, instance->tail);
lr_ff_write_u32(flipper_format, LAND_ROVER_V0_FF_EXTRA_BIT,
instance->extra_bit ? 1U : 0U);
}
return ret;
}
SubGhzProtocolStatus subghz_protocol_decoder_land_rover_v0_deserialize(
void* context,
FlipperFormat* flipper_format) {
furi_check(context);
SubGhzProtocolDecoderLandRoverV0* instance = context;
SubGhzProtocolStatus ret = subghz_block_generic_deserialize_check_count_bit(
&instance->generic,
flipper_format,
subghz_protocol_land_rover_v0_const.min_count_bit_for_found);
if(ret == SubGhzProtocolStatusOk) {
uint8_t key_bytes[8] = {0};
bool have_key = false;
flipper_format_rewind(flipper_format);
if(flipper_format_read_hex(
flipper_format, LR_FF_KEY, key_bytes, sizeof(key_bytes))) {
instance->key = lr_bytes_to_u64_be(key_bytes);
have_key = true;
}
if(!have_key) {
instance->key = instance->generic.data;
lr_u64_to_bytes_be(instance->key, key_bytes);
}
uint32_t temp = 0;
flipper_format_rewind(flipper_format);
if(lr_ff_read_u32(flipper_format, LAND_ROVER_V0_FF_TAIL, &temp)) {
instance->tail = (uint16_t)(temp & 0xFFFFU);
} else {
const uint32_t count =
((uint32_t)key_bytes[6] << 1) | ((key_bytes[7] >> 7) & 1U);
instance->tail = land_rover_v0_calculate_tail(count);
}
flipper_format_rewind(flipper_format);
if(lr_ff_read_u32(flipper_format, LAND_ROVER_V0_FF_EXTRA_BIT, &temp)) {
instance->extra_bit = (temp & 1U) != 0;
} else {
instance->extra_bit = true;
}
land_rover_v0_validate_frame(
instance->key, instance->tail, instance->extra_bit,
&instance->check_ok, &instance->tail_ok);
land_rover_v0_parse_key_fields(
instance->key,
&instance->command_signature,
&instance->serial,
&instance->count,
&instance->button,
&instance->check);
instance->generic.data = instance->key;
instance->generic.data_count_bit =
subghz_protocol_land_rover_v0_const.min_count_bit_for_found;
instance->generic.serial = instance->serial;
instance->generic.btn = instance->button;
instance->generic.cnt = instance->count;
}
return ret;
}
void subghz_protocol_decoder_land_rover_v0_get_string(void* context, FuriString* output) {
furi_check(context);
SubGhzProtocolDecoderLandRoverV0* instance = context;
furi_string_cat_printf(
output,
"%s %dbit\r\n"
"Key:%016llX\r\n"
"Sn:%06lX Btn:%02X - %s\r\n"
"BtnSig:%06lX\r\n"
"Cnt:%05lX Chk:%02X [%s] Tail:%05lX [%s]\r\n",
instance->generic.protocol_name,
instance->generic.data_count_bit,
(unsigned long long)instance->key,
(unsigned long)instance->serial,
instance->button,
land_rover_v0_button_name(instance->button),
(unsigned long)instance->command_signature,
(unsigned long)instance->count,
instance->check,
instance->check_ok ? "OK" : "BAD",
(unsigned long)(((instance->tail >> 15) & 1U) ? 0x1FFFFUL : 0x0FFFFUL),
instance->tail_ok ? "OK" : "BAD");
}
/* ═══════════════════════════════════════════════════════════════════════════
* Encoder helpers
* ═════════════════════════════════════════════════════════════════════════*/
static uint32_t land_rover_v0_signature_from_button(uint8_t button) {
switch(button) {
case LAND_ROVER_V0_BTN_LOCK: return LAND_ROVER_V0_SIG_LOCK;
case LAND_ROVER_V0_BTN_UNLOCK: return LAND_ROVER_V0_SIG_UNLOCK;
default: return 0;
}
}
static uint64_t land_rover_v0_build_key(
uint32_t signature,
uint32_t serial,
uint32_t count) {
uint8_t key_bytes[8] = {0};
key_bytes[0] = (uint8_t)((signature >> 16) & 0xFFU);
key_bytes[1] = (uint8_t)((signature >> 8) & 0xFFU);
key_bytes[2] = (uint8_t)( signature & 0xFFU);
key_bytes[3] = (uint8_t)((serial >> 16) & 0xFFU);
key_bytes[4] = (uint8_t)((serial >> 8) & 0xFFU);
key_bytes[5] = (uint8_t)( serial & 0xFFU);
key_bytes[6] = (uint8_t)((count >> 1) & 0xFFU);
const bool counter_lsb = (count & 1U) != 0;
const uint8_t check = land_rover_v0_calculate_check(count);
key_bytes[7] = (counter_lsb ? 0x80U : 0x00U) | check;
return lr_bytes_to_u64_be(key_bytes);
}
/* ═══════════════════════════════════════════════════════════════════════════
* Encoder public API
* ═════════════════════════════════════════════════════════════════════════*/
void* subghz_protocol_encoder_land_rover_v0_alloc(SubGhzEnvironment* environment) {
UNUSED(environment);
SubGhzProtocolEncoderLandRoverV0* instance =
calloc(1, sizeof(SubGhzProtocolEncoderLandRoverV0));
furi_check(instance);
instance->base.protocol = &subghz_protocol_land_rover_v0;
instance->generic.protocol_name = instance->base.protocol->name;
/* Allocate the waveform upload buffer */
instance->encoder.upload =
malloc(LAND_ROVER_V0_UPLOAD_CAPACITY * sizeof(LevelDuration));
furi_check(instance->encoder.upload);
instance->encoder.repeat = 10;
instance->encoder.size_upload = 0;
instance->encoder.front = 0;
instance->encoder.is_running = false;
return instance;
}
void subghz_protocol_encoder_land_rover_v0_free(void* context) {
furi_check(context);
SubGhzProtocolEncoderLandRoverV0* instance = context;
free(instance->encoder.upload);
free(instance);
}
SubGhzProtocolStatus subghz_protocol_encoder_land_rover_v0_deserialize(
void* context,
FlipperFormat* flipper_format) {
furi_check(context);
SubGhzProtocolEncoderLandRoverV0* instance = context;
SubGhzProtocolStatus ret = SubGhzProtocolStatusError;
instance->encoder.is_running = false;
instance->encoder.front = 0;
instance->encoder.repeat = 10;
do {
if(lr_verify_protocol_name(flipper_format, instance->base.protocol->name) !=
SubGhzProtocolStatusOk)
break;
SubGhzProtocolStatus load_status =
subghz_block_generic_deserialize_check_count_bit(
&instance->generic,
flipper_format,
subghz_protocol_land_rover_v0_const.min_count_bit_for_found);
if(load_status != SubGhzProtocolStatusOk) break;
instance->serial = instance->generic.serial & 0xFFFFFFU;
instance->button = instance->generic.btn;
instance->count = instance->generic.cnt & 0x1FFU;
uint8_t key_bytes[8] = {0};
bool have_key = false;
flipper_format_rewind(flipper_format);
if(flipper_format_read_hex(
flipper_format, LR_FF_KEY, key_bytes, sizeof(key_bytes))) {
instance->key = lr_bytes_to_u64_be(key_bytes);
have_key = true;
}
if(have_key) {
land_rover_v0_parse_key_fields(
instance->key,
&instance->command_signature,
&instance->serial,
&instance->count,
&instance->button,
&instance->check);
}
uint32_t u32 = 0;
bool have_button = false;
flipper_format_rewind(flipper_format);
if(lr_ff_read_u32(flipper_format, LR_FF_SERIAL, &u32))
instance->serial = u32 & 0xFFFFFFU;
flipper_format_rewind(flipper_format);
if(lr_ff_read_u32(flipper_format, LR_FF_CNT, &u32))
instance->count = u32 & 0x1FFU;
flipper_format_rewind(flipper_format);
if(lr_ff_read_u32(flipper_format, LR_FF_BTN, &u32)) {
instance->button = (uint8_t)u32;
have_button = true;
}
flipper_format_rewind(flipper_format);
if(lr_ff_read_u32(flipper_format, LAND_ROVER_V0_FF_BTNSIG, &u32))
instance->command_signature = u32 & 0xFFFFFFU;
if(have_button) {
const uint32_t sig = land_rover_v0_signature_from_button(instance->button);
if(sig != 0U) instance->command_signature = sig;
}
if(instance->command_signature == 0U) break;
instance->key = land_rover_v0_build_key(
instance->command_signature, instance->serial, instance->count);
lr_u64_to_bytes_be(instance->key, key_bytes);
instance->tail = land_rover_v0_calculate_tail(instance->count);
land_rover_v0_parse_key_fields(
instance->key,
&instance->command_signature,
&instance->serial,
&instance->count,
&instance->button,
&instance->check);
instance->generic.data = instance->key;
instance->generic.data_count_bit =
subghz_protocol_land_rover_v0_const.min_count_bit_for_found;
instance->generic.serial = instance->serial;
instance->generic.btn = instance->button;
instance->generic.cnt = instance->count;
flipper_format_rewind(flipper_format);
instance->encoder.repeat = lr_encoder_read_repeat(flipper_format, 10);
if(!land_rover_v0_build_upload(instance) ||
instance->encoder.size_upload == 0U)
break;
/* Update the file with all recalculated fields */
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_hex(
flipper_format, LR_FF_KEY, key_bytes, sizeof(key_bytes));
lr_ff_write_u32(flipper_format, LR_FF_SERIAL, instance->serial);
lr_ff_write_u32(flipper_format, LR_FF_BTN, instance->button);
lr_ff_write_u32(flipper_format, LAND_ROVER_V0_FF_BTNSIG, instance->command_signature);
lr_ff_write_u32(flipper_format, LR_FF_CNT, instance->count);
lr_ff_write_u32(flipper_format, LAND_ROVER_V0_FF_CHECK, instance->check);
lr_ff_write_u32(flipper_format, LAND_ROVER_V0_FF_TAIL, instance->tail);
lr_ff_write_u32(flipper_format, LAND_ROVER_V0_FF_EXTRA_BIT, 1U);
instance->encoder.is_running = true;
ret = SubGhzProtocolStatusOk;
} while(false);
return ret;
}
void subghz_protocol_encoder_land_rover_v0_stop(void* context) {
furi_check(context);
SubGhzProtocolEncoderLandRoverV0* instance = context;
instance->encoder.is_running = false;
}
LevelDuration subghz_protocol_encoder_land_rover_v0_yield(void* context) {
furi_check(context);
SubGhzProtocolEncoderLandRoverV0* instance = context;
if(instance->encoder.front >= instance->encoder.size_upload) {
/* One full repetition done; count it down */
if(instance->encoder.repeat > 0) {
instance->encoder.repeat--;
}
instance->encoder.front = 0;
if(instance->encoder.repeat == 0) {
instance->encoder.is_running = false;
return level_duration_reset();
}
}
return instance->encoder.upload[instance->encoder.front++];
}
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <furi.h>
#include <lib/subghz/protocols/base.h>
#include <lib/subghz/types.h>
#include <lib/subghz/blocks/const.h>
#include <lib/subghz/blocks/decoder.h>
#include <lib/subghz/blocks/encoder.h>
#include <lib/subghz/blocks/generic.h>
#include <lib/subghz/blocks/math.h>
#include <flipper_format/flipper_format.h>
#include <lib/toolbox/level_duration.h>
#define LAND_ROVER_PROTOCOL_V0_NAME "Land Rover V0"
extern const SubGhzProtocol subghz_protocol_land_rover_v0;
void* subghz_protocol_decoder_land_rover_v0_alloc(SubGhzEnvironment* environment);
void subghz_protocol_decoder_land_rover_v0_free(void* context);
void subghz_protocol_decoder_land_rover_v0_reset(void* context);
void subghz_protocol_decoder_land_rover_v0_feed(void* context, bool level, uint32_t duration);
uint8_t subghz_protocol_decoder_land_rover_v0_get_hash_data(void* context);
SubGhzProtocolStatus subghz_protocol_decoder_land_rover_v0_serialize(
void* context,
FlipperFormat* flipper_format,
SubGhzRadioPreset* preset);
SubGhzProtocolStatus
subghz_protocol_decoder_land_rover_v0_deserialize(void* context, FlipperFormat* flipper_format);
void subghz_protocol_decoder_land_rover_v0_get_string(void* context, FuriString* output);
void* subghz_protocol_encoder_land_rover_v0_alloc(SubGhzEnvironment* environment);
void subghz_protocol_encoder_land_rover_v0_free(void* context);
SubGhzProtocolStatus
subghz_protocol_encoder_land_rover_v0_deserialize(void* context, FlipperFormat* flipper_format);
void subghz_protocol_encoder_land_rover_v0_stop(void* context);
LevelDuration subghz_protocol_encoder_land_rover_v0_yield(void* context);
+1
View File
@@ -84,6 +84,7 @@ const SubGhzProtocol* const subghz_protocol_registry_items[] = {
&ford_protocol_v1,
&ford_protocol_v2,
&ford_protocol_v3,
&subghz_protocol_land_rover_v0,
};
+1
View File
@@ -85,3 +85,4 @@
#include "ford_v1.h"
#include "ford_v2.h"
#include "ford_v3.h"
#include "land_rover_v0.h"