mirror of
https://github.com/D4C1-Labs/Flipper-ARF.git
synced 2026-06-09 23:21:38 +00:00
Compare commits
6 Commits
dev-efa653c
...
1.0.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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
@@ -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.
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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); \
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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_v2,
|
||||
&ford_protocol_v3,
|
||||
&subghz_protocol_land_rover_v0,
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -85,3 +85,4 @@
|
||||
#include "ford_v1.h"
|
||||
#include "ford_v2.h"
|
||||
#include "ford_v3.h"
|
||||
#include "land_rover_v0.h"
|
||||
|
||||
Reference in New Issue
Block a user