Merge remote-tracking branch 'mntm/dev' into 956/prs

# Conflicts:
#	lib/subghz/protocols/protocol_items.c
#	lib/subghz/protocols/protocol_items.h
This commit is contained in:
956MB
2026-07-04 20:37:04 -05:00
248 changed files with 10829 additions and 1837 deletions
+96 -11
View File
@@ -1,52 +1,137 @@
### Added:
- Apps:
- NFC: ISO 15693-3 NFC Writer (by @ch4istO)
- Games: Checkers (by @H4W9)
- GPIO: CAN Commander (by @MatthewKuKanich)
- NFC:
- ISO 15693-3 NFC Writer (by @ch4istO)
- UL-C Bruteforce (by @noproto)
- UL-C Relay (by @noproto)
- ULCFkey (by @noproto)
- Sub-GHz: ProtoPirate (by @RocketGod-git & @xMasterX & @zero-mega et al.)
- Tools: Flipper Wedge (by @dangerous-tac0s)
- Sub-GHz:
- UL: Add support for 42+ Keeloq based systems (with partial Add Manually support) (by @zero-mega, @xMasterX, ARF Team)
- UL: Cardin S449 protocol full support (64bit keeloq) (with Add manually, and all button codes) (use FSK12K modulation to read the remote) (by @xMasterX & @zero-mega)
- UL: Beninca ARC AES128 protocol full support (128bit dynamic) (with Add manually, and 2 button codes) (by @xMasterX & @zero-mega)
- UL: Jarolift protocol full support (72bit dynamic) (with Add manually, and all button codes) (by @xMasterX & d82k & Steffen (bastelbudenbuben de))
- UL: Treadmill37 protocol support (37bit static) (by @xMasterX)
- UL: Ditec GOL4 protocol (with programming mode, button switch, add manually) (by @xMasterX & @zero-mega)
- UL: KeyFinder protocol (24bit static) (by @xMasterX & @mishamyte)
- UL: Nord ICE protocol (33 bits, Static) (by @xMasterX)
- UL: Allstar Firefly 318ALD31K protocol (18 bits, Static) (by @jlaughter)
- UL: Add CAME TOP44FGN support in CAME TWEE protocol (by @xMasterX)
- UL: New modulation FSK with 12KHz deviation (by @xMasterX)
- UL: KingGates Stylo 4k Add manually and button switch support and refactoring of encoder (by @xMasterX)
- UL: Stilmatic button 9 support (two buttons hold simulation) (mapped on arrow keys) (by @xMasterX)
- UL: Stilmatic (R-Tech) 12bit discr. fix & button 9 support (two buttons hold simulation) (mapped on arrow keys) (by @xMasterX)
- UL: Sommer last button code 0x6 support (mapped on arrow keys) (by @xMasterX)
- UL: V2 Phoenix (Phox) added 2 counter modes support (docs updated) (by @xMasterX)
- UL: Add 390MHz and 430.5MHz to default hopper list (6 elements like in OFW) (works well with Hopper RSSI level set for your enviroment) (by @xMasterX)
- UL: Add signals button editor and real remote simulation (full signal transmit with just one click) (by @Dmitry422)
- UL: KeeLoq add counter mode 7 (sends 7 signals increasing counter with 0x3333 steps) - may bypass counter on some receivers! (by @xMasterX)
- UL: TX Power setting (by @LeeroysHub)
- UL: Somfy Keytis button switch and Add Manually support (by @xMasterX)
- UL: Genius Echo/Bravo add 2 buttons hold simulation (0xB btn code) (by @xMasterX)
- UL: Add all 0x0s and all 0xFs KeeLoq MF codes for normal and simple learning (by @xMasterX)
- UL: Better support for CAME Atomo type remotes (TOPD4REN) (decode + button codes) (by @xMasterX & Roman)
- NFC:
- Add Mifare Ultralight C Write Support (#524 by @haw8411)
- UL: Add ISO15693-3 and SLIX write-back support (by @DoniyorI)
- UL: Add new parsers SZPPK, SKPPK and SevPPK, upgrade Plantain parser, fix TwoCities parser (by @mxcdoam)
- OFW PR 4343: RFID: Add Indala 224-bit (long format) protocol support (by @kuzaxak)
- UL: JS: Add IR capabilities to the JS engine (by @LuisMayo)
- FBT: Allow apps to specify custom cflags (by @WillyJL)
- UL: Docs: Add [full list of supported SubGHz protocols](https://github.com/Next-Flip/Momentum-Firmware/blob/dev/documentation/SubGHzSupportedSystems.md) and their frequencies/modulations that can be used for reading remotes (by @xMasterX)
UL: API: Add `canvas_get_buffer`, `canvas_get_buffer_size` to public API (by @xMasterX)
### Updated:
- Apps:
- XERO: MFKey: 4.1 with 40% faster key recovery, improved memory efficiency (by @dchristle), new SEN dictionary for 10x faster recovery (by @noproto)
- UL: Update Sub-GHz apps for FM12K modulation (by @xMasterX)
- UL: USB/BT Remote: PTT improvements (by @hryamzik)
- Ami Tool: Remove copyrighted names, write card fully using low level poller (by @Firefox2100)
- CAN Tools: Parity with DBC format, support importing DBC files (by @MatthewKuKanich)
- ESP Flasher: Bump Marauder 1.9.1 (by @justcallmekoko), Marauder 1.9.0 support (by @H4W9)
- FlipSocial: Autocomplete, keyboard improvements, bugfixes (by @jblanked)
- ESP Flasher: Bump Marauder 1.12.1 (by @justcallmekoko) FlipperHTTP 2.1.1 (by @jblanked) Blackmagic 0.1.2-rc (by @hedger), advanced flashing mode (by @H4W9)
- ESP32 WiFi Marauder: Marauder 1.12.0 support (by @pfefferle), Marauder 1.10.0 support (by @justcallmekoko), Marauder 1.9.0 support (by @H4W9)
- FlipLibrary: Added Fahrenheit, current weather, and wind speed/direction (by @H4W9)
- FlipSocial: Autocomplete, keyboard improvements, explore and profile view enhancements, bugfixes (by @jblanked)
- FlipWeather: Added Fahrenheit, current weather, and wind speed/direction, custom location option (by @H4W9)
- Free Roam: Minimap, code optimization, ux improvements, online mode, option to hide/show player (by @jblanked)
- Flipper Blackhat: TUI command (by @o7-machinehum)
- Geometry Dash: Major refactor, bugfixes and performance improvements, rename from Geometry Flip (by @gooseprjkt)
- GhostESP: Many bugfixes, added Flock Detection, Flock List, and Stop Flock Detection commands (by @jaylikesbunda)
- HC-SR04 Distance Sensor: Option to change measure units (by @Tyl3rA)
- IconEdit: Save/Send animations, settings tab with canvas scale and cursor guides, bugfixes (by @rdefeo)
- NFC Login: Code refactor, bugfixes, renamed from NFC PC Login (by @Play2BReal)
- Seader: SAM ATR3 support, better IFSC/IFSD handling, various improvements (by @bettse)
- Seos Compatible: Seos write support, various improvements (by @aaronjamt)
- INA2xx INA Meter: Fixed application freezing when the sensor is not connected (by @cepetr)
- KT0803 FM Transmitter: Many bugfixes (by @nixvio64)
- Lidar Emulator: Settings menu with external output and 5V power on support, separate thread for timing the infrared signal (by @ANTodorov)
- Lidar Emulator: Support external IR boards (by @ANTodorov)
- Metroflip: Bugfixes (by @baalpeteor2)
- NearbyFiles: GPS Baudrate selection, Ability to add GPS to file (by @xMasterX)
- NFC Login: Code refactor, bugfixes, renamed from NFC PC Login, restore BLE profile on app exit (by @Play2BReal)
- Passy: Bugfixes, Save advanced files as well (by @bettse)
- Picopass: Option to Create credential without a card (by @redteamlife), info screen improvements, allow Emulation of NRMAC dumped cards, Clean MKF option (by @bettse)
- Seader: SAM ATR3 support, better IFSC/IFSD handling, various improvements (by @bettse), reverse engineered U90 packet, SNMP support, multi-card selection, memory optimization, refactoring (by @cindersocket)
- Seos Compatible: Seos write support, various improvements (by @aaronjamt), support switching key sets (by @pcunning), code refactoring, various bugfixes (by @bettse)
- Sub-GHz Scheduler: Added new interval times, bugfixes and improvements (by @shalebridge)
- Unitemp: Numerous improvements from @MLAB-project fork (by @MLAB-project)
- UL: Update Sub-GHz apps for FM12K modulation (by @xMasterX)
- Tetris: Various bugfixes (by @Bricktech2000)
- Unitemp: Numerous improvements from @MLAB-project fork (by @MLAB-project), Many changes, new sensors and code refactoring in v2.0 (by @quen0n)
- Weebo: Cycle between .nfc files in the same directory (by @sethgregory), new figure IDs (by @Brickleberry19), bugfixes and refactoring (by @bettse)
- XRemote: Add dolphin xp and crash bugfix (by @teohumeau)
- Yatzee: Fix Full House calculation, improve code readability (by @zessx)
- Sub-GHz:
- Sub-GHz app is now external on SD card, like on OFW (by @WillyJL)
- This was done to free some space on flash, which is always running out
- It means RAM consumption while using the Sub-GHz app has increased
- Usability remains mostly unaffected, our testing showed it can receive up to ~50 signals normally and ~10 signals with qFlipper / Flipper Mobile App connected
- You may experience some "out of memory" messages, this is inevitable as RAM eventually fragments and needs to be rearraged, simply resume as normal after the reboot
- Add Manually menu transparently refactored into a "separate app" for lower RAM usage, it is still accessed as before from Sub-GHz app (by @WillyJL)
- UL: Counter editor refactoring (by @Dmitry422)
- UL: Alutech AT-4N & Nice Flor S turbo speedup (by @Dmitry422)
- UL: Sommer fm2 in Add manually now uses FM12K modulation (Sommer without fm2 tag uses FM476) (try this if regular option doesn't work for you) (by @xMasterX)
- UL: Replaced Cars ignore option with Revers RB2 protocol ignore option (by @xMasterX)
- UL: Improve docs on low level code (by @Dmitry422)
- UL: KeeLoq display decrypted `Hop` instead of showing encrypted as is (encrypted non byte reversed hop is still displayed in `Key` field) (by @xMasterX)
- UL: BFT KeeLoq try decoding with zero seed too (by @xMasterX)
- UL: KeeLoq display BFT programming mode TX (when arrow button is held) (by @xMasterX)
- UL: Signal Settings Improvements (by @Dmitry422)
- UL: KeeLoq change delta size (by @xMasterX)
- Archive: Support opening and pinning ProtoPirate files from Archive (#510 by @LeeroysHub)
- Display: Extend lcd contrast range to -10 to +18 (by @ShaTie)
- OFW PR 4297: RFID: Make FDX-B readout more descriptive (by @snowsign)
- OFW PR 4320: API: Make `view_port_send_to_back()` public (by @loftyinclination)
### Fixed:
- Sub-GHz:
- Fix display of memory full (by @WillyJL)
- UL: Fixed button mapping for FAAC RC/XT (by @xMasterX)
- UL: Possible Sommer timings fix (by @xMasterX)
- UL: Various fixes (by @xMasterX)
- UL: Various fixes and cleanup (by @xMasterX)
- UL: Nice Flor S remove extra uint64 variable (by @xMasterX)
- UL: Fix Alutech AT4N false positives (by @xMasterX)
- UL: Fix documentation link for HT12A protocol (by @carlogrisetti)
- UL: BFT Mitto fix decode bug (seed was not resetting after one successful decode) (by @xMasterX)
- UL: KeeLoq fix display of AN-Motors and HCS101 keys (by @xMasterX)
- UL: Fix CAME TWEE repeats count for button click (by @xMasterX)
- UL: Fix small logic error in KingGatesStylo4k protocol (by @xMasterX & @X-Stuff)
- UL: Improve Nice FLO decoding (by @xMasterX & Roman)
- UL: Fix duplicate code in RAW protocol (by @xMasterX)
- UL: Fix FAAC SLH wrong decode/encode, apply little code cleanup (by @xMasterX)
- NFC:
- Fix sending 32+ byte ISO 15693-3 commands (by @WillyJL)
- Fixes to `READ_MULTI` and `GET_BLOCK_SECURITY` commands in ISO 15693-3 emulation (#501 by @WillyJL & aaronjamt)
- Fix CLI with NTAG4xx and Type 4 Tag support (by @WillyJL)
- UL: Fix LED not blinking at SLIX unlock (by @xMasterX)
- UL: Fix "MIR" and other EMV cards crash on Read (by @Dmitry422)
- OFW PR 4362: Fix BusFault in Write to Initial Card (by @akrylysov)
- OFW PR 4369: Fix stack buffer overflows in MFUL FAST_READ and DESFire file settings parsers (by @qp-x-qp)
- uFBT: Fix .clangd config for IDEs besides VSCode (by @WillyJL)
- UL: Settings: Storage settings exit scenes properly if used via favourites (by @xMasterX)
- UL: UI: Some small changes (by @xMasterX)
- OFW PR 4364: JS: Address utf8 support when uploading JavaScript application (by @bekindpleaserewind)
- OFW PR 4338: USB: Fix USB HID keyboard LED state reporting (by @Caballosanex)
### Removed:
- Sub-GHz:
- Removed Starline, ScherKhan and Kia protocols from main Sub-GHz app, they can be decoded with `Apps > Sub-GHz > ProtoPirate` external app
- Disabled X10 and Hormann Bisecur protocols due to flash space constraints and very limited usefulness, Momentum now has same protocol list as Unleashed
- Disabled X10 and Hormann Bisecur protocols due to flash space constraints and very limited usefulness, Momentum now has the same protocol list as Unleashed
- Desktop: Removed winter holidays anims
@@ -526,6 +526,134 @@ MU_TEST(test_lfrfid_protocol_fdxb_read_simple) {
protocol_dict_free(dict);
}
// Indala224: 224-bit PSK2 frame, no known FC/CN descramble,
// test data uses the Proxmark3-verified reference (lf indala reader output)
#define INDALA224_TEST_DATA_SIZE 28
#define INDALA224_TEST_DATA \
{0x80, 0x00, 0x00, 0x01, 0xB2, 0x35, 0x23, 0xA6, 0xC2, 0xE3, 0x1E, 0xBA, 0x3C, 0xBE, \
0xE4, 0xAF, 0xB3, 0xC6, 0xAD, 0x1F, 0xCF, 0x64, 0x93, 0x93, 0x92, 0x8C, 0x14, 0xE5}
#define INDALA224_BITS_PER_FRAME 224
#define INDALA224_US_PER_BIT 255
#define INDALA224_FRAMES_TO_DECODE 3
MU_TEST(test_lfrfid_protocol_indala224_roundtrip) {
ProtocolDict* dict = protocol_dict_alloc(lfrfid_protocols, LFRFIDProtocolMax);
mu_assert_int_eq(
INDALA224_TEST_DATA_SIZE, protocol_dict_get_data_size(dict, LFRFIDProtocolIndala224));
mu_assert_string_eq("Indala224", protocol_dict_get_name(dict, LFRFIDProtocolIndala224));
mu_assert_string_eq("Motorola", protocol_dict_get_manufacturer(dict, LFRFIDProtocolIndala224));
const uint8_t data[INDALA224_TEST_DATA_SIZE] = INDALA224_TEST_DATA;
// Encode: extract bit-level polarity from encoder output.
// PSK encoder yields carrier-level oscillation (duration=1 per half-cycle).
// PulseGlue produces ~16us outputs which are below the decoder's 127us threshold,
// so we extract the bit-level polarity directly: sample the encoder's phase state
// at the start of each bit period (every 32 yields = 16 carrier cycles).
protocol_dict_set_data(dict, LFRFIDProtocolIndala224, data, INDALA224_TEST_DATA_SIZE);
mu_check(protocol_dict_encoder_start(dict, LFRFIDProtocolIndala224));
const size_t total_bits = INDALA224_BITS_PER_FRAME * INDALA224_FRAMES_TO_DECODE;
bool* bit_levels = malloc(total_bits);
mu_check(bit_levels != NULL);
for(size_t i = 0; i < total_bits; i++) {
LevelDuration ld = protocol_dict_encoder_yield(dict, LFRFIDProtocolIndala224);
bit_levels[i] = level_duration_get_level(ld);
// Skip remaining 31 yields for this bit period
for(size_t skip = 0; skip < 31; skip++) {
protocol_dict_encoder_yield(dict, LFRFIDProtocolIndala224);
}
}
// Decode: convert bit-level polarities to run-length timing pairs
// and feed directly to decoder (simulates hardware PSK demodulator output).
protocol_dict_decoders_start(dict);
ProtocolId protocol = PROTOCOL_NO;
size_t i = 0;
while(i < total_bits) {
bool cur = bit_levels[i];
size_t run = 1;
while(i + run < total_bits && bit_levels[i + run] == cur) {
run++;
}
uint32_t duration = (uint32_t)(run * INDALA224_US_PER_BIT);
protocol = protocol_dict_decoders_feed(dict, cur, duration);
if(protocol != PROTOCOL_NO) break;
i += run;
}
free(bit_levels);
mu_assert_int_eq(LFRFIDProtocolIndala224, protocol);
uint8_t received_data[INDALA224_TEST_DATA_SIZE] = {0};
protocol_dict_get_data(dict, protocol, received_data, INDALA224_TEST_DATA_SIZE);
mu_assert_mem_eq(data, received_data, INDALA224_TEST_DATA_SIZE);
protocol_dict_free(dict);
}
// Indala224 phase-alternating test: data with odd number of 1-bits
// causes PSK2 carrier phase to invert between consecutive frames.
// Decoder must accept inverted preamble at the second frame boundary.
#define INDALA224_ALT_TEST_DATA \
{0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
MU_TEST(test_lfrfid_protocol_indala224_alternating_phase) {
ProtocolDict* dict = protocol_dict_alloc(lfrfid_protocols, LFRFIDProtocolMax);
const uint8_t data[INDALA224_TEST_DATA_SIZE] = INDALA224_ALT_TEST_DATA;
protocol_dict_set_data(dict, LFRFIDProtocolIndala224, data, INDALA224_TEST_DATA_SIZE);
mu_check(protocol_dict_encoder_start(dict, LFRFIDProtocolIndala224));
const size_t total_bits = INDALA224_BITS_PER_FRAME * INDALA224_FRAMES_TO_DECODE;
bool* bit_levels = malloc(total_bits);
mu_check(bit_levels != NULL);
for(size_t i = 0; i < total_bits; i++) {
LevelDuration ld = protocol_dict_encoder_yield(dict, LFRFIDProtocolIndala224);
bit_levels[i] = level_duration_get_level(ld);
for(size_t skip = 0; skip < 31; skip++) {
protocol_dict_encoder_yield(dict, LFRFIDProtocolIndala224);
}
}
// Verify phase alternation: frame 1 and frame 2 start with opposite polarity
mu_check(bit_levels[0] != bit_levels[INDALA224_BITS_PER_FRAME]);
protocol_dict_decoders_start(dict);
ProtocolId protocol = PROTOCOL_NO;
size_t i = 0;
while(i < total_bits) {
bool cur = bit_levels[i];
size_t run = 1;
while(i + run < total_bits && bit_levels[i + run] == cur) {
run++;
}
uint32_t duration = (uint32_t)(run * INDALA224_US_PER_BIT);
protocol = protocol_dict_decoders_feed(dict, cur, duration);
if(protocol != PROTOCOL_NO) break;
i += run;
}
free(bit_levels);
mu_assert_int_eq(LFRFIDProtocolIndala224, protocol);
uint8_t received_data[INDALA224_TEST_DATA_SIZE] = {0};
protocol_dict_get_data(dict, protocol, received_data, INDALA224_TEST_DATA_SIZE);
mu_assert_mem_eq(data, received_data, INDALA224_TEST_DATA_SIZE);
protocol_dict_free(dict);
}
MU_TEST_SUITE(test_lfrfid_protocols_suite) {
MU_RUN_TEST(test_lfrfid_protocol_em_read_simple);
MU_RUN_TEST(test_lfrfid_protocol_em_emulate_simple);
@@ -538,6 +666,9 @@ MU_TEST_SUITE(test_lfrfid_protocols_suite) {
MU_RUN_TEST(test_lfrfid_protocol_inadala26_emulate_simple);
MU_RUN_TEST(test_lfrfid_protocol_indala224_roundtrip);
MU_RUN_TEST(test_lfrfid_protocol_indala224_alternating_phase);
MU_RUN_TEST(test_lfrfid_protocol_fdxb_read_simple);
MU_RUN_TEST(test_lfrfid_protocol_fdxb_emulate_simple);
}
@@ -826,12 +826,17 @@ bool subghz_device_cc1101_ext_start_async_tx(SubGhzDeviceCC1101ExtCallback callb
subghz_device_cc1101_ext->async_tx.buffer =
malloc(SUBGHZ_DEVICE_CC1101_EXT_ASYNC_TX_BUFFER_FULL * sizeof(uint32_t));
// here we do the same things as in /unleashed-firmware/targets/f7/furi_hal/furi_hal_subghz.c
// use first DMA to update timer TIM17 durations, but TIM17 have not output chanel
// so we use second DMA to transfer data from gpio_tx_buff directly to gpio pin using BSSR.
// BSSR allow us tranfer data directly to pin in gpio port.
//Signal generation with mem-to-mem DMA
furi_hal_gpio_write(subghz_device_cc1101_ext->g0_pin, false);
furi_hal_gpio_init(
subghz_device_cc1101_ext->g0_pin, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
// Configure DMA update timer
// Configure DMA to update timer TIM17 ARR by durations from buffer
LL_DMA_SetMemoryAddress(
SUBGHZ_DEVICE_CC1101_EXT_DMA_CH3_DEF, (uint32_t)subghz_device_cc1101_ext->async_tx.buffer);
LL_DMA_SetPeriphAddress(SUBGHZ_DEVICE_CC1101_EXT_DMA_CH3_DEF, (uint32_t) & (TIM17->ARR));
@@ -853,7 +858,7 @@ bool subghz_device_cc1101_ext_start_async_tx(SubGhzDeviceCC1101ExtCallback callb
furi_hal_bus_enable(FuriHalBusTIM17);
// Configure TIM
// Configure TIM 17
// Set the timer resolution to 2 us
LL_TIM_SetCounterMode(TIM17, LL_TIM_COUNTERMODE_UP);
LL_TIM_SetClockDivision(TIM17, LL_TIM_CLOCKDIVISION_DIV1);
@@ -867,7 +872,7 @@ bool subghz_device_cc1101_ext_start_async_tx(SubGhzDeviceCC1101ExtCallback callb
subghz_device_cc1101_ext_async_tx_refill(
subghz_device_cc1101_ext->async_tx.buffer, SUBGHZ_DEVICE_CC1101_EXT_ASYNC_TX_BUFFER_FULL);
// Configure tx gpio dma
// Configure DMA to transfer data from gpio_tx_buff directly to gpio pin using BSSR
const GpioPin* gpio = subghz_device_cc1101_ext->g0_pin;
subghz_device_cc1101_ext->async_tx.gpio_tx_buff[0] = (uint32_t)gpio->pin << GPIO_NUMBER;
+30 -1
View File
@@ -306,6 +306,33 @@ App(
sources=["plugins/supported_cards/plantain.c"],
)
App(
appid="szppk_so_parser",
apptype=FlipperAppType.PLUGIN,
entry_point="szppk_so_plugin_ep",
targets=["f7"],
requires=["nfc"],
sources=["plugins/supported_cards/szppk_so.c"],
)
App(
appid="sk_tk_parser",
apptype=FlipperAppType.PLUGIN,
entry_point="sk_tk_plugin_ep",
targets=["f7"],
requires=["nfc"],
sources=["plugins/supported_cards/sk_tk.c"],
)
App(
appid="sev_tk_parser",
apptype=FlipperAppType.PLUGIN,
entry_point="sev_tk_plugin_ep",
targets=["f7"],
requires=["nfc"],
sources=["plugins/supported_cards/sevppk_tk.c"],
)
App(
appid="two_cities_parser",
apptype=FlipperAppType.PLUGIN,
@@ -605,13 +632,15 @@ App(
"cli/commands/dump/protocols/iso14443_4a/nfc_cli_dump_iso14443_4a.c",
"cli/commands/dump/protocols/iso14443_4b/nfc_cli_dump_iso14443_4b.c",
"cli/commands/dump/protocols/iso15693_3/nfc_cli_dump_iso15693_3.c",
"cli/commands/dump/protocols/felica/nfc_cli_dump_felica.c",
"cli/commands/dump/protocols/mf_ultralight/nfc_cli_dump_mf_ultralight.c",
"cli/commands/dump/protocols/mf_classic/nfc_cli_dump_mf_classic.c",
"cli/commands/dump/protocols/mf_plus/nfc_cli_dump_mf_plus.c",
"cli/commands/dump/protocols/mf_desfire/nfc_cli_dump_mf_desfire.c",
"cli/commands/dump/protocols/slix/nfc_cli_dump_slix.c",
"cli/commands/dump/protocols/st25tb/nfc_cli_dump_st25tb.c",
"cli/commands/dump/protocols/felica/nfc_cli_dump_felica.c",
"cli/commands/dump/protocols/ntag4xx/nfc_cli_dump_ntag4xx.c",
"cli/commands/dump/protocols/type_4_tag/nfc_cli_dump_type_4_tag.c",
"cli/commands/mfu/nfc_cli_command_mfu.c",
"cli/commands/mfu/nfc_cli_action_info.c",
"cli/commands/mfu/nfc_cli_action_rdbl.c",
@@ -9,13 +9,15 @@
#include "protocols/iso14443_4a/nfc_cli_dump_iso14443_4a.h"
#include "protocols/iso14443_4b/nfc_cli_dump_iso14443_4b.h"
#include "protocols/iso15693_3/nfc_cli_dump_iso15693_3.h"
#include "protocols/mf_classic/nfc_cli_dump_mf_classic.h"
#include "protocols/mf_desfire/nfc_cli_dump_mf_desfire.h"
#include "protocols/mf_plus/nfc_cli_dump_mf_plus.h"
#include "protocols/felica/nfc_cli_dump_felica.h"
#include "protocols/mf_ultralight/nfc_cli_dump_mf_ultralight.h"
#include "protocols/mf_classic/nfc_cli_dump_mf_classic.h"
#include "protocols/mf_plus/nfc_cli_dump_mf_plus.h"
#include "protocols/mf_desfire/nfc_cli_dump_mf_desfire.h"
#include "protocols/slix/nfc_cli_dump_slix.h"
#include "protocols/st25tb/nfc_cli_dump_st25tb.h"
#include "protocols/felica/nfc_cli_dump_felica.h"
#include "protocols/ntag4xx/nfc_cli_dump_ntag4xx.h"
#include "protocols/type_4_tag/nfc_cli_dump_type_4_tag.h"
#include <datetime.h>
#include <furi_hal_rtc.h>
@@ -81,18 +83,20 @@ static bool nfc_cli_dump_parse_filename_key(FuriString* value, void* output) {
}
NfcGenericCallback protocol_poller_callbacks[NfcProtocolNum] = {
[NfcProtocolMfUltralight] = nfc_cli_dump_poller_callback_mf_ultralight,
[NfcProtocolMfClassic] = nfc_cli_dump_poller_callback_mf_classic,
[NfcProtocolFelica] = nfc_cli_dump_poller_callback_felica,
[NfcProtocolIso14443_3a] = nfc_cli_dump_poller_callback_iso14443_3a,
[NfcProtocolIso14443_3b] = nfc_cli_dump_poller_callback_iso14443_3b,
[NfcProtocolIso14443_4a] = nfc_cli_dump_poller_callback_iso14443_4a,
[NfcProtocolIso14443_4b] = nfc_cli_dump_poller_callback_iso14443_4b,
[NfcProtocolIso15693_3] = nfc_cli_dump_poller_callback_iso15693_3,
[NfcProtocolSlix] = nfc_cli_dump_poller_callback_slix,
[NfcProtocolMfDesfire] = nfc_cli_dump_poller_callback_mf_desfire,
[NfcProtocolFelica] = nfc_cli_dump_poller_callback_felica,
[NfcProtocolMfUltralight] = nfc_cli_dump_poller_callback_mf_ultralight,
[NfcProtocolMfClassic] = nfc_cli_dump_poller_callback_mf_classic,
[NfcProtocolMfPlus] = nfc_cli_dump_poller_callback_mf_plus,
[NfcProtocolMfDesfire] = nfc_cli_dump_poller_callback_mf_desfire,
[NfcProtocolSlix] = nfc_cli_dump_poller_callback_slix,
[NfcProtocolSt25tb] = nfc_cli_dump_poller_callback_st25tb,
[NfcProtocolNtag4xx] = nfc_cli_dump_poller_callback_ntag4xx,
[NfcProtocolType4Tag] = nfc_cli_dump_poller_callback_type_4_tag,
};
static void nfc_cli_dump_generate_filename(FuriString* file_path) {
@@ -219,6 +223,8 @@ static const NfcProtocolNameValuePair supported_protocols[] = {
{.name = "des", .value = NfcProtocolMfDesfire},
{.name = "slix", .value = NfcProtocolSlix},
{.name = "st25", .value = NfcProtocolSt25tb},
{.name = "ntag4", .value = NfcProtocolNtag4xx},
{.name = "t4t", .value = NfcProtocolType4Tag},
};
static bool nfc_cli_dump_parse_protocol(FuriString* value, void* output) {
@@ -0,0 +1,31 @@
#include "nfc_cli_dump_ntag4xx.h"
#include <nfc/protocols/ntag4xx/ntag4xx_poller.h>
#define TAG "NTAG4XX"
NfcCommand nfc_cli_dump_poller_callback_ntag4xx(NfcGenericEvent event, void* context) {
furi_assert(context);
furi_assert(event.protocol == NfcProtocolNtag4xx);
furi_assert(event.event_data);
NfcCliDumpContext* instance = context;
const Ntag4xxPollerEvent* ntag4xx_event = event.event_data;
NfcCommand command = NfcCommandContinue;
if(ntag4xx_event->type == Ntag4xxPollerEventTypeReadSuccess) {
nfc_device_set_data(
instance->nfc_device, NfcProtocolNtag4xx, nfc_poller_get_data(instance->poller));
instance->result = NfcCliDumpErrorNone;
command = NfcCommandStop;
} else if(ntag4xx_event->type == Ntag4xxPollerEventTypeReadFailed) {
instance->result = NfcCliDumpErrorFailedToRead;
command = NfcCommandReset;
}
if(command == NfcCommandStop) {
furi_semaphore_release(instance->sem_done);
}
return command;
}
@@ -0,0 +1,5 @@
#pragma once
#include "../nfc_cli_dump_common_types.h"
NfcCommand nfc_cli_dump_poller_callback_ntag4xx(NfcGenericEvent event, void* context);
@@ -0,0 +1,31 @@
#include "nfc_cli_dump_type_4_tag.h"
#include <nfc/protocols/type_4_tag/type_4_tag_poller.h>
#define TAG "TYPE4TAG"
NfcCommand nfc_cli_dump_poller_callback_type_4_tag(NfcGenericEvent event, void* context) {
furi_assert(context);
furi_assert(event.protocol == NfcProtocolType4Tag);
furi_assert(event.event_data);
NfcCliDumpContext* instance = context;
const Type4TagPollerEvent* type_4_tag_event = event.event_data;
NfcCommand command = NfcCommandContinue;
if(type_4_tag_event->type == Type4TagPollerEventTypeReadSuccess) {
nfc_device_set_data(
instance->nfc_device, NfcProtocolType4Tag, nfc_poller_get_data(instance->poller));
instance->result = NfcCliDumpErrorNone;
command = NfcCommandStop;
} else if(type_4_tag_event->type == Type4TagPollerEventTypeReadFailed) {
instance->result = NfcCliDumpErrorFailedToRead;
command = NfcCommandReset;
}
if(command == NfcCommandStop) {
furi_semaphore_release(instance->sem_done);
}
return command;
}
@@ -0,0 +1,5 @@
#pragma once
#include "../nfc_cli_dump_common_types.h"
NfcCommand nfc_cli_dump_poller_callback_type_4_tag(NfcGenericEvent event, void* context);
@@ -9,10 +9,12 @@ static const char* protocol_names[NfcProtocolNum] = {
[NfcProtocolFelica] = "FeliCa",
[NfcProtocolMfUltralight] = "Mifare Ultralight",
[NfcProtocolMfClassic] = "Mifare Classic",
[NfcProtocolMfDesfire] = "Mifare DESFire",
[NfcProtocolMfPlus] = "Mifare Plus",
[NfcProtocolMfDesfire] = "Mifare DESFire",
[NfcProtocolSlix] = "Slix",
[NfcProtocolSt25tb] = "St25tb",
[NfcProtocolNtag4xx] = "Ntag4xx",
[NfcProtocolType4Tag] = "Type 4 Tag",
};
const char* nfc_cli_get_protocol_name(NfcProtocol protocol) {
@@ -117,9 +117,12 @@ const NfcCliRawProtocolSpecificHandler nfc_cli_raw_protocol_handlers[] = {
[NfcProtocolFelica] = nfc_cli_raw_felica_handler,
[NfcProtocolMfUltralight] = NULL,
[NfcProtocolMfClassic] = NULL,
[NfcProtocolMfPlus] = NULL,
[NfcProtocolMfDesfire] = NULL,
[NfcProtocolSlix] = NULL,
[NfcProtocolSt25tb] = NULL,
[NfcProtocolNtag4xx] = NULL,
[NfcProtocolType4Tag] = NULL,
};
static NfcCommand nfc_cli_raw_poller_callback(NfcGenericEventEx event, void* context) {
@@ -109,9 +109,76 @@ static void nfc_scene_emulate_on_enter_iso15693_3(NfcApp* instance) {
instance->listener, nfc_scene_emulate_listener_callback_iso15693_3, instance);
}
static NfcCommand
nfc_scene_write_poller_callback_iso15693_3(NfcGenericEvent event, void* context) {
furi_assert(event.protocol == NfcProtocolIso15693_3);
NfcApp* instance = context;
Iso15693_3Poller* poller = event.instance;
const Iso15693_3PollerEvent* iso15693_3_event = event.event_data;
NfcCommand command = NfcCommandContinue;
if(iso15693_3_event->type == Iso15693_3PollerEventTypeReady) {
const Iso15693_3Data* write_data =
nfc_device_get_data(instance->nfc_device, NfcProtocolIso15693_3);
const Iso15693_3Data* card_data = iso15693_3_poller_get_data(poller);
view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventCardDetected);
if(card_data->system_info.block_count != write_data->system_info.block_count ||
card_data->system_info.block_size != write_data->system_info.block_size) {
furi_string_set(
instance->text_box_store, "Incompatible card\n(block count/size\nmismatch)");
view_dispatcher_send_custom_event(
instance->view_dispatcher, NfcCustomEventPollerFailure);
} else {
Iso15693_3Error error = Iso15693_3ErrorNone;
const uint8_t block_size = write_data->system_info.block_size;
const uint8_t* write_block_data =
(const uint8_t*)simple_array_cget_data(write_data->block_data);
const uint8_t* card_block_data =
(const uint8_t*)simple_array_cget_data(card_data->block_data);
for(uint16_t i = 0; i < write_data->system_info.block_count; i++) {
if(iso15693_3_is_block_locked(write_data, i)) continue;
// Skip blocks that already contain the correct data
if(memcmp(
write_block_data + i * block_size,
card_block_data + i * block_size,
block_size) == 0)
continue;
error = iso15693_3_poller_write_block(
poller, write_block_data + i * block_size, i, block_size);
if(error == Iso15693_3ErrorInternal) {
// Block is locked on the target card, skip it
error = Iso15693_3ErrorNone;
continue;
}
if(error != Iso15693_3ErrorNone) break;
}
if(error == Iso15693_3ErrorNone) {
view_dispatcher_send_custom_event(
instance->view_dispatcher, NfcCustomEventPollerSuccess);
} else {
view_dispatcher_send_custom_event(
instance->view_dispatcher, NfcCustomEventPollerFailure);
}
}
command = NfcCommandStop;
}
return command;
}
static void nfc_scene_write_on_enter_iso15693_3(NfcApp* instance) {
furi_string_set(instance->text_box_store, "Apply the\ntarget card now");
instance->poller = nfc_poller_alloc(instance->nfc, NfcProtocolIso15693_3);
nfc_poller_start(instance->poller, nfc_scene_write_poller_callback_iso15693_3, instance);
}
const NfcProtocolSupportBase nfc_protocol_support_iso15693_3 = {
.features = NfcProtocolFeatureEmulateFull | NfcProtocolFeatureEditUid |
NfcProtocolFeatureMoreInfo,
NfcProtocolFeatureMoreInfo | NfcProtocolFeatureWrite,
.scene_info =
{
@@ -155,7 +222,7 @@ const NfcProtocolSupportBase nfc_protocol_support_iso15693_3 = {
},
.scene_write =
{
.on_enter = nfc_protocol_support_common_on_enter_empty,
.on_enter = nfc_scene_write_on_enter_iso15693_3,
.on_event = nfc_protocol_support_common_on_event_empty,
},
};
@@ -14,7 +14,9 @@ enum {
SubmenuIndexUnlock = SubmenuIndexCommonMax,
SubmenuIndexUnlockByReader,
SubmenuIndexUnlockByPassword,
SubmenuIndexDictAttack
SubmenuIndexDictAttack,
SubmenuIndexWriteKeepKey, // ULC: write data pages, keep target card's existing key
SubmenuIndexWriteCopyKey, // ULC: write all pages including key from source card
};
enum {
@@ -214,8 +216,26 @@ static void nfc_scene_read_and_saved_menu_on_enter_mf_ultralight(NfcApp* instanc
if(is_locked ||
(data->type != MfUltralightTypeNTAG213 && data->type != MfUltralightTypeNTAG215 &&
data->type != MfUltralightTypeNTAG216 && data->type != MfUltralightTypeUL11 &&
data->type != MfUltralightTypeUL21 && data->type != MfUltralightTypeOrigin)) {
data->type != MfUltralightTypeUL21 && data->type != MfUltralightTypeOrigin &&
data->type != MfUltralightTypeMfulC)) {
submenu_remove_item(submenu, SubmenuIndexCommonWrite);
} else if(data->type == MfUltralightTypeMfulC) {
// Replace the generic Write item with two ULC-specific options so the user
// can choose whether to keep or overwrite the target card's 3DES key.
// This avoids any mid-write dialog/view-switching complexity entirely.
submenu_remove_item(submenu, SubmenuIndexCommonWrite);
submenu_add_item(
submenu,
"Write (Keep Key)",
SubmenuIndexWriteKeepKey,
nfc_protocol_support_common_submenu_callback,
instance);
submenu_add_item(
submenu,
"Write (Copy Key)",
SubmenuIndexWriteCopyKey,
nfc_protocol_support_common_submenu_callback,
instance);
}
if(is_locked) {
@@ -291,6 +311,14 @@ static bool nfc_scene_read_and_saved_menu_on_event_mf_ultralight(
scene_manager_next_scene(instance->scene_manager, NfcSceneMfUltralightCDictAttack);
}
consumed = true;
} else if(event.event == SubmenuIndexWriteKeepKey) {
instance->mf_ultralight_c_write_context.copy_key = false;
scene_manager_next_scene(instance->scene_manager, NfcSceneWrite);
consumed = true;
} else if(event.event == SubmenuIndexWriteCopyKey) {
instance->mf_ultralight_c_write_context.copy_key = true;
scene_manager_next_scene(instance->scene_manager, NfcSceneWrite);
consumed = true;
}
}
return consumed;
@@ -307,12 +335,139 @@ static NfcCommand
if(mf_ultralight_event->type == MfUltralightPollerEventTypeRequestMode) {
mf_ultralight_event->data->poller_mode = MfUltralightPollerModeWrite;
furi_string_reset(instance->text_box_store);
if(instance->mf_ultralight_c_dict_context.dict) {
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
}
instance->mf_ultralight_c_dict_context.dict = NULL;
instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictIdle;
view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventCardDetected);
} else if(mf_ultralight_event->type == MfUltralightPollerEventTypeAuthRequest) {
// Skip auth during the read phase of write - we'll authenticate
// against the target card in RequestWriteData using source key or dict attack
mf_ultralight_event->data->auth_context.skip_auth = true;
} else if(mf_ultralight_event->type == MfUltralightPollerEventTypeRequestKey) {
// Dict attack key provider - user dict first, then system dict
if(!instance->mf_ultralight_c_dict_context.dict &&
instance->mf_ultralight_c_write_context.dict_state == NfcMfUltralightCWriteDictIdle) {
if(keys_dict_check_presence(NFC_APP_MF_ULTRALIGHT_C_DICT_USER_PATH)) {
instance->mf_ultralight_c_dict_context.dict = keys_dict_alloc(
NFC_APP_MF_ULTRALIGHT_C_DICT_USER_PATH,
KeysDictModeOpenExisting,
sizeof(MfUltralightC3DesAuthKey));
instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictUser;
}
if(!instance->mf_ultralight_c_dict_context.dict) {
instance->mf_ultralight_c_dict_context.dict = keys_dict_alloc(
NFC_APP_MF_ULTRALIGHT_C_DICT_SYSTEM_PATH,
KeysDictModeOpenExisting,
sizeof(MfUltralightC3DesAuthKey));
instance->mf_ultralight_c_write_context.dict_state =
NfcMfUltralightCWriteDictSystem;
}
}
MfUltralightC3DesAuthKey key = {};
bool got_key = false;
if(instance->mf_ultralight_c_dict_context.dict) {
got_key = keys_dict_get_next_key(
instance->mf_ultralight_c_dict_context.dict,
key.data,
sizeof(MfUltralightC3DesAuthKey));
}
if(!got_key &&
instance->mf_ultralight_c_write_context.dict_state == NfcMfUltralightCWriteDictUser) {
// Exhausted user dict, switch to system dict
if(instance->mf_ultralight_c_dict_context.dict) {
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
}
instance->mf_ultralight_c_dict_context.dict = keys_dict_alloc(
NFC_APP_MF_ULTRALIGHT_C_DICT_SYSTEM_PATH,
KeysDictModeOpenExisting,
sizeof(MfUltralightC3DesAuthKey));
instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictSystem;
if(instance->mf_ultralight_c_dict_context.dict) {
got_key = keys_dict_get_next_key(
instance->mf_ultralight_c_dict_context.dict,
key.data,
sizeof(MfUltralightC3DesAuthKey));
}
}
if(got_key) {
mf_ultralight_event->data->key_request_data.key = key;
mf_ultralight_event->data->key_request_data.key_provided = true;
FURI_LOG_D(
"MfULC",
"Trying dict key: "
"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
key.data[0],
key.data[1],
key.data[2],
key.data[3],
key.data[4],
key.data[5],
key.data[6],
key.data[7],
key.data[8],
key.data[9],
key.data[10],
key.data[11],
key.data[12],
key.data[13],
key.data[14],
key.data[15]);
} else {
mf_ultralight_event->data->key_request_data.key_provided = false;
FURI_LOG_D("MfULC", "Dict exhausted - no more keys");
if(instance->mf_ultralight_c_dict_context.dict) {
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
instance->mf_ultralight_c_dict_context.dict = NULL;
}
instance->mf_ultralight_c_write_context.dict_state =
NfcMfUltralightCWriteDictExhausted;
}
} else if(mf_ultralight_event->type == MfUltralightPollerEventTypeRequestWriteData) {
mf_ultralight_event->data->write_data =
nfc_device_get_data(instance->nfc_device, NfcProtocolMfUltralight);
// Reset dict context so RequestKey starts fresh for the write-phase auth
if(instance->mf_ultralight_c_dict_context.dict) {
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
instance->mf_ultralight_c_dict_context.dict = NULL;
}
instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictIdle;
} else if(mf_ultralight_event->type == MfUltralightPollerEventTypeWriteKeyRequest) {
// Apply the user's key choice - read from static, not scene state (scene manager
// resets state to 0 on scene entry, wiping any value set before next_scene).
bool keep_key = !instance->mf_ultralight_c_write_context.copy_key;
mf_ultralight_event->data->write_key_skip = keep_key;
if(mf_ultralight_event->data->key_request_data.key_provided) {
MfUltralightC3DesAuthKey found_key = mf_ultralight_event->data->key_request_data.key;
FURI_LOG_D(
"MfULC",
"WriteKeyRequest: target key = "
"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
found_key.data[0],
found_key.data[1],
found_key.data[2],
found_key.data[3],
found_key.data[4],
found_key.data[5],
found_key.data[6],
found_key.data[7],
found_key.data[8],
found_key.data[9],
found_key.data[10],
found_key.data[11],
found_key.data[12],
found_key.data[13],
found_key.data[14],
found_key.data[15]);
}
FURI_LOG_D(
"MfULC",
"WriteKeyRequest: decision = %s (copy_key=%d)",
keep_key ? "KEEP target key (pages 44-47 NOT written)" :
"OVERWRITE with source key (pages 44-47 WILL be written)",
(int)instance->mf_ultralight_c_write_context.copy_key);
} else if(mf_ultralight_event->type == MfUltralightPollerEventTypeCardMismatch) {
furi_string_set(instance->text_box_store, "Card of the same\ntype should be\n presented");
view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventWrongCard);
@@ -323,6 +478,7 @@ static NfcCommand
view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventPollerFailure);
command = NfcCommandStop;
} else if(mf_ultralight_event->type == MfUltralightPollerEventTypeWriteFail) {
view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventPollerFailure);
command = NfcCommandStop;
} else if(mf_ultralight_event->type == MfUltralightPollerEventTypeWriteSuccess) {
furi_string_reset(instance->text_box_store);
@@ -334,9 +490,18 @@ static NfcCommand
}
static void nfc_scene_write_on_enter_mf_ultralight(NfcApp* instance) {
// Free any dict the write callback opened (dict_state != Idle means we own it).
// After a DictAttack scene, on_exit now NULLs the pointer so a simple NULL check
// is safe here too — but the state enum is the authoritative ownership record.
if(instance->mf_ultralight_c_write_context.dict_state != NfcMfUltralightCWriteDictIdle &&
instance->mf_ultralight_c_dict_context.dict) {
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
}
instance->mf_ultralight_c_dict_context.dict = NULL;
instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictIdle;
furi_string_set(instance->text_box_store, "\nApply the\ntarget\ncard now");
instance->poller = nfc_poller_alloc(instance->nfc, NfcProtocolMfUltralight);
nfc_poller_start(instance->poller, nfc_scene_write_poller_callback_mf_ultralight, instance);
furi_string_set(instance->text_box_store, "Apply the initial\ncard only");
}
const NfcProtocolSupportBase nfc_protocol_support_mf_ultralight = {
@@ -9,6 +9,10 @@
#include "../nfc_protocol_support_common.h"
#include "../nfc_protocol_support_gui_common.h"
#include <string.h>
#define TAG "SlixWrite"
static void nfc_scene_info_on_enter_slix(NfcApp* instance) {
const NfcDevice* device = instance->nfc_device;
const SlixData* data = nfc_device_get_data(device, NfcProtocolSlix);
@@ -106,8 +110,104 @@ static void nfc_scene_emulate_on_enter_slix(NfcApp* instance) {
nfc_listener_start(instance->listener, nfc_scene_emulate_listener_callback_slix, instance);
}
static NfcCommand nfc_scene_write_poller_callback_slix(NfcGenericEvent event, void* context) {
furi_assert(event.protocol == NfcProtocolSlix);
NfcApp* instance = context;
SlixPoller* poller = event.instance;
const SlixPollerEvent* slix_event = event.event_data;
NfcCommand command = NfcCommandContinue;
if(slix_event->type == SlixPollerEventTypeReady) {
const SlixData* write_data = nfc_device_get_data(instance->nfc_device, NfcProtocolSlix);
const Iso15693_3Data* write_iso_data = slix_get_base_data(write_data);
const Iso15693_3Data* card_iso_data =
slix_get_base_data((const SlixData*)nfc_poller_get_data(instance->poller));
view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventCardDetected);
if(card_iso_data->system_info.block_count != write_iso_data->system_info.block_count ||
card_iso_data->system_info.block_size != write_iso_data->system_info.block_size) {
furi_string_set(
instance->text_box_store, "Incompatible card\n(block count/size\nmismatch)");
view_dispatcher_send_custom_event(
instance->view_dispatcher, NfcCustomEventPollerFailure);
} else {
SlixError error = SlixErrorNone;
uint16_t failed_block = 0;
const uint8_t block_size = write_iso_data->system_info.block_size;
const uint8_t* write_data_ptr =
(const uint8_t*)simple_array_cget_data(write_iso_data->block_data);
const uint8_t* card_data_ptr =
(const uint8_t*)simple_array_cget_data(card_iso_data->block_data);
for(uint16_t i = 0; i < write_iso_data->system_info.block_count; i++) {
if(iso15693_3_is_block_locked(write_iso_data, i)) continue;
// Skip blocks that already contain the correct data
if(memcmp(
write_data_ptr + i * block_size,
card_data_ptr + i * block_size,
block_size) == 0)
continue;
error = slix_poller_write_block(
poller, write_data_ptr + i * block_size, i, block_size);
if(error == SlixErrorInternal) {
// Block is locked on the target card, skip it
error = SlixErrorNone;
continue;
}
if(error != SlixErrorNone) {
failed_block = i;
FURI_LOG_E(TAG, "Write failed: block %u, error %d", failed_block, (int)error);
break;
}
}
if(error == SlixErrorNone) {
view_dispatcher_send_custom_event(
instance->view_dispatcher, NfcCustomEventPollerSuccess);
} else {
const char* err_name;
switch(error) {
case SlixErrorTimeout:
err_name = "Timeout";
break;
case SlixErrorFormat:
err_name = "BadCRC";
break;
case SlixErrorNotSupported:
err_name = "NotSupported";
break;
case SlixErrorInternal:
err_name = "Locked";
break;
case SlixErrorWrongPassword:
err_name = "WrongPwd";
break;
default:
err_name = "Unknown";
break;
}
furi_string_printf(
instance->text_box_store, "Block %u: %s", failed_block, err_name);
view_dispatcher_send_custom_event(
instance->view_dispatcher, NfcCustomEventPollerFailure);
}
}
command = NfcCommandStop;
}
return command;
}
static void nfc_scene_write_on_enter_slix(NfcApp* instance) {
furi_string_set(instance->text_box_store, "Apply the\ntarget card now");
instance->poller = nfc_poller_alloc(instance->nfc, NfcProtocolSlix);
nfc_poller_start(instance->poller, nfc_scene_write_poller_callback_slix, instance);
}
const NfcProtocolSupportBase nfc_protocol_support_slix = {
.features = NfcProtocolFeatureEmulateFull | NfcProtocolFeatureMoreInfo,
.features = NfcProtocolFeatureEmulateFull | NfcProtocolFeatureMoreInfo |
NfcProtocolFeatureWrite,
.scene_info =
{
@@ -151,7 +251,7 @@ const NfcProtocolSupportBase nfc_protocol_support_slix = {
},
.scene_write =
{
.on_enter = nfc_protocol_support_common_on_enter_empty,
.on_enter = nfc_scene_write_on_enter_slix,
.on_event = nfc_protocol_support_common_on_event_empty,
},
};
+13
View File
@@ -126,6 +126,18 @@ typedef struct {
size_t dict_keys_current;
} NfcMfUltralightCDictContext;
typedef enum {
NfcMfUltralightCWriteDictIdle, /**< No dict open; safe to open either dict. */
NfcMfUltralightCWriteDictUser, /**< User dict currently open. */
NfcMfUltralightCWriteDictSystem, /**< System dict currently open. */
NfcMfUltralightCWriteDictExhausted, /**< All dicts tried; do not re-open. */
} NfcMfUltralightCWriteDictState;
typedef struct {
bool copy_key; /**< True = overwrite target 3DES key with source key pages. */
NfcMfUltralightCWriteDictState dict_state; /**< Which dict is open for write-phase auth. */
} NfcMfUltralightCWriteContext;
struct NfcApp {
DialogsApp* dialogs;
Storage* storage;
@@ -165,6 +177,7 @@ struct NfcApp {
SlixUnlock* slix_unlock;
NfcMfClassicDictAttackContext nfc_dict_context;
NfcMfUltralightCDictContext mf_ultralight_c_dict_context;
NfcMfUltralightCWriteContext mf_ultralight_c_write_context;
Mfkey32Logger* mfkey32_logger;
MfUserDict* mf_user_dict;
MfClassicKeyCache* mfc_key_cache;
@@ -1,23 +1,21 @@
//Based on parsers written by Leptoptilos and Assasinfil. Also, thanks to WillyJL (<me@willyjl.dev>) for help!
#include "nfc_supported_card_plugin.h"
#include <flipper_application/flipper_application.h>
#include <nfc/nfc_device.h>
#include <bit_lib/bit_lib.h>
#include <datetime.h>
#include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
#define TAG "Plantain"
void from_minutes_to_datetime(uint32_t minutes, DateTime* datetime, uint16_t start_year) {
uint32_t timestamp = minutes * 60;
DateTime start_datetime = {0};
start_datetime.year = start_year - 1;
start_datetime.month = 12;
start_datetime.day = 31;
timestamp += datetime_datetime_to_timestamp(&start_datetime);
datetime_timestamp_to_datetime(timestamp, datetime);
}
#include <flipper_format/flipper_format.h>
#define PLANTAIN_EPOCH_START 1262304000 //2010-01-01
#define PPK_WHOLE_EPOCH_START 946684800 //2000-01-01
#define PPK_CURRENT_EPOCH_START 1388534400 //2014-01-01
#define SECONDS_IN_A_DAY 86400
#define SECONDS_IN_A_MINUTE 60
#define FIRST_PPK_TICKET_OFFSET 101
#define FIRST_TICKET_VALUE_BLOCK 104
#define SECOND_PPK_TICKET_OFFSET 102
#define SECOND_TICKET_VALUE_BLOCK 108
typedef struct {
uint64_t a;
@@ -30,79 +28,549 @@ typedef struct {
} PlantainCardConfig;
static const MfClassicKeyPair plantain_1k_keys[] = {
{.a = 0xffffffffffff, .b = 0xffffffffffff},
{.a = 0xffffffffffff, .b = 0xffffffffffff},
{.a = 0xffffffffffff, .b = 0xffffffffffff},
{.a = 0xffffffffffff, .b = 0xffffffffffff},
{.a = 0xe56ac127dd45, .b = 0x19fc84a3784b},
{.a = 0x77dabc9825e1, .b = 0x9764fec3154a},
{.a = 0xffffffffffff, .b = 0xffffffffffff},
{.a = 0xffffffffffff, .b = 0xffffffffffff},
{.a = 0x26973ea74321, .b = 0xd27058c6e2c7},
{.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3},
{.a = 0xea0fd73cb149, .b = 0x29c35fa068fb},
{.a = 0xc76bf71a2509, .b = 0x9ba241db3f56},
{.a = 0xacffffffffff, .b = 0x71f3a315ad26},
{.a = 0xffffffffffff, .b = 0xffffffffffff},
{.a = 0xffffffffffff, .b = 0xffffffffffff},
{.a = 0xffffffffffff, .b = 0xffffffffffff},
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //0
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //1
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //2
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //3
{.a = 0xe56ac127dd45, .b = 0x19fc84a3784b}, //4
{.a = 0x77dabc9825e1, .b = 0x9764fec3154a}, //5
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //6
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //7
{.a = 0x26973ea74321, .b = 0xd27058c6e2c7}, //8
{.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3}, //9
{.a = 0xea0fd73cb149, .b = 0x29c35fa068fb}, //10
{.a = 0xc76bf71a2509, .b = 0x9ba241db3f56}, //11
{.a = 0xacffffffffff, .b = 0x71f3a315ad26}, //12
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //13
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //14
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //15
{.a = 0x72f96bdd3714, .b = 0x462225cd34cf}, //16
{.a = 0x044ce1872bc3, .b = 0x8c90c70cff4a}, //17
{.a = 0xbc2d1791dec1, .b = 0xca96a487de0b}, //18
{.a = 0x8791b2ccb5c4, .b = 0xc956c3b80da3}, //19
{.a = 0x8e26e45e7d65, .b = 0x8e65b3af7d22}, //20
{.a = 0x0f318130ed18, .b = 0x0c420a20e056}, //21
{.a = 0x045ceca15535, .b = 0x31bec3d9e510}, //22
{.a = 0x9d993c5d4ef4, .b = 0x86120e488abf}, //23
{.a = 0xc65d4eaa645b, .b = 0xb69d40d1a439}, //24
{.a = 0x3a8a139c20b4, .b = 0x8818a9c5d406}, //25
{.a = 0xbaff3053b496, .b = 0x4b7cb25354d3}, //26
{.a = 0x7413b599c4ea, .b = 0xb0a2aaf3a1ba}, //27
{.a = 0x7413b599c4ea, .b = 0xb0a2aaf3a1ba}, //28
{.a = 0x0ce7cd2cc72b, .b = 0xfa1fbb3f0f1f}, //29
{.a = 0x0eb23cc8110b, .b = 0x04dc35277635}, //30
{.a = 0xbc4580b7f20b, .b = 0xd0a4131fb290}, //31
};
static const MfClassicKeyPair plantain_4k_keys[] = {
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
{.a = 0xe56ac127dd45, .b = 0x19fc84a3784b}, {.a = 0x77dabc9825e1, .b = 0x9764fec3154a},
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
{.a = 0x26973ea74321, .b = 0xd27058c6e2c7}, {.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3},
{.a = 0xea0fd73cb149, .b = 0x29c35fa068fb}, {.a = 0xc76bf71a2509, .b = 0x9ba241db3f56},
{.a = 0xacffffffffff, .b = 0x71f3a315ad26}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
{.a = 0x72f96bdd3714, .b = 0x462225cd34cf}, {.a = 0x044ce1872bc3, .b = 0x8c90c70cff4a},
{.a = 0xbc2d1791dec1, .b = 0xca96a487de0b}, {.a = 0x8791b2ccb5c4, .b = 0xc956c3b80da3},
{.a = 0x8e26e45e7d65, .b = 0x8e65b3af7d22}, {.a = 0x0f318130ed18, .b = 0x0c420a20e056},
{.a = 0x045ceca15535, .b = 0x31bec3d9e510}, {.a = 0x9d993c5d4ef4, .b = 0x86120e488abf},
{.a = 0xc65d4eaa645b, .b = 0xb69d40d1a439}, {.a = 0x3a8a139c20b4, .b = 0x8818a9c5d406},
{.a = 0xbaff3053b496, .b = 0x4b7cb25354d3}, {.a = 0x7413b599c4ea, .b = 0xb0a2AAF3A1BA},
{.a = 0x0ce7cd2cc72b, .b = 0xfa1fbb3f0f1f}, {.a = 0x0be5fac8b06a, .b = 0x6f95887a4fd3},
{.a = 0x0eb23cc8110b, .b = 0x04dc35277635}, {.a = 0xbc4580b7f20b, .b = 0xd0a4131fb290},
{.a = 0x7a396f0d633d, .b = 0xad2bdc097023}, {.a = 0xa3faa6daff67, .b = 0x7600e889adf9},
{.a = 0xfd8705e721b0, .b = 0x296fc317a513}, {.a = 0x22052b480d11, .b = 0xe19504c39461},
{.a = 0xa7141147d430, .b = 0xff16014fefc7}, {.a = 0x8a8d88151a00, .b = 0x038b5f9b5a2a},
{.a = 0xb27addfb64b0, .b = 0x152fd0c420a7}, {.a = 0x7259fa0197c6, .b = 0x5583698df085},
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //0
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //1
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //2
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //3
{.a = 0xe56ac127dd45, .b = 0x19fc84a3784b}, //4
{.a = 0x77dabc9825e1, .b = 0x9764fec3154a}, //5
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //6
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //7
{.a = 0x26973ea74321, .b = 0xd27058c6e2c7}, //8
{.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3}, //9
{.a = 0xea0fd73cb149, .b = 0x29c35fa068fb}, //10
{.a = 0xc76bf71a2509, .b = 0x9ba241db3f56}, //11
{.a = 0xacffffffffff, .b = 0x71f3a315ad26}, //12
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //13
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //14
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //15
{.a = 0x72f96bdd3714, .b = 0x462225cd34cf}, //16
{.a = 0x044ce1872bc3, .b = 0x8c90c70cff4a}, //17
{.a = 0xbc2d1791dec1, .b = 0xca96a487de0b}, //18
{.a = 0x8791b2ccb5c4, .b = 0xc956c3b80da3}, //19
{.a = 0x8e26e45e7d65, .b = 0x8e65b3af7d22}, //20
{.a = 0x0f318130ed18, .b = 0x0c420a20e056}, //21
{.a = 0x045ceca15535, .b = 0x31bec3d9e510}, //22
{.a = 0x9d993c5d4ef4, .b = 0x86120e488abf}, //23
{.a = 0xc65d4eaa645b, .b = 0xb69d40d1a439}, //24
{.a = 0x3a8a139c20b4, .b = 0x8818a9c5d406}, //25
{.a = 0xbaff3053b496, .b = 0x4b7cb25354d3}, //26
{.a = 0x7413b599c4ea, .b = 0xb0a2AAF3A1BA}, //27
{.a = 0x0ce7cd2cc72b, .b = 0xfa1fbb3f0f1f}, //28
{.a = 0x0be5fac8b06a, .b = 0x6f95887a4fd3}, //29
{.a = 0x0eb23cc8110b, .b = 0x04dc35277635}, //30
{.a = 0xbc4580b7f20b, .b = 0xd0a4131fb290}, //31
{.a = 0x7a396f0d633d, .b = 0xad2bdc097023}, //32
{.a = 0xa3faa6daff67, .b = 0x7600e889adf9}, //33
{.a = 0xfd8705e721b0, .b = 0x296fc317a513}, //34
{.a = 0x22052b480d11, .b = 0xe19504c39461}, //35
{.a = 0xa7141147d430, .b = 0xff16014fefc7}, //36
{.a = 0x8a8d88151a00, .b = 0x038b5f9b5a2a}, //37
{.a = 0xb27addfb64b0, .b = 0x152fd0c420a7}, //38
{.a = 0x7259fa0197c6, .b = 0x5583698df085}, //39
};
static const MfClassicKeyPair plantain_4k_keys_legacy[] = {
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
{.a = 0xe56ac127dd45, .b = 0x19fc84a3784b}, {.a = 0x77dabc9825e1, .b = 0x9764fec3154a},
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
{.a = 0x26973ea74321, .b = 0xd27058c6e2c7}, {.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3},
{.a = 0xea0fd73cb149, .b = 0x29c35fa068fb}, {.a = 0xc76bf71a2509, .b = 0x9ba241db3f56},
{.a = 0xacffffffffff, .b = 0x71f3a315ad26}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
{.a = 0x72f96bdd3714, .b = 0x462225cd34cf}, {.a = 0x044ce1872bc3, .b = 0x8c90c70cff4a},
{.a = 0xbc2d1791dec1, .b = 0xca96a487de0b}, {.a = 0x8791b2ccb5c4, .b = 0xc956c3b80da3},
{.a = 0x8e26e45e7d65, .b = 0x8e65b3af7d22}, {.a = 0x0f318130ed18, .b = 0x0c420a20e056},
{.a = 0x045ceca15535, .b = 0x31bec3d9e510}, {.a = 0x9d993c5d4ef4, .b = 0x86120e488abf},
{.a = 0xc65d4eaa645b, .b = 0xb69d40d1a439}, {.a = 0x46d78e850a7e, .b = 0xa470f8130991},
{.a = 0x42e9b54e51ab, .b = 0x0231b86df52e}, {.a = 0x0f01ceff2742, .b = 0x6fec74559ca7},
{.a = 0xb81f2b0c2f66, .b = 0xa7e2d95f0003}, {.a = 0x9ea3387a63c1, .b = 0x437e59f57561},
{.a = 0x0eb23cc8110b, .b = 0x04dc35277635}, {.a = 0xbc4580b7f20b, .b = 0xd0a4131fb290},
{.a = 0x7a396f0d633d, .b = 0xad2bdc097023}, {.a = 0xa3faa6daff67, .b = 0x7600e889adf9},
{.a = 0xfd8705e721b0, .b = 0x296fc317a513}, {.a = 0x22052b480d11, .b = 0xe19504c39461},
{.a = 0xa7141147d430, .b = 0xff16014fefc7}, {.a = 0x8a8d88151a00, .b = 0x038b5f9b5a2a},
{.a = 0xb27addfb64b0, .b = 0x152fd0c420a7}, {.a = 0x7259fa0197c6, .b = 0x5583698df085},
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //0
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //1
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //2
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //3
{.a = 0xe56ac127dd45, .b = 0x19fc84a3784b}, //4
{.a = 0x77dabc9825e1, .b = 0x9764fec3154a}, //5
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //6
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //7
{.a = 0x26973ea74321, .b = 0xd27058c6e2c7}, //8
{.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3}, //9
{.a = 0xea0fd73cb149, .b = 0x29c35fa068fb}, //10
{.a = 0xc76bf71a2509, .b = 0x9ba241db3f56}, //11
{.a = 0xacffffffffff, .b = 0x71f3a315ad26}, //12
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //13
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //14
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //15
{.a = 0x72f96bdd3714, .b = 0x462225cd34cf}, //16
{.a = 0x044ce1872bc3, .b = 0x8c90c70cff4a}, //17
{.a = 0xbc2d1791dec1, .b = 0xca96a487de0b}, //18
{.a = 0x8791b2ccb5c4, .b = 0xc956c3b80da3}, //19
{.a = 0x8e26e45e7d65, .b = 0x8e65b3af7d22}, //20
{.a = 0x0f318130ed18, .b = 0x0c420a20e056}, //21
{.a = 0x045ceca15535, .b = 0x31bec3d9e510}, //22
{.a = 0x9d993c5d4ef4, .b = 0x86120e488abf}, //23
{.a = 0xc65d4eaa645b, .b = 0xb69d40d1a439}, //24
{.a = 0x46d78e850a7e, .b = 0xa470f8130991}, //25
{.a = 0x42e9b54e51ab, .b = 0x0231b86df52e}, //26
{.a = 0x0f01ceff2742, .b = 0x6fec74559ca7}, //27
{.a = 0xb81f2b0c2f66, .b = 0xa7e2d95f0003}, //28
{.a = 0x9ea3387a63c1, .b = 0x437e59f57561}, //29
{.a = 0x0eb23cc8110b, .b = 0x04dc35277635}, //30
{.a = 0xbc4580b7f20b, .b = 0xd0a4131fb290}, //31
{.a = 0x7a396f0d633d, .b = 0xad2bdc097023}, //32
{.a = 0xa3faa6daff67, .b = 0x7600e889adf9}, //33
{.a = 0xfd8705e721b0, .b = 0x296fc317a513}, //34
{.a = 0x22052b480d11, .b = 0xe19504c39461}, //35
{.a = 0xa7141147d430, .b = 0xff16014fefc7}, //36
{.a = 0x8a8d88151a00, .b = 0x038b5f9b5a2a}, //37
{.a = 0xb27addfb64b0, .b = 0x152fd0c420a7}, //38
{.a = 0x7259fa0197c6, .b = 0x5583698df085}, //39
};
typedef struct {
uint16_t departure_uic;
uint16_t destination_uic;
uint16_t trip_start_uic;
uint16_t trip_end_uic;
FuriString* departure_name;
FuriString* destination_name;
FuriString* trip_start_sta_name;
FuriString* trip_end_sta_name;
uint8_t value_data;
uint8_t direction;
uint8_t current_status;
uint16_t valid_from_data;
uint16_t valid_till_data;
DateTime valid_from;
DateTime valid_till;
uint32_t tap_data;
DateTime tap_time;
uint8_t first_ticket_marker;
uint8_t second_ticket_marker;
uint64_t sys_n;
uint8_t ppk_cnt;
} PPKData;
typedef struct {
uint64_t card_number;
FuriString* card_number_str;
uint32_t balance;
uint8_t trips_metro;
uint8_t trips_ground;
uint32_t last_trip_data;
DateTime last_trip_time;
uint16_t validator;
uint16_t fare;
uint32_t last_payment_date_data;
DateTime last_payment_date;
uint16_t last_payment_amount;
uint8_t keyset;
} PlantainData;
// Function to map UIC codes to station names
static inline void sz_uic_to_sta(Storage* storage, const char* file_name, PPKData* ticket) {
FlipperFormat* file = flipper_format_file_alloc(storage);
FuriString* departure_uic = furi_string_alloc_printf("%04X", ticket->departure_uic);
FuriString* destination_uic = furi_string_alloc_printf("%04X", ticket->destination_uic);
FuriString* trip_start_uic = furi_string_alloc_printf("%04X", ticket->trip_start_uic);
FuriString* trip_end_uic = furi_string_alloc_printf("%04X", ticket->trip_end_uic);
if(flipper_format_file_open_existing(file, file_name)) {
flipper_format_read_string(
file, furi_string_get_cstr(departure_uic), ticket->departure_name);
flipper_format_rewind(file);
flipper_format_read_string(
file, furi_string_get_cstr(destination_uic), ticket->destination_name);
flipper_format_rewind(file);
flipper_format_read_string(
file, furi_string_get_cstr(trip_start_uic), ticket->trip_start_sta_name);
flipper_format_rewind(file);
flipper_format_read_string(
file, furi_string_get_cstr(trip_end_uic), ticket->trip_end_sta_name);
}
flipper_format_free(file);
furi_string_free(departure_uic);
furi_string_free(destination_uic);
furi_string_free(trip_start_uic);
furi_string_free(trip_end_uic);
}
// Function to resolve station names for a ticket, and if not found, set to "1E" + UIC code
static void resolve_station_name(Storage* storage, PPKData* ticket) {
sz_uic_to_sta(storage, EXT_PATH("nfc/assets/sz_id.nfc"), ticket);
if(furi_string_utf8_length(ticket->departure_name) <= 2) {
furi_string_printf(ticket->departure_name, "1E%04X", ticket->departure_uic);
}
if(furi_string_utf8_length(ticket->destination_name) <= 2) {
furi_string_printf(ticket->destination_name, "1E%04X", ticket->destination_uic);
}
if(furi_string_utf8_length(ticket->trip_start_sta_name) <= 2) {
furi_string_printf(ticket->trip_start_sta_name, "1E%04X", ticket->trip_start_uic);
}
if(furi_string_utf8_length(ticket->trip_end_sta_name) <= 2) {
furi_string_printf(ticket->trip_end_sta_name, "1E%04X", ticket->trip_end_uic);
}
}
// Function to extract plantain purse data from the card data
static inline void extract_purse_data(
const MfClassicData* data,
PlantainData* purse,
PPKData* ticket,
uint8_t first_ppk_ticket_offset,
uint8_t second_ppk_ticket_offset) {
uint64_t card_number = 0;
size_t uid_len = 0;
const uint8_t* uid = mf_classic_get_uid(data, &uid_len);
purse->card_number_str = furi_string_alloc();
const uint8_t* temp_ptr = &uid[0];
uint8_t card_number_tmp[uid_len];
if(uid_len == 4) {
for(size_t i = 0; i < 4; i++) {
card_number_tmp[i] = temp_ptr[3 - i];
}
} else if(uid_len == 7) {
for(size_t i = 0; i < 7; i++) {
card_number_tmp[i] = temp_ptr[6 - i];
}
} else {
return;
}
for(size_t i = 0; i < uid_len; i++) {
card_number = (card_number << 8) | card_number_tmp[i];
}
FuriString* card_number_s = furi_string_alloc();
furi_string_cat_printf(card_number_s, "%lld", card_number);
FuriString* tmp_s = furi_string_alloc_set_str("9643 3078 ");
for(uint8_t i = 0; i < 24; i += 4) {
for(uint8_t j = 0; j < 4; j++) {
furi_string_push_back(tmp_s, furi_string_get_char(card_number_s, i + j));
}
furi_string_push_back(tmp_s, ' ');
}
furi_string_set(purse->card_number_str, furi_string_get_cstr(tmp_s));
furi_string_free(card_number_s);
furi_string_free(tmp_s);
if(data->type == MfClassicType1k) {
uint32_t balance = 0;
for(uint8_t i = 0; i < 4; i++)
balance = (balance << 8) | data->block[16].data[3 - i];
balance /= 100;
purse->balance = balance;
purse->trips_metro = data->block[21].data[0];
purse->trips_ground = data->block[21].data[1];
uint32_t last_trip_timestamp = 0;
for(uint8_t i = 0; i < 3; i++) {
last_trip_timestamp = (last_trip_timestamp << 8) | data->block[21].data[4 - i];
}
for(uint8_t i = 0; i < 3; i++) {
purse->last_trip_data = (purse->last_trip_data << 8) | data->block[21].data[4 - i];
}
purse->validator = (data->block[20].data[5] << 8) | data->block[20].data[4];
uint16_t fare = ((data->block[20].data[7] << 8) | data->block[20].data[6]) / 100;
purse->fare = fare;
for(uint8_t i = 0; i < 3; i++) {
purse->last_payment_date_data = (purse->last_payment_date_data << 8) |
data->block[18].data[4 - i];
}
purse->last_payment_amount = ((data->block[18].data[10] << 16) |
(data->block[18].data[9] << 8) | (data->block[18].data[8])) /
100;
last_trip_timestamp = PLANTAIN_EPOCH_START + purse->last_trip_data * SECONDS_IN_A_MINUTE;
const uint32_t last_payment_timestamp =
PLANTAIN_EPOCH_START + purse->last_payment_date_data * SECONDS_IN_A_MINUTE;
datetime_timestamp_to_datetime(last_trip_timestamp, &purse->last_trip_time);
datetime_timestamp_to_datetime(last_payment_timestamp, &purse->last_payment_date);
} else if(data->type == MfClassicType4k) {
uint32_t balance = 0;
for(uint8_t i = 0; i < 4; i++)
balance = (balance << 8) | data->block[16].data[3 - i];
balance /= 100;
purse->balance = balance;
purse->trips_metro = data->block[21].data[0];
purse->trips_ground = data->block[21].data[1];
for(uint8_t i = 0; i < 3; i++) {
purse->last_trip_data = (purse->last_trip_data << 8) | data->block[21].data[4 - i];
}
purse->validator = (data->block[20].data[5] << 8) | data->block[20].data[4];
uint16_t fare = ((data->block[20].data[7] << 8) | data->block[20].data[6]) / 100;
purse->fare = fare;
for(uint8_t i = 0; i < 3; i++) {
purse->last_payment_date_data = (purse->last_payment_date_data << 8) |
data->block[18].data[4 - i];
}
purse->last_payment_amount = ((data->block[18].data[10] << 16) |
(data->block[18].data[9] << 8) | (data->block[18].data[8])) /
100;
uint32_t last_trip_timestamp =
PLANTAIN_EPOCH_START + purse->last_trip_data * SECONDS_IN_A_MINUTE;
const uint32_t last_payment_timestamp =
PLANTAIN_EPOCH_START + purse->last_payment_date_data * SECONDS_IN_A_MINUTE;
datetime_timestamp_to_datetime(last_trip_timestamp, &purse->last_trip_time);
datetime_timestamp_to_datetime(last_payment_timestamp, &purse->last_payment_date);
}
ticket->first_ticket_marker = data->block[first_ppk_ticket_offset].data[0];
ticket->second_ticket_marker = data->block[second_ppk_ticket_offset].data[0];
}
// Function to extract PPK ticket data from the card data
static inline void extract_ppk_data(
Storage* storage,
const MfClassicData* data,
PPKData* ticket,
bool second_ticket) {
const uint8_t* temp_ptr = &data->block[SECOND_TICKET_VALUE_BLOCK + 2].data[0];
uint8_t sys_n_arr[6] = {0};
if(second_ticket == 0) {
for(size_t i = 0; i < 6; i++) {
sys_n_arr[i] = temp_ptr[7 - i];
}
ticket->departure_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[6] << 8) |
(data->block[FIRST_PPK_TICKET_OFFSET].data[5]);
ticket->destination_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[9] << 8) |
(data->block[FIRST_PPK_TICKET_OFFSET].data[8]);
ticket->value_data = data->block[FIRST_TICKET_VALUE_BLOCK].data[0];
ticket->current_status = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[8];
ticket->valid_from_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[2] << 8) |
(data->block[FIRST_PPK_TICKET_OFFSET].data[1]);
ticket->valid_till_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[4] << 8) |
(data->block[FIRST_PPK_TICKET_OFFSET].data[3]);
ticket->direction = data->block[FIRST_PPK_TICKET_OFFSET].data[14];
ticket->tap_data = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[0];
ticket->ppk_cnt = data->block[105].data[10];
ticket->trip_start_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[3]);
ticket->trip_end_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[6]);
} else if(second_ticket == 1) {
for(size_t i = 0; i < 6; i++) {
sys_n_arr[i] = temp_ptr[13 - i];
}
ticket->departure_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[6] << 8) |
(data->block[SECOND_PPK_TICKET_OFFSET].data[5]);
ticket->destination_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[9] << 8) |
(data->block[SECOND_PPK_TICKET_OFFSET].data[8]);
ticket->value_data = data->block[SECOND_TICKET_VALUE_BLOCK].data[0];
ticket->current_status = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[8];
ticket->valid_from_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[2] << 8) |
(data->block[SECOND_PPK_TICKET_OFFSET].data[1]);
ticket->valid_till_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[4] << 8) |
(data->block[SECOND_PPK_TICKET_OFFSET].data[3]);
ticket->direction = data->block[SECOND_PPK_TICKET_OFFSET].data[14];
ticket->tap_data = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[0];
ticket->ppk_cnt = data->block[109].data[10];
ticket->trip_start_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[3]);
ticket->trip_end_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[6]);
}
ticket->first_ticket_marker = data->block[FIRST_PPK_TICKET_OFFSET].data[0];
ticket->second_ticket_marker = data->block[SECOND_PPK_TICKET_OFFSET].data[0];
uint64_t sys_n = 0;
for(size_t i = 0; i < 6; i++) {
sys_n = (sys_n << 8) | sys_n_arr[i];
}
ticket->sys_n = sys_n;
const uint32_t valid_from_timestamp =
PPK_WHOLE_EPOCH_START + ticket->valid_from_data * SECONDS_IN_A_DAY;
const uint32_t valid_till_timestamp =
PPK_WHOLE_EPOCH_START + ticket->valid_till_data * SECONDS_IN_A_DAY;
const uint32_t tap_timestamp =
PPK_CURRENT_EPOCH_START + ticket->tap_data * SECONDS_IN_A_MINUTE;
datetime_timestamp_to_datetime(valid_from_timestamp, &ticket->valid_from);
datetime_timestamp_to_datetime(valid_till_timestamp, &ticket->valid_till);
datetime_timestamp_to_datetime(tap_timestamp, &ticket->tap_time);
resolve_station_name(storage, ticket);
}
// Function to format and print plantain purse data
static void printf_plantain_data(FuriString* parsed_data, PlantainData* purse) {
furi_string_printf(parsed_data, "\e#Plantain Card");
furi_string_cat_printf(
parsed_data,
"\nNumber: %s\nBalance: %ld RUB\nMetro Trips: %d\nGround Trips: %d\nLast Trip: %02d.%02d.%04d %02d:%02d",
furi_string_get_cstr(purse->card_number_str),
purse->balance,
purse->trips_metro,
purse->trips_ground,
purse->last_trip_time.day,
purse->last_trip_time.month,
purse->last_trip_time.year,
purse->last_trip_time.hour,
purse->last_trip_time.minute);
furi_string_cat_printf(
parsed_data,
"\nValidator: %d\nFare: %d RUB\nRefilled on: %02d.%02d.%04d %02d:%02d\nAmount: %d RUB",
purse->validator,
purse->fare,
purse->last_payment_date.day,
purse->last_payment_date.month,
purse->last_payment_date.year,
purse->last_payment_date.hour,
purse->last_payment_date.minute,
purse->last_payment_amount);
if(purse->keyset == 1)
furi_string_cat_printf(parsed_data, "\nPPK keys installed:> YES");
else
furi_string_cat_printf(parsed_data, "\nPPK keys installed:> NO");
furi_string_free(purse->card_number_str);
}
// Function to format and print PPK ticket data
static void printf_ppk_data(FuriString* parsed_data, PPKData* ticket, bool ticket_number) {
if(ticket_number == 0) {
furi_string_cat_printf(parsed_data, "\n\n\e#PPK Ticket:\n");
switch(ticket->first_ticket_marker) {
case 0x33:
furi_string_cat_printf(parsed_data, "Type:> 1 ride");
break;
case 0x34:
furi_string_cat_printf(parsed_data, "Type:> 2 rides (Mon.-Fri.)");
break;
case 0x35:
furi_string_cat_printf(parsed_data, "Type:> 2 rides (Fri.-Mon.)");
break;
case 0x36:
furi_string_cat_printf(parsed_data, "Type:> 2 rides (Sat.-Mon.");
break;
default:
furi_string_cat_printf(
parsed_data, "Type:> Unknown, 0x%02X", ticket->first_ticket_marker);
}
} else if(ticket_number == 1) {
furi_string_cat_printf(parsed_data, "\n\nSecond PPK Ticket:\n");
switch(ticket->second_ticket_marker) {
case 0x33:
furi_string_cat_printf(parsed_data, "Type:> 1 ride");
break;
case 0x34:
furi_string_cat_printf(parsed_data, "Type:> 2 rides (Mon.-Fri.)");
break;
case 0x35:
furi_string_cat_printf(parsed_data, "Type:> 2 rides (Fri.-Mon.)");
break;
case 0x36:
furi_string_cat_printf(parsed_data, "Type:> 2 rides (Sat.-Mon.)");
break;
default:
furi_string_cat_printf(
parsed_data, "Type:> Unknown, 0x%02X", ticket->second_ticket_marker);
}
}
furi_string_cat_printf(
parsed_data,
"\nFrom:> %s\nTo:> %s",
furi_string_get_cstr(ticket->departure_name),
furi_string_get_cstr(ticket->destination_name));
if(ticket->valid_from.day == ticket->valid_till.day) {
furi_string_cat_printf(
parsed_data,
"\nValid On: %02d-%02d-%04d",
ticket->valid_from.day,
ticket->valid_from.month,
ticket->valid_from.year);
} else {
furi_string_cat_printf(
parsed_data,
"\nValid From: %02d-%02d-%04d\nValid thru: %02d-%02d-%04d",
ticket->valid_from.day,
ticket->valid_from.month,
ticket->valid_from.year,
ticket->valid_till.day,
ticket->valid_till.month,
ticket->valid_till.year);
}
if(ticket->direction == 1) {
furi_string_cat_printf(parsed_data, "\nDirection: One-way ->>");
} else if(ticket->direction == 2) {
furi_string_cat_printf(parsed_data, "\nDirection: Round-trip <<-->>");
}
furi_string_cat_printf(parsed_data, "\nRides left:> %02d", ticket->value_data);
if(ticket->current_status == 0) {
furi_string_cat_printf(parsed_data, "\nStatus:> TICKET IS READY\n");
} else if(ticket->current_status == 0x80)
furi_string_cat_printf(
parsed_data,
"\nStatus:> ENTERED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\n",
furi_string_get_cstr(ticket->trip_start_sta_name),
ticket->tap_time.day,
ticket->tap_time.month,
ticket->tap_time.year,
ticket->tap_time.hour,
ticket->tap_time.minute);
else if(ticket->current_status == 0x1E)
furi_string_cat_printf(
parsed_data,
"\nStatus:> EXITED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\n",
furi_string_get_cstr(ticket->trip_end_sta_name),
ticket->tap_time.day,
ticket->tap_time.month,
ticket->tap_time.year,
ticket->tap_time.hour,
ticket->tap_time.minute);
else
furi_string_cat_printf(parsed_data, "\nStatus:> UNKNOWN (%02X)\n", ticket->current_status);
furi_string_cat_printf(
parsed_data, "SYS N:> %lld\nPPK CNT:> %03d", ticket->sys_n, ticket->ppk_cnt);
}
//Function to select a keyset based on card type
static bool plantain_get_card_config(PlantainCardConfig* config, MfClassicType type) {
bool success = true;
if(type == MfClassicType1k) {
config->data_sector = 8;
config->keys = plantain_1k_keys;
} else if(type == MfClassicType4k) {
config->data_sector = 8;
config->keys = plantain_4k_keys;
} else {
success = false;
}
@@ -114,11 +582,10 @@ static bool plantain_verify_type(Nfc* nfc, MfClassicType type) {
bool verified = false;
do {
PlantainCardConfig cfg = {};
PlantainCardConfig cfg = {0};
if(!plantain_get_card_config(&cfg, type)) break;
const uint8_t block_num = mf_classic_get_first_block_num_of_sector(cfg.data_sector);
FURI_LOG_D(TAG, "Verifying sector %lu", cfg.data_sector);
MfClassicKey key = {0};
bit_lib_num_to_bytes_be(cfg.keys[cfg.data_sector].a, COUNT_OF(key.data), key.data);
@@ -127,7 +594,6 @@ static bool plantain_verify_type(Nfc* nfc, MfClassicType type) {
MfClassicError error =
mf_classic_poller_sync_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_context);
if(error != MfClassicErrorNone) {
FURI_LOG_D(TAG, "Failed to read block %u: %d", block_num, error);
break;
}
@@ -157,7 +623,7 @@ static bool plantain_read(Nfc* nfc, NfcDevice* device) {
if(error != MfClassicErrorNone) break;
data->type = type;
PlantainCardConfig cfg = {};
PlantainCardConfig cfg = {0};
if(!plantain_get_card_config(&cfg, data->type)) break;
const uint8_t legacy_check_sec_num = 26;
@@ -171,21 +637,28 @@ static bool plantain_read(Nfc* nfc, NfcDevice* device) {
error = mf_classic_poller_sync_auth(
nfc, legacy_check_block_num, &key, MfClassicKeyTypeA, NULL);
if(error == MfClassicErrorNone) {
FURI_LOG_D(TAG, "Legacy keys detected");
cfg.keys = plantain_4k_keys_legacy;
}
MfClassicDeviceKeys keys = {};
for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
bit_lib_num_to_bytes_be(cfg.keys[i].a, sizeof(MfClassicKey), keys.key_a[i].data);
FURI_BIT_SET(keys.key_a_mask, i);
bit_lib_num_to_bytes_be(cfg.keys[i].b, sizeof(MfClassicKey), keys.key_b[i].data);
FURI_BIT_SET(keys.key_b_mask, i);
if(data->type == MfClassicType1k) {
for(size_t i = 0; i < 32; i++) {
bit_lib_num_to_bytes_be(cfg.keys[i].a, sizeof(MfClassicKey), keys.key_a[i].data);
FURI_BIT_SET(keys.key_a_mask, i);
bit_lib_num_to_bytes_be(cfg.keys[i].b, sizeof(MfClassicKey), keys.key_b[i].data);
FURI_BIT_SET(keys.key_b_mask, i);
}
} else {
for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
bit_lib_num_to_bytes_be(cfg.keys[i].a, sizeof(MfClassicKey), keys.key_a[i].data);
FURI_BIT_SET(keys.key_a_mask, i);
bit_lib_num_to_bytes_be(cfg.keys[i].b, sizeof(MfClassicKey), keys.key_b[i].data);
FURI_BIT_SET(keys.key_b_mask, i);
}
}
error = mf_classic_poller_sync_read(nfc, &keys, data);
if(error == MfClassicErrorNotPresent) {
FURI_LOG_W(TAG, "Failed to read data");
break;
}
@@ -198,18 +671,22 @@ static bool plantain_read(Nfc* nfc, NfcDevice* device) {
return is_read;
}
//Main parsing function
static bool plantain_parse(const NfcDevice* device, FuriString* parsed_data) {
furi_assert(device);
size_t uid_len = 0;
const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
const uint8_t* uid = mf_classic_get_uid(data, &uid_len);
PPKData ticket = {0};
ticket.departure_name = furi_string_alloc();
ticket.destination_name = furi_string_alloc();
ticket.trip_start_sta_name = furi_string_alloc();
ticket.trip_end_sta_name = furi_string_alloc();
PlantainData purse = {0};
Storage* storage = furi_record_open(RECORD_STORAGE);
bool parsed = false;
do {
// Verify card type
PlantainCardConfig cfg = {};
PlantainCardConfig cfg = {0};
if(!plantain_get_card_config(&cfg, data->type)) break;
// Verify key
@@ -220,167 +697,47 @@ static bool plantain_parse(const NfcDevice* device, FuriString* parsed_data) {
bit_lib_bytes_to_num_be(sec_tr->key_a.data, COUNT_OF(sec_tr->key_a.data));
if(key != cfg.keys[cfg.data_sector].a) break;
furi_string_printf(parsed_data, "\e#Plantain card\n");
if(data->block[107].data[10] == 0x02)
purse.keyset = 0;
else
purse.keyset = 1;
const uint8_t* temp_ptr = &uid[0];
// UID is read from last to first byte
uint8_t card_number_tmp[uid_len];
if(uid_len == 4) {
for(size_t i = 0; i < 4; i++) {
card_number_tmp[i] = temp_ptr[3 - i];
}
} else if(uid_len == 7) {
for(size_t i = 0; i < 7; i++) {
card_number_tmp[i] = temp_ptr[6 - i];
}
} else {
break;
// Extract plantain purse data and fill PPK tickets markers
extract_purse_data(
data, &purse, &ticket, FIRST_PPK_TICKET_OFFSET, SECOND_PPK_TICKET_OFFSET);
// Print plantain purse data
printf_plantain_data(parsed_data, &purse);
if(purse.card_number_str) furi_string_free(purse.card_number_str);
// Extract and print PPK ticket data if present
if(ticket.first_ticket_marker != 0) {
extract_ppk_data(storage, data, &ticket, 0);
printf_ppk_data(parsed_data, &ticket, false);
}
//UID is converted to a card number
uint64_t card_number = 0;
for(size_t i = 0; i < uid_len; i++) {
card_number = (card_number << 8) | card_number_tmp[i];
// Extract and print second PPK ticket data if present
if(ticket.second_ticket_marker != 0) {
PPKData second_ticket = {0};
second_ticket.departure_name = furi_string_alloc();
second_ticket.destination_name = furi_string_alloc();
second_ticket.trip_start_sta_name = furi_string_alloc();
second_ticket.trip_end_sta_name = furi_string_alloc();
extract_ppk_data(storage, data, &second_ticket, 1);
printf_ppk_data(parsed_data, &second_ticket, true);
furi_string_free(second_ticket.departure_name);
furi_string_free(second_ticket.destination_name);
furi_string_free(second_ticket.trip_start_sta_name);
furi_string_free(second_ticket.trip_end_sta_name);
}
// Print card number with 4-digit groups. "3" in "3078" denotes a ticket type "3 - full ticket", will differ on discounted cards.
furi_string_cat_printf(parsed_data, "Number: ");
FuriString* card_number_s = furi_string_alloc();
furi_string_cat_printf(card_number_s, "%lld", card_number);
FuriString* tmp_s = furi_string_alloc_set_str("9643 3078 ");
for(uint8_t i = 0; i < 24; i += 4) {
for(uint8_t j = 0; j < 4; j++) {
furi_string_push_back(tmp_s, furi_string_get_char(card_number_s, i + j));
}
furi_string_push_back(tmp_s, ' ');
}
furi_string_cat_printf(parsed_data, "%s\n", furi_string_get_cstr(tmp_s));
// this works for 2K Plantain
if(data->type == MfClassicType1k) {
//balance
uint32_t balance = 0;
for(uint8_t i = 0; i < 4; i++) {
balance = (balance << 8) | data->block[16].data[3 - i];
}
furi_string_cat_printf(parsed_data, "Balance: %ld rub\n", balance / 100);
//trips
uint8_t trips_metro = data->block[21].data[0];
uint8_t trips_ground = data->block[21].data[1];
furi_string_cat_printf(parsed_data, "Trips: %d\n", trips_metro + trips_ground);
//trip time
uint32_t last_trip_timestamp = 0;
for(uint8_t i = 0; i < 3; i++) {
last_trip_timestamp = (last_trip_timestamp << 8) | data->block[21].data[4 - i];
}
DateTime last_trip = {0};
from_minutes_to_datetime(last_trip_timestamp + 24 * 60, &last_trip, 2010);
furi_string_cat_printf(
parsed_data,
"Trip start: %02d.%02d.%04d %02d:%02d\n",
last_trip.day,
last_trip.month,
last_trip.year,
last_trip.hour,
last_trip.minute);
//validator
uint16_t validator = (data->block[20].data[5] << 8) | data->block[20].data[4];
furi_string_cat_printf(parsed_data, "Validator: %d\n", validator);
//tariff
uint16_t fare = (data->block[20].data[7] << 8) | data->block[20].data[6];
furi_string_cat_printf(parsed_data, "Tariff: %d rub\n", fare / 100);
//trips in metro
furi_string_cat_printf(parsed_data, "Trips (Metro): %d\n", trips_metro);
//trips on ground
furi_string_cat_printf(parsed_data, "Trips (Ground): %d\n", trips_ground);
//last payment
uint32_t last_payment_timestamp = 0;
for(uint8_t i = 0; i < 3; i++) {
last_payment_timestamp = (last_payment_timestamp << 8) |
data->block[18].data[4 - i];
}
DateTime last_payment_date = {0};
from_minutes_to_datetime(last_payment_timestamp + 24 * 60, &last_payment_date, 2010);
furi_string_cat_printf(
parsed_data,
"Last pay: %02d.%02d.%04d %02d:%02d\n",
last_payment_date.day,
last_payment_date.month,
last_payment_date.year,
last_payment_date.hour,
last_payment_date.minute);
//Last payment amount.
uint16_t last_payment = ((data->block[18].data[10] << 16) |
(data->block[18].data[9] << 8) | (data->block[18].data[8])) /
100;
furi_string_cat_printf(parsed_data, "Amount: %d rub", last_payment);
furi_string_free(card_number_s);
furi_string_free(tmp_s);
//This is for 4K Plantains.
} else if(data->type == MfClassicType4k) {
//balance
uint32_t balance = 0;
for(uint8_t i = 0; i < 4; i++) {
balance = (balance << 8) | data->block[16].data[3 - i];
}
furi_string_cat_printf(parsed_data, "Balance: %ld rub\n", balance / 100);
//trips
uint8_t trips_metro = data->block[21].data[0];
uint8_t trips_ground = data->block[21].data[1];
furi_string_cat_printf(parsed_data, "Trips: %d\n", trips_metro + trips_ground);
//trip time
uint32_t last_trip_timestamp = 0;
for(uint8_t i = 0; i < 3; i++) {
last_trip_timestamp = (last_trip_timestamp << 8) | data->block[21].data[4 - i];
}
DateTime last_trip = {0};
from_minutes_to_datetime(last_trip_timestamp + 24 * 60, &last_trip, 2010);
furi_string_cat_printf(
parsed_data,
"Trip start: %02d.%02d.%04d %02d:%02d\n",
last_trip.day,
last_trip.month,
last_trip.year,
last_trip.hour,
last_trip.minute);
//validator
uint16_t validator = (data->block[20].data[5] << 8) | data->block[20].data[4];
furi_string_cat_printf(parsed_data, "Validator: %d\n", validator);
//tariff
uint16_t fare = (data->block[20].data[7] << 8) | data->block[20].data[6];
furi_string_cat_printf(parsed_data, "Tariff: %d rub\n", fare / 100);
//trips in metro
furi_string_cat_printf(parsed_data, "Trips (Metro): %d\n", trips_metro);
//trips on ground
furi_string_cat_printf(parsed_data, "Trips (Ground): %d\n", trips_ground);
//last payment
uint32_t last_payment_timestamp = 0;
for(uint8_t i = 0; i < 3; i++) {
last_payment_timestamp = (last_payment_timestamp << 8) |
data->block[18].data[4 - i];
}
DateTime last_payment_date = {0};
from_minutes_to_datetime(last_payment_timestamp + 24 * 60, &last_payment_date, 2010);
furi_string_cat_printf(
parsed_data,
"Last pay: %02d.%02d.%04d %02d:%02d\n",
last_payment_date.day,
last_payment_date.month,
last_payment_date.year,
last_payment_date.hour,
last_payment_date.minute);
//Last payment amount
uint16_t last_payment = ((data->block[18].data[10] << 16) |
(data->block[18].data[9] << 8) | (data->block[18].data[8])) /
100;
furi_string_cat_printf(parsed_data, "Amount: %d rub", last_payment);
furi_string_free(card_number_s);
furi_string_free(tmp_s);
}
parsed = true;
} while(false);
furi_string_free(ticket.departure_name);
furi_string_free(ticket.destination_name);
furi_string_free(ticket.trip_start_sta_name);
furi_string_free(ticket.trip_end_sta_name);
furi_record_close(RECORD_STORAGE);
return parsed;
}
@@ -0,0 +1,399 @@
//Based on parsers written by Leptoptilos and Assasinfil. Also, thanks to WillyJL (<me@willyjl.dev>) for help!
#include "nfc_supported_card_plugin.h"
#include <flipper_application/flipper_application.h>
#include <nfc/nfc_device.h>
#include <bit_lib/bit_lib.h>
#include <datetime.h>
#include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
#include <flipper_format/flipper_format.h>
#define PPK_WHOLE_EPOCH_START 946684800 //2000-01-01
#define PPK_CURRENT_EPOCH_START 1388534400 //2014-01-01
#define SECONDS_IN_A_DAY 86400
#define SECONDS_IN_A_MINUTE 60
#define FIRST_PPK_TICKET_OFFSET 76
#define FIRST_TICKET_VALUE_BLOCK 77
#define SECOND_PPK_TICKET_OFFSET 88
#define SECOND_TICKET_VALUE_BLOCK 89
typedef struct {
uint64_t a;
uint64_t b;
} MfClassicKeyPair;
typedef struct {
uint16_t departure_uic;
uint16_t destination_uic;
uint16_t trip_start_uic;
uint16_t trip_end_uic;
FuriString* departure_name;
FuriString* destination_name;
FuriString* trip_start_sta_name;
FuriString* trip_end_sta_name;
uint8_t value_data;
uint8_t current_status;
uint16_t valid_from_data;
uint16_t valid_till_data;
DateTime valid_from;
DateTime valid_till;
uint32_t tap_data;
DateTime tap_time;
uint8_t first_ticket_marker;
uint8_t second_ticket_marker;
uint8_t ppk_cnt;
} TicketData;
static const MfClassicKeyPair t_card_2k[] = {
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //0
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //1
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //2
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //3
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //4
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //5
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //6
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //7
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //8
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //9
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //10
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //11
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //12
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //13
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //14
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //15
{.a = 0x061029A4A6A4, .b = 0xCF43E52FC23D}, //16
{.a = 0xE09F2EC8229C, .b = 0x044D78B70F92}, //17
{.a = 0x66843B4FF86E, .b = 0x4474EB131577}, //18
{.a = 0x9AC9A9B5E809, .b = 0x558B8E45E568}, //19
{.a = 0xC114DE1AD90F, .b = 0xA0A9033E2A09}, //20
{.a = 0xF696EFFDD838, .b = 0xA38333EC3F72}, //21
{.a = 0xAB50BDE9CD04, .b = 0x67991D678AED}, //22
{.a = 0x9041DD7B236C, .b = 0x8A527C8CA237}, //23
{.a = 0x7D40A5AA63D0, .b = 0x4A61C563DD8A}, //24
{.a = 0x05D0520EC52B, .b = 0xB40BD14E6CB4}, //25
{.a = 0x9354F1BFF80F, .b = 0xF99D40CBBA63}, //26
{.a = 0x84132D9FD1AF, .b = 0x1BAA2563C14D}, //27
{.a = 0x27A8E6436B01, .b = 0x4DB883715A5C}, //28
{.a = 0x58B173568D26, .b = 0x2E4ADD66E35E}, //29
{.a = 0x1BE71601E73C, .b = 0xB855F30C54FB}, //30
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //31
};
static inline void sz_uic_to_sta(Storage* storage, const char* file_name, TicketData* ticket) {
FlipperFormat* file = flipper_format_file_alloc(storage);
FuriString* departure_uic = furi_string_alloc_printf("%04X", ticket->departure_uic);
FuriString* destination_uic = furi_string_alloc_printf("%04X", ticket->destination_uic);
FuriString* trip_start_uic = furi_string_alloc_printf("%04X", ticket->trip_start_uic);
FuriString* trip_end_uic = furi_string_alloc_printf("%04X", ticket->trip_end_uic);
if(flipper_format_file_open_existing(file, file_name)) {
flipper_format_read_string(
file, furi_string_get_cstr(departure_uic), ticket->departure_name);
flipper_format_rewind(file);
flipper_format_read_string(
file, furi_string_get_cstr(destination_uic), ticket->destination_name);
flipper_format_rewind(file);
flipper_format_read_string(
file, furi_string_get_cstr(trip_start_uic), ticket->trip_start_sta_name);
flipper_format_rewind(file);
flipper_format_read_string(
file, furi_string_get_cstr(trip_end_uic), ticket->trip_end_sta_name);
}
flipper_format_free(file);
furi_string_free(departure_uic);
furi_string_free(destination_uic);
furi_string_free(trip_start_uic);
furi_string_free(trip_end_uic);
}
static void resolve_station_name(Storage* storage, TicketData* ticket) {
sz_uic_to_sta(storage, EXT_PATH("nfc/assets/sev_id.nfc"), ticket);
if(furi_string_utf8_length(ticket->departure_name) <= 2) {
furi_string_printf(ticket->departure_name, "1f%04X", ticket->departure_uic);
}
if(furi_string_utf8_length(ticket->destination_name) <= 2) {
furi_string_printf(ticket->destination_name, "1F%04X", ticket->destination_uic);
}
if(furi_string_utf8_length(ticket->trip_start_sta_name) <= 2) {
furi_string_printf(ticket->trip_start_sta_name, "1F%04X", ticket->trip_start_uic);
}
if(furi_string_utf8_length(ticket->trip_end_sta_name) <= 2) {
furi_string_printf(ticket->trip_end_sta_name, "1F%04X", ticket->trip_end_uic);
}
}
static inline void extract_ppk_data(
Storage* storage,
const MfClassicData* data,
TicketData* ticket,
bool ticket_number) {
if(ticket_number == 0) {
ticket->departure_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[6] << 8) |
(data->block[FIRST_PPK_TICKET_OFFSET].data[5]);
ticket->destination_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[9] << 8) |
(data->block[FIRST_PPK_TICKET_OFFSET].data[8]);
ticket->value_data = data->block[FIRST_TICKET_VALUE_BLOCK].data[0];
ticket->current_status = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[8];
ticket->valid_from_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[2] << 8) |
(data->block[FIRST_PPK_TICKET_OFFSET].data[1]);
ticket->valid_till_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[4] << 8) |
(data->block[FIRST_PPK_TICKET_OFFSET].data[3]);
ticket->tap_data = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[0];
ticket->ppk_cnt = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[10];
ticket->trip_start_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[3]);
ticket->trip_end_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[6]);
} else {
ticket->departure_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[6] << 8) |
(data->block[SECOND_PPK_TICKET_OFFSET].data[5]);
ticket->destination_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[9] << 8) |
(data->block[SECOND_PPK_TICKET_OFFSET].data[8]);
ticket->value_data = data->block[SECOND_TICKET_VALUE_BLOCK].data[0];
ticket->current_status = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[8];
ticket->valid_from_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[2] << 8) |
(data->block[SECOND_PPK_TICKET_OFFSET].data[1]);
ticket->valid_till_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[4] << 8) |
(data->block[SECOND_PPK_TICKET_OFFSET].data[3]);
ticket->tap_data = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[0];
ticket->ppk_cnt = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[10];
ticket->trip_start_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[3]);
ticket->trip_end_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[6]);
}
ticket->first_ticket_marker = data->block[FIRST_PPK_TICKET_OFFSET].data[0];
ticket->second_ticket_marker = data->block[SECOND_PPK_TICKET_OFFSET].data[0];
const uint32_t valid_from_timestamp =
PPK_WHOLE_EPOCH_START + ticket->valid_from_data * SECONDS_IN_A_DAY;
const uint32_t valid_till_timestamp =
PPK_WHOLE_EPOCH_START + ticket->valid_till_data * SECONDS_IN_A_DAY;
const uint32_t tap_timestamp =
PPK_CURRENT_EPOCH_START + ticket->tap_data * SECONDS_IN_A_MINUTE;
datetime_timestamp_to_datetime(valid_from_timestamp, &ticket->valid_from);
datetime_timestamp_to_datetime(valid_till_timestamp, &ticket->valid_till);
datetime_timestamp_to_datetime(tap_timestamp, &ticket->tap_time);
resolve_station_name(storage, ticket);
}
static void
printf_transport_card(FuriString* parsed_data, TicketData* ticket, bool ticket_number) {
if(ticket->departure_uic == 0x0000) {
furi_string_cat_printf(
parsed_data,
"\e#Unknown SevPPK Card\n NO TICKET DATA FOUND \n\nTHE TICKET IS NOT ISSUED\nOR LAYOUT IS UNKNOWN\n");
return;
} else {
if(ticket_number == 0) {
furi_string_cat_printf(parsed_data, "\e#SevPPK Transport Card\n");
switch(ticket->first_ticket_marker) {
case 0x02:
furi_string_cat_printf(parsed_data, "Type:> 5 days unlim.");
break;
case 0x06:
furi_string_cat_printf(parsed_data, "Type:> 10 rides");
break;
case 0x18:
furi_string_cat_printf(parsed_data, "Type:> 30 days unlim.");
break;
case 0x1B:
furi_string_cat_printf(parsed_data, "Type:> 20 rides");
break;
default:
furi_string_cat_printf(
parsed_data, "Type: Unknown, 0x%02X", ticket->first_ticket_marker);
}
} else {
furi_string_cat_printf(parsed_data, "\e#Second Ticket:");
switch(ticket->second_ticket_marker) {
case 0x02:
furi_string_cat_printf(parsed_data, "Type:> 5 days unlim.");
break;
case 0x06:
furi_string_cat_printf(parsed_data, "Type:> 10 rides");
break;
case 0x18:
furi_string_cat_printf(parsed_data, "Type:> 30 days unlim.");
break;
case 0x1B:
furi_string_cat_printf(parsed_data, "Type:> 20 rides");
break;
default:
furi_string_cat_printf(
parsed_data, "Type: Unknown, 0x%02X", ticket->second_ticket_marker);
}
}
}
furi_string_cat_printf(
parsed_data,
"\nFrom:>%s\nTo:>%s\nValid From: %02d-%02d-%04d\nValid thru: %02d-%02d-%04d\n",
furi_string_get_cstr(ticket->departure_name),
furi_string_get_cstr(ticket->destination_name),
ticket->valid_from.day,
ticket->valid_from.month,
ticket->valid_from.year,
ticket->valid_till.day,
ticket->valid_till.month,
ticket->valid_till.year);
if(ticket->value_data > 0) {
furi_string_cat_printf(parsed_data, "Rides remain: %02d\n", ticket->value_data);
}
switch(ticket->current_status) {
case 0x00:
furi_string_cat_printf(parsed_data, "Status:> NOT USED\n");
break;
case 0x80:
furi_string_cat_printf(
parsed_data,
"Status:> ENTERED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n",
furi_string_get_cstr(ticket->trip_start_sta_name),
ticket->tap_time.day,
ticket->tap_time.month,
ticket->tap_time.year,
ticket->tap_time.hour,
ticket->tap_time.minute,
ticket->ppk_cnt);
break;
case 0x1F:
furi_string_cat_printf(
parsed_data,
"Status:> EXITED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n",
furi_string_get_cstr(ticket->trip_end_sta_name),
ticket->tap_time.day,
ticket->tap_time.month,
ticket->tap_time.year,
ticket->tap_time.hour,
ticket->tap_time.minute,
ticket->ppk_cnt);
break;
default:
furi_string_cat_printf(
parsed_data,
"Status:> UNKNOWN (%02X)\nPPK CNT: %03d",
ticket->current_status,
ticket->ppk_cnt);
break;
}
}
bool sev_tk_verify(Nfc* nfc) {
const uint8_t verify_sector = 19;
const uint8_t block_num = mf_classic_get_first_block_num_of_sector(verify_sector);
MfClassicKey key = {};
bit_lib_num_to_bytes_be(t_card_2k[verify_sector].a, COUNT_OF(key.data), key.data);
MfClassicAuthContext auth_ctx = {};
MfClassicError error =
mf_classic_poller_sync_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_ctx);
return error == MfClassicErrorNone;
}
static bool sev_tk_read(Nfc* nfc, NfcDevice* device) {
furi_assert(nfc);
furi_assert(device);
MfClassicData* data = mf_classic_alloc();
nfc_device_copy_data(device, NfcProtocolMfClassic, data);
bool is_read = false;
MfClassicType type = MfClassicType1k;
MfClassicError error = mf_classic_poller_sync_detect_type(nfc, &type);
if(error == MfClassicErrorNone) {
data->type = type;
MfClassicDeviceKeys keys = {};
for(size_t i = 0; i < 32; i++) {
bit_lib_num_to_bytes_be(t_card_2k[i].a, sizeof(MfClassicKey), keys.key_a[i].data);
FURI_BIT_SET(keys.key_a_mask, i);
bit_lib_num_to_bytes_be(t_card_2k[i].b, sizeof(MfClassicKey), keys.key_b[i].data);
FURI_BIT_SET(keys.key_b_mask, i);
}
error = mf_classic_poller_sync_read(nfc, &keys, data);
if(error != MfClassicErrorNotPresent) {
nfc_device_set_data(device, NfcProtocolMfClassic, data);
is_read = (error == MfClassicErrorNone);
}
}
mf_classic_free(data);
return is_read;
}
static bool sev_tk_parse(const NfcDevice* device, FuriString* parsed_data) {
furi_assert(device);
const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
bool parsed = false;
do {
MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, 19);
uint64_t key = bit_lib_bytes_to_num_be(sec_tr->key_a.data, 6);
if(key != t_card_2k[19].a) break;
Storage* storage = furi_record_open(RECORD_STORAGE);
TicketData primary_ticket = {0};
primary_ticket.departure_name = furi_string_alloc();
primary_ticket.destination_name = furi_string_alloc();
primary_ticket.trip_start_sta_name = furi_string_alloc();
primary_ticket.trip_end_sta_name = furi_string_alloc();
extract_ppk_data(storage, data, &primary_ticket, 0);
printf_transport_card(parsed_data, &primary_ticket, false);
furi_string_free(primary_ticket.departure_name);
furi_string_free(primary_ticket.destination_name);
furi_string_free(primary_ticket.trip_start_sta_name);
furi_string_free(primary_ticket.trip_end_sta_name);
if(primary_ticket.second_ticket_marker != 0) {
TicketData secondary_ticket = {0};
secondary_ticket.departure_name = furi_string_alloc();
secondary_ticket.destination_name = furi_string_alloc();
secondary_ticket.trip_start_sta_name = furi_string_alloc();
secondary_ticket.trip_end_sta_name = furi_string_alloc();
extract_ppk_data(storage, data, &secondary_ticket, 1);
printf_transport_card(parsed_data, &secondary_ticket, true);
furi_string_free(primary_ticket.departure_name);
furi_string_free(primary_ticket.destination_name);
furi_string_free(primary_ticket.trip_start_sta_name);
furi_string_free(primary_ticket.trip_end_sta_name);
}
furi_record_close(RECORD_STORAGE);
parsed = true;
} while(false);
return parsed;
}
static const NfcSupportedCardsPlugin sev_tk_plugin = {
.protocol = NfcProtocolMfClassic,
.verify = sev_tk_verify,
.read = sev_tk_read,
.parse = sev_tk_parse,
};
static const FlipperAppPluginDescriptor sev_tk_plugin_descriptor = {
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
.entry_point = &sev_tk_plugin,
};
const FlipperAppPluginDescriptor* sev_tk_plugin_ep(void) {
return &sev_tk_plugin_descriptor;
}
@@ -0,0 +1,406 @@
//Based on parsers written by Leptoptilos and Assasinfil. Also, thanks to WillyJL (<me@willyjl.dev>) for help!
#include "nfc_supported_card_plugin.h"
#include <flipper_application/flipper_application.h>
#include <nfc/nfc_device.h>
#include <bit_lib/bit_lib.h>
#include <datetime.h>
#include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
#include <flipper_format/flipper_format.h>
#define PPK_WHOLE_EPOCH_START 946684800 //2000-01-01
#define PPK_CURRENT_EPOCH_START 1388534400 //2014-01-01
#define SECONDS_IN_A_DAY 86400
#define SECONDS_IN_A_MINUTE 60
#define FIRST_PPK_TICKET_OFFSET 76
#define FIRST_TICKET_VALUE_BLOCK 77
#define SECOND_PPK_TICKET_OFFSET 88
#define SECOND_TICKET_VALUE_BLOCK 89
typedef struct {
uint64_t a;
uint64_t b;
} MfClassicKeyPair;
typedef struct {
uint16_t departure_uic;
uint16_t destination_uic;
uint16_t trip_start_uic;
uint16_t trip_end_uic;
FuriString* departure_name;
FuriString* destination_name;
FuriString* trip_start_sta_name;
FuriString* trip_end_sta_name;
uint8_t value_data;
uint8_t current_status;
uint16_t valid_from_data;
uint16_t valid_till_data;
DateTime valid_from;
DateTime valid_till;
uint32_t tap_data;
DateTime tap_time;
uint8_t first_ticket_marker;
uint8_t second_ticket_marker;
uint8_t ppk_cnt;
} TicketData;
static const MfClassicKeyPair t_card_4k[] = {
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B4B2B1B3B6}, //0
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B5B2B4B9B0}, //1
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B8B4B6B1B3}, //2
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B3B8B5B7}, //3
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B6B4B2B8}, //4
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B4B5B3B8}, //5
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B8B1B7B8}, //6
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B5B1B8B1}, //7
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B6B4B8B7}, //8
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B9B2B9B2B4}, //9
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B1B8B0B6}, //10
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B7B8B9B5B5}, //11
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B7B1B0B0}, //12
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B9B2B8B3B2}, //13
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B4B1B0B4B6}, //14
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B4B2B5B4B8}, //15
{.a = 0x684CE8377AB8, .b = 0x91888D728D9E}, //16
{.a = 0xF462ED255B44, .b = 0x0D40620AC610}, //17
{.a = 0xF1E8FD5B5C9F, .b = 0xABB5763550B0}, //18
{.a = 0xE9E3265B45B1, .b = 0x11D848B26034}, //19
{.a = 0xAEA3755DFA82, .b = 0x36D1BAC1395E}, //20
{.a = 0x83F58B205854, .b = 0x967DAFCF2674}, //21
{.a = 0x5CDF18E68A75, .b = 0x1689C175B14E}, //22
{.a = 0x126DC25C5D53, .b = 0x346B03AF1FF3}, //23
{.a = 0xF1B013C4495C, .b = 0x74CE14DBC71F}, //24
{.a = 0xA5FE4FAD0269, .b = 0x0025FEA845E5}, //25
{.a = 0x43080428049C, .b = 0x2E91BB6F511E}, //26
{.a = 0x4F44A08C51BC, .b = 0xE44CC58DF833}, //27
{.a = 0x3FEC92F652BE, .b = 0x942039006B83}, //28
{.a = 0x3A3BE5B635FA, .b = 0xFC564425A9BA}, //29
{.a = 0x78C9E1C688BB, .b = 0xA29E362B22F3}, //30
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //31
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //32
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //33
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //34
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //35
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //36
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //37
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //38
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //39
};
static inline void sz_uic_to_sta(Storage* storage, const char* file_name, TicketData* ticket) {
FlipperFormat* file = flipper_format_file_alloc(storage);
FuriString* departure_uic = furi_string_alloc_printf("%04X", ticket->departure_uic);
FuriString* destination_uic = furi_string_alloc_printf("%04X", ticket->destination_uic);
FuriString* trip_start_uic = furi_string_alloc_printf("%04X", ticket->trip_start_uic);
FuriString* trip_end_uic = furi_string_alloc_printf("%04X", ticket->trip_end_uic);
if(flipper_format_file_open_existing(file, file_name)) {
flipper_format_read_string(
file, furi_string_get_cstr(departure_uic), ticket->departure_name);
flipper_format_rewind(file);
flipper_format_read_string(
file, furi_string_get_cstr(destination_uic), ticket->destination_name);
flipper_format_rewind(file);
flipper_format_read_string(
file, furi_string_get_cstr(trip_start_uic), ticket->trip_start_sta_name);
flipper_format_rewind(file);
flipper_format_read_string(
file, furi_string_get_cstr(trip_end_uic), ticket->trip_end_sta_name);
}
flipper_format_free(file);
furi_string_free(departure_uic);
furi_string_free(destination_uic);
furi_string_free(trip_start_uic);
furi_string_free(trip_end_uic);
}
static void resolve_station_name(Storage* storage, TicketData* ticket) {
sz_uic_to_sta(storage, EXT_PATH("nfc/assets/sk_id.nfc"), ticket);
if(furi_string_utf8_length(ticket->departure_name) <= 2) {
furi_string_printf(ticket->departure_name, "1f%04X", ticket->departure_uic);
}
if(furi_string_utf8_length(ticket->destination_name) <= 2) {
furi_string_printf(ticket->destination_name, "1F%04X", ticket->destination_uic);
}
if(furi_string_utf8_length(ticket->trip_start_sta_name) <= 2) {
furi_string_printf(ticket->trip_start_sta_name, "1F%04X", ticket->trip_start_uic);
}
if(furi_string_utf8_length(ticket->trip_end_sta_name) <= 2) {
furi_string_printf(ticket->trip_end_sta_name, "1F%04X", ticket->trip_end_uic);
}
}
static inline void extract_ppk_data(
Storage* storage,
const MfClassicData* data,
TicketData* ticket,
bool ticket_number) {
if(ticket_number == 0) {
ticket->departure_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[6] << 8) |
(data->block[FIRST_PPK_TICKET_OFFSET].data[5]);
ticket->destination_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[9] << 8) |
(data->block[FIRST_PPK_TICKET_OFFSET].data[8]);
ticket->value_data = data->block[FIRST_TICKET_VALUE_BLOCK].data[0];
ticket->current_status = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[8];
ticket->valid_from_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[2] << 8) |
(data->block[FIRST_PPK_TICKET_OFFSET].data[1]);
ticket->valid_till_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[4] << 8) |
(data->block[FIRST_PPK_TICKET_OFFSET].data[3]);
ticket->tap_data = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[0];
ticket->ppk_cnt = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[10];
ticket->trip_start_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[3]);
ticket->trip_end_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[6]);
} else {
ticket->departure_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[6] << 8) |
(data->block[SECOND_PPK_TICKET_OFFSET].data[5]);
ticket->destination_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[9] << 8) |
(data->block[SECOND_PPK_TICKET_OFFSET].data[8]);
ticket->value_data = data->block[SECOND_TICKET_VALUE_BLOCK].data[0];
ticket->current_status = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[8];
ticket->valid_from_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[2] << 8) |
(data->block[SECOND_PPK_TICKET_OFFSET].data[1]);
ticket->valid_till_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[4] << 8) |
(data->block[SECOND_PPK_TICKET_OFFSET].data[3]);
ticket->tap_data = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[0];
ticket->ppk_cnt = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[10];
ticket->trip_start_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[3]);
ticket->trip_end_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[6]);
}
ticket->first_ticket_marker = data->block[FIRST_PPK_TICKET_OFFSET].data[0];
ticket->second_ticket_marker = data->block[SECOND_PPK_TICKET_OFFSET].data[0];
const uint32_t valid_from_timestamp =
PPK_WHOLE_EPOCH_START + ticket->valid_from_data * SECONDS_IN_A_DAY;
const uint32_t valid_till_timestamp =
PPK_WHOLE_EPOCH_START + ticket->valid_till_data * SECONDS_IN_A_DAY;
const uint32_t tap_timestamp =
PPK_CURRENT_EPOCH_START + ticket->tap_data * SECONDS_IN_A_MINUTE;
datetime_timestamp_to_datetime(valid_from_timestamp, &ticket->valid_from);
datetime_timestamp_to_datetime(valid_till_timestamp, &ticket->valid_till);
datetime_timestamp_to_datetime(tap_timestamp, &ticket->tap_time);
resolve_station_name(storage, ticket);
}
static void
printf_transport_card(FuriString* parsed_data, TicketData* ticket, bool ticket_number) {
if(ticket->departure_uic == 0x0000) {
furi_string_cat_printf(
parsed_data,
"\e#Unknown SKPPK Card\n NO TICKET DATA FOUND \n\nTHE TICKET IS NOT ISSUED\nOR LAYOUT IS UNKNOWN\n");
return;
} else {
if(ticket_number == 0) {
furi_string_cat_printf(parsed_data, "\e#SKPPK Transport Card\n");
switch(ticket->first_ticket_marker) {
case 0x02:
furi_string_cat_printf(parsed_data, "Type:> 5 days unlim.");
break;
case 0x06:
furi_string_cat_printf(parsed_data, "Type:> 10 rides");
break;
case 0x18:
furi_string_cat_printf(parsed_data, "Type:> 30 days unlim.");
break;
case 0x1B:
furi_string_cat_printf(parsed_data, "Type:> 20 rides.");
break;
default:
furi_string_cat_printf(
parsed_data, "Type: Unknown, 0x%02X\n", ticket->first_ticket_marker);
}
} else {
furi_string_cat_printf(parsed_data, "\e#Second Ticket:\n");
switch(ticket->second_ticket_marker) {
case 0x02:
furi_string_cat_printf(parsed_data, "Type:> 5 days unlim.");
break;
case 0x06:
furi_string_cat_printf(parsed_data, "Type:> 10 rides");
break;
case 0x18:
furi_string_cat_printf(parsed_data, "Type:> 30 days unlim.");
break;
case 0x1B:
furi_string_cat_printf(parsed_data, "Type:> 20 rides.");
break;
default:
furi_string_cat_printf(
parsed_data, "Type: Unknown, 0x%02X", ticket->second_ticket_marker);
}
}
furi_string_cat_printf(
parsed_data,
"\nFrom:>%s\nTo:>%s\nValid From: %02d-%02d-%04d\nValid thru: %02d-%02d-%04d\n",
furi_string_get_cstr(ticket->departure_name),
furi_string_get_cstr(ticket->destination_name),
ticket->valid_from.day,
ticket->valid_from.month,
ticket->valid_from.year,
ticket->valid_till.day,
ticket->valid_till.month,
ticket->valid_till.year);
if(ticket->value_data > 0) {
furi_string_cat_printf(parsed_data, "Rides remain: %02d\n", ticket->value_data);
}
switch(ticket->current_status) {
case 0x00:
furi_string_cat_printf(parsed_data, "Status:> NOT USED\n");
break;
case 0x80:
furi_string_cat_printf(
parsed_data,
"Status:> ENTERED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n",
furi_string_get_cstr(ticket->trip_start_sta_name),
ticket->tap_time.day,
ticket->tap_time.month,
ticket->tap_time.year,
ticket->tap_time.hour,
ticket->tap_time.minute,
ticket->ppk_cnt);
break;
case 0x1F:
furi_string_cat_printf(
parsed_data,
"Status:> EXITED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n",
furi_string_get_cstr(ticket->trip_end_sta_name),
ticket->tap_time.day,
ticket->tap_time.month,
ticket->tap_time.year,
ticket->tap_time.hour,
ticket->tap_time.minute,
ticket->ppk_cnt);
break;
default:
furi_string_cat_printf(
parsed_data,
"Status:> UNKNOWN (%02X)\nPPK CNT: %03d",
ticket->current_status,
ticket->ppk_cnt);
break;
}
}
}
bool sk_tk_verify(Nfc* nfc) {
const uint8_t verify_sector = 19;
const uint8_t block_num = mf_classic_get_first_block_num_of_sector(verify_sector);
MfClassicKey key = {};
bit_lib_num_to_bytes_be(t_card_4k[verify_sector].a, COUNT_OF(key.data), key.data);
MfClassicAuthContext auth_ctx = {};
MfClassicError error =
mf_classic_poller_sync_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_ctx);
return error == MfClassicErrorNone;
}
static bool sk_tk_read(Nfc* nfc, NfcDevice* device) {
furi_assert(nfc);
furi_assert(device);
MfClassicData* data = mf_classic_alloc();
nfc_device_copy_data(device, NfcProtocolMfClassic, data);
bool is_read = false;
MfClassicType type = MfClassicType4k;
MfClassicError error = mf_classic_poller_sync_detect_type(nfc, &type);
if(error == MfClassicErrorNone) {
data->type = type;
MfClassicDeviceKeys keys = {};
for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
bit_lib_num_to_bytes_be(t_card_4k[i].a, sizeof(MfClassicKey), keys.key_a[i].data);
FURI_BIT_SET(keys.key_a_mask, i);
bit_lib_num_to_bytes_be(t_card_4k[i].b, sizeof(MfClassicKey), keys.key_b[i].data);
FURI_BIT_SET(keys.key_b_mask, i);
}
error = mf_classic_poller_sync_read(nfc, &keys, data);
if(error != MfClassicErrorNotPresent) {
nfc_device_set_data(device, NfcProtocolMfClassic, data);
is_read = (error == MfClassicErrorNone);
}
}
mf_classic_free(data);
return is_read;
}
static bool sk_tk_parse(const NfcDevice* device, FuriString* parsed_data) {
furi_assert(device);
const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
bool parsed = false;
do {
MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, 19);
uint64_t key = bit_lib_bytes_to_num_be(sec_tr->key_a.data, 6);
if(key != t_card_4k[19].a) break;
Storage* storage = furi_record_open(RECORD_STORAGE);
TicketData primary_ticket = {0};
primary_ticket.departure_name = furi_string_alloc();
primary_ticket.destination_name = furi_string_alloc();
primary_ticket.trip_start_sta_name = furi_string_alloc();
primary_ticket.trip_end_sta_name = furi_string_alloc();
extract_ppk_data(storage, data, &primary_ticket, 0);
printf_transport_card(parsed_data, &primary_ticket, false);
furi_string_free(primary_ticket.departure_name);
furi_string_free(primary_ticket.destination_name);
furi_string_free(primary_ticket.trip_start_sta_name);
furi_string_free(primary_ticket.trip_end_sta_name);
if(primary_ticket.second_ticket_marker != 0) {
TicketData secondary_ticket = {0};
secondary_ticket.departure_name = furi_string_alloc();
secondary_ticket.destination_name = furi_string_alloc();
secondary_ticket.trip_start_sta_name = furi_string_alloc();
secondary_ticket.trip_end_sta_name = furi_string_alloc();
extract_ppk_data(storage, data, &secondary_ticket, 1);
printf_transport_card(parsed_data, &secondary_ticket, true);
furi_string_free(primary_ticket.departure_name);
furi_string_free(primary_ticket.destination_name);
furi_string_free(primary_ticket.trip_start_sta_name);
furi_string_free(primary_ticket.trip_end_sta_name);
}
furi_record_close(RECORD_STORAGE);
parsed = true;
} while(false);
return parsed;
}
static const NfcSupportedCardsPlugin sk_tk_plugin = {
.protocol = NfcProtocolMfClassic,
.verify = sk_tk_verify,
.read = sk_tk_read,
.parse = sk_tk_parse,
};
static const FlipperAppPluginDescriptor sk_tk_plugin_descriptor = {
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
.entry_point = &sk_tk_plugin,
};
const FlipperAppPluginDescriptor* sk_tk_plugin_ep(void) {
return &sk_tk_plugin_descriptor;
}
@@ -0,0 +1,449 @@
//Based on parsers written by Leptoptilos and Assasinfil. Also, thanks to WillyJL (<me@willyjl.dev>) for help!
#include "nfc_supported_card_plugin.h"
#include <flipper_application/flipper_application.h>
#include <nfc/nfc_device.h>
#include <bit_lib/bit_lib.h>
#include <datetime.h>
#include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
#include <flipper_format/flipper_format.h>
#define PPK_WHOLE_EPOCH_START 946684800 //2000-01-01
#define PPK_CURRENT_EPOCH_START 1388534400 //1388530800 //2014-01-01
#define SECONDS_IN_A_DAY 86400
#define SECONDS_IN_A_MINUTE 60
#define FIRST_PPK_TICKET_OFFSET 76
#define FIRST_TICKET_VALUE_BLOCK 77
#define SECOND_PPK_TICKET_OFFSET 88
#define SECOND_TICKET_VALUE_BLOCK 89
typedef struct {
uint64_t a;
uint64_t b;
} MfClassicKeyPair;
typedef struct {
uint16_t departure_uic;
uint16_t destination_uic;
uint16_t trip_start_uic;
uint16_t trip_end_uic;
FuriString* departure_name;
FuriString* destination_name;
FuriString* trip_start_sta_name;
FuriString* trip_end_sta_name;
uint8_t value_data;
uint8_t current_status;
uint16_t valid_from_data;
uint16_t valid_till_data;
DateTime valid_from;
DateTime valid_till;
uint32_t tap_data;
DateTime tap_time;
uint8_t first_ticket_marker;
uint8_t second_ticket_marker;
uint8_t ppk_cnt;
} TicketData;
static const MfClassicKeyPair so_card_2k[] = {
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B7B5B8B4B5}, //0
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B6B2B9B8B7}, //1
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B2B4B4B4B0}, //2
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B3B5B2B2B2}, //3
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B8B4B8B0B9}, //4
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B3B7B9B8B6}, //5
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B6B5B5B5B6}, //6
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B7B0B0B2B0}, //7
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B1B2B8B0}, //8
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B1B8B3B2}, //9
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B0B3B1B8}, //10
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B2B6B7B5B2}, //11
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B9B2B0B2B0}, //12
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B7B8B8B7B3}, //13
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B3B4B9B2B0}, //14
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B7B2B0B0B9}, //15
{.a = 0xA94CAB611187, .b = 0x389109BD1D82}, //16
{.a = 0xBF4280329F11, .b = 0x28D9EDD2096D}, //17
{.a = 0xDE6BD90BD6B0, .b = 0x94866C16E9A4}, //18
{.a = 0x2EA9493CAA7C, .b = 0x5068BCE2BC1C}, //19
{.a = 0x15A41BA53F6C, .b = 0x3BD3CF43571C}, //20
{.a = 0x1290FFD80DB5, .b = 0xD821B7916B7E}, //21
{.a = 0x68C1A07E96A9, .b = 0x2B3323E75750}, //22
{.a = 0xC699831AB307, .b = 0xCD7F7E9111F1}, //23
{.a = 0x4E5884BF23E9, .b = 0x2287812A6AEE}, //24
{.a = 0xC55212F716DC, .b = 0x594E368CCEFF}, //25
{.a = 0x6EF127E674B1, .b = 0xDD21C8D3E0B9}, //26
{.a = 0xFB79FAF4B55C, .b = 0xFE52B3B2A93B}, //27
{.a = 0x6CF85CDFF647, .b = 0xCCAD7C41FC8A}, //28
{.a = 0x591F6C130F91, .b = 0x2D2B734ECF91}, //29
{.a = 0xEEB83529B79B, .b = 0xCB14E70EBA38}, //30
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //31
};
static inline void sz_uic_to_sta(Storage* storage, const char* file_name, TicketData* ticket) {
FlipperFormat* file = flipper_format_file_alloc(storage);
FuriString* departure_uic = furi_string_alloc_printf("%04X", ticket->departure_uic);
FuriString* destination_uic = furi_string_alloc_printf("%04X", ticket->destination_uic);
FuriString* trip_start_uic = furi_string_alloc_printf("%04X", ticket->trip_start_uic);
FuriString* trip_end_uic = furi_string_alloc_printf("%04X", ticket->trip_end_uic);
if(flipper_format_file_open_existing(file, file_name)) {
flipper_format_read_string(
file, furi_string_get_cstr(departure_uic), ticket->departure_name);
flipper_format_rewind(file);
flipper_format_read_string(
file, furi_string_get_cstr(destination_uic), ticket->destination_name);
flipper_format_rewind(file);
flipper_format_read_string(
file, furi_string_get_cstr(trip_start_uic), ticket->trip_start_sta_name);
flipper_format_rewind(file);
flipper_format_read_string(
file, furi_string_get_cstr(trip_end_uic), ticket->trip_end_sta_name);
}
flipper_format_free(file);
furi_string_free(departure_uic);
furi_string_free(destination_uic);
furi_string_free(trip_start_uic);
furi_string_free(trip_end_uic);
}
// Function to resolve station names for a ticket, and if not found, set to "1E" + UIC code
static void resolve_station_name(Storage* storage, TicketData* ticket) {
sz_uic_to_sta(storage, EXT_PATH("nfc/assets/sz_id.nfc"), ticket);
if(furi_string_utf8_length(ticket->departure_name) <= 2) {
furi_string_printf(ticket->departure_name, "1E%04X", ticket->departure_uic);
}
if(furi_string_utf8_length(ticket->destination_name) <= 2) {
furi_string_printf(ticket->destination_name, "1E%04X", ticket->destination_uic);
}
if(furi_string_utf8_length(ticket->trip_start_sta_name) <= 2) {
furi_string_printf(ticket->trip_start_sta_name, "1E%04X", ticket->trip_start_uic);
}
if(furi_string_utf8_length(ticket->trip_end_sta_name) <= 2) {
furi_string_printf(ticket->trip_end_sta_name, "1E%04X", ticket->trip_end_uic);
}
}
static inline void extract_ppk_data(
Storage* storage,
const MfClassicData* data,
TicketData* ticket,
bool ticket_number) {
if(ticket_number == 0) {
ticket->departure_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[6] << 8) |
(data->block[FIRST_PPK_TICKET_OFFSET].data[5]);
ticket->destination_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[9] << 8) |
(data->block[FIRST_PPK_TICKET_OFFSET].data[8]);
ticket->value_data = data->block[FIRST_TICKET_VALUE_BLOCK].data[0];
ticket->current_status = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[8];
ticket->valid_from_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[2] << 8) |
(data->block[FIRST_PPK_TICKET_OFFSET].data[1]);
ticket->valid_till_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[4] << 8) |
(data->block[FIRST_PPK_TICKET_OFFSET].data[3]);
ticket->tap_data = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[0];
ticket->ppk_cnt = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[10];
ticket->trip_start_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[3]);
ticket->trip_end_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[6]);
} else {
ticket->departure_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[6] << 8) |
(data->block[SECOND_PPK_TICKET_OFFSET].data[5]);
ticket->destination_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[9] << 8) |
(data->block[SECOND_PPK_TICKET_OFFSET].data[8]);
ticket->value_data = data->block[SECOND_TICKET_VALUE_BLOCK].data[0];
ticket->current_status = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[8];
ticket->valid_from_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[2] << 8) |
(data->block[SECOND_PPK_TICKET_OFFSET].data[1]);
ticket->valid_till_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[4] << 8) |
(data->block[SECOND_PPK_TICKET_OFFSET].data[3]);
ticket->tap_data = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[0];
ticket->ppk_cnt = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[10];
ticket->trip_start_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[3]);
ticket->trip_end_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[6]);
}
ticket->first_ticket_marker = data->block[FIRST_PPK_TICKET_OFFSET].data[0];
ticket->second_ticket_marker = data->block[SECOND_PPK_TICKET_OFFSET].data[0];
const uint32_t valid_from_timestamp =
PPK_WHOLE_EPOCH_START + ticket->valid_from_data * SECONDS_IN_A_DAY;
const uint32_t valid_till_timestamp =
PPK_WHOLE_EPOCH_START + ticket->valid_till_data * SECONDS_IN_A_DAY;
const uint32_t tap_timestamp =
PPK_CURRENT_EPOCH_START + ticket->tap_data * SECONDS_IN_A_MINUTE;
datetime_timestamp_to_datetime(valid_from_timestamp, &ticket->valid_from);
datetime_timestamp_to_datetime(valid_till_timestamp, &ticket->valid_till);
datetime_timestamp_to_datetime(tap_timestamp, &ticket->tap_time);
resolve_station_name(storage, ticket);
}
static inline bool is_accompany_card(uint16_t departure_uic, uint16_t destination_uic) {
return departure_uic == destination_uic;
}
static void printf_accompany_card(TicketData* ticket, FuriString* parsed_data) {
if(ticket->departure_uic == 0x0000) {
furi_string_cat_printf(
parsed_data,
"\e#Unknown SZPPK Card\n NO TICKET DATA FOUND \n\nTHE TICKET IS NOT ISSUED\nOR LAYOUT IS UNKNOWN\n");
return;
} else {
furi_string_cat_printf(
parsed_data,
"\e#SZPPK Accompany Card\nValid on: %02d-%02d-%04d\nStation: > %s\n",
ticket->valid_from.day,
ticket->valid_from.month,
ticket->valid_from.year,
furi_string_get_cstr(ticket->departure_name));
switch(ticket->current_status) {
case 0x00:
furi_string_cat_printf(
parsed_data, "Status:> NOT USED\nPPK CNT: %03d\n", ticket->ppk_cnt);
break;
case 0x80:
furi_string_cat_printf(
parsed_data,
"Status:> ENTERED STATION\nChecked in at:> %02d:%02d\nPPK CNT: %03d\n",
ticket->tap_time.hour,
ticket->tap_time.minute,
ticket->ppk_cnt);
break;
case 0x1E:
furi_string_cat_printf(
parsed_data,
"Status:> EXITED STATION\nChecked out at:> %02d:%02d\nPPK CNT: %03d\n",
ticket->tap_time.hour,
ticket->tap_time.minute,
ticket->ppk_cnt);
break;
default:
furi_string_cat_printf(
parsed_data, "Status:> UNKNOWN\nPPK CNT: %03d\n", ticket->ppk_cnt);
break;
}
}
}
static void
printf_transport_card(FuriString* parsed_data, TicketData* ticket, bool ticket_number) {
if(ticket->departure_uic == 0x0000) {
furi_string_cat_printf(
parsed_data,
"\e#Unknown SZPPK Card\n NO TICKET DATA FOUND \n\nTHE TICKET IS NOT ISSUED\nOR LAYOUT IS UNKNOWN\n");
return;
} else {
if(ticket_number == 0) {
furi_string_cat_printf(parsed_data, "\e#SZPPK Transport Card\n");
switch(ticket->first_ticket_marker) {
case 0x02:
furi_string_cat_printf(parsed_data, "Type:> 5 days unlim.");
break;
case 0x06:
furi_string_cat_printf(parsed_data, "Type:> 10 rides");
break;
case 0x18:
furi_string_cat_printf(parsed_data, "Type:> 30 days unlim.");
break;
case 0x1B:
furi_string_cat_printf(parsed_data, "Type:> 20 rides.");
break;
default:
furi_string_cat_printf(
parsed_data, "Type: Unknown, 0x%02X\n", ticket->first_ticket_marker);
}
} else {
furi_string_cat_printf(parsed_data, "\e#Second Ticket:\n");
switch(ticket->second_ticket_marker) {
case 0x02:
furi_string_cat_printf(parsed_data, "Type:> 5 days unlim.");
break;
case 0x06:
furi_string_cat_printf(parsed_data, "Type:> 10 rides");
break;
case 0x18:
furi_string_cat_printf(parsed_data, "Type:> 30 days unlim.");
break;
case 0x1B:
furi_string_cat_printf(parsed_data, "Type:> 20 rides.");
break;
default:
furi_string_cat_printf(
parsed_data, "Type: Unknown, 0x%02X", ticket->second_ticket_marker);
}
}
furi_string_cat_printf(
parsed_data,
"\nFrom:>%s\nTo:>%s\nValid From: %02d-%02d-%04d\nValid thru: %02d-%02d-%04d",
furi_string_get_cstr(ticket->departure_name),
furi_string_get_cstr(ticket->destination_name),
ticket->valid_from.day,
ticket->valid_from.month,
ticket->valid_from.year,
ticket->valid_till.day,
ticket->valid_till.month,
ticket->valid_till.year);
if(ticket->value_data > 0) {
furi_string_cat_printf(parsed_data, "Rides remain: %02d\n", ticket->value_data);
}
switch(ticket->current_status) {
case 0x00:
furi_string_cat_printf(
parsed_data, "Status:> NOT USED\nPPK CNT: %03d\n", ticket->ppk_cnt);
break;
case 0x80:
furi_string_cat_printf(
parsed_data,
"\nStatus:> ENTERED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n",
furi_string_get_cstr(ticket->trip_start_sta_name),
ticket->tap_time.day,
ticket->tap_time.month,
ticket->tap_time.year,
ticket->tap_time.hour,
ticket->tap_time.minute,
ticket->ppk_cnt);
break;
case 0x1E:
furi_string_cat_printf(
parsed_data,
"\nStatus:> EXITED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n",
furi_string_get_cstr(ticket->trip_end_sta_name),
ticket->tap_time.day,
ticket->tap_time.month,
ticket->tap_time.year,
ticket->tap_time.hour,
ticket->tap_time.minute,
ticket->ppk_cnt);
break;
default:
furi_string_cat_printf(
parsed_data,
"Status:> UNKNOWN (%02X)\nPPK CNT: %03d",
ticket->current_status,
ticket->ppk_cnt);
break;
}
}
}
bool szppk_so_verify(Nfc* nfc) {
const uint8_t verify_sector = 19;
const uint8_t block_num = mf_classic_get_first_block_num_of_sector(verify_sector);
MfClassicKey key = {};
bit_lib_num_to_bytes_be(so_card_2k[verify_sector].a, COUNT_OF(key.data), key.data);
MfClassicAuthContext auth_ctx = {};
MfClassicError error =
mf_classic_poller_sync_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_ctx);
return error == MfClassicErrorNone;
}
static bool szppk_so_read(Nfc* nfc, NfcDevice* device) {
furi_assert(nfc);
furi_assert(device);
MfClassicData* data = mf_classic_alloc();
nfc_device_copy_data(device, NfcProtocolMfClassic, data);
bool is_read = false;
MfClassicType type = MfClassicType1k;
MfClassicError error = mf_classic_poller_sync_detect_type(nfc, &type);
if(error == MfClassicErrorNone) {
data->type = type;
MfClassicDeviceKeys keys = {};
for(size_t i = 0; i < 32; i++) {
bit_lib_num_to_bytes_be(so_card_2k[i].a, sizeof(MfClassicKey), keys.key_a[i].data);
FURI_BIT_SET(keys.key_a_mask, i);
bit_lib_num_to_bytes_be(so_card_2k[i].b, sizeof(MfClassicKey), keys.key_b[i].data);
FURI_BIT_SET(keys.key_b_mask, i);
}
error = mf_classic_poller_sync_read(nfc, &keys, data);
if(error != MfClassicErrorNotPresent) {
nfc_device_set_data(device, NfcProtocolMfClassic, data);
is_read = (error == MfClassicErrorNone);
}
}
mf_classic_free(data);
return is_read;
}
static bool szppk_so_parse(const NfcDevice* device, FuriString* parsed_data) {
furi_assert(device);
const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
bool parsed = false;
do {
MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, 19);
uint64_t key = bit_lib_bytes_to_num_be(sec_tr->key_a.data, 6);
if(key != so_card_2k[19].a) break;
Storage* storage = furi_record_open(RECORD_STORAGE);
TicketData primary_ticket = {0};
primary_ticket.departure_name = furi_string_alloc();
primary_ticket.destination_name = furi_string_alloc();
primary_ticket.trip_start_sta_name = furi_string_alloc();
primary_ticket.trip_end_sta_name = furi_string_alloc();
extract_ppk_data(storage, data, &primary_ticket, 0);
if(is_accompany_card(primary_ticket.departure_uic, primary_ticket.destination_uic)) {
printf_accompany_card(&primary_ticket, parsed_data);
} else {
printf_transport_card(parsed_data, &primary_ticket, false);
}
furi_string_free(primary_ticket.departure_name);
furi_string_free(primary_ticket.destination_name);
furi_string_free(primary_ticket.trip_start_sta_name);
furi_string_free(primary_ticket.trip_end_sta_name);
if(primary_ticket.second_ticket_marker != 0) {
TicketData secondary_ticket = {0};
secondary_ticket.departure_name = furi_string_alloc();
secondary_ticket.destination_name = furi_string_alloc();
secondary_ticket.trip_start_sta_name = furi_string_alloc();
secondary_ticket.trip_end_sta_name = furi_string_alloc();
extract_ppk_data(storage, data, &secondary_ticket, 1);
printf_transport_card(parsed_data, &secondary_ticket, true);
furi_string_free(primary_ticket.departure_name);
furi_string_free(primary_ticket.destination_name);
furi_string_free(primary_ticket.trip_start_sta_name);
furi_string_free(primary_ticket.trip_end_sta_name);
}
furi_record_close(RECORD_STORAGE);
parsed = true;
} while(false);
return parsed;
}
static const NfcSupportedCardsPlugin szppk_so_plugin = {
.protocol = NfcProtocolMfClassic,
.verify = szppk_so_verify,
.read = szppk_so_read,
.parse = szppk_so_parse,
};
static const FlipperAppPluginDescriptor szppk_so_plugin_descriptor = {
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
.entry_point = &szppk_so_plugin,
};
const FlipperAppPluginDescriptor* szppk_so_plugin_ep(void) {
return &szppk_so_plugin_descriptor;
}
@@ -39,7 +39,7 @@ bool two_cities_verify(Nfc* nfc) {
bool verified = false;
do {
const uint8_t verify_sector = 4;
const uint8_t verify_sector = 9;
uint8_t block_num = mf_classic_get_first_block_num_of_sector(verify_sector);
FURI_LOG_D(TAG, "Verifying sector %u", verify_sector);
@@ -0,0 +1,124 @@
Filetype: Flipper NFC resources
Version: 1
# staID: Name
9468: KULISHKI
9469: KRASNOE
968C: UGLICH
97A7: 40 KM
9C61: 29 KM
AB91: YAROSLAVL-GL.
AB92: YAROSLAVL-PASS
AB93: PRIVOLZHYE
AB94: FILINO
ABD5: 300 KM
ABFF: 4 KM
AC4D: SKALINO
AC4F: MARFINO
AC50: PRECHISTOE
AC51: MAKAROVO
AC53: PURSHEVO
AC54: RODIONOVO
AC55: MASLOVO
AC56: NEKOUZ
AC57: SHESTIKHINO
AC59: VOLGA
AC5A: KOBOSTOVO
AC5B: TIKHMENEVO
AC5C: LOM
AC5D: VAULOVO
AC5E: CHEBAKOVO
AC64: LYUTOVO
AC65: TOSCHIKHA
AC66: BURMAKINO
AC68: PANTELEEVO
AC69: PUTYATINO
AC6A: PUCHKOVSKIY
AC6B: UTKINO
AC6F: KOZMODEMNSK
AC70: KOROMYSLOVO
AC71: SEMIBRATOVO
AC72: ROSTOV-YARSLV.
AC73: PETROVSK
AC74: SIL'NITSY
AC75: ITLAR'
AC77: BEKLEMISHEVO
AC78: RYAZANTSEVO
AC79: SHUSHKOVO
AC7A: BERENDEEVO
AC8A: RYBINSK-PASS.
ACA6: SEKSHA
ACA7: LYUBIM
ACA9: ZHAROK
ACAA: SOT'
ACAB: LUNKA
ACD4: DANOLOV
ACE9: DUNAIKA
ACEA: LIPOVAYA GORA
AD03: 155 KM
AD04: 147 KM
AD06: 187 KM
AD07: 231 KM
AD08: 259 KM
AD09: 268 KM
AD0A: 274 KM
AD0B: DEPO
AD0C: 310 KM
AD0D: 322 KM
AD0E: 331 KM
AD0F: 343 KM
AD10: 351 KM
AD45: 361 KM
AD46: 370 KM
AD47: 378 KM
AD48: 388 KM
AD4A: 399 KM
AD4B: 409 KM
AD4C: 419 KM
AEB1: KOCHENYATINO
AEB3: BAGRIMOVO
AEB4: REKA
AEB5: SOSNOVTSY
AEB9: DOGADTSEVO
AEBD: KUDRYAVTSEVO
AEBF: PROSVET
AEC1: GAVROLOV YAM
AEC3: LEVTSOVO
AEC7: HOZHAEVO
AECA: TSYBRINO
AECB: ROKSHA
AEF4: 69 KM
AEF5: 56 KM
AEF6: 323 KM
AEFA: 287 KM
AEFB: 296 KM
AEFC: 305 KM
AEFD: 310 KM
AEFE: 318 KM
AF0E: 265 KM
AF0F: 285 KM
B057: MAKAROVSKAYA
B05A: USHAKOVO
B05F: VAREGOVO
B07A: 306 KM
B07B: 296 KM
B07C: YURINSKIY
B07D: TOROPOVO
B07E: PINYAGI
B07F: CHIZHOVO
B080: KLINTSEVO
B081: TENINO
B082: MOLOT
B083: TELISCHEVO
B084: SUHAREZH
B086: 324 KM
B087: POLYANKI
B08A: DEBOLOVSKAYA
B08B: 2 KM
B090: RUSHA
B0AC: KOTOROSL'
B0D1: 403 KM
B0D2: SHOLOSOVO
B0D3: MURINSKOE
B0D4: ONAN'JINO
B0D5: L'NOZAVOD
B0F3: 359 KM
@@ -0,0 +1,85 @@
Filetype: Flipper NFC resources
Version: 1
# staID: Name
8377: OP 1725 KM
8350: STEKOL'NYI
834F: SELHOZTECHNIKA
834D: KANGLY
834B: OP 1861 KM
834A: OP 1856 KM
8349: OP 1842 KM
8348: OP 1832 KM
8347: OP 1806 KM
8344: OP 44 KM
8343: OP 129 KM
8288: SUVOROVSKAYA
8188: NESKUCHNYI
8183: OP 1741 KM
8182: KOCHUBEEVSKII
817E: SURKUL
817D: OP 1823 KM
8177: TERSKII
8176: KRASNAYA
8175: TEPLAYA RECHKA
8165: DVORTSOVYI
8164: PIONER
8161: ORBEL'YANOVO
815F: DZHEMUHA
815D: ETOKA
8158: OBIL'NYI
8157: NINY
813C: STAVROPOL
8104: KARMALINOVSKII
8084: KRASN.DEREVNIA
807E: MIHAILOVSKAIA
7FF8: ZMEIKA
7FF6: MASHUK
7FF5: LERMONTOVSKII
7FF4: NOVOPIATIGORSK
7FF3: SKACHKI
7FF2: ZOLOTUSHKA
7FF1: BELYI UGOL'
7FF0: PODKUMOK
7FEF: MINUTKA
7FBA: PLAKSEIKA
7FB9: MASLOV KUT
7FB8: ZELENOKUMSK
7FB7: KUMA
7FB6: GEORGIEVSK
7FA6: VIAZNIKI
7FA4: YAGODKA
7FA1: MAIAK
7F9B: BEDENNOVSK
7F6A: PALAGIADA
7F69: RYZDVIANAIA
7F68: PEREDOVAIA
7F65: GRIGOROPOLISSK.
7F5C: NEVINNOMYSSK.
7F2B: INOZEMTSEVO
7F2A: MIN.VODY
7F1B: ZHELEZNOVODSK
7F17: BESHTAU
7EFD: RASSHEVATKA
7EBC: PIATIGORSK
7EB2: KISLOVODSK
7EA5: ZOLSKII
7EA4: VINOGRADNAIA
7EA2: KUMAGORSK
7E9F: NAGUTSKAIA
7E9D: KURSAVKA
7E9B: KIAN
7E9A: ZELENCHUK
7E96: BOGLOVSKAIA
7E95: OVECHKA
8162: KURSHAVA
7F97: CHUKOTSKII
83AE: DEBRI
8306: DEGTIAREVSKI
8185: NIVA
8160: OP 1814 KM
815B: CHISTAIA
7FA5: BERMEDSKII
7F93: IZOBILNAIA
7F64: KRASNOKUBANSK.
7ED0: ESSENTUKI
7EA6: APOLLONSKAIA
@@ -0,0 +1,151 @@
Filetype: Flipper NFC resources
Version: 1
# staID: Name
9426: LADOZH.VOKZ.
9421: MOSKOV.VOKZ.
9423: VITEBS.VOKZ
9424: FINLND.VOKZ
9425: BALT.VOKZ
948B: IM.MOROZOVA
94B4: TOSNO
94B5: SABLINO
94BE: GORY
94C0: IZHORY
94C1: RYBATSKOE
24C2: OBUKHOVO
94C3: SLAVYANKA
94C4: KOLPINO
94C9: MSHINSKAYA
94CC: STROGANOVO
94CD: SUI'DA
94CE: GATCHINA BLT.
94D4: VYRITSA
94D5: PAVLOVSK
94D6: TSARSKOE SELO
94E2: KALISHCHE
94E3: LEBYAZHYE
94E4: BOLSH.IZHORA
9507: BELOOSTROV
9508: ZELENOGORSK
9511: RUCH'I
9512: TOKSOVO
9513: OSEL'KI
9514: PERI
9515: GRUZINO
9516: VASKELOVO
9517: OREKHOVO
9518: PETYAJARVI
9519: LOSEVO
951A: GROMOVO
951F: KUZNECHNOE
95C3: KAPITOLOVO
969A: MGA
969C: LUGA
969D: TOLMACHEVO
969E: SIVERSKAYA
969F: GATCHINA VRSH.
96A1: OREDEZH
96A3: SLANTSY
96A5: ORANIENBAUM 1
96A8: VOLOSOVO
96BD: TIKHVIN
96C0: VOLKHOVSTROY 1
96C1: VOLKHOVSTROY 2
96CA: VYBORG
96CB: SOSNOVO
96CC: PRIOZYORSK
970A: BOLOTISTOE
971D: STAR.DEREVNYA
9721: 54 KM
9723: KOLOSKOVO(79KM)
972C: PARAVOZNY MUZEI
9741: BOROVAYA
97BB: DETSKOSELSKAYA
9809: KUPCHINO
9810: PETROKREPOST'
9811: ALEKSANDROVSK.F
9817: BERNGARDOVKA
981C: PROSPEKT SLAVY
9826: SOSNOVAYA POL.
9829: PUDOST'
982B: LENINSKI' PR.
982D: PAVLOVO-NA-NEVE
9835: LANSKAYA
9838: KANNELYARVI
983A: MELN.RUCHEI
983F: PISKARYOVKA
9849: UDEL'NAYA
9852: BRONKA
9853: VSEVOLOZHSK.
985D: FARFOROVSKAYA
9867: RZHEVKA
9868: KOBRALOVO
986B: AEROPORT
9871: BRONEVAYA
987B: SESTRORETSK
9880: TATIANINO
988A: KUSHELEVKA
9890: RAKH'YA
9894: UL'YANKA
9899: PESOCHNAYA
989A: NAVALOCHNAYA
989E: STREL'NA
98A3: ST.PETERGOF
98A5: KOLTUSHI
98A8: KUZ'MOLOVO
98AD: KRASN. SELO
98AE: SHUSHARY
98BC: GORELOVO
98C1: ROSHCHINO
98CB: LISII' NOS
98D5: DACHNOYE
98D6: PREDPORTOVAYA
98E4: NOVAYA OKHTA
98E9: REPINO
98F3: LEVASHOVO
98FD: NOV.DEREVNYA
98FE: IZHORSK.ZAVOD
9908: VAGANOVO
990C: SHUVALOVO
9911: DUNAI'
9916: IRINOVKA
991C: TARKHOVKA
9923: PUPYSHEVO
9925: TAI'TSY
9926: LAVRIKI
992A: LADOZHSK.OZ.
992D: ROMANOVKA
9936: RAZLIV
9938: KIRILLOVSKOE
9939: OL'GINO
993D: METALLOSTROY
993E: KAVGOLOVO
9943: VOZD.PARK
9944: POST KOVALEVO
9949: LAPPELOVO
994D: KORNEVO
9952: SOLNECHNOYE
9956: MYAGLOVO
995B: OZERKI
995C: KOMAROVO
9969: PROBA
996A: KIRPICH.ZAVOD
9970: KURORT
9975: 67 KM
9976: USHKOVO
9981: DUDERGOF
9989: UNIVERSITETSK.
9992: YAKHTENNAYA
9996: KOVALEVO
9997: DIBUNY
99AF: MANUSHKINO
99CF: PARGOLOVO
99D4: LIGOVO
9C09: DEVYATKINO
9C58: NEV.DUBROVKA
9C5A: ALEKSANDROVSK.
9C5B: GORSKAYA
9D1C: NOV.PETERGOF
9FDC: BORISOVA GRIVA
98B7: SERGIEVO
9957: LAKHTA
@@ -77,6 +77,15 @@ void nfc_scene_mf_ultralight_c_dict_attack_prepare_view(NfcApp* instance) {
// Set attack type to Ultralight C
dict_attack_set_type(instance->dict_attack, DictAttackTypeMfUltralightC);
// Guard: if a previous write phase left a dict handle open, close it now.
// Without this, navigating write->back->read->dict-attack would open the same
// file twice, corrupting VFS state and causing a ViewPort lockup.
if(instance->mf_ultralight_c_dict_context.dict) {
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
instance->mf_ultralight_c_dict_context.dict = NULL;
instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictIdle;
}
if(state == DictAttackStateUserDictInProgress) {
do {
if(!keys_dict_check_presence(NFC_APP_MF_ULTRALIGHT_C_DICT_USER_PATH)) {
@@ -167,6 +176,7 @@ bool nfc_scene_mf_ultralight_c_dict_attack_on_event(void* context, SceneManagerE
nfc_poller_stop(instance->poller);
nfc_poller_free(instance->poller);
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
instance->mf_ultralight_c_dict_context.dict = NULL;
scene_manager_set_scene_state(
instance->scene_manager,
NfcSceneMfUltralightCDictAttack,
@@ -199,6 +209,7 @@ bool nfc_scene_mf_ultralight_c_dict_attack_on_event(void* context, SceneManagerE
nfc_poller_stop(instance->poller);
nfc_poller_free(instance->poller);
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
instance->mf_ultralight_c_dict_context.dict = NULL;
scene_manager_set_scene_state(
instance->scene_manager,
NfcSceneMfUltralightCDictAttack,
@@ -230,6 +241,7 @@ void nfc_scene_mf_ultralight_c_dict_attack_on_exit(void* context) {
NfcSceneMfUltralightCDictAttack,
DictAttackStateUserDictInProgress);
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
instance->mf_ultralight_c_dict_context.dict = NULL;
instance->mf_ultralight_c_dict_context.dict_keys_total = 0;
instance->mf_ultralight_c_dict_context.dict_keys_current = 0;
instance->mf_ultralight_c_dict_context.auth_success = false;
+56 -18
View File
@@ -1,38 +1,76 @@
App(
appid="subghz",
name="Sub-GHz",
apptype=FlipperAppType.APP,
apptype=FlipperAppType.MENUEXTERNAL,
targets=["f7"],
entry_point="subghz_app",
icon="A_Sub1ghz_14",
stack_size=3 * 1024,
order=10,
# Sources separation breaks linking with subghz on internal, commented for now
# sources=[
# "*.c",
# "!helpers/subghz_gps.c",
# "!helpers/minmea.c",
# "!subghz_cli.c",
# "!helpers/subghz_chat.c",
# "!subghz_extended_freq.c",
# ],
# Sources separation breaks linking with subghz on internal
sources=[
"*.c",
"!subghz_fap.c",
"!subghz_gps.c",
"!minmea.c",
"!subghz_cli.c",
"!subghz_chat.c",
"!subghz_extended_freq.c",
"!subghz_gen_info.c",
"!subghz_txrx_create_protocol_key.c",
"!subghz_scene_set_button.c",
"!subghz_scene_set_counter.c",
"!subghz_scene_set_key.c",
"!subghz_scene_set_seed.c",
"!subghz_scene_set_serial.c",
"!subghz_scene_set_type.c",
],
requires=["region"],
resources="resources",
fap_libs=["assets", "hwdrivers"],
fap_icon="icon.png",
fap_category="Sub-GHz",
sdk_headers=["subghz_fap.h"],
# sdk_headers=["subghz_fap.h"],
)
# App(
# appid="subghz_fap",
# name="Sub-GHz",
# apptype=FlipperAppType.EXTERNAL,
# entry_point="subghz_fap",
# stack_size=3 * 1024,
# sources=["subghz_fap.c"],
# fap_icon="icon.png",
# fap_category="Sub-GHz",
# )
App(
appid="subghz_fap",
name="Sub-GHz",
appid="subghz_add_manually",
name="Sub-GHz Add Manually",
apptype=FlipperAppType.EXTERNAL,
entry_point="subghz_fap",
targets=["f7"],
entry_point="subghz_add_manually",
stack_size=3 * 1024,
sources=["subghz_fap.c"],
fap_icon="icon.png",
fap_category="Sub-GHz",
sources=[
"subghz.c",
"subghz_i.c",
"helpers/subghz_gen_info.c",
"helpers/subghz_txrx.c",
"helpers/subghz_txrx_create_protocol_key.c",
"scenes/subghz_scene.c",
"scenes/subghz_scene_set_button.c",
"scenes/subghz_scene_set_counter.c",
"scenes/subghz_scene_set_key.c",
"scenes/subghz_scene_set_seed.c",
"scenes/subghz_scene_set_serial.c",
"scenes/subghz_scene_set_type.c",
"scenes/subghz_scene_save_name.c",
"scenes/subghz_scene_save_success.c",
"scenes/subghz_scene_show_error.c",
"scenes/subghz_scene_show_error_sub.c",
],
cdefines=["SUBGHZ_ADD_MANUALLY"],
fap_category="assets",
)
App(
@@ -58,6 +96,6 @@ App(
targets=["f7"],
apptype=FlipperAppType.STARTUP,
entry_point="subghz_extended_freq",
# sources=["subghz_extended_freq.c"],
sources=["subghz_extended_freq.c"],
order=650,
)
@@ -77,9 +77,13 @@ typedef enum {
SetTypeFaacSLH_868,
SetTypeFaacSLH_433,
SetTypeBFTMitto,
SetTypeErreka433,
SetTypeSomfyTelis,
SetTypeSomfyKeytis,
SetTypeKingGatesStylo4k,
SetTypeBenincaARC,
SetTypeJarolift,
SetTypeDitecGOL4,
SetTypeANMotorsAT4,
SetTypeAlutechAT4N,
SetTypePhoenix_V2_433,
@@ -114,6 +118,19 @@ typedef enum {
SetTypeNovoferm_433_92,
SetTypeHormannEcoStar_433_92,
SetTypeCardinS449_433FM,
SetTypePujol433,
SetTypePujol_Vario433,
SetTypeET_Blue433,
SetTypeET_Blue_Mix433,
SetTypeATA_PTX4_433,
SetTypeSeav433,
SetTypeWisniowski433,
SetTypeFadini433,
SetTypeMc_Garcia433,
SetTypeClemsa_Mutancode433,
SetTypeDoormatic433,
SetTypeElvox433,
SetTypeVerex433,
SetTypeFAACRCXT_433_92,
SetTypeFAACRCXT_868,
SetTypeGeniusBravo433,
@@ -496,14 +496,25 @@ void subghz_scene_set_type_fill_generation_infos(GenInfo* infos_dest, SetType ty
break;
case SetTypeBFTMitto:
gen_info = (GenInfo){
.type = GenKeeloqBFT,
.type = GenKeeloqSeed,
.mod = "AM650",
.freq = 433920000,
.keeloq_bft.serial = key & 0x000FFFFF,
.keeloq_bft.btn = 0x02,
.keeloq_bft.cnt = 0x02,
.keeloq_bft.seed = key & 0x000FFFFF,
.keeloq_bft.manuf = "BFT"};
.keeloq_seed.serial = key & 0x000FFFFF,
.keeloq_seed.btn = 0x02,
.keeloq_seed.cnt = 0x02,
.keeloq_seed.seed = key & 0x000FFFFF,
.keeloq_seed.manuf = "BFT"};
break;
case SetTypeErreka433:
gen_info = (GenInfo){
.type = GenKeeloqSeed,
.mod = "AM650",
.freq = 433920000,
.keeloq_seed.serial = key & 0x000FFFFF,
.keeloq_seed.btn = 0x02,
.keeloq_seed.cnt = 0x02,
.keeloq_seed.seed = key & 0x000FFFFF,
.keeloq_seed.manuf = "Erreka"};
break;
case SetTypeAlutechAT4N:
gen_info = (GenInfo){
@@ -523,6 +534,15 @@ void subghz_scene_set_type_fill_generation_infos(GenInfo* infos_dest, SetType ty
.somfy_telis.btn = 0x02,
.somfy_telis.cnt = 0x03};
break;
case SetTypeSomfyKeytis:
gen_info = (GenInfo){
.type = GenSomfyKeytis,
.mod = "AM650",
.freq = 433420000,
.somfy_keytis.serial = (key & 0x0000FFFF) | 0x00D50000,
.somfy_keytis.btn = 0x04,
.somfy_keytis.cnt = 0x03};
break;
case SetTypeKingGatesStylo4k:
gen_info = (GenInfo){
.type = GenKingGatesStylo4k,
@@ -541,6 +561,24 @@ void subghz_scene_set_type_fill_generation_infos(GenInfo* infos_dest, SetType ty
.beninca_arc.btn = 0x02,
.beninca_arc.cnt = 0x03};
break;
case SetTypeJarolift:
gen_info = (GenInfo){
.type = GenJarolift,
.mod = "AM650",
.freq = 433920000,
.jarolift.serial = key & 0xFFFFF00,
.jarolift.btn = 0x02,
.jarolift.cnt = 0x03};
break;
case SetTypeDitecGOL4:
gen_info = (GenInfo){
.type = GenDitecGOL4,
.mod = "AM650",
.freq = 433920000,
.ditec_gol4.serial = (key & 0x0000FFFF) | 0xCC090000,
.ditec_gol4.btn = 0x01,
.ditec_gol4.cnt = 0xC200};
break;
case SetTypeMotorline433:
gen_info = (GenInfo){
.type = GenKeeloq,
@@ -627,7 +665,7 @@ void subghz_scene_set_type_fill_generation_infos(GenInfo* infos_dest, SetType ty
.mod = "AM650",
.freq = 433920000,
.keeloq.serial = key & 0x00FFFFFF,
.keeloq.btn = 0x06,
.keeloq.btn = 0x09,
.keeloq.cnt = 0x03,
.keeloq.manuf = "Genius_Bravo"};
break;
@@ -671,6 +709,136 @@ void subghz_scene_set_type_fill_generation_infos(GenInfo* infos_dest, SetType ty
.keeloq.cnt = 0x03,
.keeloq.manuf = "Cardin_S449"};
break;
case SetTypePujol433:
gen_info = (GenInfo){
.type = GenKeeloq,
.mod = "AM650",
.freq = 433920000,
.keeloq.serial = key & 0x00FFFFFF,
.keeloq.btn = 0x02,
.keeloq.cnt = 0x03,
.keeloq.manuf = "Pujol"};
break;
case SetTypeET_Blue433:
gen_info = (GenInfo){
.type = GenKeeloq,
.mod = "AM650",
.freq = 433920000,
.keeloq.serial = key & 0x00FFFFFF,
.keeloq.btn = 0x04,
.keeloq.cnt = 0x03,
.keeloq.manuf = "ET_Blue"};
break;
case SetTypeET_Blue_Mix433:
gen_info = (GenInfo){
.type = GenKeeloq,
.mod = "AM650",
.freq = 433920000,
.keeloq.serial = key & 0x00FFFFFF,
.keeloq.btn = 0x04,
.keeloq.cnt = 0x03,
.keeloq.manuf = "ET_Blue_Mix"};
break;
case SetTypeATA_PTX4_433:
gen_info = (GenInfo){
.type = GenKeeloq,
.mod = "AM650",
.freq = 433920000,
.keeloq.serial = key & 0x00FFFFFF,
.keeloq.btn = 0x02,
.keeloq.cnt = 0x03,
.keeloq.manuf = "ATA_PTX4"};
break;
case SetTypePujol_Vario433:
gen_info = (GenInfo){
.type = GenKeeloq,
.mod = "AM650",
.freq = 433920000,
.keeloq.serial = key & 0x00FFFFFF,
.keeloq.btn = 0x02,
.keeloq.cnt = 0x03,
.keeloq.manuf = "Pujol_Vario"};
break;
case SetTypeSeav433:
gen_info = (GenInfo){
.type = GenKeeloq,
.mod = "AM650",
.freq = 433920000,
.keeloq.serial = key & 0x00FFFFFF,
.keeloq.btn = 0x02,
.keeloq.cnt = 0x03,
.keeloq.manuf = "Seav"};
break;
case SetTypeWisniowski433:
gen_info = (GenInfo){
.type = GenKeeloq,
.mod = "AM650",
.freq = 433920000,
.keeloq.serial = key & 0x00FFFFFF,
.keeloq.btn = 0x02,
.keeloq.cnt = 0x03,
.keeloq.manuf = "Wisniowski"};
break;
case SetTypeFadini433:
gen_info = (GenInfo){
.type = GenKeeloq,
.mod = "AM650",
.freq = 433920000,
.keeloq.serial = key & 0x00FFFFFF,
.keeloq.btn = 0x02,
.keeloq.cnt = 0x03,
.keeloq.manuf = "Fadini"};
break;
case SetTypeMc_Garcia433:
gen_info = (GenInfo){
.type = GenKeeloq,
.mod = "AM650",
.freq = 433920000,
.keeloq.serial = key & 0x00FFFFFF,
.keeloq.btn = 0x02,
.keeloq.cnt = 0x03,
.keeloq.manuf = "Mc_Garcia"};
break;
case SetTypeClemsa_Mutancode433:
gen_info = (GenInfo){
.type = GenKeeloq,
.mod = "AM650",
.freq = 433920000,
.keeloq.serial = key & 0x00FFFFFF,
.keeloq.btn = 0x02,
.keeloq.cnt = 0x03,
.keeloq.manuf = "Clemsa_Mutancode"};
break;
case SetTypeDoormatic433:
gen_info = (GenInfo){
.type = GenKeeloq,
.mod = "AM650",
.freq = 433920000,
.keeloq.serial = key & 0x00FFFFFF,
.keeloq.btn = 0x02,
.keeloq.cnt = 0x03,
.keeloq.manuf = "Doormatic"};
break;
case SetTypeElvox433:
gen_info = (GenInfo){
.type = GenKeeloq,
.mod = "AM650",
.freq = 433920000,
.keeloq.serial = key & 0x00FFFFFF,
.keeloq.btn = 0x02,
.keeloq.cnt = 0x03,
.keeloq.manuf = "Elvox"};
break;
case SetTypeVerex433:
gen_info = (GenInfo){
.type = GenKeeloq,
.mod = "AM650",
.freq = 433920000,
.keeloq.serial = key & 0x00FFFFFF,
.keeloq.btn = 0x02,
.keeloq.cnt = 0x03,
.keeloq.manuf = "Verex"};
break;
case SetTypeFAACRCXT_433_92:
gen_info = (GenInfo){
.type = GenKeeloq,
@@ -7,11 +7,14 @@ typedef enum {
GenFaacSLH,
GenKeeloq,
GenCameAtomo,
GenKeeloqBFT,
GenKeeloqSeed,
GenAlutechAt4n,
GenSomfyTelis,
GenSomfyKeytis,
GenKingGatesStylo4k,
GenBenincaARC,
GenJarolift,
GenDitecGOL4,
GenNiceFlorS,
GenSecPlus1,
GenSecPlus2,
@@ -52,7 +55,7 @@ typedef struct {
uint16_t cnt;
uint32_t seed;
const char* manuf;
} keeloq_bft;
} keeloq_seed;
struct {
uint32_t serial;
uint8_t btn;
@@ -63,6 +66,11 @@ typedef struct {
uint8_t btn;
uint16_t cnt;
} somfy_telis;
struct {
uint32_t serial;
uint8_t btn;
uint16_t cnt;
} somfy_keytis;
struct {
uint32_t serial;
uint8_t btn;
@@ -73,6 +81,11 @@ typedef struct {
uint8_t btn;
uint32_t cnt;
} beninca_arc;
struct {
uint32_t serial;
uint8_t btn;
uint16_t cnt;
} jarolift;
struct {
uint32_t serial;
uint8_t btn;
@@ -88,6 +101,11 @@ typedef struct {
uint32_t serial;
uint16_t cnt;
} phoenix_v2;
struct {
uint32_t serial;
uint8_t btn;
uint16_t cnt;
} ditec_gol4;
};
} GenInfo;
+79 -14
View File
@@ -59,11 +59,13 @@ SubGhzTxRx* subghz_txrx_alloc(void) {
instance->worker, (SubGhzWorkerPairCallback)subghz_receiver_decode);
subghz_worker_set_context(instance->worker, instance->receiver);
#ifndef SUBGHZ_ADD_MANUALLY
//set default device External
subghz_devices_init();
instance->radio_device_type = SubGhzRadioDeviceTypeInternal;
instance->radio_device_type =
subghz_txrx_radio_device_set(instance, SubGhzRadioDeviceTypeExternalCC1101);
#endif
return instance;
}
@@ -71,12 +73,14 @@ SubGhzTxRx* subghz_txrx_alloc(void) {
void subghz_txrx_free(SubGhzTxRx* instance) {
furi_assert(instance);
#ifndef SUBGHZ_ADD_MANUALLY
if(instance->radio_device_type != SubGhzRadioDeviceTypeInternal) {
subghz_txrx_radio_device_power_off(instance);
subghz_devices_end(instance->radio_device);
}
subghz_devices_deinit();
#endif
subghz_worker_free(instance->worker);
subghz_receiver_free(instance->receiver);
@@ -104,6 +108,7 @@ void subghz_txrx_set_preset(
size_t preset_data_size) {
furi_assert(instance);
furi_string_set(instance->preset->name, preset_name);
SubGhzRadioPreset* preset = instance->preset;
preset->frequency = frequency;
preset->latitude = latitude;
@@ -112,6 +117,65 @@ void subghz_txrx_set_preset(
preset->data_size = preset_data_size;
}
uint8_t*
subghz_txrx_set_tx_power(uint8_t* preset_data, size_t preset_data_size, uint8_t tx_power) {
#define PRESET_POWER_OFFSET_FM 8
#define PRESET_POWER_OFFSET_AM 7
#define TX_PATABLE_OFFSET_AM 8
#define TX_PATABLE_COUNT 17
//I had to skip the +10dBM and -6dBm Values, use only ones AM/FM have in common.
//Highest Value is 12dBm for AM, 10 for FM. So Menu needs to reflect that.
static const uint8_t tx_pa_table[TX_PATABLE_COUNT] = {
0,
0xC0, //12dBm
0xCD, //7dBm
0x86, //5dBm
0x50, //0dBm
0x26, // -10dBm
0x1D, // -15dBm
0x17, //-20dBm
0x03, //-30dBm
0xC0, // 10dBm
0xC8, //7dBm
0x84, //5dBm
0x60, //0dBm
0x34, //-10dBm
0x1D, //-15dBm
0x0E, // -20dBm
0x12, //-30dBm
};
//Grab the AM and FM byte now, so we can do proper checks.
uint8_t fm_byte = preset_data[preset_data_size - PRESET_POWER_OFFSET_FM];
uint8_t am_byte = preset_data[preset_data_size - PRESET_POWER_OFFSET_AM];
//Set the TX Power Here in the CC1101 register...
//If we have both bytes 1st bytes set or none, this isnt a preset we can deal with here.
if(fm_byte && !am_byte) {
//Use FM Table
if(tx_power) {
preset_data[preset_data_size - PRESET_POWER_OFFSET_FM] =
tx_pa_table[TX_PATABLE_OFFSET_AM + tx_power];
} else {
preset_data[preset_data_size - PRESET_POWER_OFFSET_FM] =
tx_pa_table[1]; //Max Power 0xC0 10dBm
}
} else if(am_byte && !fm_byte) {
//Use AM Table
if(tx_power) {
preset_data[preset_data_size - PRESET_POWER_OFFSET_AM] = tx_pa_table[tx_power];
} else {
preset_data[preset_data_size - PRESET_POWER_OFFSET_AM] =
tx_pa_table[1]; //Max Power 0xC0 12dBm
}
}
//Pass back the preset_so we can call one liners.
return preset_data;
}
const char* subghz_txrx_get_preset_name(SubGhzTxRx* instance, const char* preset) {
UNUSED(instance);
const char* preset_name = "";
@@ -249,7 +313,6 @@ SubGhzTxRxStartTxState subghz_txrx_tx_start(SubGhzTxRx* instance, FlipperFormat*
SubGhzTxRxStartTxState ret = SubGhzTxRxStartTxStateErrorParserOthers;
FuriString* temp_str = furi_string_alloc();
uint32_t repeat = 200;
do {
if(!flipper_format_rewind(flipper_format)) {
FURI_LOG_E(TAG, "Rewind error");
@@ -259,10 +322,6 @@ SubGhzTxRxStartTxState subghz_txrx_tx_start(SubGhzTxRx* instance, FlipperFormat*
FURI_LOG_E(TAG, "Missing Protocol");
break;
}
if(!flipper_format_insert_or_update_uint32(flipper_format, "Repeat", &repeat, 1)) {
FURI_LOG_E(TAG, "Unable Repeat");
break;
}
ret = SubGhzTxRxStartTxStateOk;
SubGhzRadioPreset* preset = instance->preset;
@@ -691,22 +750,28 @@ void subghz_txrx_set_default_preset(SubGhzTxRx* instance, uint32_t frequency) {
subghz_txrx_set_preset(instance, default_modulation, frequency, NAN, NAN, NULL, 0);
}
const char*
subghz_txrx_set_preset_internal(SubGhzTxRx* instance, uint32_t frequency, uint8_t index) {
const char* subghz_txrx_set_preset_internal(
SubGhzTxRx* instance,
uint32_t frequency,
uint8_t index,
uint8_t tx_power) {
furi_assert(instance);
//Grab the prset name.
SubGhzSetting* setting = subghz_txrx_get_setting(instance);
const char* preset_name = subghz_setting_get_preset_name(setting, index);
subghz_setting_set_default_frequency(setting, frequency);
//Get the preset data now so we can set TX power.
uint8_t* preset_data = subghz_setting_get_preset_data(setting, index);
size_t preset_data_size = subghz_setting_get_preset_data_size(setting, index);
//Edit TX power, if necessary.
subghz_txrx_set_tx_power(preset_data, preset_data_size, tx_power);
//Set the Updated Preset.
subghz_txrx_set_preset(
instance,
preset_name,
frequency,
NAN,
NAN,
subghz_setting_get_preset_data(setting, index),
subghz_setting_get_preset_data_size(setting, index));
instance, preset_name, frequency, NAN, NAN, preset_data, preset_data_size);
return preset_name;
}
+15 -2
View File
@@ -61,6 +61,15 @@ void subghz_txrx_set_preset(
uint8_t* preset_data,
size_t preset_data_size);
/**
* Set TX Power
*
* @param preset_data Data of preset
* @param preset_data_size Size of preset data
* @param tx_power Menu Index of TX Power Setting. (Saves iterating in Config enter)
*/
uint8_t* subghz_txrx_set_tx_power(uint8_t* preset_data, size_t preset_data_size, uint8_t tx_power);
/**
* Get name of preset
*
@@ -390,7 +399,11 @@ void subghz_txrx_set_default_preset(SubGhzTxRx* instance, uint32_t frequency);
* @param instance - instance Pointer to a SubGhzTxRx
* @param frequency - frequency of new preset
* @param index - index of preset taken from SubGhzSetting
* @param tx_power - index of TX Power menu index option to use.
* @return const char* - name of preset
*/
const char*
subghz_txrx_set_preset_internal(SubGhzTxRx* instance, uint32_t frequency, uint8_t index);
const char* subghz_txrx_set_preset_internal(
SubGhzTxRx* instance,
uint32_t frequency,
uint8_t index,
uint8_t tx_power);
@@ -125,7 +125,7 @@ bool subghz_txrx_gen_keeloq_protocol( //TODO lead to a general appearance
return res;
}
bool subghz_txrx_gen_keeloq_bft_protocol(
bool subghz_txrx_gen_keeloq_seed_protocol(
void* context,
const char* preset_name,
uint32_t frequency,
@@ -142,7 +142,7 @@ bool subghz_txrx_gen_keeloq_bft_protocol(
subghz_transmitter_alloc_init(txrx->environment, SUBGHZ_PROTOCOL_KEELOQ_NAME);
subghz_txrx_set_preset(txrx, preset_name, frequency, NAN, NAN, NULL, 0);
if(txrx->transmitter && subghz_protocol_keeloq_bft_create_data(
if(txrx->transmitter && subghz_protocol_keeloq_seed_create_data(
subghz_transmitter_get_protocol_instance(txrx->transmitter),
txrx->fff_data,
serial,
@@ -162,7 +162,7 @@ bool subghz_txrx_gen_keeloq_bft_protocol(
flipper_format_write_hex(txrx->fff_data, "Seed", seed_data, sizeof(uint32_t));
flipper_format_write_string_cstr(txrx->fff_data, "Manufacture", "BFT");
flipper_format_write_string_cstr(txrx->fff_data, "Manufacture", manufacture_name);
}
subghz_transmitter_free(txrx->transmitter);
@@ -335,6 +335,36 @@ bool subghz_txrx_gen_somfy_telis_protocol(
return res;
}
bool subghz_txrx_gen_somfy_keytis_protocol(
void* context,
const char* preset_name,
uint32_t frequency,
uint32_t serial,
uint8_t btn,
uint16_t cnt) {
SubGhzTxRx* txrx = context;
bool res = false;
txrx->transmitter =
subghz_transmitter_alloc_init(txrx->environment, SUBGHZ_PROTOCOL_SOMFY_KEYTIS_NAME);
subghz_txrx_set_preset(txrx, preset_name, frequency, NAN, NAN, NULL, 0);
if(txrx->transmitter && subghz_protocol_somfy_keytis_create_data(
subghz_transmitter_get_protocol_instance(txrx->transmitter),
txrx->fff_data,
serial,
btn,
cnt,
txrx->preset)) {
res = true;
}
subghz_transmitter_free(txrx->transmitter);
return res;
}
bool subghz_txrx_gen_kinggates_stylo_4k_protocol(
void* context,
const char* preset_name,
@@ -395,6 +425,66 @@ bool subghz_txrx_gen_beninca_arc_protocol(
return res;
}
bool subghz_txrx_gen_jarolift_protocol(
void* context,
const char* preset_name,
uint32_t frequency,
uint32_t serial,
uint8_t btn,
uint16_t cnt) {
SubGhzTxRx* txrx = context;
bool res = false;
txrx->transmitter =
subghz_transmitter_alloc_init(txrx->environment, SUBGHZ_PROTOCOL_JAROLIFT_NAME);
subghz_txrx_set_preset(txrx, preset_name, frequency, NAN, NAN, NULL, 0);
if(txrx->transmitter && subghz_protocol_jarolift_create_data(
subghz_transmitter_get_protocol_instance(txrx->transmitter),
txrx->fff_data,
serial,
btn,
cnt,
txrx->preset)) {
res = true;
}
subghz_transmitter_free(txrx->transmitter);
return res;
}
bool subghz_txrx_gen_ditec_gol4_protocol(
void* context,
const char* preset_name,
uint32_t frequency,
uint32_t serial,
uint8_t btn,
uint16_t cnt) {
SubGhzTxRx* txrx = context;
bool res = false;
txrx->transmitter =
subghz_transmitter_alloc_init(txrx->environment, SUBGHZ_PROTOCOL_DITEC_GOL4_NAME);
subghz_txrx_set_preset(txrx, preset_name, frequency, NAN, NAN, NULL, 0);
if(txrx->transmitter && subghz_protocol_ditec_gol4_create_data(
subghz_transmitter_get_protocol_instance(txrx->transmitter),
txrx->fff_data,
serial,
btn,
cnt,
txrx->preset)) {
res = true;
}
subghz_transmitter_free(txrx->transmitter);
return res;
}
bool subghz_txrx_gen_secplus_v2_protocol(
SubGhzTxRx* instance,
const char* name_preset,
@@ -63,7 +63,7 @@ bool subghz_txrx_gen_keeloq_protocol(
uint16_t cnt,
const char* manufacture_name);
bool subghz_txrx_gen_keeloq_bft_protocol(
bool subghz_txrx_gen_keeloq_seed_protocol(
void* context,
const char* preset_name,
uint32_t frequency,
@@ -108,6 +108,14 @@ bool subghz_txrx_gen_somfy_telis_protocol(
uint8_t btn,
uint16_t cnt);
bool subghz_txrx_gen_somfy_keytis_protocol(
void* context,
const char* preset_name,
uint32_t frequency,
uint32_t serial,
uint8_t btn,
uint16_t cnt);
bool subghz_txrx_gen_kinggates_stylo_4k_protocol(
void* context,
const char* preset_name,
@@ -124,6 +132,22 @@ bool subghz_txrx_gen_beninca_arc_protocol(
uint8_t btn,
uint32_t cnt);
bool subghz_txrx_gen_jarolift_protocol(
void* context,
const char* preset_name,
uint32_t frequency,
uint32_t serial,
uint8_t btn,
uint16_t cnt);
bool subghz_txrx_gen_ditec_gol4_protocol(
void* context,
const char* preset_name,
uint32_t frequency,
uint32_t serial,
uint8_t btn,
uint16_t cnt);
bool subghz_txrx_gen_came_atomo_protocol(
void* context,
const char* preset_name,
@@ -10,6 +10,7 @@ typedef enum {
SubGhzNotificationStateTx,
SubGhzNotificationStateRx,
SubGhzNotificationStateRxDone,
SubGhzNotificationStateTxWait,
} SubGhzNotificationState;
/** SubGhzTxRx state */
@@ -1,71 +1,120 @@
Filetype: Flipper SubGhz Keystore File
Version: 0
Encryption: 1
IV: 4F 77 4F 20 66 75 72 72 79 20 49 56 20 55 77 55
1D0560740B25F58EE0E85BF949139971E5AA08C5499CC74B11992D124C281012
C05E2D2C715D8E24C518EF2841DA02173C05DD5BA5310EE85D09709500DB1726
9EA5721836369FF918859077F50E33100F7AC53E8E8F31E25296579F875359DF
D2A8AD1B65BC66B459525124CDC5011C79D98F542702FC69EABD64F908C0D80D
2FA5F078BEB59851D42BC7E4E331AE3A8C384892DF003238CDA82450A6CD02AC
E54ED5F49A093BC938521195C86FECF35FB6EC463C54C6E1609592DC5FA03CCB5E1EDA362FC9AB008C85E66B60147EBA
048F4A28B18496487D65A924F4E37766C3563F41090D442DE61D7A5DD82F5FF1
DA876A11401727E5102B578F87CCF9596AB9D9925FC90CDF99C9DEC7261B2C8C
03D3842335D69A3AA42452274130B3FAAEE6087CB8D783B0E770062C034BB302
7F50401E9FFAF10D1F61067C2E830EBBEC7C8B3B20F5C0AC2E10E68912BF2C82
160BC0CA2FF01E076830F29846C1F6CCBAC7857F2043E8163449048BA8C99AA8
09357F089CB148DDC578E0F11EC10659EF68A57440700F584922CB9842E2BF08AC977CCAA11355E89FA5C18113349F5E
910D166F40F264225BD4C8EB16C5CC6374F8F8E1202D5BF28FA2E8BA38E420A36E67611E6D151051F7C74843E4A72BBC
D9ACEBC528D7CA74B894A0378095E03C9BDBBE13DDA3FCD6D60CD3CAC49746B8
90F94FD262E1859B7E5C08E7ED5B16CDB56D0E930034E315CD011DE3759150DA
6620E176FC61DF250EF2BBC2AB27E0DA45A6E1E27F4E94405EE01C36E892EFBF
7D6A062453236E4C353F19B875D079157D016B223B7D429C8F5BEA9D0F703EED
20631589CB541B32C23CA7EA93B12E2016B5A90A2C9CAA6F5849C0B67158EA635858F26A86282C124AEA3FD31B7FD365
E6CF167CEA0639D1504E0BFC8BAAA7FC7FA36A2286CBBE1312803422018A9F00
0EB81E23FF37366BA4890CBD46BC8AF5A2CD56E9802B8DF5CDDD5114F677CDF6
62195292F6F920DAF91F812FC3B94E8539C157D35D3BC94F2FB7A8481406C655AC0F112C4E10AC36892D43D95827BC1D
65FB9BEBDB9BE975168D47B02CA8A2E0ADE2CD1949E90B06689B0475395663BD5AF3C8FEC42C4138CBBB8956AC55B475
EF0A6961C754FCDBAE0222099DD8AA38DD6A92BCD53A576E105BC5BBD23400B7
A608752C8602A5BD538441DE046DD7AE011FBA87210B57372BCBB471ECC2B720
222C83DDC445F72152E563CD068851389364C1D83C9F7D6353325EFD553560B1
96977544F2F821408C1A88FADB9B1E12D9CA97638A622190F83BC640508B6029
B596A98BDDA5BD2FF1F812BE67FD00456D4D313E9497147E2439B51972B6B752
FC276CAC90756F397C3BC616631CE8B9257E6C25D0DC15C5ED1CEB439ACF04D9
383DD624B98E650E5B4BA28990B4D1912B785C689E6B6A05D77B47B6501CD98F
410814DC8B38DD6EF781B55CA02730092F252082A77400AD90F22BF45A41C849
8DA240E13E8B512B50FF4504A61037D0A3920B00523D51EEF9996CC3907C175E
E655B7C31E346154C5EF7C59E3A710A2A2F145E7403E4ADB388B3A27D6FF59DE
E2FB7F96EA16B20589995E95D0959B4ECC1EDDF86E347EB85FD29D0D5933A2A1
00D910F2B050900735A8446220FF7321813252862A69C05A7A534118E50E61DB
BA1B9E7E26E04542183F085060421553CDD3FD9034AAAFCB7EE980A68B98087E
216EBA33FCE4B834BF64621E557923D8AE41F5895266B7BBDFCA6EAF985F036E
2E9075A45ED6D86C172C9ADAAF5E991DA8DA9CBF2F24D746D22A331E236FBA4A
04E4B185C150AF45A67E15D68282C7558B13BFFB05BFCB71BCFD2B92DE5D9701
1FDC4D759EA89681F76A8F7D336118FE6801EFD10D73925C2749775D9DFED282
FFB32167FBF860418AAABF29B0D4FEF57BB07454ECF4BD2CF175D44E84C04CED
7C6E419658872D298F2E7B02568B9ED870FDFFE5082ED0BBE689FD04EFFFE7E7
ADAB0F3108398C75ECC6D2E572960B5685C336DDF3D6F5B9C12D069F27BA15A8
DAC772C1A81181738CDB8C0E89C2E5B7A57E2DA65CA15232DE96A3C4A599A0E9
7F6204274D90E88B3F5D5AC86EEEE76C27C0D083E79ADA7BCF7D060FE6F05A3E
5BAB4CCB593418CD8965C09C0925EDB78BB4C8A10892F264DF12F50E532F006E
7C67525C921ABAEC4BFAA376162A7B2B2827AE4C9840DC37F067FE3B72BE7304B6EEB5FB1AD17CFF5F079EFECC04AD56
94FF6DD4CE63381778E86E61423EDDDD9CC71C3462D66F1AD9A0AD5D378AECE2
E9CA5BA5C6D6101EA3A51F28E48D49789D60273A9F70D56020D003265517AFA3
EBF55649D226E9FDAAB57C2E1D75E3FD3A8D216488E97D00E932B3542E731D20
2B576C3616DDAFEE23A8ECE24BBD89590C8C2D551F14D8EFA4DBB30216F89C31
EE8D3CF3C503DE4FF368F23585F1A7017FF66A910696A6760AFD2B9822911960
A32910F791C5EE9998DCE5371B3252427315C9D11AD506CE65760611D873C81D
740646D11A32C65A8549B3AEA8A499866C35D926B2BA21ED73934AA37ADCC1E6
0E66EF4CA934A5D1ACED28CBEEAA3AF7941E10918DA79379090B6339F11E267E
D79D8666FD947B0D0D504FF10B048B147CB000AD8CDC1F0DEB395FB72B789963
2F7BA07F18F4A91AEDC08867E9CC4B8689B0831A7CE0E0AEF3D92C0CA9BBC698
1AB4351CFF02CC600C972CE87F69B23F8ECCC32A90BD5F429F8017A80306F23D
ED2AD447E7DF7A34D78A313395FA1C3AF63CE02B77A5B08CE19493CFA1173232
C8C8DEAF10AA3994EE7D6DC8E1EB403042627E0F3524409F40C03A7C0C106A80
5778B4A3E7BE82C07BA6A311A87649F3C7AE5107A89571E14AEF05B9E285C87A
30080347DB3B580B18E8EAC66E1B7227F791773F0342EE0DF8267EC993EC3F24
2DB3B2A17C165B5C6A1D4944A5B595016588F028DD4F763C4ED6B7FE7849E918
C1F0CF343B77F31D9A2E810821393EE9D1E0D4B54A87B2DD8CCFBB16FBD77A75B50A0E78D1E8A86310572443731B9DB5
88EF373C37AADAE1155E7DBDBB7E0B048E3BFEFB412DC49EA8A48E1544B6DC87
98694781F3EE698ABA8D2CDFFB1CA0425AA17BFE904FC7812E65A78DF1CA06C2
6BBBCA6672311E1A3BF7001B3222890C8A68A8B7D87DE91624BB9D1FEC0E2728
4550A44B654085C3A3620B5D4D2C6A7F962275BC5926B9B7E3A706F128BF6D6C967E2BF2ACD4DD000BDC8BDE69684F6F
IV: 2E 2E 2E 4D 65 6F 77 20 4D 65 6F 77 20 4F 77 4F
411F604B4F4487BEFB5285C7023822BE66D08DD0AAB0B659D58A35E68850BD85
12BB80D35A9750D218BDE6D4BE4E5FD3B5CE20EDA464E1FA80A000D49FF567B4
81B613E6CFD99287BCBF0957733704E61B4FFE9CBEED2DC721FBABFEB1A249CE91EEEAE285FC10E0A83A6E1BBEA7EB56
421C18DF4D8280560EFADDBF0BB02F90942CC5B5B2986327D96F5446CC531BD0
8BB08D9758943404ACE935B58640E78478C91140E5871D7D070B104DE470C3F2
1E45CCF03C56DDA4403214A430330647D58D443F665794F5F04D9701C6F32AA2
4429DDE801D7EB69ADD042A70F83AD6160E8504D6C28A9A4A3D7531140549343
64BC31C7E98E5223E7D74B844CF4BA26594119D3E468188123D6056C52AF11E7
F7133D37B1A05BCFB3267D7659B85598E3B2B23502C090C042F0AAEB84E4326D
97FA5E05D70495342BACADA35918C339518DC3EC4AC94F94BC73A08BC539A80F
45F22E4ED7559C875725F2DFCF2DFC0631DF1565A12F455F13C4A101A54E607B
95C78F6417110955813DA8E4AFDCF118A1E19006B4D627FCD1B1688F16CE1933
7B19D2DA8DEA82083E9F52CED668A0CD6B997AED26F40F6BED30EE7D0E3BD117
A9B4AECEB0C40BFE38F06302769702C89CF530C16308D0DB7A74AF9B70D3034B
D97B2D17062FD5264C88A2369ACA8BBA064B9B99D064C8831B12B1A23E51BEC1
A26401DADE20E5D99F53703D17B6E247D169FE0073512A23F6EDB8B3FD9AA73E
FFA8F754EC999FA789C333D7C2EEE62CE7ABEFEF9EDA191DDF9C0BEE59E30927
82FB6C3FB0AD818834B4335855599741728466E3418EBB0C03382C01F2CFE517
2D284BC94C8C3EAE99FB8060524540C1FA07866A648CD49B82B874DF2073691E
5EFE887394C60DADCDBE4E25E323CAFDC8E0E5A435FE4941CE29274A319E2F4B
8B3CD3C3310933B59C328892998E434CF34F5230B219B9F00EE888154F71D234
FF176D5DD6D6C296498E40E30FFF2F8498760ED5609B2E3080661A29B1F3C7E7
CFFD9A9B1210EE4B49849E00B8513400B5399BEDCC98D8C1CEFB5F062BA0F2D7
99DACFB462669C3F2C87C09F2B3FB0AC8F9F26DEDF6D04CF0C306F7F91615FCF
47A99EB39C8655BD8CB93D349478514E0ECA46C6463011B6B1F0A9B807DEF300
CC2EDDB4A96311B0A3D4A55F485292CF7585E1CAE9D3D46ADBD40A3391396DFF
82DE652C1008367C54C8C431DADC5026CCF1B300BB112D2F4B3366B319F61284
71BBA17FDC6261801D17434538524D3B6EE42355B8ABF99C07DBD69F3C1031CE
974DC30EDA9E1CA01C0532E82593D7D84E7FE282659EDAA8FC3474DA422D57B2
6975A600597A196A21D8432881D95D977C168027D9FFAB273DE00A5AEE3BC5E1
0B7EC247D17B8C80E151E9DD60B94D2D6CD4E7A03E6404FA044D6BC2A15CF214
73D12A173C2B97A77231901E94B1C81EA1AF79BD1EA01F8F8E2A3B38177A84EC
9D309CE4B225FC251019B1354E8AEB47EE1D6FA792FF0E14FAFB0C0944D79350
816B8EB8CB5ADEAC80C9359F9BF538A6641A20AB8BE422AD65E50802DFF38CED
1F41BBFFA9338DB4143D4D3CBA84637FB397EEA36D5D89D4A5D08B3FB8181540
295359A1E73F5F94186DBFC13175A6DA08771483043408C145B0FB4A10716C86
990E0B2ABE53757378385879D4D5BBCEF45A69F659963DB6813931973B69ABD6
6509E69551E5D3E38349DC16CBABBD059DB80852ECA56F7902DEE11F280B14B6
C8D22F8E7592087432BC4FF32D1810697406948F2C69C54BC5CA8F59BE65278F
E53840FE4593AB7708C14E3E1A33D689160C018CF2CEC3A6FB309929BDA2A66F
3CE77B617C5E133046704DAE55B5328C861CE02F223CE75E6CDFF296927D0012
0CD25E6B29860621BED286C97675EC6515D9A93B7BCC5E8571460DF90A73F748
C387DAC55E4B8F15BCB228CF58F857419C91BAF619D38D2700E7149BE56C9D37
E1AEB1E36C5C0D30B858101BE459E29CE2A49470022495A8D4869D44826F7928
173D2BB833DCB4291D25E43C9E85AA7900198BF92D6E97EE861BBF6CBE1D2147ADABA5DAEA976F6D2A2716B7E3E75DEC
A388246FAB037912C9B1D2D25C144795B16E917BD2653693346E1A076718FC03
4EA223311825F64F88AB509D69B708A2143A76A180ED1AB46C8F1AA1C4A9E5AF
0CBBC26D7C458E415DCEECC9F73008B556BED1B77A3AA6BE8C557B46058180D9
9DE0FFD924900E5B3FD4CCE2B69D7B20FF52EE1E747FFACC6A631056D7D2B4CE
2C05D81B20DA46ED217BE759EFD95919D434FAC98B08405FB9604B61982E4141
4EEBB881DBE7A97C9C043221B2038F8B919C144D33ACDCE0F9AB5A2D63363E07
F19CDBD22DCC70D2A11AD7D6210FEA7601BA2093F1E9766C4F8452F1AD14046B
AA165E848FB25FAF3AC74D1AD809BA2A45FE2E79090256BF9E77BDDCD7A4A4E2
C63B40C559AE339DB491E1E5B39C0EB321EC3A3BD2441763A5B2DF7C2DF534F4
DA1CEA4B84C64D744AC3BE1D29B1E752CCFB3CFAA56AEB85C1BED37293BEAF93
C67E0C619482F59D63C6B097347A4C777EBC60D800DD586C10A0BCEC67628618
0EDE32F4FB6ED09526381B638A7CA5869C7B20FE6E003311065398E3ED3E3474
43714DC1C28BC0FD3DDD348E93169523D6BC4C8AC1F62545C44D405ED1CA4927
B49A42DCCC1A76C1643AD96CAE981BE430BEC9CA372A115C02E87BECE4FFFAA6
E66E1F05704B9BE33AC1A52D33B3CE1D373106B61C10252E56618F64FB2E665B
A78DD00AF303F280CACF7DC5C91778096D77989AAE29EDDC3D772EA32A6A5CB1038196BB1C2394C3E09A706EB421447F
7EC357770322CFB212E77C7A1557C46AF1048A87CB14AE0339FF18E031927DD9
698F99BEC7BBDA2F06BA8F393235783C05F1E324CB89A2F9239A93C63A96D657
5955B7E969ECD7E5DDAB6AC8C103CD1C03504A8DA71346452302887FD72ED9D4
A6856ADA5AD21EAC6255C1A3166CBAFC8763DD6BB4F2AB42BCCB1B71D02B722C
DB86290A20BD5A1CAFED7D06511096B32BAA7CAEDC49A53488542F3944B72531
83B8E69B9A50EB7D3F51D4221C4002110AB129A5E32A5CF07763BF5A198EBC68978CCF097D4396AFD920DD5A23C4DE14
95C4CA690A9214E66520F67150A42E5ADE143E9C8D0C5B9CFABDFC93D193FC3D
A3B3E5E7B57F758FDB2062613DC7CF3990DAEF6F2F1EABCD169EFA85C1237F8D
2FD52B5A55EA063680E6CD164F6CD50148E6CBF0DAD1250494CF0E536D39687D46876E1E2C0A9902F785C60273BB5026
38F372E8F07F16FB50942FFAC6FC4D49601CF09A65C714AC0998EB1FE0DD8F5A
94B863DA82DD1BD27D083690AB4B59D9B6236FCB1172B9480F2E11BC420FC46C3D554B78B2476C964379E028DC89BA29
4D52AAF64319E435FDD046276964E7A819A3CCF9EEC576A0993FE6F8E29095BB
F44CE71DE07CFAD16BEE0041832A532EA266557C640B4FAE6245252B47C6B8C05AFD9C6F305596ECF68BFE8829B9032D
8520769CE953F5F02C132DE95141E8F78FF2C846B9F672970771BBFCB51386BF
FAA8C71F036FD5A1BDCFCEC05E890BCCA8EB92E7AABF4C7D64D1EA3D25730DD0
CDB89D5B8B73DB2013EDBB34D8444B37EBAA308345A9626E153FF0A9574FC4D6A2B9588BC20EC0D9B1498228F116D3F6
13351D444888E92AD03EFFE3466EFD5719DA41FB5BA0805C09CF3E79B188223B
B36B0913A714AB0DD7C0005426127B3A2C02DD3112E745064D7A5899DE26A128
C3AB7BBA577D74FD581FDA6BA17C1D2E1EBD4A4AA5CB42852E59CEE91D81B3E263549755171B22A5FBF4A1156C8C0A7B
18F579227253BAB1E3CA2E71331C6090286480D19BB1FEF242793D27BCA157D3
BF613CE44B9BDA278D884D2F756FBE164A9736D28A52B0101611D61B643EB978
9DE9087BD023D6F6ABDF4A345E85A1110D6B205CCC31DC7372126CCC5EC618EF
764602226E608E5371E693185D57B7C4D6FE2CBDA379D986A251CC7286B283FEE35E07E5B32B93649D52D9CE4FFC90A7
3AA3162A345891481F09C9231238AD644B6804643C5EED165B3B3F078E0FC216435AE2385B3AF4EB44C029D9A60FC5D9
867DD29F637DDFBE24CA6AFDA94366D865D701CBCAA9E7074B6FB29592AE31933F230C6471C5A513D19560F5E0820BCB
EDB4BC2F025C11311527B227E42726C3BB4A6CFF49898E2FFEB2097F0924E8EE
D54BFE2E7C21D84AD967433C7329BBBD257A026BFC7866071C80396B45E764D6
ED13A30EC29B22AB34D5EFE27C74C112ACD42CFFFD0695CE743FE2D0ECCCB35EF58222519E54289E7AD0624A11698986
84AF1F78D666058274DE1896D706E6196B55B782C395216915C1023E1E44F4A2
648F762C38B32B930029E5730609A46CF0BCB4117BEF41D36ED47BE8EB8DB2AC
745FAFAFC79AD2DFBE3B03387EE5C3FC05EBF1A61A371E439B7FA977A1ACA3B0
8C4FE20E0F71A9822FD32A866571D9ED6F8A1F393430AF5DB5FDBF4F7030165A
CC54F712C0C63A6B207C9ACAD7943BDCCC0F8126449E82CA5681F88A0491625B
9F1654268308E44DD6A9128AB1BF9B01D521D5BA9A98E5A77D35564C3678D9EAF2C8A2EC09FC3E5CD1A831FDB95486D0
58E094157F881A156E0D4EBB79ABFED8D855A9BE1589471A8BDA6AA9F9C58F4B8BF6B326653B6CE28C45DDC3DE4CC4BE
B3BE094F6E487FD211792E424BB5EB95FC11AC78832FE9EAFDB076867A9B4AA2
9FC6BE151041C150003E5BA940ADABDD02A080A9F4EE61215A6162BB1C13B7D9
7CFE8E8FE7E2218425F581F70AC6E043E9253774A80450113C836EBAF4EAF4A0
8610A9694E586306A4078E5DD3C757870C21130D6F67593994568FA1E7B7F4CA
774DDBA8E8AA78BFF82B1E9C8CC58873C73AD0B977ED4EDD465513F9D238AA7DB2BE64536F4950BDAA00F971551F0922
DB103B1AB06E9115193F5DC6F0C2D806AE9518C91D7D72945B27BE86B2D16351
86C4A3F2ACBFA942CF4FF3DC062A4786D9D005C05671E232D93DEBCF5C1D7B96
909EB49451C1D7A1B6E01FFC55EED8DA3C5DB4AFEA5C347DD4E79642746C963E64113CEDECF951947C095B12390BA10F
AD7CFDF6D5C93806F8E5321EC2B4E2C6C7B750874E046154EC76EC2678B6F0C938D2D99A6646663ABC730190C7108C1F
F3DC34F33197A7D2DCBA4D25ACC6EAA0612723C92BFF226E7B3A73BD07B2EBEA
B7074A0B79FE44FA94C3EC55C2C694ADD593C021DED43085CB5D69DBA7F6F6F1
9C7B26FC47E16E41E8F1F560EBEE3B580E5F44C1A2DA0BA415C279530416F231
EBED9733677546781E77DC9E6668AE3CA6D1B6C0E1446EE10EBEC8F8ABF5228C
D21EB73FE8C693DD4C580A51DB46D5DF26D798A8A67095FF087B8F14907993C2
CB9D919298CB3EF9458A962BDD79736F6FC00E209B389C0543D06687AB16ADE0
EED58F3D2003F1F288718544B95F8F7728AB6D0A7D10E3BEFED3C7F4A55A6B7CCBF8EACE17F0FADCDA6B19DBA74A2324
D4E83675B9B5BB81D1E5C17CB88225C514C6DD2852D22A8C4681F0683248B21E
0949495177B2C21F42679921EA4C83CD7A9700FB34B13DDF3E61CE26162DE0AF
2C38210508654DE730157BA138118793DDA2BCD7958EDD21B02796B689D0990C
47C78252386879DB6F9F3516FBCA29BAD3EB5F837B881422D7D421A41CCB851EFEA209D371E6E81EF957441D31D71842
@@ -1,22 +1,13 @@
#ifndef SUBGHZ_ADD_MANUALLY
ADD_SCENE(subghz, start, Start)
ADD_SCENE(subghz, receiver, Receiver)
ADD_SCENE(subghz, receiver_config, ReceiverConfig)
ADD_SCENE(subghz, receiver_info, ReceiverInfo)
ADD_SCENE(subghz, save_name, SaveName)
ADD_SCENE(subghz, save_success, SaveSuccess)
ADD_SCENE(subghz, saved, Saved)
ADD_SCENE(subghz, transmitter, Transmitter)
ADD_SCENE(subghz, show_error, ShowError)
ADD_SCENE(subghz, show_error_sub, ShowErrorSub)
ADD_SCENE(subghz, saved_menu, SavedMenu)
ADD_SCENE(subghz, delete, Delete)
ADD_SCENE(subghz, delete_success, DeleteSuccess)
ADD_SCENE(subghz, set_type, SetType)
ADD_SCENE(subghz, set_key, SetKey)
ADD_SCENE(subghz, set_serial, SetSerial)
ADD_SCENE(subghz, set_button, SetButton)
ADD_SCENE(subghz, set_counter, SetCounter)
ADD_SCENE(subghz, set_seed, SetSeed)
ADD_SCENE(subghz, frequency_analyzer, FrequencyAnalyzer)
ADD_SCENE(subghz, radio_settings, ExtModuleSettings)
ADD_SCENE(subghz, read_raw, ReadRAW)
@@ -27,3 +18,16 @@ ADD_SCENE(subghz, need_saving, NeedSaving)
ADD_SCENE(subghz, rpc, Rpc)
ADD_SCENE(subghz, show_gps, ShowGps)
ADD_SCENE(subghz, signal_settings, SignalSettings)
#else
ADD_SCENE(subghz, set_type, SetType)
ADD_SCENE(subghz, set_key, SetKey)
ADD_SCENE(subghz, set_serial, SetSerial)
ADD_SCENE(subghz, set_button, SetButton)
ADD_SCENE(subghz, set_counter, SetCounter)
ADD_SCENE(subghz, set_seed, SetSeed)
#define SubGhzSceneStart SubGhzSceneSetType
#endif
ADD_SCENE(subghz, save_name, SaveName)
ADD_SCENE(subghz, save_success, SaveSuccess)
ADD_SCENE(subghz, show_error, ShowError)
ADD_SCENE(subghz, show_error_sub, ShowErrorSub)
@@ -45,48 +45,27 @@ bool subghz_scene_more_raw_on_event(void* context, SceneManagerEvent event) {
SubGhz* subghz = context;
if(event.type == SceneManagerEventTypeCustom) {
if(!subghz_file_available(subghz)) {
if(!scene_manager_search_and_switch_to_previous_scene(
subghz->scene_manager, SubGhzSceneStart)) {
scene_manager_stop(subghz->scene_manager);
view_dispatcher_stop(subghz->view_dispatcher);
}
return true;
}
scene_manager_set_scene_state(subghz->scene_manager, SubGhzSceneMoreRAW, event.event);
if(event.event == SubmenuIndexDelete) {
if(subghz_file_available(subghz)) {
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneReadRAW, SubGhzCustomEventManagerNoSet);
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneMoreRAW, SubmenuIndexDelete);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneDeleteRAW);
return true;
} else {
if(!scene_manager_search_and_switch_to_previous_scene(
subghz->scene_manager, SubGhzSceneStart)) {
scene_manager_stop(subghz->scene_manager);
view_dispatcher_stop(subghz->view_dispatcher);
}
}
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneReadRAW, SubGhzCustomEventManagerNoSet);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneDeleteRAW);
return true;
} else if(event.event == SubmenuIndexEdit) {
if(subghz_file_available(subghz)) {
furi_string_reset(subghz->file_path_tmp);
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneMoreRAW, SubmenuIndexEdit);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSaveName);
return true;
} else {
if(!scene_manager_search_and_switch_to_previous_scene(
subghz->scene_manager, SubGhzSceneStart)) {
scene_manager_stop(subghz->scene_manager);
view_dispatcher_stop(subghz->view_dispatcher);
}
}
furi_string_reset(subghz->file_path_tmp);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSaveName);
return true;
} else if(event.event == SubmenuIndexDecode) {
if(subghz_file_available(subghz)) {
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneMoreRAW, SubmenuIndexDecode);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneDecodeRAW);
return true;
} else {
if(!scene_manager_search_and_switch_to_previous_scene(
subghz->scene_manager, SubGhzSceneStart)) {
scene_manager_stop(subghz->scene_manager);
view_dispatcher_stop(subghz->view_dispatcher);
}
}
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneDecodeRAW);
return true;
}
}
return false;
@@ -76,6 +76,20 @@ const int32_t debug_counter_val[DEBUG_COUNTER_COUNT] = {
-50,
};
//TX Power
#define TX_POWER_COUNT 9
const char* const tx_power_text[TX_POWER_COUNT] = {
"Preset",
"10dBm +",
"7dBm",
"5dBm",
"0dBm",
"-10dBm",
"-15dBm",
"-20dBm",
"-30dBm",
};
static void subghz_scene_radio_settings_set_device(VariableItem* item) {
SubGhz* subghz = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
@@ -90,6 +104,20 @@ static void subghz_scene_radio_settings_set_device(VariableItem* item) {
subghz_txrx_radio_device_set(subghz->txrx, radio_device_value[index]);
}
static void subghz_scene_radio_settings_set_tx_power(VariableItem* item) {
SubGhz* subghz = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
//Update the Menu Item on screen
variable_item_set_current_value_text(item, tx_power_text[index]);
//Set TX power and remember setting
subghz->last_settings->tx_power = subghz->tx_power = index;
//Save the settings now, this is the convention here!
subghz_last_settings_save(subghz->last_settings);
}
static void subghz_scene_receiver_config_set_debug_pin(VariableItem* item) {
SubGhz* subghz = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
@@ -175,6 +203,18 @@ void subghz_scene_radio_settings_on_enter(void* context) {
variable_item_set_current_value_index(item, value_index);
variable_item_set_current_value_text(item, radio_device_text[value_index]);
//Add TX Power
item = variable_item_list_add(
subghz->variable_item_list,
"TX Power",
TX_POWER_COUNT,
subghz_scene_radio_settings_set_tx_power,
subghz);
value_index = subghz->tx_power;
variable_item_set_current_value_index(item, value_index);
variable_item_set_current_value_text(item, tx_power_text[value_index]);
item = variable_item_list_add(
variable_item_list,
"GPS Baudrate",
@@ -111,7 +111,8 @@ void subghz_scene_read_raw_on_enter(void* context) {
subghz_txrx_set_preset_internal(
subghz->txrx,
subghz->last_settings->frequency,
subghz->last_settings->preset_index);
subghz->last_settings->preset_index,
subghz->last_settings->tx_power);
}
}
subghz_scene_read_raw_update_statusbar(subghz);
@@ -97,9 +97,9 @@ static void subghz_scene_receiver_update_statusbar(void* context) {
} else {
subghz_view_receiver_add_data_statusbar(
subghz->subghz_receiver,
"",
"",
furi_string_get_cstr(history_stat_str),
"",
"",
subghz_txrx_hopper_get_state(subghz->txrx) != SubGhzHopperStateOFF,
READ_BIT(subghz->filter, SubGhzProtocolFlag_BinRAW) > 0,
show_sats,
@@ -255,12 +255,16 @@ void subghz_scene_receiver_on_enter(void* context) {
if(subghz_rx_key_state_get(subghz) == SubGhzRxKeyStateIDLE) {
subghz_txrx_set_preset_internal(
subghz->txrx, subghz->last_settings->frequency, subghz->last_settings->preset_index);
subghz->txrx,
subghz->last_settings->frequency,
subghz->last_settings->preset_index,
subghz->last_settings->tx_power);
subghz->filter = subghz->last_settings->filter;
subghz_txrx_receiver_set_filter(subghz->txrx, subghz->filter);
subghz->ignore_filter = subghz->last_settings->ignore_filter;
subghz_txrx_receiver_set_ignore_filter(subghz->txrx, subghz->ignore_filter);
subghz->tx_power = subghz->last_settings->tx_power;
subghz_history_reset(history);
subghz_rx_key_state_set(subghz, SubGhzRxKeyStateStart);
@@ -207,6 +207,11 @@ static void subghz_scene_receiver_config_set_frequency(VariableItem* item) {
frequency / 1000000,
(frequency % 1000000) / 10000);
variable_item_set_current_value_text(item, text_buf);
//Set TX Power
subghz_txrx_set_tx_power(preset.data, preset.data_size, subghz->tx_power);
//Set the preset now.
subghz_txrx_set_preset(
subghz->txrx,
furi_string_get_cstr(preset.name),
@@ -235,14 +240,14 @@ static void subghz_scene_receiver_config_set_preset(VariableItem* item) {
variable_item_set_current_value_text(item, preset_name);
//subghz->last_settings->preset = index;
SubGhzRadioPreset preset = subghz_txrx_get_preset(subghz->txrx);
uint8_t* preset_data = subghz_setting_get_preset_data(setting, index);
size_t preset_data_size = subghz_setting_get_preset_data_size(setting, index);
//Edit TX power, if necessary.
subghz_txrx_set_tx_power(preset_data, preset_data_size, subghz->tx_power);
subghz_txrx_set_preset(
subghz->txrx,
preset_name,
preset.frequency,
NAN,
NAN,
subghz_setting_get_preset_data(setting, index),
subghz_setting_get_preset_data_size(setting, index));
subghz->txrx, preset_name, preset.frequency, NAN, NAN, preset_data, preset_data_size);
subghz->last_settings->preset_index = index;
}
@@ -268,6 +273,9 @@ static void subghz_scene_receiver_config_set_hopping(VariableItem* item) {
(frequency % 1000000) / 10000);
variable_item_set_current_value_text(frequency_item, text_buf);
//Edit TX power, if necessary.
subghz_txrx_set_tx_power(preset.data, preset.data_size, subghz->tx_power);
// Maybe better add one more function with only with the frequency argument?
subghz_txrx_set_preset(
subghz->txrx,
@@ -453,7 +461,8 @@ static void subghz_scene_receiver_config_var_list_enter_callback(void* context,
subghz_txrx_set_preset_internal(
subghz->txrx,
SUBGHZ_LAST_SETTING_DEFAULT_FREQUENCY,
SUBGHZ_LAST_SETTING_DEFAULT_PRESET);
SUBGHZ_LAST_SETTING_DEFAULT_PRESET,
subghz->tx_power);
SubGhzSetting* setting = subghz_txrx_get_setting(subghz->txrx);
SubGhzRadioPreset preset = subghz_txrx_get_preset(subghz->txrx);
@@ -477,6 +486,7 @@ static void subghz_scene_receiver_config_var_list_enter_callback(void* context,
subghz->repeater = SubGhzRepeaterStateOff;
subghz->last_settings->delete_old_signals = false;
subghz->last_settings->autosave = false;
subghz->last_settings->tx_power = subghz->tx_power = 0;
subghz_txrx_speaker_set_state(subghz->txrx, speaker_value[default_index]);
subghz->last_settings->enable_sound = false;
@@ -2,6 +2,9 @@
#include <lib/subghz/blocks/custom_btn.h>
#include "applications/main/subghz/helpers/subghz_txrx_i.h"
#include <lib/subghz/blocks/generic.h>
#define TAG "SubGhzSceneReceiverInfo"
void subghz_scene_receiver_info_callback(GuiButtonType result, InputType type, void* context) {
@@ -36,6 +39,10 @@ static bool subghz_scene_receiver_info_update_parser(void* context) {
SubGhzRadioPreset* preset =
subghz_history_get_radio_preset(subghz->history, subghz->idx_menu_chosen);
//Edit TX power, if necessary.
subghz_txrx_set_tx_power(preset->data, preset->data_size, subghz->tx_power);
subghz_txrx_set_preset(
subghz->txrx,
furi_string_get_cstr(preset->name),
@@ -140,32 +147,29 @@ bool subghz_scene_receiver_info_on_event(void* context, SceneManagerEvent event)
}
//CC1101 Stop RX -> Start TX
subghz_txrx_hopper_pause(subghz->txrx);
// key concept: we start endless TX until user release OK button, and after this we send last
// protocols repeats - this guarantee that one press OK will
// be guarantee send the required minimum protocol data packets
// for all of this we use subghz_block_generic_global.endless_tx in protocols _yield function.
subghz->state_notifications = SubGhzNotificationStateTx;
subghz_block_generic_global.endless_tx = true;
if(!subghz_tx_start(
subghz,
subghz_history_get_raw_data(subghz->history, subghz->idx_menu_chosen))) {
subghz_txrx_rx_start(subghz->txrx);
subghz_txrx_hopper_unpause(subghz->txrx);
subghz->state_notifications = SubGhzNotificationStateRx;
} else {
subghz->state_notifications = SubGhzNotificationStateTx;
subghz_block_generic_global.endless_tx = false;
}
return true;
} else if(event.event == SubGhzCustomEventSceneReceiverInfoTxStop) {
//CC1101 Stop Tx -> Start RX
subghz->state_notifications = SubGhzNotificationStateIDLE;
//CC1101 Stop Tx -> next tick event Start RX
// user release OK
// we switch off endless_tx - that mean protocols yield finish endless transmission,
// send upload "repeat=xx" times, and after will be stoped by the tick event down in this code
subghz->state_notifications = SubGhzNotificationStateTxWait;
subghz_block_generic_global.endless_tx = false;
widget_reset(subghz->widget);
subghz_scene_receiver_info_draw_widget(subghz);
subghz_txrx_stop(subghz->txrx);
if(!scene_manager_has_previous_scene(subghz->scene_manager, SubGhzSceneDecodeRAW)) {
subghz_txrx_rx_start(subghz->txrx);
subghz_txrx_hopper_unpause(subghz->txrx);
if(!subghz_history_full(subghz->history)) {
subghz->state_notifications = SubGhzNotificationStateRx;
}
}
return true;
} else if(event.event == SubGhzCustomEventSceneReceiverInfoSave) {
//CC1101 Stop RX -> Save
@@ -218,6 +222,27 @@ bool subghz_scene_receiver_info_on_event(void* context, SceneManagerEvent event)
notification_message(subghz->notifications, &sequence_blink_green_100);
subghz->state_notifications = SubGhzNotificationStateRx;
break;
case SubGhzNotificationStateTxWait:
// we wait until hardware TX finished and after stop TX and start RX, else just blink led
if(!subghz_devices_is_async_complete_tx(subghz->txrx->radio_device)) {
notification_message(subghz->notifications, &sequence_blink_magenta_10);
} else {
subghz_txrx_stop(subghz->txrx);
// update screen
widget_reset(subghz->widget);
subghz_scene_receiver_info_draw_widget(subghz);
subghz->state_notifications = SubGhzNotificationStateIDLE;
if(!scene_manager_has_previous_scene(subghz->scene_manager, SubGhzSceneDecodeRAW)) {
subghz_txrx_rx_start(subghz->txrx);
subghz_txrx_hopper_unpause(subghz->txrx);
if(!subghz_history_full(subghz->history)) {
subghz->state_notifications = SubGhzNotificationStateRx;
}
}
}
break;
default:
break;
}
@@ -1,6 +1,8 @@
#include "../subghz_i.h"
#include <lib/subghz/blocks/custom_btn.h>
#include <lib/subghz/blocks/generic.h>
#include "applications/main/subghz/helpers/subghz_txrx_i.h"
typedef enum {
SubGhzRpcStateIdle,
@@ -52,9 +54,13 @@ bool subghz_scene_rpc_on_event(void* context, SceneManagerEvent event) {
} else if(event.event == SubGhzCustomEventSceneRpcButtonPress) {
bool result = false;
if(state == SubGhzRpcStateLoaded) {
// START endless TX until user release button
// variable used in protocol yield for endless TX
subghz_block_generic_global.endless_tx = true;
switch(
subghz_txrx_tx_start(subghz->txrx, subghz_txrx_get_fff_data(subghz->txrx))) {
case SubGhzTxRxStartTxStateErrorOnlyRx:
subghz_block_generic_global.endless_tx = false;
rpc_system_app_set_error_code(
subghz->rpc_ctx, RpcAppSystemErrorCodeRegionLock);
rpc_system_app_set_error_text(
@@ -62,6 +68,7 @@ bool subghz_scene_rpc_on_event(void* context, SceneManagerEvent event) {
"Transmission on this frequency is restricted in your settings");
break;
case SubGhzTxRxStartTxStateErrorParserOthers:
subghz_block_generic_global.endless_tx = false;
rpc_system_app_set_error_code(
subghz->rpc_ctx, RpcAppSystemErrorCodeInternalParse);
rpc_system_app_set_error_text(
@@ -80,50 +87,53 @@ bool subghz_scene_rpc_on_event(void* context, SceneManagerEvent event) {
} else if(event.event == SubGhzCustomEventSceneRpcButtonRelease) {
bool result = false;
if(state == SubGhzRpcStateTx) {
subghz_txrx_stop(subghz->txrx);
subghz_blink_stop(subghz);
// user release button
// set endless TX to OFF and switch off TX in section event.type == SceneManagerEventTypeTick
subghz_block_generic_global.endless_tx = false;
result = true;
}
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneRpc, SubGhzRpcStateIdle);
// scene_manager_set_scene_state(
// subghz->scene_manager, SubGhzSceneRpc, SubGhzRpcStateIdle);
rpc_system_app_confirm(subghz->rpc_ctx, result);
} else if(event.event == SubGhzCustomEventSceneRpcButtonPressRelease) {
bool result = false;
if(state == SubGhzRpcStateLoaded) {
switch(
subghz_txrx_tx_start(subghz->txrx, subghz_txrx_get_fff_data(subghz->txrx))) {
case SubGhzTxRxStartTxStateErrorOnlyRx:
rpc_system_app_set_error_code(
subghz->rpc_ctx, RpcAppSystemErrorCodeRegionLock);
rpc_system_app_set_error_text(
subghz->rpc_ctx,
"Transmission on this frequency is restricted in your region");
break;
case SubGhzTxRxStartTxStateErrorParserOthers:
rpc_system_app_set_error_code(
subghz->rpc_ctx, RpcAppSystemErrorCodeInternalParse);
rpc_system_app_set_error_text(
subghz->rpc_ctx, "Error in protocol parameters description");
break;
default: //if(SubGhzTxRxStartTxStateOk)
result = true;
subghz_blink_start(subghz);
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneRpc, SubGhzRpcStateTx);
break;
}
}
// USELESS PART
// } else if(event.event == SubGhzCustomEventSceneRpcButtonPressRelease) {
// bool result = false;
// if(state == SubGhzRpcStateLoaded) {
// switch(
// subghz_txrx_tx_start(subghz->txrx, subghz_txrx_get_fff_data(subghz->txrx))) {
// case SubGhzTxRxStartTxStateErrorOnlyRx:
// rpc_system_app_set_error_code(
// subghz->rpc_ctx, RpcAppSystemErrorCodeRegionLock);
// rpc_system_app_set_error_text(
// subghz->rpc_ctx,
// "Transmission on this frequency is restricted in your region");
// break;
// case SubGhzTxRxStartTxStateErrorParserOthers:
// rpc_system_app_set_error_code(
// subghz->rpc_ctx, RpcAppSystemErrorCodeInternalParse);
// rpc_system_app_set_error_text(
// subghz->rpc_ctx, "Error in protocol parameters description");
// break;
// Stop transmission
if(state == SubGhzRpcStateTx) {
subghz_txrx_stop(subghz->txrx);
subghz_blink_stop(subghz);
result = true;
}
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneRpc, SubGhzRpcStateIdle);
rpc_system_app_confirm(subghz->rpc_ctx, result);
// default: //if(SubGhzTxRxStartTxStateOk)
// result = true;
// subghz_blink_start(subghz);
// scene_manager_set_scene_state(
// subghz->scene_manager, SubGhzSceneRpc, SubGhzRpcStateTx);
// break;
// }
// }
// // Stop transmission
// if(state == SubGhzRpcStateTx) {
// subghz_txrx_stop(subghz->txrx);
// subghz_blink_stop(subghz);
// result = true;
// }
// scene_manager_set_scene_state(
// subghz->scene_manager, SubGhzSceneRpc, SubGhzRpcStateIdle);
// rpc_system_app_confirm(subghz->rpc_ctx, result);
} else if(event.event == SubGhzCustomEventSceneRpcLoad) {
bool result = false;
if(state == SubGhzRpcStateIdle) {
@@ -139,6 +149,19 @@ bool subghz_scene_rpc_on_event(void* context, SceneManagerEvent event) {
}
rpc_system_app_confirm(subghz->rpc_ctx, result);
}
} else if(event.type == SceneManagerEventTypeTick) {
// if hardware TX finished then stop TX correctly
if(subghz_devices_is_async_complete_tx(subghz->txrx->radio_device)) {
bool result = false;
if(state == SubGhzRpcStateTx) {
subghz_txrx_stop(subghz->txrx);
subghz_blink_stop(subghz);
result = true;
}
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneRpc, SubGhzRpcStateIdle);
rpc_system_app_confirm(subghz->rpc_ctx, result);
}
}
return consumed;
}
@@ -25,11 +25,17 @@ void subghz_scene_save_name_on_enter(void* context) {
char file_name_buf[SUBGHZ_MAX_LEN_NAME] = {0};
DateTime* datetime = subghz->save_datetime_set ? &subghz->save_datetime : NULL;
subghz->save_datetime_set = false;
#ifdef SUBGHZ_ADD_MANUALLY
name_generator_make_auto_datetime(
file_name_buf, SUBGHZ_MAX_LEN_NAME, SUBGHZ_APP_FILENAME_PREFIX, datetime);
furi_string_set(file_name, file_name_buf);
furi_string_set(subghz->file_path, SUBGHZ_APP_FOLDER);
dev_name_empty = true;
#else
if(!subghz_path_is_file(subghz->file_path)) {
SubGhzProtocolDecoderBase* decoder_result = subghz_txrx_get_decoder(subghz->txrx);
if(subghz->last_settings->protocol_file_names && decoder_result != NULL &&
strlen(decoder_result->protocol->name) != 0 &&
!scene_manager_has_previous_scene(subghz->scene_manager, SubGhzSceneSetType)) {
strlen(decoder_result->protocol->name) != 0) {
name_generator_make_auto_datetime(
file_name_buf, SUBGHZ_MAX_LEN_NAME, decoder_result->protocol->name, datetime);
} else {
@@ -62,6 +68,7 @@ void subghz_scene_save_name_on_enter(void* context) {
}
furi_string_set(subghz->file_path, dir_name);
}
#endif
strlcpy(subghz->file_name_tmp, furi_string_get_cstr(file_name), SUBGHZ_MAX_LEN_NAME);
text_input_set_header_text(text_input, "Name signal");
@@ -89,6 +96,7 @@ bool subghz_scene_save_name_on_event(void* context, SceneManagerEvent event) {
// Set file path to default
furi_string_set(subghz->file_path, SUBGHZ_APP_FOLDER);
//
#ifndef SUBGHZ_ADD_MANUALLY
if(!(strcmp(subghz->file_name_tmp, "") == 0) ||
scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneReadRAW) !=
SubGhzCustomEventManagerNoSet) {
@@ -96,6 +104,7 @@ bool subghz_scene_save_name_on_event(void* context, SceneManagerEvent event) {
furi_string_set(subghz->file_path, subghz->file_path_tmp);
}
}
#endif
scene_manager_previous_scene(subghz->scene_manager);
@@ -113,24 +122,26 @@ bool subghz_scene_save_name_on_event(void* context, SceneManagerEvent event) {
return false;
}
} else {
if(scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneSetType) !=
SubGhzCustomEventManagerNoSet) {
subghz_save_protocol_to_file(
subghz,
subghz_txrx_get_fff_data(subghz->txrx),
furi_string_get_cstr(subghz->file_path));
scene_manager_set_scene_state(
subghz->scene_manager,
SubGhzSceneSetType,
SubGhzCustomEventManagerNoSet);
} else {
subghz_save_protocol_to_file(
subghz,
subghz_history_get_raw_data(subghz->history, subghz->idx_menu_chosen),
furi_string_get_cstr(subghz->file_path));
#ifdef SUBGHZ_ADD_MANUALLY
if(!subghz_save_protocol_to_file(
subghz,
subghz_txrx_get_fff_data(subghz->txrx),
furi_string_get_cstr(subghz->file_path))) {
return true;
}
#else
if(!subghz_save_protocol_to_file(
subghz,
subghz_history_get_raw_data(subghz->history, subghz->idx_menu_chosen),
furi_string_get_cstr(subghz->file_path))) {
return true;
}
#endif
}
#ifdef SUBGHZ_ADD_MANUALLY
subghz_file_name_clear(subghz);
#else
if(scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneReadRAW) !=
SubGhzCustomEventManagerNoSet) {
subghz_protocol_raw_gen_fff_data(
@@ -142,19 +153,21 @@ bool subghz_scene_save_name_on_event(void* context, SceneManagerEvent event) {
} else {
subghz_file_name_clear(subghz);
}
#endif
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSaveSuccess);
#ifdef SUBGHZ_ADD_MANUALLY
dolphin_deed(DolphinDeedSubGhzAddManually);
#else
if(scene_manager_has_previous_scene(subghz->scene_manager, SubGhzSceneSavedMenu)) {
// Nothing, do not count editing as saving
} else if(scene_manager_has_previous_scene(
subghz->scene_manager, SubGhzSceneMoreRAW)) {
// Ditto, for RAW signals
} else if(scene_manager_has_previous_scene(
subghz->scene_manager, SubGhzSceneSetType)) {
dolphin_deed(DolphinDeedSubGhzAddManually);
} else {
dolphin_deed(DolphinDeedSubGhzSave);
}
#endif
return true;
} else {
furi_string_set(subghz->error_str, "No name file");
@@ -24,6 +24,12 @@ bool subghz_scene_save_success_on_event(void* context, SceneManagerEvent event)
SubGhz* subghz = context;
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == SubGhzCustomEventSceneSaveSuccess) {
#ifdef SUBGHZ_ADD_MANUALLY
while(scene_manager_previous_scene(subghz->scene_manager))
;
scene_manager_stop(subghz->scene_manager);
view_dispatcher_stop(subghz->view_dispatcher);
#else
if(!scene_manager_has_previous_scene(subghz->scene_manager, SubGhzSceneDecodeRAW)) {
if(!scene_manager_search_and_switch_to_previous_scene(
subghz->scene_manager, SubGhzSceneReceiver)) {
@@ -59,6 +65,7 @@ bool subghz_scene_save_success_on_event(void* context, SceneManagerEvent event)
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSaved);
}
}
#endif
return true;
}
}
@@ -69,30 +69,21 @@ bool subghz_scene_saved_menu_on_event(void* context, SceneManagerEvent event) {
SubGhz* subghz = context;
if(event.type == SceneManagerEventTypeCustom) {
scene_manager_set_scene_state(subghz->scene_manager, SubGhzSceneSavedMenu, event.event);
if(event.event == SubmenuIndexEmulate) {
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexEmulate);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneTransmitter);
return true;
} else if(event.event == SubmenuIndexDelete) {
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexDelete);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneDelete);
return true;
} else if(event.event == SubmenuIndexEdit) {
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexEdit);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSaveName);
return true;
} else if(event.event == SubmenuIndexGeo) {
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexGeo);
scene_manager_set_scene_state(subghz->scene_manager, SubGhzSceneShowGps, true);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneShowGps);
return true;
} else if(event.event == SubmenuIndexSignalSettings) {
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexSignalSettings);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSignalSettings);
return true;
}
@@ -24,9 +24,9 @@ void subghz_scene_set_button_on_enter(void* context) {
byte_ptr = &subghz->gen_info->keeloq.btn;
byte_count = sizeof(subghz->gen_info->keeloq.btn);
break;
case GenKeeloqBFT:
byte_ptr = &subghz->gen_info->keeloq_bft.btn;
byte_count = sizeof(subghz->gen_info->keeloq_bft.btn);
case GenKeeloqSeed:
byte_ptr = &subghz->gen_info->keeloq_seed.btn;
byte_count = sizeof(subghz->gen_info->keeloq_seed.btn);
break;
case GenAlutechAt4n:
byte_ptr = &subghz->gen_info->alutech_at_4n.btn;
@@ -36,6 +36,10 @@ void subghz_scene_set_button_on_enter(void* context) {
byte_ptr = &subghz->gen_info->somfy_telis.btn;
byte_count = sizeof(subghz->gen_info->somfy_telis.btn);
break;
case GenSomfyKeytis:
byte_ptr = &subghz->gen_info->somfy_keytis.btn;
byte_count = sizeof(subghz->gen_info->somfy_keytis.btn);
break;
case GenKingGatesStylo4k:
byte_ptr = &subghz->gen_info->kinggates_stylo_4k.btn;
byte_count = sizeof(subghz->gen_info->kinggates_stylo_4k.btn);
@@ -44,6 +48,14 @@ void subghz_scene_set_button_on_enter(void* context) {
byte_ptr = &subghz->gen_info->beninca_arc.btn;
byte_count = sizeof(subghz->gen_info->beninca_arc.btn);
break;
case GenJarolift:
byte_ptr = &subghz->gen_info->jarolift.btn;
byte_count = sizeof(subghz->gen_info->jarolift.btn);
break;
case GenDitecGOL4:
byte_ptr = &subghz->gen_info->ditec_gol4.btn;
byte_count = sizeof(subghz->gen_info->ditec_gol4.btn);
break;
case GenNiceFlorS:
byte_ptr = &subghz->gen_info->nice_flor_s.btn;
byte_count = sizeof(subghz->gen_info->nice_flor_s.btn);
@@ -87,12 +99,15 @@ bool subghz_scene_set_button_on_event(void* context, SceneManagerEvent event) {
switch(subghz->gen_info->type) {
case GenFaacSLH:
case GenKeeloq:
case GenKeeloqBFT:
case GenKeeloqSeed:
case GenAlutechAt4n:
case GenSomfyTelis:
case GenKingGatesStylo4k:
case GenBenincaARC:
case GenJarolift:
case GenDitecGOL4:
case GenNiceFlorS:
case GenSomfyKeytis:
case GenSecPlus2:
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSetCounter);
break;
@@ -30,9 +30,9 @@ void subghz_scene_set_counter_on_enter(void* context) {
byte_ptr = (uint8_t*)&subghz->gen_info->came_atomo.cnt;
byte_count = sizeof(subghz->gen_info->came_atomo.cnt);
break;
case GenKeeloqBFT:
byte_ptr = (uint8_t*)&subghz->gen_info->keeloq_bft.cnt;
byte_count = sizeof(subghz->gen_info->keeloq_bft.cnt);
case GenKeeloqSeed:
byte_ptr = (uint8_t*)&subghz->gen_info->keeloq_seed.cnt;
byte_count = sizeof(subghz->gen_info->keeloq_seed.cnt);
break;
case GenAlutechAt4n:
byte_ptr = (uint8_t*)&subghz->gen_info->alutech_at_4n.cnt;
@@ -42,6 +42,10 @@ void subghz_scene_set_counter_on_enter(void* context) {
byte_ptr = (uint8_t*)&subghz->gen_info->somfy_telis.cnt;
byte_count = sizeof(subghz->gen_info->somfy_telis.cnt);
break;
case GenSomfyKeytis:
byte_ptr = (uint8_t*)&subghz->gen_info->somfy_keytis.cnt;
byte_count = sizeof(subghz->gen_info->somfy_keytis.cnt);
break;
case GenKingGatesStylo4k:
byte_ptr = (uint8_t*)&subghz->gen_info->kinggates_stylo_4k.cnt;
byte_count = sizeof(subghz->gen_info->kinggates_stylo_4k.cnt);
@@ -50,6 +54,14 @@ void subghz_scene_set_counter_on_enter(void* context) {
byte_ptr = (uint8_t*)&subghz->gen_info->beninca_arc.cnt;
byte_count = sizeof(subghz->gen_info->beninca_arc.cnt);
break;
case GenJarolift:
byte_ptr = (uint8_t*)&subghz->gen_info->jarolift.cnt;
byte_count = sizeof(subghz->gen_info->jarolift.cnt);
break;
case GenDitecGOL4:
byte_ptr = (uint8_t*)&subghz->gen_info->ditec_gol4.cnt;
byte_count = sizeof(subghz->gen_info->ditec_gol4.cnt);
break;
case GenNiceFlorS:
byte_ptr = (uint8_t*)&subghz->gen_info->nice_flor_s.cnt;
byte_count = sizeof(subghz->gen_info->nice_flor_s.cnt);
@@ -111,8 +123,8 @@ bool subghz_scene_set_counter_on_event(void* context, SceneManagerEvent event) {
case GenCameAtomo:
subghz->gen_info->came_atomo.cnt = __bswap16(subghz->gen_info->came_atomo.cnt);
break;
case GenKeeloqBFT:
subghz->gen_info->keeloq_bft.cnt = __bswap16(subghz->gen_info->keeloq_bft.cnt);
case GenKeeloqSeed:
subghz->gen_info->keeloq_seed.cnt = __bswap16(subghz->gen_info->keeloq_seed.cnt);
break;
case GenAlutechAt4n:
subghz->gen_info->alutech_at_4n.cnt =
@@ -121,6 +133,9 @@ bool subghz_scene_set_counter_on_event(void* context, SceneManagerEvent event) {
case GenSomfyTelis:
subghz->gen_info->somfy_telis.cnt = __bswap16(subghz->gen_info->somfy_telis.cnt);
break;
case GenSomfyKeytis:
subghz->gen_info->somfy_keytis.cnt = __bswap16(subghz->gen_info->somfy_keytis.cnt);
break;
case GenKingGatesStylo4k:
subghz->gen_info->kinggates_stylo_4k.cnt =
__bswap16(subghz->gen_info->kinggates_stylo_4k.cnt);
@@ -128,6 +143,12 @@ bool subghz_scene_set_counter_on_event(void* context, SceneManagerEvent event) {
case GenBenincaARC:
subghz->gen_info->beninca_arc.cnt = __bswap32(subghz->gen_info->beninca_arc.cnt);
break;
case GenJarolift:
subghz->gen_info->jarolift.cnt = __bswap16(subghz->gen_info->jarolift.cnt);
break;
case GenDitecGOL4:
subghz->gen_info->ditec_gol4.cnt = __bswap16(subghz->gen_info->ditec_gol4.cnt);
break;
case GenNiceFlorS:
subghz->gen_info->nice_flor_s.cnt = __bswap16(subghz->gen_info->nice_flor_s.cnt);
break;
@@ -147,7 +168,7 @@ bool subghz_scene_set_counter_on_event(void* context, SceneManagerEvent event) {
switch(subghz->gen_info->type) {
case GenFaacSLH:
case GenKeeloqBFT:
case GenKeeloqSeed:
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSetSeed);
return true;
case GenKeeloq:
@@ -186,6 +207,15 @@ bool subghz_scene_set_counter_on_event(void* context, SceneManagerEvent event) {
subghz->gen_info->somfy_telis.btn,
subghz->gen_info->somfy_telis.cnt);
break;
case GenSomfyKeytis:
generated_protocol = subghz_txrx_gen_somfy_keytis_protocol(
subghz->txrx,
subghz->gen_info->mod,
subghz->gen_info->freq,
subghz->gen_info->somfy_keytis.serial,
subghz->gen_info->somfy_keytis.btn,
subghz->gen_info->somfy_keytis.cnt);
break;
case GenKingGatesStylo4k:
generated_protocol = subghz_txrx_gen_kinggates_stylo_4k_protocol(
subghz->txrx,
@@ -204,6 +234,24 @@ bool subghz_scene_set_counter_on_event(void* context, SceneManagerEvent event) {
subghz->gen_info->beninca_arc.btn,
subghz->gen_info->beninca_arc.cnt);
break;
case GenJarolift:
generated_protocol = subghz_txrx_gen_jarolift_protocol(
subghz->txrx,
subghz->gen_info->mod,
subghz->gen_info->freq,
subghz->gen_info->jarolift.serial,
subghz->gen_info->jarolift.btn,
subghz->gen_info->jarolift.cnt);
break;
case GenDitecGOL4:
generated_protocol = subghz_txrx_gen_ditec_gol4_protocol(
subghz->txrx,
subghz->gen_info->mod,
subghz->gen_info->freq,
subghz->gen_info->ditec_gol4.serial,
subghz->gen_info->ditec_gol4.btn,
subghz->gen_info->ditec_gol4.cnt);
break;
case GenNiceFlorS:
generated_protocol = subghz_txrx_gen_nice_flor_s_protocol(
subghz->txrx,
@@ -248,8 +296,6 @@ bool subghz_scene_set_counter_on_event(void* context, SceneManagerEvent event) {
} else {
subghz_file_name_clear(subghz);
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneSetType, SubGhzCustomEventManagerSet);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSaveName);
}
}
@@ -76,8 +76,6 @@ bool subghz_scene_set_key_on_event(void* context, SceneManagerEvent event) {
} else {
subghz_file_name_clear(subghz);
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneSetType, SubGhzCustomEventManagerSet);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSaveName);
}
}
@@ -22,16 +22,19 @@ void subghz_scene_set_seed_on_enter(void* context) {
byte_ptr = (uint8_t*)&subghz->gen_info->faac_slh.seed;
byte_count = sizeof(subghz->gen_info->faac_slh.seed);
break;
case GenKeeloqBFT:
byte_ptr = (uint8_t*)&subghz->gen_info->keeloq_bft.seed;
byte_count = sizeof(subghz->gen_info->keeloq_bft.seed);
case GenKeeloqSeed:
byte_ptr = (uint8_t*)&subghz->gen_info->keeloq_seed.seed;
byte_count = sizeof(subghz->gen_info->keeloq_seed.seed);
break;
// Not needed for these types
case GenKeeloq:
case GenAlutechAt4n:
case GenSomfyTelis:
case GenSomfyKeytis:
case GenKingGatesStylo4k:
case GenBenincaARC:
case GenJarolift:
case GenDitecGOL4:
case GenNiceFlorS:
case GenSecPlus2:
case GenPhoenixV2:
@@ -75,24 +78,27 @@ bool subghz_scene_set_seed_on_event(void* context, SceneManagerEvent event) {
subghz->gen_info->faac_slh.seed,
subghz->gen_info->faac_slh.manuf);
break;
case GenKeeloqBFT:
subghz->gen_info->keeloq_bft.seed = __bswap32(subghz->gen_info->keeloq_bft.seed);
generated_protocol = subghz_txrx_gen_keeloq_bft_protocol(
case GenKeeloqSeed:
subghz->gen_info->keeloq_seed.seed = __bswap32(subghz->gen_info->keeloq_seed.seed);
generated_protocol = subghz_txrx_gen_keeloq_seed_protocol(
subghz->txrx,
subghz->gen_info->mod,
subghz->gen_info->freq,
subghz->gen_info->keeloq_bft.serial,
subghz->gen_info->keeloq_bft.btn,
subghz->gen_info->keeloq_bft.cnt,
subghz->gen_info->keeloq_bft.seed,
subghz->gen_info->keeloq_bft.manuf);
subghz->gen_info->keeloq_seed.serial,
subghz->gen_info->keeloq_seed.btn,
subghz->gen_info->keeloq_seed.cnt,
subghz->gen_info->keeloq_seed.seed,
subghz->gen_info->keeloq_seed.manuf);
break;
// Not needed for these types
case GenKeeloq:
case GenAlutechAt4n:
case GenSomfyTelis:
case GenSomfyKeytis:
case GenKingGatesStylo4k:
case GenBenincaARC:
case GenJarolift:
case GenDitecGOL4:
case GenNiceFlorS:
case GenSecPlus2:
case GenPhoenixV2:
@@ -113,8 +119,6 @@ bool subghz_scene_set_seed_on_event(void* context, SceneManagerEvent event) {
} else {
subghz_file_name_clear(subghz);
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneSetType, SubGhzCustomEventManagerSet);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSaveName);
}
}
@@ -30,9 +30,9 @@ void subghz_scene_set_serial_on_enter(void* context) {
byte_ptr = (uint8_t*)&subghz->gen_info->came_atomo.serial;
byte_count = sizeof(subghz->gen_info->came_atomo.serial);
break;
case GenKeeloqBFT:
byte_ptr = (uint8_t*)&subghz->gen_info->keeloq_bft.serial;
byte_count = sizeof(subghz->gen_info->keeloq_bft.serial);
case GenKeeloqSeed:
byte_ptr = (uint8_t*)&subghz->gen_info->keeloq_seed.serial;
byte_count = sizeof(subghz->gen_info->keeloq_seed.serial);
break;
case GenAlutechAt4n:
byte_ptr = (uint8_t*)&subghz->gen_info->alutech_at_4n.serial;
@@ -42,6 +42,10 @@ void subghz_scene_set_serial_on_enter(void* context) {
byte_ptr = (uint8_t*)&subghz->gen_info->somfy_telis.serial;
byte_count = sizeof(subghz->gen_info->somfy_telis.serial);
break;
case GenSomfyKeytis:
byte_ptr = (uint8_t*)&subghz->gen_info->somfy_keytis.serial;
byte_count = sizeof(subghz->gen_info->somfy_keytis.serial);
break;
case GenKingGatesStylo4k:
byte_ptr = (uint8_t*)&subghz->gen_info->kinggates_stylo_4k.serial;
byte_count = sizeof(subghz->gen_info->kinggates_stylo_4k.serial);
@@ -50,6 +54,14 @@ void subghz_scene_set_serial_on_enter(void* context) {
byte_ptr = (uint8_t*)&subghz->gen_info->beninca_arc.serial;
byte_count = sizeof(subghz->gen_info->beninca_arc.serial);
break;
case GenJarolift:
byte_ptr = (uint8_t*)&subghz->gen_info->jarolift.serial;
byte_count = sizeof(subghz->gen_info->jarolift.serial);
break;
case GenDitecGOL4:
byte_ptr = (uint8_t*)&subghz->gen_info->ditec_gol4.serial;
byte_count = sizeof(subghz->gen_info->ditec_gol4.serial);
break;
case GenNiceFlorS:
byte_ptr = (uint8_t*)&subghz->gen_info->nice_flor_s.serial;
byte_count = sizeof(subghz->gen_info->nice_flor_s.serial);
@@ -106,9 +118,9 @@ bool subghz_scene_set_serial_on_event(void* context, SceneManagerEvent event) {
subghz->gen_info->came_atomo.serial =
__bswap32(subghz->gen_info->came_atomo.serial);
break;
case GenKeeloqBFT:
subghz->gen_info->keeloq_bft.serial =
__bswap32(subghz->gen_info->keeloq_bft.serial);
case GenKeeloqSeed:
subghz->gen_info->keeloq_seed.serial =
__bswap32(subghz->gen_info->keeloq_seed.serial);
break;
case GenAlutechAt4n:
subghz->gen_info->alutech_at_4n.serial =
@@ -118,10 +130,21 @@ bool subghz_scene_set_serial_on_event(void* context, SceneManagerEvent event) {
subghz->gen_info->somfy_telis.serial =
__bswap32(subghz->gen_info->somfy_telis.serial);
break;
case GenSomfyKeytis:
subghz->gen_info->somfy_keytis.serial =
__bswap32(subghz->gen_info->somfy_keytis.serial);
break;
case GenKingGatesStylo4k:
subghz->gen_info->kinggates_stylo_4k.serial =
__bswap32(subghz->gen_info->kinggates_stylo_4k.serial);
break;
case GenJarolift:
subghz->gen_info->jarolift.serial = __bswap32(subghz->gen_info->jarolift.serial);
break;
case GenDitecGOL4:
subghz->gen_info->ditec_gol4.serial =
__bswap32(subghz->gen_info->ditec_gol4.serial);
break;
case GenBenincaARC:
subghz->gen_info->beninca_arc.serial =
__bswap32(subghz->gen_info->beninca_arc.serial);
@@ -149,11 +172,14 @@ bool subghz_scene_set_serial_on_event(void* context, SceneManagerEvent event) {
switch(subghz->gen_info->type) {
case GenFaacSLH:
case GenKeeloq:
case GenKeeloqBFT:
case GenKeeloqSeed:
case GenAlutechAt4n:
case GenSomfyTelis:
case GenSomfyKeytis:
case GenKingGatesStylo4k:
case GenBenincaARC:
case GenJarolift:
case GenDitecGOL4:
case GenNiceFlorS:
case GenSecPlus2:
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSetButton);
@@ -15,13 +15,17 @@ static const char* submenu_names[SetTypeMAX] = {
[SetTypeFaacSLH_868] = "FAAC SLH 868MHz",
[SetTypeFaacSLH_433] = "FAAC SLH 433MHz",
[SetTypeBFTMitto] = "BFT Mitto 433MHz",
[SetTypeErreka433] = "Erreka 433MHz",
[SetTypeSomfyTelis] = "Somfy Telis 433MHz",
[SetTypeSomfyKeytis] = "Somfy Keytis 433MHz",
[SetTypeANMotorsAT4] = "AN-Motors AT4 433MHz",
[SetTypeAlutechAT4N] = "Alutech AT4N 433MHz",
[SetTypeRoger_433] = "Roger 433MHz",
[SetTypePhoenix_V2_433] = "V2 Phoenix 433MHz",
[SetTypeKingGatesStylo4k] = "KingGates Stylo4k 433M.",
[SetTypeBenincaARC] = "Beninca ARC 433MHz",
[SetTypeJarolift] = "Jarolift 433MHz",
[SetTypeDitecGOL4] = "Ditec GOL4 433MHz",
[SetTypeHCS101_433_92] = "KL: HCS101 433MHz",
[SetTypeDoorHan_315_00] = "KL: DoorHan 315MHz",
[SetTypeDoorHan_433_92] = "KL: DoorHan 433MHz",
@@ -53,9 +57,22 @@ static const char* submenu_names[SetTypeMAX] = {
[SetTypeNovoferm_433_92] = "KL: Novoferm 433MHz",
[SetTypeHormannEcoStar_433_92] = "KL: Hor. EcoStar 433MHz",
[SetTypeCardinS449_433FM] = "KL: Cardin S449 433MHz",
[SetTypePujol433] = "KL: Pujol 433MHz",
[SetTypePujol_Vario433] = "KL: Pujol Vario 433MHz",
[SetTypeET_Blue433] = "KL: ET Blue 433MHz",
[SetTypeET_Blue_Mix433] = "KL: ET Blue Mix 433MHz",
[SetTypeATA_PTX4_433] = "KL: ATA PTX4 433MHz",
[SetTypeSeav433] = "KL: Seav 433MHz",
[SetTypeWisniowski433] = "KL: Wisniowski 433MHz",
[SetTypeFadini433] = "KL: Fadini 433MHz",
[SetTypeMc_Garcia433] = "KL: Mc Garcia 433MHz",
[SetTypeClemsa_Mutancode433] = "KL: Clm.Mutancode 433M.",
[SetTypeDoormatic433] = "KL: Doormatic 433MHz",
[SetTypeElvox433] = "KL: Elvox 433MHz",
[SetTypeVerex433] = "KL: Verex 433MHz",
[SetTypeFAACRCXT_433_92] = "KL: FAAC RC,XT 433MHz",
[SetTypeFAACRCXT_868] = "KL: FAAC RC,XT 868MHz",
[SetTypeGeniusBravo433] = "KL: Genius Bravo 433MHz",
[SetTypeGeniusBravo433] = "KL: Genius TX4RC 433M.",
[SetTypeNiceMHouse_433_92] = "KL: Mhouse 433MHz",
[SetTypeNiceSmilo_433_92] = "KL: Nice Smilo 433MHz",
[SetTypeNiceFlorS_433_92] = "Nice FloR-S 433MHz",
@@ -101,9 +118,6 @@ void subghz_scene_set_type_on_enter(void* context) {
subghz->submenu, submenu_names[i], i, subghz_scene_set_type_submenu_callback, subghz);
}
submenu_set_selected_item(
subghz->submenu, scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneSetType));
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdMenu);
}
@@ -160,16 +174,16 @@ bool subghz_scene_set_type_generate_protocol_from_infos(SubGhz* subghz) {
gen_info.came_atomo.serial,
gen_info.came_atomo.cnt);
break;
case GenKeeloqBFT:
generated_protocol = subghz_txrx_gen_keeloq_bft_protocol(
case GenKeeloqSeed:
generated_protocol = subghz_txrx_gen_keeloq_seed_protocol(
subghz->txrx,
gen_info.mod,
gen_info.freq,
gen_info.keeloq_bft.serial,
gen_info.keeloq_bft.btn,
gen_info.keeloq_bft.cnt,
gen_info.keeloq_bft.seed,
gen_info.keeloq_bft.manuf);
gen_info.keeloq_seed.serial,
gen_info.keeloq_seed.btn,
gen_info.keeloq_seed.cnt,
gen_info.keeloq_seed.seed,
gen_info.keeloq_seed.manuf);
break;
case GenAlutechAt4n:
generated_protocol = subghz_txrx_gen_alutech_at_4n_protocol(
@@ -189,6 +203,15 @@ bool subghz_scene_set_type_generate_protocol_from_infos(SubGhz* subghz) {
gen_info.somfy_telis.btn,
gen_info.somfy_telis.cnt);
break;
case GenSomfyKeytis:
generated_protocol = subghz_txrx_gen_somfy_keytis_protocol(
subghz->txrx,
gen_info.mod,
gen_info.freq,
gen_info.somfy_keytis.serial,
gen_info.somfy_keytis.btn,
gen_info.somfy_keytis.cnt);
break;
case GenKingGatesStylo4k:
generated_protocol = subghz_txrx_gen_kinggates_stylo_4k_protocol(
subghz->txrx,
@@ -207,6 +230,24 @@ bool subghz_scene_set_type_generate_protocol_from_infos(SubGhz* subghz) {
gen_info.beninca_arc.btn,
gen_info.beninca_arc.cnt);
break;
case GenJarolift:
generated_protocol = subghz_txrx_gen_jarolift_protocol(
subghz->txrx,
gen_info.mod,
gen_info.freq,
gen_info.jarolift.serial,
gen_info.jarolift.btn,
gen_info.jarolift.cnt);
break;
case GenDitecGOL4:
generated_protocol = subghz_txrx_gen_ditec_gol4_protocol(
subghz->txrx,
gen_info.mod,
gen_info.freq,
gen_info.ditec_gol4.serial,
gen_info.ditec_gol4.btn,
gen_info.ditec_gol4.cnt);
break;
case GenNiceFlorS:
generated_protocol = subghz_txrx_gen_nice_flor_s_protocol(
subghz->txrx,
@@ -245,8 +286,6 @@ bool subghz_scene_set_type_generate_protocol_from_infos(SubGhz* subghz) {
if(generated_protocol) {
subghz_file_name_clear(subghz);
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneSetType, SubGhzCustomEventManagerSet);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSaveName);
} else {
furi_string_set(subghz->error_str, "Function requires\nan SD card with\nfresh databases.");
@@ -263,16 +302,15 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) {
if(event.event >= SetTypeMAX) {
return false;
}
scene_manager_set_scene_state(subghz->scene_manager, SubGhzSceneSetType, event.event);
subghz_gen_info_reset(subghz->gen_info);
subghz_scene_set_type_fill_generation_infos(subghz->gen_info, event.event);
if(scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneStart) ==
if(scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneSetType) ==
SubmenuIndexAddManually) {
generated_protocol = subghz_scene_set_type_generate_protocol_from_infos(subghz);
} else if(
scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneStart) ==
scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneSetType) ==
SubmenuIndexAddManuallyAdvanced) {
switch(subghz->gen_info->type) {
case GenData: // Key (u64)
@@ -283,11 +321,14 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) {
case GenFaacSLH: // Serial (u32), Button (u8), Counter (u32), Seed (u32)
case GenKeeloq: // Serial (u32), Button (u8), Counter (u16)
case GenCameAtomo: // Serial (u32), Counter (u16)
case GenKeeloqBFT: // Serial (u32), Button (u8), Counter (u16), Seed (u32)
case GenKeeloqSeed: // Serial (u32), Button (u8), Counter (u16), Seed (u32)
case GenAlutechAt4n: // Serial (u32), Button (u8), Counter (u16)
case GenSomfyTelis: // Serial (u32), Button (u8), Counter (u16)
case GenSomfyKeytis: // Serial (u32), Button (u8), Counter (u16)
case GenKingGatesStylo4k: // Serial (u32), Button (u8), Counter (u16)
case GenBenincaARC: // Serial (u32), Button (u8), Counter (u32)
case GenJarolift: // Serial (u32), Button (u4), Counter (u16)
case GenDitecGOL4: // Serial (u32), Button (u4), Counter (u16)
case GenNiceFlorS: // Serial (u32), Button (u8), Counter (u16)
case GenSecPlus2: // Serial (u32), Button (u8), Counter (u32)
case GenPhoenixV2: // Serial (u32), Counter (u16)
@@ -11,11 +11,18 @@
static uint32_t counter_mode = 0xff;
static uint32_t counter32 = 0x0;
static uint16_t counter16 = 0x0;
static uint8_t byte_count = 0;
static uint8_t* byte_ptr = NULL;
static uint8_t cnt_byte_count = 0;
static uint8_t* cnt_byte_ptr = NULL;
static FuriString* byte_input_text;
#define COUNTER_MODE_COUNT 7
static uint8_t button = 0x0;
static uint8_t btn_byte_count = 1;
static uint8_t* btn_byte_ptr = NULL;
static uint8_t submenu_called = 0;
#define COUNTER_MODE_COUNT 8
static const char* const counter_mode_text[COUNTER_MODE_COUNT] = {
"System",
"Mode 1",
@@ -24,6 +31,7 @@ static const char* const counter_mode_text[COUNTER_MODE_COUNT] = {
"Mode 4",
"Mode 5",
"Mode 6",
"Mode 7",
};
static const int32_t counter_mode_value[COUNTER_MODE_COUNT] = {
@@ -34,6 +42,7 @@ static const int32_t counter_mode_value[COUNTER_MODE_COUNT] = {
4,
5,
6,
7,
};
typedef struct {
@@ -42,11 +51,12 @@ typedef struct {
} Protocols;
// List of protocols names and appropriate CounterMode counts
static Protocols protocols[] = {
static const Protocols protocols[] = {
{"Nice FloR-S", 3},
{"CAME Atomo", 4},
{"Alutech AT-4N", 3},
{"KeeLoq", 7},
{"KeeLoq", 8},
{"Phoenix_V2", 3},
};
#define PROTOCOLS_COUNT (sizeof(protocols) / sizeof(Protocols));
@@ -55,6 +65,38 @@ void subghz_scene_signal_settings_counter_mode_changed(VariableItem* item) {
uint8_t index = variable_item_get_current_value_index(item);
variable_item_set_current_value_text(item, counter_mode_text[index]);
counter_mode = counter_mode_value[index];
SubGhz* subghz = variable_item_get_context(item);
const char* file_path = furi_string_get_cstr(subghz->file_path);
furi_assert(subghz);
furi_assert(file_path);
// update file every time when we change mode
Storage* storage = furi_record_open(RECORD_STORAGE);
FlipperFormat* fff_data_file = flipper_format_file_alloc(storage);
// check is the file available for update/insert CounterMode value
if(flipper_format_file_open_existing(fff_data_file, file_path)) {
if(flipper_format_insert_or_update_uint32(fff_data_file, "CounterMode", &counter_mode, 1)) {
FURI_LOG_D(TAG, "Successfully updated/inserted CounterMode value %li", counter_mode);
} else {
FURI_LOG_E(TAG, "Error update/insert CounterMode value");
}
} else {
FURI_LOG_E(TAG, "Error open file %s for writing", file_path);
}
flipper_format_file_close(fff_data_file);
flipper_format_free(fff_data_file);
furi_record_close(RECORD_STORAGE);
// we need to reload file after editing it
if(subghz_key_load(subghz, file_path, false)) {
FURI_LOG_D(TAG, "Subghz file was successfully reloaded");
} else {
FURI_LOG_E(TAG, "Error reloading subghz file");
}
}
void subghz_scene_signal_settings_byte_input_callback(void* context) {
@@ -67,6 +109,7 @@ void subghz_scene_signal_settings_variable_item_list_enter_callback(void* contex
// when we click OK on "Edit counter" item
if(index == 1) {
submenu_called = 1;
furi_string_cat_printf(byte_input_text, "%i", subghz_block_generic_global.cnt_length_bit);
furi_string_cat_str(byte_input_text, "-bits counter in HEX");
@@ -79,8 +122,27 @@ void subghz_scene_signal_settings_variable_item_list_enter_callback(void* contex
subghz_scene_signal_settings_byte_input_callback,
NULL,
subghz,
byte_ptr,
byte_count);
cnt_byte_ptr,
cnt_byte_count);
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdByteInput);
}
// when we click OK on "Edit button" item
if(index == 2) {
submenu_called = 2;
furi_string_cat_printf(byte_input_text, "%i", subghz_block_generic_global.btn_length_bit);
furi_string_cat_str(byte_input_text, "-bits button in HEX");
// Setup byte_input view
ByteInput* byte_input = subghz->byte_input;
byte_input_set_header_text(byte_input, furi_string_get_cstr(byte_input_text));
byte_input_set_result_callback(
byte_input,
subghz_scene_signal_settings_byte_input_callback,
NULL,
subghz,
btn_byte_ptr,
btn_byte_count);
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdByteInput);
}
}
@@ -130,44 +192,9 @@ void subghz_scene_signal_settings_on_enter(void* context) {
flipper_format_free(fff_data_file);
furi_record_close(RECORD_STORAGE);
// ### Counter edit section ###
byte_input_text = furi_string_alloc_set_str("Enter ");
bool counter_not_available = true;
SubGhzProtocolDecoderBase* decoder = subghz_txrx_get_decoder(subghz->txrx);
// deserialaze and decode loaded sugbhz file and push data to subghz_block_generic_global variable
if(subghz_protocol_decoder_base_deserialize(decoder, subghz_txrx_get_fff_data(subghz->txrx)) ==
SubGhzProtocolStatusOk) {
subghz_protocol_decoder_base_get_string(decoder, tmp_text);
} else {
FURI_LOG_E(TAG, "Cant deserialize this subghz file");
}
if(!subghz_block_generic_global.cnt_is_available) {
counter_mode = 0xff;
FURI_LOG_D(TAG, "Counter mode and edit not available for this protocol");
} else {
counter_not_available = false;
// Check is there byte_count more than 2 hex bytes long or not
// To show hex value we must correct revert bytes for ByteInput view with __bswapХХ
if(subghz_block_generic_global.cnt_length_bit > 16) {
counter32 = subghz_block_generic_global.current_cnt;
furi_string_printf(tmp_text, "%lX", counter32);
counter32 = __bswap32(counter32);
byte_ptr = (uint8_t*)&counter32;
byte_count = 4;
} else {
counter16 = subghz_block_generic_global.current_cnt;
furi_string_printf(tmp_text, "%X", counter16);
counter16 = __bswap16(counter16);
byte_ptr = (uint8_t*)&counter16;
byte_count = 2;
}
}
furi_assert(byte_ptr);
furi_assert(byte_count > 0);
bool button_not_available = true;
//Create and Enable/Disable variable_item_list depending on current values
VariableItemList* variable_item_list = subghz->variable_item_list;
@@ -186,15 +213,72 @@ void subghz_scene_signal_settings_on_enter(void* context) {
subghz_scene_signal_settings_counter_mode_changed,
subghz);
value_index = value_index_int32(counter_mode, counter_mode_value, mode_count);
variable_item_set_current_value_index(item, value_index);
variable_item_set_current_value_text(item, counter_mode_text[value_index]);
variable_item_set_locked(item, (counter_mode == 0xff), "Not available\nfor this\nprotocol !");
//
SubGhzProtocolDecoderBase* decoder = subghz_txrx_get_decoder(subghz->txrx);
// deserialaze and decode loaded sugbhz file and push data to subghz_block_generic_global variable
if(subghz_protocol_decoder_base_deserialize(decoder, subghz_txrx_get_fff_data(subghz->txrx)) ==
SubGhzProtocolStatusOk) {
subghz_protocol_decoder_base_get_string(decoder, tmp_text);
} else {
FURI_LOG_E(TAG, "Cant deserialize this subghz file");
}
// ### Counter edit section ###
if(!subghz_block_generic_global.cnt_is_available) {
counter_mode = 0xff;
FURI_LOG_D(TAG, "Counter mode and edit not available for this protocol");
} else {
counter_not_available = false;
// Check is there byte_count more than 2 hex bytes long or not
// To show hex value we must correct revert bytes for ByteInput view with __bswapХХ
if(subghz_block_generic_global.cnt_length_bit > 16) {
counter32 = subghz_block_generic_global.current_cnt;
furi_string_printf(tmp_text, "%lX", counter32);
counter32 = __bswap32(counter32);
cnt_byte_ptr = (uint8_t*)&counter32;
cnt_byte_count = 4;
} else {
counter16 = subghz_block_generic_global.current_cnt;
furi_string_printf(tmp_text, "%X", counter16);
counter16 = __bswap16(counter16);
cnt_byte_ptr = (uint8_t*)&counter16;
cnt_byte_count = 2;
}
}
item = variable_item_list_add(variable_item_list, "Edit Counter", 1, NULL, subghz);
variable_item_set_current_value_index(item, 0);
variable_item_set_current_value_text(item, furi_string_get_cstr(tmp_text));
variable_item_set_locked(item, (counter_not_available), "Not available\nfor this\nprotocol !");
//
// ### Button edit section ###
if(!subghz_block_generic_global.btn_is_available) {
FURI_LOG_D(TAG, "Button edit not available for this protocol");
} else {
button_not_available = false;
button = subghz_block_generic_global.current_btn;
furi_string_printf(tmp_text, "%X", button);
btn_byte_ptr = (uint8_t*)&button;
}
item = variable_item_list_add(variable_item_list, "Edit Button", 1, NULL, subghz);
variable_item_set_current_value_index(item, 0);
variable_item_set_current_value_text(item, furi_string_get_cstr(tmp_text));
variable_item_set_locked(item, (button_not_available), "Not available\nfor this\nprotocol !");
//
furi_assert(cnt_byte_ptr);
furi_assert(cnt_byte_count > 0);
furi_assert(btn_byte_ptr);
furi_string_free(tmp_text);
@@ -206,21 +290,40 @@ bool subghz_scene_signal_settings_on_event(void* context, SceneManagerEvent even
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == SubGhzCustomEventByteInputDone) {
switch(byte_count) {
switch(submenu_called) {
// edit counter
case 1:
switch(cnt_byte_count) {
case 2:
// set new cnt value and override_flag to global variable and call transmit to generate and save subghz signal
counter16 = __bswap16(counter16);
subghz_block_generic_global_counter_override_set(counter16);
subghz_tx_start(subghz, subghz_txrx_get_fff_data(subghz->txrx));
subghz_txrx_stop(subghz->txrx);
break;
case 4:
// the same for 32 bit Counter
counter32 = __bswap32(counter32);
subghz_block_generic_global_counter_override_set(counter32);
subghz_tx_start(subghz, subghz_txrx_get_fff_data(subghz->txrx));
subghz_txrx_stop(subghz->txrx);
break;
default:
break;
}
break;
// edit button
case 2:
// set new cnt value and override_flag to global variable and call transmit to generate and save subghz signal
counter16 = __bswap16(counter16);
subghz_block_generic_global_counter_override_set(counter16);
subghz_tx_start(subghz, subghz_txrx_get_fff_data(subghz->txrx));
subghz_txrx_stop(subghz->txrx);
break;
case 4:
// the same for 32 bit Counter
counter32 = __bswap32(counter32);
subghz_block_generic_global_counter_override_set(counter32);
subghz_block_generic_global_button_override_set(button);
// save counter mult to rewrite subghz singnal without changing counter
int32_t tmp_counter = furi_hal_subghz_get_rolling_counter_mult();
furi_hal_subghz_set_rolling_counter_mult(0);
subghz_tx_start(subghz, subghz_txrx_get_fff_data(subghz->txrx));
subghz_txrx_stop(subghz->txrx);
// restore counter mult
furi_hal_subghz_set_rolling_counter_mult(tmp_counter);
break;
default:
break;
}
@@ -240,40 +343,8 @@ bool subghz_scene_signal_settings_on_event(void* context, SceneManagerEvent even
void subghz_scene_signal_settings_on_exit(void* context) {
SubGhz* subghz = context;
const char* file_path = furi_string_get_cstr(subghz->file_path);
furi_assert(subghz);
furi_assert(file_path);
// if ConterMode was changed from 0xff then we must update or write new value to file
if(counter_mode != 0xff) {
Storage* storage = furi_record_open(RECORD_STORAGE);
FlipperFormat* fff_data_file = flipper_format_file_alloc(storage);
// check is the file available for update/insert CounterMode value
if(flipper_format_file_open_existing(fff_data_file, file_path)) {
if(flipper_format_insert_or_update_uint32(
fff_data_file, "CounterMode", &counter_mode, 1)) {
FURI_LOG_D(
TAG, "Successfully updated/inserted CounterMode value %li", counter_mode);
} else {
FURI_LOG_E(TAG, "Error update/insert CounterMode value");
}
} else {
FURI_LOG_E(TAG, "Error open file %s for writing", file_path);
}
flipper_format_file_close(fff_data_file);
flipper_format_free(fff_data_file);
furi_record_close(RECORD_STORAGE);
// we need to reload file after editing when we exit from Signal Settings menu.
if(subghz_key_load(subghz, file_path, false)) {
FURI_LOG_D(TAG, "Subghz file was successfully reloaded");
} else {
FURI_LOG_E(TAG, "Error reloading subghz file");
}
}
// Clear views
variable_item_list_set_selected_item(subghz->variable_item_list, 0);
@@ -1,6 +1,7 @@
#include "../subghz_i.h"
#include "subghz_scene_start.h"
#include <dolphin/dolphin.h>
#include <loader/loader.h>
#include <lib/subghz/protocols/raw.h>
@@ -63,42 +64,46 @@ bool subghz_scene_start_on_event(void* context, SceneManagerEvent event) {
view_dispatcher_stop(subghz->view_dispatcher);
return true;
} else if(event.type == SceneManagerEventTypeCustom) {
scene_manager_set_scene_state(subghz->scene_manager, SubGhzSceneStart, event.event);
if(event.event == SubmenuIndexReadRAW) {
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneStart, SubmenuIndexReadRAW);
subghz_rx_key_state_set(subghz, SubGhzRxKeyStateIDLE);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneReadRAW);
return true;
} else if(event.event == SubmenuIndexRead) {
subghz_rx_key_state_set(subghz, SubGhzRxKeyStateIDLE);
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneStart, SubmenuIndexRead);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneReceiver);
return true;
} else if(event.event == SubmenuIndexSaved) {
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneStart, SubmenuIndexSaved);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSaved);
return true;
} else if(event.event == SubmenuIndexAddManually) {
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneStart, SubmenuIndexAddManually);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSetType);
return true;
} else if(event.event == SubmenuIndexAddManuallyAdvanced) {
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneStart, SubmenuIndexAddManuallyAdvanced);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSetType);
} else if(
event.event == SubmenuIndexAddManually ||
event.event == SubmenuIndexAddManuallyAdvanced) {
const char* arg = (event.event == SubmenuIndexAddManuallyAdvanced) ?
"AddManuallyAdvanced" :
"AddManually";
FuriString* self_path = furi_string_alloc();
Loader* loader = furi_record_open(RECORD_LOADER);
furi_check(loader_get_application_launch_path(loader, self_path));
loader_enqueue_launch(
loader,
EXT_PATH("apps/assets/subghz_add_manually.fap"),
arg,
LoaderDeferredLaunchFlagGui);
loader_enqueue_launch(
loader, furi_string_get_cstr(self_path), arg, LoaderDeferredLaunchFlagGui);
furi_record_close(RECORD_LOADER);
furi_string_free(self_path);
while(scene_manager_previous_scene(subghz->scene_manager))
;
scene_manager_stop(subghz->scene_manager);
view_dispatcher_stop(subghz->view_dispatcher);
return true;
} else if(event.event == SubmenuIndexFrequencyAnalyzer) {
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneStart, SubmenuIndexFrequencyAnalyzer);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneFrequencyAnalyzer);
dolphin_deed(DolphinDeedSubGhzFrequencyAnalyzer);
return true;
} else if(event.event == SubmenuIndexExtSettings) {
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneStart, SubmenuIndexExtSettings);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneExtModuleSettings);
return true;
}
@@ -5,6 +5,9 @@
#include <lib/subghz/blocks/custom_btn.h>
#include "applications/main/subghz/helpers/subghz_txrx_i.h"
#include "lib/subghz/blocks/generic.h"
#define TAG "SubGhzSceneTransmitter"
void subghz_scene_transmitter_callback(SubGhzCustomEvent event, void* context) {
@@ -35,6 +38,7 @@ bool subghz_scene_transmitter_update_data_show(void* context) {
furi_string_get_cstr(frequency_str),
furi_string_get_cstr(modulation_str),
subghz_txrx_protocol_is_transmittable(subghz->txrx, false));
ret = true;
}
furi_string_free(frequency_str);
@@ -80,11 +84,16 @@ void subghz_scene_transmitter_on_enter(void* context) {
}
bool subghz_scene_transmitter_on_event(void* context, SceneManagerEvent event) {
// key concept: we start endless TX until user release OK button, and after this we send last
// protocols repeats - this guarantee that one press OK will
// be guarantee send the required minimum protocol data packets
// for all of this we use subghz_block_generic_global.endless_tx in protocols _yield function.
SubGhz* subghz = context;
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == SubGhzCustomEventViewTransmitterSendStart) {
// user press OK - start endless TX
subghz->state_notifications = SubGhzNotificationStateIDLE;
subghz_block_generic_global.endless_tx = true;
if(subghz_tx_start(subghz, subghz_txrx_get_fff_data(subghz->txrx))) {
subghz->state_notifications = SubGhzNotificationStateTx;
subghz_scene_transmitter_update_data_show(subghz);
@@ -92,29 +101,12 @@ bool subghz_scene_transmitter_on_event(void* context, SceneManagerEvent event) {
}
return true;
} else if(event.event == SubGhzCustomEventViewTransmitterSendStop) {
subghz->state_notifications = SubGhzNotificationStateIDLE;
subghz_txrx_stop(subghz->txrx);
if(subghz_custom_btn_get() != SUBGHZ_CUSTOM_BTN_OK) {
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_OK);
int32_t tmp_counter = furi_hal_subghz_get_rolling_counter_mult();
furi_hal_subghz_set_rolling_counter_mult(0);
// Calling restore!
subghz_tx_start(subghz, subghz_txrx_get_fff_data(subghz->txrx));
subghz_txrx_stop(subghz->txrx);
// Calling restore 2nd time special for FAAC SLH!
// TODO: Find better way to restore after custom button is used!!!
subghz_tx_start(subghz, subghz_txrx_get_fff_data(subghz->txrx));
subghz_txrx_stop(subghz->txrx);
furi_hal_subghz_set_rolling_counter_mult(tmp_counter);
}
if(subghz->fav_timeout) {
while(scene_manager_handle_back_event(subghz->scene_manager))
;
view_dispatcher_stop(subghz->view_dispatcher);
}
// user release OK
// we switch off endless_tx - that mean protocols yield finish endless transmission,
// send upload "repeat=xx" times, and after will be stoped by tick event down this code.
subghz_block_generic_global.endless_tx = false;
return true;
} else if(event.event == SubGhzCustomEventViewTransmitterBack) {
subghz->state_notifications = SubGhzNotificationStateIDLE;
scene_manager_search_and_switch_to_previous_scene(
subghz->scene_manager, SubGhzSceneStart);
return true;
@@ -124,7 +116,33 @@ bool subghz_scene_transmitter_on_event(void* context, SceneManagerEvent event) {
}
} else if(event.type == SceneManagerEventTypeTick) {
if(subghz->state_notifications == SubGhzNotificationStateTx) {
notification_message(subghz->notifications, &sequence_blink_magenta_10);
// if hardware TX still working at this time so we just blink led and return
if(!subghz_devices_is_async_complete_tx(subghz->txrx->radio_device)) {
notification_message(subghz->notifications, &sequence_blink_magenta_10);
return true;
// if hardware TX was stoped so we stop TX correctly
} else {
subghz->state_notifications = SubGhzNotificationStateIDLE;
subghz_txrx_stop(subghz->txrx);
if(subghz_custom_btn_get() != SUBGHZ_CUSTOM_BTN_OK) {
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_OK);
int32_t tmp_counter = furi_hal_subghz_get_rolling_counter_mult();
furi_hal_subghz_set_rolling_counter_mult(0);
// Calling restore!
subghz_tx_start(subghz, subghz_txrx_get_fff_data(subghz->txrx));
subghz_txrx_stop(subghz->txrx);
// Calling restore 2nd time special for FAAC SLH!
// TODO: Find better way to restore after custom button is used!!!
subghz_tx_start(subghz, subghz_txrx_get_fff_data(subghz->txrx));
subghz_txrx_stop(subghz->txrx);
furi_hal_subghz_set_rolling_counter_mult(tmp_counter);
}
if(subghz->fav_timeout) {
while(scene_manager_handle_back_event(subghz->scene_manager))
;
view_dispatcher_stop(subghz->view_dispatcher);
}
}
}
return true;
}
+83 -4
View File
@@ -8,6 +8,8 @@
#include <applications/main/archive/helpers/archive_helpers_ext.h>
#include <momentum/momentum.h>
#include "scenes/subghz_scene_start.h"
#include "subghz_fap.h"
#define TAG "SubGhzApp"
@@ -64,7 +66,7 @@ static void subghz_rpc_command_callback(const RpcAppSystemEvent* event, void* co
static void subghz_load_custom_presets(SubGhzSetting* setting) {
furi_assert(setting);
const char* presets[][2] = {
static const char* presets[][2] = {
// FM95
{"FM95",
"02 0D 0B 06 08 32 07 04 14 00 13 02 12 04 11 83 10 67 15 24 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"},
@@ -128,12 +130,14 @@ SubGhz* subghz_alloc(bool alloc_for_tx_only) {
view_dispatcher_add_view(
subghz->view_dispatcher, SubGhzViewIdMenu, submenu_get_view(subghz->submenu));
#ifndef SUBGHZ_ADD_MANUALLY
// Receiver
subghz->subghz_receiver = subghz_view_receiver_alloc();
view_dispatcher_add_view(
subghz->view_dispatcher,
SubGhzViewIdReceiver,
subghz_view_receiver_get_view(subghz->subghz_receiver));
#endif
}
// Popup
subghz->popup = popup_alloc();
@@ -162,12 +166,14 @@ SubGhz* subghz_alloc(bool alloc_for_tx_only) {
//Dialog
subghz->dialogs = furi_record_open(RECORD_DIALOGS);
#ifndef SUBGHZ_ADD_MANUALLY
// Transmitter
subghz->subghz_transmitter = subghz_view_transmitter_alloc();
view_dispatcher_add_view(
subghz->view_dispatcher,
SubGhzViewIdTransmitter,
subghz_view_transmitter_get_view(subghz->subghz_transmitter));
#endif
if(!alloc_for_tx_only) {
// Variable Item List
subghz->variable_item_list = variable_item_list_alloc();
@@ -176,6 +182,7 @@ SubGhz* subghz_alloc(bool alloc_for_tx_only) {
SubGhzViewIdVariableItemList,
variable_item_list_get_view(subghz->variable_item_list));
#ifndef SUBGHZ_ADD_MANUALLY
// Frequency Analyzer
// View knows too much
subghz->subghz_frequency_analyzer = subghz_frequency_analyzer_alloc(subghz->txrx);
@@ -183,7 +190,10 @@ SubGhz* subghz_alloc(bool alloc_for_tx_only) {
subghz->view_dispatcher,
SubGhzViewIdFrequencyAnalyzer,
subghz_frequency_analyzer_get_view(subghz->subghz_frequency_analyzer));
#endif
}
#ifndef SUBGHZ_ADD_MANUALLY
// Read RAW
subghz->subghz_read_raw = subghz_read_raw_alloc(alloc_for_tx_only);
view_dispatcher_add_view(
@@ -229,23 +239,27 @@ SubGhz* subghz_alloc(bool alloc_for_tx_only) {
}
subghz_txrx_set_preset_internal(
subghz->txrx, subghz->last_settings->frequency, subghz->last_settings->preset_index);
subghz->txrx,
subghz->last_settings->frequency,
subghz->last_settings->preset_index,
subghz->tx_power);
subghz->history = subghz_history_alloc();
}
subghz_rx_key_state_set(subghz, SubGhzRxKeyStateIDLE);
subghz->gen_info = malloc(sizeof(GenInfo));
if(!alloc_for_tx_only) {
subghz->remove_duplicates = subghz->last_settings->remove_duplicates;
subghz->ignore_filter = subghz->last_settings->ignore_filter;
subghz->filter = subghz->last_settings->filter;
subghz->tx_power = subghz->last_settings->tx_power;
} else {
subghz->filter = SubGhzProtocolFlag_Decodable;
subghz->ignore_filter = 0x0;
subghz->remove_duplicates = false;
subghz->tx_power = 0;
}
subghz_txrx_receiver_set_filter(subghz->txrx, subghz->filter);
subghz_txrx_receiver_set_ignore_filter(subghz->txrx, subghz->ignore_filter);
subghz_txrx_set_need_save_callback(subghz->txrx, subghz_save_to_file, subghz);
@@ -257,6 +271,9 @@ SubGhz* subghz_alloc(bool alloc_for_tx_only) {
subghz->last_settings->rssi = SUBGHZ_LAST_SETTING_FREQUENCY_ANALYZER_TRIGGER;
}
}
#else
subghz->gen_info = malloc(sizeof(GenInfo));
#endif
#if SUBGHZ_MEASURE_LOADING
load_ticks = furi_get_tick() - load_ticks;
FURI_LOG_I(TAG, "Loaded: %ld ms.", load_ticks);
@@ -264,9 +281,11 @@ SubGhz* subghz_alloc(bool alloc_for_tx_only) {
//Init Error_str
subghz->error_str = furi_string_alloc();
#ifndef SUBGHZ_ADD_MANUALLY
if(subghz->last_settings->gps_baudrate != 0) {
subghz->gps = subghz_gps_plugin_init(subghz->last_settings->gps_baudrate);
}
#endif
return subghz;
}
@@ -274,6 +293,7 @@ SubGhz* subghz_alloc(bool alloc_for_tx_only) {
void subghz_free(SubGhz* subghz, bool alloc_for_tx_only) {
furi_assert(subghz);
#ifndef SUBGHZ_ADD_MANUALLY
if(subghz->rpc_ctx) {
rpc_system_app_set_callback(subghz->rpc_ctx, NULL, NULL);
rpc_system_app_send_exited(subghz->rpc_ctx);
@@ -284,11 +304,14 @@ void subghz_free(SubGhz* subghz, bool alloc_for_tx_only) {
subghz_txrx_speaker_off(subghz->txrx);
subghz_txrx_stop(subghz->txrx);
subghz_txrx_sleep(subghz->txrx);
#endif
if(!alloc_for_tx_only) {
#ifndef SUBGHZ_ADD_MANUALLY
// Receiver
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdReceiver);
subghz_view_receiver_free(subghz->subghz_receiver);
#endif
// TextInput
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdTextInput);
@@ -305,21 +328,27 @@ void subghz_free(SubGhz* subghz, bool alloc_for_tx_only) {
//Dialog
furi_record_close(RECORD_DIALOGS);
#ifndef SUBGHZ_ADD_MANUALLY
// Transmitter
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdTransmitter);
subghz_view_transmitter_free(subghz->subghz_transmitter);
#endif
if(!alloc_for_tx_only) {
// Variable Item List
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdVariableItemList);
variable_item_list_free(subghz->variable_item_list);
#ifndef SUBGHZ_ADD_MANUALLY
// Frequency Analyzer
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdFrequencyAnalyzer);
subghz_frequency_analyzer_free(subghz->subghz_frequency_analyzer);
#endif
}
#ifndef SUBGHZ_ADD_MANUALLY
// Read RAW
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdReadRAW);
subghz_read_raw_free(subghz->subghz_read_raw);
#endif
if(!alloc_for_tx_only) {
// Submenu
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdMenu);
@@ -339,6 +368,7 @@ void subghz_free(SubGhz* subghz, bool alloc_for_tx_only) {
furi_record_close(RECORD_GUI);
subghz->gui = NULL;
#ifndef SUBGHZ_ADD_MANUALLY
// threshold rssi
subghz_threshold_rssi_free(subghz->threshold_rssi);
@@ -346,7 +376,9 @@ void subghz_free(SubGhz* subghz, bool alloc_for_tx_only) {
subghz_history_free(subghz->history);
}
#else
free(subghz->gen_info);
#endif
//TxRx
subghz_txrx_free(subghz->txrx);
@@ -362,18 +394,32 @@ void subghz_free(SubGhz* subghz, bool alloc_for_tx_only) {
furi_string_free(subghz->file_path);
furi_string_free(subghz->file_path_tmp);
#ifndef SUBGHZ_ADD_MANUALLY
// GPS
if(subghz->gps) {
subghz_gps_plugin_deinit(subghz->gps);
}
subghz_last_settings_free(subghz->last_settings);
#endif
// The rest
free(subghz);
}
#ifndef SUBGHZ_ADD_MANUALLY
int32_t subghz_app(char* p) {
enum SubmenuIndex start_scene_state = 0;
if(p) {
if(!strcmp(p, "AddManually")) {
start_scene_state = SubmenuIndexAddManually;
p = NULL;
} else if(!strcmp(p, "AddManuallyAdvanced")) {
start_scene_state = SubmenuIndexAddManuallyAdvanced;
p = NULL;
}
}
bool alloc_for_tx;
if(p && strlen(p)) {
alloc_for_tx = true;
@@ -426,6 +472,7 @@ int32_t subghz_app(char* p) {
view_dispatcher_attach_to_gui(
subghz->view_dispatcher, subghz->gui, ViewDispatcherTypeFullscreen);
furi_string_set(subghz->file_path, SUBGHZ_APP_FOLDER);
scene_manager_set_scene_state(subghz->scene_manager, SubGhzSceneStart, start_scene_state);
if(subghz_txrx_is_database_loaded(subghz->txrx)) {
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneStart);
} else {
@@ -453,3 +500,35 @@ int32_t subghz_app(char* p) {
return 0;
}
#else
int32_t subghz_add_manually(void* p) {
enum SubmenuIndex add_manually_scene_state = 0;
if(p) {
if(!strcmp(p, "AddManually")) {
add_manually_scene_state = SubmenuIndexAddManually;
} else if(!strcmp(p, "AddManuallyAdvanced")) {
add_manually_scene_state = SubmenuIndexAddManuallyAdvanced;
}
}
if(!add_manually_scene_state) return 0;
bool alloc_for_tx = false;
SubGhz* subghz = subghz_alloc(alloc_for_tx);
UNUSED(subghz_rpc_command_callback);
UNUSED(subghz_load_custom_presets);
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneSetType, add_manually_scene_state);
view_dispatcher_attach_to_gui(
subghz->view_dispatcher, subghz->gui, ViewDispatcherTypeFullscreen);
furi_string_set(subghz->file_path, SUBGHZ_APP_FOLDER);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSetType);
view_dispatcher_run(subghz->view_dispatcher);
subghz_free(subghz, alloc_for_tx);
return 0;
}
#endif
+4 -10
View File
@@ -5,7 +5,8 @@
#include <furi.h>
#define SUBGHZ_HISTORY_MAX 65535 // uint16_t index max, ram limit below
#define SUBGHZ_HISTORY_FREE_HEAP (10240 * (3 - MIN(rpc_get_sessions_count(instance->rpc), 2U)))
// #define SUBGHZ_HISTORY_FREE_HEAP (23624 * (1 - MIN(rpc_get_sessions_count(instance->rpc), 1U)))
#define SUBGHZ_HISTORY_FREE_HEAP (4 * 1024)
#define TAG "SubGhzHistory"
@@ -199,11 +200,11 @@ bool subghz_history_get_text_space_left(
furi_assert(instance);
if(!ignore_full) {
if(memmgr_get_free_heap() < SUBGHZ_HISTORY_FREE_HEAP) {
if(output != NULL) furi_string_printf(output, " Memory is FULL");
if(output != NULL) furi_string_set(output, "Memory is FULL");
return true;
}
if(instance->last_index_write == SUBGHZ_HISTORY_MAX) {
if(output != NULL) furi_string_printf(output, " History is FULL");
if(output != NULL) furi_string_set(output, "History is FULL");
return true;
}
}
@@ -312,13 +313,6 @@ bool subghz_history_add_to_history(
break;
}
furi_string_cat(instance->tmp_string, text);
} else if(!strcmp(furi_string_get_cstr(instance->tmp_string), "Star Line")) {
furi_string_set(instance->tmp_string, "SL ");
if(!flipper_format_read_string(item->flipper_string, "Manufacture", text)) {
FURI_LOG_E(TAG, "Missing Protocol");
break;
}
furi_string_cat(instance->tmp_string, text);
}
if(!flipper_format_rewind(item->flipper_string)) {
FURI_LOG_E(TAG, "Rewind error");
+9 -2
View File
@@ -183,14 +183,21 @@ bool subghz_key_load(SubGhz* subghz, const char* file_path, bool show_dialog) {
size_t preset_index =
subghz_setting_get_inx_preset_by_name(setting, furi_string_get_cstr(temp_str));
//Edit TX power, if necessary.
uint8_t* preset_data = subghz_setting_get_preset_data(setting, preset_index);
size_t preset_data_size = subghz_setting_get_preset_data_size(setting, preset_index);
subghz_txrx_set_tx_power(preset_data, preset_data_size, subghz->tx_power);
//Set the Updated Preset.
subghz_txrx_set_preset(
subghz->txrx,
furi_string_get_cstr(temp_str),
temp_data32,
temp_lat,
temp_lon,
subghz_setting_get_preset_data(setting, preset_index),
subghz_setting_get_preset_data_size(setting, preset_index));
preset_data,
preset_data_size);
//Load protocol
if(!flipper_format_read_string(fff_data_file, "Protocol", temp_str)) {
+1
View File
@@ -103,6 +103,7 @@ struct SubGhz {
bool fav_timeout;
FuriTimer* timer;
uint8_t tx_power;
void* rpc_ctx;
};
@@ -24,6 +24,7 @@
#define SUBGHZ_LAST_SETTING_FIELD_ENABLE_SOUND "Sound"
#define SUBGHZ_LAST_SETTING_FIELD_AUTOSAVE "Autosave"
#define SUBGHZ_LAST_SETTING_FIELD_HOPPING_THRESHOLD "HoppingThreshold"
#define SUBGHZ_LAST_SETTING_FIELD_TX_POWER "TXPower"
SubGhzLastSettings* subghz_last_settings_alloc(void) {
SubGhzLastSettings* instance = malloc(sizeof(SubGhzLastSettings));
@@ -162,6 +163,12 @@ void subghz_last_settings_load(SubGhzLastSettings* instance, size_t preset_count
1)) {
flipper_format_rewind(fff_data_file);
}
uint32_t tx_power = 0;
if(!flipper_format_read_uint32(
fff_data_file, SUBGHZ_LAST_SETTING_FIELD_TX_POWER, &tx_power, 1)) {
flipper_format_rewind(fff_data_file);
}
instance->tx_power = (uint8_t)(tx_power & 0xFF);
} while(0);
} else {
FURI_LOG_E(TAG, "Error open file %s", SUBGHZ_LAST_SETTINGS_PATH);
@@ -281,6 +288,10 @@ bool subghz_last_settings_save(SubGhzLastSettings* instance) {
1)) {
break;
}
uint32_t tx_power = instance->tx_power;
if(!flipper_format_write_uint32(file, SUBGHZ_LAST_SETTING_FIELD_TX_POWER, &tx_power, 1)) {
break;
}
saved = true;
} while(0);
@@ -31,6 +31,7 @@ typedef struct {
bool enable_sound;
bool autosave;
float hopping_threshold;
uint8_t tx_power;
} SubGhzLastSettings;
SubGhzLastSettings* subghz_last_settings_alloc(void);
+4 -2
View File
@@ -435,8 +435,10 @@ void subghz_view_receiver_draw(Canvas* canvas, SubGhzViewReceiverModel* model) {
canvas_draw_str(canvas, 44, 62, frequency_str);
#ifdef SUBGHZ_EXT_PRESET_NAME
if(model->history_item == 0 && model->mode == SubGhzViewReceiverModeLive) {
canvas_draw_str(
canvas, 44 + canvas_string_width(canvas, frequency_str) + 1, 62, "MHz");
if(*frequency_str) {
canvas_draw_str(
canvas, 44 + canvas_string_width(canvas, frequency_str) + 1, 62, "MHz");
}
const char* str = furi_string_get_cstr(model->preset_str);
const uint8_t vertical_offset = 7;
const uint8_t horizontal_offset = 3;
@@ -151,11 +151,12 @@ void subghz_read_raw_update_sin(SubGhzReadRAW* instance) {
}
static int8_t subghz_read_raw_tab_sin(uint8_t x) {
const uint8_t tab_sin[64] = {0, 3, 6, 9, 12, 16, 19, 22, 25, 28, 31, 34, 37,
40, 43, 46, 49, 51, 54, 57, 60, 63, 65, 68, 71, 73,
76, 78, 81, 83, 85, 88, 90, 92, 94, 96, 98, 100, 102,
104, 106, 107, 109, 111, 112, 113, 115, 116, 117, 118, 120, 121,
122, 122, 123, 124, 125, 125, 126, 126, 126, 127, 127, 127};
static const uint8_t tab_sin[64] = {0, 3, 6, 9, 12, 16, 19, 22, 25, 28, 31,
34, 37, 40, 43, 46, 49, 51, 54, 57, 60, 63,
65, 68, 71, 73, 76, 78, 81, 83, 85, 88, 90,
92, 94, 96, 98, 100, 102, 104, 106, 107, 109, 111,
112, 113, 115, 116, 117, 118, 120, 121, 122, 122, 123,
124, 125, 125, 126, 126, 126, 127, 127, 127};
int8_t r = tab_sin[((x & 0x40) ? -x - 1 : x) & 0x3f];
if(x & 0x80) return -r;
+1
View File
@@ -1,4 +1,5 @@
#include "canvas_i.h"
#include "canvas.h"
#include "icon_animation_i.h"
#include <furi.h>
+16
View File
@@ -87,6 +87,22 @@ extern const CanvasFontParameters canvas_font_params[FontTotalNumber];
/** Canvas anonymous structure */
typedef struct Canvas Canvas;
/** Get canvas buffer.
*
* @param canvas Canvas instance
*
* @return pointer to buffer
*/
uint8_t* canvas_get_buffer(Canvas* canvas);
/** Get canvas buffer size.
*
* @param canvas Canvas instance
*
* @return size of canvas in bytes
*/
size_t canvas_get_buffer_size(const Canvas* canvas);
/** Reset canvas drawing tools configuration
*
* @param canvas Canvas instance
-16
View File
@@ -61,22 +61,6 @@ Canvas* canvas_init(void);
*/
void canvas_free(Canvas* canvas);
/** Get canvas buffer.
*
* @param canvas Canvas instance
*
* @return pointer to buffer
*/
uint8_t* canvas_get_buffer(Canvas* canvas);
/** Get canvas buffer size.
*
* @param canvas Canvas instance
*
* @return size of canvas in bytes
*/
size_t canvas_get_buffer_size(const Canvas* canvas);
/** Set drawing region relative to real screen buffer
*
* @param canvas Canvas instance
@@ -20,44 +20,15 @@ static const NotificationSequence sequence_note_c = {
NULL,
};
#define CONTRAST_COUNT 17
#define CONTRAST_COUNT 29
const char* const contrast_text[CONTRAST_COUNT] = {
"-8",
"-7",
"-6",
"-5",
"-4",
"-3",
"-2",
"-1",
"0",
"+1",
"+2",
"+3",
"+4",
"+5",
"+6",
"+7",
"+8",
"-10", "-9", "-8", "-7", "-6", "-5", "-4", "-3", "-2", "-1",
"0", "+1", "+2", "+3", "+4", "+5", "+6", "+7", "+8", "+9",
"+10", "+11", "+12", "+13", "+14", "+15", "+16", "+17", "+18",
};
const int32_t contrast_value[CONTRAST_COUNT] = {
-8,
-7,
-6,
-5,
-4,
-3,
-2,
-1,
0,
1,
2,
3,
4,
5,
6,
7,
8,
-10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4,
5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
};
#define BACKLIGHT_COUNT 21
@@ -156,14 +156,26 @@ bool storage_settings_scene_benchmark_on_event(void* context, SceneManagerEvent
if(event.type == SceneManagerEventTypeCustom) {
switch(event.event) {
case DialogExResultCenter:
consumed = scene_manager_search_and_switch_to_previous_scene(
app->scene_manager, StorageSettingsStart);
if(app->from_favorites) {
scene_manager_stop(app->scene_manager);
view_dispatcher_stop(app->view_dispatcher);
return true;
} else {
consumed = scene_manager_search_and_switch_to_previous_scene(
app->scene_manager, StorageSettingsStart);
}
break;
}
} else if(event.type == SceneManagerEventTypeBack) {
if(sd_status == FSE_OK) {
consumed = scene_manager_search_and_switch_to_previous_scene(
app->scene_manager, StorageSettingsStart);
if(app->from_favorites) {
scene_manager_stop(app->scene_manager);
view_dispatcher_stop(app->view_dispatcher);
return true;
} else {
consumed = scene_manager_search_and_switch_to_previous_scene(
app->scene_manager, StorageSettingsStart);
}
} else {
consumed = true;
}
@@ -48,7 +48,13 @@ bool storage_settings_scene_benchmark_confirm_on_event(void* context, SceneManag
switch(event.event) {
case DialogExResultLeft:
case DialogExResultCenter:
consumed = scene_manager_previous_scene(app->scene_manager);
if(app->from_favorites) {
scene_manager_stop(app->scene_manager);
view_dispatcher_stop(app->view_dispatcher);
return true;
} else {
consumed = scene_manager_previous_scene(app->scene_manager);
}
break;
case DialogExResultRight:
scene_manager_next_scene(app->scene_manager, StorageSettingsBenchmark);
@@ -44,7 +44,13 @@ bool storage_settings_scene_factory_reset_on_event(void* context, SceneManagerEv
switch(event.event) {
case DialogExResultLeft:
scene_manager_set_scene_state(app->scene_manager, StorageSettingsFactoryReset, 0);
consumed = scene_manager_previous_scene(app->scene_manager);
if(app->from_favorites) {
scene_manager_stop(app->scene_manager);
view_dispatcher_stop(app->view_dispatcher);
return true;
} else {
consumed = scene_manager_previous_scene(app->scene_manager);
}
break;
case DialogExResultRight:
counter++;
@@ -41,7 +41,13 @@ bool storage_settings_scene_format_confirm_on_event(void* context, SceneManagerE
switch(event.event) {
case DialogExResultLeft:
case DialogExResultCenter:
consumed = scene_manager_previous_scene(app->scene_manager);
if(app->from_favorites) {
scene_manager_stop(app->scene_manager);
view_dispatcher_stop(app->view_dispatcher);
return true;
} else {
consumed = scene_manager_previous_scene(app->scene_manager);
}
break;
case DialogExResultRight:
scene_manager_set_scene_state(app->scene_manager, StorageSettingsFormatting, false);
@@ -75,8 +75,14 @@ bool storage_settings_scene_formatting_on_event(void* context, SceneManagerEvent
if(event.type == SceneManagerEventTypeCustom) {
switch(event.event) {
case DialogExResultLeft:
consumed = scene_manager_search_and_switch_to_previous_scene(
app->scene_manager, StorageSettingsStart);
if(app->from_favorites) {
scene_manager_stop(app->scene_manager);
view_dispatcher_stop(app->view_dispatcher);
return true;
} else {
consumed = scene_manager_search_and_switch_to_previous_scene(
app->scene_manager, StorageSettingsStart);
}
break;
}
} else if(event.type == SceneManagerEventTypeBack) {
@@ -56,7 +56,13 @@ bool storage_settings_scene_internal_info_on_event(void* context, SceneManagerEv
if(event.type == SceneManagerEventTypeCustom) {
switch(event.event) {
case DialogExResultLeft:
consumed = scene_manager_previous_scene(app->scene_manager);
if(app->from_favorites) {
scene_manager_stop(app->scene_manager);
view_dispatcher_stop(app->view_dispatcher);
return true;
} else {
consumed = scene_manager_previous_scene(app->scene_manager);
}
break;
}
}
@@ -82,7 +82,13 @@ bool storage_settings_scene_sd_info_on_event(void* context, SceneManagerEvent ev
switch(event.event) {
case DialogExResultLeft:
case DialogExResultCenter:
consumed = scene_manager_previous_scene(app->scene_manager);
if(app->from_favorites) {
scene_manager_stop(app->scene_manager);
view_dispatcher_stop(app->view_dispatcher);
return true;
} else {
consumed = scene_manager_previous_scene(app->scene_manager);
}
break;
case DialogExResultRight:
scene_manager_next_scene(app->scene_manager, StorageSettingsUnmounted);
@@ -46,7 +46,13 @@ bool storage_settings_scene_unmount_confirm_on_event(void* context, SceneManager
switch(event.event) {
case DialogExResultCenter:
case DialogExResultLeft:
consumed = scene_manager_previous_scene(app->scene_manager);
if(app->from_favorites) {
scene_manager_stop(app->scene_manager);
view_dispatcher_stop(app->view_dispatcher);
return true;
} else {
consumed = scene_manager_previous_scene(app->scene_manager);
}
break;
case DialogExResultRight:
scene_manager_next_scene(app->scene_manager, StorageSettingsUnmounted);
@@ -57,8 +57,14 @@ bool storage_settings_scene_unmounted_on_event(void* context, SceneManagerEvent
if(event.type == SceneManagerEventTypeCustom) {
switch(event.event) {
case DialogExResultCenter:
consumed = scene_manager_search_and_switch_to_previous_scene(
app->scene_manager, StorageSettingsStart);
if(app->from_favorites) {
scene_manager_stop(app->scene_manager);
view_dispatcher_stop(app->view_dispatcher);
return true;
} else {
consumed = scene_manager_search_and_switch_to_previous_scene(
app->scene_manager, StorageSettingsStart);
}
break;
}
} else if(event.type == SceneManagerEventTypeBack) {
@@ -99,7 +99,10 @@ int32_t storage_settings_app(void* p) {
StorageSettings* app = storage_settings_alloc();
if(!submenu_settings_helpers_app_start(app->settings_helper, p)) {
app->from_favorites = false;
scene_manager_next_scene(app->scene_manager, StorageSettingsStart);
} else {
app->from_favorites = true;
}
view_dispatcher_run(app->view_dispatcher);
@@ -44,6 +44,8 @@ typedef struct {
// helpers
SubmenuSettingsHelperDescriptor* helper_descriptor;
SubmenuSettingsHelper* settings_helper;
bool from_favorites;
} StorageSettings;
typedef enum {
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 B

+363 -99
View File
@@ -1,19 +1,60 @@
#include "hid_ptt.h"
#include "hid_ptt_menu.h"
#include <gui/elements.h>
#include <locale/locale.h>
#include <notification/notification_messages.h>
#include <gui/modules/widget.h>
#include <furi_hal_power.h>
#include <furi_hal_rtc.h>
#include "../hid.h"
#include "../views.h"
#include "hid_icons.h"
#define TAG "HidPushToTalk"
#define TAG "HidPushToTalk"
#define HID_PTT_LEFT_HOLD_ANIM_STEP_MS 100U
#define HID_PTT_LEFT_HOLD_ANIM_STEPS 5U
// Exact home status-bar Bluetooth icon pixels (from assets/icons/StatusBar).
// Bitmap format for canvas_draw_bitmap(): row-major, 1-bit.
// Full compressed frame data including leading 0x00 heatshrink marker,
// copied verbatim from build/f7-firmware-D/assets/compiled/assets_icons.c
static const uint8_t hid_ptt_bluetooth_connected_16x8_bits[] = {
0x00,
0x04,
0x00,
0x0d,
0x00,
0x16,
0x60,
0x4c,
0x97,
0x4c,
0x97,
0x16,
0x60,
0x0d,
0x00,
0x04,
0x00,
};
static const uint8_t hid_ptt_bluetooth_idle_5x8_bits[] = {
0x00,
0x04,
0x0d,
0x16,
0x0c,
0x0c,
0x16,
0x0d,
0x04,
};
struct HidPushToTalk {
View* view;
Hid* hid;
Widget* help;
FuriTimer* left_hold_timer;
};
typedef void (*PushToTalkActionCallback)(HidPushToTalk* hid_ptt);
@@ -32,6 +73,7 @@ typedef struct {
size_t osIndex;
size_t appIndex;
size_t window_position;
uint8_t left_hold_progress;
PushToTalkActionCallback callback_trigger_mute;
PushToTalkActionCallback callback_trigger_camera;
PushToTalkActionCallback callback_trigger_hand;
@@ -59,6 +101,66 @@ enum HidPushToTalkAppIndex {
HidPushToTalkAppIndexSize,
};
static bool hid_ptt_is_zoom_app(size_t app_index) {
return (app_index == HidPushToTalkAppIndexZoom) ||
(app_index == HidPushToTalkAppIndexZoomGlobal);
}
static void hid_ptt_left_hold_timer_callback(void* context) {
furi_assert(context);
HidPushToTalk* hid_ptt = context;
with_view_model(
hid_ptt->view,
HidPushToTalkModel * model,
{
if(model->left_pressed && hid_ptt_is_zoom_app(model->appIndex)) {
if(model->left_hold_progress < HID_PTT_LEFT_HOLD_ANIM_STEPS) {
model->left_hold_progress++;
}
} else {
model->left_hold_progress = 0;
furi_timer_stop(hid_ptt->left_hold_timer);
}
},
true);
}
static void hid_ptt_draw_zoom_enter_hint(
Canvas* canvas,
uint8_t x,
uint8_t y,
uint8_t progress,
bool pressed) {
const uint8_t width = 18;
const uint8_t height = 10;
const uint8_t inner_x = x + 1;
const uint8_t inner_y = y + 1;
const uint8_t inner_w = width - 2;
const uint8_t inner_h = height - 1;
if(progress > 0) {
const uint8_t fill_w = (inner_w * progress) / HID_PTT_LEFT_HOLD_ANIM_STEPS;
if(fill_w > 0) {
canvas_draw_box(canvas, inner_x, inner_y, fill_w, inner_h - 1);
}
}
for(uint8_t dot_x = x; dot_x < x + width; dot_x += 2) {
canvas_draw_dot(canvas, dot_x, y + height - 1);
}
for(uint8_t dot_y = y + 1; dot_y < y + height; dot_y += 2) {
canvas_draw_dot(canvas, x, dot_y);
canvas_draw_dot(canvas, x + width - 1, dot_y);
}
UNUSED(pressed);
if(progress > 0) {
canvas_set_color(canvas, ColorWhite);
}
canvas_draw_icon(canvas, x + 4, y + 1, &I_Enter_11x7);
canvas_set_color(canvas, ColorBlack);
}
// meet, zoom
static void hid_ptt_start_ptt_meet_zoom(HidPushToTalk* hid_ptt) {
hid_hal_keyboard_press(hid_ptt->hid, HID_KEYBOARD_SPACEBAR);
@@ -128,9 +230,9 @@ static void hid_ptt_trigger_hand_zoom(HidPushToTalk* hid_ptt) {
// zoom global macos
static void hid_ptt_trigger_mute_macos_zoom_global(HidPushToTalk* hid_ptt) {
hid_hal_keyboard_press(
hid_ptt->hid, KEY_MOD_LEFT_GUI | KEY_MOD_RIGHT_ALT | KEY_MOD_LEFT_SHIFT | HID_KEYBOARD_M);
hid_ptt->hid, KEY_MOD_LEFT_GUI | KEY_MOD_RIGHT_ALT | KEY_MOD_LEFT_SHIFT | HID_KEYBOARD_N);
hid_hal_keyboard_release(
hid_ptt->hid, KEY_MOD_LEFT_GUI | KEY_MOD_RIGHT_ALT | KEY_MOD_LEFT_SHIFT | HID_KEYBOARD_M);
hid_ptt->hid, KEY_MOD_LEFT_GUI | KEY_MOD_RIGHT_ALT | KEY_MOD_LEFT_SHIFT | HID_KEYBOARD_N);
}
static void hid_ptt_trigger_camera_macos_zoom_global(HidPushToTalk* hid_ptt) {
@@ -387,6 +489,97 @@ static void hid_ptt_trigger_mute_linux_gather(HidPushToTalk* hid_ptt) {
hid_ptt->hid, KEY_MOD_LEFT_CTRL | KEY_MOD_LEFT_SHIFT | HID_KEYBOARD_A);
}
static void hid_ptt_populate_help(HidPushToTalk* hid_ptt, uint32_t appIndex) {
widget_reset(hid_ptt->help);
char* app_specific_help = "";
switch(appIndex) {
case HidPushToTalkAppIndexGoogleMeet:
app_specific_help =
"Google Meet:\n"
"This feature is off by default in your audio settings "
"and may not work for Windows users who use their screen "
"reader. In this situation, the spacebar performs a different action.\n\n";
break;
case HidPushToTalkAppIndexGoogleMeetGlobal:
app_specific_help = "Google Meet (Global):\n"
"1. Install \"Google Meet - Global Shortcuts\" extension.\n"
"2. Open chrome://extensions/shortcuts.\n"
"3. Set 'Toggle microphone' to Cmd+Ctrl+7 and enable Global.\n"
"4. Set 'Toggle camera' to Cmd+Ctrl+8 and enable Global.\n"
"5. Set 'Raise hand' to Cmd+Ctrl+9 and enable Global.\n\n";
break;
case HidPushToTalkAppIndexDiscord:
app_specific_help = "Discord:\n"
"1. Under App Settings, click Voice & Video. Under Input Mode, "
"check the box next to Push to Talk.\n"
"2. Scroll down to SHORTCUT, click Record Keybinder.\n"
"3. Press PTT in the app to bind it."
"4. Go to Keybinds and assign mute button.\n\n";
break;
case HidPushToTalkAppIndexTeamSpeak:
app_specific_help = "TeamSpeak:\n"
"To make keys working bind them in TeamSpeak settings.\n\n";
break;
case HidPushToTalkAppIndexTeams:
app_specific_help =
"Teams:\n"
"Go to Settings > Privacy. Make sure Keyboard shortcut to unmute is toggled on.\n\n";
break;
case HidPushToTalkAppIndexZoomGlobal:
app_specific_help = "Zoom (Global):\n"
"1. Go to Settings > Keyboard Shortcuts.\n"
"2. Find the 'Mute/Unmute' shortcut and click 'Edit'.\n"
"3. Press the Mute button in the app to bind it.\n"
"4. Check global checkbox.\n"
"5. Repeat for video and hand shortcuts.\n"
"6. Long-press < to send Enter key.\n\n";
break;
case HidPushToTalkAppIndexZoom:
app_specific_help = "Zoom:\n"
"1. Go to Settings > Keyboard Shortcuts.\n"
"2. Find the 'Mute/Unmute' shortcut and click 'Edit'.\n"
"3. Press the Mute button in the app to bind it.\n"
"4. Repeat for video and hand shortcuts.\n"
"5. Long-press < to send Enter key.\n\n";
break;
}
char* left_button_help = "";
if(appIndex == HidPushToTalkAppIndexZoom || appIndex == HidPushToTalkAppIndexZoomGlobal) {
left_button_help = "Long-press < sends Enter.\n";
}
FuriString* msg = furi_string_alloc();
furi_string_cat_printf(
msg,
"%sGeneral:\n"
"To operate properly flipper microphone "
"status must be in sync with your computer.\n"
"Hold > to change mic status.\n"
"%s"
"Long-press OK in menu to open this help.\n"
"Press BACK to switch mic on/off.\n"
"Hold 'o' for PTT mode (mic will be off once you release 'o')\n"
"Hold BACK to exit.",
app_specific_help,
left_button_help);
widget_add_text_scroll_element(hid_ptt->help, 0, 0, 128, 64, furi_string_get_cstr(msg));
furi_string_free(msg);
}
static void hid_ptt_menu_help_callback(
void* context,
uint32_t osIndex,
FuriString* osLabel,
uint32_t appIndex,
FuriString* appLabel) {
UNUSED(osIndex);
UNUSED(osLabel);
UNUSED(appLabel);
furi_assert(context);
HidPushToTalk* hid_ptt = context;
hid_ptt_populate_help(hid_ptt, appIndex);
view_dispatcher_switch_to_view(hid_ptt->hid->view_dispatcher, HidViewPushToTalkHelp);
}
static void hid_ptt_menu_callback(
void* context,
uint32_t osIndex,
@@ -593,68 +786,9 @@ static void hid_ptt_menu_callback(
break;
}
}
char* app_specific_help = "";
switch(appIndex) {
case HidPushToTalkAppIndexGoogleMeet:
app_specific_help =
"Google Meet:\n"
"This feature is off by default in your audio settings "
"and may not work for Windows users who use their screen "
"reader. In this situation, the spacebar performs a different action.\n\n";
break;
case HidPushToTalkAppIndexGoogleMeetGlobal:
app_specific_help = "Google Meet (Global):\n"
"1. Install \"Google Meet - Global Shortcuts\" extension.\n"
"2. Open chrome://extensions/shortcuts.\n"
"3. Set 'Toggle microphone' to Cmd+Ctrl+7 and enable Global.\n"
"4. Set 'Toggle camera' to Cmd+Ctrl+8 and enable Global.\n"
"5. Set 'Raise hand' to Cmd+Ctrl+9 and enable Global.\n\n";
break;
case HidPushToTalkAppIndexDiscord:
app_specific_help =
"Discord:\n"
"1. Under App Settings, click Voice & Video. Under Input Mode, "
"check the box next to Push to Talk.\n"
"2. Scroll down to SHORTCUT, click Record Keybinder.\n"
"3. Press PTT in the app to bind it."
"4. Go to Keybinds and assign mute button.\n\n";
break;
case HidPushToTalkAppIndexTeamSpeak:
app_specific_help = "TeamSpeak:\n"
"To make keys working bind them in TeamSpeak settings.\n\n";
break;
case HidPushToTalkAppIndexTeams:
app_specific_help =
"Teams:\n"
"Go to Settings > Privacy. Make sure Keyboard shortcut to unmute is toggled on.\n\n";
break;
case HidPushToTalkAppIndexZoomGlobal:
app_specific_help = "Zoom (Global):\n"
"1. Go to Settings > Keyboard Shortcuts.\n"
"2. Find the 'Mute/Unmute' shortcut and click 'Edit'.\n"
"3. Press the Mute button in the app to bind it.\n"
"4. Check global checkbox.\n"
"5. Repeat for video and hand shortcuts.\n\n";
}
FuriString* msg = furi_string_alloc();
furi_string_cat_printf(
msg,
"%sGeneral:\n"
"To operate properly flipper microphone "
"status must be in sync with your computer.\n"
"Hold > to change mic status.\n"
"Hold < to open this help.\n"
"Press BACK to switch mic on/off.\n"
"Hold 'o' for PTT mode (mic will be off once you release 'o')\n"
"Hold BACK to exit.",
app_specific_help);
widget_add_text_scroll_element(
hid_ptt->help, 0, 0, 128, 64, furi_string_get_cstr(msg));
furi_string_free(msg);
},
true);
hid_ptt_populate_help(hid_ptt, appIndex);
view_dispatcher_switch_to_view(hid_ptt->hid->view_dispatcher, HidViewPushToTalk);
}
@@ -673,43 +807,148 @@ static void hid_ptt_draw_text_centered(Canvas* canvas, uint8_t y, FuriString* st
furi_string_free(disp_str);
}
static void hid_ptt_draw_app_label(Canvas* canvas, uint8_t first_line_y, FuriString* app) {
FuriString* first_line = furi_string_alloc_set(app);
if(canvas_string_width(canvas, furi_string_get_cstr(first_line)) <= canvas_width(canvas)) {
hid_ptt_draw_text_centered(canvas, first_line_y, first_line);
furi_string_free(first_line);
return;
}
const char* app_cstr = furi_string_get_cstr(app);
const char* split = strrchr(app_cstr, ' ');
if(!split) {
hid_ptt_draw_text_centered(canvas, first_line_y, first_line);
furi_string_free(first_line);
return;
}
FuriString* second_line = furi_string_alloc();
furi_string_set_strn(first_line, app_cstr, split - app_cstr);
furi_string_set_str(second_line, split + 1);
elements_string_fit_width(canvas, first_line, canvas_width(canvas));
elements_string_fit_width(canvas, second_line, canvas_width(canvas));
hid_ptt_draw_text_centered(canvas, first_line_y, first_line);
hid_ptt_draw_text_centered(canvas, first_line_y + 10, second_line);
furi_string_free(second_line);
furi_string_free(first_line);
}
static void hid_ptt_draw_status_bar(Canvas* canvas, bool show_bt, bool connected) {
char time_str[16];
DateTime dt;
furi_hal_rtc_get_datetime(&dt);
uint8_t hour = dt.hour;
if(locale_get_time_format() == LocaleTimeFormat12h) {
if(hour > 12) {
hour -= 12;
}
if(hour == 0) {
hour = 12;
}
}
snprintf(time_str, sizeof(time_str), "%02u:%02u", hour, dt.minute);
uint8_t battery = furi_hal_power_get_pct();
if(battery > 100) {
battery = 100;
}
canvas_set_color(canvas, ColorWhite);
canvas_draw_box(canvas, 0, 0, 64, 13);
canvas_set_color(canvas, ColorBlack);
canvas_draw_rframe(canvas, 0, 0, 64, 13, 1);
canvas_draw_line(canvas, 1, 11, 62, 11);
if(show_bt) {
if(connected) {
canvas_draw_bitmap(canvas, 2, 2, 16, 8, hid_ptt_bluetooth_connected_16x8_bits);
} else {
canvas_draw_bitmap(canvas, 2, 2, 5, 8, hid_ptt_bluetooth_idle_5x8_bits);
}
}
const uint8_t battery_x = 48;
const uint8_t battery_y = 3;
const uint8_t battery_w = 13;
const uint8_t battery_h = 6;
canvas_draw_frame(canvas, battery_x, battery_y, battery_w, battery_h);
canvas_draw_box(canvas, battery_x + battery_w, battery_y + 2, 1, 2);
canvas_draw_box(canvas, battery_x + 1, battery_y + 1, ((battery_w - 2) * battery) / 100, 4);
canvas_set_font(canvas, FontPrimary);
canvas_draw_str_aligned(canvas, 33, 10, AlignCenter, AlignBottom, time_str);
}
static void hid_ptt_draw_callback(Canvas* canvas, void* context) {
furi_assert(context);
HidPushToTalkModel* model = context;
// Header
canvas_set_font(canvas, FontPrimary);
#ifdef HID_TRANSPORT_BLE
if(model->connected) {
canvas_draw_icon(canvas, 0, 0, &I_Ble_connected_15x15);
} else {
canvas_draw_icon(canvas, 0, 0, &I_Ble_disconnected_15x15);
const bool app_label_needs_two_lines =
canvas_string_width(canvas, furi_string_get_cstr(model->app)) > canvas_width(canvas);
const uint8_t top_offset = 13;
const uint8_t status_bar_bottom_y = top_offset;
// For Zoom/Zoom Global, keep helper banner higher to show Enter key hint space
// For other apps, move it down to close the gap
uint8_t helper_top_y = 102;
if((model->appIndex == HidPushToTalkAppIndexZoom ||
model->appIndex == HidPushToTalkAppIndexZoomGlobal) &&
!app_label_needs_two_lines) {
helper_top_y = 92;
}
#endif
// OS and App labels
canvas_set_font(canvas, FontSecondary);
hid_ptt_draw_text_centered(canvas, 73, model->app);
hid_ptt_draw_text_centered(canvas, 84, model->os);
// Help label
canvas_draw_icon(canvas, 0, 88, &I_Help_top_64x17);
canvas_draw_line(canvas, 4, 105, 4, 114);
canvas_draw_line(canvas, 63, 105, 63, 114);
canvas_draw_icon(canvas, 7, 107, &I_Hold_15x5);
canvas_draw_icon(canvas, 24, 105, &I_BtnLeft_9x9);
canvas_draw_icon(canvas, 34, 108, &I_for_help_27x5);
canvas_draw_icon(canvas, 0, 115, &I_Help_exit_64x9);
canvas_draw_icon(canvas, 24, 115, &I_BtnBackV_9x9);
const uint8_t x_1 = 0;
const uint8_t x_2 = x_1 + 19 + 4;
const uint8_t x_3 = x_1 + 19 * 2 + 8;
const uint8_t y_1 = 3;
const uint8_t y_1 = top_offset + 3;
const uint8_t y_2 = y_1 + 19;
const uint8_t y_3 = y_2 + 19;
const uint8_t controls_bottom_y = y_3 + 18;
const uint8_t labels_center_y = (controls_bottom_y + helper_top_y) / 2;
const uint8_t app_label_y = app_label_needs_two_lines ? (controls_bottom_y + 10) :
(labels_center_y - 1);
const uint8_t os_label_y = app_label_needs_two_lines ? (app_label_y + 20) : (app_label_y + 11);
// Header
canvas_set_font(canvas, FontPrimary);
#ifdef HID_TRANSPORT_BLE
hid_ptt_draw_status_bar(canvas, true, model->connected);
#else
hid_ptt_draw_status_bar(canvas, false, false);
#endif
// OS and App labels
canvas_set_font(canvas, FontSecondary);
if(app_label_needs_two_lines) {
hid_ptt_draw_app_label(canvas, app_label_y, model->app);
} else {
hid_ptt_draw_text_centered(canvas, app_label_y, model->app);
}
hid_ptt_draw_text_centered(canvas, os_label_y, model->os);
// Help label
canvas_draw_icon(canvas, 0, helper_top_y, &I_Help_top_64x17);
canvas_draw_line(canvas, 4, 109, 4, 118);
canvas_draw_line(canvas, 63, 109, 63, 118);
canvas_draw_icon(canvas, 0, 119, &I_Help_exit_64x9);
canvas_draw_icon(canvas, 24, 119, &I_BtnBackV_9x9);
// For Zoom/Zoom Global: show "Hold < for Enter" hint
if(model->appIndex == HidPushToTalkAppIndexZoom ||
model->appIndex == HidPushToTalkAppIndexZoomGlobal) {
canvas_draw_icon(canvas, 7, 111, &I_Hold_15x5);
canvas_draw_icon(canvas, 24, 109, &I_BtnLeft_9x9);
canvas_draw_icon(canvas, 35, 112, &I_for_11x5);
canvas_draw_icon(canvas, 48, 112, &I_Return_10x7);
}
// Up
canvas_draw_icon(canvas, x_2, y_1, &I_Button_18x18);
if(model->up_pressed) {
@@ -741,6 +980,11 @@ static void hid_ptt_draw_callback(Canvas* canvas, void* context) {
}
canvas_set_color(canvas, ColorBlack);
if(hid_ptt_is_zoom_app(model->appIndex)) {
hid_ptt_draw_zoom_enter_hint(
canvas, x_1, y_2 + 18, model->left_hold_progress, model->left_pressed);
}
// Right / Camera
canvas_draw_icon(canvas, x_3, y_2, &I_Button_18x18);
if(model->right_pressed) {
@@ -756,23 +1000,24 @@ static void hid_ptt_draw_callback(Canvas* canvas, void* context) {
// Back / Mic
const uint8_t x_mic = x_3;
canvas_draw_icon(canvas, x_mic, 0, &I_RoundButtonUnpressed_16x16);
const uint8_t y_mic = status_bar_bottom_y + ((y_2 - status_bar_bottom_y - 16) / 2);
canvas_draw_icon(canvas, x_mic, y_mic, &I_RoundButtonUnpressed_16x16);
if(!(!model->muted || (model->ptt_pressed))) {
// show muted
if(model->mic_pressed) {
// canvas_draw_icon(canvas, x_mic + 1, 0, &I_MicrophonePressedCrossed_15x15);
canvas_draw_icon(canvas, x_mic, 0, &I_MicrophonePressedCrossedBtn_16x16);
canvas_draw_icon(canvas, x_mic, y_mic, &I_MicrophonePressedCrossedBtn_16x16);
} else {
canvas_draw_icon(canvas, x_mic, 0, &I_MicrophoneCrossed_16x16);
canvas_draw_icon(canvas, x_mic, y_mic, &I_MicrophoneCrossed_16x16);
}
} else {
// show unmuted
if(model->mic_pressed) {
// canvas_draw_icon(canvas, x_mic + 1, 0, &I_MicrophonePressed_15x15);
canvas_draw_icon(canvas, x_mic, 0, &I_MicrophonePressedBtn_16x16);
canvas_draw_icon(canvas, x_mic, y_mic, &I_MicrophonePressedBtn_16x16);
} else {
canvas_draw_icon(canvas, x_mic + 5, 2, &I_Mic_7x11);
canvas_draw_icon(canvas, x_mic + 5, y_mic + 2, &I_Mic_7x11);
}
}
@@ -813,6 +1058,12 @@ static void hid_ptt_process(HidPushToTalk* hid_ptt, InputEvent* event) {
hid_hal_consumer_key_press(hid_ptt->hid, HID_CONSUMER_VOLUME_DECREMENT);
} else if(event->key == InputKeyLeft) {
model->left_pressed = true;
if(hid_ptt_is_zoom_app(model->appIndex)) {
model->left_hold_progress = 0;
furi_timer_start(
hid_ptt->left_hold_timer,
furi_ms_to_ticks(HID_PTT_LEFT_HOLD_ANIM_STEP_MS));
}
} else if(event->key == InputKeyRight) {
model->right_pressed = true;
} else if(event->key == InputKeyOk) {
@@ -836,6 +1087,8 @@ static void hid_ptt_process(HidPushToTalk* hid_ptt, InputEvent* event) {
}
} else if(event->key == InputKeyLeft) {
model->left_pressed = false;
model->left_hold_progress = 0;
furi_timer_stop(hid_ptt->left_hold_timer);
} else if(event->key == InputKeyRight) {
model->right_pressed = false;
@@ -866,10 +1119,14 @@ static void hid_ptt_process(HidPushToTalk* hid_ptt, InputEvent* event) {
model->muted = !model->muted;
notification_message(hid_ptt->hid->notifications, &sequence_single_vibro);
} else if(event->type == InputTypeLong && event->key == InputKeyLeft) {
notification_message(hid_ptt->hid->notifications, &sequence_single_vibro);
model->left_pressed = false;
view_dispatcher_switch_to_view(
hid_ptt->hid->view_dispatcher, HidViewPushToTalkHelp);
if(model->appIndex == HidPushToTalkAppIndexZoom ||
model->appIndex == HidPushToTalkAppIndexZoomGlobal) {
model->left_hold_progress = HID_PTT_LEFT_HOLD_ANIM_STEPS;
furi_timer_stop(hid_ptt->left_hold_timer);
hid_hal_keyboard_press(hid_ptt->hid, HID_KEYBOARD_RETURN);
hid_hal_keyboard_release(hid_ptt->hid, HID_KEYBOARD_RETURN);
notification_message(hid_ptt->hid->notifications, &sequence_single_vibro);
}
}
//LED
if(!model->muted || (model->ptt_pressed)) {
@@ -886,6 +1143,7 @@ static bool hid_ptt_input_callback(InputEvent* event, void* context) {
HidPushToTalk* hid_ptt = context;
bool consumed = false;
if(event->type == InputTypeLong && event->key == InputKeyBack) {
furi_timer_stop(hid_ptt->left_hold_timer);
hid_hal_keyboard_release_all(hid_ptt->hid);
notification_message(hid_ptt->hid->notifications, &sequence_double_vibro);
widget_reset(hid_ptt->help);
@@ -901,14 +1159,16 @@ View* hid_ptt_get_view(HidPushToTalk* hid_ptt) {
return hid_ptt->view;
}
static uint32_t hid_ptt_view(void* context) {
static uint32_t hid_ptt_menu_view(void* context) {
UNUSED(context);
return HidViewPushToTalk;
return HidViewPushToTalkMenu;
}
HidPushToTalk* hid_ptt_alloc(Hid* hid) {
HidPushToTalk* hid_ptt = malloc(sizeof(HidPushToTalk));
hid_ptt->hid = hid;
hid_ptt->left_hold_timer =
furi_timer_alloc(hid_ptt_left_hold_timer_callback, FuriTimerTypePeriodic, hid_ptt);
hid_ptt->view = view_alloc();
view_set_context(hid_ptt->view, hid_ptt);
view_allocate_model(hid_ptt->view, ViewModelTypeLocking, sizeof(HidPushToTalkModel));
@@ -921,6 +1181,7 @@ HidPushToTalk* hid_ptt_alloc(Hid* hid) {
HidPushToTalkModel * model,
{
model->muted = true; // assume we're muted
model->left_hold_progress = 0;
model->os = furi_string_alloc();
model->app = furi_string_alloc();
},
@@ -1134,14 +1395,17 @@ HidPushToTalk* hid_ptt_alloc(Hid* hid) {
hid_ptt);
hid_ptt->help = widget_alloc();
view_set_previous_callback(widget_get_view(hid_ptt->help), hid_ptt_view);
view_set_previous_callback(widget_get_view(hid_ptt->help), hid_ptt_menu_view);
view_dispatcher_add_view(
hid->view_dispatcher, HidViewPushToTalkHelp, widget_get_view(hid_ptt->help));
ptt_menu_set_long_ok_callback(hid->hid_ptt_menu, hid_ptt_menu_help_callback, hid_ptt);
return hid_ptt;
}
void hid_ptt_free(HidPushToTalk* hid_ptt) {
furi_assert(hid_ptt);
furi_timer_stop(hid_ptt->left_hold_timer);
furi_timer_free(hid_ptt->left_hold_timer);
notification_message(hid_ptt->hid->notifications, &sequence_reset_red);
with_view_model(
hid_ptt->view,
@@ -5,11 +5,18 @@
#include "../hid.h"
#include "../views.h"
#define TAG "HidPushToTalkMenu"
#include "hid_icons.h"
#define TAG "HidPushToTalkMenu"
#define PTT_MENU_HELP_HINT_DELAY_MS 5000U
#define PTT_MENU_HINT_TIMER_PERIOD_MS 150U
struct HidPushToTalkMenu {
View* view;
Hid* hid;
PushToTalkMenuLongOkCallback long_ok_callback;
void* long_ok_callback_context;
FuriTimer* hint_timer;
};
typedef struct {
@@ -60,8 +67,69 @@ typedef struct {
size_t window_position;
PushToTalkMenuList* lists;
int lists_count;
uint32_t last_interaction_tick;
size_t hint_list_position;
size_t hint_item_position;
bool hint_visible;
uint16_t hint_scroll_tick;
} HidPushToTalkMenuModel;
static void hid_ptt_menu_mark_interaction(HidPushToTalkMenu* hid_ptt_menu) {
with_view_model(
hid_ptt_menu->view,
HidPushToTalkMenuModel * model,
{
model->last_interaction_tick = furi_get_tick();
model->hint_list_position = model->list_position;
model->hint_item_position = model->position;
model->hint_visible = false;
model->hint_scroll_tick = 0;
},
true);
}
static void hid_ptt_menu_hint_timer_callback(void* context) {
furi_assert(context);
HidPushToTalkMenu* hid_ptt_menu = context;
with_view_model(
hid_ptt_menu->view,
HidPushToTalkMenuModel * model,
{
const uint32_t now = furi_get_tick();
const bool selection_changed = (model->list_position != model->hint_list_position) ||
(model->position != model->hint_item_position);
if(selection_changed) {
model->hint_list_position = model->list_position;
model->hint_item_position = model->position;
model->last_interaction_tick = now;
model->hint_visible = false;
model->hint_scroll_tick = 0;
} else if(!model->hint_visible) {
if((now - model->last_interaction_tick) >= PTT_MENU_HELP_HINT_DELAY_MS) {
model->hint_visible = true;
model->hint_scroll_tick = 0;
}
} else {
model->hint_scroll_tick++;
}
},
true);
}
static void hid_ptt_menu_enter_callback(void* context) {
furi_assert(context);
HidPushToTalkMenu* hid_ptt_menu = context;
hid_ptt_menu_mark_interaction(hid_ptt_menu);
furi_timer_start(hid_ptt_menu->hint_timer, furi_ms_to_ticks(PTT_MENU_HINT_TIMER_PERIOD_MS));
}
static void hid_ptt_menu_exit_callback(void* context) {
furi_assert(context);
HidPushToTalkMenu* hid_ptt_menu = context;
furi_timer_stop(hid_ptt_menu->hint_timer);
}
static void
hid_ptt_menu_draw_list(Canvas* canvas, void* context, const PushToTalkMenuItemArray_t items) {
furi_assert(context);
@@ -94,13 +162,44 @@ static void
FuriString* disp_str;
disp_str = furi_string_alloc_set(PushToTalkMenuItemArray_cref(it)->label);
elements_string_fit_width(canvas, disp_str, item_width - (6 * 2));
const int32_t text_y = y_offset + (item_position * item_height) + item_height - 4;
const int32_t row_left_x = 6;
const int32_t row_right_x = item_width - 2;
const size_t row_width = row_right_x - row_left_x;
canvas_draw_str(
canvas,
6,
y_offset + (item_position * item_height) + item_height - 4,
furi_string_get_cstr(disp_str));
if((position == model->position) && model->hint_visible) {
const char* hint_prefix = " | Long press";
const char* hint_suffix = "for help";
const int32_t icon_y = text_y - 8;
const size_t label_w = canvas_string_width(canvas, furi_string_get_cstr(disp_str));
const size_t prefix_w = canvas_string_width(canvas, hint_prefix);
const size_t icon_w = 9;
const size_t suffix_w = canvas_string_width(canvas, hint_suffix);
const size_t group_w = label_w + 2 + prefix_w + 2 + icon_w + 2 + suffix_w;
int32_t scroll_offset = 0;
if(group_w > row_width) {
const size_t overflow = group_w - row_width;
const size_t cycle = overflow * 2;
const size_t step = cycle ? (model->hint_scroll_tick % cycle) : 0;
scroll_offset = (step <= overflow) ? step : (cycle - step);
}
const int32_t draw_x = row_left_x - scroll_offset;
canvas_draw_str(canvas, draw_x, text_y, furi_string_get_cstr(disp_str));
int32_t hint_x = draw_x + label_w + 2;
canvas_draw_str(canvas, hint_x, text_y, hint_prefix);
hint_x += prefix_w + 2;
canvas_draw_icon(canvas, hint_x, icon_y, &I_Ok_btn_9x9);
hint_x += icon_w + 2;
canvas_draw_str(canvas, hint_x, text_y, hint_suffix);
} else {
elements_string_fit_width(canvas, disp_str, item_width - (6 * 2));
canvas_draw_str(canvas, row_left_x, text_y, furi_string_get_cstr(disp_str));
}
furi_string_free(disp_str);
}
@@ -338,6 +437,39 @@ void ptt_menu_process_ok(HidPushToTalkMenu* hid_ptt_menu) {
}
}
void ptt_menu_process_long_ok(HidPushToTalkMenu* hid_ptt_menu) {
PushToTalkMenuList* list = NULL;
PushToTalkMenuItem* item = NULL;
with_view_model(
hid_ptt_menu->view,
HidPushToTalkMenuModel * model,
{
list = &model->lists[model->list_position];
const size_t items_size = PushToTalkMenuItemArray_size(list->items);
if(model->position < items_size) {
item = PushToTalkMenuItemArray_get(list->items, model->position);
}
},
false);
if(item && list && hid_ptt_menu->long_ok_callback) {
hid_ptt_menu->long_ok_callback(
hid_ptt_menu->long_ok_callback_context,
list->index,
list->label,
item->index,
item->label);
}
}
void ptt_menu_set_long_ok_callback(
HidPushToTalkMenu* hid_ptt_menu,
PushToTalkMenuLongOkCallback callback,
void* callback_context) {
furi_assert(hid_ptt_menu);
hid_ptt_menu->long_ok_callback = callback;
hid_ptt_menu->long_ok_callback_context = callback_context;
}
static bool hid_ptt_menu_input_callback(InputEvent* event, void* context) {
furi_assert(context);
HidPushToTalkMenu* hid_ptt_menu = context;
@@ -375,7 +507,15 @@ static bool hid_ptt_menu_input_callback(InputEvent* event, void* context) {
consumed = true;
ptt_menu_process_down(hid_ptt_menu);
}
} else if(event->type == InputTypeLong && event->key == InputKeyOk) {
consumed = true;
ptt_menu_process_long_ok(hid_ptt_menu);
}
if(event->type != InputTypeRelease) {
hid_ptt_menu_mark_interaction(hid_ptt_menu);
}
return consumed;
}
@@ -392,6 +532,11 @@ HidPushToTalkMenu* hid_ptt_menu_alloc(Hid* hid) {
view_allocate_model(hid_ptt_menu->view, ViewModelTypeLocking, sizeof(HidPushToTalkMenuModel));
view_set_draw_callback(hid_ptt_menu->view, hid_ptt_menu_draw_callback);
view_set_input_callback(hid_ptt_menu->view, hid_ptt_menu_input_callback);
view_set_enter_callback(hid_ptt_menu->view, hid_ptt_menu_enter_callback);
view_set_exit_callback(hid_ptt_menu->view, hid_ptt_menu_exit_callback);
hid_ptt_menu->hint_timer =
furi_timer_alloc(hid_ptt_menu_hint_timer_callback, FuriTimerTypePeriodic, hid_ptt_menu);
with_view_model(
hid_ptt_menu->view,
@@ -400,6 +545,11 @@ HidPushToTalkMenu* hid_ptt_menu_alloc(Hid* hid) {
model->lists_count = 0;
model->position = 0;
model->window_position = 0;
model->last_interaction_tick = furi_get_tick();
model->hint_list_position = 0;
model->hint_item_position = 0;
model->hint_visible = false;
model->hint_scroll_tick = 0;
},
true);
return hid_ptt_menu;
@@ -407,6 +557,8 @@ HidPushToTalkMenu* hid_ptt_menu_alloc(Hid* hid) {
void hid_ptt_menu_free(HidPushToTalkMenu* hid_ptt_menu) {
furi_assert(hid_ptt_menu);
furi_timer_stop(hid_ptt_menu->hint_timer);
furi_timer_free(hid_ptt_menu->hint_timer);
with_view_model(
hid_ptt_menu->view,
HidPushToTalkMenuModel * model,
@@ -12,6 +12,13 @@ typedef void (*PushToTalkMenuItemCallback)(
uint32_t itemIndex,
FuriString* itemLabel);
typedef void (*PushToTalkMenuLongOkCallback)(
void* context,
uint32_t listIndex,
FuriString* listLabel,
uint32_t itemIndex,
FuriString* itemLabel);
HidPushToTalkMenu* hid_ptt_menu_alloc(Hid* bt_hid);
void hid_ptt_menu_free(HidPushToTalkMenu* hid_ptt_menu);
@@ -27,3 +34,8 @@ void ptt_menu_add_item_to_list(
void* callback_context);
void ptt_menu_add_list(HidPushToTalkMenu* hid_ptt_menu, const char* label, uint32_t index);
void ptt_menu_set_long_ok_callback(
HidPushToTalkMenu* hid_ptt_menu,
PushToTalkMenuLongOkCallback callback,
void* callback_context);
@@ -253,6 +253,14 @@ App(
sources=["modules/js_subghz/*.c"],
)
App(
appid="js_infrared",
apptype=FlipperAppType.PLUGIN,
entry_point="js_infrared_ep",
requires=["js_app"],
sources=["modules/js_infrared/*.c"],
)
App(
appid="js_blebeacon",
apptype=FlipperAppType.PLUGIN,
@@ -0,0 +1,47 @@
checkSdkFeatures(["infrared-send"]);
let infrared = require("infrared");
print("Sending Samsung32 signal (lowers volume)...");
infrared.sendSignal("Samsung32", 0x00000007, 0x0000000b);
delay(1000);
print("Sending raw signal... (Fujitsu AC)");
infrared.sendRawSignal(
[
3298, 1571, 442, 368, 442, 367, 443, 1180, 442, 370, 440, 1181, 442, 368,
442, 367, 442, 368, 442, 1180, 443, 1180, 442, 368, 441, 367, 442, 369, 441,
1180, 442, 1180, 443, 368, 442, 369, 441, 368, 442, 368, 442, 368, 442, 368,
441, 368, 441, 368, 442, 368, 442, 368, 442, 367, 442, 368, 442, 366, 444,
1181, 442, 368, 441, 367, 442, 368, 442, 367, 442, 369, 441, 367, 443, 367,
442, 1180, 442, 368, 442, 368, 442, 368, 441, 368, 441, 1180, 442, 1180,
442, 1181, 442, 1181, 441, 1181, 442, 1181, 442, 1181, 442, 1181, 442, 369,
441, 368, 441, 1182, 442, 367, 443, 367, 443, 367, 442, 368, 442, 1181, 441,
369, 441, 369, 441, 369, 441, 1180, 442, 1181, 441, 369, 442, 368, 442, 367,
442, 368, 441, 1181, 441, 1183, 440, 368, 442, 368, 441, 369, 441, 1181,
442, 368, 442, 367, 443, 1181, 442, 368, 442, 369, 441, 368, 441, 369, 440,
368, 442, 367, 443, 367, 442, 367, 442, 367, 442, 368, 442, 369, 441, 369,
441, 368, 442, 369, 441, 368, 441, 367, 443, 368, 442, 367, 442, 369, 441,
369, 441, 368, 441, 367, 442, 367, 442, 368, 442, 368, 441, 368, 442, 368,
442, 368, 441, 367, 442, 367, 442, 368, 442, 368, 441, 368, 442, 369, 441,
368, 441, 367, 442, 368, 441, 368, 442, 368, 442, 368, 442, 368, 442, 367,
442, 1181, 442, 367, 442, 368, 441, 1181, 442, 1182, 441, 1181, 442, 1181,
442, 1181, 441, 368, 442, 368, 442, 369, 441,
],
true,
{ frequency: 38000, dutyCycle: 0.33 },
);
delay(1000);
print(
"Sending raw signal... (Fujitsu AC) with default frequency and duty cycle",
);
infrared.sendRawSignal([
3300, 1596, 416, 362, 448, 363, 446, 1177, 445, 363, 446, 1177, 445, 362, 448,
362, 448, 364, 446, 1178, 444, 1207, 415, 362, 448, 362, 448, 363, 447, 1177,
445, 1177, 446, 362, 448, 362, 447, 362, 447, 362, 448, 363, 447, 362, 447,
363, 447, 363, 447, 363, 446, 363, 446, 362, 447, 362, 447, 363, 446, 1177,
445, 363, 447, 364, 446, 362, 448, 363, 447, 363, 446, 362, 447, 362, 448,
1175, 447, 363, 447, 364, 446, 362, 448, 362, 448, 1176, 446, 362, 448, 362,
448, 363, 446, 362, 448, 362, 448, 363, 447, 1175, 446, 394, 415, 1176, 446,
1178, 444, 1174, 449, 1177, 445, 1180, 443, 1179, 443,
]);
print("Success");
+1
View File
@@ -277,6 +277,7 @@ static const char* extra_features[] = {
"blebeacon",
"i2c",
"spi",
"infrared-send",
"subghz",
"usbdisk",
"vgm",
@@ -0,0 +1,129 @@
#include "../../js_modules.h"
#include <infrared_worker.h>
#include <lib/infrared/signal/infrared_signal.h>
#include <lib/infrared/worker/infrared_transmit.h>
#define TAG "JsMath"
static void ret_bad_args(struct mjs* mjs, const char* error) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error);
mjs_return(mjs, MJS_UNDEFINED);
}
void js_send_protocol_signal(struct mjs* mjs) {
size_t num_args = mjs_nargs(mjs);
if(num_args < 3 || num_args > 4) {
ret_bad_args(mjs, "Wrong argument count");
return;
}
if(!mjs_is_string(mjs_arg(mjs, 0)) || !mjs_is_number(mjs_arg(mjs, 1)) ||
!mjs_is_number(mjs_arg(mjs, 2)) || (num_args == 4 && !mjs_is_object(mjs_arg(mjs, 3)))) {
ret_bad_args(mjs, "Wrong argument type");
return;
}
bool repeat = false;
int times = 1;
if(num_args == 4) {
mjs_val_t options_obj = mjs_arg(mjs, 3);
mjs_val_t repeat_val = mjs_get(mjs, options_obj, "repeat", ~0);
if(mjs_is_boolean(repeat_val)) {
repeat = mjs_get_bool(mjs, repeat_val);
} else if(!mjs_is_undefined(repeat_val)) {
ret_bad_args(mjs, "Wrong 'repeat' option type");
return;
}
mjs_val_t times_val = mjs_get(mjs, options_obj, "times", ~0);
if(mjs_is_number(times_val)) {
times = mjs_get_int(mjs, times_val);
} else if(!mjs_is_undefined(times_val)) {
ret_bad_args(mjs, "Wrong 'times' option type");
return;
}
}
InfraredMessage message;
message.repeat = repeat;
mjs_val_t protocol_arg = mjs_arg(mjs, 0);
message.protocol = infrared_get_protocol_by_name(mjs_get_cstring(mjs, &protocol_arg));
message.address = mjs_get_int(mjs, mjs_arg(mjs, 1));
message.command = mjs_get_int(mjs, mjs_arg(mjs, 2));
infrared_send(&message, times);
}
void js_send_raw_signal(struct mjs* mjs) {
size_t num_args = mjs_nargs(mjs);
if(num_args < 1 || num_args > 3) {
ret_bad_args(mjs, "Wrong argument count");
return;
}
if(!mjs_is_array(mjs_arg(mjs, 0)) || (num_args > 1 && !mjs_is_boolean(mjs_arg(mjs, 1))) ||
(num_args > 2 && !mjs_is_object(mjs_arg(mjs, 2)))) {
ret_bad_args(mjs, "Wrong argument type");
return;
}
int array_length = mjs_array_length(mjs, mjs_arg(mjs, 0));
uint32_t timings[array_length];
for(int i = 0; i < array_length; i++) {
mjs_val_t elem = mjs_array_get(mjs, mjs_arg(mjs, 0), i);
if(!mjs_is_number(elem)) {
ret_bad_args(mjs, "Timings array must contain only numbers");
return;
}
timings[i] = mjs_get_int(mjs, elem);
}
bool start_from_mark = true;
if(num_args > 1) {
start_from_mark = mjs_get_bool(mjs, mjs_arg(mjs, 1));
}
if(num_args > 2) {
mjs_val_t options_obj = mjs_arg(mjs, 2);
mjs_val_t frequency_val = mjs_get(mjs, options_obj, "frequency", ~0);
if(!mjs_is_number(frequency_val)) {
ret_bad_args(mjs, "Wrong 'frequency' option type");
return;
}
mjs_val_t duty_val = mjs_get(mjs, options_obj, "dutyCycle", ~0);
if(!mjs_is_number(duty_val)) {
ret_bad_args(mjs, "Wrong 'dutyCycle' option type");
return;
}
uint32_t frequency = mjs_get_int(mjs, frequency_val);
float duty_cycle = mjs_get_double(mjs, duty_val);
infrared_send_raw_ext(timings, array_length, start_from_mark, frequency, duty_cycle);
} else {
infrared_send_raw(timings, array_length, start_from_mark);
}
return;
}
static void* js_infrared_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
mjs_val_t infrared_object = mjs_mk_object(mjs);
mjs_set(mjs, infrared_object, "sendSignal", ~0, MJS_MK_FN(js_send_protocol_signal));
mjs_set(mjs, infrared_object, "sendRawSignal", ~0, MJS_MK_FN(js_send_raw_signal));
*object = infrared_object;
return (void*)1;
}
static const JsModuleDescriptor js_infrared_desc = {
"infrared",
js_infrared_create,
NULL,
NULL,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_infrared_desc,
};
const FlipperAppPluginDescriptor* js_infrared_ep(void) {
return &plugin_descriptor;
}
@@ -0,0 +1,41 @@
/**
* Module for using Infrared blaster/receptor
* @version Available with JS feature `infrared-send`
* @module
*/
/**
* Sends an IR signal using a known protocol by Flipper Firmware
* @param address Note that the address expects a number. If you're reading from Flipper's IR files, the address is usually in little-endian hex format. Javascript numbers are defined as big endian by default.
* @param command Note that the command expects a number. If you're reading from Flipper's IR files, the command is usually in little-endian hex format. Javascript numbers are defined as big endian by default.
* @param options Repeat marks the signal as a repeat signal, times indicates how many times to send the signal
*/
export declare function sendSignal(
protocol:
| "NEC"
| "NECext"
| "NEC42"
| "NEC42ext"
| "Samsung32"
| "RC6"
| "RC5"
| "RC5X"
| "SIRC"
| "SIRC15"
| "SIRC20"
| "Kaseikyo"
| "RCA",
address: number,
command: number,
options?: { repeat?: boolean; times?: number },
): void;
/**
* Sends a signal from an unknown protocol
* @param startFromMark defaults to true
*/
export declare function sendRawSignal(
timings: number[],
startFromMark?: boolean,
advancedSettings?: { frequency: number; dutyCycle: number },
): void;
@@ -1,6 +1,6 @@
{
"name": "@next-flip/fz-sdk-mntm",
"version": "1.0.0",
"version": "1.0.1",
"description": "Type declarations and documentation for native JS modules available on Momentum Custom Firmware for Flipper Zero",
"keywords": [
"momentum",
@@ -153,10 +153,11 @@ async function upload(config) {
port.write(`storage remove ${config.output}\x0d`);
port.drain();
await waitFor(">: ", 1000);
port.write(`storage write_chunk ${config.output} ${appFile.length}\x0d`);
const appFileBuffer = Buffer.from(appFile, "utf8");
port.write(`storage write_chunk ${config.output} ${appFileBuffer.length}\x0d`);
await waitFor("Ready", 1000);
port.write(appFile);
port.drain();
port.write(appFileBuffer);
await new Promise(resolve => port.drain(resolve));
await waitFor(">: ", 1000);
console.log("Launching application");
Binary file not shown.

Before

Width:  |  Height:  |  Size: 858 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 872 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 861 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 853 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 851 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 852 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 850 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 851 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 860 B

Some files were not shown because too many files have changed in this diff Show More