Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 018a5feb29 | |||
| 4478f99dfe | |||
| ad4c171054 | |||
| 048fcc39e4 | |||
| f5c211041b | |||
| 589a2e36f2 | |||
| 161e26f2dc | |||
| bf9ca01621 | |||
| 86f5aae002 | |||
| 46f3a5c993 | |||
| 52015fb289 | |||
| 23ba62cd69 | |||
| cd1e9d6945 | |||
| c49b843096 | |||
| 0c35337bb7 | |||
| e419b9865a | |||
| a89cb55529 | |||
| efa653c7cf | |||
| 07957617e5 | |||
| 903104239b | |||
| 291c5320bb | |||
| edbc2f291e | |||
| c32ee61a4f | |||
| 0995609391 | |||
| 29fef56be1 | |||
| 6a348dd304 | |||
| 32a96e580d | |||
| 54f03a39c2 | |||
| a55189e2a4 | |||
| 14d10c0794 | |||
| 27818ccb1f | |||
| 0ebf26eff4 | |||
| ac620e2b0e | |||
| 46115cdf6c | |||
| f465c6edbb | |||
| ad795ae7ef | |||
| efff8d2f2e | |||
| c9c9c74117 | |||
| dc0f30dad9 | |||
| 38f261e23b | |||
| cb1daaa4f1 | |||
| b318b3e9ff | |||
| 117381e5a1 | |||
| 702cf5abc8 | |||
| 17011180d1 | |||
| d85657b6b3 | |||
| 2fd01bb911 | |||
| d23a892a16 | |||
| 16d06d75fe | |||
| aaf16ec0de | |||
| cdd7c56b69 | |||
| 0fd985c67a | |||
| e93606aa87 | |||
| 3933a77b72 |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
@@ -81,4 +81,12 @@ node_modules/
|
||||
|
||||
#companion app
|
||||
/companion
|
||||
/Flipper-Android-App
|
||||
/Flipper-Android-App
|
||||
|
||||
#WIP not ready to push protocols
|
||||
|
||||
lib/subghz/protocols/subghz_protocol_honda_pandora.c
|
||||
lib/subghz/protocols/honda_rolling.c
|
||||
lib/subghz/protocols/honda_rolling.h
|
||||
lib/subghz/protocols/honda_pandora.c
|
||||
lib/subghz/protocols/honda_pandora.h
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
---
|
||||
|
||||
### Added
|
||||
- Protocol name allowlist filter: in Receiver Config, a new "Proto Filter"
|
||||
field accepts a comma-separated list of protocol names (e.g. "Ford V2,VAG").
|
||||
When set, the receiver ignores all decoded signals that are not in the list,
|
||||
reducing RAM usage and increasing the chance of capturing the target protocol.
|
||||
Leave empty to disable (default behavior, all protocols accepted).
|
||||
Setting is persisted in last_subghz.settings under the ProtocolFilter key.
|
||||
|
||||
### Changed
|
||||
- Protocol Filter: replaced free-text input with a dedicated protocol list
|
||||
scene (Proto Filter in Receiver Config). All registered protocols are shown
|
||||
as toggleable items (--- / ONLY). Selecting one or more protocols restricts
|
||||
the receiver to only show those; leaving all as --- disables the filter.
|
||||
The active count is shown inline in Receiver Config ("N set" or "All").
|
||||
Filter is persisted across sessions and cleared by Reset to default.
|
||||
@@ -22,6 +22,7 @@ This project may incorporate, adapt, or build upon **other open-source projects*
|
||||
- [Contribution Policy](#contribution-policy)
|
||||
- [Citations & References](#citations--references)
|
||||
- [Disclaimer](#disclaimer)
|
||||
- [Special Thanks](#special-thanks-to-everyone-who-contributes-to-this-project)
|
||||
|
||||
---
|
||||
|
||||
@@ -35,6 +36,8 @@ This project may incorporate, adapt, or build upon **other open-source projects*
|
||||
| Keeloq Key Manager | Mod Hopping Config |
|
||||
|  |  |
|
||||
| PSA XTEA Decrypt | Counter BruteForce |
|
||||
|  |  |
|
||||
| Custom Emulation Settings | Custom Emulation Scene |
|
||||
|
||||
---
|
||||
|
||||
@@ -48,8 +51,10 @@ This project may incorporate, adapt, or build upon **other open-source projects*
|
||||
| Porsche | Porsche AG | 433/868 MHz | AM | Yes | Yes | No |
|
||||
| PSA (Peugeot/Citroën/DS) | PSA GROUP | 433 MHz | AM/FM | Yes | Yes | Yes |
|
||||
| Ford | Ford V0 | 315/433 MHz | AM | Yes | Yes | Yes |
|
||||
| Ford | Ford V1 | 315/433 MHz | FM | Yes | Yes | Yes |
|
||||
| Fiat | Fiat SpA | 433 MHz | AM | Yes | Yes | Yes |
|
||||
| Fiat | Marelli/Delphi | 433 MHz | AM | No | Yes | No |
|
||||
| Fiat | Marelli/Delphi | 433 MHz | AM | No | Yes | Yes |
|
||||
| Renault (old models) | Marelli | 433 MHz | AM | No | Yes | No|
|
||||
| Mazda | Siemens (5WK49365D) | 315/433 MHz | AM/FM | Yes | Yes | Yes |
|
||||
| Kia/Hyundai | KIA/HYU V0 | 433 MHz | FM | Yes | Yes | Yes |
|
||||
| Kia/Hyundai | KIA/HYU V1 | 315/433 MHz | AM | Yes | Yes | Yes |
|
||||
@@ -57,9 +62,17 @@ This project may incorporate, adapt, or build upon **other open-source projects*
|
||||
| Kia/Hyundai | KIA/HYU V3/V4 | 315/433 MHz | AM/FM | Yes | Yes | Yes |
|
||||
| Kia/Hyundai | KIA/HYU V5 | 433 MHz | FM | Yes | Yes | Yes |
|
||||
| Kia/Hyundai | KIA/HYU V6 | 433 MHz | FM | Yes | Yes | Yes |
|
||||
| Kia/Hyundai | KIA V7 | 433 MHz | FM | Yes | Yes | Yes |
|
||||
| Subaru | Subaru | 433 MHz | AM | Yes | Yes | No |
|
||||
| Suzuki | Suzuki | 433 MHz | FM | Yes | Yes | Yes |
|
||||
| Mitsubishi | Mitsubishi V0 | 868 MHz | FM | Yes | Yes | No |
|
||||
| Honda | Honda Type A/B | 433 MHz | FM (custom) | Yes | Yes | No |
|
||||
| Starline | Star Line | 433 MHz | AM | Yes | Yes | No |
|
||||
| Scher-Khan | Scher-Khan | 433 MHz | FM | Yes | Yes | No |
|
||||
| Scher-Khan | Magic Code PRO1/PRO2 | 433 MHz | FM | Yes | Yes | Yes |
|
||||
| Sheriff | Sheriff CFM (ZX-750/930) | 433 MHz | AM | Yes | Yes | No |
|
||||
| Chrysler/Dodge/Jeep | FOBIK GQ43VT | 315/433 MHz | AM | Yes | Yes | No |
|
||||
| Honda | Honda Static | 433 MHz | AM | Yes | Yes | No |
|
||||
|
||||
### Gate / Access Protocols
|
||||
|
||||
@@ -338,3 +351,36 @@ THIS SOFTWARE IS PROVIDED **"AS IS,"** WITHOUT ANY WARRANTIES OF ANY KIND, EXPRE
|
||||
IN NO EVENT SHALL THE AUTHORS, COPYRIGHT HOLDERS, OR CONTRIBUTORS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR ITS USE.
|
||||
|
||||
**ALL RISKS FROM THE USE OR PERFORMANCE OF THIS SOFTWARE REMAIN WITH THE USER.**
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Special thanks to everyone who contributes to this project:
|
||||
|
||||
## Contributors (GitHub)
|
||||
|
||||
<a href="https://github.com/d4c1-labs/Flipper-ARF/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=d4c1-labs/Flipper-ARF"/>
|
||||
</a>
|
||||
|
||||
## Special Thanks
|
||||
|
||||
<table align="center">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/whatthefxck">
|
||||
<img src="https://avatars.githubusercontent.com/whatthefxck?s=80" width="80" height="80" alt="whatthefxck"/>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/zero-mega">
|
||||
<img src="https://avatars.githubusercontent.com/zero-mega?s=80" width="80" height="80" alt="zero-mega"/>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p align="center">
|
||||
Special thanks to everyone who contributed code, testing, reversing,
|
||||
research, ideas, captures and documentation.
|
||||
</p>
|
||||
|
||||
@@ -58,11 +58,16 @@ typedef enum {
|
||||
SubGhzCustomEventViewTransmitterSendStart,
|
||||
SubGhzCustomEventViewTransmitterSendStop,
|
||||
SubGhzCustomEventViewTransmitterError,
|
||||
SubGhzCustomEventViewTransmitterPageChange,
|
||||
|
||||
SubGhzCustomEventViewFreqAnalOkShort,
|
||||
SubGhzCustomEventViewFreqAnalOkLong,
|
||||
|
||||
SubGhzCustomEventByteInputDone,
|
||||
SubGhzCustomEventCarEmulateTransmit,
|
||||
SubGhzCustomEventCarEmulateStop,
|
||||
SubGhzCustomEventCarEmulateExit,
|
||||
|
||||
} SubGhzCustomEvent;
|
||||
|
||||
typedef enum {
|
||||
|
||||
@@ -94,6 +94,7 @@ typedef enum {
|
||||
SubGhzViewIdReadRAW,
|
||||
SubGhzViewIdPsaDecrypt,
|
||||
SubGhzViewIdKeeloqDecrypt,
|
||||
SubGhzViewIdCarEmulate,
|
||||
|
||||
} SubGhzViewId;
|
||||
|
||||
|
||||
@@ -119,3 +119,13 @@ Custom_preset_data: 02 0D 07 04 08 32 0B 06 10 67 11 83 12 04 13 02 15 24 18 18
|
||||
Custom_preset_name: FM15k
|
||||
Custom_preset_module: CC1101
|
||||
Custom_preset_data: 02 0D 03 47 08 32 0B 06 10 A7 11 32 12 00 13 00 14 00 15 32 18 18 19 1D 1B 04 1C 00 1D 92 20 FB 21 B6 22 17 00 00 00 12 0E 34 60 C5 C1 C0
|
||||
|
||||
Custom_preset_name: Honda1
|
||||
Custom_preset_module: CC1101
|
||||
# G2 G3 G4 D L0 L1 L2
|
||||
Custom_preset_data: 02 0D 0B 06 08 32 07 04 14 00 13 02 12 04 11 36 10 69 15 32 18 18 19 16 1D 91 1C 00 1B 07 20 FB 22 10 21 56 00 00 C0 00 00 00 00 00 00 00
|
||||
|
||||
Custom_preset_name: Honda2
|
||||
Custom_preset_module: CC1101
|
||||
# G2 G3 G4 D L0 L1 L2
|
||||
Custom_preset_data: 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
|
||||
|
||||
@@ -0,0 +1,499 @@
|
||||
/**
|
||||
* Scene: CarEmulate
|
||||
* Custom automotive-key emulation GUI ported from ProtoPirate.
|
||||
* Activated when SubGhzLastSettings::custom_car_emulate == true and the
|
||||
* user presses "Emulate" on a saved dynamic protocol.
|
||||
*
|
||||
* Flow:
|
||||
* SavedMenu → Emulate → (custom_car_emulate?) CarEmulate : Transmitter
|
||||
*/
|
||||
#include "../subghz_i.h"
|
||||
#include "../views/subghz_car_emulate.h"
|
||||
#include "../helpers/subghz_custom_event.h"
|
||||
#include <lib/subghz/blocks/generic.h>
|
||||
#include <notification/notification_messages.h>
|
||||
#include "../helpers/subghz_txrx_i.h"
|
||||
#include <lib/subghz/blocks/custom_btn_i.h>
|
||||
|
||||
#define TAG "SubGhzSceneCarEmulate"
|
||||
#define MIN_TX_TICKS 66U /* ~666 ms at 100 ms tick */
|
||||
|
||||
/* ── Per-session state (heap, freed on exit) ─────────────────────────────── */
|
||||
typedef struct {
|
||||
/* Signal metadata read from fff_data */
|
||||
char protocol_name[48];
|
||||
uint32_t serial;
|
||||
uint8_t original_button;
|
||||
uint32_t original_counter;
|
||||
uint32_t current_counter;
|
||||
uint32_t freq;
|
||||
char preset_short[12]; /* "AM650", "FM476", … */
|
||||
|
||||
/* TX state */
|
||||
bool is_transmitting;
|
||||
bool stop_pending; /* stop requested before MIN_TX_TICKS elapsed */
|
||||
uint32_t tx_start_tick;
|
||||
|
||||
/* Pending button key (InputKey) decoded from the packed custom event */
|
||||
uint8_t pending_button;
|
||||
} CarEmulateState;
|
||||
|
||||
static CarEmulateState* s_state = NULL;
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* Button mapping (protocol-name → InputKey → button byte)
|
||||
* Ported verbatim from protopirate_scene_emulate.c
|
||||
* ═════════════════════════════════════════════════════════════════════════*/
|
||||
//static uint8_t car_emulate_map_button(
|
||||
// const char* protocol,
|
||||
// InputKey key,
|
||||
// uint8_t original) {
|
||||
|
||||
/* Land Rover V0 */
|
||||
// if(strstr(protocol, "Land Rover")) {
|
||||
// switch(key) {
|
||||
// case InputKeyUp: return 0x02; /* Lock */
|
||||
// case InputKeyOk: return 0x04; /* Unlock */
|
||||
// default: return original;
|
||||
// }
|
||||
// }
|
||||
/* Mazda */
|
||||
// if(strstr(protocol, "Mazda")) {
|
||||
// switch(key) {
|
||||
// case InputKeyUp: return 0x01;
|
||||
// case InputKeyOk: return 0x02;
|
||||
// case InputKeyDown: return 0x04;
|
||||
// case InputKeyRight: return 0x08;
|
||||
// default: return original;
|
||||
// }
|
||||
// }
|
||||
/* PSA */
|
||||
// if(strstr(protocol, "PSA")) {
|
||||
// switch(key) {
|
||||
// case InputKeyUp: return 0x1;
|
||||
// case InputKeyOk: return 0x2;
|
||||
// case InputKeyDown: return 0x4;
|
||||
// case InputKeyLeft: return 0x8;
|
||||
// default: return original;
|
||||
// }
|
||||
// }
|
||||
/* VAG */
|
||||
// if(strstr(protocol, "VAG")) {
|
||||
// if(original == 0x10 || original == 0x20 || original == 0x40) {
|
||||
// switch(key) {
|
||||
// case InputKeyUp: return 0x20;
|
||||
// case InputKeyOk: return 0x10;
|
||||
// case InputKeyDown: return 0x40;
|
||||
// default: return original;
|
||||
// }
|
||||
// }
|
||||
// switch(key) {
|
||||
// case InputKeyUp: return 0x2;
|
||||
// case InputKeyOk: return 0x1;
|
||||
// case InputKeyDown: return 0x4;
|
||||
// case InputKeyLeft: return 0x8;
|
||||
// case InputKeyRight: return 0x3;
|
||||
// default: return original;
|
||||
// }
|
||||
// }
|
||||
/* Honda Static */
|
||||
// if(strstr(protocol, "Honda Static")) {
|
||||
// switch(key) {
|
||||
// case InputKeyUp: return 0x1;
|
||||
// case InputKeyOk: return 0x2;
|
||||
// case InputKeyDown: return 0x4;
|
||||
// case InputKeyRight: return 0x5;
|
||||
// case InputKeyLeft: return 0x8;
|
||||
// default: return original;
|
||||
// }
|
||||
// }
|
||||
/* Ford */
|
||||
// if(strstr(protocol, "Ford")) {
|
||||
// switch(key) {
|
||||
// case InputKeyLeft: return 0x1;
|
||||
// case InputKeyUp: return 0x2;
|
||||
// case InputKeyOk: return 0x4;
|
||||
// case InputKeyDown: return 0x8;
|
||||
// case InputKeyRight: return 0x10;
|
||||
// default: return original;
|
||||
// }
|
||||
// }
|
||||
/* Chrysler */
|
||||
// if(strstr(protocol, "Chrysler")) {
|
||||
// switch(key) {
|
||||
// case InputKeyUp: return 0x1;
|
||||
// case InputKeyOk: return 0x2;
|
||||
// default: return original;
|
||||
// }
|
||||
// }
|
||||
/* Subaru */
|
||||
// if(strstr(protocol, "Subaru")) {
|
||||
// switch(key) {
|
||||
// case InputKeyUp: return 0x1;
|
||||
// case InputKeyOk: return 0x2;
|
||||
// case InputKeyDown: return 0x3;
|
||||
// case InputKeyLeft: return 0x4;
|
||||
// case InputKeyRight: return 0x8;
|
||||
// default: return original;
|
||||
// }
|
||||
// }
|
||||
/* Fiat V1 */
|
||||
// if(strstr(protocol, "Fiat V1")) {
|
||||
// switch(key) {
|
||||
// case InputKeyUp: return 0x8;
|
||||
// case InputKeyOk: return 0x0;
|
||||
// case InputKeyDown: return 0xD;
|
||||
// default: return original;
|
||||
// }
|
||||
// }
|
||||
/* Generic KeeLoq / KIA etc. – simple 4-button layout */
|
||||
// if(strstr(protocol, "Kia") || strstr(protocol, "KIA") ||
|
||||
// strstr(protocol, "KeeLoq") || strstr(protocol, "Keeloq")) {
|
||||
// switch(key) {
|
||||
// case InputKeyUp: return 0x1;
|
||||
// case InputKeyOk: return 0x2;
|
||||
// case InputKeyDown: return 0x3;
|
||||
// case InputKeyLeft: return 0x4;
|
||||
// case InputKeyRight: return 0x8;
|
||||
// default: return original;
|
||||
// }
|
||||
// }
|
||||
|
||||
// return original;
|
||||
//}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* TX helpers
|
||||
* ═════════════════════════════════════════════════════════════════════════*/
|
||||
|
||||
/**
|
||||
* Read frequency and short preset name from fff_data.
|
||||
* Falls back to 433.92 MHz / "AM650" on failure.
|
||||
*/
|
||||
static void car_emulate_read_freq_preset(SubGhz* subghz, CarEmulateState* st) {
|
||||
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
|
||||
|
||||
st->freq = 433920000UL;
|
||||
strncpy(st->preset_short, "AM650", sizeof(st->preset_short) - 1);
|
||||
|
||||
if(!fff) return;
|
||||
|
||||
uint32_t freq = 0;
|
||||
flipper_format_rewind(fff);
|
||||
if(flipper_format_read_uint32(fff, "Frequency", &freq, 1) && freq > 0) {
|
||||
st->freq = freq;
|
||||
}
|
||||
|
||||
FuriString* preset_str = furi_string_alloc();
|
||||
flipper_format_rewind(fff);
|
||||
if(flipper_format_read_string(fff, "Preset", preset_str)) {
|
||||
/* Convert long FuriHal name → short token used by the setting */
|
||||
const char* raw = furi_string_get_cstr(preset_str);
|
||||
const char* short_name = "AM650";
|
||||
if(strstr(raw, "Ook270")) short_name = "AM270";
|
||||
else if(strstr(raw, "Ook650")) short_name = "AM650";
|
||||
else if(strstr(raw, "238")) short_name = "FM238";
|
||||
else if(strstr(raw, "12K")) short_name = "FM12K";
|
||||
else if(strstr(raw, "476")) short_name = "FM476";
|
||||
else if(strstr(raw, "Custom")) short_name = "CUST";
|
||||
strncpy(st->preset_short, short_name, sizeof(st->preset_short) - 1);
|
||||
}
|
||||
furi_string_free(preset_str);
|
||||
}
|
||||
|
||||
/** Update Btn and Cnt fields in fff_data so the transmitter re-serialises them. */
|
||||
static void car_emulate_apply_button(SubGhz* subghz, InputKey key) {
|
||||
UNUSED(subghz);
|
||||
|
||||
uint8_t custom_btn_id;
|
||||
switch(key) {
|
||||
case InputKeyUp: custom_btn_id = SUBGHZ_CUSTOM_BTN_UP; break;
|
||||
case InputKeyDown: custom_btn_id = SUBGHZ_CUSTOM_BTN_DOWN; break;
|
||||
case InputKeyLeft: custom_btn_id = SUBGHZ_CUSTOM_BTN_LEFT; break;
|
||||
case InputKeyRight: custom_btn_id = SUBGHZ_CUSTOM_BTN_RIGHT; break;
|
||||
case InputKeyOk:
|
||||
default: custom_btn_id = SUBGHZ_CUSTOM_BTN_OK; break;
|
||||
}
|
||||
|
||||
subghz_custom_btn_set(custom_btn_id);
|
||||
}
|
||||
|
||||
|
||||
/** Update Cnt in fff_data (Btn is handled by the protocol via custom_btn). */
|
||||
static void car_emulate_update_fff(SubGhz* subghz, uint32_t counter) {
|
||||
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
|
||||
if(!fff) return;
|
||||
flipper_format_rewind(fff);
|
||||
flipper_format_insert_or_update_uint32(fff, "Cnt", &counter, 1);
|
||||
}
|
||||
|
||||
|
||||
/** Apply tx_power to the current preset and start a single transmission burst. */
|
||||
static bool car_emulate_start_tx(SubGhz* subghz, uint8_t custom_btn_id) {
|
||||
SubGhzRadioPreset preset = subghz_txrx_get_preset(subghz->txrx);
|
||||
if(preset.data && preset.data_size > 0 && subghz->tx_power > 0) {
|
||||
subghz_txrx_set_tx_power(preset.data, preset.data_size, subghz->tx_power);
|
||||
FURI_LOG_I(TAG, "TX power index applied: %u", subghz->tx_power);
|
||||
}
|
||||
|
||||
subghz_custom_btn_set(custom_btn_id);
|
||||
|
||||
bool ok = subghz_tx_start(subghz, subghz_txrx_get_fff_data(subghz->txrx));
|
||||
if(ok) {
|
||||
subghz->state_notifications = SubGhzNotificationStateTx;
|
||||
notification_message(subghz->notifications, &sequence_blink_magenta_10);
|
||||
FURI_LOG_I(TAG, "TX started");
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "subghz_tx_start failed");
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
|
||||
/** Stop an active transmission. */
|
||||
static void car_emulate_stop_tx(SubGhz* subghz) {
|
||||
subghz_txrx_stop(subghz->txrx);
|
||||
subghz->state_notifications = SubGhzNotificationStateIDLE;
|
||||
notification_message(subghz->notifications, &sequence_blink_stop);
|
||||
FURI_LOG_I(TAG, "TX stopped");
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* View callback (fired from the View's input handler)
|
||||
* ═════════════════════════════════════════════════════════════════════════*/
|
||||
static void subghz_scene_car_emulate_view_callback(uint32_t event, void* context) {
|
||||
SubGhz* subghz = context;
|
||||
view_dispatcher_send_custom_event(subghz->view_dispatcher, event);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* Helpers to keep the view in sync
|
||||
* ═════════════════════════════════════════════════════════════════════════*/
|
||||
static void car_emulate_refresh_view(SubGhz* subghz) {
|
||||
furi_assert(s_state);
|
||||
subghz_car_emulate_view_set_data(
|
||||
subghz->car_emulate_view,
|
||||
s_state->protocol_name,
|
||||
s_state->serial,
|
||||
s_state->current_counter,
|
||||
s_state->original_counter,
|
||||
s_state->freq,
|
||||
s_state->preset_short,
|
||||
s_state->is_transmitting);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* Scene on_enter
|
||||
* ═════════════════════════════════════════════════════════════════════════*/
|
||||
void subghz_scene_car_emulate_on_enter(void* context) {
|
||||
SubGhz* subghz = context;
|
||||
furi_assert(subghz);
|
||||
|
||||
/* Allocate per-session state */
|
||||
s_state = malloc(sizeof(CarEmulateState));
|
||||
furi_check(s_state);
|
||||
memset(s_state, 0, sizeof(CarEmulateState));
|
||||
|
||||
/* ── Read metadata from the loaded fff_data ── */
|
||||
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
|
||||
if(fff) {
|
||||
FuriString* tmp = furi_string_alloc();
|
||||
|
||||
flipper_format_rewind(fff);
|
||||
if(flipper_format_read_string(fff, "Protocol", tmp)) {
|
||||
strncpy(
|
||||
s_state->protocol_name,
|
||||
furi_string_get_cstr(tmp),
|
||||
sizeof(s_state->protocol_name) - 1);
|
||||
}
|
||||
|
||||
flipper_format_rewind(fff);
|
||||
flipper_format_read_uint32(fff, "Serial", &s_state->serial, 1);
|
||||
|
||||
flipper_format_rewind(fff);
|
||||
uint32_t btn_tmp = 0;
|
||||
if(flipper_format_read_uint32(fff, "Btn", &btn_tmp, 1)) {
|
||||
s_state->original_button = (uint8_t)btn_tmp;
|
||||
}
|
||||
|
||||
flipper_format_rewind(fff);
|
||||
flipper_format_read_uint32(fff, "Cnt", &s_state->original_counter, 1);
|
||||
s_state->current_counter = s_state->original_counter;
|
||||
|
||||
furi_string_free(tmp);
|
||||
}
|
||||
|
||||
/* ── Initialize the custom_btn system ──────────────────────────────────
|
||||
* Reset first so any leftover state from a previous session is cleared.
|
||||
* Then deserialize the decoder once: this causes the protocol's own
|
||||
* deserialize() to call subghz_custom_btn_set_original() and
|
||||
* subghz_custom_btn_set_max(), which is exactly what the standard
|
||||
* Transmitter scene does via subghz_scene_transmitter_update_data_show().
|
||||
* After this call:
|
||||
* - subghz_custom_btn_get_original() → the button that was in the file
|
||||
* - subghz_custom_btn_is_allowed() → true if protocol supports it
|
||||
* - subghz_custom_btn_get_max() → number of buttons available */
|
||||
subghz_custom_btns_reset();
|
||||
|
||||
SubGhzProtocolDecoderBase* decoder = subghz_txrx_get_decoder(subghz->txrx);
|
||||
if(decoder && fff) {
|
||||
flipper_format_rewind(fff);
|
||||
subghz_protocol_decoder_base_deserialize(decoder, fff);
|
||||
/* Rewind again so subsequent reads in car_emulate_read_freq_preset()
|
||||
* start from the beginning of the file. */
|
||||
flipper_format_rewind(fff);
|
||||
}
|
||||
|
||||
subghz_car_emulate_view_set_labels(
|
||||
subghz->car_emulate_view,
|
||||
"UNLOCK", /* OK */
|
||||
"LOCK", /* Up */
|
||||
"TRUNK", /* Down */
|
||||
"PANIC", /* Left */
|
||||
"START" /* Right */
|
||||
);
|
||||
|
||||
car_emulate_read_freq_preset(subghz, s_state);
|
||||
|
||||
/* ── Configure the view ── */
|
||||
subghz_car_emulate_view_set_callback(
|
||||
subghz->car_emulate_view, subghz_scene_car_emulate_view_callback, subghz);
|
||||
|
||||
car_emulate_refresh_view(subghz);
|
||||
|
||||
subghz->state_notifications = SubGhzNotificationStateIDLE;
|
||||
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdCarEmulate);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* Scene on_event
|
||||
* ═════════════════════════════════════════════════════════════════════════*/
|
||||
bool subghz_scene_car_emulate_on_event(void* context, SceneManagerEvent event) {
|
||||
SubGhz* subghz = context;
|
||||
furi_assert(s_state);
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
|
||||
/* ── Transmit ── */
|
||||
if((event.event & 0xFFFFU) == SubGhzCustomEventCarEmulateTransmit) {
|
||||
InputKey key = (InputKey)((event.event >> 16) & 0xFFU);
|
||||
|
||||
/* Stop any ongoing TX first */
|
||||
if(subghz->state_notifications == SubGhzNotificationStateTx) {
|
||||
car_emulate_stop_tx(subghz);
|
||||
}
|
||||
|
||||
/* Bump counter */
|
||||
s_state->current_counter++;
|
||||
|
||||
/* Set the custom button BEFORE deserialize() is called inside
|
||||
* subghz_tx_start() → subghz_txrx_tx_start().
|
||||
* The protocol's deserialize() will call subghz_custom_btn_get()
|
||||
* to pick the right button code. */
|
||||
car_emulate_apply_button(subghz, key);
|
||||
|
||||
/* Only update the counter in fff_data; the protocol handles Btn. */
|
||||
car_emulate_update_fff(subghz, s_state->current_counter);
|
||||
|
||||
s_state->is_transmitting = true;
|
||||
s_state->stop_pending = false;
|
||||
s_state->tx_start_tick = (uint32_t)furi_get_tick();
|
||||
|
||||
uint8_t cur_btn = subghz_custom_btn_get();
|
||||
if(!car_emulate_start_tx(subghz, cur_btn)) {
|
||||
s_state->is_transmitting = false;
|
||||
notification_message(subghz->notifications, &sequence_error);
|
||||
}
|
||||
|
||||
car_emulate_refresh_view(subghz);
|
||||
consumed = true;
|
||||
|
||||
/* ── Stop ── */
|
||||
} else if(event.event == SubGhzCustomEventCarEmulateStop) {
|
||||
if(s_state->is_transmitting &&
|
||||
subghz->state_notifications == SubGhzNotificationStateTx) {
|
||||
|
||||
uint32_t elapsed = (uint32_t)furi_get_tick() - s_state->tx_start_tick;
|
||||
if(elapsed >= MIN_TX_TICKS) {
|
||||
car_emulate_stop_tx(subghz);
|
||||
s_state->is_transmitting = false;
|
||||
s_state->stop_pending = false;
|
||||
} else {
|
||||
s_state->stop_pending = true;
|
||||
}
|
||||
}
|
||||
car_emulate_refresh_view(subghz);
|
||||
consumed = true;
|
||||
|
||||
/* ── Exit ── */
|
||||
} else if(event.event == SubGhzCustomEventCarEmulateExit) {
|
||||
if(subghz->state_notifications == SubGhzNotificationStateTx) {
|
||||
car_emulate_stop_tx(subghz);
|
||||
}
|
||||
scene_manager_search_and_switch_to_previous_scene(
|
||||
subghz->scene_manager, SubGhzSceneSavedMenu);
|
||||
consumed = true;
|
||||
}
|
||||
|
||||
} else if(event.type == SceneManagerEventTypeTick) {
|
||||
|
||||
if(s_state->is_transmitting &&
|
||||
subghz->state_notifications == SubGhzNotificationStateTx) {
|
||||
|
||||
/* Check if hardware is done */
|
||||
if(subghz_devices_is_async_complete_tx(subghz->txrx->radio_device)) {
|
||||
subghz->state_notifications = SubGhzNotificationStateIDLE;
|
||||
subghz_txrx_stop(subghz->txrx);
|
||||
|
||||
if(s_state->stop_pending) {
|
||||
s_state->is_transmitting = false;
|
||||
s_state->stop_pending = false;
|
||||
notification_message(subghz->notifications, &sequence_blink_stop);
|
||||
}
|
||||
} else {
|
||||
/* Still transmitting – blink LED */
|
||||
notification_message(subghz->notifications, &sequence_blink_magenta_10);
|
||||
}
|
||||
|
||||
/* Enforce MIN_TX_TICKS stop gate */
|
||||
if(s_state->stop_pending) {
|
||||
uint32_t elapsed = (uint32_t)furi_get_tick() - s_state->tx_start_tick;
|
||||
if(elapsed >= MIN_TX_TICKS) {
|
||||
car_emulate_stop_tx(subghz);
|
||||
s_state->is_transmitting = false;
|
||||
s_state->stop_pending = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Refresh view every tick for animation */
|
||||
car_emulate_refresh_view(subghz);
|
||||
consumed = true;
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* Scene on_exit
|
||||
* ═════════════════════════════════════════════════════════════════════════*/
|
||||
void subghz_scene_car_emulate_on_exit(void* context) {
|
||||
SubGhz* subghz = context;
|
||||
|
||||
if(subghz->state_notifications == SubGhzNotificationStateTx) {
|
||||
car_emulate_stop_tx(subghz);
|
||||
}
|
||||
|
||||
subghz->state_notifications = SubGhzNotificationStateIDLE;
|
||||
notification_message(subghz->notifications, &sequence_blink_stop);
|
||||
|
||||
/* Clear view callbacks */
|
||||
subghz_car_emulate_view_set_callback(subghz->car_emulate_view, NULL, NULL);
|
||||
|
||||
/* Free per-session state */
|
||||
if(s_state) {
|
||||
free(s_state);
|
||||
s_state = NULL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Scene: CarEmulateSettings
|
||||
* Toggle: Custom Emulate Off / On
|
||||
* Selector: TX Power (reuses the same table as Radio Settings)
|
||||
* Both settings are persisted in SubGhzLastSettings.
|
||||
*/
|
||||
#include "../subghz_i.h"
|
||||
#include <lib/toolbox/value_index.h>
|
||||
|
||||
#define TAG "SubGhzCarEmulateSettings"
|
||||
|
||||
/* ── Toggle ──────────────────────────────────────────────────────────────── */
|
||||
static const char* const toggle_text[] = {"Off", "On"};
|
||||
|
||||
static void subghz_scene_car_emulate_settings_toggle_changed(VariableItem* item) {
|
||||
SubGhz* subghz = variable_item_get_context(item);
|
||||
furi_assert(subghz);
|
||||
|
||||
uint8_t index = variable_item_get_current_value_index(item);
|
||||
variable_item_set_current_value_text(item, toggle_text[index]);
|
||||
|
||||
subghz->last_settings->custom_car_emulate = (index == 1);
|
||||
subghz_last_settings_save(subghz->last_settings);
|
||||
}
|
||||
|
||||
/* ── TX Power ────────────────────────────────────────────────────────────── */
|
||||
/* Must match the table in subghz_scene_radio_settings.c exactly */
|
||||
#define CE_TX_POWER_COUNT 9
|
||||
static const char* const ce_tx_power_text[CE_TX_POWER_COUNT] = {
|
||||
"Preset", /* index 0 → use whatever the preset has baked in */
|
||||
"10dBm +",
|
||||
"7dBm",
|
||||
"5dBm",
|
||||
"0dBm",
|
||||
"-10dBm",
|
||||
"-15dBm",
|
||||
"-20dBm",
|
||||
"-30dBm",
|
||||
};
|
||||
|
||||
static void subghz_scene_car_emulate_settings_power_changed(VariableItem* item) {
|
||||
SubGhz* subghz = variable_item_get_context(item);
|
||||
furi_assert(subghz);
|
||||
|
||||
uint8_t index = variable_item_get_current_value_index(item);
|
||||
variable_item_set_current_value_text(item, ce_tx_power_text[index]);
|
||||
|
||||
/* Mirror the same fields that Radio Settings touches so the value is
|
||||
* visible everywhere and survives app restart. */
|
||||
subghz->tx_power = index;
|
||||
subghz->last_settings->tx_power = index;
|
||||
subghz_last_settings_save(subghz->last_settings);
|
||||
|
||||
/* Patch the live preset buffer immediately so any subsequent TX in this
|
||||
* session uses the new power without needing a restart. */
|
||||
SubGhzRadioPreset preset = subghz_txrx_get_preset(subghz->txrx);
|
||||
if(preset.data && preset.data_size > 0) {
|
||||
subghz_txrx_set_tx_power(preset.data, preset.data_size, index);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Scene callbacks ─────────────────────────────────────────────────────── */
|
||||
void subghz_scene_car_emulate_settings_on_enter(void* context) {
|
||||
SubGhz* subghz = context;
|
||||
furi_assert(subghz);
|
||||
|
||||
VariableItemList* list = subghz->variable_item_list;
|
||||
variable_item_list_reset(list);
|
||||
|
||||
/* ── Row 1: Custom Emulate toggle ── */
|
||||
VariableItem* item = variable_item_list_add(
|
||||
list,
|
||||
"Custom Emulate",
|
||||
2,
|
||||
subghz_scene_car_emulate_settings_toggle_changed,
|
||||
subghz);
|
||||
|
||||
uint8_t toggle_idx = subghz->last_settings->custom_car_emulate ? 1 : 0;
|
||||
variable_item_set_current_value_index(item, toggle_idx);
|
||||
variable_item_set_current_value_text(item, toggle_text[toggle_idx]);
|
||||
|
||||
/* ── Row 2: TX Power ── */
|
||||
item = variable_item_list_add(
|
||||
list,
|
||||
"TX Power",
|
||||
CE_TX_POWER_COUNT,
|
||||
subghz_scene_car_emulate_settings_power_changed,
|
||||
subghz);
|
||||
|
||||
/* Clamp stored value to valid range in case settings file is corrupt */
|
||||
uint8_t power_idx = subghz->tx_power;
|
||||
if(power_idx >= CE_TX_POWER_COUNT) power_idx = 0;
|
||||
|
||||
variable_item_set_current_value_index(item, power_idx);
|
||||
variable_item_set_current_value_text(item, ce_tx_power_text[power_idx]);
|
||||
|
||||
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdVariableItemList);
|
||||
}
|
||||
|
||||
bool subghz_scene_car_emulate_settings_on_event(void* context, SceneManagerEvent event) {
|
||||
UNUSED(context);
|
||||
UNUSED(event);
|
||||
return false;
|
||||
}
|
||||
|
||||
void subghz_scene_car_emulate_settings_on_exit(void* context) {
|
||||
SubGhz* subghz = context;
|
||||
variable_item_list_reset(subghz->variable_item_list);
|
||||
}
|
||||
@@ -34,3 +34,5 @@ ADD_SCENE(subghz, keeloq_decrypt, KeeloqDecrypt)
|
||||
ADD_SCENE(subghz, keeloq_bf2, KeeloqBf2)
|
||||
ADD_SCENE(subghz, kl_bf_cleanup, KlBfCleanup)
|
||||
ADD_SCENE(subghz, counter_bf, CounterBf)
|
||||
ADD_SCENE(subghz, car_emulate, CarEmulate)
|
||||
ADD_SCENE(subghz, car_emulate_settings, CarEmulateSettings)
|
||||
|
||||
@@ -1,53 +1,135 @@
|
||||
#include "../subghz_i.h"
|
||||
#include <lib/subghz/subghz_protocol_registry.h>
|
||||
|
||||
void subghz_scene_protocol_list_submenu_callback(void* context, uint32_t index) {
|
||||
SubGhz* subghz = context;
|
||||
view_dispatcher_send_custom_event(subghz->view_dispatcher, index);
|
||||
}
|
||||
#define TAG "SubGhzSceneProtocolList"
|
||||
|
||||
void subghz_scene_protocol_list_on_enter(void* context) {
|
||||
SubGhz* subghz = context;
|
||||
/* ── helpers ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
submenu_reset(subghz->submenu);
|
||||
|
||||
size_t protocol_count = subghz_protocol_registry_count(&subghz_protocol_registry);
|
||||
|
||||
char header_str[32];
|
||||
snprintf(header_str, sizeof(header_str), "Protocols: %zu", protocol_count);
|
||||
submenu_set_header(subghz->submenu, header_str);
|
||||
|
||||
for(size_t i = 0; i < protocol_count; i++) {
|
||||
const SubGhzProtocol* protocol =
|
||||
subghz_protocol_registry_get_by_index(&subghz_protocol_registry, i);
|
||||
if(protocol) {
|
||||
submenu_add_item(
|
||||
subghz->submenu,
|
||||
protocol->name,
|
||||
i,
|
||||
subghz_scene_protocol_list_submenu_callback,
|
||||
subghz);
|
||||
}
|
||||
}
|
||||
|
||||
submenu_set_selected_item(
|
||||
subghz->submenu,
|
||||
scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneProtocolList));
|
||||
|
||||
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdMenu);
|
||||
}
|
||||
|
||||
bool subghz_scene_protocol_list_on_event(void* context, SceneManagerEvent event) {
|
||||
SubGhz* subghz = context;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
scene_manager_set_scene_state(subghz->scene_manager, SubGhzSceneProtocolList, event.event);
|
||||
return true;
|
||||
static bool proto_filter_contains(const char* filter, const char* name) {
|
||||
const char* p = filter;
|
||||
while(*p) {
|
||||
const char* comma = strchr(p, ',');
|
||||
size_t len = comma ? (size_t)(comma - p) : strlen(p);
|
||||
if(len == strlen(name) && strncmp(p, name, len) == 0) return true;
|
||||
if(!comma) break;
|
||||
p = comma + 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void proto_filter_toggle(char* filter, size_t filter_size, const char* name) {
|
||||
if(proto_filter_contains(filter, name)) {
|
||||
/* remove it */
|
||||
char tmp[256] = {0};
|
||||
const char* p = filter;
|
||||
bool first = true;
|
||||
while(*p) {
|
||||
const char* comma = strchr(p, ',');
|
||||
size_t len = comma ? (size_t)(comma - p) : strlen(p);
|
||||
if(!(len == strlen(name) && strncmp(p, name, len) == 0)) {
|
||||
if(!first) strncat(tmp, ",", sizeof(tmp) - strlen(tmp) - 1);
|
||||
strncat(tmp, p, len < sizeof(tmp) - strlen(tmp) - 1 ? len : 0);
|
||||
first = false;
|
||||
}
|
||||
if(!comma) break;
|
||||
p = comma + 1;
|
||||
}
|
||||
strncpy(filter, tmp, filter_size - 1);
|
||||
filter[filter_size - 1] = '\0';
|
||||
} else {
|
||||
/* add it */
|
||||
if(filter[0] != '\0') strncat(filter, ",", filter_size - strlen(filter) - 1);
|
||||
strncat(filter, name, filter_size - strlen(filter) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── callbacks ────────────────────────────────────────────────────────────── */
|
||||
|
||||
static void subghz_scene_protocol_list_item_changed(VariableItem* item) {
|
||||
SubGhz* subghz = variable_item_get_context(item);
|
||||
uint8_t value_index = variable_item_get_current_value_index(item);
|
||||
|
||||
uint32_t proto_idx =
|
||||
scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneProtocolList);
|
||||
size_t selected =
|
||||
variable_item_list_get_selected_item_index(subghz->variable_item_list);
|
||||
UNUSED(proto_idx);
|
||||
|
||||
const SubGhzProtocol* protocol =
|
||||
subghz_protocol_registry_get_by_index(&subghz_protocol_registry, selected);
|
||||
if(!protocol) return;
|
||||
|
||||
const char* name = protocol->name;
|
||||
|
||||
bool currently_in =
|
||||
proto_filter_contains(subghz->last_settings->protocol_filter, name);
|
||||
|
||||
if((value_index == 1) != currently_in) {
|
||||
proto_filter_toggle(
|
||||
subghz->last_settings->protocol_filter,
|
||||
sizeof(subghz->last_settings->protocol_filter),
|
||||
name);
|
||||
}
|
||||
|
||||
variable_item_set_current_value_text(item, value_index ? "ONLY" : "---");
|
||||
subghz_last_settings_save(subghz->last_settings);
|
||||
}
|
||||
|
||||
/* ── scene callbacks ──────────────────────────────────────────────────────── */
|
||||
|
||||
void subghz_scene_protocol_list_on_enter(void* context) {
|
||||
SubGhz* subghz = context;
|
||||
VariableItemList* list = subghz->variable_item_list;
|
||||
variable_item_list_reset(list);
|
||||
|
||||
size_t protocol_count = subghz_protocol_registry_count(&subghz_protocol_registry);
|
||||
|
||||
for(size_t i = 0; i < protocol_count; i++) {
|
||||
const SubGhzProtocol* protocol =
|
||||
subghz_protocol_registry_get_by_index(&subghz_protocol_registry, i);
|
||||
if(!protocol) continue;
|
||||
|
||||
VariableItem* item = variable_item_list_add(
|
||||
list,
|
||||
protocol->name,
|
||||
2,
|
||||
subghz_scene_protocol_list_item_changed,
|
||||
subghz);
|
||||
|
||||
bool enabled = proto_filter_contains(
|
||||
subghz->last_settings->protocol_filter, protocol->name);
|
||||
variable_item_set_current_value_index(item, enabled ? 1 : 0);
|
||||
variable_item_set_current_value_text(item, enabled ? "ONLY" : "---");
|
||||
}
|
||||
|
||||
variable_item_list_set_selected_item(
|
||||
list,
|
||||
scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneProtocolList));
|
||||
|
||||
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdVariableItemList);
|
||||
}
|
||||
|
||||
bool subghz_scene_protocol_list_on_event(void* context, SceneManagerEvent event) {
|
||||
SubGhz* subghz = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
scene_manager_set_scene_state(
|
||||
subghz->scene_manager, SubGhzSceneProtocolList, event.event);
|
||||
consumed = true;
|
||||
} else if(event.type == SceneManagerEventTypeBack) {
|
||||
scene_manager_previous_scene(subghz->scene_manager);
|
||||
consumed = true;
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void subghz_scene_protocol_list_on_exit(void* context) {
|
||||
SubGhz* subghz = context;
|
||||
submenu_reset(subghz->submenu);
|
||||
scene_manager_set_scene_state(
|
||||
subghz->scene_manager,
|
||||
SubGhzSceneProtocolList,
|
||||
variable_item_list_get_selected_item_index(subghz->variable_item_list));
|
||||
variable_item_list_reset(subghz->variable_item_list);
|
||||
}
|
||||
|
||||
@@ -105,6 +105,29 @@ static void subghz_scene_add_to_history_callback(
|
||||
SubGhz* subghz = context;
|
||||
|
||||
// The check can be moved to /lib/subghz/receiver.c, but may result in false positives
|
||||
/* Protocol name allowlist filter — if non-empty, drop anything not in the list */
|
||||
if(subghz->last_settings->protocol_filter[0] != '\0') {
|
||||
const char* proto_name = decoder_base->protocol->name;
|
||||
const char* filter = subghz->last_settings->protocol_filter;
|
||||
bool allowed = false;
|
||||
/* Walk the comma-separated list */
|
||||
const char* p = filter;
|
||||
while(*p) {
|
||||
const char* comma = strchr(p, ',');
|
||||
size_t len = comma ? (size_t)(comma - p) : strlen(p);
|
||||
if(len == strlen(proto_name) && strncmp(p, proto_name, len) == 0) {
|
||||
allowed = true;
|
||||
break;
|
||||
}
|
||||
if(!comma) break;
|
||||
p = comma + 1;
|
||||
}
|
||||
if(!allowed) {
|
||||
FURI_LOG_D(TAG, "%s filtered by allowlist", proto_name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if((decoder_base->protocol->flag & subghz->ignore_filter) == 0) {
|
||||
SubGhzHistory* history = subghz->history;
|
||||
FuriString* item_name = furi_string_alloc();
|
||||
|
||||
@@ -17,6 +17,7 @@ enum SubGhzSettingIndex {
|
||||
SubGhzSettingIndexIgnoreNiceFlorS,
|
||||
SubGhzSettingIndexDeleteOldSignals,
|
||||
SubGhzSettingIndexSound,
|
||||
SubGhzSettingIndexProtoFilter,
|
||||
SubGhzSettingIndexResetToDefault,
|
||||
SubGhzSettingIndexLock,
|
||||
SubGhzSettingIndexRAWThresholdRSSI,
|
||||
@@ -445,7 +446,9 @@ static void subghz_scene_receiver_config_set_delete_old_signals(VariableItem* it
|
||||
static void subghz_scene_receiver_config_var_list_enter_callback(void* context, uint32_t index) {
|
||||
furi_assert(context);
|
||||
SubGhz* subghz = context;
|
||||
if(index == SubGhzSettingIndexLock) {
|
||||
if(index == SubGhzSettingIndexProtoFilter) {
|
||||
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneProtocolList);
|
||||
} else if(index == SubGhzSettingIndexLock) {
|
||||
view_dispatcher_send_custom_event(
|
||||
subghz->view_dispatcher, SubGhzCustomEventSceneSettingLock);
|
||||
} else if(index == SubGhzSettingIndexResetToDefault) {
|
||||
@@ -473,6 +476,7 @@ static void subghz_scene_receiver_config_var_list_enter_callback(void* context,
|
||||
subghz->last_settings->filter = subghz->filter;
|
||||
subghz->last_settings->delete_old_signals = false;
|
||||
subghz->last_settings->tx_power = subghz->tx_power = 0;
|
||||
subghz->last_settings->protocol_filter[0] = '\0';
|
||||
subghz_txrx_speaker_set_state(subghz->txrx, speaker_value[default_index]);
|
||||
|
||||
subghz_txrx_hopper_set_state(subghz->txrx, hopping_value[default_index]);
|
||||
@@ -668,6 +672,25 @@ void subghz_scene_receiver_config_on_enter(void* context) {
|
||||
|
||||
if(scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneReadRAW) !=
|
||||
SubGhzCustomEventManagerSet) {
|
||||
/* Protocol filter */
|
||||
item = variable_item_list_add(
|
||||
subghz->variable_item_list,
|
||||
"Proto Filter",
|
||||
1,
|
||||
NULL,
|
||||
subghz);
|
||||
if(subghz->last_settings->protocol_filter[0] == '\0') {
|
||||
variable_item_set_current_value_text(item, "All");
|
||||
} else {
|
||||
uint8_t count = 1;
|
||||
for(const char* p = subghz->last_settings->protocol_filter; *p; p++) {
|
||||
if(*p == ',') count++;
|
||||
}
|
||||
static char filter_count_str[8];
|
||||
snprintf(filter_count_str, sizeof(filter_count_str), "%u set", count);
|
||||
variable_item_set_current_value_text(item, filter_count_str);
|
||||
}
|
||||
|
||||
// Reset to default
|
||||
variable_item_list_add(subghz->variable_item_list, "Reset to default", 1, NULL, NULL);
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ enum SubmenuIndex {
|
||||
SubmenuIndexEdit,
|
||||
SubmenuIndexDelete,
|
||||
SubmenuIndexSignalSettings,
|
||||
SubmenuIndexCounterBf
|
||||
SubmenuIndexCounterBf, /* <-- comma was missing here */
|
||||
SubmenuIndexCarEmulateSettings,
|
||||
};
|
||||
|
||||
void subghz_scene_saved_menu_submenu_callback(void* context, uint32_t index) {
|
||||
@@ -77,6 +78,13 @@ void subghz_scene_saved_menu_on_enter(void* context) {
|
||||
subghz_scene_saved_menu_submenu_callback,
|
||||
subghz);
|
||||
|
||||
submenu_add_item(
|
||||
subghz->submenu,
|
||||
"Custom Emulate Settings",
|
||||
SubmenuIndexCarEmulateSettings,
|
||||
subghz_scene_saved_menu_submenu_callback,
|
||||
subghz);
|
||||
|
||||
if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) {
|
||||
submenu_add_item(
|
||||
subghz->submenu,
|
||||
@@ -109,7 +117,22 @@ bool subghz_scene_saved_menu_on_event(void* context, SceneManagerEvent event) {
|
||||
if(event.event == SubmenuIndexEmulate) {
|
||||
scene_manager_set_scene_state(
|
||||
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexEmulate);
|
||||
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneTransmitter);
|
||||
|
||||
bool use_custom = subghz->last_settings->custom_car_emulate;
|
||||
if(use_custom) {
|
||||
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
|
||||
uint32_t cnt_tmp = 0;
|
||||
flipper_format_rewind(fff);
|
||||
if(!flipper_format_read_uint32(fff, "Cnt", &cnt_tmp, 1)) {
|
||||
use_custom = false;
|
||||
}
|
||||
}
|
||||
|
||||
if(use_custom) {
|
||||
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneCarEmulate);
|
||||
} else {
|
||||
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneTransmitter);
|
||||
}
|
||||
return true;
|
||||
} else if(event.event == SubmenuIndexPsaDecrypt) {
|
||||
scene_manager_set_scene_state(
|
||||
@@ -136,6 +159,14 @@ bool subghz_scene_saved_menu_on_event(void* context, SceneManagerEvent event) {
|
||||
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexCounterBf);
|
||||
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneCounterBf);
|
||||
return true;
|
||||
} else if(event.event == SubmenuIndexCarEmulateSettings) {
|
||||
/* <-- was outside the if block due to misplaced brace, now fixed */
|
||||
scene_manager_set_scene_state(
|
||||
subghz->scene_manager,
|
||||
SubGhzSceneSavedMenu,
|
||||
SubmenuIndexCarEmulateSettings);
|
||||
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneCarEmulateSettings);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -94,6 +94,10 @@ bool subghz_scene_transmitter_on_event(void* context, SceneManagerEvent event) {
|
||||
scene_manager_search_and_switch_to_previous_scene(
|
||||
subghz->scene_manager, SubGhzSceneStart);
|
||||
return true;
|
||||
} else if(event.event == SubGhzCustomEventViewTransmitterPageChange) {
|
||||
// Page changed via OK button, refresh display
|
||||
subghz_scene_transmitter_update_data_show(subghz);
|
||||
return true;
|
||||
} else if(event.event == SubGhzCustomEventViewTransmitterError) {
|
||||
furi_string_set(subghz->error_str, "Protocol not\nfound!");
|
||||
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneShowErrorSub);
|
||||
|
||||
@@ -206,6 +206,12 @@ SubGhz* subghz_alloc(bool alloc_for_tx_only) {
|
||||
SubGhzViewIdKeeloqDecrypt,
|
||||
subghz_view_keeloq_decrypt_get_view(subghz->subghz_keeloq_decrypt));
|
||||
|
||||
subghz->car_emulate_view = subghz_car_emulate_view_alloc();
|
||||
view_dispatcher_add_view(
|
||||
subghz->view_dispatcher,
|
||||
SubGhzViewIdCarEmulate,
|
||||
subghz_car_emulate_view_get_view(subghz->car_emulate_view));
|
||||
|
||||
//init threshold rssi
|
||||
subghz->threshold_rssi = subghz_threshold_rssi_alloc();
|
||||
|
||||
@@ -321,6 +327,10 @@ void subghz_free(SubGhz* subghz, bool alloc_for_tx_only) {
|
||||
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdKeeloqDecrypt);
|
||||
subghz_view_keeloq_decrypt_free(subghz->subghz_keeloq_decrypt);
|
||||
|
||||
// Custom car-emulate view
|
||||
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdCarEmulate);
|
||||
subghz_car_emulate_view_free(subghz->car_emulate_view);
|
||||
|
||||
// Read RAW
|
||||
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdReadRAW);
|
||||
subghz_read_raw_free(subghz->subghz_read_raw);
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
#include "helpers/subghz_txrx.h"
|
||||
#include "helpers/subghz_keeloq_keys.h"
|
||||
|
||||
#include "views/subghz_car_emulate.h"
|
||||
|
||||
#define SUBGHZ_MAX_LEN_NAME 64
|
||||
#define SUBGHZ_EXT_PRESET_NAME true
|
||||
#define SUBGHZ_RAW_THRESHOLD_MIN (-90.0f)
|
||||
@@ -76,6 +78,7 @@ struct SubGhz {
|
||||
SubGhzReadRAW* subghz_read_raw;
|
||||
SubGhzViewPsaDecrypt* subghz_psa_decrypt;
|
||||
SubGhzViewKeeloqDecrypt* subghz_keeloq_decrypt;
|
||||
SubGhzCarEmulateView* car_emulate_view;
|
||||
bool raw_send_only;
|
||||
|
||||
bool save_datetime_set;
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#define SUBGHZ_LAST_SETTING_FIELD_HOPPING_THRESHOLD "HoppingThreshold"
|
||||
#define SUBGHZ_LAST_SETTING_FIELD_LED_AND_POWER_AMP "LedAndPowerAmp"
|
||||
#define SUBGHZ_LAST_SETTING_FIELD_TX_POWER "TXPower"
|
||||
#define SUBGHZ_LAST_SETTING_FIELD_CUSTOM_CAR_EMULATE "CustomCarEmulate"
|
||||
#define SUBGHZ_LAST_SETTING_FIELD_PROTOCOL_FILTER "ProtocolFilter"
|
||||
|
||||
SubGhzLastSettings* subghz_last_settings_alloc(void) {
|
||||
SubGhzLastSettings* instance = malloc(sizeof(SubGhzLastSettings));
|
||||
@@ -50,6 +52,7 @@ void subghz_last_settings_load(SubGhzLastSettings* instance, size_t preset_count
|
||||
instance->enable_preset_hopping = false;
|
||||
instance->preset_hopping_threshold = SUBGHZ_LAST_SETTING_DEFAULT_PRESET_HOPPING_THRESHOLD;
|
||||
instance->leds_and_amp = true;
|
||||
instance->protocol_filter[0] = '\0';
|
||||
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
FlipperFormat* fff_data_file = flipper_format_file_alloc(storage);
|
||||
@@ -163,6 +166,27 @@ void subghz_last_settings_load(SubGhzLastSettings* instance, size_t preset_count
|
||||
1)) {
|
||||
flipper_format_rewind(fff_data_file);
|
||||
}
|
||||
if(!flipper_format_read_bool(
|
||||
fff_data_file,
|
||||
SUBGHZ_LAST_SETTING_FIELD_CUSTOM_CAR_EMULATE,
|
||||
&instance->custom_car_emulate,
|
||||
1)) {
|
||||
instance->custom_car_emulate = false;
|
||||
flipper_format_rewind(fff_data_file);
|
||||
}
|
||||
FuriString* filter_str = furi_string_alloc();
|
||||
if(flipper_format_read_string(
|
||||
fff_data_file, SUBGHZ_LAST_SETTING_FIELD_PROTOCOL_FILTER, filter_str)) {
|
||||
strncpy(
|
||||
instance->protocol_filter,
|
||||
furi_string_get_cstr(filter_str),
|
||||
sizeof(instance->protocol_filter) - 1);
|
||||
instance->protocol_filter[sizeof(instance->protocol_filter) - 1] = '\0';
|
||||
} else {
|
||||
instance->protocol_filter[0] = '\0';
|
||||
flipper_format_rewind(fff_data_file);
|
||||
}
|
||||
furi_string_free(filter_str);
|
||||
|
||||
} while(0);
|
||||
} else {
|
||||
@@ -281,6 +305,19 @@ bool subghz_last_settings_save(SubGhzLastSettings* instance) {
|
||||
file, SUBGHZ_LAST_SETTING_FIELD_LED_AND_POWER_AMP, &instance->leds_and_amp, 1)) {
|
||||
break;
|
||||
}
|
||||
if(!flipper_format_write_bool(
|
||||
file,
|
||||
SUBGHZ_LAST_SETTING_FIELD_CUSTOM_CAR_EMULATE,
|
||||
&instance->custom_car_emulate,
|
||||
1)) {
|
||||
break;
|
||||
}
|
||||
if(!flipper_format_write_string_cstr(
|
||||
file,
|
||||
SUBGHZ_LAST_SETTING_FIELD_PROTOCOL_FILTER,
|
||||
instance->protocol_filter)) {
|
||||
break;
|
||||
}
|
||||
|
||||
saved = true;
|
||||
} while(0);
|
||||
|
||||
@@ -30,6 +30,8 @@ typedef struct {
|
||||
float preset_hopping_threshold;
|
||||
bool leds_and_amp;
|
||||
uint8_t tx_power;
|
||||
bool custom_car_emulate;
|
||||
char protocol_filter[256]; /* comma-separated allowlist, empty = disabled */
|
||||
} SubGhzLastSettings;
|
||||
|
||||
SubGhzLastSettings* subghz_last_settings_alloc(void);
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
#include "subghz_car_emulate.h"
|
||||
#include "../helpers/subghz_custom_event.h"
|
||||
|
||||
#include <gui/elements.h>
|
||||
#include <input/input.h>
|
||||
#include <furi.h>
|
||||
|
||||
#define TAG "SubGhzCarEmulateView"
|
||||
|
||||
/* ── Model ──────────────────────────────────────────────────────────────── */
|
||||
typedef struct {
|
||||
char protocol_name[32];
|
||||
uint32_t serial;
|
||||
uint32_t counter;
|
||||
uint32_t original_counter;
|
||||
uint32_t freq;
|
||||
char preset[12];
|
||||
bool is_transmitting;
|
||||
uint8_t anim_frame;
|
||||
char label_ok[12];
|
||||
char label_up[12];
|
||||
char label_down[12];
|
||||
char label_left[12];
|
||||
char label_right[12];
|
||||
} SubGhzCarEmulateViewModel;
|
||||
|
||||
/* ── Handle ─────────────────────────────────────────────────────────────── */
|
||||
struct SubGhzCarEmulateView {
|
||||
View* view;
|
||||
SubGhzCarEmulateViewCallback callback;
|
||||
void* context;
|
||||
};
|
||||
|
||||
/* ── Draw ───────────────────────────────────────────────────────────────── */
|
||||
static void subghz_car_emulate_view_draw(Canvas* canvas, void* model_ptr) {
|
||||
SubGhzCarEmulateViewModel* m = model_ptr;
|
||||
|
||||
m->anim_frame = (m->anim_frame + 1) % 8;
|
||||
|
||||
canvas_clear(canvas);
|
||||
|
||||
/* Header bar */
|
||||
canvas_draw_box(canvas, 0, 0, 128, 11);
|
||||
canvas_invert_color(canvas);
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
canvas_draw_str_aligned(canvas, 64, 2, AlignCenter, AlignTop, m->protocol_name);
|
||||
canvas_invert_color(canvas);
|
||||
|
||||
/* Info row 1: serial + counter */
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
char buf[32];
|
||||
|
||||
if(m->serial <= 0xFFFFFFUL) {
|
||||
snprintf(buf, sizeof(buf), "SN:%06lX", (unsigned long)(m->serial & 0xFFFFFFUL));
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "SN:%08lX", (unsigned long)m->serial);
|
||||
}
|
||||
canvas_draw_str(canvas, 2, 20, buf);
|
||||
|
||||
snprintf(buf, sizeof(buf), "CNT:%04lX", (unsigned long)m->counter);
|
||||
canvas_draw_str(canvas, 68, 20, buf);
|
||||
|
||||
if(m->counter > m->original_counter) {
|
||||
snprintf(buf, sizeof(buf), "+%ld", (long)(m->counter - m->original_counter));
|
||||
canvas_draw_str(canvas, 112, 20, buf);
|
||||
}
|
||||
|
||||
/* Info row 2: frequency + preset */
|
||||
snprintf(
|
||||
buf,
|
||||
sizeof(buf),
|
||||
"F:%lu.%02lu",
|
||||
(unsigned long)(m->freq / 1000000UL),
|
||||
(unsigned long)((m->freq % 1000000UL) / 10000UL));
|
||||
canvas_draw_str(canvas, 2, 30, buf);
|
||||
canvas_draw_str(canvas, 95, 30, m->preset);
|
||||
|
||||
/* ── Button labels ── */
|
||||
const uint8_t font_h = canvas_current_font_height(canvas);
|
||||
|
||||
/* Centre → UNLOCK (OK button) */
|
||||
{
|
||||
const char* lbl = m->label_ok;
|
||||
uint8_t w = (uint8_t)(canvas_string_width(canvas, lbl) + 8U);
|
||||
canvas_draw_rbox(canvas, 64 - w / 2, 45 - font_h / 2, w, font_h, 3);
|
||||
canvas_invert_color(canvas);
|
||||
canvas_draw_str_aligned(canvas, 64, 49, AlignCenter, AlignBottom, lbl);
|
||||
canvas_invert_color(canvas);
|
||||
}
|
||||
|
||||
/* Up → LOCK */
|
||||
{
|
||||
const char* lbl = m->label_up;
|
||||
uint8_t w = (uint8_t)(canvas_string_width(canvas, lbl) + 8U);
|
||||
canvas_draw_rbox(canvas, 64 - w / 2, 33 - font_h / 2, w, font_h, 3);
|
||||
canvas_invert_color(canvas);
|
||||
canvas_draw_str_aligned(canvas, 64, 37, AlignCenter, AlignBottom, lbl);
|
||||
canvas_invert_color(canvas);
|
||||
}
|
||||
|
||||
/* Left → PANIC */
|
||||
{
|
||||
const char* lbl = m->label_left;
|
||||
uint8_t w = (uint8_t)(canvas_string_width(canvas, lbl) + 8U);
|
||||
canvas_draw_rbox(canvas, 0, 46 - font_h / 2, w, font_h, 3);
|
||||
canvas_invert_color(canvas);
|
||||
canvas_draw_str_aligned(canvas, w / 2, 50, AlignCenter, AlignBottom, lbl);
|
||||
canvas_invert_color(canvas);
|
||||
}
|
||||
|
||||
/* Right → generic extra */
|
||||
{
|
||||
const char* lbl = m->label_right;
|
||||
uint8_t w = (uint8_t)(canvas_string_width(canvas, lbl) + 8U);
|
||||
canvas_draw_rbox(canvas, 127 - w, 46 - font_h / 2, w, font_h, 3);
|
||||
canvas_invert_color(canvas);
|
||||
canvas_draw_str_aligned(canvas, 127 - w / 2, 50, AlignCenter, AlignBottom, lbl);
|
||||
canvas_invert_color(canvas);
|
||||
}
|
||||
|
||||
/* Down → BOOT */
|
||||
{
|
||||
const char* lbl = m->label_down;
|
||||
uint8_t w = (uint8_t)(canvas_string_width(canvas, lbl) + 8U);
|
||||
canvas_draw_rbox(canvas, 64 - w / 2, 57 - font_h / 2, w, font_h, 3);
|
||||
canvas_invert_color(canvas);
|
||||
canvas_draw_str_aligned(canvas, 64, 61, AlignCenter, AlignBottom, lbl);
|
||||
canvas_invert_color(canvas);
|
||||
}
|
||||
|
||||
/* TX overlay */
|
||||
if(m->is_transmitting) {
|
||||
canvas_draw_rbox(canvas, 24, 18, 80, 18, 3);
|
||||
canvas_invert_color(canvas);
|
||||
int wave = m->anim_frame % 3;
|
||||
canvas_draw_str(canvas, 28 + wave * 2, 25, ")))");
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
canvas_draw_str_aligned(canvas, 64, 24, AlignCenter, AlignCenter, "TX");
|
||||
canvas_invert_color(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Input ──────────────────────────────────────────────────────────────── */
|
||||
static bool subghz_car_emulate_view_input(InputEvent* event, void* context) {
|
||||
SubGhzCarEmulateView* instance = context;
|
||||
furi_assert(instance);
|
||||
|
||||
if(event->type == InputTypePress) {
|
||||
if(event->key == InputKeyBack) {
|
||||
if(instance->callback) {
|
||||
instance->callback(SubGhzCustomEventCarEmulateExit, instance->context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Any directional / OK key → start TX */
|
||||
if(instance->callback) {
|
||||
/* Pack the raw InputKey into the upper bits of the event so the
|
||||
scene can read which button was pressed.
|
||||
Lower 16 bits = SubGhzCustomEventCarEmulateTransmit marker,
|
||||
upper 16 bits = InputKey value. */
|
||||
uint32_t ev = ((uint32_t)event->key << 16) |
|
||||
(uint32_t)SubGhzCustomEventCarEmulateTransmit;
|
||||
instance->callback(ev, instance->context);
|
||||
}
|
||||
return true;
|
||||
|
||||
} else if(event->type == InputTypeRelease) {
|
||||
if(event->key != InputKeyBack) {
|
||||
if(instance->callback) {
|
||||
instance->callback(SubGhzCustomEventCarEmulateStop, instance->context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ── Alloc / Free ───────────────────────────────────────────────────────── */
|
||||
SubGhzCarEmulateView* subghz_car_emulate_view_alloc(void) {
|
||||
SubGhzCarEmulateView* instance = malloc(sizeof(SubGhzCarEmulateView));
|
||||
furi_check(instance);
|
||||
|
||||
instance->view = view_alloc();
|
||||
instance->callback = NULL;
|
||||
instance->context = NULL;
|
||||
|
||||
view_set_context(instance->view, instance);
|
||||
view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(SubGhzCarEmulateViewModel));
|
||||
view_set_draw_callback(instance->view, subghz_car_emulate_view_draw);
|
||||
view_set_input_callback(instance->view, subghz_car_emulate_view_input);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
void subghz_car_emulate_view_free(SubGhzCarEmulateView* instance) {
|
||||
furi_check(instance);
|
||||
view_free(instance->view);
|
||||
free(instance);
|
||||
}
|
||||
|
||||
View* subghz_car_emulate_view_get_view(SubGhzCarEmulateView* instance) {
|
||||
furi_check(instance);
|
||||
return instance->view;
|
||||
}
|
||||
|
||||
void subghz_car_emulate_view_set_callback(
|
||||
SubGhzCarEmulateView* instance,
|
||||
SubGhzCarEmulateViewCallback callback,
|
||||
void* context) {
|
||||
furi_check(instance);
|
||||
instance->callback = callback;
|
||||
instance->context = context;
|
||||
}
|
||||
|
||||
void subghz_car_emulate_view_set_data(
|
||||
SubGhzCarEmulateView* instance,
|
||||
const char* protocol_name,
|
||||
uint32_t serial,
|
||||
uint32_t counter,
|
||||
uint32_t original_counter,
|
||||
uint32_t freq,
|
||||
const char* preset,
|
||||
bool is_transmitting) {
|
||||
furi_check(instance);
|
||||
|
||||
with_view_model(
|
||||
instance->view,
|
||||
SubGhzCarEmulateViewModel * m,
|
||||
{
|
||||
strncpy(m->protocol_name, protocol_name, sizeof(m->protocol_name) - 1);
|
||||
m->protocol_name[sizeof(m->protocol_name) - 1] = '\0';
|
||||
m->serial = serial;
|
||||
m->counter = counter;
|
||||
m->original_counter = original_counter;
|
||||
m->freq = freq;
|
||||
strncpy(m->preset, preset, sizeof(m->preset) - 1);
|
||||
m->preset[sizeof(m->preset) - 1] = '\0';
|
||||
m->is_transmitting = is_transmitting;
|
||||
},
|
||||
true);
|
||||
}
|
||||
|
||||
void subghz_car_emulate_view_set_labels(
|
||||
SubGhzCarEmulateView* instance,
|
||||
const char* ok,
|
||||
const char* up,
|
||||
const char* down,
|
||||
const char* left,
|
||||
const char* right) {
|
||||
furi_check(instance);
|
||||
with_view_model(
|
||||
instance->view,
|
||||
SubGhzCarEmulateViewModel * m,
|
||||
{
|
||||
strncpy(m->label_ok, ok ? ok : "", sizeof(m->label_ok) - 1);
|
||||
strncpy(m->label_up, up ? up : "", sizeof(m->label_up) - 1);
|
||||
strncpy(m->label_down, down ? down : "", sizeof(m->label_down) - 1);
|
||||
strncpy(m->label_left, left ? left : "", sizeof(m->label_left) - 1);
|
||||
strncpy(m->label_right, right ? right : "", sizeof(m->label_right) - 1);
|
||||
},
|
||||
true);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <gui/view.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct SubGhzCarEmulateView SubGhzCarEmulateView;
|
||||
|
||||
typedef void (*SubGhzCarEmulateViewCallback)(uint32_t event, void* context);
|
||||
|
||||
SubGhzCarEmulateView* subghz_car_emulate_view_alloc(void);
|
||||
void subghz_car_emulate_view_free(SubGhzCarEmulateView* instance);
|
||||
View* subghz_car_emulate_view_get_view(SubGhzCarEmulateView* instance);
|
||||
|
||||
void subghz_car_emulate_view_set_callback(
|
||||
SubGhzCarEmulateView* instance,
|
||||
SubGhzCarEmulateViewCallback callback,
|
||||
void* context);
|
||||
|
||||
/** Update the fields shown on the view.
|
||||
* All strings are copied internally so the caller can free them after the call.
|
||||
*/
|
||||
void subghz_car_emulate_view_set_labels(
|
||||
SubGhzCarEmulateView* instance,
|
||||
const char* ok,
|
||||
const char* up,
|
||||
const char* down,
|
||||
const char* left,
|
||||
const char* right);
|
||||
|
||||
void subghz_car_emulate_view_set_data(
|
||||
SubGhzCarEmulateView* instance,
|
||||
const char* protocol_name,
|
||||
uint32_t serial,
|
||||
uint32_t counter,
|
||||
uint32_t original_counter,
|
||||
uint32_t freq,
|
||||
const char* preset,
|
||||
bool is_transmitting);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -155,23 +155,68 @@ bool subghz_view_transmitter_input(InputEvent* event, void* context) {
|
||||
true);
|
||||
|
||||
if(can_be_sent) {
|
||||
if(event->key == InputKeyOk && event->type == InputTypePress) {
|
||||
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_OK);
|
||||
with_view_model(
|
||||
subghz_transmitter->view,
|
||||
SubGhzViewTransmitterModel * model,
|
||||
{
|
||||
furi_string_reset(model->temp_button_id);
|
||||
model->draw_temp_button = false;
|
||||
},
|
||||
true);
|
||||
subghz_transmitter->callback(
|
||||
SubGhzCustomEventViewTransmitterSendStart, subghz_transmitter->context);
|
||||
return true;
|
||||
} else if(event->key == InputKeyOk && event->type == InputTypeRelease) {
|
||||
subghz_transmitter->callback(
|
||||
SubGhzCustomEventViewTransmitterSendStop, subghz_transmitter->context);
|
||||
return true;
|
||||
// Long press d-pad: set custom btn + long flag (no send here, send happens below)
|
||||
if(event->type == InputTypeLong) {
|
||||
if(event->key == InputKeyUp) {
|
||||
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_UP);
|
||||
subghz_custom_btn_set_long(true);
|
||||
} else if(event->key == InputKeyDown) {
|
||||
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_DOWN);
|
||||
subghz_custom_btn_set_long(true);
|
||||
} else if(event->key == InputKeyLeft) {
|
||||
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_LEFT);
|
||||
subghz_custom_btn_set_long(true);
|
||||
} else if(event->key == InputKeyRight) {
|
||||
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_RIGHT);
|
||||
subghz_custom_btn_set_long(true);
|
||||
}
|
||||
}
|
||||
|
||||
// OK button handling
|
||||
if(event->key == InputKeyOk) {
|
||||
if(event->type == InputTypePress) {
|
||||
if(subghz_custom_btn_has_pages()) {
|
||||
// Multi-page protocol: cycle pages, do NOT send
|
||||
uint8_t max_pages = subghz_custom_btn_get_max_pages();
|
||||
uint8_t next_page = (subghz_custom_btn_get_page() + 1) % max_pages;
|
||||
subghz_custom_btn_set_page(next_page);
|
||||
// Reset d-pad selection to OK so display shows original btn
|
||||
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_OK);
|
||||
with_view_model(
|
||||
subghz_transmitter->view,
|
||||
SubGhzViewTransmitterModel * model,
|
||||
{
|
||||
furi_string_reset(model->temp_button_id);
|
||||
furi_string_printf(model->temp_button_id, "P%u", next_page + 1);
|
||||
model->draw_temp_button = true;
|
||||
},
|
||||
true);
|
||||
// Refresh display with new page mapping
|
||||
subghz_transmitter->callback(
|
||||
SubGhzCustomEventViewTransmitterPageChange, subghz_transmitter->context);
|
||||
return true;
|
||||
}
|
||||
// Normal protocol: send original button
|
||||
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_OK);
|
||||
with_view_model(
|
||||
subghz_transmitter->view,
|
||||
SubGhzViewTransmitterModel * model,
|
||||
{
|
||||
furi_string_reset(model->temp_button_id);
|
||||
model->draw_temp_button = false;
|
||||
},
|
||||
true);
|
||||
subghz_transmitter->callback(
|
||||
SubGhzCustomEventViewTransmitterSendStart, subghz_transmitter->context);
|
||||
return true;
|
||||
} else if(event->type == InputTypeRelease) {
|
||||
// Only stop TX if we actually started it (not a page toggle)
|
||||
if(!subghz_custom_btn_has_pages()) {
|
||||
subghz_transmitter->callback(
|
||||
SubGhzCustomEventViewTransmitterSendStop, subghz_transmitter->context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // Finish "OK" key processing
|
||||
|
||||
if(subghz_custom_btn_is_allowed()) {
|
||||
|
||||
@@ -3,7 +3,7 @@ App(
|
||||
name="System",
|
||||
apptype=FlipperAppType.SETTINGS,
|
||||
entry_point="system_settings_app",
|
||||
requires=["gui", "locale"],
|
||||
stack_size=1 * 1024,
|
||||
requires=["gui", "locale", "storage"],
|
||||
stack_size=2 * 1024,
|
||||
order=30,
|
||||
)
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
#include <loader/loader.h>
|
||||
#include <lib/toolbox/value_index.h>
|
||||
#include <locale/locale.h>
|
||||
#include <flipper_format/flipper_format.h>
|
||||
#include <power/power_service/power.h>
|
||||
#include <applications/services/namechanger/namechanger.h>
|
||||
|
||||
const char* const log_level_text[] = {
|
||||
"Default",
|
||||
@@ -208,6 +211,81 @@ static void filename_scheme_changed(VariableItem* item) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Device Name --------------------------------------------------------
|
||||
|
||||
#define DEVICE_NAME_ITEM_INDEX 11
|
||||
|
||||
static bool system_settings_device_name_validator(
|
||||
const char* text,
|
||||
FuriString* error,
|
||||
void* context) {
|
||||
UNUSED(context);
|
||||
for(; *text; ++text) {
|
||||
const char c = *text;
|
||||
if((c < '0' || c > '9') && (c < 'A' || c > 'Z') && (c < 'a' || c > 'z')) {
|
||||
furi_string_printf(error, "Letters and\nnumbers only!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void system_settings_device_name_callback(void* context) {
|
||||
SystemSettings* app = context;
|
||||
|
||||
// Save name to SD card (same path as namechanger service)
|
||||
FlipperFormat* file = flipper_format_file_alloc(app->storage);
|
||||
bool saved = false;
|
||||
do {
|
||||
if(app->device_name[0] == '\0') {
|
||||
// Empty name -> remove file to restore real name
|
||||
storage_simply_remove(app->storage, NAMECHANGER_PATH);
|
||||
saved = true;
|
||||
break;
|
||||
}
|
||||
if(!flipper_format_file_open_always(file, NAMECHANGER_PATH)) break;
|
||||
if(!flipper_format_write_header_cstr(file, NAMECHANGER_HEADER, NAMECHANGER_VERSION)) break;
|
||||
if(!flipper_format_write_string_cstr(file, "Name", app->device_name)) break;
|
||||
saved = true;
|
||||
} while(false);
|
||||
flipper_format_free(file);
|
||||
|
||||
if(saved) {
|
||||
// Reboot to apply
|
||||
Power* power = furi_record_open(RECORD_POWER);
|
||||
power_reboot(power, PowerBootModeNormal);
|
||||
} else {
|
||||
// Go back silently on failure
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, SystemSettingsViewVarItemList);
|
||||
}
|
||||
}
|
||||
|
||||
static void system_settings_enter_callback(void* context, uint32_t index) {
|
||||
SystemSettings* app = context;
|
||||
if(index == DEVICE_NAME_ITEM_INDEX) {
|
||||
text_input_reset(app->text_input);
|
||||
text_input_set_header_text(app->text_input, "Device Name (empty=reset)");
|
||||
text_input_set_validator(
|
||||
app->text_input, system_settings_device_name_validator, NULL);
|
||||
text_input_set_minimum_length(app->text_input, 0);
|
||||
text_input_set_result_callback(
|
||||
app->text_input,
|
||||
system_settings_device_name_callback,
|
||||
app,
|
||||
app->device_name,
|
||||
FURI_HAL_VERSION_ARRAY_NAME_LENGTH,
|
||||
false);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, SystemSettingsViewTextInput);
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t system_settings_text_input_back(void* context) {
|
||||
UNUSED(context);
|
||||
return SystemSettingsViewVarItemList;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
static uint32_t system_settings_exit(void* context) {
|
||||
UNUSED(context);
|
||||
return VIEW_NONE;
|
||||
@@ -218,6 +296,7 @@ SystemSettings* system_settings_alloc(void) {
|
||||
|
||||
// Load settings
|
||||
app->gui = furi_record_open(RECORD_GUI);
|
||||
app->storage = furi_record_open(RECORD_STORAGE);
|
||||
|
||||
app->view_dispatcher = view_dispatcher_alloc();
|
||||
view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
|
||||
@@ -314,6 +393,19 @@ SystemSettings* system_settings_alloc(void) {
|
||||
variable_item_set_current_value_index(item, value_index);
|
||||
variable_item_set_current_value_text(item, filename_scheme[value_index]);
|
||||
|
||||
// Device Name (index = DEVICE_NAME_ITEM_INDEX = 11)
|
||||
const char* current_name = furi_hal_version_get_name_ptr();
|
||||
strlcpy(
|
||||
app->device_name,
|
||||
current_name ? current_name : "",
|
||||
FURI_HAL_VERSION_ARRAY_NAME_LENGTH);
|
||||
item = variable_item_list_add(app->var_item_list, "Device Name", 0, NULL, app);
|
||||
variable_item_set_current_value_text(
|
||||
item, app->device_name[0] != '\0' ? app->device_name : "<default>");
|
||||
|
||||
variable_item_list_set_enter_callback(
|
||||
app->var_item_list, system_settings_enter_callback, app);
|
||||
|
||||
view_set_previous_callback(
|
||||
variable_item_list_get_view(app->var_item_list), system_settings_exit);
|
||||
view_dispatcher_add_view(
|
||||
@@ -321,6 +413,15 @@ SystemSettings* system_settings_alloc(void) {
|
||||
SystemSettingsViewVarItemList,
|
||||
variable_item_list_get_view(app->var_item_list));
|
||||
|
||||
// TextInput for device name
|
||||
app->text_input = text_input_alloc();
|
||||
view_set_previous_callback(
|
||||
text_input_get_view(app->text_input), system_settings_text_input_back);
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher,
|
||||
SystemSettingsViewTextInput,
|
||||
text_input_get_view(app->text_input));
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, SystemSettingsViewVarItemList);
|
||||
|
||||
return app;
|
||||
@@ -328,12 +429,16 @@ SystemSettings* system_settings_alloc(void) {
|
||||
|
||||
void system_settings_free(SystemSettings* app) {
|
||||
furi_assert(app);
|
||||
// TextInput
|
||||
view_dispatcher_remove_view(app->view_dispatcher, SystemSettingsViewTextInput);
|
||||
text_input_free(app->text_input);
|
||||
// Variable item list
|
||||
view_dispatcher_remove_view(app->view_dispatcher, SystemSettingsViewVarItemList);
|
||||
variable_item_list_free(app->var_item_list);
|
||||
// View dispatcher
|
||||
view_dispatcher_free(app->view_dispatcher);
|
||||
// Records
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
furi_record_close(RECORD_GUI);
|
||||
free(app);
|
||||
}
|
||||
|
||||
@@ -2,17 +2,24 @@
|
||||
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
#include <furi_hal_version.h>
|
||||
|
||||
#include <gui/gui.h>
|
||||
#include <gui/view_dispatcher.h>
|
||||
#include <gui/modules/variable_item_list.h>
|
||||
#include <gui/modules/text_input.h>
|
||||
#include <storage/storage.h>
|
||||
|
||||
typedef struct {
|
||||
Gui* gui;
|
||||
ViewDispatcher* view_dispatcher;
|
||||
VariableItemList* var_item_list;
|
||||
TextInput* text_input;
|
||||
Storage* storage;
|
||||
char device_name[FURI_HAL_VERSION_ARRAY_NAME_LENGTH];
|
||||
} SystemSettings;
|
||||
|
||||
typedef enum {
|
||||
SystemSettingsViewVarItemList,
|
||||
SystemSettingsViewTextInput,
|
||||
} SystemSettingsView;
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to the 24cxxprog EEPROM Programmer application will be documented in this file.
|
||||
|
||||
## [2.0.0] - 2026-03-11
|
||||
|
||||
### 🚀 Major Features Added
|
||||
|
||||
#### Dynamic Memory Support for All 24Cxx Chips
|
||||
- **Full chip type support**: Added complete support for all EEPROM sizes from 24C01 (128B) to 24C512 (64KB)
|
||||
- **Dynamic buffer allocation**: Memory buffers now automatically resize based on selected chip type
|
||||
- **Configurable in Settings**: Users can now select chip type in Settings menu, and all operations adapt automatically
|
||||
|
||||
### ✨ Enhancements
|
||||
|
||||
#### Memory Management
|
||||
- Replaced fixed 256-byte buffers with dynamic allocation:
|
||||
- `memory_data` - dynamically allocated based on chip size
|
||||
- `file_data` - dynamically allocated based on chip size
|
||||
- `verify_buffer` - dynamically allocated based on chip size
|
||||
- Added `get_eeprom_size()` helper function returning size in bytes for each chip type
|
||||
- Added `reallocate_buffers()` function for automatic buffer reallocation on chip type change
|
||||
- Memory size tracked in `memory_size` field (32-bit for chips up to 64KB)
|
||||
|
||||
#### Read/Write/Erase Operations
|
||||
- **Read operation**: Now reads entire EEPROM regardless of size (128B to 64KB)
|
||||
- **Write operation**: Supports writing to full address range of selected chip
|
||||
- **Erase operation**: Clears entire memory of selected chip type
|
||||
- **File operations**: Binary dumps now save/load full chip capacity
|
||||
|
||||
#### User Interface Improvements
|
||||
- Address display format adapts to memory size:
|
||||
- Small chips (≤256B): `0x00` format
|
||||
- Large chips (>256B): `0000` hex format (4 digits)
|
||||
- Progress indicators updated for all memory sizes
|
||||
- Navigation (Up/Down) works across entire address range
|
||||
- File size display shows actual chip capacity
|
||||
|
||||
#### File Naming
|
||||
- Filename generation now includes all chip types:
|
||||
- Examples: `24C01_2026-03-11_10-30.bin`, `24C256_2026-03-11_10-30.bin`
|
||||
- Automatic timestamp-based naming for all chip variants
|
||||
|
||||
### 🔧 Technical Changes
|
||||
|
||||
#### Type Updates
|
||||
- Changed address/size types from `uint8_t` to `uint32_t` for large memory support:
|
||||
- `current_address`: now `uint32_t`
|
||||
- `read_total_bytes`: now `uint32_t`
|
||||
- `write_total_bytes_async`: now `uint32_t`
|
||||
- `verify_total_bytes`: now `uint32_t`
|
||||
- `erase_current_addr`: now `uint32_t`
|
||||
- `progress_value`: now `uint32_t`
|
||||
- `file_size`: now `uint32_t`
|
||||
|
||||
#### Format Specifiers
|
||||
- Updated all `printf`/`snprintf` calls to use correct format for `uint32_t`:
|
||||
- Changed `%d` to `%lu` for unsigned long
|
||||
- Changed `%X` to `%lX` for hex unsigned long
|
||||
|
||||
#### Memory Safety
|
||||
- Added proper memory initialization in `reallocate_buffers()`
|
||||
- Added null pointer checks for all dynamically allocated buffers
|
||||
- Proper cleanup in `eeprom_app_free()` - all buffers freed correctly
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
- Fixed buffer overflow risk in memory operations for larger chips
|
||||
- Fixed format specifier warnings causing compilation errors
|
||||
- Fixed address boundary checking for chips larger than 256 bytes
|
||||
- Fixed progress bar calculations for larger memory sizes
|
||||
|
||||
### 🔄 Behavioral Changes
|
||||
- Settings → Chip Type now immediately reallocates buffers
|
||||
- Current address is reset to 0 if it exceeds new chip size after type change
|
||||
- File load operation respects maximum chip capacity (won't load more than chip can hold)
|
||||
|
||||
### 📋 Supported Chip Types
|
||||
|
||||
Complete support matrix:
|
||||
| Chip Type | Size | Status |
|
||||
|-----------|------|--------|
|
||||
| 24C01 | 128 bytes | ✅ Full Support |
|
||||
| 24C02 | 256 bytes | ✅ Full Support |
|
||||
| 24C04 | 512 bytes | ✅ Full Support |
|
||||
| 24C08 | 1 KB | ✅ Full Support |
|
||||
| 24C16 | 2 KB | ✅ Full Support |
|
||||
| 24C32 | 4 KB | ✅ Full Support |
|
||||
| 24C64 | 8 KB | ✅ Full Support |
|
||||
| 24C128 | 16 KB | ✅ Full Support |
|
||||
| 24C256 | 32 KB | ✅ Full Support |
|
||||
| 24C512 | 64 KB | ✅ Full Support |
|
||||
|
||||
### ⚠️ Breaking Changes
|
||||
- Binary dump files from previous versions (always 256 bytes) are incompatible with chip-specific sizes
|
||||
- Users should re-read and save new dumps after upgrading
|
||||
|
||||
---
|
||||
|
||||
## [1.0.0] - Previous Version
|
||||
|
||||
### Initial Release
|
||||
- Basic read/write/erase operations
|
||||
- Fixed 256-byte buffer (24C02 only)
|
||||
- I2C address configuration
|
||||
- File load/save operations
|
||||
- Basic hex viewer
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Dr.Mosfet
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,222 @@
|
||||
# 🔧 24cxxprog - EEPROM 24Cxx Programmer
|
||||
|
||||
<h2 align="center">A Comprehensive EEPROM Programmer for Flipper Zero</h2>
|
||||
|
||||
<div align="center">
|
||||
<table style="width:100%; border:none;">
|
||||
<tr style="border:none;">
|
||||
<td style="border:none; padding:10px;">
|
||||
<img src="screenshots/1.png" alt="Main Menu - Operations" style="width:100%;">
|
||||
<br>
|
||||
<em>Menu główne z operacjami (Odczyt, Zapis, Kasowanie)</em>
|
||||
</td>
|
||||
<td style="border:none; padding:10px;">
|
||||
<img src="screenshots/2.png" alt="Configuration Menu" style="width:100%;">
|
||||
<br>
|
||||
<em>Menu konfiguracji (Adres I2C, Rozmiar pamięci)</em>
|
||||
</td>
|
||||
<td style="border:none; padding:10px;">
|
||||
<img src="screenshots/3.png" alt="Data Display - EEPROM Contents" style="width:100%;">
|
||||
<br>
|
||||
<em>Wyświetlanie zawartości EEPROM (Dane heksadecymalne)</em>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
This is a **comprehensive EEPROM programmer application** designed for the **Flipper Zero** that interfaces with the **24Cxx series I2C memory chips**. The application provides a complete suite of tools for reading, writing, erasing, and managing EEPROM memory with a user-friendly interface on the Flipper's screen.
|
||||
|
||||
## ✨ Features Overview
|
||||
|
||||
### 📝 EEPROM Operations
|
||||
|
||||
Complete toolset for memory management:
|
||||
|
||||
* **Read Operations:** View complete EEPROM contents with address and hexadecimal data display.
|
||||
* **Write Operations:** Program custom data into specific memory addresses.
|
||||
* **Erase Functions:** Clear individual bytes, pages, or entire memory sections.
|
||||
* **Dump to Storage:** Export EEPROM contents to Flipper SD card for backup and analysis.
|
||||
* **Restore from Backup:** Load previously saved EEPROM data back into the chip.
|
||||
|
||||
### 🎨 User Interface & Experience
|
||||
|
||||
Intuitive interface optimized for Flipper Zero's display:
|
||||
|
||||
* **Main Menu:** Clear operation selection with visual feedback.
|
||||
* **Data Viewer:** Scrollable hex display showing actual EEPROM contents.
|
||||
* **Configuration Menu:** Easy access to sensor parameters and device settings.
|
||||
* **Address Navigation:** Precise control over memory location selection.
|
||||
* **Progress Indicator:** Real-time feedback during long operations.
|
||||
|
||||
### ⚙️ Configuration Options
|
||||
|
||||
Customize the programmer for your specific hardware:
|
||||
|
||||
* **I2C Address Selection:** Choose between multiple I2C addresses (**0x50-0x57**) for different chip variants.
|
||||
* **Memory Size Selection:** Automatically detect or manually set chip capacity (**1KB to 64KB** and larger).
|
||||
* **Page Size Configuration:** Adapt to different chip architectures (**8 bytes to 256 bytes per page**).
|
||||
* **Persistent Settings:** Configurations are automatically saved for quick access.
|
||||
|
||||
### 💻 Technical Features & Robustness
|
||||
|
||||
Built for reliability on the Flipper Zero platform:
|
||||
|
||||
* **I2C Protocol Support:** Robust communication with error checking.
|
||||
* **Address Validation:** Prevents out-of-bounds memory access.
|
||||
* **Timeout Protection:** Safeguards against communication errors.
|
||||
* **Error Handling:** Comprehensive error messages for troubleshooting.
|
||||
* **Non-blocking Operations:** Responsive UI that doesn't freeze during I2C transactions.
|
||||
* **Data Verification:** Verify written data integrity after programming.
|
||||
|
||||
## 🔋 Supported 24Cxx Chips
|
||||
|
||||
Comprehensive support for the entire 24Cxx family:
|
||||
|
||||
<table style="width:100%; border:1px solid #ddd; border-collapse: collapse; text-align: left;">
|
||||
<thead style="background-color: #f8f8f8;">
|
||||
<tr>
|
||||
<th style="padding: 8px; border:1px solid #ddd;">Chip Model</th>
|
||||
<th style="padding: 8px; border:1px solid #ddd;">Memory Size</th>
|
||||
<th style="padding: 8px; border:1px solid #ddd;">Page Size</th>
|
||||
<th style="padding: 8px; border:1px solid #ddd;">Address Range</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding: 8px; border:1px solid #ddd;"><strong>24C01</strong></td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">128 Bytes</td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">8 Bytes</td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">0x00 - 0x7F</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border:1px solid #ddd;"><strong>24C02</strong></td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">256 Bytes</td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">8 Bytes</td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">0x00 - 0xFF</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border:1px solid #ddd;"><strong>24C04 - 24C16</strong></td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">512B - 2KB</td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">16 Bytes</td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">0x00 - 0xFFFF</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border:1px solid #ddd;"><strong>24C32 - 24C64</strong></td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">4KB - 8KB</td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">32 Bytes</td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">0x0000 - 0x1FFF</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border:1px solid #ddd;"><strong>24C128 - 24C512</strong></td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">16KB - 64KB</td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">64 Bytes</td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">0x0000 - 0xFFFF</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
---
|
||||
|
||||
## 🕹️ Navigation Guide
|
||||
|
||||
<table style="width:100%; border:1px solid #ddd; border-collapse: collapse; text-align: left;">
|
||||
<thead style="background-color: #f8f8f8;">
|
||||
<tr>
|
||||
<th style="padding: 8px; border:1px solid #ddd;">Screen</th>
|
||||
<th style="padding: 8px; border:1px solid #ddd;">D-Pad Up/Down</th>
|
||||
<th style="padding: 8px; border:1px solid #ddd;">D-Pad Left/Right</th>
|
||||
<th style="padding: 8px; border:1px solid #ddd;">OK Button</th>
|
||||
<th style="padding: 8px; border:1px solid #ddd;">Back Button</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding: 8px; border:1px solid #ddd;"><strong>Main Menu</strong></td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">Browse operations (Read, Write, Erase, Dump, Restore)</td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">-</td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">Select operation</td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;"><strong>Exit</strong> application</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border:1px solid #ddd;"><strong>Read/Write</strong></td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">Navigate through addresses</td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">Adjust byte values (Write mode)</td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">Confirm operation</td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">Return to Main Menu</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border:1px solid #ddd;"><strong>Configuration</strong></td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">Navigate between settings</td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">Adjust parameter values</td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">Apply settings</td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">Cancel and return</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border:1px solid #ddd;"><strong>Data View</strong></td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">Scroll data up/down</td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">Jump to address</td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">Show hex/ASCII toggle</td>
|
||||
<td style="padding: 8px; border:1px solid #ddd;">Exit data view</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## 🔌 Hardware Connections
|
||||
|
||||
Standard I2C pinout for Flipper Zero GPIO:
|
||||
|
||||
```
|
||||
24Cxx EEPROM Module Flipper Zero GPIO
|
||||
───────────────── ─────────────────
|
||||
SDA (Pin 5) ───→ GPIO_SDA (Pin 16)
|
||||
SCL (Pin 6) ───→ GPIO_SCL (Pin 15)
|
||||
GND (Pin 4) ───→ GND (Pin 8)
|
||||
VCC (Pin 8) ───→ 3.3V (Pin 9)
|
||||
|
||||
Optional Pull-ups: 4.7kΩ from SDA and SCL to 3.3V
|
||||
```
|
||||
|
||||
## 📋 Operation Details
|
||||
|
||||
### Read
|
||||
- Displays EEPROM contents in hexadecimal format
|
||||
- Shows address, data bytes, and ASCII representation
|
||||
- Scrollable for chips larger than display capacity
|
||||
|
||||
### Write
|
||||
- Enter target address and data values
|
||||
- Supports single byte or page programming
|
||||
- Automatic write cycle delay handling
|
||||
|
||||
### Erase
|
||||
- Clear individual bytes to 0xFF
|
||||
- Erase entire pages
|
||||
- Full chip erase with confirmation
|
||||
|
||||
### Dump
|
||||
- Export EEPROM to **`/ext/apps_data/24cxxprog/`** directory
|
||||
- Creates timestamped backup files
|
||||
- Preserves complete memory state
|
||||
|
||||
### Restore
|
||||
- Load previously dumped EEPROM data
|
||||
- Verify before writing
|
||||
- Restore to specified starting address
|
||||
|
||||
---
|
||||
|
||||
## 👨💻 Developer
|
||||
|
||||
This application was created by **Dr. Mosfet** for the Flipper Zero community.
|
||||
|
||||
**Repository:** [kamylwnb/24cxxprog](https://github.com/kamylwnb/24cxxprog)
|
||||
|
||||
**Version:** 1.0
|
||||
**Category:** GPIO / Tools
|
||||
**Platform:** Flipper Zero F7
|
||||
|
||||
---
|
||||
|
||||
**Happy EEPROM programming! 🔧**
|
||||
@@ -0,0 +1,22 @@
|
||||
App(
|
||||
appid="24cxxprog",
|
||||
name="24Cxx Programmer",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="eeprom_app_24cxx",
|
||||
cdefines=["APP_24CXXPROG"],
|
||||
requires=[
|
||||
"gui",
|
||||
"i2c",
|
||||
],
|
||||
sources=[
|
||||
"i2c_24c02_app.cpp",
|
||||
"i2c_24c02.cpp",
|
||||
],
|
||||
stack_size=2 * 1024,
|
||||
order=21,
|
||||
fap_icon="icons/ikon.png",
|
||||
fap_category="GPIO",
|
||||
fap_author="@Dr.Mosfet",
|
||||
fap_version="2.0",
|
||||
fap_description="EEPROM 24Cxx programmer via I2C with read, write, erase and dump/restore options.",
|
||||
)
|
||||
@@ -0,0 +1,212 @@
|
||||
#include "i2c_24c02.hpp"
|
||||
#include "furi_hal_i2c.h"
|
||||
#include <furi.h>
|
||||
|
||||
EEPROM24C02::EEPROM24C02(uint8_t i2c_address_7bit)
|
||||
: _i2c_addr_8bit(i2c_address_7bit << 1) {
|
||||
}
|
||||
|
||||
bool EEPROM24C02::init() {
|
||||
// Just check if device is responding
|
||||
return isAvailable();
|
||||
}
|
||||
|
||||
bool EEPROM24C02::isAvailable() {
|
||||
furi_hal_i2c_acquire(&furi_hal_i2c_handle_external);
|
||||
|
||||
// Try to read a dummy byte to check if device responds
|
||||
uint8_t dummy_data;
|
||||
bool success = furi_hal_i2c_rx(
|
||||
&furi_hal_i2c_handle_external, _i2c_addr_8bit, &dummy_data, 1, EEPROM_I2C_TIMEOUT);
|
||||
|
||||
furi_hal_i2c_release(&furi_hal_i2c_handle_external);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
bool EEPROM24C02::readByte(uint8_t memory_addr, uint8_t& data) {
|
||||
furi_hal_i2c_acquire(&furi_hal_i2c_handle_external);
|
||||
|
||||
// Send memory address first
|
||||
bool success_tx = furi_hal_i2c_tx_ext(
|
||||
&furi_hal_i2c_handle_external,
|
||||
_i2c_addr_8bit,
|
||||
false,
|
||||
&memory_addr,
|
||||
1,
|
||||
FuriHalI2cBeginStart,
|
||||
FuriHalI2cEndAwaitRestart,
|
||||
EEPROM_I2C_TIMEOUT);
|
||||
|
||||
// Read the data
|
||||
bool success_rx = false;
|
||||
if(success_tx) {
|
||||
success_rx = furi_hal_i2c_rx_ext(
|
||||
&furi_hal_i2c_handle_external,
|
||||
_i2c_addr_8bit,
|
||||
false,
|
||||
&data,
|
||||
1,
|
||||
FuriHalI2cBeginRestart,
|
||||
FuriHalI2cEndStop,
|
||||
EEPROM_I2C_TIMEOUT);
|
||||
}
|
||||
|
||||
furi_hal_i2c_release(&furi_hal_i2c_handle_external);
|
||||
|
||||
return success_tx && success_rx;
|
||||
}
|
||||
|
||||
bool EEPROM24C02::writeByte(uint8_t memory_addr, uint8_t data) {
|
||||
uint8_t write_buffer[2];
|
||||
write_buffer[0] = memory_addr;
|
||||
write_buffer[1] = data;
|
||||
|
||||
furi_hal_i2c_acquire(&furi_hal_i2c_handle_external);
|
||||
|
||||
bool success = furi_hal_i2c_tx_ext(
|
||||
&furi_hal_i2c_handle_external,
|
||||
_i2c_addr_8bit,
|
||||
false,
|
||||
write_buffer,
|
||||
2,
|
||||
FuriHalI2cBeginStart,
|
||||
FuriHalI2cEndStop,
|
||||
EEPROM_I2C_TIMEOUT);
|
||||
|
||||
furi_hal_i2c_release(&furi_hal_i2c_handle_external);
|
||||
|
||||
if(success) {
|
||||
// Wait for write cycle to complete (typically 5ms for 24C02)
|
||||
furi_delay_ms(10);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
bool EEPROM24C02::readBytes(uint8_t start_addr, uint8_t* buffer, uint8_t length) {
|
||||
if(length == 0 || buffer == nullptr) return false;
|
||||
|
||||
furi_hal_i2c_acquire(&furi_hal_i2c_handle_external);
|
||||
|
||||
// Send start address
|
||||
bool success_tx = furi_hal_i2c_tx_ext(
|
||||
&furi_hal_i2c_handle_external,
|
||||
_i2c_addr_8bit,
|
||||
false,
|
||||
&start_addr,
|
||||
1,
|
||||
FuriHalI2cBeginStart,
|
||||
FuriHalI2cEndAwaitRestart,
|
||||
EEPROM_I2C_TIMEOUT);
|
||||
|
||||
// Sequential read
|
||||
bool success_rx = false;
|
||||
if(success_tx) {
|
||||
success_rx = furi_hal_i2c_rx_ext(
|
||||
&furi_hal_i2c_handle_external,
|
||||
_i2c_addr_8bit,
|
||||
false,
|
||||
buffer,
|
||||
length,
|
||||
FuriHalI2cBeginRestart,
|
||||
FuriHalI2cEndStop,
|
||||
EEPROM_I2C_TIMEOUT);
|
||||
}
|
||||
|
||||
furi_hal_i2c_release(&furi_hal_i2c_handle_external);
|
||||
|
||||
return success_tx && success_rx;
|
||||
}
|
||||
|
||||
bool EEPROM24C02::writeBytes(uint8_t start_addr, const uint8_t* buffer, uint8_t length) {
|
||||
if(length == 0 || buffer == nullptr) return false;
|
||||
|
||||
// 24C02 has 8-byte page size - we need to handle page boundaries
|
||||
uint8_t bytes_written = 0;
|
||||
|
||||
while(bytes_written < length) {
|
||||
uint8_t current_addr = start_addr + bytes_written;
|
||||
uint8_t page_offset = current_addr % EEPROM_24C02_PAGE_SIZE;
|
||||
uint8_t bytes_in_page = EEPROM_24C02_PAGE_SIZE - page_offset;
|
||||
uint8_t bytes_to_write =
|
||||
(length - bytes_written < bytes_in_page) ? (length - bytes_written) : bytes_in_page;
|
||||
|
||||
// Prepare write buffer for this page
|
||||
uint8_t write_buffer[EEPROM_24C02_PAGE_SIZE + 1]; // +1 for address
|
||||
write_buffer[0] = start_addr + bytes_written;
|
||||
|
||||
for(uint8_t i = 0; i < bytes_to_write; i++) {
|
||||
write_buffer[i + 1] = buffer[bytes_written + i];
|
||||
}
|
||||
|
||||
furi_hal_i2c_acquire(&furi_hal_i2c_handle_external);
|
||||
|
||||
bool success = furi_hal_i2c_tx_ext(
|
||||
&furi_hal_i2c_handle_external,
|
||||
_i2c_addr_8bit,
|
||||
false,
|
||||
write_buffer,
|
||||
bytes_to_write + 1,
|
||||
FuriHalI2cBeginStart,
|
||||
FuriHalI2cEndStop,
|
||||
EEPROM_I2C_TIMEOUT);
|
||||
|
||||
furi_hal_i2c_release(&furi_hal_i2c_handle_external);
|
||||
|
||||
if(!success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait for write cycle to complete
|
||||
furi_delay_ms(10);
|
||||
|
||||
bytes_written += bytes_to_write;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EEPROM24C02::eraseAll() {
|
||||
// Fill entire memory with 0xFF
|
||||
uint8_t erase_buffer[EEPROM_24C02_PAGE_SIZE];
|
||||
for(uint8_t i = 0; i < EEPROM_24C02_PAGE_SIZE; i++) {
|
||||
erase_buffer[i] = 0xFF;
|
||||
}
|
||||
|
||||
// Erase page by page
|
||||
for(uint8_t page = 0; page < EEPROM_24C02_SIZE / EEPROM_24C02_PAGE_SIZE; page++) {
|
||||
uint8_t start_addr = page * EEPROM_24C02_PAGE_SIZE;
|
||||
if(!writeBytes(start_addr, erase_buffer, EEPROM_24C02_PAGE_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EEPROM24C02::eraseRange(uint8_t start_addr, uint8_t length) {
|
||||
if(length == 0) return false;
|
||||
|
||||
// Check if range goes beyond memory
|
||||
uint16_t end_addr = (uint16_t)start_addr + length;
|
||||
if(end_addr > EEPROM_24C02_SIZE) {
|
||||
length = EEPROM_24C02_SIZE - start_addr;
|
||||
}
|
||||
|
||||
// Fill range with 0xFF
|
||||
uint8_t erase_buffer[EEPROM_24C02_PAGE_SIZE];
|
||||
for(uint8_t i = 0; i < EEPROM_24C02_PAGE_SIZE; i++) {
|
||||
erase_buffer[i] = 0xFF;
|
||||
}
|
||||
|
||||
return writeBytes(start_addr, erase_buffer, length);
|
||||
}
|
||||
|
||||
void EEPROM24C02::setAddress(uint8_t i2c_address_7bit) {
|
||||
_i2c_addr_8bit = i2c_address_7bit << 1;
|
||||
}
|
||||
|
||||
uint8_t EEPROM24C02::getAddress() {
|
||||
return _i2c_addr_8bit >> 1;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// 24C02 EEPROM I2C addresses (7-bit)
|
||||
// Standard addresses: 0x50-0x57 (A0-A2 pins)
|
||||
#define EEPROM_24C02_BASE_ADDR 0x50
|
||||
#define EEPROM_24C02_MAX_ADDR 0x57
|
||||
|
||||
// Memory size for 24C02
|
||||
#define EEPROM_24C02_SIZE 256 // 2KB = 2048 bits = 256 bytes
|
||||
#define EEPROM_24C02_PAGE_SIZE 8 // Page write size
|
||||
|
||||
// I2C operation timeout
|
||||
#define EEPROM_I2C_TIMEOUT 100
|
||||
|
||||
class EEPROM24C02 {
|
||||
private:
|
||||
uint8_t _i2c_addr_8bit;
|
||||
|
||||
public:
|
||||
EEPROM24C02(uint8_t i2c_address_7bit);
|
||||
|
||||
// Initialize communication with EEPROM
|
||||
bool init();
|
||||
|
||||
// Read single byte from address
|
||||
bool readByte(uint8_t memory_addr, uint8_t& data);
|
||||
|
||||
// Write single byte to address
|
||||
bool writeByte(uint8_t memory_addr, uint8_t data);
|
||||
|
||||
// Read multiple bytes (sequential read)
|
||||
bool readBytes(uint8_t start_addr, uint8_t* buffer, uint8_t length);
|
||||
|
||||
// Write multiple bytes (page write)
|
||||
bool writeBytes(uint8_t start_addr, const uint8_t* buffer, uint8_t length);
|
||||
|
||||
// Erase entire memory (fill with 0xFF)
|
||||
bool eraseAll();
|
||||
|
||||
// Erase range of bytes
|
||||
bool eraseRange(uint8_t start_addr, uint8_t length);
|
||||
|
||||
// Check if EEPROM is responding
|
||||
bool isAvailable();
|
||||
|
||||
// Set I2C address
|
||||
void setAddress(uint8_t i2c_address_7bit);
|
||||
|
||||
// Get current I2C address
|
||||
uint8_t getAddress();
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
#pragma once
|
||||
|
||||
#include <gui/canvas.h>
|
||||
|
||||
static const uint8_t image_DolphinMafia_0_bits[] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x0e, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0xe0, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xfe, 0x7f, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x55,
|
||||
0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xae, 0xaa, 0x00,
|
||||
0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x55, 0x55, 0x15, 0x00,
|
||||
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xaa, 0x2a, 0x00, 0x00, 0x04,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x55, 0x55, 0x55, 0x00, 0x08, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0xaa, 0x2a, 0x00, 0x00, 0x08, 0xff, 0x3f,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x55, 0x55, 0x55, 0x00, 0xf8, 0x00, 0xc0, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0xaa, 0x2a, 0x00, 0x00, 0x10, 0x00, 0x00, 0x01, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x55, 0x55, 0x55, 0x00, 0x10, 0x00, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0xa0, 0xaa, 0x2a, 0x00, 0x00, 0x20, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x60, 0xd5, 0xff, 0xff, 0x3f, 0x20, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0xa0, 0xfa, 0xff, 0xff, 0xff, 0xff, 0x03, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x60, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xe0, 0xff, 0xff, 0xbf, 0xaa, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0,
|
||||
0xff, 0xff, 0x57, 0x55, 0x55, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xff,
|
||||
0xff, 0xaa, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x5f,
|
||||
0x15, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xff, 0xff, 0xaf, 0x02,
|
||||
0xf8, 0x0f, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xff, 0xff, 0x55, 0x01, 0xff,
|
||||
0x1f, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xff, 0xff, 0x2a, 0xc0, 0x0f, 0xf8,
|
||||
0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xff, 0x5f, 0x15, 0xf0, 0x0f, 0xf8, 0x0f,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xf8, 0xff, 0xaf, 0x02, 0xf8, 0x0f, 0x1c, 0x0e, 0x1f,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xfc, 0xff, 0x57, 0x15, 0xf8, 0x1f, 0x1e, 0xfe, 0x60, 0x00,
|
||||
0x00, 0x00, 0xd6, 0x00, 0x50, 0xfe, 0xff, 0xab, 0xff, 0xff, 0xff, 0x0f, 0x0f, 0x80, 0x00, 0x00,
|
||||
0x80, 0x01, 0xc8, 0x4e, 0xfe, 0xff, 0xf5, 0x17, 0xf0, 0xff, 0xcf, 0x00, 0x00, 0x01, 0x00, 0x40,
|
||||
0x00, 0x00, 0x20, 0xff, 0xff, 0xfb, 0x00, 0xe0, 0xff, 0x07, 0x00, 0x60, 0x01, 0x00, 0x20, 0x00,
|
||||
0x00, 0x18, 0xff, 0xff, 0x5d, 0x05, 0xc0, 0xff, 0x03, 0x00, 0x70, 0xe1, 0x07, 0xa0, 0xa3, 0xb0,
|
||||
0x06, 0xff, 0xff, 0xaa, 0x00, 0x80, 0xff, 0x01, 0x00, 0xfc, 0x01, 0x00, 0x60, 0x00, 0x00, 0x00,
|
||||
0xff, 0xff, 0x5d, 0x05, 0xc0, 0x7f, 0x00, 0x00, 0xb3, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0xfe,
|
||||
0xff, 0xae, 0x00, 0x20, 0x60, 0x00, 0xc0, 0x80, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x9f,
|
||||
0x55, 0x05, 0x10, 0x40, 0x00, 0x30, 0x80, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x80, 0xaa,
|
||||
0x00, 0x10, 0x00, 0x00, 0x0c, 0x80, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x80, 0x55, 0x05,
|
||||
0x00, 0x00, 0x00, 0x03, 0x40, 0x00, 0x80, 0x10, 0x00, 0x00, 0x00, 0x00, 0x80, 0xaa, 0x00, 0x00,
|
||||
0x02, 0x80, 0x00, 0x20, 0x00, 0x40, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x05, 0x00, 0x02,
|
||||
0x60, 0x00, 0x10, 0x00, 0x40, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0xab, 0x00, 0x00, 0x0c, 0x18,
|
||||
0x00, 0x0c, 0x00, 0x20, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x15, 0x00, 0xf0, 0x07, 0x00,
|
||||
0x03, 0xc0, 0x11, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0xab, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00,
|
||||
0x20, 0x16, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x15, 0x00, 0x00, 0x00, 0x30, 0x00, 0x20,
|
||||
0x08, 0x21, 0x00, 0x00, 0x00, 0x00, 0x80, 0xab, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x20, 0x80,
|
||||
0x40, 0x00, 0x00, 0x00, 0x00, 0x40, 0x5d, 0x15, 0x00, 0x00, 0x00, 0x03, 0x00, 0x40, 0x80, 0x80,
|
||||
0x00, 0x00, 0x00, 0x00, 0xc0, 0xea, 0x02, 0x00, 0x00, 0xc0, 0x07, 0x00, 0x40, 0x40, 0x00, 0x01,
|
||||
0x00, 0x00, 0x00, 0x40, 0x55, 0x57, 0x00, 0x00, 0xf8, 0x07, 0x00, 0x80, 0x40, 0x00, 0x01, 0x00,
|
||||
0x00, 0x00, 0xc0, 0xaa, 0x3a, 0x00, 0x00, 0xfe, 0x05, 0x00, 0x80, 0x40, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0xe0, 0x55, 0xd5, 0x01, 0x00, 0xfc, 0x05, 0x00, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00,
|
||||
0xf0, 0xab, 0x0a, 0x1e, 0x00, 0x70, 0x0c, 0x00, 0x80, 0x00, 0x81, 0x00, 0x00, 0x00, 0x00, 0xd8,
|
||||
0x57, 0x55, 0xe1, 0x01, 0x00, 0x14, 0x00, 0x80, 0x00, 0x42, 0x00, 0x00, 0x00, 0x00, 0xec, 0xaf,
|
||||
0x0a, 0x00, 0xfe, 0x07, 0x14, 0x00, 0xe0, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0xf2, 0x7f, 0x55,
|
||||
0x05, 0x00, 0x18, 0x24, 0x00, 0x10, 0x01, 0x20, 0x00, 0x00, 0x00, 0x00, 0xeb, 0xff, 0x0a, 0x00,
|
||||
0x00, 0x20, 0x22, 0x00, 0x08, 0x02, 0x20, 0x00, 0x00, 0x00, 0x00, 0xf5, 0xff, 0x55, 0x05, 0x00,
|
||||
0x48, 0x22, 0x00, 0x04, 0x04, 0x10, 0x00, 0x00, 0x00, 0x80, 0xfa, 0xff, 0x0f, 0x00, 0x00, 0x8c,
|
||||
0x42, 0x00, 0x06, 0x08, 0x08, 0x00, 0x00, 0x00, 0x40, 0xf5, 0xff, 0x5f, 0x15, 0x00, 0x06, 0x4b,
|
||||
0x80, 0x09, 0x30, 0x04, 0x00, 0x00, 0x00, 0xc0, 0xfa, 0xff, 0xea, 0x00, 0x00, 0x07, 0x52, 0x40,
|
||||
0x10, 0xc0, 0x06, 0x00, 0x00, 0x00, 0x60, 0xf5, 0x7f, 0x55, 0x17, 0x80, 0x0f, 0x72, 0x30, 0x20,
|
||||
0x00, 0x07, 0x00, 0x00, 0x00, 0xb0, 0xfa, 0xbf, 0xaa, 0x38, 0xc0, 0x1f, 0xf4, 0xac, 0x42, 0x00,
|
||||
0x04, 0x00, 0x00, 0x00, 0x50, 0xf5, 0x5f, 0x55, 0xd5, 0xe1, 0x3f, 0x74, 0x57, 0x81, 0x01, 0x02,
|
||||
0x00, 0x00, 0x00, 0xa8, 0xfa, 0xaf, 0xaa, 0x80, 0xf3, 0x7f, 0xf8, 0xaa, 0x0a, 0x06, 0x01, 0x00,
|
||||
0x00, 0x00};
|
||||
|
||||
void drawScreen_1(Canvas* canvas) {
|
||||
canvas_set_bitmap_mode(canvas, true);
|
||||
|
||||
// Layer 3
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
canvas_draw_str(canvas, 86, 22, "24cXX ");
|
||||
|
||||
// Layer 3
|
||||
canvas_draw_str(canvas, 77, 10, "Dr.Mosfet ");
|
||||
|
||||
// Layer 4
|
||||
canvas_draw_str(canvas, 67, 34, "Programmer");
|
||||
|
||||
// DolphinMafia
|
||||
canvas_draw_xbm(canvas, -9, 12, 119, 62, image_DolphinMafia_0_bits);
|
||||
}
|
||||
|
After Width: | Height: | Size: 86 B |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,325 @@
|
||||
#include <stdio.h>
|
||||
#include <furi.h>
|
||||
#include <gui/gui.h>
|
||||
#include <input/input.h>
|
||||
#include <notification/notification.h>
|
||||
#include <notification/notification_messages.h>
|
||||
#include <dolphin/dolphin.h>
|
||||
|
||||
static int matrix[6][7] = {0};
|
||||
static int cursorx = 3;
|
||||
static int cursory = 5;
|
||||
static int player = 1;
|
||||
static int scoreX = 0;
|
||||
static int scoreO = 0;
|
||||
|
||||
typedef struct {
|
||||
FuriMutex* mutex;
|
||||
} FourInRowState;
|
||||
|
||||
void init() {
|
||||
for(size_t i = 0; i < 6; i++) {
|
||||
for(size_t j = 0; j < 7; j++) {
|
||||
matrix[i][j] = 0;
|
||||
}
|
||||
}
|
||||
cursorx = 3;
|
||||
cursory = 5;
|
||||
player = 1;
|
||||
}
|
||||
|
||||
const NotificationSequence end = {
|
||||
&message_vibro_on,
|
||||
|
||||
&message_note_ds4,
|
||||
&message_delay_10,
|
||||
&message_sound_off,
|
||||
&message_delay_10,
|
||||
|
||||
&message_note_ds4,
|
||||
&message_delay_10,
|
||||
&message_sound_off,
|
||||
&message_delay_10,
|
||||
|
||||
&message_note_ds4,
|
||||
&message_delay_10,
|
||||
&message_sound_off,
|
||||
&message_delay_10,
|
||||
|
||||
&message_vibro_off,
|
||||
NULL,
|
||||
};
|
||||
|
||||
void intToStr(int num, char* str) {
|
||||
int i = 0, sign = 0;
|
||||
|
||||
if(num < 0) {
|
||||
num = -num;
|
||||
sign = 1;
|
||||
}
|
||||
|
||||
do {
|
||||
str[i++] = num % 10 + '0';
|
||||
num /= 10;
|
||||
} while(num > 0);
|
||||
|
||||
if(sign) {
|
||||
str[i++] = '-';
|
||||
}
|
||||
|
||||
str[i] = '\0';
|
||||
|
||||
// Reverse the string
|
||||
int j, len = i;
|
||||
char temp;
|
||||
for(j = 0; j < len / 2; j++) {
|
||||
temp = str[j];
|
||||
str[j] = str[len - j - 1];
|
||||
str[len - j - 1] = temp;
|
||||
}
|
||||
}
|
||||
|
||||
int next_height(int x) {
|
||||
if(matrix[0][x] != 0) {
|
||||
return -1;
|
||||
}
|
||||
for(size_t y = 1; y < 6; y++) {
|
||||
if(matrix[y][x] != 0) {
|
||||
return y - 1;
|
||||
}
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
|
||||
int wincheck() {
|
||||
for(size_t y = 0; y <= 2; y++) {
|
||||
for(size_t x = 0; x <= 6; x++) {
|
||||
if(matrix[y][x] != 0 && matrix[y][x] == matrix[y + 1][x] &&
|
||||
matrix[y][x] == matrix[y + 2][x] && matrix[y][x] == matrix[y + 3][x]) {
|
||||
return matrix[y][x];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(size_t y = 0; y <= 5; y++) {
|
||||
for(size_t x = 0; x <= 3; x++) {
|
||||
if(matrix[y][x] != 0 && matrix[y][x] == matrix[y][x + 1] &&
|
||||
matrix[y][x] == matrix[y][x + 2] && matrix[y][x] == matrix[y][x + 3]) {
|
||||
return matrix[y][x];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(size_t y = 0; y <= 2; y++) {
|
||||
for(size_t x = 0; x <= 3; x++) {
|
||||
if(matrix[y][x] != 0 && matrix[y][x] == matrix[y + 1][x + 1] &&
|
||||
matrix[y][x] == matrix[y + 2][x + 2] && matrix[y][x] == matrix[y + 3][x + 3]) {
|
||||
return matrix[y][x];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(size_t y = 3; y <= 5; y++) {
|
||||
for(size_t x = 0; x <= 3; x++) {
|
||||
if(matrix[y][x] != 0 && matrix[y][x] == matrix[y - 1][x + 1] &&
|
||||
matrix[y][x] == matrix[y - 2][x + 2] && matrix[y][x] == matrix[y - 3][x + 3]) {
|
||||
return matrix[y][x];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool tf = true;
|
||||
for(size_t y = 0; y < 6; y++) {
|
||||
for(size_t x = 0; x < 7; x++) {
|
||||
if(matrix[y][x] == 0) {
|
||||
tf = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(tf) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void draw_callback(Canvas* canvas, void* ctx) {
|
||||
furi_assert(ctx);
|
||||
const FourInRowState* fourinrow_state = ctx;
|
||||
|
||||
furi_mutex_acquire(fourinrow_state->mutex, FuriWaitForever);
|
||||
canvas_clear(canvas);
|
||||
|
||||
if(wincheck() != -1) {
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
|
||||
if(wincheck() == 0) {
|
||||
canvas_draw_str(canvas, 30, 35, "Draw! O_o");
|
||||
}
|
||||
if(wincheck() == 1) {
|
||||
canvas_draw_str(canvas, 30, 35, "Player X win!");
|
||||
}
|
||||
if(wincheck() == 2) {
|
||||
canvas_draw_str(canvas, 30, 35, "Player O win!");
|
||||
}
|
||||
|
||||
furi_mutex_release(fourinrow_state->mutex);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for(size_t i = 0; i < 6; i++) {
|
||||
for(size_t j = 0; j < 7; j++) {
|
||||
char el[2];
|
||||
switch(matrix[i][j]) {
|
||||
case 0:
|
||||
strcpy(el, "_\0");
|
||||
break;
|
||||
|
||||
case 1:
|
||||
strcpy(el, "X\0");
|
||||
break;
|
||||
|
||||
case 2:
|
||||
strcpy(el, "O\0");
|
||||
break;
|
||||
}
|
||||
canvas_draw_str(canvas, j * 10 + 10, i * 10 + 10, el);
|
||||
}
|
||||
}
|
||||
canvas_draw_str(canvas, cursorx * 10 + 8, cursory * 10 + 10, "[ ]");
|
||||
|
||||
if(player == 1) {
|
||||
canvas_draw_str(canvas, 80, 10, "Turn: X");
|
||||
}
|
||||
if(player == 2) {
|
||||
canvas_draw_str(canvas, 80, 10, "Turn: O");
|
||||
}
|
||||
char scX[1];
|
||||
intToStr(scoreX, scX);
|
||||
char scO[1];
|
||||
intToStr(scoreO, scO);
|
||||
|
||||
canvas_draw_str(canvas, 80, 20, "X:");
|
||||
canvas_draw_str(canvas, 90, 20, scX);
|
||||
|
||||
canvas_draw_str(canvas, 80, 30, "O:");
|
||||
canvas_draw_str(canvas, 90, 30, scO);
|
||||
|
||||
furi_mutex_release(fourinrow_state->mutex);
|
||||
}
|
||||
|
||||
static void input_callback(InputEvent* input_event, void* ctx) {
|
||||
// Проверяем, что контекст не нулевой
|
||||
furi_assert(ctx);
|
||||
FuriMessageQueue* event_queue = ctx;
|
||||
|
||||
furi_message_queue_put(event_queue, input_event, FuriWaitForever);
|
||||
}
|
||||
|
||||
int32_t four_in_row_app(void* p) {
|
||||
UNUSED(p);
|
||||
|
||||
// Текущее событие типа InputEvent
|
||||
InputEvent event;
|
||||
// Очередь событий на 8 элементов размера InputEvent
|
||||
FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
|
||||
|
||||
FourInRowState* fourinrow_state = malloc(sizeof(FourInRowState));
|
||||
|
||||
fourinrow_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal); // Alloc Mutex
|
||||
if(!fourinrow_state->mutex) {
|
||||
FURI_LOG_E("4inRow", "cannot create mutex\r\n");
|
||||
furi_message_queue_free(event_queue);
|
||||
free(fourinrow_state);
|
||||
return 255;
|
||||
}
|
||||
|
||||
dolphin_deed(DolphinDeedPluginGameStart);
|
||||
|
||||
// Создаем новый view port
|
||||
ViewPort* view_port = view_port_alloc();
|
||||
// Создаем callback отрисовки, без контекста
|
||||
view_port_draw_callback_set(view_port, draw_callback, fourinrow_state);
|
||||
// Создаем callback нажатий на клавиши, в качестве контекста передаем
|
||||
// нашу очередь сообщений, чтоб запихивать в неё эти события
|
||||
view_port_input_callback_set(view_port, input_callback, event_queue);
|
||||
|
||||
// Создаем GUI приложения
|
||||
Gui* gui = furi_record_open(RECORD_GUI);
|
||||
// Подключаем view port к GUI в полноэкранном режиме
|
||||
gui_add_view_port(gui, view_port, GuiLayerFullscreen);
|
||||
NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
|
||||
notification_message_block(notification, &sequence_display_backlight_enforce_on);
|
||||
|
||||
// Бесконечный цикл обработки очереди событий
|
||||
while(1) {
|
||||
// Выбираем событие из очереди в переменную event (ждем бесконечно долго, если очередь пуста)
|
||||
// и проверяем, что у нас получилось это сделать
|
||||
if(furi_message_queue_get(event_queue, &event, FuriWaitForever) == FuriStatusOk) {
|
||||
if((event.type == InputTypePress) && (event.key == InputKeyBack)) {
|
||||
break;
|
||||
}
|
||||
|
||||
furi_mutex_acquire(fourinrow_state->mutex, FuriWaitForever);
|
||||
if(wincheck() != -1) {
|
||||
notification_message(notification, &end);
|
||||
furi_delay_ms(1000);
|
||||
if(wincheck() == 1) {
|
||||
scoreX++;
|
||||
}
|
||||
if(wincheck() == 2) {
|
||||
scoreO++;
|
||||
}
|
||||
init();
|
||||
furi_mutex_release(fourinrow_state->mutex);
|
||||
continue;
|
||||
}
|
||||
|
||||
if(event.type == InputTypePress) {
|
||||
if(event.key == InputKeyOk) {
|
||||
int nh = next_height(cursorx);
|
||||
if(nh != -1) {
|
||||
matrix[nh][cursorx] = player;
|
||||
player = 3 - player;
|
||||
}
|
||||
}
|
||||
if(event.key == InputKeyUp) {
|
||||
//cursory--;
|
||||
}
|
||||
if(event.key == InputKeyDown) {
|
||||
//cursory++;
|
||||
}
|
||||
if(event.key == InputKeyLeft) {
|
||||
if(cursorx > 0) {
|
||||
cursorx--;
|
||||
}
|
||||
}
|
||||
if(event.key == InputKeyRight) {
|
||||
if(cursorx < 6) {
|
||||
cursorx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
furi_mutex_release(fourinrow_state->mutex);
|
||||
}
|
||||
view_port_update(view_port);
|
||||
}
|
||||
|
||||
// Чистим созданные объекты, связанные с интерфейсом
|
||||
view_port_enabled_set(view_port, false);
|
||||
gui_remove_view_port(gui, view_port);
|
||||
view_port_free(view_port);
|
||||
furi_message_queue_free(event_queue);
|
||||
furi_record_close(RECORD_GUI);
|
||||
// Clear notification
|
||||
notification_message_block(notification, &sequence_display_backlight_enforce_auto);
|
||||
furi_record_close(RECORD_NOTIFICATION);
|
||||
|
||||
furi_mutex_free(fourinrow_state->mutex);
|
||||
|
||||
free(fourinrow_state);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
After Width: | Height: | Size: 200 B |
@@ -0,0 +1 @@
|
||||
Four in row for flipper zero!!
|
||||
@@ -0,0 +1,17 @@
|
||||
App(
|
||||
appid="4inrow",
|
||||
name="4 in row",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="four_in_row_app",
|
||||
requires=[
|
||||
"gui",
|
||||
],
|
||||
stack_size=1 * 1024,
|
||||
order=90,
|
||||
fap_icon="4inrow_10px.png",
|
||||
fap_category="Games",
|
||||
fap_author="leo-need-more-coffee",
|
||||
fap_weburl="https://github.com/leo-need-more-coffee/flipperzero-4inrow",
|
||||
fap_version="1.3",
|
||||
fap_description="4 in row Game",
|
||||
)
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,6 @@
|
||||
# Flipper-DVD-Bounce
|
||||
**simple dvd-bounce application for flipper**
|
||||
|
||||
Y'know how dvd players got that thing that bounces around?
|
||||
|
||||
*This is that*
|
||||
@@ -0,0 +1,15 @@
|
||||
# qv. https://github.com/flipperdevices/flipperzero-firmware/blob/dev/documentation/AppManifests.md
|
||||
|
||||
App(
|
||||
appid="dvd_bounce",
|
||||
name="DVD Bouncer",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="bounce_moment",
|
||||
requires=[
|
||||
"gui",
|
||||
],
|
||||
stack_size=1 * 1024,
|
||||
fap_icon="iconimage.png",
|
||||
fap_category="Games",
|
||||
fap_icon_assets="assets",
|
||||
)
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,142 @@
|
||||
#include <string.h>
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
#include <gui/gui.h>
|
||||
#include <input/input.h>
|
||||
#include "dvd_bounce_icons.h"
|
||||
|
||||
//init some variables
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int mode = 0;
|
||||
bool bounce_up = false;
|
||||
bool bounce_right = true;
|
||||
char mode_str[12];
|
||||
|
||||
//the thing to draw to the screen
|
||||
static void app_draw_callback(Canvas* canvas, void* ctx) {
|
||||
UNUSED(ctx);
|
||||
canvas_clear(canvas);
|
||||
|
||||
//draws the ball to positions x and y
|
||||
canvas_draw_icon(canvas, x, y, &I_Ok_btn_pressed_13x13);
|
||||
|
||||
//displays the current mode
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
canvas_draw_str(canvas, 2, 8, "Mode:");
|
||||
//converts mode int to string
|
||||
itoa(mode, mode_str, 10);
|
||||
canvas_draw_str(canvas, 28, 8, mode_str);
|
||||
switch(mode) {
|
||||
case 1:
|
||||
canvas_draw_str(canvas, 2, 16, "Left/Right");
|
||||
break;
|
||||
case 2:
|
||||
canvas_draw_str(canvas, 2, 16, "Up/Down");
|
||||
break;
|
||||
default:
|
||||
canvas_draw_str(canvas, 2, 16, "Normal");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void app_input_callback(InputEvent* input_event, void* ctx) {
|
||||
furi_assert(ctx);
|
||||
|
||||
FuriMessageQueue* event_queue = ctx;
|
||||
furi_message_queue_put(event_queue, input_event, FuriWaitForever);
|
||||
}
|
||||
|
||||
int32_t bounce_moment(void* p) {
|
||||
UNUSED(p);
|
||||
FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
|
||||
|
||||
// Configure viewport
|
||||
ViewPort* view_port = view_port_alloc();
|
||||
view_port_draw_callback_set(view_port, app_draw_callback, view_port);
|
||||
view_port_input_callback_set(view_port, app_input_callback, event_queue);
|
||||
|
||||
// Register viewport in GUI
|
||||
Gui* gui = furi_record_open(RECORD_GUI);
|
||||
gui_add_view_port(gui, view_port, GuiLayerFullscreen);
|
||||
|
||||
InputEvent event;
|
||||
|
||||
bool running = true;
|
||||
while(running) {
|
||||
if(furi_message_queue_get(event_queue, &event, 100) == FuriStatusOk) {
|
||||
if((event.type == InputTypePress) || (event.type == InputTypeRepeat)) {
|
||||
//arrows move the ball by 10 in their respective directions
|
||||
switch(event.key) {
|
||||
case InputKeyUp:
|
||||
y += -10;
|
||||
break;
|
||||
|
||||
case InputKeyDown:
|
||||
y += 10;
|
||||
break;
|
||||
|
||||
case InputKeyLeft:
|
||||
x += -10;
|
||||
break;
|
||||
|
||||
case InputKeyRight:
|
||||
x += 10;
|
||||
break;
|
||||
//sets the ball to the middle of the screen and sets the current mode
|
||||
case InputKeyOk:
|
||||
x = 51;
|
||||
y = 19;
|
||||
if(mode == 2) {
|
||||
mode = 0;
|
||||
} else {
|
||||
mode += 1;
|
||||
}
|
||||
break;
|
||||
//exits the program if back is pressed
|
||||
default:
|
||||
running = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//bunch of conditionals determining how the ball should move
|
||||
if(x <= 0) {
|
||||
bounce_up = false;
|
||||
}
|
||||
if(x >= 115) {
|
||||
bounce_up = true;
|
||||
}
|
||||
if(y <= 0) {
|
||||
bounce_right = true;
|
||||
}
|
||||
if(y >= 51) {
|
||||
bounce_right = false;
|
||||
}
|
||||
|
||||
if((bounce_up) && (mode != 2)) {
|
||||
x += -1;
|
||||
}
|
||||
if((!bounce_up) && (mode != 2)) {
|
||||
x += 1;
|
||||
}
|
||||
if((bounce_right) && (mode != 1)) {
|
||||
y += 1;
|
||||
}
|
||||
if((!bounce_right) && (mode != 1)) {
|
||||
y += -1;
|
||||
}
|
||||
|
||||
view_port_update(view_port);
|
||||
}
|
||||
//cleanup go brrrrr
|
||||
view_port_enabled_set(view_port, false);
|
||||
gui_remove_view_port(gui, view_port);
|
||||
view_port_free(view_port);
|
||||
furi_message_queue_free(event_queue);
|
||||
|
||||
furi_record_close(RECORD_GUI);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
After Width: | Height: | Size: 170 B |
@@ -0,0 +1,246 @@
|
||||
---
|
||||
Language: Cpp
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: BlockIndent
|
||||
AlignArrayOfStructures: None
|
||||
AlignConsecutiveAssignments:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
AlignFunctionPointers: false
|
||||
PadOperators: true
|
||||
AlignConsecutiveBitFields:
|
||||
Enabled: true
|
||||
AcrossEmptyLines: true
|
||||
AcrossComments: true
|
||||
AlignCompound: false
|
||||
AlignFunctionPointers: false
|
||||
PadOperators: true
|
||||
AlignConsecutiveDeclarations:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
AlignFunctionPointers: false
|
||||
PadOperators: true
|
||||
AlignConsecutiveMacros:
|
||||
Enabled: true
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: true
|
||||
AlignCompound: true
|
||||
AlignFunctionPointers: false
|
||||
PadOperators: true
|
||||
AlignConsecutiveShortCaseStatements:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCaseColons: false
|
||||
AlignEscapedNewlines: Left
|
||||
AlignOperands: Align
|
||||
AlignTrailingComments:
|
||||
Kind: Never
|
||||
OverEmptyLines: 0
|
||||
AllowAllArgumentsOnNextLine: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: false
|
||||
AllowBreakBeforeNoexceptSpecifier: Never
|
||||
AllowShortBlocksOnASingleLine: Never
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortCompoundRequirementOnASingleLine: true
|
||||
AllowShortEnumsOnASingleLine: false
|
||||
AllowShortFunctionsOnASingleLine: None
|
||||
AllowShortIfStatementsOnASingleLine: WithoutElse
|
||||
AllowShortLambdasOnASingleLine: All
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: Yes
|
||||
AttributeMacros:
|
||||
- __capability
|
||||
BinPackArguments: false
|
||||
BinPackParameters: false
|
||||
BitFieldColonSpacing: Both
|
||||
BraceWrapping:
|
||||
AfterCaseLabel: false
|
||||
AfterClass: false
|
||||
AfterControlStatement: Never
|
||||
AfterEnum: false
|
||||
AfterExternBlock: false
|
||||
AfterFunction: false
|
||||
AfterNamespace: false
|
||||
AfterObjCDeclaration: false
|
||||
AfterStruct: false
|
||||
AfterUnion: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
BeforeLambdaBody: false
|
||||
BeforeWhile: false
|
||||
IndentBraces: false
|
||||
SplitEmptyFunction: true
|
||||
SplitEmptyRecord: true
|
||||
SplitEmptyNamespace: true
|
||||
BreakAdjacentStringLiterals: true
|
||||
BreakAfterAttributes: Leave
|
||||
BreakAfterJavaFieldAnnotations: false
|
||||
BreakArrays: true
|
||||
BreakBeforeBinaryOperators: None
|
||||
BreakBeforeConceptDeclarations: Always
|
||||
BreakBeforeBraces: Attach
|
||||
BreakBeforeInlineASMColon: OnlyMultiline
|
||||
BreakBeforeTernaryOperators: false
|
||||
BreakConstructorInitializers: AfterColon
|
||||
BreakInheritanceList: AfterComma
|
||||
BreakStringLiterals: false
|
||||
ColumnLimit: 130
|
||||
CommentPragmas: '^ IWYU pragma:'
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerIndentWidth: 8
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: true
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
EmptyLineAfterAccessModifier: Never
|
||||
EmptyLineBeforeAccessModifier: LogicalBlock
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
FixNamespaceComments: false
|
||||
ForEachMacros:
|
||||
- foreach
|
||||
- Q_FOREACH
|
||||
- BOOST_FOREACH
|
||||
- M_EACH
|
||||
IfMacros:
|
||||
- KJ_IF_MAYBE
|
||||
IncludeBlocks: Preserve
|
||||
IncludeCategories:
|
||||
- Regex: '.*'
|
||||
Priority: 1
|
||||
SortPriority: 0
|
||||
CaseSensitive: false
|
||||
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
|
||||
Priority: 3
|
||||
SortPriority: 0
|
||||
CaseSensitive: false
|
||||
- Regex: '.*'
|
||||
Priority: 1
|
||||
SortPriority: 0
|
||||
CaseSensitive: false
|
||||
IncludeIsMainRegex: '(Test)?$'
|
||||
IncludeIsMainSourceRegex: ''
|
||||
IndentAccessModifiers: false
|
||||
IndentCaseBlocks: false
|
||||
IndentCaseLabels: false
|
||||
IndentExternBlock: AfterExternBlock
|
||||
IndentGotoLabels: true
|
||||
IndentPPDirectives: None
|
||||
IndentRequiresClause: false
|
||||
IndentWidth: 4
|
||||
IndentWrappedFunctionNames: true
|
||||
InsertBraces: false
|
||||
InsertNewlineAtEOF: true
|
||||
InsertTrailingCommas: None
|
||||
IntegerLiteralSeparator:
|
||||
Binary: 0
|
||||
BinaryMinDigits: 0
|
||||
Decimal: 0
|
||||
DecimalMinDigits: 0
|
||||
Hex: 0
|
||||
HexMinDigits: 0
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||
KeepEmptyLinesAtEOF: false
|
||||
LambdaBodyIndentation: Signature
|
||||
LineEnding: DeriveLF
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: None
|
||||
ObjCBinPackProtocolList: Auto
|
||||
ObjCBlockIndentWidth: 4
|
||||
ObjCBreakBeforeNestedBlockParam: true
|
||||
ObjCSpaceAfterProperty: true
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PackConstructorInitializers: BinPack
|
||||
PenaltyBreakAssignment: 10
|
||||
PenaltyBreakBeforeFirstCallParameter: 30
|
||||
PenaltyBreakComment: 10
|
||||
PenaltyBreakFirstLessLess: 0
|
||||
PenaltyBreakOpenParenthesis: 0
|
||||
PenaltyBreakScopeResolution: 500
|
||||
PenaltyBreakString: 10
|
||||
PenaltyBreakTemplateDeclaration: 10
|
||||
PenaltyExcessCharacter: 100
|
||||
PenaltyIndentedWhitespace: 0
|
||||
PenaltyReturnTypeOnItsOwnLine: 60
|
||||
PointerAlignment: Left
|
||||
PPIndentWidth: -1
|
||||
QualifierAlignment: Leave
|
||||
ReferenceAlignment: Pointer
|
||||
ReflowComments: false
|
||||
RemoveBracesLLVM: false
|
||||
RemoveParentheses: Leave
|
||||
RemoveSemicolon: true
|
||||
RequiresClausePosition: OwnLine
|
||||
RequiresExpressionIndentation: OuterScope
|
||||
SeparateDefinitionBlocks: Leave
|
||||
ShortNamespaceLines: 1
|
||||
SkipMacroDefinitionBody: false
|
||||
SortIncludes: Never
|
||||
SortJavaStaticImport: Before
|
||||
SortUsingDeclarations: Never
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterLogicalNot: false
|
||||
SpaceAfterTemplateKeyword: true
|
||||
SpaceAroundPointerQualifiers: Default
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeCaseColon: false
|
||||
SpaceBeforeCpp11BracedList: false
|
||||
SpaceBeforeCtorInitializerColon: true
|
||||
SpaceBeforeInheritanceColon: true
|
||||
SpaceBeforeJsonColon: false
|
||||
SpaceBeforeParens: Never
|
||||
SpaceBeforeParensOptions:
|
||||
AfterControlStatements: false
|
||||
AfterForeachMacros: false
|
||||
AfterFunctionDefinitionName: false
|
||||
AfterFunctionDeclarationName: false
|
||||
AfterIfMacros: false
|
||||
AfterOverloadedOperator: false
|
||||
AfterPlacementOperator: true
|
||||
AfterRequiresInClause: false
|
||||
AfterRequiresInExpression: false
|
||||
BeforeNonEmptyParentheses: false
|
||||
SpaceBeforeRangeBasedForLoopColon: true
|
||||
SpaceBeforeSquareBrackets: false
|
||||
SpaceInEmptyBlock: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: Never
|
||||
SpacesInContainerLiterals: false
|
||||
SpacesInLineCommentPrefix:
|
||||
Minimum: 1
|
||||
Maximum: -1
|
||||
SpacesInParens: Never
|
||||
SpacesInParensOptions:
|
||||
InCStyleCasts: false
|
||||
InConditionalStatements: false
|
||||
InEmptyParentheses: false
|
||||
Other: false
|
||||
SpacesInSquareBrackets: false
|
||||
Standard: c++20
|
||||
StatementAttributeLikeMacros:
|
||||
- Q_EMIT
|
||||
StatementMacros:
|
||||
- Q_UNUSED
|
||||
- QT_REQUIRE_VERSION
|
||||
TabWidth: 4
|
||||
UseTab: Never
|
||||
VerilogBreakBetweenInstancePorts: true
|
||||
WhitespaceSensitiveMacros:
|
||||
- STRINGIZE
|
||||
- PP_STRINGIZE
|
||||
- BOOST_PP_STRINGIZE
|
||||
- NS_SWIFT_NAME
|
||||
- CF_SWIFT_NAME
|
||||
...
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Denr01
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Chief Cooker
|
||||
Your ultimate Flipper Zero restaurant pager tool. Be a _real chief_ of all the restaurants on the food court!
|
||||
|
||||
This app supports receiving, decoding, editing and sending restaurant pager signals.
|
||||
|
||||
**Developed & compatible with [Momentum firmware](https://github.com/Next-Flip/Momentum-Firmware).** Other firmwares are most likely not supported (but I've not tried).
|
||||
|
||||
## Video demo
|
||||
[](https://youtube.com/shorts/iuQSyesS9-o)
|
||||
|
||||
More demos:
|
||||
- [Video 2](https://youtube.com/shorts/KGDAGblbtFo)
|
||||
- [Video 3](https://youtube.com/shorts/QqbfHF-yDiE)
|
||||
|
||||
## Disclaimer
|
||||
I've built this app for research and learning purposes. But please, don't use it in a way that could hurt anyone or anything.
|
||||
|
||||
Use it responsibly, okay?
|
||||
|
||||
## [Usage instructions](instructions/instructions.md)
|
||||
Please, read the [instructions](instructions/instructions.md) before using the app. It will definitely make your life easier!
|
||||
|
||||
## Features
|
||||
- **Receive** signals from pager stations
|
||||
- Automatically **decode** them and dsiplay station number, pager number and action (Ring/Mute/etc)
|
||||
- Manually **change encoding in real-time** to look for the best one if automatically detected encoding is not working
|
||||
- **Resend** captured message to specific pager to all at once to **make them all ring**!
|
||||
- **Modify** captured signal, e.g. change pager number or action
|
||||
- **Save** captured signals (and give each station a name)
|
||||
- Create separate **categories** for each food court you are chief on
|
||||
- Display signals from saved stations **by ther names** (instead of HEX code) or hide them from list
|
||||
- **Send** signals from saved stations at any time, no need to capture it again
|
||||
- Of course, suports working with **external CC1101 module** to cover the area of all the pagers on your food court!
|
||||
|
||||
## Supported protocols
|
||||
- Princeton
|
||||
- SMC5326
|
||||
|
||||
## Supported pager encodings
|
||||
- Retekess TD157
|
||||
- Retekess TD165/T119
|
||||
- Retekess TD174
|
||||
- L8R / Retekess T111 (not tested)
|
||||
- L8S / iBells ZJ-68 (check [source code](app/pager/decoder/L8SDecoder.hpp#L8) for description)
|
||||
|
||||
## Contributing
|
||||
If you want to add any new pager encoding, please feel free to create PR with it!
|
||||
|
||||
Also you can open issue and share with me any captured data (and at least pager number) if you have any and maybe I'll try create a decoder for it
|
||||
|
||||
## Building
|
||||
If you build the source code just with regular `ufbt` command, the app will probably crash due to out of memory error because your device will have less that 10 kb of free RAM on the "Scan" screen.
|
||||
|
||||
This is because after you compile the app with ufbt, the result executable will contain hundreds of sections with very long names like `.fast.rel.text._ZNSt17_Function_handlerIFvmEZN18PagerActionsScreenC4EP9AppConfigSt8functionIFP15StoredPagerDatavEEP12PagerDecoderP13PagerProtocolP12SubGhzModuleEUlmE_E9_M_invokeERKSt9_Any_dataOm`.
|
||||
**These names stay in RAM during the execution and consume about 20kb of heap!**
|
||||
|
||||
The reason for it is [name mangling](https://en.wikipedia.org/wiki/Name_mangling). Perhaps, the gcc parameter `-fno-mangle` could disable it, but unfortunately, it is not possible to pass any arguments to gcc when you compile app with `ufbt`.
|
||||
Luckily the sections inside the compiled file can be renamed using gcc's `objcopy` tool with `--rename-section` parameter. To automate it, I built a small python script which renames them all and gives them short names like `_s1`, `_s2`, `_s228` etc...
|
||||
|
||||
**Therfore you must use [scripts/build-and-clear.py](scripts/build-and-clear.py) script instead!** It will build, rename sections and upload the fap to flipper.
|
||||
|
||||
After building and cleaning your `.fap` with it, you'll get extra +20kb of free RAM which will make compiled app work stably.
|
||||
|
||||
The `.fap` files under the release tab are already cleared with this script, so if you just want to use this app without modifications, just forget about it and download the latest one from releases.
|
||||
|
||||
## Support & Donate
|
||||
|
||||
> [PayPal](https://paypal.me/denr01)
|
||||
|
||||
## Special Thanks
|
||||
- [meoker/pagger](https://github.com/meoker/pagger) for Retekess pager encodings
|
||||
- This [awesome repository](https://dev.xcjs.com/r0073dl053r/flipper-playground/-/tree/main/Sub-GHz/Restaurant_Pagers?ref_type=heads) for Retekess T111 and iBells ZJ-68 files
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "lib/ui/UiManager.hpp"
|
||||
#include "lib/hardware/notification/Notification.hpp"
|
||||
#include "lib/hardware/subghz/FrequencyManager.hpp"
|
||||
|
||||
#include "AppConfig.hpp"
|
||||
#include "app/screen/MainMenuScreen.hpp"
|
||||
|
||||
using namespace std;
|
||||
|
||||
class App {
|
||||
public:
|
||||
void Run() {
|
||||
UiManager* ui = UiManager::GetInstance();
|
||||
ui->InitGui();
|
||||
|
||||
FrequencyManager* frequencyManager = FrequencyManager::GetInstance();
|
||||
AppConfig* config = new AppConfig();
|
||||
config->Load();
|
||||
|
||||
MainMenuScreen* mainMenuScreen = new MainMenuScreen(config);
|
||||
ui->PushView(mainMenuScreen->GetView());
|
||||
ui->RunEventLoop();
|
||||
|
||||
delete frequencyManager;
|
||||
delete mainMenuScreen;
|
||||
delete config;
|
||||
delete ui;
|
||||
|
||||
Notification::Dispose();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "app/AppFileSystem.hpp"
|
||||
#include "lib/file/FileManager.hpp"
|
||||
#include "app/pager/SavedStationStrategy.hpp"
|
||||
|
||||
#define KEY_CONFIG_FREQUENCY "Frequency"
|
||||
#define KEY_CONFIG_MAX_PAGERS "MaxPagerForBatchOrDetection"
|
||||
#define KEY_CONFIG_REPEATS "SignalRepeats"
|
||||
#define KEY_CONFIG_SAVED_STRATEGY "SavedStationStrategy"
|
||||
#define KEY_CONFIG_AUTOSAVE "AutosaveFoundSignals"
|
||||
#define KEY_CONFIG_USER_CATGEGORY "UserCategory"
|
||||
|
||||
class AppConfig {
|
||||
public:
|
||||
uint32_t Frequency = 433920000;
|
||||
uint32_t MaxPagerForBatchOrDetection = 30;
|
||||
uint32_t SignalRepeats = 10;
|
||||
SavedStationStrategy SavedStrategy = SHOW_NAME;
|
||||
bool AutosaveFoundSignals = true;
|
||||
String* CurrentUserCategory = NULL;
|
||||
|
||||
private:
|
||||
void readFromFile(FlipperFile* file) {
|
||||
String* userCat = new String();
|
||||
uint32_t savedStrategyValue = SavedStrategy;
|
||||
if(CurrentUserCategory != NULL) {
|
||||
delete CurrentUserCategory;
|
||||
}
|
||||
|
||||
file->ReadUInt32(KEY_CONFIG_FREQUENCY, &Frequency);
|
||||
file->ReadUInt32(KEY_CONFIG_MAX_PAGERS, &MaxPagerForBatchOrDetection);
|
||||
file->ReadUInt32(KEY_CONFIG_REPEATS, &SignalRepeats);
|
||||
file->ReadUInt32(KEY_CONFIG_SAVED_STRATEGY, &savedStrategyValue);
|
||||
file->ReadBool(KEY_CONFIG_AUTOSAVE, &AutosaveFoundSignals);
|
||||
file->ReadString(KEY_CONFIG_USER_CATGEGORY, userCat);
|
||||
|
||||
SavedStrategy = static_cast<enum SavedStationStrategy>(savedStrategyValue);
|
||||
if(!userCat->isEmpty()) {
|
||||
CurrentUserCategory = userCat;
|
||||
} else {
|
||||
CurrentUserCategory = NULL;
|
||||
delete userCat;
|
||||
}
|
||||
}
|
||||
|
||||
void writeToFile(FlipperFile* file) {
|
||||
file->WriteUInt32(KEY_CONFIG_FREQUENCY, Frequency);
|
||||
file->WriteUInt32(KEY_CONFIG_MAX_PAGERS, MaxPagerForBatchOrDetection);
|
||||
file->WriteUInt32(KEY_CONFIG_REPEATS, SignalRepeats);
|
||||
file->WriteUInt32(KEY_CONFIG_SAVED_STRATEGY, SavedStrategy);
|
||||
file->WriteBool(KEY_CONFIG_AUTOSAVE, AutosaveFoundSignals);
|
||||
file->WriteString(KEY_CONFIG_USER_CATGEGORY, CurrentUserCategory != NULL ? CurrentUserCategory->cstr() : "");
|
||||
}
|
||||
|
||||
public:
|
||||
void Load() {
|
||||
FlipperFile* configFile = FileManager().OpenRead(CONFIG_FILE_PATH);
|
||||
if(configFile != NULL) {
|
||||
readFromFile(configFile);
|
||||
delete configFile;
|
||||
}
|
||||
}
|
||||
|
||||
void Save() {
|
||||
FlipperFile* configFile = FileManager().OpenWrite(CONFIG_FILE_PATH);
|
||||
if(configFile != NULL) {
|
||||
writeToFile(configFile);
|
||||
delete configFile;
|
||||
}
|
||||
}
|
||||
|
||||
const char* GetCurrentUserCategoryCstr() {
|
||||
return CurrentUserCategory == NULL ? NULL : CurrentUserCategory->cstr();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,188 @@
|
||||
#pragma once
|
||||
|
||||
#include <storage/storage.h>
|
||||
#include <forward_list>
|
||||
|
||||
#include "app/pager/PagerSerializer.hpp"
|
||||
#include "lib/file/FileManager.hpp"
|
||||
#include "pager/data/NamedPagerData.hpp"
|
||||
|
||||
// .fff stands for (f)lipper (f)ile (f)ormat
|
||||
#define CONFIG_FILE_PATH APP_DATA_PATH("config.fff")
|
||||
|
||||
#define STATIONS_PATH APP_DATA_PATH("stations")
|
||||
#define STATIONS_PATH_OF(path) STATIONS_PATH "/" path
|
||||
|
||||
#define SAVED_STATIONS_PATH STATIONS_PATH_OF("saved")
|
||||
#define AUTOSAVED_STATIONS_PATH STATIONS_PATH_OF("autosaved")
|
||||
|
||||
#define MAX_FILENAME_LENGTH 16
|
||||
|
||||
using namespace std;
|
||||
|
||||
enum CategoryType {
|
||||
User,
|
||||
Autosaved,
|
||||
|
||||
NotSelected,
|
||||
};
|
||||
|
||||
class AppFileSysytem {
|
||||
private:
|
||||
String* getCategoryPath(CategoryType categoryType, const char* category) {
|
||||
switch(categoryType) {
|
||||
case User:
|
||||
if(category != NULL) {
|
||||
return new String("%s/%s", SAVED_STATIONS_PATH, category);
|
||||
} else {
|
||||
return new String(SAVED_STATIONS_PATH);
|
||||
}
|
||||
|
||||
case Autosaved:
|
||||
return new String("%s/%s", AUTOSAVED_STATIONS_PATH, category);
|
||||
|
||||
default:
|
||||
case NotSelected:
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
String* getFilePath(CategoryType categoryType, const char* category, StoredPagerData* pager) {
|
||||
String* categoryPath = getCategoryPath(categoryType, category);
|
||||
String* pagerFilename = PagerSerializer().GetFilename(pager);
|
||||
String* filePath = new String("%s/%s", categoryPath->cstr(), pagerFilename->cstr());
|
||||
delete categoryPath;
|
||||
delete pagerFilename;
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public:
|
||||
int GetCategories(forward_list<char*>* categoryList, CategoryType categoryType) {
|
||||
const char* dirPath;
|
||||
switch(categoryType) {
|
||||
case User:
|
||||
dirPath = SAVED_STATIONS_PATH;
|
||||
break;
|
||||
|
||||
case Autosaved:
|
||||
dirPath = AUTOSAVED_STATIONS_PATH;
|
||||
break;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
FileManager fileManager = FileManager();
|
||||
Directory* dir = fileManager.OpenDirectory(dirPath);
|
||||
uint16_t categoriesLoaded = 0;
|
||||
|
||||
if(dir != NULL) {
|
||||
char fileName[MAX_FILENAME_LENGTH];
|
||||
while(dir->GetNextDir(fileName, MAX_FILENAME_LENGTH)) {
|
||||
char* category = new char[strlen(fileName)];
|
||||
strcpy(category, fileName);
|
||||
categoryList->push_front(category);
|
||||
categoriesLoaded++;
|
||||
}
|
||||
}
|
||||
|
||||
delete dir;
|
||||
return categoriesLoaded;
|
||||
}
|
||||
|
||||
size_t GetStationsFromDirectory(
|
||||
forward_list<NamedPagerData>* stationList,
|
||||
ProtocolAndDecoderProvider* pdProvider,
|
||||
CategoryType categoryType,
|
||||
const char* category,
|
||||
bool loadNames
|
||||
) {
|
||||
FileManager fileManager = FileManager();
|
||||
String* stationDirPath = getCategoryPath(categoryType, category);
|
||||
Directory* dir = fileManager.OpenDirectory(stationDirPath->cstr());
|
||||
PagerSerializer serializer = PagerSerializer();
|
||||
size_t stationsLoaded = 0;
|
||||
|
||||
if(dir != NULL) {
|
||||
char fileName[MAX_FILENAME_LENGTH];
|
||||
while(dir->GetNextFile(fileName, MAX_FILENAME_LENGTH)) {
|
||||
String* stationName = new String();
|
||||
StoredPagerData pager =
|
||||
serializer.LoadPagerData(&fileManager, stationName, stationDirPath->cstr(), fileName, pdProvider);
|
||||
|
||||
if(!loadNames) {
|
||||
delete stationName;
|
||||
stationName = NULL;
|
||||
}
|
||||
|
||||
NamedPagerData returnData = NamedPagerData();
|
||||
returnData.storedData = pager;
|
||||
returnData.name = stationName;
|
||||
stationList->push_front(returnData);
|
||||
stationsLoaded++;
|
||||
}
|
||||
}
|
||||
|
||||
delete dir;
|
||||
delete stationDirPath;
|
||||
|
||||
return stationsLoaded;
|
||||
}
|
||||
|
||||
String* GetOnlyStationName(CategoryType categoryType, const char* category, StoredPagerData* pager) {
|
||||
FileManager fileManager = FileManager();
|
||||
String* categoryPath = getCategoryPath(categoryType, category);
|
||||
String* name = PagerSerializer().LoadOnlyStationName(&fileManager, categoryPath->cstr(), pager);
|
||||
delete categoryPath;
|
||||
return name;
|
||||
}
|
||||
|
||||
void AutoSave(StoredPagerData* storedData, PagerDecoder* decoder, PagerProtocol* protocol, uint32_t frequency) {
|
||||
DateTime datetime;
|
||||
furi_hal_rtc_get_datetime(&datetime);
|
||||
String* todayDate = new String("%d-%02d-%02d", datetime.year, datetime.month, datetime.day);
|
||||
String* todaysDir = getCategoryPath(Autosaved, todayDate->cstr());
|
||||
|
||||
FileManager fileManager = FileManager();
|
||||
fileManager.CreateDirIfNotExists(STATIONS_PATH);
|
||||
fileManager.CreateDirIfNotExists(AUTOSAVED_STATIONS_PATH);
|
||||
fileManager.CreateDirIfNotExists(todaysDir->cstr());
|
||||
|
||||
PagerSerializer().SavePagerData(&fileManager, todaysDir->cstr(), "", storedData, decoder, protocol, frequency);
|
||||
|
||||
delete todaysDir;
|
||||
delete todayDate;
|
||||
}
|
||||
|
||||
void SaveToUserCategory(
|
||||
const char* userCategory,
|
||||
const char* stationName,
|
||||
StoredPagerData* storedData,
|
||||
PagerDecoder* decoder,
|
||||
PagerProtocol* protocol,
|
||||
uint32_t frequency
|
||||
) {
|
||||
String* catDir = getCategoryPath(User, userCategory);
|
||||
|
||||
FileManager fileManager = FileManager();
|
||||
fileManager.CreateDirIfNotExists(STATIONS_PATH);
|
||||
fileManager.CreateDirIfNotExists(AUTOSAVED_STATIONS_PATH);
|
||||
fileManager.CreateDirIfNotExists(catDir->cstr());
|
||||
|
||||
PagerSerializer().SavePagerData(&fileManager, catDir->cstr(), stationName, storedData, decoder, protocol, frequency);
|
||||
|
||||
delete catDir;
|
||||
}
|
||||
|
||||
void DeletePager(const char* userCategory, StoredPagerData* storedData) {
|
||||
String* filePath = getFilePath(User, userCategory, storedData);
|
||||
FileManager().DeleteFile(filePath->cstr());
|
||||
delete filePath;
|
||||
}
|
||||
|
||||
void DeleteCategory(const char* userCategory) {
|
||||
String* catPath = getCategoryPath(User, userCategory);
|
||||
FileManager().DeleteFile(catPath->cstr());
|
||||
delete catPath;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "lib/hardware/notification/Notification.hpp"
|
||||
|
||||
const NotificationSequence NOTIFICATION_PAGER_RECEIVE = {
|
||||
&message_vibro_on,
|
||||
&message_note_e6,
|
||||
|
||||
&message_blue_255,
|
||||
&message_delay_50,
|
||||
|
||||
&message_sound_off,
|
||||
&message_vibro_off,
|
||||
|
||||
NULL,
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
enum PagerAction {
|
||||
UNKNOWN,
|
||||
RING,
|
||||
MUTE,
|
||||
DESYNC,
|
||||
TURN_OFF_ALL,
|
||||
|
||||
PagerActionCount,
|
||||
};
|
||||
|
||||
class PagerActions {
|
||||
public:
|
||||
static const char* GetDescription(PagerAction action) {
|
||||
switch(action) {
|
||||
case UNKNOWN:
|
||||
return "?";
|
||||
case RING:
|
||||
return "RING";
|
||||
case MUTE:
|
||||
return "MUTE";
|
||||
case DESYNC:
|
||||
return "DESYNC_ALL";
|
||||
case TURN_OFF_ALL:
|
||||
return "ALL_OFF";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsPagerActionSpecial(PagerAction action) {
|
||||
switch(action) {
|
||||
case DESYNC:
|
||||
case TURN_OFF_ALL:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,319 @@
|
||||
#pragma once
|
||||
|
||||
#include "ProtocolAndDecoderProvider.hpp"
|
||||
#include <cstring>
|
||||
#include <forward_list>
|
||||
|
||||
#include "lib/hardware/subghz/FrequencyManager.hpp"
|
||||
#include "lib/hardware/subghz/data/SubGhzReceivedData.hpp"
|
||||
|
||||
#include "app/AppConfig.hpp"
|
||||
|
||||
#include "data/ReceivedPagerData.hpp"
|
||||
#include "data/KnownStationData.hpp"
|
||||
|
||||
#include "protocol/PrincetonProtocol.hpp"
|
||||
#include "protocol/Smc5326Protocol.hpp"
|
||||
|
||||
#include "decoder/Td157Decoder.hpp"
|
||||
#include "decoder/Td165Decoder.hpp"
|
||||
#include "decoder/Td174Decoder.hpp"
|
||||
#include "decoder/L8RDecoder.hpp"
|
||||
#include "decoder/L8SDecoder.hpp"
|
||||
|
||||
#undef LOG_TAG
|
||||
#define LOG_TAG "PGR_RCV"
|
||||
|
||||
#define MAX_REPEATS 99
|
||||
#define PAGERS_ARRAY_SIZE_MULTIPLIER 8
|
||||
|
||||
using namespace std;
|
||||
|
||||
class PagerReceiver : public ProtocolAndDecoderProvider {
|
||||
public:
|
||||
static const uint8_t protocolsCount = 2;
|
||||
PagerProtocol* protocols[protocolsCount]{
|
||||
new PrincetonProtocol(),
|
||||
new Smc5326Protocol(),
|
||||
};
|
||||
|
||||
static const uint8_t decodersCount = 5;
|
||||
PagerDecoder* decoders[decodersCount]{
|
||||
new Td157Decoder(),
|
||||
new Td165Decoder(),
|
||||
new Td174Decoder(),
|
||||
new L8RDecoder(),
|
||||
new L8SDecoder(),
|
||||
};
|
||||
|
||||
private:
|
||||
AppConfig* config;
|
||||
uint16_t nextPagerIndex = 0;
|
||||
uint16_t pagersArraySize = PAGERS_ARRAY_SIZE_MULTIPLIER;
|
||||
StoredPagerData* pagers = new StoredPagerData[pagersArraySize];
|
||||
size_t knownStationsSize = 0;
|
||||
KnownStationData* knownStations;
|
||||
uint32_t lastFrequency = 0;
|
||||
uint8_t lastFrequencyIndex = 0;
|
||||
bool knownStationsLoaded = false;
|
||||
const char* userCategory;
|
||||
|
||||
void loadKnownStations() {
|
||||
AppFileSysytem appFilesystem;
|
||||
forward_list<NamedPagerData> stations;
|
||||
bool withNames = config->SavedStrategy == SHOW_NAME;
|
||||
|
||||
size_t count = appFilesystem.GetStationsFromDirectory(&stations, this, User, userCategory, withNames);
|
||||
|
||||
knownStations = new KnownStationData[count];
|
||||
for(size_t i = 0; i < count; i++) {
|
||||
knownStations[i] = buildKnownStationWithName(stations.front());
|
||||
stations.pop_front();
|
||||
}
|
||||
|
||||
knownStationsSize = count;
|
||||
knownStationsLoaded = true;
|
||||
}
|
||||
|
||||
void unloadKnownStations() {
|
||||
for(size_t i = 0; i < knownStationsSize; i++) {
|
||||
if(knownStations[i].name != NULL) {
|
||||
delete knownStations[i].name;
|
||||
}
|
||||
}
|
||||
|
||||
delete[] knownStations;
|
||||
|
||||
knownStationsLoaded = false;
|
||||
knownStationsSize = 0;
|
||||
}
|
||||
|
||||
KnownStationData buildKnownStationWithName(NamedPagerData pager) {
|
||||
KnownStationData data = KnownStationData();
|
||||
data.frequency = pager.storedData.frequency;
|
||||
data.protocol = pager.storedData.protocol;
|
||||
data.decoder = pager.storedData.decoder;
|
||||
data.station = decoders[pager.storedData.decoder]->GetStation(pager.storedData.data);
|
||||
data.name = pager.name;
|
||||
return data;
|
||||
}
|
||||
|
||||
KnownStationData buildKnownStationWithoutName(StoredPagerData* pager) {
|
||||
KnownStationData data = KnownStationData();
|
||||
data.frequency = pager->frequency;
|
||||
data.protocol = pager->protocol;
|
||||
data.decoder = pager->decoder;
|
||||
data.station = decoders[pager->decoder]->GetStation(pager->data);
|
||||
data.name = NULL;
|
||||
return data;
|
||||
}
|
||||
|
||||
PagerDecoder* getDecoder(StoredPagerData* pagerData) {
|
||||
for(size_t i = 0; i < decodersCount; i++) {
|
||||
pagerData->decoder = i;
|
||||
if(IsKnown(pagerData)) {
|
||||
return decoders[i];
|
||||
}
|
||||
}
|
||||
|
||||
for(size_t i = 0; i < decodersCount; i++) {
|
||||
if(decoders[i]->GetPager(pagerData->data) <= config->MaxPagerForBatchOrDetection) {
|
||||
return decoders[i];
|
||||
}
|
||||
}
|
||||
|
||||
return decoders[0];
|
||||
}
|
||||
|
||||
void addPager(StoredPagerData data) {
|
||||
if(nextPagerIndex == pagersArraySize) {
|
||||
pagersArraySize += PAGERS_ARRAY_SIZE_MULTIPLIER;
|
||||
StoredPagerData* newPagers = new StoredPagerData[pagersArraySize];
|
||||
for(int i = 0; i < nextPagerIndex; i++) {
|
||||
newPagers[i] = pagers[i];
|
||||
}
|
||||
delete[] pagers;
|
||||
pagers = newPagers;
|
||||
}
|
||||
pagers[nextPagerIndex++] = data;
|
||||
}
|
||||
|
||||
public:
|
||||
PagerReceiver(AppConfig* config) {
|
||||
this->config = config;
|
||||
|
||||
for(size_t i = 0; i < protocolsCount; i++) {
|
||||
protocols[i]->id = i;
|
||||
}
|
||||
|
||||
for(size_t i = 0; i < decodersCount; i++) {
|
||||
decoders[i]->id = i;
|
||||
}
|
||||
|
||||
SetUserCategory(config->CurrentUserCategory);
|
||||
}
|
||||
|
||||
void SetUserCategory(String* category) {
|
||||
SetUserCategory(category != NULL ? category->cstr() : NULL);
|
||||
}
|
||||
|
||||
const char* GetCurrentUserCategory() {
|
||||
return userCategory;
|
||||
}
|
||||
|
||||
void SetUserCategory(const char* category) {
|
||||
userCategory = category;
|
||||
}
|
||||
|
||||
PagerProtocol* GetProtocolByName(const char* systemProtocolName) {
|
||||
for(size_t i = 0; i < protocolsCount; i++) {
|
||||
if(strcmp(systemProtocolName, protocols[i]->GetSystemName()) == 0) {
|
||||
return protocols[i];
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PagerDecoder* GetDecoderByName(const char* shortName) {
|
||||
for(size_t i = 0; i < decodersCount; i++) {
|
||||
if(strcmp(shortName, decoders[i]->GetShortName()) == 0) {
|
||||
return decoders[i];
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void ReloadKnownStations() {
|
||||
unloadKnownStations();
|
||||
loadKnownStations();
|
||||
}
|
||||
|
||||
void LoadStationsFromDirectory(
|
||||
CategoryType categoryType,
|
||||
const char* category,
|
||||
function<void(ReceivedPagerData*)> pagerHandler
|
||||
) {
|
||||
AppFileSysytem appFilesystem;
|
||||
forward_list<NamedPagerData> stations;
|
||||
bool withNames = !knownStationsLoaded && config->SavedStrategy == SHOW_NAME;
|
||||
|
||||
int count = appFilesystem.GetStationsFromDirectory(&stations, this, categoryType, category, withNames);
|
||||
|
||||
delete[] pagers;
|
||||
pagers = new StoredPagerData[count];
|
||||
|
||||
if(!knownStationsLoaded) {
|
||||
knownStations = new KnownStationData[count];
|
||||
}
|
||||
|
||||
for(int i = 0; i < count; i++) {
|
||||
NamedPagerData pagerData = stations.front();
|
||||
pagers[i] = pagerData.storedData;
|
||||
if(!knownStationsLoaded) {
|
||||
knownStations[i] = buildKnownStationWithName(pagerData);
|
||||
}
|
||||
stations.pop_front();
|
||||
|
||||
pagerHandler(new ReceivedPagerData(PagerGetter(i), i, true));
|
||||
}
|
||||
|
||||
if(!knownStationsLoaded) {
|
||||
knownStationsSize = count;
|
||||
}
|
||||
|
||||
nextPagerIndex = count;
|
||||
pagersArraySize = count;
|
||||
knownStationsLoaded = true;
|
||||
}
|
||||
|
||||
PagerDataGetter PagerGetter(size_t index) {
|
||||
return [this, index]() { return &pagers[index]; };
|
||||
}
|
||||
|
||||
String* GetName(StoredPagerData* pager) {
|
||||
uint32_t stationId = buildKnownStationWithoutName(pager).toInt();
|
||||
for(size_t i = 0; i < knownStationsSize; i++) {
|
||||
if(knownStations[i].toInt() == stationId) {
|
||||
return knownStations[i].name;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool IsKnown(StoredPagerData* pager) {
|
||||
uint32_t stationId = buildKnownStationWithoutName(pager).toInt();
|
||||
for(size_t i = 0; i < knownStationsSize; i++) {
|
||||
if(knownStations[i].toInt() == stationId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ReceivedPagerData* Receive(SubGhzReceivedData* data) {
|
||||
PagerProtocol* protocol = GetProtocolByName(data->GetProtocolName());
|
||||
if(protocol == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int index = -1;
|
||||
uint32_t dataHash = data->GetHash();
|
||||
|
||||
for(size_t i = 0; i < nextPagerIndex; i++) {
|
||||
if(pagers[i].data == dataHash && pagers[i].protocol == protocol->id) {
|
||||
if(pagers[i].repeats < MAX_REPEATS) {
|
||||
pagers[i].repeats++;
|
||||
} else {
|
||||
return NULL; // no need to modify element any more
|
||||
}
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool isNew = index < 0;
|
||||
if(isNew) {
|
||||
if(data->GetFrequency() != lastFrequency) {
|
||||
lastFrequencyIndex = FrequencyManager::GetInstance()->GetFrequencyIndex(data->GetFrequency());
|
||||
lastFrequency = data->GetFrequency();
|
||||
}
|
||||
|
||||
StoredPagerData storedData = StoredPagerData();
|
||||
storedData.data = dataHash;
|
||||
storedData.protocol = protocol->id;
|
||||
storedData.repeats = 1;
|
||||
storedData.te = data->GetTE();
|
||||
storedData.frequency = lastFrequencyIndex;
|
||||
storedData.decoder = getDecoder(&storedData)->id;
|
||||
storedData.edited = false;
|
||||
|
||||
if(config->SavedStrategy == HIDE && IsKnown(&storedData)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if(config->AutosaveFoundSignals) {
|
||||
AppFileSysytem().AutoSave(&storedData, decoders[storedData.decoder], protocol, lastFrequency);
|
||||
}
|
||||
|
||||
index = nextPagerIndex;
|
||||
addPager(storedData);
|
||||
}
|
||||
|
||||
return new ReceivedPagerData(PagerGetter(index), index, isNew);
|
||||
}
|
||||
|
||||
~PagerReceiver() {
|
||||
for(PagerProtocol* protocol : protocols) {
|
||||
delete protocol;
|
||||
}
|
||||
|
||||
for(PagerDecoder* decoder : decoders) {
|
||||
delete decoder;
|
||||
}
|
||||
|
||||
delete[] pagers;
|
||||
unloadKnownStations();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
#pragma once
|
||||
|
||||
#include "ProtocolAndDecoderProvider.hpp"
|
||||
#include "lib/String.hpp"
|
||||
#include "lib/file/FileManager.hpp"
|
||||
#include "lib/file/FlipperFile.hpp"
|
||||
|
||||
#include "data/StoredPagerData.hpp"
|
||||
#include "lib/hardware/subghz/FrequencyManager.hpp"
|
||||
#include "protocol/PagerProtocol.hpp"
|
||||
#include "decoder/PagerDecoder.hpp"
|
||||
|
||||
#define KEY_PAGER_STATION_NAME "StationName"
|
||||
#define KEY_PAGER_FREQUENCY "Frequency"
|
||||
#define KEY_PAGER_PROTOCOL "Protocol"
|
||||
#define KEY_PAGER_DECODER "Decoder"
|
||||
#define KEY_PAGER_DATA "Data"
|
||||
#define KEY_PAGER_TE "TE"
|
||||
|
||||
#define NAME_MIN_LENGTH 2
|
||||
#define NAME_MAX_LENGTH 20
|
||||
|
||||
class PagerSerializer {
|
||||
private:
|
||||
public:
|
||||
String* GetFilename(StoredPagerData* pager) {
|
||||
return new String("%06X.fff", pager->data);
|
||||
}
|
||||
|
||||
void SavePagerData(
|
||||
FileManager* fileManager,
|
||||
const char* dir,
|
||||
const char* stationName,
|
||||
StoredPagerData* pager,
|
||||
PagerDecoder* decoder,
|
||||
PagerProtocol* protocol,
|
||||
uint32_t frequency
|
||||
) {
|
||||
String* fileName = GetFilename(pager);
|
||||
FlipperFile* stationFile = fileManager->OpenWrite(dir, fileName->cstr());
|
||||
|
||||
stationFile->WriteString(KEY_PAGER_STATION_NAME, stationName);
|
||||
stationFile->WriteUInt32(KEY_PAGER_FREQUENCY, frequency);
|
||||
stationFile->WriteString(KEY_PAGER_PROTOCOL, protocol->GetSystemName());
|
||||
stationFile->WriteString(KEY_PAGER_DECODER, decoder->GetShortName());
|
||||
stationFile->WriteUInt32(KEY_PAGER_TE, pager->te);
|
||||
stationFile->WriteHex(KEY_PAGER_DATA, pager->data);
|
||||
|
||||
delete stationFile;
|
||||
delete fileName;
|
||||
}
|
||||
|
||||
String* LoadOnlyStationName(FileManager* fileManager, const char* dir, StoredPagerData* pager) {
|
||||
String* filename = GetFilename(pager);
|
||||
FlipperFile* stationFile = fileManager->OpenRead(dir, filename->cstr());
|
||||
delete filename;
|
||||
|
||||
String* stationName = NULL;
|
||||
if(stationFile != NULL) {
|
||||
stationName = new String();
|
||||
stationFile->ReadString(KEY_PAGER_STATION_NAME, stationName);
|
||||
delete stationFile;
|
||||
}
|
||||
return stationName;
|
||||
}
|
||||
|
||||
StoredPagerData LoadPagerData(
|
||||
FileManager* fileManager,
|
||||
String* stationName,
|
||||
const char* dir,
|
||||
const char* fileName,
|
||||
ProtocolAndDecoderProvider* pdProvider
|
||||
) {
|
||||
FlipperFile* stationFile = fileManager->OpenRead(dir, fileName);
|
||||
|
||||
uint32_t te = 0;
|
||||
uint64_t hex = 0;
|
||||
uint32_t frequency = 0;
|
||||
String protocolName;
|
||||
String decoderName;
|
||||
|
||||
stationFile->ReadString(KEY_PAGER_STATION_NAME, stationName);
|
||||
stationFile->ReadUInt32(KEY_PAGER_FREQUENCY, &frequency);
|
||||
stationFile->ReadString(KEY_PAGER_PROTOCOL, &protocolName);
|
||||
stationFile->ReadString(KEY_PAGER_DECODER, &decoderName);
|
||||
stationFile->ReadUInt32(KEY_PAGER_TE, &te);
|
||||
stationFile->ReadHex(KEY_PAGER_DATA, &hex);
|
||||
|
||||
delete stationFile;
|
||||
|
||||
StoredPagerData pager;
|
||||
pager.data = hex;
|
||||
pager.te = te;
|
||||
pager.edited = false;
|
||||
pager.frequency = FrequencyManager::GetInstance()->GetFrequencyIndex(frequency);
|
||||
pager.protocol = pdProvider->GetProtocolByName(protocolName.cstr())->id;
|
||||
pager.decoder = pdProvider->GetDecoderByName(decoderName.cstr())->id;
|
||||
return pager;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "protocol/PagerProtocol.hpp"
|
||||
#include "decoder/PagerDecoder.hpp"
|
||||
|
||||
class ProtocolAndDecoderProvider {
|
||||
public:
|
||||
virtual PagerProtocol* GetProtocolByName(const char* name) = 0;
|
||||
virtual PagerDecoder* GetDecoderByName(const char* name) = 0;
|
||||
virtual ~ProtocolAndDecoderProvider() {
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
enum SavedStationStrategy {
|
||||
IGNORE, // don't check if station is saved, show as unknown
|
||||
SHOW_NAME, // show station name instead of hex and station number
|
||||
HIDE, // hide all station signals from the search
|
||||
|
||||
SavedStationStrategyValuesCount,
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "lib/String.hpp"
|
||||
#include <cstdint>
|
||||
|
||||
struct KnownStationData {
|
||||
uint8_t frequency : 8;
|
||||
uint8_t protocol : 2;
|
||||
uint8_t decoder : 4;
|
||||
uint8_t unused : 2; // align
|
||||
uint16_t station : 16;
|
||||
String* name;
|
||||
|
||||
public:
|
||||
uint32_t toInt();
|
||||
};
|
||||
|
||||
union KnownStationDataUnion {
|
||||
KnownStationData stationData;
|
||||
uint32_t intValue;
|
||||
};
|
||||
|
||||
uint32_t KnownStationData::toInt() {
|
||||
KnownStationDataUnion u;
|
||||
u.stationData = *this;
|
||||
u.stationData.unused = 0;
|
||||
return u.intValue;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "StoredPagerData.hpp"
|
||||
#include "lib/String.hpp"
|
||||
|
||||
struct NamedPagerData {
|
||||
StoredPagerData storedData;
|
||||
String* name;
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include "StoredPagerData.hpp"
|
||||
|
||||
class ReceivedPagerData {
|
||||
private:
|
||||
PagerDataGetter getStoredData;
|
||||
uint32_t index;
|
||||
bool isNew;
|
||||
|
||||
public:
|
||||
ReceivedPagerData(PagerDataGetter storedDataGetter, uint32_t index, bool isNew) {
|
||||
this->getStoredData = storedDataGetter;
|
||||
this->index = index;
|
||||
this->isNew = isNew;
|
||||
}
|
||||
|
||||
bool IsNew() {
|
||||
return isNew;
|
||||
}
|
||||
|
||||
uint32_t GetIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
StoredPagerData* GetData() {
|
||||
return getStoredData();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
using namespace std;
|
||||
|
||||
struct StoredPagerData {
|
||||
// first 4-byte
|
||||
uint32_t data : 25;
|
||||
uint8_t repeats : 7;
|
||||
|
||||
// second 4-byte
|
||||
// byte 1
|
||||
uint8_t frequency : 8;
|
||||
|
||||
// byte 2
|
||||
uint8_t decoder : 4; // max 16 decoders, enough for now
|
||||
uint8_t protocol : 2; // max 4 protocols (only )
|
||||
bool edited : 1;
|
||||
uint8_t : 0;
|
||||
|
||||
// byte 3-4
|
||||
uint16_t te : 11; // 2048 values should be enough
|
||||
|
||||
// 5 bits still unused
|
||||
};
|
||||
|
||||
// StoredPagerData is short-living because it's stored as array of stack allocated objects in PagerReceiver class.
|
||||
// If array size changes, it reallocates all the objects on the new addresses in memory. We could store pointers in array instead of stack objects,
|
||||
// but it would take more memory which is not acceptable (sizeof(StoredPagerData) + sizeof(StoredPagerData*)) vs sizeof(StoredPagerData).
|
||||
// That's why we pass the getter instead of object itself, to make sure we always have the right pointer to the StoredPagerData structure.
|
||||
typedef function<StoredPagerData*()> PagerDataGetter;
|
||||
@@ -0,0 +1,81 @@
|
||||
#pragma once
|
||||
|
||||
#include "PagerDecoder.hpp"
|
||||
|
||||
#define T111_ACTION_RING 0
|
||||
|
||||
// Retekess T111 / L8R
|
||||
// L8R — (L)ast (8) bits (R)eversed order (for pager number)
|
||||
// seems to be Retekess T111 encoding, but cannot check it due to lack of information
|
||||
// So I decided to keep it's name as L8R
|
||||
class L8RDecoder : public PagerDecoder {
|
||||
private:
|
||||
const uint32_t stationMask = 0b111111111111100000000000; // leading 13 bits (of 24) are station (any maybe more)
|
||||
const uint32_t actionMask = 0b11100000000; // next 3 bits are action (possibly, just my guess, may be they are also station)
|
||||
const uint32_t pagerMask = 0b11111111; // and the last 8 bits seem to be pager number
|
||||
|
||||
const uint8_t stationBitCount = 13;
|
||||
const uint8_t stationOffset = 11;
|
||||
|
||||
const uint8_t actionBitCount = 3;
|
||||
const uint8_t actionOffset = 8;
|
||||
|
||||
const uint8_t pagerBitCount = 8;
|
||||
|
||||
public:
|
||||
const char* GetShortName() {
|
||||
return "L8R";
|
||||
}
|
||||
|
||||
uint16_t GetStation(uint32_t data) {
|
||||
uint32_t stationReversed = (data & stationMask) >> stationOffset;
|
||||
return reverseBits(stationReversed, stationBitCount);
|
||||
}
|
||||
|
||||
uint16_t GetPager(uint32_t data) {
|
||||
uint32_t pagerReversed = data & pagerMask;
|
||||
return reverseBits(pagerReversed, pagerBitCount);
|
||||
}
|
||||
|
||||
uint8_t GetActionValue(uint32_t data) {
|
||||
uint32_t actionReversed = (data & actionMask) >> actionOffset;
|
||||
return reverseBits(actionReversed, actionBitCount);
|
||||
}
|
||||
|
||||
PagerAction GetAction(uint32_t data) {
|
||||
switch(GetActionValue(data)) {
|
||||
case T111_ACTION_RING:
|
||||
return RING;
|
||||
|
||||
default:
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t SetPager(uint32_t data, uint16_t pagerNum) {
|
||||
return (data & ~pagerMask) | reverseBits(pagerNum, pagerBitCount);
|
||||
}
|
||||
|
||||
uint32_t SetActionValue(uint32_t data, uint8_t actionValue) {
|
||||
uint32_t actionCleared = data & ~actionMask;
|
||||
return actionCleared | (reverseBits(actionValue, actionBitCount) << actionOffset);
|
||||
}
|
||||
|
||||
uint32_t SetAction(uint32_t data, PagerAction action) {
|
||||
switch(action) {
|
||||
case RING:
|
||||
return SetActionValue(data, T111_ACTION_RING);
|
||||
|
||||
default:
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
bool IsSupported(PagerAction) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t GetActionsCount() {
|
||||
return 8;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/core_defines.h"
|
||||
|
||||
#include "PagerDecoder.hpp"
|
||||
|
||||
// iBells ZJ-68 / L8S
|
||||
// L8S — (L)ast (8) bits (S)traight order (non-reversed) (for pager number)
|
||||
class L8SDecoder : public PagerDecoder {
|
||||
private:
|
||||
const uint32_t stationMask = 0b111111111111100000000000; // leading 13 bits (of 24) are station (let it be)
|
||||
const uint32_t actionMask = 0b11100000000; // next 3 bits are action (possibly, just my guess, may be they are also station)
|
||||
const uint32_t pagerMask = 0b11111111; // and the last 8 bits should be enough for pager number
|
||||
|
||||
const uint8_t stationOffset = 11;
|
||||
const uint8_t actionOffset = 8;
|
||||
|
||||
public:
|
||||
const char* GetShortName() {
|
||||
return "L8S";
|
||||
}
|
||||
|
||||
uint16_t GetStation(uint32_t data) {
|
||||
return (data & stationMask) >> stationOffset;
|
||||
}
|
||||
|
||||
uint16_t GetPager(uint32_t data) {
|
||||
return data & pagerMask;
|
||||
}
|
||||
|
||||
uint8_t GetActionValue(uint32_t data) {
|
||||
return (data & actionMask) >> actionOffset;
|
||||
}
|
||||
|
||||
PagerAction GetAction(uint32_t data) {
|
||||
UNUSED(data);
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
uint32_t SetPager(uint32_t data, uint16_t pagerNum) {
|
||||
return (data & ~pagerMask) | pagerNum;
|
||||
}
|
||||
|
||||
uint32_t SetActionValue(uint32_t data, uint8_t actionValue) {
|
||||
uint32_t actionCleared = data & ~actionMask;
|
||||
return actionCleared | (actionValue << actionOffset);
|
||||
}
|
||||
|
||||
uint32_t SetAction(uint32_t data, PagerAction action) {
|
||||
UNUSED(action);
|
||||
return data;
|
||||
}
|
||||
|
||||
bool IsSupported(PagerAction) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t GetActionsCount() {
|
||||
return 8;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include "../PagerAction.hpp"
|
||||
|
||||
using namespace std;
|
||||
|
||||
class PagerDecoder {
|
||||
public:
|
||||
uint8_t id;
|
||||
virtual const char* GetShortName() = 0;
|
||||
|
||||
virtual uint16_t GetStation(uint32_t data) = 0;
|
||||
|
||||
virtual uint16_t GetPager(uint32_t data) = 0;
|
||||
virtual uint32_t SetPager(uint32_t data, uint16_t pagerNum) = 0;
|
||||
|
||||
virtual uint8_t GetActionValue(uint32_t data) = 0;
|
||||
virtual PagerAction GetAction(uint32_t data) = 0;
|
||||
virtual uint32_t SetAction(uint32_t data, PagerAction action) = 0;
|
||||
virtual uint32_t SetActionValue(uint32_t data, uint8_t action) = 0;
|
||||
virtual bool IsSupported(PagerAction action) = 0;
|
||||
virtual uint8_t GetActionsCount() = 0;
|
||||
|
||||
uint8_t GetSupportedActionsCount() {
|
||||
uint8_t count = 0;
|
||||
for(uint8_t i = 0; i < PagerActionCount; i++) {
|
||||
if(IsSupported(static_cast<enum PagerAction>(i))) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
virtual ~PagerDecoder() {
|
||||
}
|
||||
|
||||
protected:
|
||||
uint32_t reverseBits(uint32_t number, int count) {
|
||||
uint32_t rev = 0;
|
||||
|
||||
while(count-- > 0) {
|
||||
rev <<= 1;
|
||||
if((number & 1) == 1) {
|
||||
rev ^= 1;
|
||||
}
|
||||
number >>= 1;
|
||||
}
|
||||
|
||||
return rev;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
|
||||
#include "PagerDecoder.hpp"
|
||||
|
||||
#define TD157_ACTION_RING 0b0010
|
||||
#define TD157_ACTION_TURN_OFF_ALL 0b1111
|
||||
#define TD157_PAGER_TURN_OFF_ALL 999
|
||||
|
||||
// Retekess TD157
|
||||
class Td157Decoder : public PagerDecoder {
|
||||
private:
|
||||
const uint32_t stationMask = 0b111111111100000000000000; // leading 10 bits (of 24) are station
|
||||
const uint32_t pagerMask = 0b11111111110000; // next 10 bits are pager
|
||||
const uint32_t actionMask = 0b1111; // and the last 4 bits is action
|
||||
|
||||
const uint8_t stationOffset = 14;
|
||||
const uint8_t pagerOffset = 4;
|
||||
|
||||
public:
|
||||
const char* GetShortName() {
|
||||
return "TD157";
|
||||
}
|
||||
|
||||
uint16_t GetStation(uint32_t data) {
|
||||
uint32_t station = (data & stationMask) >> stationOffset;
|
||||
return (uint16_t)station;
|
||||
}
|
||||
|
||||
uint16_t GetPager(uint32_t data) {
|
||||
uint32_t pager = (data & pagerMask) >> pagerOffset;
|
||||
return (uint16_t)pager;
|
||||
}
|
||||
|
||||
uint32_t SetPager(uint32_t data, uint16_t pagerNum) {
|
||||
uint32_t pagerClearedData = data & ~pagerMask;
|
||||
return pagerClearedData | (pagerNum << pagerOffset);
|
||||
}
|
||||
|
||||
uint8_t GetActionValue(uint32_t data) {
|
||||
return data & actionMask;
|
||||
}
|
||||
|
||||
PagerAction GetAction(uint32_t data) {
|
||||
switch(GetActionValue(data)) {
|
||||
case TD157_ACTION_RING:
|
||||
return RING;
|
||||
case TD157_ACTION_TURN_OFF_ALL:
|
||||
if(GetPager(data) == TD157_PAGER_TURN_OFF_ALL) {
|
||||
return TURN_OFF_ALL;
|
||||
}
|
||||
return UNKNOWN;
|
||||
default:
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t SetAction(uint32_t data, PagerAction action) {
|
||||
switch(action) {
|
||||
case RING:
|
||||
return SetActionValue(data, TD157_ACTION_RING);
|
||||
case TURN_OFF_ALL:
|
||||
return SetActionValue(SetPager(data, TD157_PAGER_TURN_OFF_ALL), TD157_ACTION_TURN_OFF_ALL);
|
||||
default:
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
virtual uint32_t SetActionValue(uint32_t data, uint8_t action) {
|
||||
return (data & ~actionMask) | action;
|
||||
}
|
||||
|
||||
bool IsSupported(PagerAction action) {
|
||||
switch(action) {
|
||||
case RING:
|
||||
case TURN_OFF_ALL:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t GetActionsCount() {
|
||||
return actionMask + 1;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
#pragma once
|
||||
|
||||
#include "PagerDecoder.hpp"
|
||||
|
||||
#define TD165_ACTION_RING 0
|
||||
#define TD165_ACTION_MUTE 1
|
||||
#define TD165_PAGER_TURN_OFF_ALL 1005
|
||||
|
||||
// Retekess TD165/T119
|
||||
class Td165Decoder : public PagerDecoder {
|
||||
private:
|
||||
const uint32_t stationMask = 0b111111111111100000000000; // leading 13 bits (of 24) are station
|
||||
const uint32_t pagerMask = 0b11111111110; // next 10 bits are pager
|
||||
const uint32_t actionMask = 0b1; // and the last 1 bit is action
|
||||
|
||||
const uint8_t stationBitCount = 13;
|
||||
const uint8_t stationOffset = 11;
|
||||
|
||||
const uint8_t pagerBitCount = 10;
|
||||
const uint8_t pagerOffset = 1;
|
||||
|
||||
public:
|
||||
const char* GetShortName() {
|
||||
return "TD165";
|
||||
}
|
||||
|
||||
uint16_t GetStation(uint32_t data) {
|
||||
uint32_t stationReversed = (data & stationMask) >> stationOffset;
|
||||
return reverseBits(stationReversed, stationBitCount);
|
||||
}
|
||||
|
||||
uint16_t GetPager(uint32_t data) {
|
||||
uint32_t pagerReversed = (data & pagerMask) >> pagerOffset;
|
||||
return reverseBits(pagerReversed, pagerBitCount);
|
||||
}
|
||||
|
||||
uint8_t GetActionValue(uint32_t data) {
|
||||
return data & actionMask;
|
||||
}
|
||||
|
||||
PagerAction GetAction(uint32_t data) {
|
||||
switch(GetActionValue(data)) {
|
||||
case TD165_ACTION_RING:
|
||||
if(GetPager(data) == TD165_PAGER_TURN_OFF_ALL) {
|
||||
return TURN_OFF_ALL;
|
||||
}
|
||||
return RING;
|
||||
|
||||
case TD165_ACTION_MUTE:
|
||||
return MUTE;
|
||||
|
||||
default:
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t SetPager(uint32_t data, uint16_t pagerNum) {
|
||||
uint32_t pagerCleared = data & ~pagerMask;
|
||||
return pagerCleared | (reverseBits(pagerNum, pagerBitCount) << pagerOffset);
|
||||
}
|
||||
|
||||
uint32_t SetActionValue(uint32_t data, uint8_t actionValue) {
|
||||
return (data & ~actionMask) | actionValue;
|
||||
}
|
||||
|
||||
uint32_t SetAction(uint32_t data, PagerAction action) {
|
||||
switch(action) {
|
||||
case RING:
|
||||
return SetActionValue(data, TD165_ACTION_RING);
|
||||
|
||||
case MUTE:
|
||||
return SetActionValue(data, TD165_ACTION_MUTE);
|
||||
|
||||
case TURN_OFF_ALL:
|
||||
return SetActionValue(SetPager(data, TD165_PAGER_TURN_OFF_ALL), TD165_ACTION_RING);
|
||||
|
||||
default:
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
bool IsSupported(PagerAction action) {
|
||||
switch(action) {
|
||||
case RING:
|
||||
case MUTE:
|
||||
case TURN_OFF_ALL:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t GetActionsCount() {
|
||||
return actionMask + 1;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
|
||||
#include "PagerDecoder.hpp"
|
||||
|
||||
#define TD174_ACTION_RING 0
|
||||
#define TD174_ACTION_DESYNC 3
|
||||
#define TD174_PAGER_DESYNC 237
|
||||
|
||||
// Retekess TD174
|
||||
class Td174Decoder : public PagerDecoder {
|
||||
private:
|
||||
const uint32_t stationMask = 0b111111111111100000000000; // leading 13 bits (of 24) are station
|
||||
const uint32_t actionMask = 0b11000000000; // next 2 bits are action
|
||||
const uint32_t pagerMask = 0b111111111; // and the last 9 bits is pager
|
||||
|
||||
const uint8_t stationBitCount = 13;
|
||||
const uint8_t stationOffset = 11;
|
||||
|
||||
const uint8_t actionBitCount = 2;
|
||||
const uint8_t actionOffset = 9;
|
||||
|
||||
const uint8_t pagerBitCount = 9;
|
||||
|
||||
public:
|
||||
const char* GetShortName() {
|
||||
return "TD174";
|
||||
}
|
||||
|
||||
uint16_t GetStation(uint32_t data) {
|
||||
uint32_t stationReversed = (data & stationMask) >> stationOffset;
|
||||
return reverseBits(stationReversed, stationBitCount);
|
||||
}
|
||||
|
||||
uint16_t GetPager(uint32_t data) {
|
||||
uint32_t pagerReversed = data & pagerMask;
|
||||
return reverseBits(pagerReversed, pagerBitCount);
|
||||
}
|
||||
|
||||
uint8_t GetActionValue(uint32_t data) {
|
||||
uint32_t actionReversed = (data & actionMask) >> actionOffset;
|
||||
return reverseBits(actionReversed, actionBitCount);
|
||||
}
|
||||
|
||||
PagerAction GetAction(uint32_t data) {
|
||||
switch(GetActionValue(data)) {
|
||||
case TD174_ACTION_RING:
|
||||
return RING;
|
||||
|
||||
case TD174_ACTION_DESYNC:
|
||||
if(GetPager(data) == TD174_PAGER_DESYNC) {
|
||||
return DESYNC;
|
||||
}
|
||||
return UNKNOWN;
|
||||
|
||||
default:
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t SetPager(uint32_t data, uint16_t pagerNum) {
|
||||
return (data & ~pagerMask) | reverseBits(pagerNum, pagerBitCount);
|
||||
}
|
||||
|
||||
uint32_t SetActionValue(uint32_t data, uint8_t actionValue) {
|
||||
uint32_t actionCleared = data & ~actionMask;
|
||||
return actionCleared | (reverseBits(actionValue, actionBitCount) << actionOffset);
|
||||
}
|
||||
|
||||
uint32_t SetAction(uint32_t data, PagerAction action) {
|
||||
switch(action) {
|
||||
case RING:
|
||||
return SetActionValue(data, TD174_ACTION_RING);
|
||||
|
||||
case DESYNC:
|
||||
return SetActionValue(SetPager(data, TD174_PAGER_DESYNC), TD174_ACTION_DESYNC);
|
||||
|
||||
default:
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
bool IsSupported(PagerAction action) {
|
||||
switch(action) {
|
||||
case RING:
|
||||
case DESYNC:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t GetActionsCount() {
|
||||
return 4;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "lib/hardware/subghz/SubGhzPayload.hpp"
|
||||
|
||||
class PagerProtocol {
|
||||
public:
|
||||
uint8_t id;
|
||||
virtual const char* GetSystemName() = 0;
|
||||
virtual int GetFallbackTE() = 0;
|
||||
virtual int GetMaxTE() = 0;
|
||||
virtual SubGhzPayload* CreatePayload(uint64_t data, uint32_t te, uint32_t repeats) = 0;
|
||||
virtual ~PagerProtocol() {
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <stream/stream.h>
|
||||
|
||||
#include "PagerProtocol.hpp"
|
||||
|
||||
class PrincetonProtocol : public PagerProtocol {
|
||||
public:
|
||||
const char* GetSystemName() {
|
||||
return "Princeton";
|
||||
}
|
||||
|
||||
int GetFallbackTE() {
|
||||
return 212;
|
||||
}
|
||||
|
||||
int GetMaxTE() {
|
||||
return 1200;
|
||||
}
|
||||
|
||||
SubGhzPayload* CreatePayload(uint64_t data, uint32_t te, uint32_t repeats) {
|
||||
SubGhzPayload* payload = new SubGhzPayload(GetSystemName());
|
||||
payload->SetBits(24);
|
||||
payload->SetKey(data);
|
||||
payload->SetTE(te);
|
||||
// somewhy repeats are always 10 even if we set it, so use here "software repeats" instead
|
||||
payload->SetSoftwareRepeats(ceil(repeats / 10.0));
|
||||
payload->SetRepeat(10); // just in case they'll fix it
|
||||
|
||||
return payload;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "PagerProtocol.hpp"
|
||||
|
||||
class Smc5326Protocol : public PagerProtocol {
|
||||
const char* GetSystemName() {
|
||||
return "SMC5326";
|
||||
}
|
||||
|
||||
int GetFallbackTE() {
|
||||
return 326;
|
||||
}
|
||||
|
||||
int GetMaxTE() {
|
||||
return 900;
|
||||
}
|
||||
|
||||
SubGhzPayload* CreatePayload(uint64_t data, uint32_t te, uint32_t repeats) {
|
||||
SubGhzPayload* payload = new SubGhzPayload(GetSystemName());
|
||||
payload->SetBits(25);
|
||||
payload->SetKey(data);
|
||||
payload->SetTE(te);
|
||||
payload->SetRepeat(repeats);
|
||||
return payload;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "lib/String.hpp"
|
||||
#include "lib/ui/view/UiView.hpp"
|
||||
#include "lib/ui/view/ProgressbarPopupUiView.hpp"
|
||||
|
||||
class BatchTransmissionScreen {
|
||||
private:
|
||||
ProgressbarPopupUiView* popup;
|
||||
String statusStr;
|
||||
|
||||
public:
|
||||
BatchTransmissionScreen(int pagersTotal) {
|
||||
popup = new ProgressbarPopupUiView("Transmitting...");
|
||||
SetProgress(0, pagersTotal);
|
||||
popup->SetOnDestroyHandler(HANDLER(&BatchTransmissionScreen::destroy));
|
||||
}
|
||||
|
||||
void SetProgress(int pagerNum, int pagersTotal) {
|
||||
float progressValue = (float)pagerNum / pagersTotal;
|
||||
popup->SetProgress(statusStr.format("Pager %d / %d", pagerNum, pagersTotal), progressValue);
|
||||
}
|
||||
|
||||
private:
|
||||
void destroy() {
|
||||
delete this;
|
||||
}
|
||||
|
||||
public:
|
||||
UiView* GetView() {
|
||||
return popup;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,313 @@
|
||||
#pragma once
|
||||
|
||||
#include "SelectCategoryScreen.hpp"
|
||||
#include "lib/HandlerContext.hpp"
|
||||
#include "lib/String.hpp"
|
||||
#include "app/pager/PagerReceiver.hpp"
|
||||
#include "lib/hardware/subghz/SubGhzModule.hpp"
|
||||
#include "lib/ui/view/UiView.hpp"
|
||||
#include "lib/ui/view/VariableItemListUiView.hpp"
|
||||
#include "lib/ui/view/TextInputUiView.hpp"
|
||||
#include "lib/ui/view/DialogUiView.hpp"
|
||||
#include "lib/FlipperDolphin.hpp"
|
||||
#include "lib/ui/UiManager.hpp"
|
||||
#include "app/AppFileSystem.hpp"
|
||||
#include "app/pager/PagerSerializer.hpp"
|
||||
|
||||
#define TE_DIV 10
|
||||
|
||||
class EditPagerScreen {
|
||||
private:
|
||||
AppConfig* config;
|
||||
SubGhzModule* subghz;
|
||||
PagerReceiver* receiver;
|
||||
PagerDataGetter getPager;
|
||||
VariableItemListUiView* varItemList;
|
||||
|
||||
UiVariableItem* encodingItem = NULL;
|
||||
UiVariableItem* stationItem = NULL;
|
||||
UiVariableItem* pagerItem = NULL;
|
||||
UiVariableItem* actionItem = NULL;
|
||||
UiVariableItem* hexItem = NULL;
|
||||
UiVariableItem* protocolItem = NULL;
|
||||
UiVariableItem* frequencyItem = NULL;
|
||||
UiVariableItem* teItem = NULL;
|
||||
UiVariableItem* repeatsItem = NULL;
|
||||
|
||||
UiVariableItem* saveAsItem = NULL;
|
||||
UiVariableItem* deleteItem = NULL;
|
||||
|
||||
String stationStr;
|
||||
String pagerStr;
|
||||
String actionStr;
|
||||
String hexStr;
|
||||
String repeatsStr;
|
||||
String frequencyStr;
|
||||
String teStr;
|
||||
int32_t saveAsItemIndex = -1;
|
||||
int32_t deleteItemIndex = -1;
|
||||
|
||||
bool isFromFile;
|
||||
const char* saveAsName = NULL;
|
||||
|
||||
public:
|
||||
EditPagerScreen(
|
||||
AppConfig* config,
|
||||
SubGhzModule* subghz,
|
||||
PagerReceiver* receiver,
|
||||
PagerDataGetter pagerGetter,
|
||||
bool isFromFile
|
||||
) {
|
||||
this->config = config;
|
||||
this->subghz = subghz;
|
||||
this->receiver = receiver;
|
||||
this->getPager = pagerGetter;
|
||||
this->isFromFile = isFromFile;
|
||||
|
||||
StoredPagerData* pager = getPager();
|
||||
PagerDecoder* decoder = receiver->decoders[pager->decoder];
|
||||
PagerProtocol* protocol = receiver->protocols[pager->protocol];
|
||||
uint32_t frequency = FrequencyManager::GetInstance()->GetFrequency(pager->frequency);
|
||||
|
||||
varItemList = new VariableItemListUiView();
|
||||
varItemList->SetOnDestroyHandler(HANDLER(&EditPagerScreen::destroy));
|
||||
varItemList->SetEnterPressHandler(HANDLER_1ARG(&EditPagerScreen::enterPressed));
|
||||
|
||||
varItemList->AddItem(
|
||||
encodingItem = new UiVariableItem(
|
||||
"Encoding", pager->decoder, receiver->decodersCount, HANDLER_1ARG(&EditPagerScreen::encodingValueChanged)
|
||||
)
|
||||
);
|
||||
|
||||
varItemList->AddItem(stationItem = new UiVariableItem("Station", HANDLER_1ARG(&EditPagerScreen::stationValueChanged)));
|
||||
varItemList->AddItem(pagerItem = new UiVariableItem("Pager", HANDLER_1ARG(&EditPagerScreen::pagerValueChanged)));
|
||||
updatePagerIsEditable();
|
||||
|
||||
varItemList->AddItem(
|
||||
actionItem = new UiVariableItem(
|
||||
"Action",
|
||||
decoder->GetActionValue(pager->data),
|
||||
decoder->GetActionsCount(),
|
||||
HANDLER_1ARG(&EditPagerScreen::actionValueChanged)
|
||||
)
|
||||
);
|
||||
|
||||
varItemList->AddItem(hexItem = new UiVariableItem("HEX value", HANDLER_1ARG(&EditPagerScreen::hexValueChanged)));
|
||||
varItemList->AddItem(protocolItem = new UiVariableItem("Protocol", protocol->GetSystemName()));
|
||||
varItemList->AddItem(
|
||||
frequencyItem = new UiVariableItem(
|
||||
"Frequency", frequencyStr.format("%lu.%02lu", frequency / 1000000, (frequency % 1000000) / 10000)
|
||||
)
|
||||
);
|
||||
varItemList->AddItem(
|
||||
teItem = new UiVariableItem(
|
||||
"TE", pager->te / TE_DIV, protocol->GetMaxTE() / TE_DIV, HANDLER_1ARG(&EditPagerScreen::teValueChanged)
|
||||
)
|
||||
);
|
||||
varItemList->AddItem(
|
||||
repeatsItem = new UiVariableItem(
|
||||
"Signal Repeats", repeatsStr.format(pager->repeats == MAX_REPEATS ? "%d+" : "%d", pager->repeats)
|
||||
)
|
||||
);
|
||||
|
||||
if(canSave()) {
|
||||
const char* saveAsItemName = isFromFile ? "Save / Rename" : "Save signal as...";
|
||||
saveAsItemIndex = varItemList->AddItem(saveAsItem = new UiVariableItem(saveAsItemName, ""));
|
||||
}
|
||||
|
||||
if(canDelete()) {
|
||||
deleteItemIndex = varItemList->AddItem(deleteItem = new UiVariableItem("Delete station", ""));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
bool canSave() {
|
||||
return !receiver->IsKnown(getPager()) || this->isFromFile;
|
||||
}
|
||||
|
||||
bool canDelete() {
|
||||
return isFromFile;
|
||||
}
|
||||
|
||||
String* currentStationName() {
|
||||
return receiver->GetName(getPager());
|
||||
}
|
||||
|
||||
void updatePagerIsEditable() {
|
||||
StoredPagerData* pager = getPager();
|
||||
int pagerNum = receiver->decoders[pager->decoder]->GetPager(pager->data);
|
||||
if(pagerNum < UINT8_MAX) {
|
||||
pagerItem->SetSelectedItem(pagerNum, UINT8_MAX);
|
||||
} else {
|
||||
pagerItem->SetSelectedItem(0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
void enterPressed(int32_t index) {
|
||||
if(index == saveAsItemIndex) {
|
||||
saveAs();
|
||||
} else if(index == deleteItemIndex) {
|
||||
DialogUiView* removeConfirmation = new DialogUiView("Really delete?", currentStationName()->cstr());
|
||||
removeConfirmation->AddLeftButton("Nope");
|
||||
removeConfirmation->AddRightButton("Yup");
|
||||
removeConfirmation->SetResultHandler(HANDLER_1ARG(&EditPagerScreen::confirmDelete));
|
||||
|
||||
UiManager::GetInstance()->PushView(removeConfirmation);
|
||||
} else {
|
||||
transmitMessage();
|
||||
}
|
||||
}
|
||||
|
||||
void confirmDelete(DialogExResult result) {
|
||||
if(result == DialogExResultRight) {
|
||||
AppFileSysytem().DeletePager(receiver->GetCurrentUserCategory(), getPager());
|
||||
receiver->ReloadKnownStations();
|
||||
UiManager::GetInstance()->PopView(false);
|
||||
}
|
||||
}
|
||||
|
||||
void transmitMessage() {
|
||||
StoredPagerData* pager = getPager();
|
||||
PagerProtocol* protocol = receiver->protocols[pager->protocol];
|
||||
uint32_t frequency = FrequencyManager::GetInstance()->GetFrequency(pager->frequency);
|
||||
subghz->Transmit(protocol->CreatePayload(pager->data, pager->te, config->SignalRepeats), frequency);
|
||||
|
||||
FlipperDolphin::Deed(DolphinDeedSubGhzSend);
|
||||
}
|
||||
|
||||
void saveAs() {
|
||||
TextInputUiView* nameInputView = new TextInputUiView("Enter station name", NAME_MIN_LENGTH, NAME_MAX_LENGTH);
|
||||
String* name = currentStationName();
|
||||
if(name != NULL) {
|
||||
nameInputView->SetDefaultText(name);
|
||||
}
|
||||
nameInputView->SetResultHandler(HANDLER_1ARG(&EditPagerScreen::saveAsHandler));
|
||||
UiManager::GetInstance()->PushView(nameInputView);
|
||||
}
|
||||
|
||||
void saveAsHandler(const char* name) {
|
||||
saveAsName = name;
|
||||
|
||||
UiManager::GetInstance()->ShowLoading();
|
||||
UiManager::GetInstance()->PushView(
|
||||
(new SelectCategoryScreen(true, User, HANDLER_2ARG(&EditPagerScreen::categorySelected)))->GetView()
|
||||
);
|
||||
}
|
||||
|
||||
void categorySelected(CategoryType, const char* category) {
|
||||
StoredPagerData* pager = getPager();
|
||||
PagerDecoder* decoder = receiver->decoders[pager->decoder];
|
||||
PagerProtocol* protocol = receiver->protocols[pager->protocol];
|
||||
uint32_t frequency = FrequencyManager::GetInstance()->GetFrequency(pager->frequency);
|
||||
|
||||
AppFileSysytem().SaveToUserCategory(category, saveAsName, pager, decoder, protocol, frequency);
|
||||
FlipperDolphin::Deed(DolphinDeedSubGhzSave);
|
||||
|
||||
receiver->ReloadKnownStations();
|
||||
|
||||
for(int i = 0; i < 3; i++) {
|
||||
UiManager::GetInstance()->PopView(false);
|
||||
}
|
||||
}
|
||||
|
||||
const char* encodingValueChanged(uint8_t index) {
|
||||
StoredPagerData* pager = getPager();
|
||||
PagerDecoder* decoder = receiver->decoders[index];
|
||||
pager->decoder = index;
|
||||
|
||||
if(stationItem != NULL) {
|
||||
stationItem->Refresh();
|
||||
}
|
||||
if(pagerItem != NULL) {
|
||||
updatePagerIsEditable();
|
||||
pagerItem->Refresh();
|
||||
}
|
||||
if(actionItem != NULL) {
|
||||
actionItem->SetSelectedItem(decoder->GetActionValue(pager->data), decoder->GetActionsCount());
|
||||
}
|
||||
return receiver->decoders[pager->decoder]->GetShortName();
|
||||
}
|
||||
|
||||
const char* stationValueChanged(uint8_t) {
|
||||
StoredPagerData* pager = getPager();
|
||||
return stationStr.fromInt(receiver->decoders[pager->decoder]->GetStation(pager->data));
|
||||
}
|
||||
|
||||
const char* pagerValueChanged(uint8_t newPager) {
|
||||
StoredPagerData* pager = getPager();
|
||||
PagerDecoder* decoder = receiver->decoders[pager->decoder];
|
||||
if(pagerItem->Editable() && newPager != decoder->GetPager(pager->data)) {
|
||||
pager->data = decoder->SetPager(pager->data, newPager);
|
||||
pager->edited = true;
|
||||
|
||||
if(hexItem != NULL) {
|
||||
hexItem->Refresh();
|
||||
}
|
||||
}
|
||||
return pagerStr.fromInt(decoder->GetPager(pager->data));
|
||||
}
|
||||
|
||||
const char* actionValueChanged(uint8_t value) {
|
||||
StoredPagerData* pager = getPager();
|
||||
PagerDecoder* decoder = receiver->decoders[pager->decoder];
|
||||
if(decoder->GetActionValue(pager->data) != value) {
|
||||
pager->data = decoder->SetActionValue(pager->data, value);
|
||||
pager->edited = true;
|
||||
}
|
||||
|
||||
if(hexItem != NULL) {
|
||||
hexItem->Refresh();
|
||||
}
|
||||
|
||||
uint8_t actionValue = decoder->GetActionValue(pager->data);
|
||||
PagerAction action = decoder->GetAction(pager->data);
|
||||
const char* actionDesc = PagerActions::GetDescription(action);
|
||||
|
||||
return actionStr.format("%d (%s)", actionValue, actionDesc);
|
||||
}
|
||||
|
||||
const char* hexValueChanged(uint8_t) {
|
||||
StoredPagerData* pager = getPager();
|
||||
return hexStr.format("%06X", (unsigned int)pager->data);
|
||||
}
|
||||
|
||||
const char* teValueChanged(uint8_t newTeIndex) {
|
||||
StoredPagerData* pager = getPager();
|
||||
if(newTeIndex != pager->te / TE_DIV) {
|
||||
int teDiff = pager->te % TE_DIV;
|
||||
int newTe = newTeIndex * TE_DIV + teDiff;
|
||||
|
||||
pager->te = newTe;
|
||||
pager->edited = true;
|
||||
}
|
||||
|
||||
return teStr.format("%d", pager->te);
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
delete encodingItem;
|
||||
delete stationItem;
|
||||
delete pagerItem;
|
||||
delete actionItem;
|
||||
delete hexItem;
|
||||
delete frequencyItem;
|
||||
delete teItem;
|
||||
delete protocolItem;
|
||||
delete repeatsItem;
|
||||
|
||||
if(saveAsItem != NULL) {
|
||||
delete saveAsItem;
|
||||
}
|
||||
|
||||
if(deleteItem != NULL) {
|
||||
delete deleteItem;
|
||||
}
|
||||
|
||||
delete this;
|
||||
}
|
||||
|
||||
public:
|
||||
UiView* GetView() {
|
||||
return varItemList;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include "app/AppConfig.hpp"
|
||||
|
||||
#include "lib/ui/view/UiView.hpp"
|
||||
#include "lib/ui/view/SubMenuUiView.hpp"
|
||||
#include "lib/ui/UiManager.hpp"
|
||||
|
||||
#include "app/AppNotifications.hpp"
|
||||
|
||||
#include "SelectCategoryScreen.hpp"
|
||||
#include "ScanStationsScreen.hpp"
|
||||
|
||||
class MainMenuScreen {
|
||||
private:
|
||||
AppConfig* config;
|
||||
SubMenuUiView* menuView;
|
||||
|
||||
public:
|
||||
MainMenuScreen(AppConfig* config) {
|
||||
this->config = config;
|
||||
|
||||
menuView = new SubMenuUiView("Chief Cooker");
|
||||
menuView->AddItem("Scan for station signals", HANDLER_1ARG(&MainMenuScreen::scanStationsMenuPressed));
|
||||
menuView->AddItem("Saved stations database", HANDLER_1ARG(&MainMenuScreen::stationDatabasePressed));
|
||||
menuView->AddItem("About / Manual", HANDLER_1ARG(&MainMenuScreen::aboutPressed));
|
||||
menuView->SetOnDestroyHandler(HANDLER(&MainMenuScreen::destroy));
|
||||
}
|
||||
|
||||
UiView* GetView() {
|
||||
return menuView;
|
||||
}
|
||||
|
||||
private:
|
||||
void scanStationsMenuPressed(uint32_t) {
|
||||
UiManager::GetInstance()->ShowLoading();
|
||||
UiManager::GetInstance()->PushView((new ScanStationsScreen(config))->GetView());
|
||||
}
|
||||
|
||||
void stationDatabasePressed(uint32_t) {
|
||||
SubMenuUiView* savedMenuView = new SubMenuUiView("Select database");
|
||||
savedMenuView->AddItem("Saved by you", HANDLER_1ARG(&MainMenuScreen::savedStationsPressed));
|
||||
savedMenuView->AddItem("Autosaved", HANDLER_1ARG(&MainMenuScreen::autosavedStationsPressed));
|
||||
UiManager::GetInstance()->PushView(savedMenuView);
|
||||
}
|
||||
|
||||
void savedStationsPressed(uint32_t) {
|
||||
UiManager::GetInstance()->ShowLoading();
|
||||
UiManager::GetInstance()->PushView(
|
||||
(new SelectCategoryScreen(false, User, HANDLER_2ARG(&MainMenuScreen::categorySelected)))->GetView()
|
||||
);
|
||||
}
|
||||
|
||||
void autosavedStationsPressed(uint32_t) {
|
||||
UiManager::GetInstance()->ShowLoading();
|
||||
UiManager::GetInstance()->PushView(
|
||||
(new SelectCategoryScreen(false, Autosaved, HANDLER_2ARG(&MainMenuScreen::categorySelected)))->GetView()
|
||||
);
|
||||
}
|
||||
|
||||
void categorySelected(CategoryType categoryType, const char* category) {
|
||||
UiManager::GetInstance()->ShowLoading();
|
||||
UiManager::GetInstance()->PushView((new ScanStationsScreen(config, categoryType, category))->GetView());
|
||||
}
|
||||
|
||||
void aboutPressed(uint32_t index) {
|
||||
UNUSED(index);
|
||||
|
||||
Notification::Play(&NOTIFICATION_PAGER_RECEIVE);
|
||||
menuView->SetItemLabel(index, "Developed by Denr01!");
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
delete this;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,160 @@
|
||||
#pragma once
|
||||
|
||||
#include "lib/String.hpp"
|
||||
#include "app/AppConfig.hpp"
|
||||
#include "app/pager/data/StoredPagerData.hpp"
|
||||
#include "app/pager/decoder/PagerDecoder.hpp"
|
||||
#include "app/pager/protocol/PagerProtocol.hpp"
|
||||
#include "lib/hardware/subghz/SubGhzModule.hpp"
|
||||
#include "lib/ui/view/UiView.hpp"
|
||||
#include "lib/ui/view/SubMenuUiView.hpp"
|
||||
#include "app/screen/BatchTransmissionScreen.hpp"
|
||||
#include "lib/FlipperDolphin.hpp"
|
||||
#include "lib/ui/UiManager.hpp"
|
||||
|
||||
class PagerActionsScreen {
|
||||
private:
|
||||
AppConfig* config;
|
||||
SubMenuUiView* submenu;
|
||||
PagerDecoder* decoder;
|
||||
PagerProtocol* protocol;
|
||||
SubGhzModule* subghz;
|
||||
PagerDataGetter getPager;
|
||||
BatchTransmissionScreen* batchTransmissionScreen;
|
||||
|
||||
String headerStr;
|
||||
String resendToAllStr;
|
||||
String resendToCurrentStr;
|
||||
String** actionsStrings;
|
||||
|
||||
uint32_t currentBatchFrequency;
|
||||
uint32_t currentPager = 0;
|
||||
bool transmittingBatch = false;
|
||||
|
||||
public:
|
||||
PagerActionsScreen(
|
||||
AppConfig* config,
|
||||
PagerDataGetter pagerGetter,
|
||||
PagerDecoder* decoder,
|
||||
PagerProtocol* protocol,
|
||||
SubGhzModule* subghz
|
||||
) {
|
||||
this->config = config;
|
||||
this->getPager = pagerGetter;
|
||||
this->decoder = decoder;
|
||||
this->protocol = protocol;
|
||||
this->subghz = subghz;
|
||||
|
||||
StoredPagerData* pager = getPager();
|
||||
PagerAction currentAction = decoder->GetAction(pager->data);
|
||||
uint8_t actionValue = decoder->GetActionValue(pager->data);
|
||||
uint16_t stationNum = decoder->GetStation(pager->data);
|
||||
uint16_t pagerNum = decoder->GetPager(pager->data);
|
||||
|
||||
submenu = new SubMenuUiView(headerStr.format("Station %d actions", stationNum));
|
||||
submenu->SetOnDestroyHandler(HANDLER(&PagerActionsScreen::destroy));
|
||||
submenu->SetOnReturnToViewHandler(HANDLER(&PagerActionsScreen::onReturn));
|
||||
|
||||
submenu->AddItem(
|
||||
resendToAllStr.format("Resend %d (%s) to ALL", actionValue, PagerActions::GetDescription(currentAction)),
|
||||
HANDLER_1ARG(&PagerActionsScreen::resendToAll)
|
||||
);
|
||||
|
||||
if(currentAction == UNKNOWN) {
|
||||
submenu->AddItem(
|
||||
resendToCurrentStr.format("Resend only to pager %d", pagerNum), HANDLER_1ARG(&PagerActionsScreen::resendSingle)
|
||||
);
|
||||
}
|
||||
|
||||
actionsStrings = new String*[decoder->GetSupportedActionsCount()];
|
||||
for(size_t actionIndex = 0, i = 0; actionIndex < PagerActionCount; actionIndex++) {
|
||||
PagerAction action = static_cast<enum PagerAction>(actionIndex);
|
||||
if(!decoder->IsSupported(action)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(PagerActions::IsPagerActionSpecial(action)) {
|
||||
actionsStrings[i] = new String("Trigger action %s", PagerActions::GetDescription(action));
|
||||
} else {
|
||||
actionsStrings[i] = new String("%s only pager %d", PagerActions::GetDescription(action), pagerNum);
|
||||
}
|
||||
|
||||
submenu->AddItem(actionsStrings[i]->cstr(), [this, action](uint32_t) { sendAction(action); });
|
||||
i++;
|
||||
}
|
||||
|
||||
subghz->SetTransmitCompleteHandler(HANDLER(&PagerActionsScreen::txComplete));
|
||||
}
|
||||
|
||||
private:
|
||||
void resendToAll(uint32_t) {
|
||||
currentPager = 0;
|
||||
transmittingBatch = true;
|
||||
currentBatchFrequency = FrequencyManager::GetInstance()->GetFrequency(getPager()->frequency);
|
||||
|
||||
batchTransmissionScreen = new BatchTransmissionScreen(config->MaxPagerForBatchOrDetection);
|
||||
UiManager::GetInstance()->PushView(batchTransmissionScreen->GetView());
|
||||
sendCurrentPager();
|
||||
|
||||
FlipperDolphin::Deed(DolphinDeedSubGhzSend);
|
||||
}
|
||||
|
||||
void resendSingle(uint32_t) {
|
||||
StoredPagerData* pager = getPager();
|
||||
uint32_t frequency = FrequencyManager::GetInstance()->GetFrequency(pager->frequency);
|
||||
subghz->Transmit(protocol->CreatePayload(pager->data, pager->te, config->SignalRepeats), frequency);
|
||||
|
||||
FlipperDolphin::Deed(DolphinDeedSubGhzSend);
|
||||
}
|
||||
|
||||
void sendAction(PagerAction action) {
|
||||
StoredPagerData* pager = getPager();
|
||||
uint32_t frequency = FrequencyManager::GetInstance()->GetFrequency(pager->frequency);
|
||||
subghz->Transmit(
|
||||
protocol->CreatePayload(decoder->SetAction(pager->data, action), pager->te, config->SignalRepeats), frequency
|
||||
);
|
||||
|
||||
FlipperDolphin::Deed(DolphinDeedSubGhzSend);
|
||||
}
|
||||
|
||||
void sendCurrentPager() {
|
||||
StoredPagerData* pager = getPager();
|
||||
batchTransmissionScreen->SetProgress(currentPager, config->MaxPagerForBatchOrDetection);
|
||||
subghz->Transmit(
|
||||
protocol->CreatePayload(decoder->SetPager(pager->data, currentPager), pager->te, config->SignalRepeats),
|
||||
currentBatchFrequency
|
||||
);
|
||||
}
|
||||
|
||||
void txComplete() {
|
||||
if(transmittingBatch) {
|
||||
if(++currentPager <= config->MaxPagerForBatchOrDetection) {
|
||||
sendCurrentPager();
|
||||
return;
|
||||
} else {
|
||||
transmittingBatch = false;
|
||||
UiManager::GetInstance()->PopView(false);
|
||||
}
|
||||
}
|
||||
|
||||
subghz->DefaultAfterTransmissionHandler();
|
||||
}
|
||||
|
||||
void onReturn() {
|
||||
transmittingBatch = false;
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
subghz->SetTransmitCompleteHandler(NULL);
|
||||
for(size_t i = 0; i < decoder->GetSupportedActionsCount(); i++) {
|
||||
delete actionsStrings[i];
|
||||
}
|
||||
delete[] actionsStrings;
|
||||
delete this;
|
||||
}
|
||||
|
||||
public:
|
||||
UiView* GetView() {
|
||||
return submenu;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,282 @@
|
||||
#pragma once
|
||||
|
||||
#include "SettingsScreen.hpp"
|
||||
#include "lib/hardware/subghz/data/SubGhzReceivedDataStub.hpp"
|
||||
#include "lib/ui/view/ColumnOrientedListUiView.hpp"
|
||||
|
||||
#include "EditPagerScreen.hpp"
|
||||
#include "PagerActionsScreen.hpp"
|
||||
|
||||
#include "lib/hardware/subghz/SubGhzModule.hpp"
|
||||
|
||||
#include "app/AppConfig.hpp"
|
||||
#include "app/AppNotifications.hpp"
|
||||
#include "app/pager/PagerReceiver.hpp"
|
||||
|
||||
static int8_t stationScreenColumnOffsets[]{
|
||||
3, // station name (if known)
|
||||
3, // hex
|
||||
49, // station
|
||||
72, // pager
|
||||
94, // action
|
||||
128 - 8 // repeats / edit flag
|
||||
};
|
||||
static Font stationScreenColumnFonts[]{
|
||||
FontSecondary, // station name (if known)
|
||||
FontBatteryPercent, // hex
|
||||
FontSecondary, // station
|
||||
FontSecondary, // pager
|
||||
FontBatteryPercent, // action
|
||||
FontBatteryPercent, // repeats
|
||||
};
|
||||
|
||||
static Align stationScreenColumnAlignments[]{
|
||||
AlignLeft, // station name (if known)
|
||||
AlignLeft, // hex
|
||||
AlignCenter, // station
|
||||
AlignCenter, // pager
|
||||
AlignCenter, // action
|
||||
AlignRight, // repeats
|
||||
};
|
||||
|
||||
class ScanStationsScreen {
|
||||
private:
|
||||
AppConfig* config;
|
||||
ColumnOrientedListUiView* menuView;
|
||||
PagerReceiver* pagerReceiver;
|
||||
SubGhzModule* subghz;
|
||||
bool receiveMode = false;
|
||||
bool updateUserCategory = true;
|
||||
int scanForMoreButtonIndex = -1;
|
||||
uint32_t fromFilePagersCount = 0;
|
||||
|
||||
public:
|
||||
ScanStationsScreen(AppConfig* config) : ScanStationsScreen(config, true, NotSelected, NULL) {
|
||||
}
|
||||
|
||||
ScanStationsScreen(AppConfig* config, CategoryType categoryType, const char* category) :
|
||||
ScanStationsScreen(config, false, categoryType, category) {
|
||||
}
|
||||
|
||||
ScanStationsScreen(AppConfig* config, bool receiveNew, CategoryType categoryType, const char* category) {
|
||||
this->config = config;
|
||||
|
||||
menuView = new ColumnOrientedListUiView(
|
||||
stationScreenColumnOffsets,
|
||||
sizeof(stationScreenColumnOffsets),
|
||||
HANDLER_3ARG(&ScanStationsScreen::getElementColumnName)
|
||||
);
|
||||
menuView->SetOnDestroyHandler(HANDLER(&ScanStationsScreen::destroy));
|
||||
menuView->SetOnReturnToViewHandler([this]() { this->menuView->Refresh(); });
|
||||
menuView->SetGoBackHandler(HANDLER(&ScanStationsScreen::goBack));
|
||||
|
||||
menuView->SetColumnFonts(stationScreenColumnFonts);
|
||||
menuView->SetColumnAlignments(stationScreenColumnAlignments);
|
||||
|
||||
menuView->SetLeftButton("Conf", HANDLER_1ARG(&ScanStationsScreen::showConfig));
|
||||
|
||||
subghz = new SubGhzModule(config->Frequency);
|
||||
subghz->SetReceiveHandler(HANDLER_1ARG(&ScanStationsScreen::receive));
|
||||
if(receiveNew) {
|
||||
subghz->SetReceiveAfterTransmission(true);
|
||||
subghz->ReceiveAsync();
|
||||
}
|
||||
|
||||
pagerReceiver = new PagerReceiver(config);
|
||||
if(categoryType == User) {
|
||||
pagerReceiver->SetUserCategory(category);
|
||||
updateUserCategory = false;
|
||||
|
||||
if(category != NULL) {
|
||||
menuView->SetRightButton("Delete category", HANDLER_1ARG(&ScanStationsScreen::deleteCategory));
|
||||
}
|
||||
} else {
|
||||
pagerReceiver->ReloadKnownStations();
|
||||
}
|
||||
|
||||
if(receiveNew) {
|
||||
if(subghz->IsExternal()) {
|
||||
menuView->SetNoElementCaption("Receiving via EXT...");
|
||||
} else {
|
||||
menuView->SetNoElementCaption("Receiving...");
|
||||
}
|
||||
} else {
|
||||
menuView->SetNoElementCaption("No stations found!");
|
||||
}
|
||||
|
||||
if(!receiveNew) {
|
||||
pagerReceiver->LoadStationsFromDirectory(categoryType, category, HANDLER_1ARG(&ScanStationsScreen::pagerAdded));
|
||||
|
||||
if(categoryType == User && menuView->GetElementsCount() > 0) {
|
||||
scanForMoreButtonIndex = menuView->GetElementsCount();
|
||||
fromFilePagersCount = menuView->GetElementsCount();
|
||||
menuView->AddElement();
|
||||
}
|
||||
}
|
||||
|
||||
receiveMode = receiveNew;
|
||||
}
|
||||
|
||||
UiView* GetView() {
|
||||
return menuView;
|
||||
}
|
||||
|
||||
private:
|
||||
void receive(SubGhzReceivedData* data) {
|
||||
pagerAdded(pagerReceiver->Receive(data));
|
||||
delete data;
|
||||
}
|
||||
|
||||
void pagerAdded(ReceivedPagerData* pagerData) {
|
||||
if(pagerData != NULL) {
|
||||
if(pagerData->IsNew()) {
|
||||
if(receiveMode) {
|
||||
Notification::Play(&NOTIFICATION_PAGER_RECEIVE);
|
||||
}
|
||||
|
||||
if(pagerData->GetIndex() == 0) { // add buttons after capturing the first transmission
|
||||
menuView->SetCenterButton("Actions", HANDLER_1ARG(&ScanStationsScreen::showActions));
|
||||
menuView->SetRightButton("Edit", HANDLER_1ARG(&ScanStationsScreen::editPagerMessage));
|
||||
}
|
||||
|
||||
if(!receiveMode || scanForMoreButtonIndex == -1) {
|
||||
menuView->AddElement();
|
||||
} else {
|
||||
scanForMoreButtonIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if(menuView->IsOnTop()) {
|
||||
menuView->Refresh();
|
||||
}
|
||||
|
||||
delete pagerData;
|
||||
}
|
||||
}
|
||||
|
||||
void getElementColumnName(int index, int column, String* str) {
|
||||
StoredPagerData* pagerData = pagerReceiver->PagerGetter(index)();
|
||||
PagerDecoder* decoder = pagerReceiver->decoders[pagerData->decoder];
|
||||
String* name = pagerReceiver->GetName(pagerData);
|
||||
|
||||
if(index == scanForMoreButtonIndex) {
|
||||
if(column == 0) {
|
||||
if(!receiveMode) {
|
||||
str->format("> Scan here for more");
|
||||
} else {
|
||||
str->format("Scanning...");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch(column) {
|
||||
case 0: // station name
|
||||
if(name != NULL) {
|
||||
str->format("%s", name->cstr());
|
||||
}
|
||||
break;
|
||||
|
||||
case 1: // hex
|
||||
if(name == NULL) {
|
||||
str->format("%06X", pagerData->data);
|
||||
}
|
||||
break;
|
||||
|
||||
case 2: // station
|
||||
if(name == NULL) {
|
||||
str->format("%d", decoder->GetStation(pagerData->data));
|
||||
}
|
||||
break;
|
||||
|
||||
case 3: // pager
|
||||
str->format("%d", decoder->GetPager(pagerData->data));
|
||||
break;
|
||||
|
||||
case 4: // action
|
||||
{
|
||||
PagerAction action = decoder->GetAction(pagerData->data);
|
||||
if(action == UNKNOWN) {
|
||||
str->format("%d", decoder->GetActionValue(pagerData->data));
|
||||
} else {
|
||||
str->format("%.4s", PagerActions::GetDescription(action));
|
||||
}
|
||||
}; break;
|
||||
|
||||
case 5: // repeats or edit flag
|
||||
if(pagerData->edited) {
|
||||
str->format("**");
|
||||
} else if(receiveMode) {
|
||||
str->format("x%d", pagerData->repeats);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void showConfig(uint32_t) {
|
||||
SettingsScreen* screen = new SettingsScreen(config, pagerReceiver, subghz, updateUserCategory);
|
||||
UiManager::GetInstance()->PushView(screen->GetView());
|
||||
}
|
||||
|
||||
void editPagerMessage(uint32_t index) {
|
||||
if((int)index == scanForMoreButtonIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
PagerDataGetter getPager = pagerReceiver->PagerGetter(index);
|
||||
EditPagerScreen* screen = new EditPagerScreen(config, subghz, pagerReceiver, getPager, index < fromFilePagersCount);
|
||||
UiManager::GetInstance()->PushView(screen->GetView());
|
||||
}
|
||||
|
||||
void showActions(uint32_t index) {
|
||||
if((int)index == scanForMoreButtonIndex) {
|
||||
if(!receiveMode) {
|
||||
subghz->SetReceiveAfterTransmission(true);
|
||||
subghz->ReceiveAsync();
|
||||
|
||||
receiveMode = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
PagerDataGetter getPager = pagerReceiver->PagerGetter(index);
|
||||
StoredPagerData* pagerData = getPager();
|
||||
PagerDecoder* decoder = pagerReceiver->decoders[pagerData->decoder];
|
||||
PagerProtocol* protocol = pagerReceiver->protocols[pagerData->protocol];
|
||||
|
||||
PagerActionsScreen* screen = new PagerActionsScreen(config, getPager, decoder, protocol, subghz);
|
||||
UiManager::GetInstance()->PushView(screen->GetView());
|
||||
}
|
||||
|
||||
bool goBack() {
|
||||
if(receiveMode && menuView->GetElementsCount() > 0) {
|
||||
DialogUiView* confirmGoBack = new DialogUiView("Really stop scan?", "You may loose captured signals");
|
||||
confirmGoBack->AddLeftButton("No");
|
||||
confirmGoBack->AddRightButton("Yes");
|
||||
confirmGoBack->SetResultHandler(HANDLER_1ARG(&ScanStationsScreen::goBackConfirmationHandler));
|
||||
UiManager::GetInstance()->PushView(confirmGoBack);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void deleteCategory(int) {
|
||||
AppFileSysytem().DeleteCategory(pagerReceiver->GetCurrentUserCategory());
|
||||
menuView->SetRightButton("Deleted", NULL);
|
||||
}
|
||||
|
||||
void goBackConfirmationHandler(DialogExResult dialogResult) {
|
||||
if(dialogResult == DialogExResultRight) {
|
||||
UiManager::GetInstance()->PopView(false);
|
||||
}
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
delete subghz;
|
||||
delete pagerReceiver;
|
||||
delete this;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
|
||||
#include "app/AppFileSystem.hpp"
|
||||
#include "lib/ui/UiManager.hpp"
|
||||
#include "lib/ui/view/SubMenuUiView.hpp"
|
||||
#include "lib/ui/view/TextInputUiView.hpp"
|
||||
|
||||
#define MIN_CAT_NAME_LENGTH 2
|
||||
#define MAX_CAT_NAME_LENGTH MAX_FILENAME_LENGTH
|
||||
|
||||
class SelectCategoryScreen {
|
||||
private:
|
||||
SubMenuUiView* menu;
|
||||
CategoryType categoryType;
|
||||
forward_list<char*> categories;
|
||||
function<void(CategoryType, const char*)> categorySelectedHandler;
|
||||
TextInputUiView* nameInput;
|
||||
char* categoryAddedName;
|
||||
|
||||
public:
|
||||
SelectCategoryScreen(
|
||||
bool canCreateNew,
|
||||
CategoryType categoryType,
|
||||
function<void(CategoryType, const char*)> categorySelectedHandler
|
||||
) {
|
||||
this->categoryType = categoryType;
|
||||
this->categorySelectedHandler = categorySelectedHandler;
|
||||
|
||||
menu = new SubMenuUiView("Select category");
|
||||
menu->SetOnDestroyHandler(HANDLER(&SelectCategoryScreen::destory));
|
||||
|
||||
if(canCreateNew) {
|
||||
menu->AddItem("+ Create NEW", HANDLER_1ARG(&SelectCategoryScreen::createNew));
|
||||
}
|
||||
|
||||
if(categoryType == User) {
|
||||
menu->AddItem("<Default/Uncategorized>", [categoryType, categorySelectedHandler](uint32_t) {
|
||||
return categorySelectedHandler(categoryType, NULL);
|
||||
});
|
||||
}
|
||||
|
||||
AppFileSysytem().GetCategories(&categories, categoryType);
|
||||
for(char* category : categories) {
|
||||
addCategory(category);
|
||||
}
|
||||
}
|
||||
|
||||
UiView* GetView() {
|
||||
return menu;
|
||||
}
|
||||
|
||||
private:
|
||||
void createNew(uint32_t) {
|
||||
if(categoryAddedName != NULL) {
|
||||
categorySelectedHandler(categoryType, categoryAddedName);
|
||||
return;
|
||||
}
|
||||
if(nameInput == NULL) {
|
||||
nameInput = new TextInputUiView("Enter category name", MIN_CAT_NAME_LENGTH, MAX_CAT_NAME_LENGTH);
|
||||
nameInput->SetOnDestroyHandler([this]() { this->nameInput = NULL; });
|
||||
nameInput->SetResultHandler(HANDLER_1ARG(&SelectCategoryScreen::addAndSelectCategory));
|
||||
}
|
||||
UiManager::GetInstance()->PushView(nameInput);
|
||||
}
|
||||
|
||||
void addCategory(char* name) {
|
||||
menu->AddItem(name, [this, name](uint32_t) { return this->categorySelectedHandler(this->categoryType, name); });
|
||||
}
|
||||
|
||||
void addAndSelectCategory(char* name) {
|
||||
categoryAddedName = name;
|
||||
menu->SetItemLabel(0, name);
|
||||
UiManager::GetInstance()->PopView(true);
|
||||
}
|
||||
|
||||
void destory() {
|
||||
while(!categories.empty()) {
|
||||
delete[] categories.front();
|
||||
categories.pop_front();
|
||||
}
|
||||
|
||||
if(nameInput != NULL) {
|
||||
delete nameInput;
|
||||
}
|
||||
|
||||
delete this;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,177 @@
|
||||
#pragma once
|
||||
|
||||
#include "SelectCategoryScreen.hpp"
|
||||
#include "app/AppConfig.hpp"
|
||||
#include "app/pager/PagerReceiver.hpp"
|
||||
#include "lib/String.hpp"
|
||||
#include "lib/hardware/subghz/SubGhzModule.hpp"
|
||||
#include "lib/ui/UiManager.hpp"
|
||||
#include "lib/ui/view/VariableItemListUiView.hpp"
|
||||
|
||||
class SettingsScreen {
|
||||
private:
|
||||
AppConfig* config;
|
||||
SubGhzModule* subghz;
|
||||
PagerReceiver* receiver;
|
||||
VariableItemListUiView* varItemList;
|
||||
|
||||
UiVariableItem* currentCategoryItem;
|
||||
UiVariableItem* frequencyItem;
|
||||
UiVariableItem* maxPagerItem;
|
||||
UiVariableItem* signalRepeatItem;
|
||||
UiVariableItem* ignoreSavedItem;
|
||||
UiVariableItem* autosaveFoundItem;
|
||||
UiVariableItem* debugModeItem;
|
||||
|
||||
String frequencyStr;
|
||||
String maxPagerStr;
|
||||
String signalRepeatStr;
|
||||
bool updateUserCategory;
|
||||
uint32_t categoryItemIndex;
|
||||
|
||||
public:
|
||||
SettingsScreen(AppConfig* config, PagerReceiver* receiver, SubGhzModule* subghz, bool updateUserCategory) {
|
||||
this->config = config;
|
||||
this->receiver = receiver;
|
||||
this->subghz = subghz;
|
||||
this->updateUserCategory = updateUserCategory;
|
||||
|
||||
varItemList = new VariableItemListUiView();
|
||||
varItemList->SetOnDestroyHandler(HANDLER(&SettingsScreen::destroy));
|
||||
varItemList->SetEnterPressHandler(HANDLER_1ARG(&SettingsScreen::enterPressHandler));
|
||||
|
||||
categoryItemIndex = varItemList->AddItem(
|
||||
currentCategoryItem = new UiVariableItem("Category", HANDLER_1ARG(&SettingsScreen::categoryChangedHandler))
|
||||
);
|
||||
|
||||
varItemList->AddItem(
|
||||
frequencyItem = new UiVariableItem(
|
||||
"Scan frequency",
|
||||
FrequencyManager::GetInstance()->GetFrequencyIndex(config->Frequency),
|
||||
FrequencyManager::GetInstance()->GetFrequencyCount(),
|
||||
[this](uint8_t val) {
|
||||
uint32_t freq = this->config->Frequency = FrequencyManager::GetInstance()->GetFrequency(val);
|
||||
this->subghz->SetReceiveFrequency(this->config->Frequency);
|
||||
return frequencyStr.format("%lu.%02lu", freq / 1000000, (freq % 1000000) / 10000);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
varItemList->AddItem(
|
||||
maxPagerItem = new UiVariableItem(
|
||||
"Max pager value",
|
||||
config->MaxPagerForBatchOrDetection - 1,
|
||||
UINT8_MAX,
|
||||
[this](uint8_t val) {
|
||||
this->config->MaxPagerForBatchOrDetection = val + 1;
|
||||
return maxPagerStr.fromInt(this->config->MaxPagerForBatchOrDetection);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
varItemList->AddItem(
|
||||
signalRepeatItem = new UiVariableItem(
|
||||
"Times to repeat signal",
|
||||
config->SignalRepeats - 1,
|
||||
UINT8_MAX,
|
||||
[this](uint8_t val) {
|
||||
this->config->SignalRepeats = val + 1;
|
||||
return signalRepeatStr.fromInt(this->config->SignalRepeats);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
varItemList->AddItem(
|
||||
ignoreSavedItem = new UiVariableItem(
|
||||
"Saved stations",
|
||||
config->SavedStrategy,
|
||||
SavedStationStrategyValuesCount,
|
||||
[this](uint8_t val) {
|
||||
this->config->SavedStrategy = static_cast<enum SavedStationStrategy>(val);
|
||||
return savedStationsStrategy(this->config->SavedStrategy);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
varItemList->AddItem(
|
||||
autosaveFoundItem = new UiVariableItem(
|
||||
"Autosave found signals",
|
||||
config->AutosaveFoundSignals,
|
||||
2,
|
||||
[this](uint8_t val) {
|
||||
this->config->AutosaveFoundSignals = val;
|
||||
return boolOption(val);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
UiView* GetView() {
|
||||
return varItemList;
|
||||
}
|
||||
|
||||
private:
|
||||
void enterPressHandler(uint32_t index) {
|
||||
if(index != categoryItemIndex) {
|
||||
return;
|
||||
}
|
||||
UiManager::GetInstance()->PushView(
|
||||
(new SelectCategoryScreen(false, User, HANDLER_2ARG(&SettingsScreen::categorySelected)))->GetView()
|
||||
);
|
||||
}
|
||||
|
||||
void categorySelected(CategoryType, const char* category) {
|
||||
if(config->CurrentUserCategory != NULL) {
|
||||
delete config->CurrentUserCategory;
|
||||
}
|
||||
config->CurrentUserCategory = category != NULL ? new String("%s", category) : NULL;
|
||||
UiManager::GetInstance()->PopView(false);
|
||||
currentCategoryItem->Refresh();
|
||||
}
|
||||
|
||||
const char* categoryChangedHandler(uint8_t) {
|
||||
const char* category = config->GetCurrentUserCategoryCstr();
|
||||
if(category == NULL) {
|
||||
category = "Default";
|
||||
}
|
||||
return category;
|
||||
}
|
||||
|
||||
const char* boolOption(uint8_t value) {
|
||||
return value ? "ON" : "OFF";
|
||||
}
|
||||
|
||||
const char* savedStationsStrategy(SavedStationStrategy value) {
|
||||
switch(value) {
|
||||
case IGNORE:
|
||||
return "Ignore";
|
||||
|
||||
case SHOW_NAME:
|
||||
return "Show name";
|
||||
|
||||
case HIDE:
|
||||
return "Hide";
|
||||
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
config->Save();
|
||||
if(updateUserCategory) {
|
||||
receiver->SetUserCategory(config->CurrentUserCategory);
|
||||
receiver->ReloadKnownStations();
|
||||
}
|
||||
|
||||
delete currentCategoryItem;
|
||||
delete frequencyItem;
|
||||
delete maxPagerItem;
|
||||
delete signalRepeatItem;
|
||||
delete ignoreSavedItem;
|
||||
delete autosaveFoundItem;
|
||||
delete debugModeItem;
|
||||
|
||||
delete this;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
# For details & more options, see documentation/AppManifests.md in firmware repo
|
||||
|
||||
App(
|
||||
appid="chief_cooker", # Must be unique
|
||||
name="Chief Cooker", # Displayed in menus
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="chief_cooker_app",
|
||||
stack_size=2 * 1024,
|
||||
fap_category="Sub-GHz",
|
||||
# Optional values
|
||||
# fap_version="0.1",
|
||||
fap_icon="chief_cooker.png", # 10x10 1-bit PNG
|
||||
# fap_description="A simple app",
|
||||
fap_author="Denr01",
|
||||
fap_weburl="https://github.com/denr01/FZ-ChiefCooker",
|
||||
fap_icon_assets="images", # Image assets to compile for this application
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
/* generated by fbt from .png files in images folder */
|
||||
#include <chief_cooker_icons.h>
|
||||
|
||||
#include "app/App.hpp"
|
||||
|
||||
extern "C" int32_t chief_cooker_app(void* p) {
|
||||
UNUSED(p);
|
||||
|
||||
App app;
|
||||
app.Run();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
After Width: | Height: | Size: 390 B |
@@ -0,0 +1,127 @@
|
||||
# Usage instructions
|
||||
|
||||
## Installation
|
||||
To install this app simply download the `.fap` file from [latest release](https://github.com/denr01/FZ-ChiefCooker/releases/latest).
|
||||
|
||||
Then just copy it to your flipper (to `ext/apps/Sub-GHz` folder).
|
||||
|
||||
On your flipper, open up Apps -> Sub-GHz and you should see it there. Just open it as a regular app.
|
||||
|
||||
## Tutorial
|
||||
|
||||
### Your first use
|
||||
Imagine you are on a food court you want to become chief on.
|
||||
|
||||
First, open the app and select "Scan for station signals".
|
||||
|
||||
The app will start receiving signals and show you once it receives something
|
||||
|
||||
<img src="screenshots/main-scan.png" width="256"> <img src="screenshots/scan-empty.png" width="256"> <img src="screenshots/scan-capture1.png" width="256">
|
||||
|
||||
Okay, now you received something. Now, let's test if transmission decoded correctly and find the restaurant who sent the transmission!
|
||||
|
||||
To do this, click on center button (actions) and select the first one, "Resend to ALL":
|
||||
|
||||
<img src="screenshots/scan-capture1-actions.png" width="256"> <img src="screenshots/scan-capture1-resend.png" alt="Description" width="256">
|
||||
|
||||
Where are they all running? Is the dinner ready yet? Unfortunately it's not. Just their new chief is learning...
|
||||
|
||||
Let's assume that you somehow found out, that it was a restaurant called "Street Food" who sent the signal. Now let's save it's signal to your SD card.
|
||||
|
||||
Go back from actions and push the "Edit >" (right arrow) button. Then scroll down to "Save signal as...", give it a name and then create a new category for it. It's convenient to use restaurant name for signal name and mall (or food court/place name where restaurant are located) for the category name to make sure that signals from different places will not mess up.
|
||||
|
||||
<img src="screenshots/scan-capture1.png" width="256"> <img src="screenshots/scan-capture1-edit-save.png" width="256"> <img src="screenshots/scan-capture1-save-name.png" width="256">
|
||||
|
||||
<img src="screenshots/scan-capture1-categories.png" width="256"> <img src="screenshots/scan-capture1-category-name.png" width="256"> <img src="screenshots/scan-capture1-categories-with-new.png" width="256">
|
||||
|
||||
Congratulations! Your saved your first captured signal and now can use it anytime you want to call someone to the restaurant's food pickup.
|
||||
|
||||
But it would be great now to distinguish the signals from "Street Food" from the other ones. And you can do it!
|
||||
|
||||
Navigate to "< Config" (left button) and select the category to the newly created one. Then go back.
|
||||
|
||||
<img src="screenshots/scan-capture1.png" width="256"> <img src="screenshots/scan-capture1-config.png" width="256"> <img src="screenshots/scan-capture1-conf-select-cat.png" width="256">
|
||||
|
||||
<img src="screenshots/scan-capture1-conf-cat-selected.png" width="256"> <img src="screenshots/scan-capture1-with-name.png" width="256">
|
||||
|
||||
As you can see, now instead of hex value and station number there is a restaurant name you given to the signal.
|
||||
|
||||
And what's this? A new signal? Yes, but not completely new. Street Food just called pager with another number (7). But your flipper successfully recognized their signal because you already saved one to the current category and showed you it's name.
|
||||
|
||||
<img src="screenshots/scan-capture2.png" width="256">
|
||||
|
||||
### Your second use
|
||||
|
||||
Now imagine you came to the same food court next day and want to call somebody's pager at the Street Food restaurant (that your saved yesterday).
|
||||
|
||||
Navigate to "Saved stations" and then select the category of current place / mall / food court:
|
||||
|
||||
<img src="screenshots/main-saved.png" width="256"> <img src="screenshots/saved-by-you.png" width="256"> <img src="screenshots/scan-capture1-categories-with-new.png" width="256">
|
||||
|
||||
Here your will see your saved signal:
|
||||
|
||||
<img src="screenshots/saved-category.png" width="256">
|
||||
|
||||
Now your can do with it whatever you want exactly like when you captured it.
|
||||
|
||||
Let's assume now you need to call only single pager with number 9.
|
||||
|
||||
Go to "Edit >" menu, scroll down to "Pager" and change it value to 9. Now just press center button. Your flipper will blink purple LED - like when you were resending to all pagers, remember? This means that it sent the signal.
|
||||
|
||||
<img src="screenshots/pager-9.png" width="256">
|
||||
|
||||
Now imagine we want to receive more signals here. There are two ways to do it:
|
||||
- Go to "Scan" menu like on your first usage
|
||||
- Click on "Scan here for more".
|
||||
|
||||
The second way is better because you will have all you previously saved signals in quick access in cause you urgently need one of them. New signals will appear here once your flipper receive them.
|
||||
|
||||
<img src="screenshots/saved-category-scan-here.png" width="256"> <img src="screenshots/saved-category-scanning.png" width="256"> <img src="screenshots/saved-category-scanning-new.png" width="256">
|
||||
|
||||
Congratulations! You have successfully completed the tutorial! ~~Now go and troll someone real.~~
|
||||
|
||||
## App's screens explanation
|
||||
|
||||
### Scan stations screen
|
||||
|
||||
<img src="screenshots/scan-capture1.png" width="256">
|
||||
|
||||
The values here are:
|
||||
- `CBC042` - signal hex code
|
||||
- `815` - station number (in current encoding)
|
||||
- `4` - pager number (in current encoding)
|
||||
- `RING` - action (in current encoding)
|
||||
- `x8` - number of signal repeats, will not show more than `x99`
|
||||
|
||||
_Note: if you change the signal's encoding in "Edit" menu, station number, pager and action here will also change._
|
||||
|
||||
### Edit station screen
|
||||
|
||||
There are several things you can edit in captured signal using "Edit >" menu.
|
||||
|
||||
First thing is **encoding**. App tries to detect encoding automatically when it receives signal. But in some cases you may need to specify it manually. Here you can change the encoding and see how the values (station number, pager number and action) are changing in real-time:
|
||||
|
||||
<img src="screenshots/edit-decode-1.png" width="256"> <img src="screenshots/edit-decode-2.png" width="256"> <img src="screenshots/edit-decode-3.png" width="256">
|
||||
|
||||
Also you may need to change pager or action value. You can do it here and see how the hex value changes in real time:
|
||||
|
||||
<img src="screenshots/edit-pager-1.png" width="256"> <img src="screenshots/edit-pager-2.png" width="256">
|
||||
|
||||
_Note: pager number is editable only if it's decoded value is less than 255 in current encoding_
|
||||
|
||||
**Also note: pressing the center button on anywhere on the edit screen (except for save as / delete options) will trigger signal transmission with current pager/action/hex value!**
|
||||
|
||||
### Config screen
|
||||
|
||||
<img src="screenshots/scan-capture1-config.png" width="256">
|
||||
|
||||
Here some description about config parameters:
|
||||
- **Category** - the category to load saved station names from. Does not affect if you use option "Scan here for more" in saved stations screen.
|
||||
- **Scan frequency** - the frequency to receive signals on. For EU/Russia default is 433.92 Mhz, but 315.00 Mhz and 467.75 Mhz may be also used in US or somewhere else.
|
||||
- **Max pager value** - how many pagers should signal be sent to when using "Resend to ALL" action. Also affects automatic encoding detection feature: the algorithm will use the first encoding which will give a pager number less or equal than current setting value.
|
||||
- **Times to repeat signal** - speaks for itself, don't recommend changing it as the default value (10) should work in most cases.
|
||||
- **Saved stations** - what to do when receive a signal from known station (saved in current category). Possible values are:
|
||||
1. **Ignore** - treat station as unknown, show signal hex and station number.
|
||||
2. **Show name** (default) - show saved station name instead of hex value and station number
|
||||
3. **Hide** - do not show signals from saved stations at all. Show only unknown signals
|
||||
- **Autosave found signals** - any found signals will be saved to "Autosaved" folder in the subdirectory with current date. Useful in case app crashes or you accidentally close it without saving.
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |