This should work...

This commit is contained in:
d4rks1d33 committed 2026-09-19 17:16:58 +00:00
1 parent 3e19d82167
commit 724abde8ec
47 files changed
+3105 -653

No files matched your search

+218
View File
@@ -84,3 +84,221 @@ the app keep its vendored protocols:
runtime, so it cannot be linked against the current private lib safely. The
app must be updated to the current SubGhz block API before re-enabling.
Its `application.fam` is renamed to `application.fam.disabled`.
## toolbox symbols exported for the private libs
Because `lib/subghz`, `lib/nfc`, `lib/lfrfid` are no longer linked into the
firmware, any *firmware* symbol they call must be part of the public SDK API
(`targets/f7/api_symbols.csv`) so the app loader can resolve it at runtime.
Most already were, but some `lib/toolbox` helpers used only internally by these
libs were not exported. If you extract another lib or bump these libs and hit
`MissingImports`/`unresolved` for a `lib/toolbox` (or other firmware) function,
export it the SDK way — **do not hand-edit the CSV blindly**:
1. Add the header to `SDK_HEADERS` in the owning lib's SConscript
(e.g. `File("buffer_stream.h")` in `lib/toolbox/SConscript`).
2. Build once — the SDK checker stops with "API version is still WIP" and writes
the new entries into `targets/f7/api_symbols.csv` with status `?`
(and bumps `Version` to `v`).
3. Flip those `?` to `+` and the `Version,v,X.Y` to `Version,+,X.Y`, then rebuild.
Already exported this way: the 9 `buffer_stream.h` symbols (`buffer_get_data`,
`buffer_get_size`, `buffer_reset`, `buffer_stream_alloc`, `buffer_stream_free`,
`buffer_stream_get_overrun_count`, `buffer_stream_receive`, `buffer_stream_reset`,
`buffer_stream_send_from_isr`) — needed by `lib/nfc` (nfc.c, mf_classic, felica…)
and `lib/lfrfid` (raw worker). `mbedtls_des3_*` stay `-` (NOT exported): apps
that need them link `mbedtls` statically via `fap_libs=["mbedtls"]`.
## Where the private libs come from
The private lib each app carries is **the same firmware library**, reached
through a relative symlink — there is no separate copy on disk:
- `applications/*/<app>/lib/subghz -> ../../../../lib/subghz`
- `applications/*/<app>/lib/nfc -> ../../../../lib/nfc`
- `applications/*/<app>/lib/lfrfid -> ../../../../lib/lfrfid`
(4 `../` for apps at `applications/<category>/<app>/`.) The `fap_private_libs`
`Lib(name=...)` then compiles that symlinked tree into a per-app static lib.
So edits to `lib/subghz` etc. affect both the firmware headers and every app's
private lib automatically.
---
# Updating a vendored SubGhz app (ProtoPirate case study)
ProtoPirate (`applications/system/ProtoPirate`) is a vendored-protocol SubGhz app
tracked from upstream `https://protopirate.net/ProtoPirate/ProtoPirate`. It is
NOT a git submodule/subtree — it lives inside this repo, so updates are a manual
"copy upstream + re-apply our XIP adaptations". Everything we change lives in
**files under our control** (`application.fam` + `api/`); upstream `.c`/`.h`
files are kept byte-identical so future merges stay easy.
## The 4 XIP adaptations that MUST survive an update
1. **`lib/subghz` symlink** — `applications/system/ProtoPirate/lib/subghz ->
../../../../lib/subghz`. Preserve it (don't delete `lib/`).
2. **`api/` table** — `api/protopirate_api_table.cpp` (defines
`subghz_application_api_interface`) + `api/protopirate_api_table_i.h` (the
symbol list). These expose the SubGhz lib symbols the runtime `.fal` plugins
import. Preserve both.
3. **Resolver wiring in the 3 host files** — each of
`helpers/protopirate_protocol_plugin_host.c`,
`helpers/protopirate_psa_bf_host.c`,
`helpers/protopirate_tool_scene_host.c` needs, after its
`#include <loader/firmware_api/firmware_api.h>`:
```c
extern const ElfApiInterface* const subghz_application_api_interface;
```
and, right after every `composite_api_resolver_add(resolver, firmware_api_interface);`:
```c
composite_api_resolver_add(resolver, subghz_application_api_interface);
```
(2 sites in protocol_plugin_host.c, 1 each in the other two.)
4. **`application.fam`** — the `_PROTOPIRATE_SUBGHZ_LIB = Lib(name="subghz", …
sources=["*.c*","blocks/*.c*","devices/*.c*","devices/cc1101_int/*.c*",
"protocols/base.c"])` block (curated: infra only, NOT the vendored concrete
protocols), plus on the main `proto_pirate` App: `sources=_MAIN_APP_SOURCES +
["!lib"]`, `fap_libs=["hwdrivers"]`, `fap_private_libs=[_PROTOPIRATE_SUBGHZ_LIB]`,
`stack_size=8*1024`.
## Step-by-step update procedure
```bash
# 1. Clone upstream to a temp dir
git clone --depth 1 https://protopirate.net/ProtoPirate/ProtoPirate.git /tmp/pp
DEST=applications/system/ProtoPirate
# 2. See what changed / is new (excluding our api/ and lib/)
for f in $(cd /tmp/pp && find . -type f -not -path './.git*'); do
if [ -f "$DEST/$f" ]; then diff -q "$DEST/$f" "/tmp/pp/$f" >/dev/null || echo "CHANGED: $f";
else echo "NEW: $f"; fi; done
# 3. Copy everything EXCEPT application.fam (merge that by hand) and never touch api/ or lib/
for f in $(cd /tmp/pp && find . -type f -not -path './.git*' -not -name application.fam); do
mkdir -p "$DEST/$(dirname "$f")"; cp "/tmp/pp/$f" "$DEST/$f"; done
```
4. Re-apply adaptation **#3** to the 3 host files (they got overwritten).
5. Merge **#4**: `cp /tmp/pp/application.fam $DEST/application.fam`, then re-inject
the private-lib block + the main-app XIP fields (see above). Keep any NEW
upstream apps/plugins/sources from the merge.
6. Build the app only, then verify plugins resolve (see next section). Any newly
imported SubGhz symbol goes into `api/protopirate_api_table_i.h`; any newly
imported app-level (`protopirate_*`) symbol means a plugin needs an extra `.c`
added to its `sources` in the fam.
## Gotchas learned from the v3.4 update
- **New plugins that drag in the host stack.** v3.4 added
`protopirate_config_plugin`, which compiles `protopirate_txrx.c` +
`protopirate_protocol_plugin_host.c` and therefore pulls the whole RX radio +
plugin-loader stack. To make that `.fal` self-contained we added to its
`sources`: `helpers/protopirate_radio.c`, `helpers/radio_device_loader.c`,
`views/protopirate_receiver.c`, `protopirate_history.c`,
`helpers/protopirate_storage.c`, `protocols/protocols_common.c` (defines the
`FF_*` flipper-format key strings), and `api/protopirate_api_table.cpp`
(defines `subghz_application_api_interface`).
- **Generated icons header name.** `views/protopirate_receiver.c` does
`#include "proto_pirate_icons.h"`. That header is generated from the app's
`fap_icon_assets` and named `<appid>_icons.h`. For a plugin whose appid differs,
set `fap_icon_assets_symbol="proto_pirate"` in its App() so the generated header
matches the upstream include — this avoids editing upstream sources.
- **Resolving is iterative.** Adding one `.c` can pull in a new family of
undefined symbols (e.g. `radio_device_loader.c` pulled `subghz_devices_begin/
end/get_by_name/is_connect`). Re-run the verify command after each change until
it prints nothing.
---
# Troubleshooting a broken external SubGhz / NFC / RFID app
Symptoms and how to chase them down. The root cause is almost always a symbol the
private lib (or a plugin) needs that is neither in the firmware SDK
(`api_symbols.csv`) nor in the app's own API table.
## Symptom → meaning
- On device, opening the app **reboots with an out-of-memory crash**, or the log
shows `XIP: unresolved fast rel record N` and then cuts off → a symbol used by
the XIP'd `.fap` is unresolved; the XIP relocation aborts mid-stream.
- `Status [3]: … MissingImports` in the loader log → same root cause, but the app
fit in RAM (no XIP) so it failed cleanly instead of crashing.
- `[E][Fap] Failed to preload …` while browsing → the `.fap` header/manifest
couldn't be read (usually a stale/othertarget build), not a symbol issue.
## Fast offline check (do this before flashing)
Compare the app's *undefined* symbols against everything that can resolve them
(firmware SDK + the app's own API table). Anything left over (minus known
libgcc/linker builtins) is a real problem.
```bash
NM=/opt/postmarket/test/Flipper-ARF/toolchain/current/bin/arm-none-eabi-nm
ELF=build/f7-firmware-C/.extapps/<app>_d.elf # or a *_plugin_d.elf
# symbols exported by the firmware SDK
grep -E "^(Function|Variable),\+," targets/f7/api_symbols.csv | awk -F, '{print $3}' | sort -u > /tmp/resolvable.txt
# (SubGhz vendored apps also add their own table; append it:)
grep -A1 "API_METHOD(\|API_VARIABLE(" applications/system/<app>/api/*_api_table_i.h \
| grep -vE "API_METHOD|API_VARIABLE|^--" | tr -d ' ,' >> /tmp/resolvable.txt
sort -u -o /tmp/resolvable.txt /tmp/resolvable.txt
# undefined in the app that nothing can resolve (ignore builtins)
"$NM" -u "$ELF" 2>/dev/null | awk '{print $2}' | sort -u \
| comm -23 - /tmp/resolvable.txt \
| grep -vE "^__|uxTopUsedPriority"
```
Known-harmless leftovers (resolved at final `.fal`/loader link): `__aeabi_*`,
`__paritysi2`, `__popcountsi2`, `uxTopUsedPriority`. Also ignore `FF_*` style
`extern const char[]` if the app defines them in one of its own `.c` (e.g.
`protocols_common.c`) — check with `grep`.
## Fix depending on where the symbol lives
- **Symbol is a firmware/`lib/toolbox` function not in the SDK** → export it via
`SDK_HEADERS` (see "toolbox symbols exported" above). Fixes the app AND every
other external app.
- **Symbol is a SubGhz lib function** and the app is a vendored SubGhz app with
`.fal` plugins → add it to that app's `api/*_api_table_i.h` (with the exact
signature from the lib header) so the plugin resolver can satisfy it. Include
the relevant `subghz/*.h` at the top of the table if needed.
- **Symbol is an app-level function** (`<app>_*`) undefined in a `.fal` plugin →
the plugin compiles a `.c` that *calls* it but not the `.c` that *defines* it.
Add the defining `.c` to that plugin's `sources` in `application.fam`. Watch for
cascades (adding a `.c` can introduce new undefined symbols) and for duplicate
definitions (don't add a `.c` whose symbols another listed source already
defines).
- **Symbol is a `mbedtls_*`/`bit_lib`/`assets` function** → the app must link that
static lib: `fap_libs=["mbedtls", "bit_lib", "assets"]` (nfc needs all three;
subghz needs none).
## NFC-specific notes
- Every external NFC app needs `fap_libs=["mbedtls","bit_lib"]` (the nfc lib uses
both) plus the `nfc` private lib. `furi_hal_nfc` stays in the firmware SDK.
- App API table for the bundled NFC app is
`applications/main/nfc/api/nfc_app_api_table_i.h` (exposes the lib symbols the
card parsers / protocol `.fal` plugins import). If a parser plugin fails to
resolve, add the missing lib symbol there.
- **C vs C++ linkage trap:** the api table is a `.cpp`. Any app-layer header it
includes to reference a symbol MUST have `extern "C"` guards, or C++ name
mangling makes the symbol undefined. This bit us with
`helpers/protocol_support/emv/emv_render.h` and `helpers/nfc_emv_parser.h`
(they lacked the guards; every other table header had them). Symptom: nm shows
both a mangled `_Z…name` and a plain `name` undefined.
- A symbol that is *declared + called + listed in the table but never defined*
(dead code upstream tolerates via `--gc-sections`) becomes undefined once the
table takes its address. We hit `nfc_render_emv_name` (had no definition in
Moon/Momentum either) and had to define it in `emv_render.c`.
## RFID-specific notes
- External RFID apps carry the `lfrfid` private lib (symlink +
`fap_private_libs=[Lib(name="lfrfid", …)]`). No `mbedtls` needed.
- RFID apps are usually small enough to load into RAM (log:
`App fits in RAM … skipping XIP`), so unresolved symbols show as a clean
`MissingImports` rather than a reboot. Same fix paths as above — most commonly
a missing `lib/toolbox` export (this is exactly how the `buffer_stream_*` gap
surfaced first on `lfrfid.fap`).
@@ -4,6 +4,10 @@
#include <stdbool.h>
#include <storage/storage.h>
#ifdef __cplusplus
extern "C" {
#endif
/** Get EMV application name by number
* @param storage Storage instance
* @param aid - AID number array
@@ -38,3 +42,7 @@ bool nfc_emv_parser_get_currency_name(
Storage* storage,
uint16_t currency_code,
FuriString* currency_name);
#ifdef __cplusplus
}
#endif
@@ -30,6 +30,12 @@ void nfc_render_emv_uid(const uint8_t* uid, const uint8_t uid_len, FuriString* s
furi_string_cat_printf(str, "\n");
}
void nfc_render_emv_name(const char* data, FuriString* str) {
if(data == NULL || data[0] == '\0') return;
furi_string_cat_printf(str, "Name: %s\n", data);
}
void nfc_render_emv_data(const EmvData* data, FuriString* str) {
nfc_render_emv_pan(data->emv_application.pan, data->emv_application.pan_len, str);
nfc_render_emv_name(data->emv_application.application_name, str);
@@ -5,6 +5,10 @@
#include "../nfc_protocol_support_render_common.h"
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
void nfc_render_emv_info(const EmvData* data, NfcProtocolFormatType format_type, FuriString* str);
void nfc_render_emv_data(const EmvData* data, FuriString* str);
@@ -28,3 +32,7 @@ void nfc_render_emv_transactions(const EmvApplication* data, FuriString* str);
void nfc_render_emv_uid(const uint8_t* uid, const uint8_t uid_len, FuriString* str);
void nfc_render_emv_header(FuriString* str);
#ifdef __cplusplus
}
#endif
+1 -1
View File
@@ -52,7 +52,7 @@ Protocols are split into **AM** and **FM** registries. The active registry is ch
| Ford V1 | ✅ | ✅ | Manchester | F4 | Rolling Code | CRC16 | 315.00 / 433.92 |
| Ford V2 | ✅ | ✅ | Manchester | F4 | Rolling Code (simple replay) | ❌ | 434.25 |
| Ford V3 | ✅ | ❌ | Manchester | F4 | Rolling Code | ❌ | 434.25 |
| Honda Static | ✅ | ✅ | PWM | Honda1 | Static Code | Checksum | 315.00 / 433.92 |
| Honda Static | ✅ | ✅ | PWM | Honda1 | Static Code | Checksum | 433.65 |
| Kia V0 / Suzuki V0 / Honda V0 / Mitsubishi V0 | ✅ | ✅ | PWM | FM476 | Rolling Code | CRC8 | 315.00 / 433.92 |
| Kia V2 | ✅ | ✅ | Manchester | FM476 | Rolling Code | CRC4 | 315.00 / 433.92 |
| Kia V3 / V4 | ✅ | ✅ | PWM | FM476 | KeeLoq | CRC4 (BF) | 315.00 / 433.92 |
@@ -4,6 +4,7 @@
#include <subghz/transmitter.h>
#include <subghz/subghz_setting.h>
#include <subghz/subghz_keystore.h>
#include <subghz/subghz_worker.h>
#include <subghz/devices/devices.h>
#include <subghz/blocks/generic.h>
#include <subghz/blocks/decoder.h>
@@ -158,6 +159,135 @@ static constexpr auto subghz_app_api_table = sort(create_array_t<sym_entry>(
subghz_transmitter_yield,
LevelDuration,
(void* context)),
/* RX-path symbols imported by protopirate_config_plugin (v3.4) */
API_METHOD(
subghz_devices_init,
void,
(void)),
API_METHOD(
subghz_devices_deinit,
void,
(void)),
API_METHOD(
subghz_devices_begin,
bool,
(const SubGhzDevice* device)),
API_METHOD(
subghz_devices_end,
void,
(const SubGhzDevice* device)),
API_METHOD(
subghz_devices_get_by_name,
const SubGhzDevice*,
(const char* device_name)),
API_METHOD(
subghz_devices_is_connect,
bool,
(const SubGhzDevice* device)),
API_METHOD(
subghz_environment_alloc,
SubGhzEnvironment*,
(void)),
API_METHOD(
subghz_environment_free,
void,
(SubGhzEnvironment* instance)),
API_METHOD(
subghz_environment_load_keystore,
bool,
(SubGhzEnvironment* instance, const char* filename)),
API_METHOD(
subghz_devices_flush_rx,
void,
(const SubGhzDevice* device)),
API_METHOD(
subghz_devices_get_rssi,
float,
(const SubGhzDevice* device)),
API_METHOD(
subghz_devices_is_frequency_valid,
bool,
(const SubGhzDevice* device, uint32_t frequency)),
API_METHOD(
subghz_devices_set_async_mirror_pin,
void,
(const SubGhzDevice* device, const GpioPin* gpio)),
API_METHOD(
subghz_devices_set_rx,
void,
(const SubGhzDevice* device)),
API_METHOD(
subghz_devices_sleep,
void,
(const SubGhzDevice* device)),
API_METHOD(
subghz_devices_start_async_rx,
void,
(const SubGhzDevice* device, void* callback, void* context)),
API_METHOD(
subghz_devices_stop_async_rx,
void,
(const SubGhzDevice* device)),
API_METHOD(
subghz_environment_set_protocol_registry,
void,
(SubGhzEnvironment* instance, const SubGhzProtocolRegistry* protocol_registry_items)),
API_METHOD(
subghz_receiver_alloc_init,
SubGhzReceiver*,
(SubGhzEnvironment* environment)),
API_METHOD(
subghz_receiver_free,
void,
(SubGhzReceiver* instance)),
API_METHOD(
subghz_receiver_set_filter,
void,
(SubGhzReceiver* instance, SubGhzProtocolFlag filter)),
API_METHOD(
subghz_setting_get_default_frequency,
uint32_t,
(SubGhzSetting* instance)),
API_METHOD(
subghz_setting_get_frequency,
uint32_t,
(SubGhzSetting* instance, size_t idx)),
API_METHOD(
subghz_setting_get_frequency_count,
size_t,
(SubGhzSetting* instance)),
API_METHOD(
subghz_setting_get_frequency_default_index,
uint32_t,
(SubGhzSetting* instance)),
API_METHOD(
subghz_setting_get_hopper_frequency,
uint32_t,
(SubGhzSetting* instance, size_t idx)),
API_METHOD(
subghz_setting_get_hopper_frequency_count,
size_t,
(SubGhzSetting* instance)),
API_METHOD(
subghz_worker_free,
void,
(SubGhzWorker* instance)),
API_METHOD(
subghz_worker_is_running,
bool,
(SubGhzWorker* instance)),
API_METHOD(
subghz_worker_rx_callback,
void,
(bool level, uint32_t duration, void* context)),
API_METHOD(
subghz_worker_start,
void,
(SubGhzWorker* instance)),
API_METHOD(
subghz_worker_stop,
void,
(SubGhzWorker* instance)),
API_VARIABLE(subghz_device_cc1101_preset_ook_270khz_async_regs, const uint8_t[]),
API_VARIABLE(subghz_device_cc1101_preset_ook_650khz_async_regs, const uint8_t[]),
API_VARIABLE(subghz_device_cc1101_preset_2fsk_dev2_38khz_async_regs, const uint8_t[]),
@@ -17,6 +17,8 @@ _ENABLE_EMULATE = ProtoPirateDefineEnabled("ENABLE_EMULATE_FEATURE")
_MAIN_APP_SOURCES = [
"*.c*",
"!helpers/protopirate_models.c",
"!helpers/variable_item_list.c",
"!protocols/plugins",
"!scenes/plugins",
"!protocols",
@@ -69,7 +71,7 @@ App(
requires=["gui"],
stack_size=8 * 1024,
fap_description="Decode car key fob signals from Sub-GHz",
fap_version="3.3",
fap_version="3.4",
fap_icon="images/protopirate_10px.png",
fap_category="Sub-GHz",
fap_icon_assets="images",
@@ -443,3 +445,37 @@ App(
fap_icon_assets="images",
fal_embedded=True,
)
App(
appid="protopirate_config_plugin",
apptype=FlipperAppType.PLUGIN,
entry_point="protopirate_config_plugin_ep",
requires=["proto_pirate"],
sources=[
"helpers/protopirate_protocol_plugin_host.c",
"helpers/protopirate_models.c",
"helpers/protopirate_txrx.c",
"helpers/variable_item_list.c",
"protocols/protocol_items.c",
"protocols/protocols_common.c",
"scenes/plugins/protopirate_config_plugin.c",
# XIP adaptation: SubGhz lib is private to this app, so the config
# plugin (which drags in the protocol-plugin host + RX radio stack via
# protopirate_txrx.c) must be self-contained. These app-level sources
# define the protopirate_* symbols the host stack references, and the
# api table provides subghz_application_api_interface for the nested
# protocol-registry plugin resolver.
"helpers/protopirate_radio.c",
"helpers/radio_device_loader.c",
"views/protopirate_receiver.c",
"protopirate_history.c",
"helpers/protopirate_storage.c",
"api/protopirate_api_table.cpp",
],
fap_icon_assets="images",
# Generate the icon bundle header as "proto_pirate_icons.h" (matching the
# main app) so the upstream views/protopirate_receiver.c include resolves
# unchanged — keeps upstream sources untouched for easy future updates.
fap_icon_assets_symbol="proto_pirate",
fal_embedded=True,
)
@@ -0,0 +1,285 @@
#include "protopirate_models.h"
#define TAG "ProtoPirateModels"
#define PROTOPIRATE_MODELS_FILE APP_ASSETS_PATH("models.txt")
//Models File Format.
#define MODELS_FILE_HEADER "ProtoPirate Car Models Database"
#define MODELS_FILE_VERSION 2
#define MODELS_FIELD_MODEL_COUNT "ModelCount"
#define MODELS_FIELD_MODEL_NAME "ModelName"
#define MODELS_FIELD_FREQUENCY "Frequency"
#define MODELS_FIELD_PRESET_NAME "PresetName"
#define MODELS_FIELD_CUSTOM_PRESET_DATA "CustomPresetData"
bool car_model_get_by_index(
ProtoPirateCarModel* car_model,
uint16_t index,
uint16_t car_models_count,
SubGhzSetting* app_settings) {
//Temp variables
uint32_t version = 0;
uint32_t frequency;
uint8_t* preset_data = NULL;
uint32_t preset_data_size = 0;
bool error = true;
bool custom_preset = false;
//Objects that need allocating.
Storage* storage = furi_record_open(RECORD_STORAGE);
FlipperFormat* ff = flipper_format_file_alloc(storage);
FuriString* preset_name = furi_string_alloc();
FuriString* model_name = furi_string_alloc();
FuriString* header = furi_string_alloc();
//Index 0 is None.
switch(index) {
case 0:
//Leave as same as error (none)
break;
default: {
//Open Models File, check the Header, check Version
if(!flipper_format_file_open_existing(ff, PROTOPIRATE_MODELS_FILE)) {
FURI_LOG_I(TAG, "models.txt file not found");
break;
} else if(!flipper_format_read_header(ff, header, &version)) {
FURI_LOG_W(TAG, "Failed to read models.txt header");
break;
} else if(furi_string_cmp_str(header, MODELS_FILE_HEADER) != 0) {
FURI_LOG_W(TAG, "Invalid models.txt file header");
break;
} else if(version != MODELS_FILE_VERSION) {
FURI_LOG_W(TAG, "Unsupported models.txt version %lu", (unsigned long)version);
break;
}
//Fields we add the number to for searching the file.
char model_name_index[16]; //Frequency65535 + NULL
char frequency_index[16]; //Frequency65535 + NULL
char preset_name_index[18]; //PresetName65535 + NULL
char preset_data_index[23]; //CustomPresetData65535 + NULL
//Build the values we need to grab.
snprintf(model_name_index, sizeof(model_name_index), MODELS_FIELD_MODEL_NAME "%u", index);
snprintf(frequency_index, sizeof(frequency_index), MODELS_FIELD_FREQUENCY "%u", index);
snprintf(
preset_name_index, sizeof(preset_name_index), MODELS_FIELD_PRESET_NAME "%u", index);
snprintf(
preset_data_index,
sizeof(preset_data_index),
MODELS_FIELD_CUSTOM_PRESET_DATA "%u",
index);
//Read the Model Name.
if(!flipper_format_read_string(ff, model_name_index, model_name)) {
FURI_LOG_E("ProtoPirate", "Failed to read %s", model_name_index);
break;
}
// Read frequency
if(!flipper_format_read_uint32(ff, frequency_index, &frequency, 1)) {
FURI_LOG_W(TAG, "Failed to read frequency.");
frequency = 0;
break;
}
//Read the Modulation Preset.
if(!flipper_format_read_string(ff, preset_name_index, preset_name)) {
//Set the Custom preset name.
furi_string_set_str(preset_name, "Custom");
custom_preset = true;
} else {
//Get the built in preset if it exists
const char* preset_name_long = furi_string_get_cstr(preset_name);
const char* preset_name_short = preset_name_long;
//Which Preset have we selected?
if(!strcmp(preset_name_long, "FuriHalSubGhzPresetOok270Async")) {
preset_name_short = "AM270";
} else if(!strcmp(preset_name_long, "FuriHalSubGhzPresetOok650Async")) {
preset_name_short = "AM650";
} else if(!strcmp(preset_name_long, "FuriHalSubGhzPreset2FSKDev238Async")) {
preset_name_short = "FM238";
} else if(!strcmp(preset_name_long, "FuriHalSubGhzPreset2FSKDev12KAsync")) {
preset_name_short = "FM12K";
} else if(!strcmp(preset_name_long, "FuriHalSubGhzPreset2FSKDev476Async")) {
preset_name_short = "FM476";
} else if(!strcmp(preset_name_long, "FuriHalSubGhzPresetCustom")) {
preset_name_short = "Custom";
custom_preset = true;
}
//Set the preset_name
furi_string_set_str(preset_name, preset_name_short);
if(!custom_preset) {
size_t preset_index = subghz_setting_get_preset_count(app_settings);
for(size_t i = 0; i < subghz_setting_get_preset_count(app_settings); i++) {
if(!strcmp(
subghz_setting_get_preset_name(app_settings, i), preset_name_short)) {
preset_index = i;
break;
}
}
if(preset_index >= subghz_setting_get_preset_count(app_settings)) {
preset_name_short = "AM650";
for(size_t i = 0; i < subghz_setting_get_preset_count(app_settings); i++) {
if(!strcmp(
subghz_setting_get_preset_name(app_settings, i),
preset_name_short)) {
preset_index = i;
break;
}
}
if(preset_index >= subghz_setting_get_preset_count(app_settings)) {
FURI_LOG_E(TAG, "Failed to get preset index!");
break;
}
}
//Get the preset data into a buffer to copy it.
uint8_t* temp_preset_data = NULL;
uint32_t temp_preset_data_size = 0;
temp_preset_data = subghz_setting_get_preset_data(app_settings, preset_index);
temp_preset_data_size =
subghz_setting_get_preset_data_size(app_settings, preset_index);
//Error fallback
if(temp_preset_data == NULL) {
FURI_LOG_E(TAG, "Failed to get preset data!");
break;
} else {
//Copy the preset data, so we can initialize it.
preset_data = malloc(temp_preset_data_size);
memcpy(preset_data, temp_preset_data, temp_preset_data_size);
preset_data_size = temp_preset_data_size;
//Yay, we made it...
error = false;
}
}
}
/* Custom_preset_data (only if present) */
if(custom_preset &&
flipper_format_get_value_count(ff, preset_data_index, &preset_data_size) &&
preset_data_size > 0) {
if(preset_data_size >= 1024) {
FURI_LOG_E(
"ProtoPirate",
"%s too large: %lu",
preset_data_index,
(unsigned long)preset_data_size);
break;
}
preset_data = malloc(preset_data_size);
if(!preset_data) {
FURI_LOG_E(
"ProtoPirate",
"Malloc failed: %s (%lu bytes)",
preset_data_index,
(unsigned long)preset_data_size);
break;
}
flipper_format_rewind(ff);
if(!flipper_format_read_hex(ff, preset_data_index, preset_data, preset_data_size)) {
break;
}
FURI_LOG_D(TAG, "Using Custom Preset Data from Models File...");
//Yay, we made it...
error = false;
}
}
}
if(!error) {
//Set the Car Model name and index.
furi_string_set((car_model)->name, model_name);
car_model->index = index;
//Alloc a preset if none, or recycle.
if(!car_model->preset) {
car_model->preset = malloc(sizeof(SubGhzRadioPreset));
car_model->preset->name = furi_string_alloc();
} else {
//Free old data..
if(car_model->preset->data) {
free(car_model->preset->data);
car_model->preset->data = NULL;
}
}
furi_string_set(car_model->preset->name, preset_name);
car_model->preset->data = preset_data;
car_model->preset->data_size = preset_data_size;
car_model->preset->frequency = frequency;
} else {
car_model->index = 0;
furi_string_set_str(
car_model->name,
(car_models_count) ? "< Select a Car Model >" : "No Models in Database");
if(car_model->preset) {
if(car_model->preset->data) {
free(car_model->preset->data);
car_model->preset->data = NULL;
}
car_model->preset->data_size = 0;
if(car_model->preset->name) {
furi_string_free(car_model->preset->name);
car_model->preset->name = NULL;
}
free(car_model->preset);
car_model->preset = NULL;
// If we allocated preset_data but never assigned it, free it
if(custom_preset && preset_data_size) {
free(preset_data);
}
}
}
//Close up and return.
flipper_format_free(ff);
furi_string_free(header);
furi_string_free(model_name);
furi_string_free(preset_name);
furi_record_close(RECORD_STORAGE);
return (!error);
}
uint16_t car_model_get_count(void) {
uint32_t version = 0;
uint32_t model_count = 0;
//Set up storage, init temp variables
Storage* storage = furi_record_open(RECORD_STORAGE);
FlipperFormat* ff = flipper_format_file_alloc(storage);
FuriString* header = furi_string_alloc();
//Open file, read header, check version, and grab the model count.
if(!flipper_format_file_open_existing(ff, PROTOPIRATE_MODELS_FILE)) {
FURI_LOG_W(TAG, "Failed to open " PROTOPIRATE_MODELS_FILE);
} else if(!flipper_format_read_header(ff, header, &version)) {
FURI_LOG_W(TAG, "Failed to read models file header.");
} else if(version != MODELS_FILE_VERSION || furi_string_cmp_str(header, MODELS_FILE_HEADER) != 0) {
FURI_LOG_W(TAG, "Models file version is invalid.");
} else if(!flipper_format_read_uint32(ff, MODELS_FIELD_MODEL_COUNT, &model_count, 1)) {
FURI_LOG_W(TAG, "Failed to read " MODELS_FIELD_MODEL_COUNT ".");
}
//Free temp variables, close storage.
furi_string_free(header);
flipper_format_free(ff);
furi_record_close(RECORD_STORAGE);
//Finished, return the count or 0 if error.
return (uint16_t)model_count;
}
@@ -0,0 +1,20 @@
#pragma once
#include <furi.h>
#include <lib/subghz/types.h>
#include "core/record.h"
#include <lib/subghz/subghz_setting.h>
typedef struct ProtoPirateCarModel {
uint16_t index;
FuriString* name;
SubGhzRadioPreset* preset;
int16_t last_preset_index;
} ProtoPirateCarModel;
bool car_model_get_by_index(
ProtoPirateCarModel* car_model,
uint16_t index,
uint16_t model_count,
SubGhzSetting* app_settings);
uint16_t car_model_get_count(void);
@@ -3,12 +3,12 @@
#include "../protocols/protocol_items.h"
#include <loader/firmware_api/firmware_api.h>
#include <stdio.h>
#include <string.h>
/* ProtoPirate private SubGhz API table (api/protopirate_api_table.cpp) — exposes
* the SubGhz lib symbols the protocol plugins import at runtime. */
extern const ElfApiInterface* const subghz_application_api_interface;
#include <stdio.h>
#include <string.h>
#define TAG "ProtoPirateProtocolPlugin"
#ifdef ENABLE_EMULATE_FEATURE
@@ -5,13 +5,13 @@
#include "../scenes/plugins/protopirate_psa_bf_plugin.h"
#include <loader/firmware_api/firmware_api.h>
#include <lib/flipper_application/plugins/plugin_manager.h>
#include <lib/flipper_application/plugins/composite_resolver.h>
#include <notification/notification_messages.h>
/* ProtoPirate private SubGhz API table (api/protopirate_api_table.cpp) — exposes
* the SubGhz lib symbols the protocol plugins import at runtime. */
extern const ElfApiInterface* const subghz_application_api_interface;
#include <lib/flipper_application/plugins/plugin_manager.h>
#include <lib/flipper_application/plugins/composite_resolver.h>
#include <notification/notification_messages.h>
#define TAG "ProtoPiratePsaBfHost"
#define PSA_BF_PLUGIN_PATH APP_ASSETS_PATH("plugins/protopirate_psa_bf_plugin.fal")
@@ -17,9 +17,12 @@ void protopirate_settings_set_defaults(ProtoPirateSettings* settings) {
settings->preset_index = 0;
settings->tx_power = 0;
settings->auto_save = false;
settings->sound = false;
settings->hopping_enabled = false;
settings->emulate_feature_enabled = false;
settings->check_saved = false;
settings->datetime_filenames = false;
settings->car_model_index = 0;
}
void protopirate_settings_load(ProtoPirateSettings* settings) {
@@ -118,15 +121,37 @@ void protopirate_settings_load(ProtoPirateSettings* settings) {
}
settings->check_saved = (check_saved_temp == 1);
uint32_t sound_temp = 0;
if(!flipper_format_read_uint32(ff, "Sound", &sound_temp, 1)) {
check_saved_temp = 0;
}
settings->sound = (sound_temp == 1);
// Read Date/Time file names.
uint32_t datetime_filenames_temp = 0;
if(!flipper_format_read_uint32(ff, "DateTimeFilenames", &datetime_filenames_temp, 1)) {
FURI_LOG_W(TAG, "Failed to read date-time filenames, using default");
datetime_filenames_temp = 0;
}
settings->datetime_filenames = (datetime_filenames_temp == 1);
// Read Selected Car Model
uint32_t car_model_index_temp = 0;
if(!flipper_format_read_uint32(ff, "CarModelIndex", &car_model_index_temp, 1)) {
car_model_index_temp = 0;
}
settings->car_model_index = car_model_index_temp;
FURI_LOG_I(
TAG,
"Settings loaded: freq=%lu, preset=%u, auto_save=%d, hopping=%d, emulate=%d, check_saved=%d",
"Settings loaded: freq=%lu, preset=%u, auto_save=%d, hopping=%d, emulate=%d, check_saved=%d, sound = %d",
settings->frequency,
settings->preset_index,
settings->auto_save,
settings->hopping_enabled,
settings->emulate_feature_enabled,
settings->check_saved);
settings->check_saved,
settings->sound);
} while(false);
@@ -200,19 +225,33 @@ void protopirate_settings_save(ProtoPirateSettings* settings) {
FURI_LOG_E(TAG, "Failed to write check saved");
break;
}
uint32_t sound_temp = settings->sound ? 1 : 0;
if(!flipper_format_write_uint32(ff, "Sound", &sound_temp, 1)) {
FURI_LOG_E(TAG, "Failed to write Sound.");
break;
}
uint32_t datetime_filenames_temp = settings->datetime_filenames ? 1 : 0;
if(!flipper_format_write_uint32(ff, "DateTimeFilenames", &datetime_filenames_temp, 1)) {
FURI_LOG_E(TAG, "Failed to write Date Time Filenames");
}
uint32_t car_model_index_temp = settings->car_model_index;
if(!flipper_format_write_uint32(ff, "CarModelIndex", &car_model_index_temp, 1)) {
FURI_LOG_E(TAG, "Failed to write car model");
break;
}
write_ok = true;
FURI_LOG_I(
TAG,
"Settings saved: freq=%lu, preset=%u, auto_save=%d, hopping=%d, emulate=%d, check_saved=%d",
"Settings saved: freq=%lu, preset=%u, auto_save=%d, hopping=%d, emulate=%d, check_saved=%d, sound=%d",
settings->frequency,
settings->preset_index,
settings->auto_save,
settings->hopping_enabled,
settings->emulate_feature_enabled,
settings->check_saved);
settings->check_saved,
settings->sound);
} while(false);
flipper_format_free(ff);
@@ -14,9 +14,12 @@ typedef struct {
uint8_t preset_index;
uint8_t tx_power;
bool auto_save;
bool sound;
bool hopping_enabled;
bool emulate_feature_enabled;
bool check_saved;
bool datetime_filenames;
uint16_t car_model_index;
} ProtoPirateSettings;
void protopirate_settings_load(ProtoPirateSettings* settings);
@@ -140,7 +140,10 @@ static void sanitize_filename(const char* input, char* output, size_t output_siz
output[j] = '\0';
}
bool protopirate_storage_get_next_filename(const char* protocol_name, FuriString* out_filename) {
bool protopirate_storage_get_next_filename(
const char* protocol_name,
FuriString* out_filename,
bool dont_add_zero) {
if(!protocol_name || !out_filename) return false;
Storage* storage = furi_record_open(RECORD_STORAGE);
FuriString* temp_path = furi_string_alloc();
@@ -151,13 +154,18 @@ bool protopirate_storage_get_next_filename(const char* protocol_name, FuriString
sanitize_filename(protocol_name, safe_name, sizeof(safe_name));
while(!found && index <= 999) {
furi_string_printf(
temp_path,
"%s/%s_%03lu%s",
PROTOPIRATE_APP_FOLDER,
safe_name,
(unsigned long)index,
PROTOPIRATE_APP_EXTENSION);
if(index == 0 && dont_add_zero) {
furi_string_printf(
temp_path, "%s/%s%s", PROTOPIRATE_APP_FOLDER, safe_name, PROTOPIRATE_APP_EXTENSION);
} else {
furi_string_printf(
temp_path,
"%s/%s_%03lu%s",
PROTOPIRATE_APP_FOLDER,
safe_name,
(unsigned long)index,
PROTOPIRATE_APP_EXTENSION);
}
if(!storage_file_exists(storage, furi_string_get_cstr(temp_path))) {
furi_string_set(out_filename, temp_path);
@@ -222,7 +230,6 @@ static const char* const protopirate_storage_tail_u32_fields[] = {
"Encrypted",
"Decrypted",
"KIAVersion",
"Checksum",
};
static bool protopirate_storage_fail(const char* action, const char* key) {
@@ -694,7 +701,8 @@ void protopirate_storage_delete_temp(void) {
bool protopirate_storage_save_capture(
FlipperFormat* flipper_format,
const char* protocol_name,
FuriString* out_path) {
FuriString* out_path,
bool datetime_filenames) {
furi_check(flipper_format);
furi_check(protocol_name);
furi_check(out_path);
@@ -706,7 +714,7 @@ bool protopirate_storage_save_capture(
FuriString* file_path = furi_string_alloc();
if(!protopirate_storage_get_next_filename(protocol_name, file_path)) {
if(!protopirate_storage_get_next_filename(protocol_name, file_path, datetime_filenames)) {
FURI_LOG_E(TAG, "Failed to get next filename");
furi_string_free(file_path);
return false;
@@ -24,7 +24,8 @@ bool protopirate_storage_commit_temp_file(
bool protopirate_storage_save_capture(
FlipperFormat* flipper_format,
const char* protocol_name,
FuriString* out_path);
FuriString* out_path,
bool datetime_filenames);
// Save a capture to a specific file path (user-chosen name)
bool protopirate_storage_save_capture_to_path(FlipperFormat* flipper_format, const char* full_path);
@@ -33,7 +34,10 @@ bool protopirate_storage_save_capture_to_path(FlipperFormat* flipper_format, con
void protopirate_storage_delete_temp(void);
// Get next available filename for a protocol
bool protopirate_storage_get_next_filename(const char* protocol_name, FuriString* out_filename);
bool protopirate_storage_get_next_filename(
const char* protocol_name,
FuriString* out_filename,
bool dont_add_zero);
bool protopirate_storage_get_capture_display_protocol(
FlipperFormat* flipper_format,
@@ -3,11 +3,11 @@
#include "radio_device_loader.h"
#include <loader/firmware_api/firmware_api.h>
#include <notification/notification_messages.h>
/* ProtoPirate private SubGhz API table (api/protopirate_api_table.cpp) — exposes
* the SubGhz lib symbols the protocol plugins import at runtime. */
extern const ElfApiInterface* const subghz_application_api_interface;
#include <notification/notification_messages.h>
#define TAG "ProtoPirateToolScene"
@@ -133,6 +133,13 @@ uint32_t protopirate_rx(ProtoPirateApp* app, uint32_t frequency) {
subghz_devices_idle(app->txrx->radio_device);
uint32_t value = subghz_devices_set_frequency(app->txrx->radio_device, frequency);
subghz_devices_flush_rx(app->txrx->radio_device);
//Enable the Speaker so I can hear the keys.
if(app->sound) {
if(furi_hal_speaker_is_mine() || furi_hal_speaker_acquire(1000)) {
app->txrx->radio_device->interconnect->set_async_mirror_pin(&gpio_speaker);
}
}
subghz_devices_set_rx(app->txrx->radio_device);
subghz_devices_start_async_rx(
@@ -169,6 +176,13 @@ void protopirate_rx_end(ProtoPirateApp* app) {
subghz_devices_idle(app->txrx->radio_device);
}
if(app->sound) {
if(furi_hal_speaker_is_mine()) {
subghz_devices_set_async_mirror_pin(app->txrx->radio_device, NULL);
furi_hal_speaker_release();
}
}
app->txrx->txrx_state = ProtoPirateTxRxStateIDLE;
}
@@ -44,6 +44,7 @@ typedef enum {
ProtoPirateCustomEventSubDecodeEmulate,
ProtoPirateCustomEventSubDecodeBruteforceStart,
ProtoPirateCustomEventPsaBruteforceComplete,
ProtoPirateCustomEventSubDecodeSaveConfirm,
// File Browser
ProtoPirateCustomEventSavedFileSelected,
// Need saving confirmation
@@ -5,24 +5,6 @@
#define TAG "ProtoPirateViews"
bool protopirate_ensure_variable_item_list(ProtoPirateApp* app) {
furi_check(app);
if(app->variable_item_list) {
return true;
}
app->variable_item_list = variable_item_list_alloc();
if(!app->variable_item_list) {
return false;
}
view_dispatcher_add_view(
app->view_dispatcher,
ProtoPirateViewVariableItemList,
variable_item_list_get_view(app->variable_item_list));
return true;
}
bool protopirate_ensure_widget(ProtoPirateApp* app) {
furi_check(app);
if(app->widget) {
@@ -101,7 +83,6 @@ void protopirate_views_free(ProtoPirateApp* app) {
if(app->variable_item_list) {
FURI_LOG_D(TAG, "Removing variable_item_list view");
view_dispatcher_remove_view(app->view_dispatcher, ProtoPirateViewVariableItemList);
variable_item_list_free(app->variable_item_list);
app->variable_item_list = NULL;
}
@@ -133,3 +114,11 @@ void protopirate_views_free(ProtoPirateApp* app) {
app->protopirate_receiver = NULL;
}
}
void protopirate_variable_item_list_free(ProtoPirateApp* app) {
if(app->variable_item_list) {
view_dispatcher_remove_view(app->view_dispatcher, ProtoPirateViewVariableItemList);
variable_item_list_free(app->variable_item_list);
app->variable_item_list = NULL;
}
}
@@ -10,3 +10,4 @@ bool protopirate_ensure_text_input(ProtoPirateApp* app);
bool protopirate_ensure_view_about(ProtoPirateApp* app);
bool protopirate_ensure_receiver_view(ProtoPirateApp* app);
void protopirate_views_free(ProtoPirateApp* app);
void protopirate_variable_item_list_free(ProtoPirateApp* app);
@@ -0,0 +1,688 @@
#include "helpers/variable_item_list.h"
#include <gui/elements.h>
#include <gui/canvas.h>
#include <furi.h>
#include <m-array.h>
#include <stdint.h>
/**************************************************************************************************/
/* ICON ASSETS */
/**************************************************************************************************/
struct Icon {
const uint16_t width;
const uint16_t height;
const uint8_t frame_count;
const uint8_t frame_rate;
const uint8_t* const* frames;
};
const uint8_t _I_WarningDolphin_45x42e_0[] = {
0x01, 0x00, 0xc6, 0x00, 0x00, 0x1c, 0x22, 0x04, 0x05, 0x7f, 0xfc, 0x1e, 0x20, 0x05, 0x1e, 0x04,
0x02, 0x30, 0x05, 0x29, 0x84, 0x02, 0xc1, 0x20, 0x02, 0x8c, 0x22, 0x01, 0x80, 0x02, 0x94, 0x10,
0x32, 0x30, 0x10, 0x10, 0x87, 0xca, 0x84, 0x03, 0x10, 0x42, 0x81, 0x48, 0x28, 0x38, 0x08, 0x04,
0x3e, 0x01, 0x84, 0x83, 0xe0, 0x30, 0x11, 0x08, 0x05, 0xa2, 0x11, 0x40, 0xa0, 0x4b, 0xc6, 0xc5,
0x40, 0xd0, 0x56, 0xe0, 0x10, 0x60, 0x29, 0x54, 0xf0, 0x10, 0x18, 0xf0, 0x14, 0x6b, 0xf6, 0x0c,
0x04, 0x3e, 0x40, 0x05, 0x12, 0x80, 0xc1, 0xe4, 0x01, 0xd2, 0xf8, 0x40, 0xe4, 0x18, 0x09, 0xf4,
0x03, 0xf1, 0x01, 0x90, 0x40, 0x28, 0x30, 0x0f, 0xe4, 0x00, 0x16, 0x24, 0x11, 0xbf, 0x01, 0x44,
0xee, 0x53, 0xf0, 0x29, 0xf0, 0x3e, 0x02, 0x91, 0x3b, 0x8c, 0xc3, 0x81, 0x13, 0x90, 0x48, 0x20,
0x3f, 0xf9, 0xfc, 0x42, 0x60, 0x05, 0x10, 0x98, 0x81, 0x56, 0x11, 0x38, 0x02, 0x9c, 0x1a, 0x31,
0x1e, 0x02, 0x8f, 0x02, 0x03, 0x1c, 0x90, 0xc0, 0x7c, 0x02, 0xf1, 0xce, 0x02, 0x07, 0x01, 0x1f,
0x80, 0x63, 0xa8, 0x08, 0x71, 0x3c, 0x8e, 0x39, 0x24, 0x40, 0x51, 0xc7, 0x81, 0x53, 0x0f, 0x3c,
0x02, 0x9d, 0x1e, 0x38, 0x29, 0x10, 0x29, 0x17, 0xc8, 0x0a, 0x32, 0x3a, 0x00, 0x14, 0x4b, 0xa2,
0x05, 0x58, 0x98, 0x15, 0x22, 0x20, 0x54, 0x84, 0x81, 0x50,
};
const uint8_t* const _I_WarningDolphin_45x42e[] = {_I_WarningDolphin_45x42e_0};
const Icon I_WarningDolphin_45x42e = {
.width = 45,
.height = 42,
.frame_count = 1,
.frame_rate = 0,
.frames = _I_WarningDolphin_45x42e};
const uint8_t _I_Lock_7x8e_0[] = {
0x00,
0x1c,
0x22,
0x22,
0x7f,
0x7f,
0x77,
0x7f,
0x3e,
};
const uint8_t* const _I_Lock_7x8e[] = {_I_Lock_7x8e_0};
const Icon I_Lock_7x8e =
{.width = 7, .height = 8, .frame_count = 1, .frame_rate = 0, .frames = _I_Lock_7x8e};
/**************************************************************************************************/
struct VariableItem {
FuriString* label;
uint8_t current_value_index;
FuriString* current_value_text;
uint8_t values_count;
VariableItemChangeCallback change_callback;
void* context;
bool locked;
FuriString* locked_message;
};
ARRAY_DEF(VariableItemArray, VariableItem, M_POD_OPLIST); //-V658
struct VariableItemList {
View* view;
VariableItemListEnterCallback callback;
void* context;
FuriTimer* scroll_timer;
FuriTimer* locked_timer;
};
typedef struct {
VariableItemArray_t items;
uint8_t position;
uint8_t window_position;
FuriString* header;
size_t scroll_counter;
bool locked_message_visible;
} VariableItemListModel;
static void variable_item_list_process_up(VariableItemList* variable_item_list);
static void variable_item_list_process_down(VariableItemList* variable_item_list);
static void variable_item_list_process_left(VariableItemList* variable_item_list);
static void variable_item_list_process_right(VariableItemList* variable_item_list);
static void variable_item_list_process_ok(VariableItemList* variable_item_list);
static size_t variable_item_list_items_on_screen(VariableItemListModel* model) {
size_t res = 4;
return (furi_string_empty(model->header)) ? res : res - 1;
}
static void variable_item_list_draw_callback(Canvas* canvas, void* _model) {
VariableItemListModel* model = _model;
const uint8_t item_height = 16;
uint8_t item_width = canvas_width(canvas) - 5;
canvas_clear(canvas);
if(!furi_string_empty(model->header)) {
canvas_set_font(canvas, FontPrimary);
canvas_draw_str(canvas, 4, 11, furi_string_get_cstr(model->header));
}
uint8_t position = 0;
VariableItemArray_it_t it;
canvas_set_font(canvas, FontSecondary);
for(VariableItemArray_it(it, model->items); !VariableItemArray_end_p(it);
VariableItemArray_next(it)) {
uint8_t item_position = position - model->window_position;
uint8_t items_on_screen = variable_item_list_items_on_screen(model);
uint8_t y_offset = furi_string_empty(model->header) ? 0 : item_height;
if(item_position < items_on_screen) {
const VariableItem* item = VariableItemArray_cref(it);
uint8_t item_y = y_offset + (item_position * item_height);
uint8_t item_text_y = item_y + item_height - 4;
size_t scroll_counter = 0;
if(position == model->position) {
canvas_set_color(canvas, ColorBlack);
elements_slightly_rounded_box(canvas, 0, item_y + 1, item_width, item_height - 2);
canvas_set_color(canvas, ColorWhite);
scroll_counter = model->scroll_counter;
if(scroll_counter < 1) { // Show text beginning a little longer
scroll_counter = 0;
} else {
scroll_counter -= 1;
}
} else {
canvas_set_color(canvas, ColorBlack);
}
uint8_t value_pos_x = 73;
uint8_t label_width = 66;
if(item->locked) {
// Span label up to lock icon
value_pos_x = 110;
label_width = 100;
} else if(item->current_value_index == 0 && furi_string_empty(item->current_value_text)) {
// Only label text, no value text, show longer label
label_width = 109;
} else if(furi_string_size(item->current_value_text) < 4U) {
// Smaller value section for short values
value_pos_x = 80;
label_width = 71;
}
elements_scrollable_text_line(
canvas,
6,
item_text_y,
label_width,
item->label,
scroll_counter,
(position != model->position));
if(item->locked) {
canvas_draw_icon(canvas, value_pos_x, item_text_y - 8, &I_Lock_7x8e);
} else {
if(item->current_value_index > 0) {
canvas_draw_str(canvas, value_pos_x, item_text_y, "<");
}
elements_scrollable_text_line(
canvas,
value_pos_x + 6,
item_text_y,
109 - (value_pos_x),
item->current_value_text,
scroll_counter,
false);
if(item->current_value_index < (item->values_count - 1)) {
canvas_draw_str(canvas, 115, item_text_y, ">");
}
}
}
position++;
}
elements_scrollbar(canvas, model->position, VariableItemArray_size(model->items));
if(model->locked_message_visible) {
//if(momentum_settings.popup_overlay) {
// canvas_draw_overlay(canvas);
//}
canvas_set_color(canvas, ColorWhite);
canvas_draw_box(canvas, 8, 10, 110, 48);
canvas_set_color(canvas, ColorBlack);
canvas_draw_icon(canvas, 10, 14, &I_WarningDolphin_45x42e);
canvas_draw_rframe(canvas, 8, 8, 112, 50, 3);
canvas_draw_rframe(canvas, 9, 9, 110, 48, 2);
elements_multiline_text_aligned(
canvas,
84,
32,
AlignCenter,
AlignCenter,
furi_string_get_cstr(
VariableItemArray_get(model->items, model->position)->locked_message));
}
}
void variable_item_list_set_selected_item(VariableItemList* variable_item_list, uint8_t index) {
furi_check(variable_item_list);
with_view_model(
variable_item_list->view,
VariableItemListModel * model,
{
uint8_t position = index;
const size_t items_count = VariableItemArray_size(model->items);
uint8_t items_on_screen = variable_item_list_items_on_screen(model);
if(position >= items_count) {
position = 0;
}
model->position = position;
model->window_position = position;
if(model->window_position > 0) {
model->window_position -= 1;
}
if(items_count <= items_on_screen) {
model->window_position = 0;
} else {
const size_t pos = items_count - items_on_screen;
if(model->window_position > pos) {
model->window_position = pos;
}
}
},
true);
}
/* uint8_t variable_item_list_get_selected_item_index(VariableItemList* variable_item_list) {
furi_check(variable_item_list);
VariableItemListModel* model = view_get_model(variable_item_list->view);
uint8_t idx = model->position;
view_commit_model(variable_item_list->view, false);
return idx;
}*/
/* void variable_item_list_set_header(VariableItemList* variable_item_list, const char* header) {
furi_check(variable_item_list);
with_view_model(
variable_item_list->view,
VariableItemListModel * model,
{
if(header == NULL) {
furi_string_reset(model->header);
} else {
furi_string_set_str(model->header, header);
}
},
true);
}*/
static bool variable_item_list_input_callback(InputEvent* event, void* context) {
VariableItemList* variable_item_list = context;
furi_assert(variable_item_list);
bool consumed = false;
bool locked_message_visible = false;
with_view_model(
variable_item_list->view,
VariableItemListModel * model,
{ locked_message_visible = model->locked_message_visible; },
false);
if((event->type != InputTypePress && event->type != InputTypeRelease) &&
locked_message_visible) {
with_view_model(
variable_item_list->view,
VariableItemListModel * model,
{ model->locked_message_visible = false; },
true);
consumed = true;
} else if(event->type == InputTypeShort) {
switch(event->key) {
case InputKeyUp:
consumed = true;
variable_item_list_process_up(variable_item_list);
break;
case InputKeyDown:
consumed = true;
variable_item_list_process_down(variable_item_list);
break;
case InputKeyLeft:
consumed = true;
variable_item_list_process_left(variable_item_list);
break;
case InputKeyRight:
consumed = true;
variable_item_list_process_right(variable_item_list);
break;
case InputKeyOk:
variable_item_list_process_ok(variable_item_list);
break;
default:
break;
}
} else if(event->type == InputTypeRepeat) {
switch(event->key) {
case InputKeyUp:
consumed = true;
variable_item_list_process_up(variable_item_list);
break;
case InputKeyDown:
consumed = true;
variable_item_list_process_down(variable_item_list);
break;
case InputKeyLeft:
consumed = true;
variable_item_list_process_left(variable_item_list);
break;
case InputKeyRight:
consumed = true;
variable_item_list_process_right(variable_item_list);
break;
default:
break;
}
}
return consumed;
}
void variable_item_list_process_up(VariableItemList* variable_item_list) {
with_view_model(
variable_item_list->view,
VariableItemListModel * model,
{
uint8_t items_on_screen = variable_item_list_items_on_screen(model);
if(model->position > 0) {
model->position--;
if((model->position == model->window_position) && (model->window_position > 0)) {
model->window_position--;
}
} else {
model->position = VariableItemArray_size(model->items) - 1;
if(model->position > (items_on_screen - 1)) {
model->window_position = model->position - (items_on_screen - 1);
}
}
model->scroll_counter = 0;
},
true);
}
void variable_item_list_process_down(VariableItemList* variable_item_list) {
with_view_model(
variable_item_list->view,
VariableItemListModel * model,
{
uint8_t items_on_screen = variable_item_list_items_on_screen(model);
if(model->position < (VariableItemArray_size(model->items) - 1)) {
model->position++;
if((model->position - model->window_position) > (items_on_screen - 2) &&
model->window_position <
(VariableItemArray_size(model->items) - items_on_screen)) {
model->window_position++;
}
} else {
model->position = 0;
model->window_position = 0;
}
model->scroll_counter = 0;
},
true);
}
VariableItem* variable_item_list_get_selected_item(VariableItemListModel* model) {
VariableItem* item = NULL;
VariableItemArray_it_t it;
uint8_t position = 0;
for(VariableItemArray_it(it, model->items); !VariableItemArray_end_p(it);
VariableItemArray_next(it)) {
if(position == model->position) {
break;
}
position++;
}
item = VariableItemArray_ref(it);
furi_assert(item);
return item;
}
void variable_item_list_process_left(VariableItemList* variable_item_list) {
with_view_model(
variable_item_list->view,
VariableItemListModel * model,
{
VariableItem* item = variable_item_list_get_selected_item(model);
if(item->locked) {
model->locked_message_visible = true;
furi_timer_start(
variable_item_list->locked_timer, furi_kernel_get_tick_frequency() * 3);
} else if(!item->values_count) {
//No List to change. Send VariableItemListEventCycleLeft to the handler as current value index
//This way no breaking API changes are made.
item->current_value_index = VariableItemListEventCycleLeft;
if(item->change_callback) {
item->change_callback(item);
}
} else if(item->current_value_index > 0) {
item->current_value_index--;
model->scroll_counter = 0;
if(item->change_callback) {
item->change_callback(item);
}
}
},
true);
}
void variable_item_list_process_right(VariableItemList* variable_item_list) {
with_view_model(
variable_item_list->view,
VariableItemListModel * model,
{
VariableItem* item = variable_item_list_get_selected_item(model);
if(item->locked) {
model->locked_message_visible = true;
furi_timer_start(
variable_item_list->locked_timer, furi_kernel_get_tick_frequency() * 3);
} else if(!item->values_count) {
//No List to change. Send VariableItemListEventCycleLeft to the handler as current value index
//This way no breaking API changes are made.
item->current_value_index = VariableItemListEventCycleRight;
if(item->change_callback) {
item->change_callback(item);
}
} else if(item->current_value_index < (item->values_count - 1)) {
item->current_value_index++;
model->scroll_counter = 0;
if(item->change_callback) {
item->change_callback(item);
}
}
},
true);
}
void variable_item_list_process_ok(VariableItemList* variable_item_list) {
with_view_model(
variable_item_list->view,
VariableItemListModel * model,
{
VariableItem* item = variable_item_list_get_selected_item(model);
if(item->locked) {
model->locked_message_visible = true;
furi_timer_start(
variable_item_list->locked_timer, furi_kernel_get_tick_frequency() * 3);
} else if(variable_item_list->callback) {
variable_item_list->callback(variable_item_list->context, model->position);
}
},
true);
}
static void variable_item_list_scroll_timer_callback(void* context) {
furi_assert(context);
VariableItemList* variable_item_list = context;
with_view_model(
variable_item_list->view,
VariableItemListModel * model,
{ model->scroll_counter++; },
true);
}
void variable_item_list_locked_timer_callback(void* context) {
furi_assert(context);
VariableItemList* variable_item_list = context;
with_view_model(
variable_item_list->view,
VariableItemListModel * model,
{ model->locked_message_visible = false; },
true);
}
VariableItemList* variable_item_list_alloc(void) {
VariableItemList* variable_item_list = malloc(sizeof(VariableItemList));
variable_item_list->view = view_alloc();
view_set_context(variable_item_list->view, variable_item_list);
view_allocate_model(
variable_item_list->view, ViewModelTypeLocking, sizeof(VariableItemListModel));
view_set_draw_callback(variable_item_list->view, variable_item_list_draw_callback);
view_set_input_callback(variable_item_list->view, variable_item_list_input_callback);
variable_item_list->locked_timer = furi_timer_alloc(
variable_item_list_locked_timer_callback, FuriTimerTypeOnce, variable_item_list);
with_view_model(
variable_item_list->view,
VariableItemListModel * model,
{
VariableItemArray_init(model->items);
model->position = 0;
model->window_position = 0;
model->header = furi_string_alloc();
model->scroll_counter = 0;
},
true);
variable_item_list->scroll_timer = furi_timer_alloc(
variable_item_list_scroll_timer_callback, FuriTimerTypePeriodic, variable_item_list);
furi_timer_start(variable_item_list->scroll_timer, 333);
return variable_item_list;
}
void variable_item_list_free(VariableItemList* variable_item_list) {
furi_check(variable_item_list);
with_view_model(
variable_item_list->view,
VariableItemListModel * model,
{
furi_string_free(model->header);
VariableItemArray_it_t it;
for(VariableItemArray_it(it, model->items); !VariableItemArray_end_p(it);
VariableItemArray_next(it)) {
furi_string_free(VariableItemArray_ref(it)->label);
furi_string_free(VariableItemArray_ref(it)->current_value_text);
furi_string_free(VariableItemArray_ref(it)->locked_message);
}
VariableItemArray_clear(model->items);
},
false);
furi_timer_stop(variable_item_list->scroll_timer);
furi_timer_free(variable_item_list->scroll_timer);
furi_timer_stop(variable_item_list->locked_timer);
furi_timer_free(variable_item_list->locked_timer);
view_free(variable_item_list->view);
free(variable_item_list);
variable_item_list = NULL;
}
void variable_item_list_reset(VariableItemList* variable_item_list) {
furi_check(variable_item_list);
with_view_model(
variable_item_list->view,
VariableItemListModel * model,
{
VariableItemArray_it_t it;
for(VariableItemArray_it(it, model->items); !VariableItemArray_end_p(it);
VariableItemArray_next(it)) {
furi_string_free(VariableItemArray_ref(it)->label);
furi_string_free(VariableItemArray_ref(it)->current_value_text);
furi_string_free(VariableItemArray_ref(it)->locked_message);
}
VariableItemArray_reset(model->items);
furi_string_reset(model->header);
},
false);
}
View* variable_item_list_get_view(VariableItemList* variable_item_list) {
furi_check(variable_item_list);
return variable_item_list->view;
}
VariableItem* variable_item_list_add(
VariableItemList* variable_item_list,
const char* label,
uint8_t values_count,
VariableItemChangeCallback change_callback,
void* context) {
VariableItem* item = NULL;
furi_check(label);
furi_check(variable_item_list);
with_view_model(
variable_item_list->view,
VariableItemListModel * model,
{
item = VariableItemArray_push_new(model->items);
item->label = furi_string_alloc_set(label);
item->values_count = values_count;
item->change_callback = change_callback;
item->context = context;
item->current_value_index = 0;
item->current_value_text = furi_string_alloc();
item->locked = false;
item->locked_message = furi_string_alloc();
},
true);
return item;
}
VariableItem* variable_item_list_get(VariableItemList* variable_item_list, uint8_t position) {
furi_check(variable_item_list);
VariableItem* item = NULL;
with_view_model(
variable_item_list->view,
VariableItemListModel * model,
{
if(position < VariableItemArray_size(model->items)) {
item = VariableItemArray_get(model->items, position);
}
},
true);
return item;
}
void variable_item_list_set_enter_callback(
VariableItemList* variable_item_list,
VariableItemListEnterCallback callback,
void* context) {
furi_check(callback);
with_view_model(
variable_item_list->view,
VariableItemListModel * model,
{
UNUSED(model);
variable_item_list->callback = callback;
variable_item_list->context = context;
},
false);
}
void variable_item_set_current_value_index(VariableItem* item, uint8_t current_value_index) {
furi_check(item);
item->current_value_index = current_value_index;
}
/*void variable_item_set_values_count(VariableItem* item, uint8_t values_count) {
furi_check(item);
item->values_count = values_count;
}*/
void variable_item_set_item_label(VariableItem* item, const char* label) {
furi_check(item);
furi_check(label);
furi_string_set(item->label, label);
}
void variable_item_set_current_value_text(VariableItem* item, const char* current_value_text) {
furi_check(item);
furi_string_set(item->current_value_text, current_value_text);
}
void variable_item_set_locked(VariableItem* item, bool locked, const char* locked_message) {
furi_check(item);
item->locked = locked;
if(locked_message) {
furi_string_set(item->locked_message, locked_message);
} else if(locked && furi_string_empty(item->locked_message)) {
furi_string_set(item->locked_message, "Locked!");
}
}
uint8_t variable_item_get_current_value_index(VariableItem* item) {
furi_check(item);
return item->current_value_index;
}
void* variable_item_get_context(VariableItem* item) {
furi_check(item);
return item->context;
}
@@ -0,0 +1,159 @@
/**
* @file variable_item_list.h
* GUI: VariableItemList view module API
*/
#pragma once
#include <gui/view.h>
#ifdef __cplusplus
extern "C" {
#endif
//For sending Left and Right events up to app
//When there are ZERO values (no cycling used by list itself)
//This allows easy use of lists that cycle the whole line,
//And use more than 255 values.
typedef enum {
VariableItemListEventCycleReset,
VariableItemListEventCycleLeft,
VariableItemListEventCycleRight,
} VariableItemListEvents;
typedef struct VariableItemList VariableItemList;
typedef struct VariableItem VariableItem;
typedef void (*VariableItemChangeCallback)(VariableItem* item);
typedef void (*VariableItemListEnterCallback)(void* context, uint32_t index);
/** Allocate and initialize VariableItemList
*
* @return VariableItemList*
*/
VariableItemList* variable_item_list_alloc(void);
/** Deinitialize and free VariableItemList
*
* @param variable_item_list VariableItemList instance
*/
void variable_item_list_free(VariableItemList* variable_item_list);
/** Clear all elements from list
*
* @param variable_item_list VariableItemList instance
*/
void variable_item_list_reset(VariableItemList* variable_item_list);
/** Get VariableItemList View instance
*
* @param variable_item_list VariableItemList instance
*
* @return View instance
*/
View* variable_item_list_get_view(VariableItemList* variable_item_list);
/** Add item to VariableItemList
*
* @param variable_item_list VariableItemList instance
* @param label item name
* @param values_count item values count
* @param change_callback called on value change in gui
* @param context item context
*
* @return VariableItem* item instance
*/
VariableItem* variable_item_list_add(
VariableItemList* variable_item_list,
const char* label,
uint8_t values_count,
VariableItemChangeCallback change_callback,
void* context);
/** Get pre-existing item instance in VariableItemList
*
* @param variable_item_list VariableItemList instance
* @param position index of the item instance to get
*
* @return VariableItem* item instance
*/
VariableItem* variable_item_list_get(VariableItemList* variable_item_list, uint8_t position);
/** Set enter callback
*
* @param variable_item_list VariableItemList instance
* @param callback VariableItemListEnterCallback instance
* @param context pointer to context
*/
void variable_item_list_set_enter_callback(
VariableItemList* variable_item_list,
VariableItemListEnterCallback callback,
void* context);
void variable_item_list_set_selected_item(VariableItemList* variable_item_list, uint8_t index);
//uint8_t variable_item_list_get_selected_item_index(VariableItemList* variable_item_list);
/** Set optional header for variable item list
* Must be called before adding items OR after adding items and before set_selected_item()
*
* @param variable_item_list VariableItemList instance
* @param header header to set
*/
// void variable_item_list_set_header(VariableItemList* variable_item_list, const char* header);
/** Set item current selected index
*
* @param item VariableItem* instance
* @param current_value_index The current value index
*/
void variable_item_set_current_value_index(VariableItem* item, uint8_t current_value_index);
/** Set number of values for item
*
* @param item VariableItem* instance
* @param values_count The new values count
*/
//void variable_item_set_values_count(VariableItem* item, uint8_t values_count);
/** Set new label for item
*
* @param item VariableItem* instance
* @param label The new label text
*/
void variable_item_set_item_label(VariableItem* item, const char* label);
/** Set item current selected text
*
* @param item VariableItem* instance
* @param current_value_text The current value text
*/
void variable_item_set_current_value_text(VariableItem* item, const char* current_value_text);
/** Set item locked state and text
* Locked message is saved, you can set_locked(item, false, "Message") and then set_locked(item, true, NULL), it will use previous locked message
*
* @param item VariableItem* instance
* @param locked Is item locked boolean
* @param locked_message The locked message text
*/
void variable_item_set_locked(VariableItem* item, bool locked, const char* locked_message);
/** Get item current selected index
*
* @param item VariableItem* instance
*
* @return uint8_t current selected index
*/
uint8_t variable_item_get_current_value_index(VariableItem* item);
/** Get item context
*
* @param item VariableItem* instance
*
* @return void* item context
*/
void* variable_item_get_context(VariableItem* item);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,33 @@
Filetype: ProtoPirate Car Models Database
Version: 2
ModelCount: 7
ModelName1: 433.92 Mhz AM650
Frequency1: 433920000
PresetName1: FuriHalSubGhzPresetOok650Async
ModelName2: 433.92 Mhz FM476
Frequency2: 433920000
PresetName2: FuriHalSubGhzPreset2FSKDev476Async
ModelName3: 315 Mhz AM650
Frequency3: 315000000
PresetName3: FuriHalSubGhzPresetOok650Async
ModelName4: 315 Mhz FM476
Frequency4: 315000000
PresetName4: FuriHalSubGhzPreset2FSKDev476Async
ModelName5: Ford Falcon BA-FGX
Frequency5: 433920000
PresetName5: FuriHalSubGhzPresetOok650Async
ModelName6: Ford Territory SX-SYII
Frequency6: 433920000
PresetName6: FuriHalSubGhzPresetOok650Async
ModelName7: Example With Honda Preset
Frequency7: 433920000
PresetName7: FuriHalSubGhzPresetCustom
CustomPresetData7: 02 0D 0B 06 08 32 07 04 14 00 13 02 12 07 11 36 10 E9 15 32 18 18 19 16 1D 92 1C 40 1B 03 20 FB 22 10 21 56 00 00 C0 00 00 00 00 00 00 00
@@ -301,11 +301,10 @@ static void fiat_v1_decode_fields(SubGhzProtocolDecoderFiatV1* instance) {
fiat_v1_verify_hitag2_key(instance);
}
static bool
fiat_v1_commit(
SubGhzProtocolDecoderFiatV1* instance,
const uint8_t raw[FIAT_V1_WIRE_BYTES],
uint8_t te_variant) {
static bool fiat_v1_commit(
SubGhzProtocolDecoderFiatV1* instance,
const uint8_t raw[FIAT_V1_WIRE_BYTES],
uint8_t te_variant) {
if(!fiat_v1_frame_valid(raw)) {
return false;
}
@@ -833,9 +833,9 @@ void subghz_protocol_decoder_ford_v0_get_string(void* context, FuriString* outpu
"Key2: %04X"
" Sn: %08lX\r\n"
"Cnt: %05lX"
" Checksum: %02X"
" CRC: %02X\r\n"
" Btn: %02X - %s\r\n",
" Checksum: %02X\r\n"
"Btn: %02X - %s"
" CRC: %02X",
instance->generic.protocol_name,
instance->generic.data_count_bit,
crc_ok ? "OK" : "BAD",
@@ -846,7 +846,7 @@ void subghz_protocol_decoder_ford_v0_get_string(void* context, FuriString* outpu
(unsigned long)instance->count,
(instance->key2 >> 8) & 0xFF,
instance->key2 & 0xFF,
instance->button,
button_name);
button_name,
instance->key2 & 0xFF);
}
@@ -29,7 +29,7 @@ static const SubGhzBlockConst kia_protocol_v0_const = {
#define KIA_V0_SUZUKI_GAP 2000U
#define KIA_V0_SUZUKI_GAP_SPAN 500U
#define KIA_V0_TYPE1_SYNC 750U
#define KIA_V0_TYPE1_SYNC 750U
#define KIA_V0_TYPE1_PREAMBLE_PAIRS 0x13FU
#define KIA_V0_TYPE2_PREAMBLE_PAIRS 0x140U
#define KIA_V0_TAIL_PREAMBLE_PAIRS 0x0FU
@@ -37,7 +37,7 @@ static const SubGhzBlockConst kia_protocol_v0_const = {
#define KIA_V0_MITSU_PREAMBLE_MIN 72U
#define KIA_V0_MITSU_PREAMBLE_MAX 88U
#define KIA_V0_UPLOAD_CAPACITY \
#define KIA_V0_UPLOAD_CAPACITY \
((KIA_V0_TYPE2_PREAMBLE_PAIRS * 2U) + 2U + (KIA_V0_BIT_COUNT_SUZUKI * 2U) + 3U + \
(KIA_V0_MITSU_TAIL_PREAMBLE_PAIRS * 2U) + (KIA_V0_BIT_COUNT_SUZUKI * 2U) + 4U)
_Static_assert(
@@ -9,16 +9,16 @@
_Static_assert(
RENAULT_V0_UPLOAD_CAPACITY <= PP_SHARED_UPLOAD_CAPACITY,
"RENAULT_V0_UPLOAD_CAPACITY exceeds shared upload slab");
#define RENAULT_V0_TE_SHORT_US 0x7DU
#define RENAULT_V0_TE_LONG_US 0xFAU
#define RENAULT_V0_TE_DELTA_US 0x45U
#define RENAULT_V0_PREAMBLE_PAIRS 16U
#define RENAULT_V0_BURST_COUNT 3U
#define RENAULT_V0_INTER_BURST_US 0x61A8U
#define RENAULT_V0_FINAL_LOW_US 250U
#define RENAULT_V0_SYNC_HIGH_US 1000U
#define RENAULT_V0_REPEAT 1U
#define RENAULT_V0_KEY2_FIELD "Key2"
#define RENAULT_V0_TE_SHORT_US 0x7DU
#define RENAULT_V0_TE_LONG_US 0xFAU
#define RENAULT_V0_TE_DELTA_US 0x45U
#define RENAULT_V0_PREAMBLE_PAIRS 16U
#define RENAULT_V0_BURST_COUNT 3U
#define RENAULT_V0_INTER_BURST_US 0x61A8U
#define RENAULT_V0_FINAL_LOW_US 250U
#define RENAULT_V0_SYNC_HIGH_US 1000U
#define RENAULT_V0_REPEAT 1U
#define RENAULT_V0_KEY2_FIELD "Key2"
typedef enum {
RenaultV0DecoderStepReset = 0,
@@ -268,8 +268,8 @@ static bool renault_v0_emit(
bool level,
uint32_t duration) {
const size_t prev = *index;
*index = pp_emit_merge(
instance->encoder.upload, prev, RENAULT_V0_UPLOAD_CAPACITY, level, duration);
*index =
pp_emit_merge(instance->encoder.upload, prev, RENAULT_V0_UPLOAD_CAPACITY, level, duration);
if(*index > prev) {
return true;
}
@@ -7,14 +7,14 @@
#define TAG "RenaultV1Protocol"
#define HITAG2_TE_US 125U
#define HITAG2_HEADER_LOW_US 1500U
#define HITAG2_HEADER_HIGH_US 1000U
#define HITAG2_SHORT_GAP_US 21500U
#define HITAG2_PREAMBLE_PAIRS 250U
#define HITAG2_LONG_FRAMES 3U
#define HITAG2_SHORT_FRAMES 3U
#define HITAG2_UPLOAD_CAPACITY 1295U
#define HITAG2_TE_US 125U
#define HITAG2_HEADER_LOW_US 1500U
#define HITAG2_HEADER_HIGH_US 1000U
#define HITAG2_SHORT_GAP_US 21500U
#define HITAG2_PREAMBLE_PAIRS 250U
#define HITAG2_LONG_FRAMES 3U
#define HITAG2_SHORT_FRAMES 3U
#define HITAG2_UPLOAD_CAPACITY 1295U
_Static_assert(
HITAG2_UPLOAD_CAPACITY <= PP_SHARED_UPLOAD_CAPACITY,
"HITAG2_UPLOAD_CAPACITY exceeds shared upload slab");
@@ -65,6 +65,8 @@ struct SubGhzProtocolDecoderRenaultV1 {
uint64_t last_data;
uint64_t last_data_2;
bool last_frame_valid;
uint32_t seed;
uint64_t data_2;
};
#if PROTOPIRATE_WITH_ENCODER
@@ -74,6 +76,8 @@ struct SubGhzProtocolEncoderRenaultV1 {
SubGhzProtocolBlockEncoder encoder;
SubGhzBlockGeneric generic;
uint32_t seed;
uint64_t data_2;
uint8_t recovered;
uint8_t hitag2_key[6];
uint8_t tail_bits;
@@ -87,7 +91,6 @@ typedef enum {
RenaultV1DecoderStepData = 3,
} RenaultV1DecoderStep;
const SubGhzProtocolDecoder renault_v1_decoder = {
.alloc = subghz_protocol_decoder_renault_v1_alloc,
.free = pp_decoder_free_default,
@@ -213,11 +216,8 @@ static uint64_t hitag2_key_to_u64(const uint8_t key[6]) {
return key64;
}
static uint32_t hitag2_authenticator(
uint32_t uid,
uint8_t button,
uint32_t counter,
const uint8_t key[6]) {
static uint32_t
hitag2_authenticator(uint32_t uid, uint8_t button, uint32_t counter, const uint8_t key[6]) {
const uint64_t key64 = hitag2_key_to_u64(key);
const uint32_t nonce = (counter << 4U) | ((uint32_t)button & 0x0FU);
uint64_t state = 0ULL;
@@ -395,15 +395,16 @@ static void hitag2_serial_permute(const uint8_t serial[4], uint8_t perm[6]) {
perm[1] = acc;
const uint8_t sn0_shr3 = (uint8_t)(sn0 >> 3);
acc = (uint8_t)(hitag2_extract_bits(sn0, 2, 1) | (sn0_shr3 & 2U) | ((~(uint32_t)(sn3 >> 2)) & 4U) |
(sn1 & 0x10U));
acc = (uint8_t)(hitag2_extract_bits(sn0, 2, 1) | (sn0_shr3 & 2U) |
((~(uint32_t)(sn3 >> 2)) & 4U) | (sn1 & 0x10U));
acc |= (uint8_t)((~(uint32_t)(sn1 << 4)) & 0x20U);
acc |= (uint8_t)(sn3_inv_shl3 & 0x40U);
acc |= 0x80U;
perm[2] = acc;
const uint8_t sn2_inv_shl6 = (uint8_t) ~(sn2 << 6);
acc = (uint8_t)(((sn0 >> 2) & 2U) | ((sn1 >> 3) & 8U) | hitag2_extract_bits(sn2 ^ 0x20U, 5, 1));
acc =
(uint8_t)(((sn0 >> 2) & 2U) | ((sn1 >> 3) & 8U) | hitag2_extract_bits(sn2 ^ 0x20U, 5, 1));
acc |= (uint8_t)((sn1 << 5) & 0x20U);
acc |= (uint8_t)((sn3 << 1) & 0x40U);
acc |= (uint8_t)(((uint8_t)~sn0_shr3) & 4U);
@@ -640,9 +641,9 @@ static void hitag2_apply_check_remote(
static void renault_v1_check_remote_controller(SubGhzProtocolDecoderRenaultV1* instance) {
hitag2_apply_check_remote(
instance->generic.data,
instance->generic.data_2,
instance->data_2,
instance->recovered,
instance->generic.seed,
instance->seed,
&instance->generic.serial,
&instance->generic.btn,
&instance->generic.cnt,
@@ -659,8 +660,7 @@ static bool hitag2_encoder_next_frame(
uint32_t orig_hop,
uint8_t tail) {
uint8_t raw[11];
if(!hitag2_key_matches_hop(
instance->hitag2_key, orig_uid, orig_btn, orig_cnt10, orig_hop)) {
if(!hitag2_key_matches_hop(instance->hitag2_key, orig_uid, orig_btn, orig_cnt10, orig_hop)) {
return false;
}
@@ -673,7 +673,7 @@ static bool hitag2_encoder_next_frame(
uint32_t hop = hitag2_authenticator(uid, tx_btn, cnt10, instance->hitag2_key);
hitag2_pack_auth_frame(uid, tx_btn, cnt10, hop, tail, raw);
hitag2_apply_raw(raw, &instance->generic.data, &instance->generic.data_2);
hitag2_apply_raw(raw, &instance->generic.data, &instance->data_2);
instance->generic.serial = uid;
instance->generic.btn = tx_btn;
instance->generic.cnt = cnt10;
@@ -771,7 +771,7 @@ static bool renault_v1_encoder_get_upload(SubGhzProtocolEncoderRenaultV1* instan
size_t index = 0;
LevelDuration* upload = instance->encoder.upload;
const uint64_t key = instance->generic.data;
const uint64_t key_2 = instance->generic.data_2 & 0xFFFFFFULL;
const uint64_t key_2 = instance->data_2 & 0xFFFFFFULL;
for(size_t i = 0; i < HITAG2_PREAMBLE_PAIRS; i++) {
if(!hitag2_encoder_add_level(upload, &index, true, HITAG2_TE_US)) return false;
@@ -808,9 +808,7 @@ SubGhzProtocolStatus
do {
ret = subghz_block_generic_deserialize_check_count_bit(
&instance->generic,
flipper_format,
renault_v1_const.min_count_bit_for_found);
&instance->generic, flipper_format, renault_v1_const.min_count_bit_for_found);
if(ret != SubGhzProtocolStatusOk) {
break;
}
@@ -820,10 +818,9 @@ SubGhzProtocolStatus
ret = SubGhzProtocolStatusErrorParserOthers;
break;
}
instance->generic.data_2 = hitag2_bytes_to_u64_be(key2, sizeof(key2));
instance->data_2 = hitag2_bytes_to_u64_be(key2, sizeof(key2));
hitag2_read_recovered_and_seed(
flipper_format, &instance->recovered, &instance->generic.seed);
hitag2_read_recovered_and_seed(flipper_format, &instance->recovered, &instance->seed);
if(!flipper_format_rewind(flipper_format)) {
ret = SubGhzProtocolStatusErrorParserOthers;
@@ -837,7 +834,7 @@ SubGhzProtocolStatus
uint16_t wire_cnt10 = 0;
hitag2_unpack_frame(
instance->generic.data,
instance->generic.data_2,
instance->data_2,
&wire_serial,
&wire_btn,
&wire_cnt10,
@@ -845,9 +842,9 @@ SubGhzProtocolStatus
&tail);
hitag2_apply_check_remote(
instance->generic.data,
instance->generic.data_2,
instance->data_2,
instance->recovered,
instance->generic.seed,
instance->seed,
&instance->generic.serial,
&instance->generic.btn,
&instance->generic.cnt,
@@ -892,14 +889,13 @@ SubGhzProtocolStatus
instance->generic.serial,
instance->generic.cnt,
instance->generic.btn,
instance->generic.seed,
instance->seed,
out,
iv);
instance->generic.data = hitag2_bytes_to_u64_be(out, 8);
instance->generic.data_2 = ((uint64_t)out[8] << 16U) | ((uint64_t)out[9] << 8U) |
out[10];
instance->generic.seed = ((uint32_t)iv[0] << 24U) | ((uint32_t)iv[1] << 16U) |
((uint32_t)iv[2] << 8U) | iv[3];
instance->data_2 = ((uint64_t)out[8] << 16U) | ((uint64_t)out[9] << 8U) | out[10];
instance->seed = ((uint32_t)iv[0] << 24U) | ((uint32_t)iv[1] << 16U) |
((uint32_t)iv[2] << 8U) | iv[3];
} else if(!instance->hitag2_key_valid && instance->recovered != HITAG2_RECOVERED_YES) {
ret = SubGhzProtocolStatusErrorParserOthers;
break;
@@ -918,12 +914,14 @@ SubGhzProtocolStatus
}
uint8_t key_data[8];
hitag2_u64_to_bytes_be(instance->generic.data, key_data, 8);
if(!flipper_format_insert_or_update_hex(flipper_format, FF_KEY, key_data, sizeof(key_data))) {
if(!flipper_format_insert_or_update_hex(
flipper_format, FF_KEY, key_data, sizeof(key_data))) {
ret = SubGhzProtocolStatusErrorParserOthers;
break;
}
hitag2_u64_to_bytes_be(instance->generic.data_2, key_data, 8);
if(!flipper_format_insert_or_update_hex(flipper_format, "Key_2", key_data, sizeof(key_data))) {
hitag2_u64_to_bytes_be(instance->data_2, key_data, 8);
if(!flipper_format_insert_or_update_hex(
flipper_format, "Key_2", key_data, sizeof(key_data))) {
ret = SubGhzProtocolStatusErrorParserOthers;
break;
}
@@ -934,7 +932,7 @@ SubGhzProtocolStatus
ret = SubGhzProtocolStatusErrorParserOthers;
break;
}
if(!hitag2_write_hex_be(flipper_format, "Seed", instance->generic.seed, 4)) {
if(!hitag2_write_hex_be(flipper_format, "Seed", instance->seed, 4)) {
ret = SubGhzProtocolStatusErrorParserOthers;
break;
}
@@ -943,7 +941,7 @@ SubGhzProtocolStatus
{
uint32_t hop = 0;
hitag2_unpack_frame(
instance->generic.data, instance->generic.data_2, NULL, NULL, NULL, &hop, NULL);
instance->generic.data, instance->data_2, NULL, NULL, NULL, &hop, NULL);
hitag2_write_named_fields(
flipper_format,
instance->generic.serial,
@@ -1028,14 +1026,14 @@ static bool hitag2_accept_frame(SubGhzProtocolDecoderRenaultV1* instance, uint64
return false;
}
instance->generic.data_2 = key_2;
instance->data_2 = key_2;
instance->generic.data_count_bit = HITAG2_MIN_COUNT_BIT;
instance->recovered = 0;
instance->generic.seed = 0;
instance->seed = 0;
uint16_t cnt10 = 0;
hitag2_unpack_frame(
instance->generic.data,
instance->generic.data_2,
instance->data_2,
&instance->generic.serial,
&instance->generic.btn,
&cnt10,
@@ -1184,26 +1182,26 @@ SubGhzProtocolStatus subghz_protocol_decoder_renault_v1_serialize(
const uint32_t serial = instance->generic.serial;
const uint8_t btn = instance->generic.btn;
const uint32_t cnt = instance->generic.cnt;
const uint32_t seed = instance->generic.seed;
const uint64_t data_2 = instance->generic.data_2;
const uint32_t seed = instance->seed;
const uint64_t data_2 = instance->data_2;
instance->generic.serial = 0;
instance->generic.btn = 0;
instance->generic.cnt = 0;
instance->generic.seed = 0;
instance->generic.data_2 = 0;
instance->seed = 0;
instance->data_2 = 0;
SubGhzProtocolStatus ret =
subghz_block_generic_serialize(&instance->generic, flipper_format, preset);
instance->generic.serial = serial;
instance->generic.btn = btn;
instance->generic.cnt = cnt;
instance->generic.seed = seed;
instance->generic.data_2 = data_2;
instance->seed = seed;
instance->data_2 = data_2;
if(ret != SubGhzProtocolStatusOk) {
return ret;
}
ret = hitag2_write_extra_fields(
flipper_format, instance->generic.data_2, instance->recovered, instance->generic.seed);
flipper_format, instance->data_2, instance->recovered, instance->seed);
if(ret != SubGhzProtocolStatusOk) {
return ret;
}
@@ -1225,9 +1223,7 @@ SubGhzProtocolStatus
do {
ret = subghz_block_generic_deserialize_check_count_bit(
&instance->generic,
flipper_format,
renault_v1_const.min_count_bit_for_found);
&instance->generic, flipper_format, renault_v1_const.min_count_bit_for_found);
if(ret != SubGhzProtocolStatusOk) {
break;
}
@@ -1237,10 +1233,9 @@ SubGhzProtocolStatus
ret = SubGhzProtocolStatusErrorParserOthers;
break;
}
instance->generic.data_2 = hitag2_bytes_to_u64_be(key2, sizeof(key2));
instance->data_2 = hitag2_bytes_to_u64_be(key2, sizeof(key2));
hitag2_read_recovered_and_seed(
flipper_format, &instance->recovered, &instance->generic.seed);
hitag2_read_recovered_and_seed(flipper_format, &instance->recovered, &instance->seed);
renault_v1_check_remote_controller(instance);
instance->hitag2_key_valid = false;
@@ -1270,7 +1265,7 @@ void subghz_protocol_decoder_renault_v1_get_string(void* context, FuriString* ou
"%s\r\nK1:%016llX\r\nK2:%06llX Sn:%08lX\r\nBtn:%02X [%s] %db",
instance->generic.protocol_name,
(unsigned long long)instance->generic.data,
(unsigned long long)(instance->generic.data_2 & 0xFFFFFFULL),
(unsigned long long)(instance->data_2 & 0xFFFFFFULL),
(unsigned long)instance->generic.serial,
instance->generic.btn,
hitag2_get_button_name(instance->generic.btn),
@@ -1280,7 +1275,7 @@ void subghz_protocol_decoder_renault_v1_get_string(void* context, FuriString* ou
furi_string_cat_printf(
output,
"\r\nIV:%08lX Cnt:%04lX",
(unsigned long)instance->generic.seed,
(unsigned long)instance->seed,
(unsigned long)(instance->generic.cnt & 0xFFFFU));
} else if(instance->recovered == HITAG2_RECOVERED_BF_MISS) {
furi_string_cat_printf(output, "\r\nBF not found");
@@ -1472,21 +1467,19 @@ bool hitag2_bf_state_from_flipper_format(Hitag2BfState* state, FlipperFormat* ff
return true;
}
bool hitag2_bf_needs_bruteforce(FlipperFormat* ff, bool require_renault_v1) {
bool hitag2_bf_needs_bruteforce(FlipperFormat* ff) {
if(!ff) {
return false;
}
FuriString* s = furi_string_alloc();
flipper_format_rewind(ff);
if(require_renault_v1) {
if(!flipper_format_read_string(ff, FF_PROTOCOL, s) ||
furi_string_cmp_str(s, RENAULT_PROTOCOL_V1_NAME) != 0) {
furi_string_free(s);
return false;
}
flipper_format_rewind(ff);
if(!flipper_format_read_string(ff, FF_PROTOCOL, s) ||
furi_string_cmp_str(s, RENAULT_PROTOCOL_V1_NAME) != 0) {
furi_string_free(s);
return false;
}
flipper_format_rewind(ff);
bool has_key = flipper_format_read_string(ff, FF_KEY, s);
furi_string_free(s);
@@ -1515,8 +1508,8 @@ bool hitag2_bf_patch_flipper_format_on_success(FlipperFormat* ff, const Hitag2Bf
const uint32_t seed = hitag2_seed_from_iv(state->iv);
const uint64_t data = hitag2_bytes_to_u64_be(state->frame, 8);
const uint64_t data_2 = ((uint64_t)state->frame[8] << 16U) | ((uint64_t)state->frame[9] << 8U) |
state->frame[10];
const uint64_t data_2 = ((uint64_t)state->frame[8] << 16U) |
((uint64_t)state->frame[9] << 8U) | state->frame[10];
uint32_t serial = 0;
uint32_t cnt = 0;
uint32_t hop = 0;
@@ -1557,8 +1550,8 @@ bool hitag2_flipper_format_get_string(FlipperFormat* ff, FuriString* output) {
return false;
}
const bool ok =
subghz_protocol_decoder_renault_v1_deserialize(decoder, ff) == SubGhzProtocolStatusOk;
const bool ok = subghz_protocol_decoder_renault_v1_deserialize(decoder, ff) ==
SubGhzProtocolStatusOk;
if(ok) {
furi_string_reset(output);
subghz_protocol_decoder_renault_v1_get_string(decoder, output);
@@ -37,7 +37,7 @@ struct Hitag2BfState {
int32_t hitag2_brute_force_thread_entry(void* arg);
bool hitag2_bf_state_from_flipper_format(Hitag2BfState* state, FlipperFormat* ff);
bool hitag2_bf_needs_bruteforce(FlipperFormat* ff, bool require_renault_v1);
bool hitag2_bf_needs_bruteforce(FlipperFormat* ff);
bool hitag2_bf_patch_flipper_format_on_success(FlipperFormat* ff, const Hitag2BfState* state);
bool hitag2_bf_patch_flipper_format_on_miss(FlipperFormat* ff);
bool hitag2_flipper_format_get_string(FlipperFormat* ff, FuriString* output);
@@ -12,6 +12,73 @@
#define TAG "ProtoPirateApp"
// -----------------------------------------------------------------------------
// Plugin load / unload
// -----------------------------------------------------------------------------
void config_plugin_unload(ProtoPirateApp* app) {
furi_check(app);
app->config_plugin = NULL;
if(app->plugin_manager) {
plugin_manager_free(app->plugin_manager);
app->plugin_manager = NULL;
}
if(app->plugin_resolver) {
composite_api_resolver_free(app->plugin_resolver);
app->plugin_resolver = NULL;
}
}
bool config_plugin_load(ProtoPirateApp* app) {
furi_check(app);
if(app->config_plugin) return true;
if(app->plugin_manager || app->plugin_resolver) {
config_plugin_unload(app);
}
CompositeApiResolver* resolver = composite_api_resolver_alloc();
if(!resolver) {
FURI_LOG_E(TAG, "Failed to allocate config plugin resolver");
return false;
}
composite_api_resolver_add(resolver, firmware_api_interface);
PluginManager* manager = plugin_manager_alloc(
PROTOPIRATE_CONFIG_PLUGIN_APP_ID,
PROTOPIRATE_CONFIG_PLUGIN_API_VERSION,
composite_api_resolver_get(resolver));
if(!manager) {
FURI_LOG_E(TAG, "Failed to allocate config plugin manager");
composite_api_resolver_free(resolver);
return false;
}
PluginManagerError error = plugin_manager_load_single(manager, CONFIG_PLUGIN_PATH);
if(error != PluginManagerErrorNone) {
FURI_LOG_E(TAG, "Failed to load config plugin %s: %d", CONFIG_PLUGIN_PATH, (int)error);
plugin_manager_free(manager);
composite_api_resolver_free(resolver);
return false;
}
const ProtoPirateConfigPlugin* plugin = plugin_manager_get_ep(manager, 0U);
if(!plugin || !plugin->on_enter) {
FURI_LOG_E(TAG, "Config plugin entry point is invalid");
plugin_manager_free(manager);
composite_api_resolver_free(resolver);
return false;
}
app->plugin_resolver = resolver;
app->plugin_manager = manager;
app->config_plugin = plugin;
return true;
}
static bool protopirate_app_custom_event_callback(void* context, uint32_t event) {
furi_check(context);
ProtoPirateApp* app = context;
@@ -88,8 +155,10 @@ ProtoPirateApp* protopirate_app_alloc() {
// Apply auto-save setting
app->auto_save = settings.auto_save;
app->sound = settings.sound;
app->check_saved = settings.check_saved;
app->tx_power = settings.tx_power;
app->datetime_filenames = settings.datetime_filenames;
#ifdef ENABLE_EMULATE_FEATURE
app->emulate_feature_enabled = settings.emulate_feature_enabled;
#else
@@ -152,7 +221,33 @@ ProtoPirateApp* protopirate_app_alloc() {
settings.auto_save,
settings.hopping_enabled);
protopirate_preset_init(app, preset_name, frequency, preset_data, preset_data_size);
config_plugin_load(app);
app->car_models_count = app->config_plugin->car_model_get_count();
app->selected_model = malloc(sizeof(ProtoPirateCarModel));
app->selected_model->name = furi_string_alloc();
app->selected_model->preset = NULL; // important initialization
app->selected_model->index = 0; // optional but clean
app->variable_item_list = NULL;
//Grab selected car model.
if(settings.car_model_index) {
app->config_plugin->car_model_get_by_index(
app->selected_model, settings.car_model_index, app->car_models_count, app->setting);
app->selected_model->last_preset_index = settings.preset_index;
protopirate_preset_init(
app,
furi_string_get_cstr(app->selected_model->preset->name),
app->selected_model->preset->frequency,
app->selected_model->preset->data,
app->selected_model->preset->data_size);
} else {
app->config_plugin->car_model_get_by_index(
app->selected_model, 0, app->car_models_count, app->setting);
protopirate_preset_init(app, preset_name, frequency, preset_data, preset_data_size);
}
config_plugin_unload(app);
// Apply hopping state from settings
app->txrx->hopper_state = settings.hopping_enabled ? ProtoPirateHopperStateRunning :
@@ -176,8 +271,10 @@ void protopirate_app_free(ProtoPirateApp* app) {
ProtoPirateSettings settings;
settings.frequency = app->txrx->preset->frequency;
settings.auto_save = app->auto_save;
settings.sound = app->sound;
settings.check_saved = app->check_saved;
settings.tx_power = app->tx_power;
settings.datetime_filenames = app->datetime_filenames;
settings.hopping_enabled = (app->txrx->hopper_state != ProtoPirateHopperStateOFF);
#ifdef ENABLE_EMULATE_FEATURE
settings.emulate_feature_enabled = app->emulate_feature_enabled;
@@ -185,16 +282,48 @@ void protopirate_app_free(ProtoPirateApp* app) {
settings.emulate_feature_enabled = false;
#endif
// Find current preset index
settings.preset_index = 0;
const char* current_preset = furi_string_get_cstr(app->txrx->preset->name);
for(uint8_t i = 0; i < subghz_setting_get_preset_count(app->setting); i++) {
if(strcmp(subghz_setting_get_preset_name(app->setting, i), current_preset) == 0) {
settings.preset_index = i;
break;
//Get the selected Model, and get the preset to save.
if(app->selected_model && app->selected_model->index) {
//Get Preset Index before model was selected.
settings.car_model_index = app->selected_model->index;
settings.preset_index = app->selected_model->last_preset_index;
} else {
// Find current preset index
settings.car_model_index = 0; //Clear the selected model.
settings.preset_index = 0;
const char* current_preset = furi_string_get_cstr(app->txrx->preset->name);
for(uint8_t i = 0; i < subghz_setting_get_preset_count(app->setting); i++) {
if(strcmp(subghz_setting_get_preset_name(app->setting, i), current_preset) == 0) {
settings.preset_index = i;
break;
}
}
}
//Free the Model Name
furi_string_free(app->selected_model->name);
app->selected_model->index = 0;
//Free the preset information.
if(app->selected_model->preset) {
//Free the preset data
if((app->selected_model)->preset->data) {
free(app->selected_model->preset->data);
app->selected_model->preset->data = NULL;
}
//Free the Preset name
furi_string_free(app->selected_model->preset->name);
//Free the preset.
free(app->selected_model->preset);
app->selected_model->preset = NULL;
}
//Free the Model.
free(app->selected_model);
app->selected_model = NULL;
FURI_LOG_I(
TAG,
"Saving settings: freq=%lu, preset=%u, auto_save=%d, hopping=%d, emulate=%d",
@@ -3,7 +3,6 @@
#include <stddef.h>
#include "helpers/protopirate_types.h"
#include "helpers/protopirate_settings.h"
#include "scenes/protopirate_scene.h"
#include "views/protopirate_receiver.h"
#include "protopirate_history.h"
@@ -13,7 +12,7 @@
#include <gui/view_dispatcher.h>
#include <gui/scene_manager.h>
#include <gui/modules/submenu.h>
#include <gui/modules/variable_item_list.h>
#include <gui/modules/widget.h>
#include <gui/modules/text_input.h>
#include <notification/notification_messages.h>
@@ -33,16 +32,22 @@
#ifdef ENABLE_EMULATE_FEATURE
#include "scenes/plugins/protopirate_emulate_plugin.h"
#endif
#include "scenes/plugins/protopirate_config_plugin.h"
#include "scenes/plugins/protopirate_psa_bf_plugin.h"
#include "scenes/plugins/protopirate_tool_scene_plugin.h"
#include "helpers/protopirate_views.h"
#include "helpers/protopirate_radio.h"
#include "helpers/protopirate_protocol_plugin_host.h"
#include "helpers/protopirate_txrx.h"
#include "helpers/protopirate_models.h"
#include <loader/firmware_api/firmware_api.h>
#include "helpers/protopirate_settings.h"
#define CONFIG_PLUGIN_PATH APP_ASSETS_PATH("plugins/protopirate_config_plugin.fal")
#define PROTOPIRATE_KEYSTORE_DIR_NAME APP_ASSETS_PATH("encrypted")
typedef struct ProtoPirateApp ProtoPirateApp;
typedef struct VariableItemList VariableItemList;
typedef struct ProtoPirateTxRx {
SubGhzWorker* worker;
@@ -83,6 +88,8 @@ struct ProtoPirateApp {
FuriString* loaded_file_path;
bool auto_save;
bool check_saved;
bool sound;
bool datetime_filenames;
bool radio_initialized;
uint32_t start_tx_time;
uint8_t tx_power;
@@ -92,15 +99,16 @@ struct ProtoPirateApp {
bool save_from_saved_info;
bool emulate_disabled_for_loaded;
bool emulate_feature_enabled;
CompositeApiResolver* plugin_resolver;
PluginManager* plugin_manager;
#ifdef ENABLE_EMULATE_FEATURE
#define EMULATE_NAV_NONE 0U
#define EMULATE_NAV_POP 1U
#define EMULATE_NAV_STOP_APP 2U
CompositeApiResolver* emulate_plugin_resolver;
PluginManager* emulate_plugin_manager;
const ProtoPirateEmulatePlugin* emulate_plugin;
uint8_t emulate_nav_pending;
#endif
const ProtoPirateConfigPlugin* config_plugin;
CompositeApiResolver* psa_bf_plugin_resolver;
PluginManager* psa_bf_plugin_manager;
const ProtoPiratePsaBfPlugin* psa_bf_plugin;
@@ -108,13 +116,15 @@ struct ProtoPirateApp {
PluginManager* tool_scene_plugin_manager;
const ProtoPirateToolScenePlugin* tool_scene_plugin;
ProtoPirateToolScenePluginKind tool_scene_plugin_kind;
#define TOOL_SCENE_NAV_NONE 0U
#define TOOL_SCENE_NAV_POP 1U
#define TOOL_SCENE_NAV_NEXT 2U
#define TOOL_SCENE_NAV_SEARCH_PREVIOUS 3U
uint8_t tool_scene_nav_pending;
uint32_t tool_scene_nav_target;
ProtoPirateCarModel* selected_model;
uint32_t car_models_count;
};
#ifdef ENABLE_EMULATE_FEATURE
@@ -131,6 +141,9 @@ bool protopirate_tool_scene_on_event(void* app, SceneManagerEvent event);
void protopirate_tool_scene_on_exit(void* app);
void protopirate_tool_scene_plugin_release(ProtoPirateApp* app);
bool config_plugin_load(ProtoPirateApp* app);
void config_plugin_unload(ProtoPirateApp* app);
void protopirate_app_free(ProtoPirateApp* app);
static const NotificationSequence sequence_tx = {
@@ -0,0 +1,494 @@
#include "protopirate_config_plugin.h"
#include "../../protopirate_app_i.h"
#include "../../helpers/protopirate_models.h"
#define ON_OFF_COUNT 2
const char* const on_off_text[ON_OFF_COUNT] = {
"OFF",
"ON",
};
const uint32_t hopping_value[ON_OFF_COUNT] = {
ProtoPirateHopperStateOFF,
ProtoPirateHopperStateRunning,
};
const char* const sequence_time_text[ON_OFF_COUNT] = {
"Sequential",
"Time",
};
#ifdef ENABLE_EMULATE_FEATURE
#define TX_POWER_COUNT 9
const char* const tx_power_text[TX_POWER_COUNT] = {
"Preset",
"10dBm +",
"7dBm",
"5dBm",
"0dBm",
"-10dBm",
"-15dBm",
"-20dBm",
"-30dBm",
};
#endif
bool protopirate_ensure_variable_item_list(ProtoPirateApp* app) {
furi_check(app);
if(app->variable_item_list) {
view_dispatcher_remove_view(app->view_dispatcher, ProtoPirateViewVariableItemList);
variable_item_list_free(app->variable_item_list);
//Reassigned below... app->variable_item_list = NULL;
}
app->variable_item_list = variable_item_list_alloc();
if(!app->variable_item_list) {
return false;
}
view_dispatcher_add_view(
app->view_dispatcher,
ProtoPirateViewVariableItemList,
variable_item_list_get_view(app->variable_item_list));
return true;
}
static void protopirate_scene_receiver_config_set_frequency(VariableItem* item) {
ProtoPirateApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
if(app->txrx->hopper_state == ProtoPirateHopperStateOFF) {
char text_buf[10] = {0};
snprintf(
text_buf,
sizeof(text_buf),
"%lu.%02lu",
subghz_setting_get_frequency(app->setting, index) / 1000000,
(subghz_setting_get_frequency(app->setting, index) % 1000000) / 10000);
variable_item_set_current_value_text(item, text_buf);
app->txrx->preset->frequency = subghz_setting_get_frequency(app->setting, index);
} else {
variable_item_set_current_value_index(
item, subghz_setting_get_frequency_default_index(app->setting));
}
}
static void protopirate_scene_receiver_config_set_preset(VariableItem* item) {
ProtoPirateApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
variable_item_set_current_value_text(
item, subghz_setting_get_preset_name(app->setting, index));
protopirate_preset_init(
app,
subghz_setting_get_preset_name(app->setting, index),
app->txrx->preset->frequency,
subghz_setting_get_preset_data(app->setting, index),
subghz_setting_get_preset_data_size(app->setting, index));
if(!protopirate_refresh_protocol_registry(app, false)) {
notification_message(app->notifications, &sequence_error);
}
}
static void protopirate_scene_receiver_config_set_hopping_running(VariableItem* item) {
ProtoPirateApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
variable_item_set_current_value_text(item, on_off_text[index]);
if(hopping_value[index] == ProtoPirateHopperStateOFF) {
char text_buf[10] = {0};
snprintf(
text_buf,
sizeof(text_buf),
"%lu.%02lu",
subghz_setting_get_default_frequency(app->setting) / 1000000,
(subghz_setting_get_default_frequency(app->setting) % 1000000) / 10000);
variable_item_set_current_value_text(
(VariableItem*)scene_manager_get_scene_state(
app->scene_manager, ProtoPirateSceneReceiverConfig),
text_buf);
app->txrx->preset->frequency = subghz_setting_get_default_frequency(app->setting);
variable_item_set_current_value_index(
(VariableItem*)scene_manager_get_scene_state(
app->scene_manager, ProtoPirateSceneReceiverConfig),
subghz_setting_get_frequency_default_index(app->setting));
} else {
variable_item_set_current_value_text(
(VariableItem*)scene_manager_get_scene_state(
app->scene_manager, ProtoPirateSceneReceiverConfig),
" -----");
variable_item_set_current_value_index(
(VariableItem*)scene_manager_get_scene_state(
app->scene_manager, ProtoPirateSceneReceiverConfig),
subghz_setting_get_frequency_default_index(app->setting));
}
app->txrx->hopper_state = hopping_value[index];
}
static void protopirate_scene_receiver_config_set_model(VariableItem* item) {
ProtoPirateApp* app = variable_item_get_context(item);
uint8_t direction = variable_item_get_current_value_index(item);
//Get current selection
uint16_t model_index;
model_index =
(app->selected_model && app->selected_model->index) ? app->selected_model->index : 0;
uint16_t old_model_index = model_index;
//Cycle the index.
switch(direction) {
case VariableItemListEventCycleReset: {
model_index = 0;
break;
}
case VariableItemListEventCycleLeft: {
if(model_index > 0)
model_index--;
else
model_index = app->car_models_count;
break;
}
case VariableItemListEventCycleRight: {
if(model_index < app->car_models_count)
model_index++;
else
model_index = 0;
}
}
//Get a Car Model object, and dont forget to shut down on app free!
car_model_get_by_index(
(ProtoPirateCarModel*)app->selected_model,
model_index,
app->car_models_count,
app->setting);
//Add he car model the list, with the correct selection and text.
variable_item_set_item_label(item, furi_string_get_cstr(app->selected_model->name));
variable_item_set_current_value_text(item, "");
variable_item_set_current_value_index(item, 0);
//Restore Original Preset.
VariableItem* freq_menu =
variable_item_list_get(app->variable_item_list, ProtoPirateSettingIndexFrequency);
VariableItem* hop_menu =
variable_item_list_get(app->variable_item_list, ProtoPirateSettingIndexHopping);
VariableItem* preset_menu =
variable_item_list_get(app->variable_item_list, ProtoPirateSettingIndexModulation);
//set the Preset, Frequency and Hopper off or restore.
if(model_index) {
//Save Original Preset.
if(!old_model_index) {
if(!strcmp(furi_string_get_cstr(app->txrx->preset->name), "Custom")) {
app->selected_model->last_preset_index = 0;
} else {
app->selected_model->last_preset_index = subghz_setting_get_inx_preset_by_name(
app->setting, furi_string_get_cstr(app->txrx->preset->name));
}
}
protopirate_preset_init(
app,
furi_string_get_cstr(app->selected_model->preset->name),
app->selected_model->preset->frequency,
app->selected_model->preset->data,
app->selected_model->preset->data_size);
variable_item_set_current_value_text(freq_menu, "Locked");
variable_item_set_current_value_text(hop_menu, "Locked");
app->txrx->hopper_state = ProtoPirateHopperStateOFF;
//app->txrx->preset->frequency = app->selected_model->preset->frequency;
} else {
//Restore Original Preset.
protopirate_scene_receiver_config_set_frequency(freq_menu);
protopirate_scene_receiver_config_set_hopping_running(hop_menu);
protopirate_preset_init(
app,
subghz_setting_get_preset_name(app->setting, app->selected_model->last_preset_index),
app->txrx->preset->frequency,
subghz_setting_get_preset_data(app->setting, app->selected_model->last_preset_index),
subghz_setting_get_preset_data_size(
app->setting, app->selected_model->last_preset_index));
variable_item_set_current_value_text(
preset_menu,
subghz_setting_get_preset_name(app->setting, app->selected_model->last_preset_index));
}
//Refresh the protocol registry now that we have a new Modulation Type.
if(!protopirate_refresh_protocol_registry(app, false)) {
notification_message(app->notifications, &sequence_error);
}
//Lock or Unlock the menus.
bool lock = (app->selected_model->index != 0);
variable_item_set_locked(freq_menu, lock, "Turn off\nCar Model\nto do that!");
variable_item_set_locked(hop_menu, lock, "Turn off\nCar Model\nto do that!");
variable_item_set_locked(preset_menu, lock, "Turn off\nCar Model\nto do that!");
}
static uint8_t
protopirate_scene_receiver_config_next_frequency(const uint32_t value, void* context) {
furi_check(context);
ProtoPirateApp* app = context;
uint8_t index = 0;
for(uint8_t i = 0; i < subghz_setting_get_frequency_count(app->setting); i++) {
if(value == subghz_setting_get_frequency(app->setting, i)) {
index = i;
break;
} else {
index = subghz_setting_get_frequency_default_index(app->setting);
}
}
return index;
}
static uint8_t
protopirate_scene_receiver_config_next_preset(const char* preset_name, void* context) {
furi_check(context);
ProtoPirateApp* app = context;
uint8_t index = 0;
for(uint8_t i = 0; i < subghz_setting_get_preset_count(app->setting); i++) {
if(!strcmp(subghz_setting_get_preset_name(app->setting, i), preset_name)) {
index = i;
break;
}
}
return index;
}
static uint8_t protopirate_scene_receiver_config_hopper_value_index(
const uint32_t value,
const uint32_t values[],
uint8_t values_count,
void* context) {
furi_check(context);
UNUSED(values_count);
ProtoPirateApp* app = context;
if(value == values[0]) {
return 0;
} else {
variable_item_set_current_value_text(
(VariableItem*)scene_manager_get_scene_state(
app->scene_manager, ProtoPirateSceneReceiverConfig),
" -----");
return 1;
}
}
static void protopirate_scene_receiver_config_set_auto_save(VariableItem* item) {
ProtoPirateApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
app->auto_save = (index == 1);
variable_item_set_current_value_text(item, on_off_text[index]);
}
static void protopirate_scene_receiver_config_set_datetime_filenames(VariableItem* item) {
ProtoPirateApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
app->datetime_filenames = (index == 1);
variable_item_set_current_value_text(item, sequence_time_text[index]);
}
static void protopirate_scene_receiver_config_set_check_saved(VariableItem* item) {
ProtoPirateApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
app->check_saved = (index == 1);
variable_item_set_current_value_text(item, on_off_text[index]);
}
static void protopirate_scene_receiver_config_set_sound(VariableItem* item) {
ProtoPirateApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
app->sound = (index == 1);
variable_item_set_current_value_text(item, on_off_text[index]);
}
#ifdef ENABLE_EMULATE_FEATURE
static void protopirate_scene_receiver_config_set_tx_power(VariableItem* item) {
ProtoPirateApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
app->tx_power = index;
variable_item_set_current_value_text(item, tx_power_text[index]);
}
#endif
static void
protopirate_scene_receiver_config_var_list_enter_callback(void* context, uint32_t index) {
furi_check(context);
ProtoPirateApp* app = context;
switch(index) {
case ProtoPirateSettingIndexCarModel: {
//Reset the Models Menu
VariableItem* model_menu =
variable_item_list_get(app->variable_item_list, ProtoPirateSettingIndexCarModel);
variable_item_set_current_value_index(model_menu, 0);
protopirate_scene_receiver_config_set_model(model_menu);
break;
}
case ProtoPirateSettingIndexLock: {
view_dispatcher_send_custom_event(
app->view_dispatcher, ProtoPirateCustomEventSceneSettingLock);
break;
}
}
}
static void plugin_on_enter(void* context) {
ProtoPirateApp* app = context;
VariableItem* item;
uint8_t value_index;
if(!protopirate_ensure_variable_item_list(app)) {
notification_message(app->notifications, &sequence_error);
scene_manager_previous_scene(app->scene_manager);
return;
}
//Add he car model the list, with the correct selection and text.
item = variable_item_list_add(
app->variable_item_list,
furi_string_get_cstr(app->selected_model->name),
0, //Plus NONE
protopirate_scene_receiver_config_set_model,
app);
item = variable_item_list_add(
app->variable_item_list,
"Frequency:",
subghz_setting_get_frequency_count(app->setting),
protopirate_scene_receiver_config_set_frequency,
app);
value_index =
protopirate_scene_receiver_config_next_frequency(app->txrx->preset->frequency, app);
scene_manager_set_scene_state(
app->scene_manager, ProtoPirateSceneReceiverConfig, (uint32_t)item);
variable_item_set_current_value_index(item, value_index);
char text_buf[10] = {0};
snprintf(
text_buf,
sizeof(text_buf),
"%lu.%02lu",
subghz_setting_get_frequency(app->setting, value_index) / 1000000,
(subghz_setting_get_frequency(app->setting, value_index) % 1000000) / 10000);
variable_item_set_current_value_text(item, text_buf);
variable_item_set_locked(
item,
app->selected_model && (app->selected_model->index),
"Turn off\nCar Model\nto do that!");
item = variable_item_list_add(
app->variable_item_list,
"Hopping:",
ON_OFF_COUNT,
protopirate_scene_receiver_config_set_hopping_running,
app);
value_index = protopirate_scene_receiver_config_hopper_value_index(
app->txrx->hopper_state, hopping_value, ON_OFF_COUNT, app);
variable_item_set_current_value_index(item, value_index);
variable_item_set_current_value_text(item, on_off_text[value_index]);
variable_item_set_locked(
item,
app->selected_model && (app->selected_model->index),
"Turn off\nCar Model\nto do that!");
item = variable_item_list_add(
app->variable_item_list,
"Modulation:",
subghz_setting_get_preset_count(app->setting),
protopirate_scene_receiver_config_set_preset,
app);
value_index = protopirate_scene_receiver_config_next_preset(
furi_string_get_cstr(app->txrx->preset->name), app);
variable_item_set_current_value_index(item, value_index);
variable_item_set_current_value_text(
item, subghz_setting_get_preset_name(app->setting, value_index));
variable_item_set_locked(
item,
app->selected_model && (app->selected_model->index),
"Turn off\nCar Model\nto do that!");
#ifdef ENABLE_EMULATE_FEATURE
// TX power option
item = variable_item_list_add(
app->variable_item_list,
"TX Power:",
TX_POWER_COUNT,
protopirate_scene_receiver_config_set_tx_power,
app);
variable_item_set_current_value_index(item, app->tx_power);
variable_item_set_current_value_text(item, tx_power_text[app->tx_power]);
#endif
// Auto-save option
item = variable_item_list_add(
app->variable_item_list,
"Auto-Save:",
ON_OFF_COUNT,
protopirate_scene_receiver_config_set_auto_save,
app);
variable_item_set_current_value_index(item, app->auto_save ? 1 : 0);
variable_item_set_current_value_text(item, on_off_text[app->auto_save ? 1 : 0]);
// Date/time filenames option
item = variable_item_list_add(
app->variable_item_list,
"Filenames:",
2,
protopirate_scene_receiver_config_set_datetime_filenames,
app);
variable_item_set_current_value_index(item, app->datetime_filenames ? 1 : 0);
variable_item_set_current_value_text(
item, sequence_time_text[app->datetime_filenames ? 1 : 0]);
item = variable_item_list_add(
app->variable_item_list,
"Check Saved:",
ON_OFF_COUNT,
protopirate_scene_receiver_config_set_check_saved,
app);
variable_item_set_current_value_index(item, app->check_saved ? 1 : 0);
variable_item_set_current_value_text(item, on_off_text[app->check_saved ? 1 : 0]);
// Sound option
item = variable_item_list_add(
app->variable_item_list,
"Sound:",
ON_OFF_COUNT,
protopirate_scene_receiver_config_set_sound,
app);
variable_item_set_current_value_index(item, app->sound);
variable_item_set_current_value_text(item, on_off_text[app->sound ? 1 : 0]);
variable_item_list_add(app->variable_item_list, "Lock Keyboard", 1, NULL, NULL);
variable_item_list_set_enter_callback(
app->variable_item_list, protopirate_scene_receiver_config_var_list_enter_callback, app);
view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewVariableItemList);
}
static const ProtoPirateConfigPlugin protopirate_config_plugin = {
.plugin_name = "ProtoPirate Config",
.car_model_get_by_index = car_model_get_by_index,
.car_model_get_count = car_model_get_count,
.on_enter = plugin_on_enter,
};
static const FlipperAppPluginDescriptor protopirate_config_plugin_descriptor = {
.appid = PROTOPIRATE_CONFIG_PLUGIN_APP_ID,
.ep_api_version = PROTOPIRATE_CONFIG_PLUGIN_API_VERSION,
.entry_point = &protopirate_config_plugin,
};
const FlipperAppPluginDescriptor* protopirate_config_plugin_ep(void) {
return &protopirate_config_plugin_descriptor;
}
@@ -0,0 +1,34 @@
#pragma once
#include "helpers/protopirate_models.h"
#include <lib/flipper_application/flipper_application.h>
#include "helpers/variable_item_list.h"
#define PROTOPIRATE_CONFIG_PLUGIN_APP_ID "protopirate_config_plugin"
#define PROTOPIRATE_CONFIG_PLUGIN_API_VERSION 2U
enum ProtoPirateSettingIndex {
ProtoPirateSettingIndexCarModel,
ProtoPirateSettingIndexFrequency,
ProtoPirateSettingIndexHopping,
ProtoPirateSettingIndexModulation,
#ifdef ENABLE_EMULATE_FEATURE
ProtoPirateSettingIndexTXPower,
#endif
ProtoPirateSettingIndexAutoSave,
ProtoPirateSettingIndexDateTimeFilenames,
ProtoPirateSettingIndexSound,
ProtoPirateSettingIndexCheckSaved,
ProtoPirateSettingIndexLock,
};
typedef struct ProtoPirateConfigPlugin {
const char* plugin_name;
bool (*car_model_get_by_index)(
ProtoPirateCarModel* car_model,
uint16_t index,
uint16_t model_count,
SubGhzSetting* app_settings);
uint16_t (*car_model_get_count)(void);
void (*on_enter)(void* app);
} ProtoPirateConfigPlugin;
@@ -145,8 +145,7 @@ static bool emulate_write_hitag2_key(FlipperFormat* flipper_format, const uint8_
return false;
}
flipper_format_rewind(flipper_format);
if(!flipper_format_insert_or_update_hex(
flipper_format, EMU_PRESET_KEY_HITAG2_KEY, key, 6U)) {
if(!flipper_format_insert_or_update_hex(flipper_format, EMU_PRESET_KEY_HITAG2_KEY, key, 6U)) {
return false;
}
uint32_t epoch = 0U;
@@ -313,7 +312,7 @@ static const uint8_t tx_power_value[TX_PRESET_VALUES_COUNT] = {
0x0E, // -20dBm
0x12, // -30dBm
// AM Power Values for 1st PA Table Byte.
// AM Power Values for 2nd PA Table Byte.
0xC0, // 12dBm
0xCD, // 7dBm
0x86, // 5dBm
@@ -936,7 +935,15 @@ static void emulate_draw_callback(Canvas* canvas, void* model) {
(ctx->freq % 1000000) / 10000);
canvas_draw_str(canvas, 2, 30, info_str);
snprintf(info_str, sizeof(info_str), "CNT:%04lX", (unsigned long)ctx->current_counter);
if(ctx->current_counter > 0xFFFF) {
snprintf(
info_str,
sizeof(info_str),
"CNT:...%03lX",
(unsigned long)ctx->current_counter & 0xFFF);
} else {
snprintf(info_str, sizeof(info_str), "CNT:%04lX", (unsigned long)ctx->current_counter);
}
canvas_draw_str(canvas, 68, 20, info_str);
if(ctx->current_counter > ctx->original_counter) {
@@ -1200,14 +1207,13 @@ static void plugin_on_enter(void* context) {
furi_string_equal(ctx->protocol_name, "Mitsu v0") ||
furi_string_equal(ctx->protocol_name, "Mitsubishi V0") ||
furi_string_equal(ctx->protocol_name, "Mitsubishi v0")) {
uint32_t kia_v0_type =
furi_string_equal(ctx->protocol_name, "Honda V0") ? 3U :
(furi_string_equal(ctx->protocol_name, "Mitsu V0") ||
furi_string_equal(ctx->protocol_name, "Mitsu v0") ||
furi_string_equal(ctx->protocol_name, "Mitsubishi V0") ||
furi_string_equal(ctx->protocol_name, "Mitsubishi v0")) ?
4U :
2U;
uint32_t kia_v0_type = furi_string_equal(ctx->protocol_name, "Honda V0") ? 3U :
(furi_string_equal(ctx->protocol_name, "Mitsu V0") ||
furi_string_equal(ctx->protocol_name, "Mitsu v0") ||
furi_string_equal(ctx->protocol_name, "Mitsubishi V0") ||
furi_string_equal(ctx->protocol_name, "Mitsubishi v0")) ?
4U :
2U;
furi_string_set(ctx->protocol_name, KIA_PROTOCOL_V0_NAME);
flipper_format_rewind(ctx->flipper_format);
flipper_format_insert_or_update_string_cstr(
@@ -1255,8 +1261,8 @@ static void plugin_on_enter(void* context) {
uint8_t raw[13] = {0};
bool have_raw = false;
flipper_format_rewind(ctx->flipper_format);
if(flipper_format_read_hex(ctx->flipper_format, "Raw", raw, sizeof(raw)) && raw[0] == 0x00U &&
raw[1] == 0x01U) {
if(flipper_format_read_hex(ctx->flipper_format, "Raw", raw, sizeof(raw)) &&
raw[0] == 0x00U && raw[1] == 0x01U) {
have_raw = true;
}
@@ -1267,8 +1273,10 @@ static void plugin_on_enter(void* context) {
} else {
uint8_t key_bytes[8] = {0};
flipper_format_rewind(ctx->flipper_format);
if(flipper_format_read_hex(ctx->flipper_format, "Key", key_bytes, sizeof(key_bytes))) {
ctx->serial = ((uint32_t)key_bytes[0] << 24U) | ((uint32_t)key_bytes[1] << 16U) |
if(flipper_format_read_hex(
ctx->flipper_format, "Key", key_bytes, sizeof(key_bytes))) {
ctx->serial = ((uint32_t)key_bytes[0] << 24U) |
((uint32_t)key_bytes[1] << 16U) |
((uint32_t)key_bytes[2] << 8U) | key_bytes[3];
}
}
@@ -1279,8 +1287,7 @@ static void plugin_on_enter(void* context) {
}
}
if(have_raw && (ctx->current_counter > 0x3FFU || ctx->original_counter > 0x3FFU)) {
const uint32_t ctrl =
((uint32_t)(raw[6] & 0x0FU) << 6U) | ((uint32_t)raw[7] >> 2U);
const uint32_t ctrl = ((uint32_t)(raw[6] & 0x0FU) << 6U) | ((uint32_t)raw[7] >> 2U);
ctx->original_counter = ctrl;
ctx->current_counter = ctrl;
flipper_format_rewind(ctx->flipper_format);
@@ -82,17 +82,17 @@ static void bf_free_states(void) {
g_bf_kind = ProtoPirateBfKindNone;
}
static bool item_needs_bruteforce_from_ff(FlipperFormat* ff, bool require_psa_protocol) {
static bool item_needs_bruteforce_from_ff(FlipperFormat* ff) {
if(!ff) return false;
FuriString* s = furi_string_alloc();
flipper_format_rewind(ff);
if(require_psa_protocol) {
if(!flipper_format_read_string(ff, FF_PROTOCOL, s) || furi_string_cmp_str(s, "PSA") != 0) {
furi_string_free(s);
return false;
}
flipper_format_rewind(ff);
if(!flipper_format_read_string(ff, FF_PROTOCOL, s) || furi_string_cmp_str(s, "PSA") != 0) {
furi_string_free(s);
return false;
}
flipper_format_rewind(ff);
bool has_key = flipper_format_read_string(ff, FF_KEY, s);
if(!has_key) {
furi_string_free(s);
@@ -121,9 +121,11 @@ static void show_bf_progress(void* app) {
if(g_bf_kind == ProtoPirateBfKindHitag2) {
widget_add_string_element(widget, 62, 0, AlignLeft, AlignTop, FontPrimary, "Recover Key");
widget_add_string_element(widget, 62, 12, AlignLeft, AlignTop, FontSecondary, "Max ETA:");
widget_add_string_element(widget, 62, 22, AlignLeft, AlignTop, FontSecondary, "60 seconds");
widget_add_string_element(
widget, 62, 22, AlignLeft, AlignTop, FontSecondary, "60 seconds");
} else {
widget_add_string_element(widget, 62, 0, AlignLeft, AlignTop, FontPrimary, "Bruteforcing...");
widget_add_string_element(
widget, 62, 0, AlignLeft, AlignTop, FontPrimary, "Bruteforcing...");
}
FuriString* pct_str =
@@ -305,9 +307,9 @@ static void bf_cancel_thread(void) {
}
static bool plugin_needs_bruteforce(void* app, ProtoPiratePsaBfContext ctx) {
UNUSED(ctx);
FlipperFormat* ff = g_host_api->get_history_flipper_format(app);
const bool require = ctx == ProtoPiratePsaBfContextReceiverInfo;
return item_needs_bruteforce_from_ff(ff, require) || hitag2_bf_needs_bruteforce(ff, require);
return item_needs_bruteforce_from_ff(ff) || hitag2_bf_needs_bruteforce(ff);
}
static bool plugin_is_running(void* app) {
@@ -332,8 +334,7 @@ static bool start_bruteforce(void* app) {
FlipperFormat* ff = g_host_api->get_history_flipper_format(app);
if(!ff || !plugin_needs_bruteforce(app, g_active_ctx)) return false;
const bool require = g_active_ctx == ProtoPiratePsaBfContextReceiverInfo;
if(item_needs_bruteforce_from_ff(ff, require)) {
if(item_needs_bruteforce_from_ff(ff)) {
PsaBfState* state = malloc(sizeof(PsaBfState));
if(!state) {
g_host_api->notification_error(app);
@@ -349,7 +350,7 @@ static bool start_bruteforce(void* app) {
g_bf_state = state;
g_bf_kind = ProtoPirateBfKindPsa;
g_bf_thread = furi_thread_alloc_ex("PsaBf", 2048, psa_brute_force_thread_entry, state);
} else if(hitag2_bf_needs_bruteforce(ff, require)) {
} else if(hitag2_bf_needs_bruteforce(ff)) {
Hitag2BfState* state = malloc(sizeof(Hitag2BfState));
if(!state) {
g_host_api->notification_error(app);
@@ -94,14 +94,14 @@ static void emulate_plugin_unload(ProtoPirateApp* app) {
app->emulate_plugin = NULL;
if(app->emulate_plugin_manager) {
plugin_manager_free(app->emulate_plugin_manager);
app->emulate_plugin_manager = NULL;
if(app->plugin_manager) {
plugin_manager_free(app->plugin_manager);
app->plugin_manager = NULL;
}
if(app->emulate_plugin_resolver) {
composite_api_resolver_free(app->emulate_plugin_resolver);
app->emulate_plugin_resolver = NULL;
if(app->plugin_resolver) {
composite_api_resolver_free(app->plugin_resolver);
app->plugin_resolver = NULL;
}
}
@@ -110,7 +110,7 @@ static bool emulate_plugin_load(ProtoPirateApp* app) {
if(app->emulate_plugin) return true;
if(app->emulate_plugin_manager || app->emulate_plugin_resolver) {
if(app->plugin_manager || app->plugin_resolver) {
emulate_plugin_unload(app);
}
@@ -148,8 +148,8 @@ static bool emulate_plugin_load(ProtoPirateApp* app) {
return false;
}
app->emulate_plugin_resolver = resolver;
app->emulate_plugin_manager = manager;
app->plugin_resolver = resolver;
app->plugin_manager = manager;
app->emulate_plugin = plugin;
plugin->set_host_api(&protopirate_emulate_host_api);
@@ -56,8 +56,10 @@ static void protopirate_scene_receiver_callback(
protopirate_history_add_to_history(app->txrx->history, decoder_base, app->txrx->preset);
if(added) {
notification_message(app->notifications, &sequence_semi_success);
if(!(app->sound))
notification_message(app->notifications, &sequence_semi_success);
else
notification_message(app->notifications, &sequence_single_vibro);
FURI_LOG_I(
TAG,
"Added to history, total items: %u",
@@ -107,19 +109,42 @@ static bool protopirate_scene_receiver_process_auto_save(ProtoPirateApp* app) {
FlipperFormat* ff = protopirate_history_get_raw_data(app->txrx->history, idx);
if(ff) {
FuriString* protocol = furi_string_alloc();
FuriString* saved_path = furi_string_alloc();
FuriString* file_name_str = furi_string_alloc();
if(protocol && saved_path) {
if(saved_path && file_name_str) {
if(app->datetime_filenames) {
//Get the date and time to save.
DateTime date_time;
furi_hal_rtc_get_datetime(&date_time);
furi_string_printf(
file_name_str,
"%.2d%.2d%.2d_%.2d.%.2d.%.2d_",
date_time.year,
date_time.month,
date_time.day,
date_time.hour,
date_time.minute,
date_time.second);
}
// Extract protocol name
FuriString* protocol = furi_string_alloc();
flipper_format_rewind(ff);
if(!flipper_format_read_string(ff, FF_PROTOCOL, protocol)) {
if(!flipper_format_read_string(ff, "Protocol", protocol)) {
furi_string_set_str(protocol, "Unknown");
}
furi_string_replace_all(protocol, "/", "_");
furi_string_replace_all(protocol, " ", "_");
//Add the protocol
furi_string_cat(file_name_str, protocol);
furi_string_free(protocol);
if(protopirate_storage_save_capture(ff, furi_string_get_cstr(protocol), saved_path)) {
// Clean protocol name for filename
furi_string_replace_all(file_name_str, "/", "_");
furi_string_replace_all(file_name_str, " ", "_");
if(protopirate_storage_save_capture(
ff, furi_string_get_cstr(file_name_str), saved_path, app->datetime_filenames)) {
FURI_LOG_I(TAG, "Auto-saved: %s", furi_string_get_cstr(saved_path));
notification_message(app->notifications, &sequence_double_vibro);
} else {
@@ -131,8 +156,8 @@ static bool protopirate_scene_receiver_process_auto_save(ProtoPirateApp* app) {
notification_message(app->notifications, &sequence_error);
}
if(protocol) furi_string_free(protocol);
if(saved_path) furi_string_free(saved_path);
if(file_name_str) furi_string_free(file_name_str);
protopirate_history_release_scratch(app->txrx->history);
} else {
FURI_LOG_E(TAG, "Auto-save skipped: history capture unavailable");
@@ -226,15 +251,7 @@ static void protopirate_scene_receiver_start_rx_stack(ProtoPirateApp* app) {
app->txrx->hopper_state = ProtoPirateHopperStateRunning;
}
const char* preset_name = furi_string_get_cstr(app->txrx->preset->name);
uint8_t* preset_data = subghz_setting_get_preset_data_by_name(app->setting, preset_name);
if(preset_data == NULL) {
FURI_LOG_E(TAG, "Failed to get preset data for %s, using AM650", preset_name);
preset_data = subghz_setting_get_preset_data_by_name(app->setting, "AM650");
}
protopirate_begin(app, preset_data);
protopirate_begin(app, app->txrx->preset->data);
uint32_t frequency = app->txrx->preset->frequency;
if(app->txrx->hopper_state == ProtoPirateHopperStateRunning) {
@@ -313,6 +330,9 @@ void protopirate_scene_receiver_on_enter(void* context) {
view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewReceiver);
view_dispatcher_send_custom_event(
app->view_dispatcher, ProtoPirateCustomEventReceiverDeferredRxStart);
//Kill Config if it exists now to save memory.
protopirate_variable_item_list_free(app);
}
static void protopirate_scene_receiver_handle_back(ProtoPirateApp* app) {
@@ -1,302 +1,16 @@
// scenes/protopirate_scene_receiver_config.c
#include "../protopirate_app_i.h"
enum ProtoPirateSettingIndex {
ProtoPirateSettingIndexFrequency,
ProtoPirateSettingIndexHopping,
ProtoPirateSettingIndexModulation,
#ifdef ENABLE_EMULATE_FEATURE
ProtoPirateSettingIndexTXPower,
#endif
ProtoPirateSettingIndexAutoSave,
ProtoPirateSettingIndexCheckSaved,
ProtoPirateSettingIndexLock,
};
#define HOPPING_COUNT 2
const char* const hopping_text[HOPPING_COUNT] = {
"OFF",
"ON",
};
const uint32_t hopping_value[HOPPING_COUNT] = {
ProtoPirateHopperStateOFF,
ProtoPirateHopperStateRunning,
};
#define AUTO_SAVE_COUNT 2
const char* const auto_save_text[AUTO_SAVE_COUNT] = {
"OFF",
"ON",
};
#define CHECK_SAVED_COUNT 2
const char* const check_saved_text[CHECK_SAVED_COUNT] = {
"OFF",
"ON",
};
#ifdef ENABLE_EMULATE_FEATURE
#define TX_POWER_COUNT 9
const char* const tx_power_text[TX_POWER_COUNT] = {
"Preset",
"10dBm +",
"7dBm",
"5dBm",
"0dBm",
"-10dBm",
"-15dBm",
"-20dBm",
"-30dBm",
};
#endif
uint8_t protopirate_scene_receiver_config_next_frequency(const uint32_t value, void* context) {
furi_check(context);
ProtoPirateApp* app = context;
uint8_t index = 0;
for(uint8_t i = 0; i < subghz_setting_get_frequency_count(app->setting); i++) {
if(value == subghz_setting_get_frequency(app->setting, i)) {
index = i;
break;
} else {
index = subghz_setting_get_frequency_default_index(app->setting);
}
}
return index;
}
uint8_t protopirate_scene_receiver_config_next_preset(const char* preset_name, void* context) {
furi_check(context);
ProtoPirateApp* app = context;
uint8_t index = 0;
for(uint8_t i = 0; i < subghz_setting_get_preset_count(app->setting); i++) {
if(!strcmp(subghz_setting_get_preset_name(app->setting, i), preset_name)) {
index = i;
break;
}
}
return index;
}
uint8_t protopirate_scene_receiver_config_hopper_value_index(
const uint32_t value,
const uint32_t values[],
uint8_t values_count,
void* context) {
furi_check(context);
UNUSED(values_count);
ProtoPirateApp* app = context;
if(value == values[0]) {
return 0;
} else {
variable_item_set_current_value_text(
(VariableItem*)scene_manager_get_scene_state(
app->scene_manager, ProtoPirateSceneReceiverConfig),
" -----");
return 1;
}
}
static void protopirate_scene_receiver_config_set_frequency(VariableItem* item) {
ProtoPirateApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
if(app->txrx->hopper_state == ProtoPirateHopperStateOFF) {
char text_buf[10] = {0};
snprintf(
text_buf,
sizeof(text_buf),
"%lu.%02lu",
subghz_setting_get_frequency(app->setting, index) / 1000000,
(subghz_setting_get_frequency(app->setting, index) % 1000000) / 10000);
variable_item_set_current_value_text(item, text_buf);
app->txrx->preset->frequency = subghz_setting_get_frequency(app->setting, index);
} else {
variable_item_set_current_value_index(
item, subghz_setting_get_frequency_default_index(app->setting));
}
}
static void protopirate_scene_receiver_config_set_preset(VariableItem* item) {
ProtoPirateApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
variable_item_set_current_value_text(
item, subghz_setting_get_preset_name(app->setting, index));
protopirate_preset_init(
app,
subghz_setting_get_preset_name(app->setting, index),
app->txrx->preset->frequency,
subghz_setting_get_preset_data(app->setting, index),
subghz_setting_get_preset_data_size(app->setting, index));
if(!protopirate_refresh_protocol_registry(app, false)) {
notification_message(app->notifications, &sequence_error);
}
}
static void protopirate_scene_receiver_config_set_hopping_running(VariableItem* item) {
ProtoPirateApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
variable_item_set_current_value_text(item, hopping_text[index]);
if(hopping_value[index] == ProtoPirateHopperStateOFF) {
char text_buf[10] = {0};
snprintf(
text_buf,
sizeof(text_buf),
"%lu.%02lu",
subghz_setting_get_default_frequency(app->setting) / 1000000,
(subghz_setting_get_default_frequency(app->setting) % 1000000) / 10000);
variable_item_set_current_value_text(
(VariableItem*)scene_manager_get_scene_state(
app->scene_manager, ProtoPirateSceneReceiverConfig),
text_buf);
app->txrx->preset->frequency = subghz_setting_get_default_frequency(app->setting);
variable_item_set_current_value_index(
(VariableItem*)scene_manager_get_scene_state(
app->scene_manager, ProtoPirateSceneReceiverConfig),
subghz_setting_get_frequency_default_index(app->setting));
} else {
variable_item_set_current_value_text(
(VariableItem*)scene_manager_get_scene_state(
app->scene_manager, ProtoPirateSceneReceiverConfig),
" -----");
variable_item_set_current_value_index(
(VariableItem*)scene_manager_get_scene_state(
app->scene_manager, ProtoPirateSceneReceiverConfig),
subghz_setting_get_frequency_default_index(app->setting));
}
app->txrx->hopper_state = hopping_value[index];
}
static void protopirate_scene_receiver_config_set_auto_save(VariableItem* item) {
ProtoPirateApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
app->auto_save = (index == 1);
variable_item_set_current_value_text(item, auto_save_text[index]);
}
static void protopirate_scene_receiver_config_set_check_saved(VariableItem* item) {
ProtoPirateApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
app->check_saved = (index == 1);
variable_item_set_current_value_text(item, check_saved_text[index]);
}
#ifdef ENABLE_EMULATE_FEATURE
static void protopirate_scene_receiver_config_set_tx_power(VariableItem* item) {
ProtoPirateApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
app->tx_power = index;
variable_item_set_current_value_text(item, tx_power_text[index]);
}
#endif
static void
protopirate_scene_receiver_config_var_list_enter_callback(void* context, uint32_t index) {
furi_check(context);
ProtoPirateApp* app = context;
if(index == ProtoPirateSettingIndexLock) {
view_dispatcher_send_custom_event(
app->view_dispatcher, ProtoPirateCustomEventSceneSettingLock);
}
}
void protopirate_scene_receiver_config_on_enter(void* context) {
ProtoPirateApp* app = context;
VariableItem* item;
uint8_t value_index;
if(!protopirate_ensure_variable_item_list(app)) {
if(!config_plugin_load(app)) {
notification_message(app->notifications, &sequence_error);
scene_manager_previous_scene(app->scene_manager);
return;
}
item = variable_item_list_add(
app->variable_item_list,
"Frequency:",
subghz_setting_get_frequency_count(app->setting),
protopirate_scene_receiver_config_set_frequency,
app);
value_index =
protopirate_scene_receiver_config_next_frequency(app->txrx->preset->frequency, app);
scene_manager_set_scene_state(
app->scene_manager, ProtoPirateSceneReceiverConfig, (uint32_t)item);
variable_item_set_current_value_index(item, value_index);
char text_buf[10] = {0};
snprintf(
text_buf,
sizeof(text_buf),
"%lu.%02lu",
subghz_setting_get_frequency(app->setting, value_index) / 1000000,
(subghz_setting_get_frequency(app->setting, value_index) % 1000000) / 10000);
variable_item_set_current_value_text(item, text_buf);
item = variable_item_list_add(
app->variable_item_list,
"Hopping:",
HOPPING_COUNT,
protopirate_scene_receiver_config_set_hopping_running,
app);
value_index = protopirate_scene_receiver_config_hopper_value_index(
app->txrx->hopper_state, hopping_value, HOPPING_COUNT, app);
variable_item_set_current_value_index(item, value_index);
variable_item_set_current_value_text(item, hopping_text[value_index]);
item = variable_item_list_add(
app->variable_item_list,
"Modulation:",
subghz_setting_get_preset_count(app->setting),
protopirate_scene_receiver_config_set_preset,
app);
value_index = protopirate_scene_receiver_config_next_preset(
furi_string_get_cstr(app->txrx->preset->name), app);
variable_item_set_current_value_index(item, value_index);
variable_item_set_current_value_text(
item, subghz_setting_get_preset_name(app->setting, value_index));
#ifdef ENABLE_EMULATE_FEATURE
// TX power option
item = variable_item_list_add(
app->variable_item_list,
"TX Power:",
TX_POWER_COUNT,
protopirate_scene_receiver_config_set_tx_power,
app);
variable_item_set_current_value_index(item, app->tx_power);
variable_item_set_current_value_text(item, tx_power_text[app->tx_power]);
#endif
// Auto-save option
item = variable_item_list_add(
app->variable_item_list,
"Auto-Save:",
AUTO_SAVE_COUNT,
protopirate_scene_receiver_config_set_auto_save,
app);
variable_item_set_current_value_index(item, app->auto_save ? 1 : 0);
variable_item_set_current_value_text(item, auto_save_text[app->auto_save ? 1 : 0]);
item = variable_item_list_add(
app->variable_item_list,
"Check Saved:",
CHECK_SAVED_COUNT,
protopirate_scene_receiver_config_set_check_saved,
app);
variable_item_set_current_value_index(item, app->check_saved ? 1 : 0);
variable_item_set_current_value_text(item, check_saved_text[app->check_saved ? 1 : 0]);
variable_item_list_add(app->variable_item_list, "Lock Keyboard", 1, NULL, NULL);
variable_item_list_set_enter_callback(
app->variable_item_list, protopirate_scene_receiver_config_var_list_enter_callback, app);
view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewVariableItemList);
app->config_plugin->on_enter(app);
}
bool protopirate_scene_receiver_config_on_event(void* context, SceneManagerEvent event) {
@@ -315,6 +29,6 @@ bool protopirate_scene_receiver_config_on_event(void* context, SceneManagerEvent
void protopirate_scene_receiver_config_on_exit(void* context) {
ProtoPirateApp* app = context;
variable_item_list_set_selected_item(app->variable_item_list, 0);
variable_item_list_reset(app->variable_item_list);
config_plugin_unload(app);
}
@@ -280,17 +280,49 @@ bool protopirate_scene_receiver_info_on_event(void* context, SceneManagerEvent e
if(event.event == ProtoPirateCustomEventReceiverInfoSave) {
FlipperFormat* ff =
protopirate_history_get_raw_data(app->txrx->history, app->txrx->idx_menu_chosen);
if(ff) {
FuriString* protocol = furi_string_alloc();
protopirate_storage_get_capture_display_protocol(ff, protocol);
FuriString* filename_str = furi_string_alloc();
if(ff) {
if(app->datetime_filenames) {
//Get the date and time to save.
DateTime date_time;
furi_hal_rtc_get_datetime(&date_time);
furi_string_printf(
filename_str,
"%.2d%.2d%.2d_%.2d.%.2d.%.2d_",
date_time.year,
date_time.month,
date_time.day,
date_time.hour,
date_time.minute,
date_time.second);
}
// Extract protocol name
FuriString* protocol = furi_string_alloc();
flipper_format_rewind(ff);
if(!flipper_format_read_string(ff, "Protocol", protocol)) {
furi_string_set_str(protocol, "Unknown");
}
//Add the protocol
furi_string_cat(filename_str, protocol);
//furi_string_free(protocol);
// Clean protocol name for filename
furi_string_replace_all(filename_str, "/", "_");
furi_string_replace_all(filename_str, " ", "_");
// Get the next auto-generated filename (just the name part)
FuriString* auto_path = furi_string_alloc();
if(protopirate_storage_get_next_filename(
furi_string_get_cstr(protocol), auto_path)) {
furi_string_get_cstr(filename_str), auto_path, (app->datetime_filenames))) {
// Extract just the filename without folder and extension
const char* full = furi_string_get_cstr(auto_path);
const char* slash = strrchr(full, '/');
const char* name_start = slash ? slash + 1 : full;
// Copy without extension
size_t name_len = strlen(name_start);
const char* dot = strrchr(name_start, '.');
if(dot) name_len = dot - name_start;
@@ -304,11 +336,13 @@ bool protopirate_scene_receiver_info_on_event(void* context, SceneManagerEvent e
}
furi_string_free(auto_path);
// Store context for when text input confirms
if(app->save_protocol) furi_string_free(app->save_protocol);
app->save_protocol = protocol;
app->save_protocol = protocol; // transfer ownership
app->save_history_idx = app->txrx->idx_menu_chosen;
app->save_from_saved_info = false;
// Configure and show text input
text_input_reset(app->text_input);
text_input_set_header_text(app->text_input, "Save filename:");
text_input_set_result_callback(
@@ -317,17 +351,20 @@ bool protopirate_scene_receiver_info_on_event(void* context, SceneManagerEvent e
app,
app->save_filename,
sizeof(app->save_filename),
false);
false); // don't clear default text
view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewTextInput);
}
furi_string_free(filename_str);
consumed = true;
}
if(event.event == ProtoPirateCustomEventReceiverInfoSaveConfirm) {
// User confirmed the filename in text input
FlipperFormat* ff =
protopirate_history_get_raw_data(app->txrx->history, app->save_history_idx);
if(ff) {
// Build full path: folder/filename.psf
FuriString* save_path = furi_string_alloc_printf(
"%s/%s%s",
PROTOPIRATE_APP_FOLDER,
@@ -344,11 +381,13 @@ bool protopirate_scene_receiver_info_on_event(void* context, SceneManagerEvent e
furi_string_free(save_path);
}
// Clean up save protocol string
if(app->save_protocol) {
furi_string_free(app->save_protocol);
app->save_protocol = NULL;
}
// Return to the receiver info widget
view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewWidget);
consumed = true;
}
@@ -79,6 +79,9 @@ void protopirate_scene_start_on_enter(void* context) {
app->submenu, scene_manager_get_scene_state(app->scene_manager, ProtoPirateSceneStart));
view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewSubmenu);
//Kill Config if it exists now to save memory.
protopirate_variable_item_list_free(app);
}
bool protopirate_scene_start_on_event(void* context, SceneManagerEvent event) {
@@ -87,6 +90,8 @@ bool protopirate_scene_start_on_event(void* context, SceneManagerEvent event) {
bool consumed = false;
if(event.type == SceneManagerEventTypeCustom) {
scene_manager_set_scene_state(app->scene_manager, ProtoPirateSceneStart, event.event);
FURI_LOG_I(TAG, "Suppressing Charging While in a scene.");
if(event.event == SubmenuIndexProtoPirateAbout) {
scene_manager_next_scene(app->scene_manager, ProtoPirateSceneAbout);
consumed = true;
@@ -112,7 +117,6 @@ bool protopirate_scene_start_on_event(void* context, SceneManagerEvent event) {
consumed = true;
}
#endif
scene_manager_set_scene_state(app->scene_manager, ProtoPirateSceneStart, event.event);
}
return consumed;
@@ -493,6 +493,12 @@ static bool protopirate_scene_sub_decode_open_browser_for_next_file(ProtoPirateA
return true;
}
static void protopirate_scene_sub_decode_text_input_callback(void* context) {
ProtoPirateApp* app = context;
view_dispatcher_send_custom_event(
app->view_dispatcher, ProtoPirateCustomEventSubDecodeSaveConfirm);
}
void protopirate_scene_sub_decode_on_enter(void* context) {
ProtoPirateApp* app = context;
@@ -621,20 +627,81 @@ bool protopirate_scene_sub_decode_on_event(void* context, SceneManagerEvent even
protopirate_history_get_raw_data(ctx->history, ctx->selected_history_index);
if(ff) {
FuriString* file_name_str = furi_string_alloc();
if(app->datetime_filenames) {
//Get the date and time to save.
DateTime date_time;
furi_hal_rtc_get_datetime(&date_time);
furi_string_printf(
file_name_str,
"%.2d%.2d%.2d_%.2d.%.2d.%.2d_",
date_time.year,
date_time.month,
date_time.day,
date_time.hour,
date_time.minute,
date_time.second);
}
// Extract protocol name
FuriString* protocol = furi_string_alloc();
protopirate_storage_get_capture_display_protocol(ff, protocol);
FuriString* saved_path = furi_string_alloc();
if(protopirate_storage_save_capture(
ff, furi_string_get_cstr(protocol), saved_path)) {
notification_message(app->notifications, &sequence_success);
} else {
notification_message(app->notifications, &sequence_error);
}
//Add the protocol
furi_string_cat(file_name_str, protocol);
furi_string_free(protocol);
furi_string_free(saved_path);
// Clean protocol name for filename
furi_string_replace_all(file_name_str, "/", "_");
furi_string_replace_all(file_name_str, " ", "_");
// Get the next auto-generated filename (just the name part)
FuriString* auto_path = furi_string_alloc();
if(protopirate_storage_get_next_filename(
furi_string_get_cstr(file_name_str), auto_path, app->datetime_filenames)) {
// Extract just the filename without folder and extension
const char* full = furi_string_get_cstr(auto_path);
const char* slash = strrchr(full, '/');
const char* name_start = slash ? slash + 1 : full;
// Copy without extension
size_t name_len = strlen(name_start);
const char* dot = strrchr(name_start, '.');
if(dot) name_len = dot - name_start;
if(name_len >= sizeof(app->save_filename))
name_len = sizeof(app->save_filename) - 1;
memcpy(app->save_filename, name_start, name_len);
app->save_filename[name_len] = '\0';
} else {
snprintf(app->save_filename, sizeof(app->save_filename), "capture");
}
furi_string_free(auto_path);
// Store context for when text input confirms
app->save_history_idx = app->txrx->idx_menu_chosen;
app->save_from_saved_info = false;
//Make sure we have a text input window.
app->text_input = text_input_alloc();
view_dispatcher_add_view(
app->view_dispatcher,
ProtoPirateViewTextInput,
text_input_get_view(app->text_input));
// Configure and show text input
text_input_reset(app->text_input);
text_input_set_header_text(app->text_input, "Save filename:");
text_input_set_result_callback(
app->text_input,
protopirate_scene_sub_decode_text_input_callback,
app,
app->save_filename,
sizeof(app->save_filename),
false); // don't clear default text
view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewTextInput);
furi_string_free(file_name_str);
} else {
FURI_LOG_E(
TAG,
@@ -643,6 +710,40 @@ bool protopirate_scene_sub_decode_on_event(void* context, SceneManagerEvent even
notification_message(app->notifications, &sequence_error);
}
consumed = true;
} else if(event.event == ProtoPirateCustomEventSubDecodeSaveConfirm) {
// User confirmed the filename in text input
FlipperFormat* ff =
protopirate_history_get_raw_data(app->txrx->history, app->save_history_idx);
if(ff) {
// Build full path: folder/filename.psf
FuriString* save_path = furi_string_alloc_printf(
"%s/%s%s",
PROTOPIRATE_APP_FOLDER,
app->save_filename,
PROTOPIRATE_APP_EXTENSION);
if(protopirate_storage_save_capture_to_path(ff, furi_string_get_cstr(save_path))) {
notification_message(app->notifications, &sequence_success);
FURI_LOG_I(TAG, "Saved to: %s", furi_string_get_cstr(save_path));
} else {
notification_message(app->notifications, &sequence_error);
FURI_LOG_E(TAG, "Save failed");
}
furi_string_free(save_path);
}
// Return to the receiver info widget
view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewWidget);
//Kill the text_input view.
if(app->text_input) {
FURI_LOG_D(TAG, "Removing text_input view");
view_dispatcher_remove_view(app->view_dispatcher, ProtoPirateViewTextInput);
text_input_free(app->text_input);
app->text_input = NULL;
}
consumed = true;
}
#ifdef ENABLE_EMULATE_FEATURE
else if(
@@ -678,7 +779,6 @@ bool protopirate_scene_sub_decode_on_event(void* context, SceneManagerEvent even
}
consumed = true;
return consumed;
} else if(event.event == ProtoPirateCustomEventPsaBruteforceComplete) {
app->txrx->idx_menu_chosen = ctx->selected_history_index;
if(app->psa_bf_plugin) {
@@ -689,7 +789,6 @@ bool protopirate_scene_sub_decode_on_event(void* context, SceneManagerEvent even
app->view_dispatcher, ProtoPirateCustomEventSubDecodeUpdate);
consumed = true;
return consumed;
} else if(event.event == ProtoPirateCustomEventViewReceiverOK) {
// User selected a signal from history - show signal info
uint16_t idx = protopirate_view_receiver_get_idx_menu(app->protopirate_receiver);
@@ -1213,7 +1312,7 @@ bool protopirate_scene_sub_decode_on_event(void* context, SceneManagerEvent even
flipper_format_rewind(ff);
bool have_proto = flipper_format_read_string(ff, FF_PROTOCOL, proto_str);
bool offers_bf = have_proto && protopirate_protocol_catalog_offers_bruteforce(
furi_string_get_cstr(proto_str));
furi_string_get_cstr(proto_str));
if(have_proto) {
const char* protocol_name = furi_string_get_cstr(proto_str);
@@ -727,6 +727,13 @@ void protopirate_scene_timing_tuner_on_enter(void* context) {
g_timing_ctx->is_receiving = true;
view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewAbout);
//Kill Config if it exists now to save memory.
if(app->variable_item_list) {
view_dispatcher_remove_view(app->view_dispatcher, ProtoPirateViewVariableItemList);
variable_item_list_free(app->variable_item_list);
app->variable_item_list = NULL;
}
}
bool protopirate_scene_timing_tuner_on_event(void* context, SceneManagerEvent event) {
@@ -259,40 +259,6 @@ void protopirate_view_receiver_draw(Canvas* canvas, ProtoPirateReceiverModel* mo
protopirate_view_receiver_draw_progress_badge(canvas, model->sub_decode_progress);
}
//Draw To Unlock, Locked etc...
if(model->lock_count) {
if(model->sub_decode_mode) {
canvas_draw_str(canvas, 44, 63, furi_string_get_cstr(model->frequency_str));
canvas_draw_str(canvas, 96, 63, furi_string_get_cstr(model->history_stat_str));
} else {
canvas_draw_str(canvas, 44, 63, furi_string_get_cstr(model->frequency_str));
canvas_draw_str(canvas, 79, 63, furi_string_get_cstr(model->preset_str));
canvas_draw_str(canvas, 96, 63, furi_string_get_cstr(model->history_stat_str));
}
canvas_set_font(canvas, FontSecondary);
elements_bold_rounded_frame(canvas, 14, 8, 99, 48);
elements_multiline_text(canvas, 65, 26, "To unlock\npress:");
canvas_draw_icon(canvas, 65, 42, &I_Pin_back_arrow_10x8);
canvas_draw_icon(canvas, 80, 42, &I_Pin_back_arrow_10x8);
canvas_draw_icon(canvas, 95, 42, &I_Pin_back_arrow_10x8);
canvas_draw_icon(canvas, 16, 13, &I_WarningDolphin_45x42);
canvas_draw_dot(canvas, 17, 61);
} else {
if(model->lock == ProtoPirateLockOn) {
canvas_draw_icon(canvas, 64, 55, &I_Lock_7x8);
canvas_draw_str(canvas, 74, 62, "Locked");
} else {
if(model->sub_decode_mode) {
canvas_draw_str(canvas, 44, 63, furi_string_get_cstr(model->frequency_str));
canvas_draw_str(canvas, 96, 63, furi_string_get_cstr(model->history_stat_str));
} else {
canvas_draw_str(canvas, 44, 63, furi_string_get_cstr(model->frequency_str));
canvas_draw_str(canvas, 79, 63, furi_string_get_cstr(model->preset_str));
canvas_draw_str(canvas, 96, 63, furi_string_get_cstr(model->history_stat_str));
}
}
}
//Draw the List, or the Radar/Dolphin View.
if(item_count > 0) {
// Draw received items list
@@ -411,6 +377,40 @@ void protopirate_view_receiver_draw(Canvas* canvas, ProtoPirateReceiverModel* mo
canvas, 110 - canvas_string_width(canvas, auto_save_text), 7, auto_save_text);
}
}
//Draw the unlock instructions last, so they appear on top...
if(model->lock_count) {
if(model->sub_decode_mode) {
canvas_draw_str(canvas, 44, 63, furi_string_get_cstr(model->frequency_str));
canvas_draw_str(canvas, 96, 63, furi_string_get_cstr(model->history_stat_str));
} else {
canvas_draw_str(canvas, 44, 63, furi_string_get_cstr(model->frequency_str));
canvas_draw_str(canvas, 79, 63, furi_string_get_cstr(model->preset_str));
canvas_draw_str(canvas, 96, 63, furi_string_get_cstr(model->history_stat_str));
}
canvas_set_font(canvas, FontSecondary);
elements_bold_rounded_frame(canvas, 14, 8, 99, 48);
elements_multiline_text(canvas, 65, 26, "To unlock\npress:");
canvas_draw_icon(canvas, 65, 42, &I_Pin_back_arrow_10x8);
canvas_draw_icon(canvas, 80, 42, &I_Pin_back_arrow_10x8);
canvas_draw_icon(canvas, 95, 42, &I_Pin_back_arrow_10x8);
canvas_draw_icon(canvas, 16, 13, &I_WarningDolphin_45x42);
canvas_draw_dot(canvas, 17, 61);
} else {
if(model->lock == ProtoPirateLockOn) {
canvas_draw_icon(canvas, 64, 55, &I_Lock_7x8);
canvas_draw_str(canvas, 74, 62, "Locked");
} else {
if(model->sub_decode_mode) {
canvas_draw_str(canvas, 44, 63, furi_string_get_cstr(model->frequency_str));
canvas_draw_str(canvas, 96, 63, furi_string_get_cstr(model->history_stat_str));
} else {
canvas_draw_str(canvas, 44, 63, furi_string_get_cstr(model->frequency_str));
canvas_draw_str(canvas, 79, 63, furi_string_get_cstr(model->preset_str));
canvas_draw_str(canvas, 96, 63, furi_string_get_cstr(model->history_stat_str));
}
}
}
}
bool protopirate_view_receiver_input(InputEvent* event, void* context) {
+91 -83
View File
@@ -19,6 +19,7 @@ static const SubGhzBlockConst subghz_protocol_psa_const = {
#define PSA_TE_SHORT_125 0x7d
#define PSA_TE_LONG_250 0xfa
#define PSA_TE_LONG_300 0x12c
#define PSA_TE_END_1000 1000
#define PSA_TE_END_500 500
#define PSA_TOLERANCE_99 99
@@ -676,7 +677,10 @@ void subghz_protocol_decoder_psa_feed(void* context, bool level, uint32_t durati
} else {
tolerance = duration - PSA_TE_SHORT_125;
}
if(tolerance > PSA_TOLERANCE_49) {
if(tolerance > 40) {
return;
}
if(duration > 180) {
return;
}
new_state = PSADecoderState3;
@@ -960,43 +964,43 @@ void subghz_protocol_decoder_psa_feed(void* context, bool level, uint32_t durati
break;
case PSADecoderState3:
if(level) {
return;
// Fixed-TE preamble path (te_short=125us, te_long=250us), aligned with
// ProtoPirate reference decoder.
if(duration >= 250) {
if(duration >= PSA_TE_LONG_250 && duration < PSA_TE_LONG_300) {
if(instance->pattern_counter > PSA_PATTERN_THRESHOLD_2) {
new_state = PSADecoderState4;
instance->decode_data_low = 0;
instance->decode_data_high = 0;
instance->decode_count_bit = 0;
manchester_advance(instance->manchester_state, ManchesterEventReset,
&instance->manchester_state, NULL);
instance->state = new_state;
instance->pattern_counter = 0;
instance->prev_duration = duration;
return;
}
}
new_state = PSADecoderState0;
instance->pattern_counter = 0;
break;
}
// Adaptive AM preamble: accept 76-174us, average to detect actual TE
if(duration >= 76 && duration <= 174) {
if(prev_dur >= 76 && prev_dur <= 174) {
if(duration < PSA_TE_SHORT_125) {
tolerance = PSA_TE_SHORT_125 - duration;
} else {
tolerance = duration - PSA_TE_SHORT_125;
}
if(tolerance < PSA_TOLERANCE_50) {
uint32_t prev_diff = psa_abs_diff(prev_dur, PSA_TE_SHORT_125);
if(prev_diff <= PSA_TOLERANCE_49) {
instance->pattern_counter++;
instance->te_sum += duration;
instance->te_count++;
} else {
instance->pattern_counter = 0;
instance->te_sum = duration;
instance->te_count = 1;
}
instance->prev_duration = duration;
return;
} else {
// Check if this is the preamble-to-data transition (2x detected TE)
uint32_t te_avg = (instance->te_count > 0) ?
(instance->te_sum / instance->te_count) : PSA_TE_SHORT_125;
uint32_t te_long_expected = te_avg * 2;
uint32_t long_diff = psa_abs_diff(duration, te_long_expected);
if(long_diff <= te_avg && instance->pattern_counter > PSA_PATTERN_THRESHOLD_2) {
instance->te_detected = te_avg;
new_state = PSADecoderState4;
instance->decode_data_low = 0;
instance->decode_data_high = 0;
instance->decode_count_bit = 0;
manchester_advance(instance->manchester_state, ManchesterEventReset,
&instance->manchester_state, NULL);
instance->state = new_state;
instance->pattern_counter = 0;
instance->prev_duration = duration;
return;
}
}
new_state = PSADecoderState0;
@@ -1009,16 +1013,63 @@ void subghz_protocol_decoder_psa_feed(void* context, bool level, uint32_t durati
break;
}
uint32_t te_s = instance->te_detected ? instance->te_detected : PSA_TE_SHORT_125;
uint32_t te_l = te_s * 2;
uint32_t te_tol = te_s / 2;
uint32_t midpoint = (te_s + te_l) / 2;
// Fixed-TE Manchester decode (te_short=125us, te_long=250us), aligned with
// ProtoPirate reference decoder. Bits are consumed on the falling edge (!level);
// the end marker is a HIGH pulse (level) near PSA_TE_END_500.
if(!level) {
uint8_t manchester_input;
bool decoded_bit = false;
if(duration < PSA_TE_SHORT_125) {
tolerance = PSA_TE_SHORT_125 - duration;
if(tolerance > PSA_TOLERANCE_49) {
return;
}
manchester_input = ((level ^ 1) & 0x7f) << 1;
} else {
tolerance = duration - PSA_TE_SHORT_125;
if(tolerance < PSA_TOLERANCE_50) {
manchester_input = ((level ^ 1) & 0x7f) << 1;
} else if(duration >= PSA_TE_LONG_250 && duration < PSA_TE_LONG_300) {
if(level == 0) {
manchester_input = 6;
} else {
manchester_input = 4;
}
} else {
return;
}
}
if(instance->decode_count_bit < PSA_KEY2_BITS &&
manchester_advance(instance->manchester_state,
(ManchesterEvent)manchester_input,
&instance->manchester_state,
&decoded_bit)) {
uint32_t carry = (instance->decode_data_low >> 31) & 1;
instance->decode_data_low = (instance->decode_data_low << 1) | (decoded_bit ? 1 : 0);
instance->decode_data_high = (instance->decode_data_high << 1) | carry;
instance->decode_count_bit++;
if(instance->decode_count_bit == PSA_KEY1_BITS) {
instance->key1_low = instance->decode_data_low;
instance->key1_high = instance->decode_data_high;
instance->decode_data_low = 0;
instance->decode_data_high = 0;
}
}
} else if(level) {
uint32_t end_diff;
if(duration < PSA_TE_END_500) {
end_diff = PSA_TE_END_500 - duration;
} else {
end_diff = duration - PSA_TE_END_500;
}
if(end_diff <= 99) {
if(instance->decode_count_bit != PSA_KEY2_BITS) {
return;
}
// End marker check: HIGH pulse beyond long range at 80 bits
if(level && instance->decode_count_bit == PSA_KEY2_BITS && duration > midpoint) {
uint32_t end_expected = te_s * 4;
uint32_t end_diff = psa_abs_diff(duration, end_expected);
if(end_diff <= te_s * 2) {
instance->validation_field = (uint16_t)(instance->decode_data_low & 0xFFFF);
instance->key2_low = instance->decode_data_low;
instance->key2_high = instance->decode_data_high;
@@ -1058,52 +1109,9 @@ void subghz_protocol_decoder_psa_feed(void* context, bool level, uint32_t durati
new_state = PSADecoderState0;
instance->state = new_state;
return;
}
}
// Manchester decode: process BOTH high and low pulses (unlike original AM path)
if(duration > te_l + te_tol) {
if(duration > 10000) {
new_state = PSADecoderState0;
break;
}
return;
}
uint8_t manchester_input;
bool decoded_bit = false;
if(duration <= midpoint) {
if(psa_abs_diff(duration, te_s) > te_tol) {
} else {
return;
}
manchester_input = level ? ManchesterEventShortLow : ManchesterEventShortHigh;
} else {
if(psa_abs_diff(duration, te_l) > te_tol) {
return;
}
manchester_input = level ? ManchesterEventLongLow : ManchesterEventLongHigh;
}
if(instance->decode_count_bit < PSA_KEY2_BITS) {
if(manchester_advance(instance->manchester_state,
(ManchesterEvent)manchester_input,
&instance->manchester_state,
&decoded_bit)) {
uint32_t carry = (instance->decode_data_low >> 31) & 1;
// PSA AM uses inverted Manchester convention
decoded_bit = !decoded_bit;
instance->decode_data_low = (instance->decode_data_low << 1) | (decoded_bit ? 1 : 0);
instance->decode_data_high = (instance->decode_data_high << 1) | carry;
instance->decode_count_bit++;
if(instance->decode_count_bit == PSA_KEY1_BITS) {
instance->key1_low = instance->decode_data_low;
instance->key1_high = instance->decode_data_high;
instance->decode_data_low = 0;
instance->decode_data_high = 0;
}
}
}
break;
}
+241 -11
View File
@@ -128,6 +128,36 @@ static const SubGhzBlockConst toyota_const_b = {
#define TOYOTA_A_KIA_SYNC_MIN 901u
#define TOYOTA_A_KIA_SYNC_MAX 1600u
/* ----------------------------------------------------------------
* Physical constants — Variant C (Prius 2006 / Corolla Verso, 433.92 MHz)
*
* KeeLoq-style, DECODE ONLY. Framing:
* >=6 short preamble pairs (HIGH ~200-550us, LOW ~200-650us), then ONE
* extra-long LOW sync gap (1050-1500us), then PWM data.
* DATA bit is decided by the HIGH pulse:
* HIGH > C_HIGH_MIDPOINT (600us) => bit 1, else bit 0
* (HIGH must be within 200-1050us to be valid). The following LOW is
* complementary and ignored unless it's a sync gap (1050-1500 => fire+restart
* data) or oversized (>1000 ends frame -> hunt preamble).
* Accumulate up to C_FRAME_BITS bits; DECODE when bit_count >= C_BITS.
* Fields MSB-first from the START of the data (anchored to sync gap):
* hop = bits[0..32), serial = bits[32..60), button = bits[60..64).
* ---------------------------------------------------------------- */
#define TOYOTA_C_BITS 66u /* MIN data bits required to decode */
#define TOYOTA_C_FRAME_BITS 68u /* nominal data frame length */
#define TOYOTA_C_PREAMBLE_MIN 6u /* require >= this many preamble pairs */
#define TOYOTA_C_SYNC_MIN 1050u /* XL LOW between preamble and data */
#define TOYOTA_C_SYNC_MAX 1500u
#define TOYOTA_C_HIGH_MIDPOINT 600u /* HIGH >mid => bit 1, else bit 0 */
#define TOYOTA_C_PRE_HIGH_MIN 200u /* preamble HIGH window */
#define TOYOTA_C_PRE_HIGH_MAX 550u
#define TOYOTA_C_PRE_LOW_MIN 200u /* preamble LOW window (below sync) */
#define TOYOTA_C_PRE_LOW_MAX 650u
#define TOYOTA_C_DATA_HIGH_MIN 200u /* valid data HIGH window */
#define TOYOTA_C_DATA_HIGH_MAX 1050u
#define TOYOTA_C_END_LOW 1000u /* LOW longer than this ends the frame */
/* ----------------------------------------------------------------
* Button codes
* ---------------------------------------------------------------- */
@@ -138,6 +168,11 @@ static const SubGhzBlockConst toyota_const_b = {
#define TOYOTA_B_BTN_LOCK 0x0A
#define TOYOTA_B_BTN_UNLOCK 0x05
#define TOYOTA_C_BTN_LOCK 0x0B
#define TOYOTA_C_BTN_UNLOCK 0x0E
#define TOYOTA_C_BTN_TRUNK 0x0D
#define TOYOTA_C_BTN_PANIC 0x07
/* ----------------------------------------------------------------
* Parser states
* ---------------------------------------------------------------- */
@@ -148,6 +183,8 @@ typedef enum {
ToyotaStepDataA,
ToyotaStepPreambleB,
ToyotaStepDataB,
ToyotaStepPreambleC,
ToyotaStepDataC,
} ToyotaDecoderStep;
/* ----------------------------------------------------------------
@@ -167,7 +204,7 @@ typedef struct {
bool have_high;
uint16_t preamble_count;
uint8_t variant; /* 0 = Corolla/433MHz, 1 = Tundra/315MHz */
uint8_t variant; /* 0 = Corolla/433MHz, 1 = Tundra/315MHz, 2 = Prius/433MHz */
uint32_t hop;
uint32_t serial;
@@ -248,6 +285,15 @@ static uint32_t toyota_extract(
* ---------------------------------------------------------------- */
static const char* toyota_button_name(uint8_t btn, uint8_t variant) {
if(variant == 2) {
switch(btn & 0x0F) {
case TOYOTA_C_BTN_LOCK: return "Lock";
case TOYOTA_C_BTN_UNLOCK: return "Unlock";
case TOYOTA_C_BTN_TRUNK: return "Trunk";
case TOYOTA_C_BTN_PANIC: return "Panic";
default: return "Unknown";
}
}
if(variant == 1) {
switch(btn & 0x0F) {
case TOYOTA_B_BTN_LOCK: return "Lock";
@@ -268,6 +314,7 @@ static const char* toyota_button_name(uint8_t btn, uint8_t variant) {
}
static const char* toyota_model_name(uint8_t variant) {
if(variant == 2) return "Prius";
return (variant == 1) ? "Tundra" : "Corolla";
}
@@ -283,7 +330,11 @@ static bool toyota_frame_plausible(uint32_t serial, uint8_t button, uint32_t hop
// Button must belong to the closed set of Toyota-observed codes.
const uint8_t b = button & 0x0FU;
if(variant == 1U) {
if(variant == 2U) {
// Prius variant C: Lock=0x0B, Unlock=0x0E, Trunk=0x0D, Panic=0x07
if(b != TOYOTA_C_BTN_LOCK && b != TOYOTA_C_BTN_UNLOCK &&
b != TOYOTA_C_BTN_TRUNK && b != TOYOTA_C_BTN_PANIC) return false;
} else if(variant == 1U) {
// Tundra variant B: Lock=0x0A, Unlock=0x05, L+U=0x0F, Trunk=0x04
if(b != TOYOTA_B_BTN_LOCK && b != TOYOTA_B_BTN_UNLOCK &&
b != 0x0FU && b != 0x04U) return false;
@@ -601,6 +652,162 @@ static void toyota_feed_variant_b(
}
}
/* ----------------------------------------------------------------
* FEED — Variant C (Prius 2006 / Corolla Verso, 433 MHz)
*
* Framing: >=6 short preamble pairs, then ONE extra-long LOW sync gap
* (1050-1500us), then PWM data where the HIGH pulse decides the bit
* (HIGH > 600us => 1, else 0). Bits are pushed MSB-first from the START of
* the data (anchored to the sync gap), so a jittery burst delivering 66..69
* HIGH pulses still decodes. Field layout over the leading bits (MSB-first):
* [32 hop][28 serial][8 tail]; button = high nibble of tail (bits[60..64)).
*
* NOTE on extraction: the JS _extractMSB(start,length) uses
* pos = (total-1) - (start+i)
* which is IDENTICAL to the existing C toyota_extract(inst, offset, length)
* with offset=start. So we reuse toyota_extract directly:
* hop = toyota_extract(inst, 0, 32)
* serial = toyota_extract(inst, 32, 28)
* button = toyota_extract(inst, 60, 4) & 0x0F
* ---------------------------------------------------------------- */
static void toyota_start_data_c(SubGhzProtocolDecoderToyota* inst) {
inst->bits_lo = 0;
inst->bits_hi = 0;
inst->bit_count = 0;
inst->have_high = false;
inst->decoder.parser_step = ToyotaStepDataC;
}
static void toyota_decode_and_fire_c(SubGhzProtocolDecoderToyota* inst) {
if(inst->bit_count < TOYOTA_C_BITS) return;
inst->hop = toyota_extract(inst, 0, 32);
inst->serial = toyota_extract(inst, 32, 28);
inst->button = (uint8_t)(toyota_extract(inst, 60, 4) & 0x0F);
if(!toyota_frame_plausible(inst->serial, inst->button, inst->hop, 2)) {
FURI_LOG_D(TAG, "REJECT(C): implausible serial=%08lX btn=%X hop=%08lX",
(unsigned long)inst->serial, (unsigned int)inst->button,
(unsigned long)inst->hop);
return;
}
inst->variant = 2;
inst->generic.data =
((uint64_t)inst->hop << 32) |
((uint64_t)inst->serial << 4) |
((uint64_t)inst->button & 0x0F);
inst->generic.data_count_bit = inst->bit_count;
inst->generic.serial = inst->serial;
inst->generic.btn = inst->button;
inst->generic.cnt = inst->variant;
inst->decoder.decode_data = inst->generic.data;
inst->decoder.decode_count_bit = inst->generic.data_count_bit;
FURI_LOG_D(TAG, "FIRE(C) bits=%d hop=%08lX serial=%07lX btn=%X",
(int)inst->bit_count,
(unsigned long)inst->hop,
(unsigned long)inst->serial,
(unsigned int)inst->button);
if(inst->base.callback)
inst->base.callback(&inst->base, inst->base.context);
}
static void toyota_feed_variant_c(
SubGhzProtocolDecoderToyota* inst,
bool level, uint32_t duration)
{
/* ── PREAMBLE ── */
if(inst->decoder.parser_step == ToyotaStepPreambleC) {
if(level) {
/* Remember the HIGH; validate it as a preamble HIGH on the LOW. */
inst->te_last = duration;
inst->have_high = true;
return;
}
if(!inst->have_high) {
subghz_protocol_decoder_toyota_reset(inst);
return;
}
inst->have_high = false;
/* XL LOW = sync gap: with enough preamble, begin data. */
if(duration >= TOYOTA_C_SYNC_MIN && duration <= TOYOTA_C_SYNC_MAX) {
if(inst->preamble_count >= TOYOTA_C_PREAMBLE_MIN) {
toyota_start_data_c(inst);
} else {
subghz_protocol_decoder_toyota_reset(inst);
}
return;
}
/* Count a preamble pair if HIGH & LOW both fit the (wide) preamble
* windows. Otherwise the sequence is noise -> restart. */
if(inst->te_last >= TOYOTA_C_PRE_HIGH_MIN && inst->te_last <= TOYOTA_C_PRE_HIGH_MAX &&
duration >= TOYOTA_C_PRE_LOW_MIN && duration <= TOYOTA_C_PRE_LOW_MAX) {
inst->preamble_count++;
return;
}
subghz_protocol_decoder_toyota_reset(inst);
return;
}
/* ── DATA (PWM, HIGH decides bit) ── */
if(inst->decoder.parser_step == ToyotaStepDataC) {
if(level) {
/* HIGH pulse decides the bit (midpoint classifier). Reject HIGHs
* that are implausibly short/long -> end frame (maybe noise). */
if(duration >= TOYOTA_C_DATA_HIGH_MIN && duration <= TOYOTA_C_DATA_HIGH_MAX) {
if(inst->bit_count < TOYOTA_C_FRAME_BITS) {
toyota_push_bit(inst, (duration > TOYOTA_C_HIGH_MIDPOINT) ? 1 : 0);
}
inst->have_high = true;
inst->te_last = duration;
} else {
/* Out-of-range HIGH: terminate current frame. */
if(inst->bit_count >= TOYOTA_C_BITS) toyota_decode_and_fire_c(inst);
/* A large HIGH could be a preamble HIGH of the next burst;
* restart preamble tracking with this HIGH. */
subghz_protocol_decoder_toyota_reset(inst);
inst->variant = 2;
inst->decoder.parser_step = ToyotaStepPreambleC;
inst->te_last = duration;
inst->have_high = true;
}
return;
}
/* LOW pulse (complementary / gap). */
inst->have_high = false;
if(duration >= TOYOTA_C_SYNC_MIN && duration <= TOYOTA_C_SYNC_MAX) {
/* A fresh sync gap: fire the frame we have, then start a new one. */
if(inst->bit_count >= TOYOTA_C_BITS) toyota_decode_and_fire_c(inst);
toyota_start_data_c(inst);
return;
}
if(duration > TOYOTA_C_END_LOW) {
/* Oversized gap ends the frame; go back to hunting a preamble. */
if(inst->bit_count >= TOYOTA_C_BITS) toyota_decode_and_fire_c(inst);
subghz_protocol_decoder_toyota_reset(inst);
inst->variant = 2;
inst->decoder.parser_step = ToyotaStepPreambleC;
inst->preamble_count = 0;
}
/* else: normal short/medium complementary LOW -> ignore. */
}
}
/* ----------------------------------------------------------------
* Public feed — dispatcher
* ---------------------------------------------------------------- */
@@ -631,12 +838,21 @@ void subghz_protocol_decoder_toyota_feed(void* context, bool level, uint32_t dur
FURI_LOG_D(TAG, "Detected Variant B (Tundra), first HIGH=%lu",
(unsigned long)duration);
} else if(fits_a) {
inst->variant = 0;
/*
* Variant A and C share 400/800 timing. Real-world 433 fobs
* (Prius / Corolla Verso) use the Variant C framing (short-short
* preamble + XL sync gap + PWM data where the HIGH decides the
* bit), so route the shared-timing path to the Variant C state
* machine. The Variant A state machine remains reachable via the
* dispatcher's variant==0 fallback (kept referenced to avoid
* -Werror=unused-function).
*/
inst->variant = 2;
inst->te_last = duration;
inst->have_high = true;
inst->preamble_count = 0;
inst->decoder.parser_step = ToyotaStepPreambleA;
FURI_LOG_D(TAG, "Detected Variant A (Corolla), first HIGH=%lu",
inst->decoder.parser_step = ToyotaStepPreambleC;
FURI_LOG_D(TAG, "Detected Variant C (Prius/Corolla Verso), first HIGH=%lu",
(unsigned long)duration);
}
return;
@@ -644,6 +860,8 @@ void subghz_protocol_decoder_toyota_feed(void* context, bool level, uint32_t dur
if(inst->variant == 1) {
toyota_feed_variant_b(inst, level, duration);
} else if(inst->variant == 2) {
toyota_feed_variant_c(inst, level, duration);
} else {
toyota_feed_variant_a(inst, level, duration);
}
@@ -687,11 +905,20 @@ SubGhzProtocolStatus subghz_protocol_decoder_toyota_deserialize(
furi_assert(context);
SubGhzProtocolDecoderToyota* inst = context;
/*
* Accept any saved frame whose bit count is at least TOYOTA_C_BITS (66).
* Variant B is exactly 67, Variant A exactly 68, and Variant C real
* captures deliver 66-69 bits, so an exact-match check cannot cover all
* three. Use the plain deserializer plus a lower-bound guard.
*/
SubGhzProtocolStatus ret =
subghz_block_generic_deserialize_check_count_bit(
&inst->generic,
flipper_format,
toyota_const_b.min_count_bit_for_found);
subghz_block_generic_deserialize(&inst->generic, flipper_format);
if(ret == SubGhzProtocolStatusOk &&
inst->generic.data_count_bit < TOYOTA_C_BITS) {
FURI_LOG_D(TAG, "Wrong number of bits in key");
ret = SubGhzProtocolStatusErrorValueBitCount;
}
if(ret == SubGhzProtocolStatusOk) {
inst->hop = (uint32_t)(inst->generic.data >> 32);
@@ -700,7 +927,9 @@ SubGhzProtocolStatus subghz_protocol_decoder_toyota_deserialize(
inst->generic.serial = inst->serial;
inst->generic.btn = inst->button;
inst->variant = (inst->generic.cnt != 0) ? 1 : 0;
/* Persisted cnt: 0=Corolla(A), 1=Tundra(B), 2=Prius(C). Preserve 2. */
inst->variant = (inst->generic.cnt == 2) ? 2 :
((inst->generic.cnt != 0) ? 1 : 0);
inst->generic.cnt = inst->variant;
}
@@ -717,7 +946,8 @@ void subghz_protocol_decoder_toyota_get_string(void* context, FuriString* output
uint32_t serial = (uint32_t)((inst->generic.data >> 4) & 0x0FFFFFFF);
uint8_t button = (uint8_t)(inst->generic.data & 0x0F);
uint8_t var = (inst->generic.cnt != 0) ? 1 : 0;
uint8_t var = (inst->generic.cnt == 2) ? 2 :
((inst->generic.cnt != 0) ? 1 : 0);
furi_string_cat_printf(
output,
+1
View File
@@ -34,6 +34,7 @@ env.Append(
File("stream/file_stream.h"),
File("stream/string_stream.h"),
File("stream/buffered_file_stream.h"),
File("buffer_stream.h"),
File("strint.h"),
File("pipe.h"),
File("protocols/protocol_dict.h"),
+11 -1
View File
@@ -1,5 +1,5 @@
entry,status,name,type,params
Version,+,92.0,,
Version,+,92.1,,
Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,,
Header,+,applications/services/applications.h,,
Header,+,applications/services/bt/bt_service/bt.h,,
@@ -156,6 +156,7 @@ Header,+,lib/stm32wb_hal/Inc/stm32wbxx_ll_wwdg.h,,
Header,+,lib/toolbox/api_lock.h,,
Header,+,lib/toolbox/args.h,,
Header,+,lib/toolbox/bit_buffer.h,,
Header,+,lib/toolbox/buffer_stream.h,,
Header,+,lib/toolbox/cli/cli_ansi.h,,
Header,+,lib/toolbox/cli/cli_command.h,,
Header,+,lib/toolbox/cli/cli_registry.h,,
@@ -743,6 +744,15 @@ Function,+,bt_set_settings,void,"Bt*, const BtSettings*"
Function,+,bt_set_status_changed_callback,void,"Bt*, BtStatusChangedCallback, void*"
Function,+,bt_settings_load,void,BtSettings*
Function,+,bt_settings_save,void,const BtSettings*
Function,+,buffer_get_data,uint8_t*,Buffer*
Function,+,buffer_get_size,size_t,Buffer*
Function,+,buffer_reset,void,Buffer*
Function,+,buffer_stream_alloc,BufferStream*,"size_t, size_t"
Function,+,buffer_stream_free,void,BufferStream*
Function,+,buffer_stream_get_overrun_count,size_t,BufferStream*
Function,+,buffer_stream_receive,Buffer*,"BufferStream*, uint32_t"
Function,+,buffer_stream_reset,void,BufferStream*
Function,+,buffer_stream_send_from_isr,_Bool,"BufferStream*, const uint8_t*, size_t"
Function,+,buffered_file_stream_alloc,Stream*,Storage*
Function,+,buffered_file_stream_close,_Bool,Stream*
Function,+,buffered_file_stream_get_error,FS_Error,Stream*
1 entry status name type params
2 Version + 92.0 92.1
3 Header + applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h
4 Header + applications/services/applications.h
5 Header + applications/services/bt/bt_service/bt.h
156 Header + lib/toolbox/api_lock.h
157 Header + lib/toolbox/args.h
158 Header + lib/toolbox/bit_buffer.h
159 Header + lib/toolbox/buffer_stream.h
160 Header + lib/toolbox/cli/cli_ansi.h
161 Header + lib/toolbox/cli/cli_command.h
162 Header + lib/toolbox/cli/cli_registry.h
744 Function + bt_set_status_changed_callback void Bt*, BtStatusChangedCallback, void*
745 Function + bt_settings_load void BtSettings*
746 Function + bt_settings_save void const BtSettings*
747 Function + buffer_get_data uint8_t* Buffer*
748 Function + buffer_get_size size_t Buffer*
749 Function + buffer_reset void Buffer*
750 Function + buffer_stream_alloc BufferStream* size_t, size_t
751 Function + buffer_stream_free void BufferStream*
752 Function + buffer_stream_get_overrun_count size_t BufferStream*
753 Function + buffer_stream_receive Buffer* BufferStream*, uint32_t
754 Function + buffer_stream_reset void BufferStream*
755 Function + buffer_stream_send_from_isr _Bool BufferStream*, const uint8_t*, size_t
756 Function + buffered_file_stream_alloc Stream* Storage*
757 Function + buffered_file_stream_close _Bool Stream*
758 Function + buffered_file_stream_get_error FS_Error Stream*