Merge remote-tracking branch 'noproto/dev' into ulcdict

This commit is contained in:
noproto
2024-11-10 17:54:23 -05:00
157 changed files with 9343 additions and 2088 deletions

5
.gitignore vendored
View File

@@ -63,4 +63,7 @@ PVS-Studio.log
.gdbinit .gdbinit
/fbt_options_local.py /fbt_options_local.py
# JS packages
node_modules/

3
.gitmodules vendored
View File

@@ -35,3 +35,6 @@
[submodule "documentation/doxygen/doxygen-awesome-css"] [submodule "documentation/doxygen/doxygen-awesome-css"]
path = documentation/doxygen/doxygen-awesome-css path = documentation/doxygen/doxygen-awesome-css
url = https://github.com/jothepro/doxygen-awesome-css.git url = https://github.com/jothepro/doxygen-awesome-css.git
[submodule "applications/system/picopass"]
path = applications/system/picopass
url = https://gitlab.com/bettse/picopass.git

View File

@@ -80,3 +80,4 @@ Utility apps not visible in other menus, plus few external apps pre-packaged wit
- `storage_move_to_sd` - Data migration tool for internal storage - `storage_move_to_sd` - Data migration tool for internal storage
- `updater` - Update service & application - `updater` - Update service & application
- `mfkey` - MIFARE Classic key recovery tool - `mfkey` - MIFARE Classic key recovery tool
- `picopass` - Picopass tool

View File

@@ -82,7 +82,7 @@ static void view_port_input_callback(InputEvent* input_event, void* context) {
furi_message_queue_put(app->input_queue, input_event, 0); furi_message_queue_put(app->input_queue, input_event, 0);
} }
static bool input_queue_callback(FuriEventLoopObject* object, void* context) { static void input_queue_callback(FuriEventLoopObject* object, void* context) {
FuriMessageQueue* queue = object; FuriMessageQueue* queue = object;
EventLoopBlinkTestApp* app = context; EventLoopBlinkTestApp* app = context;
@@ -107,8 +107,6 @@ static bool input_queue_callback(FuriEventLoopObject* object, void* context) {
furi_event_loop_stop(app->event_loop); furi_event_loop_stop(app->event_loop);
} }
} }
return true;
} }
static void blink_timer_callback(void* context) { static void blink_timer_callback(void* context) {

View File

@@ -1,4 +1,15 @@
let tests = require("tests"); let tests = require("tests");
let flipper = require("flipper");
tests.assert_eq(1337, 1337); tests.assert_eq(1337, 1337);
tests.assert_eq("hello", "hello"); tests.assert_eq("hello", "hello");
tests.assert_eq("compatible", sdkCompatibilityStatus(0, 1));
tests.assert_eq("firmwareTooOld", sdkCompatibilityStatus(100500, 0));
tests.assert_eq("firmwareTooNew", sdkCompatibilityStatus(-100500, 0));
tests.assert_eq(true, doesSdkSupport(["baseline"]));
tests.assert_eq(false, doesSdkSupport(["abobus", "other-nonexistent-feature"]));
tests.assert_eq("flipperdevices", flipper.firmwareVendor);
tests.assert_eq(0, flipper.jsSdkVersion[0]);
tests.assert_eq(1, flipper.jsSdkVersion[1]);

View File

@@ -1,205 +0,0 @@
#include "../test.h"
#include <furi.h>
#include <furi_hal.h>
#include <FreeRTOS.h>
#include <task.h>
#define TAG "TestFuriEventLoop"
#define EVENT_LOOP_EVENT_COUNT (256u)
typedef struct {
FuriMessageQueue* mq;
FuriEventLoop* producer_event_loop;
uint32_t producer_counter;
FuriEventLoop* consumer_event_loop;
uint32_t consumer_counter;
} TestFuriData;
bool test_furi_event_loop_producer_mq_callback(FuriEventLoopObject* object, void* context) {
furi_check(context);
TestFuriData* data = context;
furi_check(data->mq == object, "Invalid queue");
FURI_LOG_I(
TAG, "producer_mq_callback: %lu %lu", data->producer_counter, data->consumer_counter);
if(data->producer_counter == EVENT_LOOP_EVENT_COUNT / 2) {
furi_event_loop_unsubscribe(data->producer_event_loop, data->mq);
furi_event_loop_subscribe_message_queue(
data->producer_event_loop,
data->mq,
FuriEventLoopEventOut,
test_furi_event_loop_producer_mq_callback,
data);
}
if(data->producer_counter == EVENT_LOOP_EVENT_COUNT) {
furi_event_loop_stop(data->producer_event_loop);
return false;
}
data->producer_counter++;
furi_check(
furi_message_queue_put(data->mq, &data->producer_counter, 0) == FuriStatusOk,
"furi_message_queue_put failed");
furi_delay_us(furi_hal_random_get() % 1000);
return true;
}
int32_t test_furi_event_loop_producer(void* p) {
furi_check(p);
TestFuriData* data = p;
FURI_LOG_I(TAG, "producer start 1st run");
data->producer_event_loop = furi_event_loop_alloc();
furi_event_loop_subscribe_message_queue(
data->producer_event_loop,
data->mq,
FuriEventLoopEventOut,
test_furi_event_loop_producer_mq_callback,
data);
furi_event_loop_run(data->producer_event_loop);
// 2 EventLoop index, 0xFFFFFFFF - all possible flags, emulate uncleared flags
xTaskNotifyIndexed(xTaskGetCurrentTaskHandle(), 2, 0xFFFFFFFF, eSetBits);
furi_event_loop_unsubscribe(data->producer_event_loop, data->mq);
furi_event_loop_free(data->producer_event_loop);
FURI_LOG_I(TAG, "producer start 2nd run");
data->producer_counter = 0;
data->producer_event_loop = furi_event_loop_alloc();
furi_event_loop_subscribe_message_queue(
data->producer_event_loop,
data->mq,
FuriEventLoopEventOut,
test_furi_event_loop_producer_mq_callback,
data);
furi_event_loop_run(data->producer_event_loop);
furi_event_loop_unsubscribe(data->producer_event_loop, data->mq);
furi_event_loop_free(data->producer_event_loop);
FURI_LOG_I(TAG, "producer end");
return 0;
}
bool test_furi_event_loop_consumer_mq_callback(FuriEventLoopObject* object, void* context) {
furi_check(context);
TestFuriData* data = context;
furi_check(data->mq == object);
furi_delay_us(furi_hal_random_get() % 1000);
furi_check(furi_message_queue_get(data->mq, &data->consumer_counter, 0) == FuriStatusOk);
FURI_LOG_I(
TAG, "consumer_mq_callback: %lu %lu", data->producer_counter, data->consumer_counter);
if(data->consumer_counter == EVENT_LOOP_EVENT_COUNT / 2) {
furi_event_loop_unsubscribe(data->consumer_event_loop, data->mq);
furi_event_loop_subscribe_message_queue(
data->consumer_event_loop,
data->mq,
FuriEventLoopEventIn,
test_furi_event_loop_consumer_mq_callback,
data);
}
if(data->consumer_counter == EVENT_LOOP_EVENT_COUNT) {
furi_event_loop_stop(data->consumer_event_loop);
return false;
}
return true;
}
int32_t test_furi_event_loop_consumer(void* p) {
furi_check(p);
TestFuriData* data = p;
FURI_LOG_I(TAG, "consumer start 1st run");
data->consumer_event_loop = furi_event_loop_alloc();
furi_event_loop_subscribe_message_queue(
data->consumer_event_loop,
data->mq,
FuriEventLoopEventIn,
test_furi_event_loop_consumer_mq_callback,
data);
furi_event_loop_run(data->consumer_event_loop);
// 2 EventLoop index, 0xFFFFFFFF - all possible flags, emulate uncleared flags
xTaskNotifyIndexed(xTaskGetCurrentTaskHandle(), 2, 0xFFFFFFFF, eSetBits);
furi_event_loop_unsubscribe(data->consumer_event_loop, data->mq);
furi_event_loop_free(data->consumer_event_loop);
FURI_LOG_I(TAG, "consumer start 2nd run");
data->consumer_counter = 0;
data->consumer_event_loop = furi_event_loop_alloc();
furi_event_loop_subscribe_message_queue(
data->consumer_event_loop,
data->mq,
FuriEventLoopEventIn,
test_furi_event_loop_consumer_mq_callback,
data);
furi_event_loop_run(data->consumer_event_loop);
furi_event_loop_unsubscribe(data->consumer_event_loop, data->mq);
furi_event_loop_free(data->consumer_event_loop);
FURI_LOG_I(TAG, "consumer end");
return 0;
}
void test_furi_event_loop(void) {
TestFuriData data = {};
data.mq = furi_message_queue_alloc(16, sizeof(uint32_t));
FuriThread* producer_thread = furi_thread_alloc();
furi_thread_set_name(producer_thread, "producer_thread");
furi_thread_set_stack_size(producer_thread, 1 * 1024);
furi_thread_set_callback(producer_thread, test_furi_event_loop_producer);
furi_thread_set_context(producer_thread, &data);
furi_thread_start(producer_thread);
FuriThread* consumer_thread = furi_thread_alloc();
furi_thread_set_name(consumer_thread, "consumer_thread");
furi_thread_set_stack_size(consumer_thread, 1 * 1024);
furi_thread_set_callback(consumer_thread, test_furi_event_loop_consumer);
furi_thread_set_context(consumer_thread, &data);
furi_thread_start(consumer_thread);
// Wait for thread to complete their tasks
furi_thread_join(producer_thread);
furi_thread_join(consumer_thread);
// The test itself
mu_assert_int_eq(data.producer_counter, data.consumer_counter);
mu_assert_int_eq(data.producer_counter, EVENT_LOOP_EVENT_COUNT);
// Release memory
furi_thread_free(consumer_thread);
furi_thread_free(producer_thread);
furi_message_queue_free(data.mq);
}

View File

@@ -0,0 +1,490 @@
#include "../test.h"
#include <furi.h>
#include <furi_hal.h>
#include <FreeRTOS.h>
#include <task.h>
#define TAG "TestFuriEventLoop"
#define MESSAGE_COUNT (256UL)
#define EVENT_FLAG_COUNT (23UL)
#define PRIMITIVE_COUNT (4UL)
#define RUN_COUNT (2UL)
typedef struct {
FuriEventLoop* event_loop;
uint32_t message_queue_count;
uint32_t stream_buffer_count;
uint32_t event_flag_count;
uint32_t semaphore_count;
uint32_t primitives_tested;
} TestFuriEventLoopThread;
typedef struct {
FuriMessageQueue* message_queue;
FuriStreamBuffer* stream_buffer;
FuriEventFlag* event_flag;
FuriSemaphore* semaphore;
TestFuriEventLoopThread producer;
TestFuriEventLoopThread consumer;
} TestFuriEventLoopData;
static void test_furi_event_loop_pending_callback(void* context) {
furi_check(context);
TestFuriEventLoopThread* test_thread = context;
furi_check(test_thread->primitives_tested < PRIMITIVE_COUNT);
test_thread->primitives_tested++;
FURI_LOG_I(TAG, "primitives tested: %lu", test_thread->primitives_tested);
if(test_thread->primitives_tested == PRIMITIVE_COUNT) {
furi_event_loop_stop(test_thread->event_loop);
}
}
static void test_furi_event_loop_thread_init(TestFuriEventLoopThread* test_thread) {
memset(test_thread, 0, sizeof(TestFuriEventLoopThread));
test_thread->event_loop = furi_event_loop_alloc();
}
static void test_furi_event_loop_thread_run_and_cleanup(TestFuriEventLoopThread* test_thread) {
furi_event_loop_run(test_thread->event_loop);
// 2 EventLoop index, 0xFFFFFFFF - all possible flags, emulate uncleared flags
xTaskNotifyIndexed(xTaskGetCurrentTaskHandle(), 2, 0xFFFFFFFF, eSetBits);
furi_event_loop_free(test_thread->event_loop);
}
static void test_furi_event_loop_producer_message_queue_callback(
FuriEventLoopObject* object,
void* context) {
furi_check(context);
TestFuriEventLoopData* data = context;
furi_check(data->message_queue == object);
FURI_LOG_I(
TAG,
"producer MessageQueue: %lu %lu",
data->producer.message_queue_count,
data->consumer.message_queue_count);
if(data->producer.message_queue_count == MESSAGE_COUNT / 2) {
furi_event_loop_unsubscribe(data->producer.event_loop, data->message_queue);
furi_event_loop_subscribe_message_queue(
data->producer.event_loop,
data->message_queue,
FuriEventLoopEventOut,
test_furi_event_loop_producer_message_queue_callback,
data);
} else if(data->producer.message_queue_count == MESSAGE_COUNT) {
furi_event_loop_unsubscribe(data->producer.event_loop, data->message_queue);
furi_event_loop_pend_callback(
data->producer.event_loop, test_furi_event_loop_pending_callback, &data->producer);
return;
}
data->producer.message_queue_count++;
furi_check(
furi_message_queue_put(data->message_queue, &data->producer.message_queue_count, 0) ==
FuriStatusOk);
furi_delay_us(furi_hal_random_get() % 100);
}
static void test_furi_event_loop_producer_stream_buffer_callback(
FuriEventLoopObject* object,
void* context) {
furi_check(context);
TestFuriEventLoopData* data = context;
furi_check(data->stream_buffer == object);
TestFuriEventLoopThread* producer = &data->producer;
TestFuriEventLoopThread* consumer = &data->consumer;
FURI_LOG_I(
TAG,
"producer StreamBuffer: %lu %lu",
producer->stream_buffer_count,
consumer->stream_buffer_count);
if(producer->stream_buffer_count == MESSAGE_COUNT / 2) {
furi_event_loop_unsubscribe(producer->event_loop, data->stream_buffer);
furi_event_loop_subscribe_stream_buffer(
producer->event_loop,
data->stream_buffer,
FuriEventLoopEventOut,
test_furi_event_loop_producer_stream_buffer_callback,
data);
} else if(producer->stream_buffer_count == MESSAGE_COUNT) {
furi_event_loop_unsubscribe(producer->event_loop, data->stream_buffer);
furi_event_loop_pend_callback(
producer->event_loop, test_furi_event_loop_pending_callback, producer);
return;
}
producer->stream_buffer_count++;
furi_check(
furi_stream_buffer_send(
data->stream_buffer, &producer->stream_buffer_count, sizeof(uint32_t), 0) ==
sizeof(uint32_t));
furi_delay_us(furi_hal_random_get() % 100);
}
static void
test_furi_event_loop_producer_event_flag_callback(FuriEventLoopObject* object, void* context) {
furi_check(context);
TestFuriEventLoopData* data = context;
furi_check(data->event_flag == object);
const uint32_t producer_flags = (1UL << data->producer.event_flag_count);
const uint32_t consumer_flags = (1UL << data->consumer.event_flag_count);
FURI_LOG_I(TAG, "producer EventFlag: 0x%06lX 0x%06lX", producer_flags, consumer_flags);
furi_check(furi_event_flag_set(data->event_flag, producer_flags) & producer_flags);
if(data->producer.event_flag_count == EVENT_FLAG_COUNT / 2) {
furi_event_loop_unsubscribe(data->producer.event_loop, data->event_flag);
furi_event_loop_subscribe_event_flag(
data->producer.event_loop,
data->event_flag,
FuriEventLoopEventOut,
test_furi_event_loop_producer_event_flag_callback,
data);
} else if(data->producer.event_flag_count == EVENT_FLAG_COUNT) {
furi_event_loop_unsubscribe(data->producer.event_loop, data->event_flag);
furi_event_loop_pend_callback(
data->producer.event_loop, test_furi_event_loop_pending_callback, &data->producer);
return;
}
data->producer.event_flag_count++;
furi_delay_us(furi_hal_random_get() % 100);
}
static void
test_furi_event_loop_producer_semaphore_callback(FuriEventLoopObject* object, void* context) {
furi_check(context);
TestFuriEventLoopData* data = context;
furi_check(data->semaphore == object);
TestFuriEventLoopThread* producer = &data->producer;
TestFuriEventLoopThread* consumer = &data->consumer;
FURI_LOG_I(
TAG, "producer Semaphore: %lu %lu", producer->semaphore_count, consumer->semaphore_count);
furi_check(furi_semaphore_release(data->semaphore) == FuriStatusOk);
if(producer->semaphore_count == MESSAGE_COUNT / 2) {
furi_event_loop_unsubscribe(producer->event_loop, data->semaphore);
furi_event_loop_subscribe_semaphore(
producer->event_loop,
data->semaphore,
FuriEventLoopEventOut,
test_furi_event_loop_producer_semaphore_callback,
data);
} else if(producer->semaphore_count == MESSAGE_COUNT) {
furi_event_loop_unsubscribe(producer->event_loop, data->semaphore);
furi_event_loop_pend_callback(
producer->event_loop, test_furi_event_loop_pending_callback, producer);
return;
}
data->producer.semaphore_count++;
furi_delay_us(furi_hal_random_get() % 100);
}
static int32_t test_furi_event_loop_producer(void* p) {
furi_check(p);
TestFuriEventLoopData* data = p;
TestFuriEventLoopThread* producer = &data->producer;
for(uint32_t i = 0; i < RUN_COUNT; ++i) {
FURI_LOG_I(TAG, "producer start run %lu", i);
test_furi_event_loop_thread_init(producer);
furi_event_loop_subscribe_message_queue(
producer->event_loop,
data->message_queue,
FuriEventLoopEventOut,
test_furi_event_loop_producer_message_queue_callback,
data);
furi_event_loop_subscribe_stream_buffer(
producer->event_loop,
data->stream_buffer,
FuriEventLoopEventOut,
test_furi_event_loop_producer_stream_buffer_callback,
data);
furi_event_loop_subscribe_event_flag(
producer->event_loop,
data->event_flag,
FuriEventLoopEventOut,
test_furi_event_loop_producer_event_flag_callback,
data);
furi_event_loop_subscribe_semaphore(
producer->event_loop,
data->semaphore,
FuriEventLoopEventOut,
test_furi_event_loop_producer_semaphore_callback,
data);
test_furi_event_loop_thread_run_and_cleanup(producer);
}
FURI_LOG_I(TAG, "producer end");
return 0;
}
static void test_furi_event_loop_consumer_message_queue_callback(
FuriEventLoopObject* object,
void* context) {
furi_check(context);
TestFuriEventLoopData* data = context;
furi_check(data->message_queue == object);
furi_delay_us(furi_hal_random_get() % 100);
furi_check(
furi_message_queue_get(data->message_queue, &data->consumer.message_queue_count, 0) ==
FuriStatusOk);
FURI_LOG_I(
TAG,
"consumer MessageQueue: %lu %lu",
data->producer.message_queue_count,
data->consumer.message_queue_count);
if(data->consumer.message_queue_count == MESSAGE_COUNT / 2) {
furi_event_loop_unsubscribe(data->consumer.event_loop, data->message_queue);
furi_event_loop_subscribe_message_queue(
data->consumer.event_loop,
data->message_queue,
FuriEventLoopEventIn,
test_furi_event_loop_consumer_message_queue_callback,
data);
} else if(data->consumer.message_queue_count == MESSAGE_COUNT) {
furi_event_loop_unsubscribe(data->consumer.event_loop, data->message_queue);
furi_event_loop_pend_callback(
data->consumer.event_loop, test_furi_event_loop_pending_callback, &data->consumer);
}
}
static void test_furi_event_loop_consumer_stream_buffer_callback(
FuriEventLoopObject* object,
void* context) {
furi_check(context);
TestFuriEventLoopData* data = context;
furi_check(data->stream_buffer == object);
TestFuriEventLoopThread* producer = &data->producer;
TestFuriEventLoopThread* consumer = &data->consumer;
furi_delay_us(furi_hal_random_get() % 100);
furi_check(
furi_stream_buffer_receive(
data->stream_buffer, &consumer->stream_buffer_count, sizeof(uint32_t), 0) ==
sizeof(uint32_t));
FURI_LOG_I(
TAG,
"consumer StreamBuffer: %lu %lu",
producer->stream_buffer_count,
consumer->stream_buffer_count);
if(consumer->stream_buffer_count == MESSAGE_COUNT / 2) {
furi_event_loop_unsubscribe(consumer->event_loop, data->stream_buffer);
furi_event_loop_subscribe_stream_buffer(
consumer->event_loop,
data->stream_buffer,
FuriEventLoopEventIn,
test_furi_event_loop_consumer_stream_buffer_callback,
data);
} else if(consumer->stream_buffer_count == MESSAGE_COUNT) {
furi_event_loop_unsubscribe(data->consumer.event_loop, data->stream_buffer);
furi_event_loop_pend_callback(
consumer->event_loop, test_furi_event_loop_pending_callback, consumer);
}
}
static void
test_furi_event_loop_consumer_event_flag_callback(FuriEventLoopObject* object, void* context) {
furi_check(context);
TestFuriEventLoopData* data = context;
furi_check(data->event_flag == object);
furi_delay_us(furi_hal_random_get() % 100);
const uint32_t producer_flags = (1UL << data->producer.event_flag_count);
const uint32_t consumer_flags = (1UL << data->consumer.event_flag_count);
furi_check(
furi_event_flag_wait(data->event_flag, consumer_flags, FuriFlagWaitAny, 0) &
consumer_flags);
FURI_LOG_I(TAG, "consumer EventFlag: 0x%06lX 0x%06lX", producer_flags, consumer_flags);
if(data->consumer.event_flag_count == EVENT_FLAG_COUNT / 2) {
furi_event_loop_unsubscribe(data->consumer.event_loop, data->event_flag);
furi_event_loop_subscribe_event_flag(
data->consumer.event_loop,
data->event_flag,
FuriEventLoopEventIn,
test_furi_event_loop_consumer_event_flag_callback,
data);
} else if(data->consumer.event_flag_count == EVENT_FLAG_COUNT) {
furi_event_loop_unsubscribe(data->consumer.event_loop, data->event_flag);
furi_event_loop_pend_callback(
data->consumer.event_loop, test_furi_event_loop_pending_callback, &data->consumer);
return;
}
data->consumer.event_flag_count++;
}
static void
test_furi_event_loop_consumer_semaphore_callback(FuriEventLoopObject* object, void* context) {
furi_check(context);
TestFuriEventLoopData* data = context;
furi_check(data->semaphore == object);
furi_delay_us(furi_hal_random_get() % 100);
TestFuriEventLoopThread* producer = &data->producer;
TestFuriEventLoopThread* consumer = &data->consumer;
furi_check(furi_semaphore_acquire(data->semaphore, 0) == FuriStatusOk);
FURI_LOG_I(
TAG, "consumer Semaphore: %lu %lu", producer->semaphore_count, consumer->semaphore_count);
if(consumer->semaphore_count == MESSAGE_COUNT / 2) {
furi_event_loop_unsubscribe(consumer->event_loop, data->semaphore);
furi_event_loop_subscribe_semaphore(
consumer->event_loop,
data->semaphore,
FuriEventLoopEventIn,
test_furi_event_loop_consumer_semaphore_callback,
data);
} else if(consumer->semaphore_count == MESSAGE_COUNT) {
furi_event_loop_unsubscribe(consumer->event_loop, data->semaphore);
furi_event_loop_pend_callback(
consumer->event_loop, test_furi_event_loop_pending_callback, consumer);
return;
}
data->consumer.semaphore_count++;
}
static int32_t test_furi_event_loop_consumer(void* p) {
furi_check(p);
TestFuriEventLoopData* data = p;
TestFuriEventLoopThread* consumer = &data->consumer;
for(uint32_t i = 0; i < RUN_COUNT; ++i) {
FURI_LOG_I(TAG, "consumer start run %lu", i);
test_furi_event_loop_thread_init(consumer);
furi_event_loop_subscribe_message_queue(
consumer->event_loop,
data->message_queue,
FuriEventLoopEventIn,
test_furi_event_loop_consumer_message_queue_callback,
data);
furi_event_loop_subscribe_stream_buffer(
consumer->event_loop,
data->stream_buffer,
FuriEventLoopEventIn,
test_furi_event_loop_consumer_stream_buffer_callback,
data);
furi_event_loop_subscribe_event_flag(
consumer->event_loop,
data->event_flag,
FuriEventLoopEventIn,
test_furi_event_loop_consumer_event_flag_callback,
data);
furi_event_loop_subscribe_semaphore(
consumer->event_loop,
data->semaphore,
FuriEventLoopEventIn,
test_furi_event_loop_consumer_semaphore_callback,
data);
test_furi_event_loop_thread_run_and_cleanup(consumer);
}
FURI_LOG_I(TAG, "consumer end");
return 0;
}
void test_furi_event_loop(void) {
TestFuriEventLoopData data = {};
data.message_queue = furi_message_queue_alloc(16, sizeof(uint32_t));
data.stream_buffer = furi_stream_buffer_alloc(16, sizeof(uint32_t));
data.event_flag = furi_event_flag_alloc();
data.semaphore = furi_semaphore_alloc(8, 0);
FuriThread* producer_thread =
furi_thread_alloc_ex("producer_thread", 1 * 1024, test_furi_event_loop_producer, &data);
furi_thread_start(producer_thread);
FuriThread* consumer_thread =
furi_thread_alloc_ex("consumer_thread", 1 * 1024, test_furi_event_loop_consumer, &data);
furi_thread_start(consumer_thread);
// Wait for thread to complete their tasks
furi_thread_join(producer_thread);
furi_thread_join(consumer_thread);
TestFuriEventLoopThread* producer = &data.producer;
TestFuriEventLoopThread* consumer = &data.consumer;
// The test itself
mu_assert_int_eq(producer->message_queue_count, consumer->message_queue_count);
mu_assert_int_eq(producer->message_queue_count, MESSAGE_COUNT);
mu_assert_int_eq(producer->stream_buffer_count, consumer->stream_buffer_count);
mu_assert_int_eq(producer->stream_buffer_count, MESSAGE_COUNT);
mu_assert_int_eq(producer->event_flag_count, consumer->event_flag_count);
mu_assert_int_eq(producer->event_flag_count, EVENT_FLAG_COUNT);
mu_assert_int_eq(producer->semaphore_count, consumer->semaphore_count);
mu_assert_int_eq(producer->semaphore_count, MESSAGE_COUNT);
// Release memory
furi_thread_free(consumer_thread);
furi_thread_free(producer_thread);
furi_message_queue_free(data.message_queue);
furi_stream_buffer_free(data.stream_buffer);
furi_event_flag_free(data.event_flag);
furi_semaphore_free(data.semaphore);
}

View File

@@ -0,0 +1,103 @@
#include <furi.h>
#include "../test.h" // IWYU pragma: keep
#define MESSAGE_QUEUE_CAPACITY (16U)
#define MESSAGE_QUEUE_ELEMENT_SIZE (sizeof(uint32_t))
#define STREAM_BUFFER_SIZE (32U)
#define STREAM_BUFFER_TRG_LEVEL (STREAM_BUFFER_SIZE / 2U)
typedef struct {
FuriMessageQueue* message_queue;
FuriStreamBuffer* stream_buffer;
} TestFuriPrimitivesData;
static void test_furi_message_queue(TestFuriPrimitivesData* data) {
FuriMessageQueue* message_queue = data->message_queue;
mu_assert_int_eq(0, furi_message_queue_get_count(message_queue));
mu_assert_int_eq(MESSAGE_QUEUE_CAPACITY, furi_message_queue_get_space(message_queue));
mu_assert_int_eq(MESSAGE_QUEUE_CAPACITY, furi_message_queue_get_capacity(message_queue));
mu_assert_int_eq(
MESSAGE_QUEUE_ELEMENT_SIZE, furi_message_queue_get_message_size(message_queue));
for(uint32_t i = 0;; ++i) {
mu_assert_int_eq(MESSAGE_QUEUE_CAPACITY - i, furi_message_queue_get_space(message_queue));
mu_assert_int_eq(i, furi_message_queue_get_count(message_queue));
if(furi_message_queue_put(message_queue, &i, 0) != FuriStatusOk) {
break;
}
}
mu_assert_int_eq(0, furi_message_queue_get_space(message_queue));
mu_assert_int_eq(MESSAGE_QUEUE_CAPACITY, furi_message_queue_get_count(message_queue));
for(uint32_t i = 0;; ++i) {
mu_assert_int_eq(i, furi_message_queue_get_space(message_queue));
mu_assert_int_eq(MESSAGE_QUEUE_CAPACITY - i, furi_message_queue_get_count(message_queue));
uint32_t value;
if(furi_message_queue_get(message_queue, &value, 0) != FuriStatusOk) {
break;
}
mu_assert_int_eq(i, value);
}
mu_assert_int_eq(0, furi_message_queue_get_count(message_queue));
mu_assert_int_eq(MESSAGE_QUEUE_CAPACITY, furi_message_queue_get_space(message_queue));
}
static void test_furi_stream_buffer(TestFuriPrimitivesData* data) {
FuriStreamBuffer* stream_buffer = data->stream_buffer;
mu_assert(furi_stream_buffer_is_empty(stream_buffer), "Must be empty");
mu_assert(!furi_stream_buffer_is_full(stream_buffer), "Must be not full");
mu_assert_int_eq(0, furi_stream_buffer_bytes_available(stream_buffer));
mu_assert_int_eq(STREAM_BUFFER_SIZE, furi_stream_buffer_spaces_available(stream_buffer));
for(uint8_t i = 0;; ++i) {
mu_assert_int_eq(i, furi_stream_buffer_bytes_available(stream_buffer));
mu_assert_int_eq(
STREAM_BUFFER_SIZE - i, furi_stream_buffer_spaces_available(stream_buffer));
if(furi_stream_buffer_send(stream_buffer, &i, sizeof(uint8_t), 0) != sizeof(uint8_t)) {
break;
}
}
mu_assert(!furi_stream_buffer_is_empty(stream_buffer), "Must be not empty");
mu_assert(furi_stream_buffer_is_full(stream_buffer), "Must be full");
mu_assert_int_eq(STREAM_BUFFER_SIZE, furi_stream_buffer_bytes_available(stream_buffer));
mu_assert_int_eq(0, furi_stream_buffer_spaces_available(stream_buffer));
for(uint8_t i = 0;; ++i) {
mu_assert_int_eq(
STREAM_BUFFER_SIZE - i, furi_stream_buffer_bytes_available(stream_buffer));
mu_assert_int_eq(i, furi_stream_buffer_spaces_available(stream_buffer));
uint8_t value;
if(furi_stream_buffer_receive(stream_buffer, &value, sizeof(uint8_t), 0) !=
sizeof(uint8_t)) {
break;
}
mu_assert_int_eq(i, value);
}
}
// This is a stub that needs expanding
void test_furi_primitives(void) {
TestFuriPrimitivesData data = {
.message_queue =
furi_message_queue_alloc(MESSAGE_QUEUE_CAPACITY, MESSAGE_QUEUE_ELEMENT_SIZE),
.stream_buffer = furi_stream_buffer_alloc(STREAM_BUFFER_SIZE, STREAM_BUFFER_TRG_LEVEL),
};
test_furi_message_queue(&data);
test_furi_stream_buffer(&data);
furi_message_queue_free(data.message_queue);
furi_stream_buffer_free(data.stream_buffer);
}

View File

@@ -9,6 +9,7 @@ void test_furi_pubsub(void);
void test_furi_memmgr(void); void test_furi_memmgr(void);
void test_furi_event_loop(void); void test_furi_event_loop(void);
void test_errno_saving(void); void test_errno_saving(void);
void test_furi_primitives(void);
static int foo = 0; static int foo = 0;
@@ -47,6 +48,10 @@ MU_TEST(mu_test_errno_saving) {
test_errno_saving(); test_errno_saving();
} }
MU_TEST(mu_test_furi_primitives) {
test_furi_primitives();
}
MU_TEST_SUITE(test_suite) { MU_TEST_SUITE(test_suite) {
MU_SUITE_CONFIGURE(&test_setup, &test_teardown); MU_SUITE_CONFIGURE(&test_setup, &test_teardown);
MU_RUN_TEST(test_check); MU_RUN_TEST(test_check);
@@ -57,6 +62,7 @@ MU_TEST_SUITE(test_suite) {
MU_RUN_TEST(mu_test_furi_memmgr); MU_RUN_TEST(mu_test_furi_memmgr);
MU_RUN_TEST(mu_test_furi_event_loop); MU_RUN_TEST(mu_test_furi_event_loop);
MU_RUN_TEST(mu_test_errno_saving); MU_RUN_TEST(mu_test_errno_saving);
MU_RUN_TEST(mu_test_furi_primitives);
} }
int run_minunit_test_furi(void) { int run_minunit_test_furi(void) {

View File

@@ -1,3 +1,12 @@
App(
appid="example_event_loop_event_flags",
name="Example: Event Loop Event Flags",
apptype=FlipperAppType.EXTERNAL,
sources=["example_event_loop_event_flags.c"],
entry_point="example_event_loop_event_flags_app",
fap_category="Examples",
)
App( App(
appid="example_event_loop_timer", appid="example_event_loop_timer",
name="Example: Event Loop Timer", name="Example: Event Loop Timer",

View File

@@ -0,0 +1,173 @@
/**
* @file example_event_loop_event_flags.c
* @brief Example application demonstrating the use of the FuriEventFlag primitive in FuriEventLoop instances.
*
* This application receives keystrokes from the input service and sets the appropriate flags,
* which are subsequently processed in the event loop
*/
#include <furi.h>
#include <gui/gui.h>
#include <gui/view_port.h>
#include <furi_hal_random.h>
#define TAG "ExampleEventLoopEventFlags"
typedef struct {
Gui* gui;
ViewPort* view_port;
FuriEventLoop* event_loop;
FuriEventFlag* event_flag;
} EventLoopEventFlagsApp;
typedef enum {
EventLoopEventFlagsOk = (1 << 0),
EventLoopEventFlagsUp = (1 << 1),
EventLoopEventFlagsDown = (1 << 2),
EventLoopEventFlagsLeft = (1 << 3),
EventLoopEventFlagsRight = (1 << 4),
EventLoopEventFlagsBack = (1 << 5),
EventLoopEventFlagsExit = (1 << 6),
} EventLoopEventFlags;
#define EVENT_LOOP_EVENT_FLAGS_MASK \
(EventLoopEventFlagsOk | EventLoopEventFlagsUp | EventLoopEventFlagsDown | \
EventLoopEventFlagsLeft | EventLoopEventFlagsRight | EventLoopEventFlagsBack | \
EventLoopEventFlagsExit)
// This function is executed in the GUI context each time an input event occurs (e.g. the user pressed a key)
static void event_loop_event_flags_app_input_callback(InputEvent* event, void* context) {
furi_assert(context);
EventLoopEventFlagsApp* app = context;
UNUSED(app);
if(event->type == InputTypePress) {
if(event->key == InputKeyOk) {
furi_event_flag_set(app->event_flag, EventLoopEventFlagsOk);
} else if(event->key == InputKeyUp) {
furi_event_flag_set(app->event_flag, EventLoopEventFlagsUp);
} else if(event->key == InputKeyDown) {
furi_event_flag_set(app->event_flag, EventLoopEventFlagsDown);
} else if(event->key == InputKeyLeft) {
furi_event_flag_set(app->event_flag, EventLoopEventFlagsLeft);
} else if(event->key == InputKeyRight) {
furi_event_flag_set(app->event_flag, EventLoopEventFlagsRight);
} else if(event->key == InputKeyBack) {
furi_event_flag_set(app->event_flag, EventLoopEventFlagsBack);
}
} else if(event->type == InputTypeLong) {
if(event->key == InputKeyBack) {
furi_event_flag_set(app->event_flag, EventLoopEventFlagsExit);
}
}
}
// This function is executed each time a new event flag is inserted in the input event flag.
static void
event_loop_event_flags_app_event_flags_callback(FuriEventLoopObject* object, void* context) {
furi_assert(context);
EventLoopEventFlagsApp* app = context;
furi_assert(object == app->event_flag);
EventLoopEventFlags events =
furi_event_flag_wait(app->event_flag, EVENT_LOOP_EVENT_FLAGS_MASK, FuriFlagWaitAny, 0);
furi_check((events) != 0);
if(events & EventLoopEventFlagsOk) {
FURI_LOG_I(TAG, "Press \"Ok\"");
}
if(events & EventLoopEventFlagsUp) {
FURI_LOG_I(TAG, "Press \"Up\"");
}
if(events & EventLoopEventFlagsDown) {
FURI_LOG_I(TAG, "Press \"Down\"");
}
if(events & EventLoopEventFlagsLeft) {
FURI_LOG_I(TAG, "Press \"Left\"");
}
if(events & EventLoopEventFlagsRight) {
FURI_LOG_I(TAG, "Press \"Right\"");
}
if(events & EventLoopEventFlagsBack) {
FURI_LOG_I(TAG, "Press \"Back\"");
}
if(events & EventLoopEventFlagsExit) {
FURI_LOG_I(TAG, "Exit App");
furi_event_loop_stop(app->event_loop);
}
}
static EventLoopEventFlagsApp* event_loop_event_flags_app_alloc(void) {
EventLoopEventFlagsApp* app = malloc(sizeof(EventLoopEventFlagsApp));
// Create event loop instances.
app->event_loop = furi_event_loop_alloc();
// Create event flag instances.
app->event_flag = furi_event_flag_alloc();
// Create GUI instance.
app->gui = furi_record_open(RECORD_GUI);
app->view_port = view_port_alloc();
// Gain exclusive access to the input events
view_port_input_callback_set(app->view_port, event_loop_event_flags_app_input_callback, app);
gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);
// Notify the event loop about incoming messages in the event flag
furi_event_loop_subscribe_event_flag(
app->event_loop,
app->event_flag,
FuriEventLoopEventIn | FuriEventLoopEventFlagEdge,
event_loop_event_flags_app_event_flags_callback,
app);
return app;
}
static void event_loop_event_flags_app_free(EventLoopEventFlagsApp* app) {
gui_remove_view_port(app->gui, app->view_port);
furi_record_close(RECORD_GUI);
app->gui = NULL;
// Delete all instances
view_port_free(app->view_port);
app->view_port = NULL;
// IMPORTANT: The user code MUST unsubscribe from all events before deleting the event loop.
// Failure to do so will result in a crash.
furi_event_loop_unsubscribe(app->event_loop, app->event_flag);
furi_event_flag_free(app->event_flag);
app->event_flag = NULL;
furi_event_loop_free(app->event_loop);
app->event_loop = NULL;
free(app);
}
static void event_loop_event_flags_app_run(EventLoopEventFlagsApp* app) {
FURI_LOG_I(TAG, "Press keys to see them printed here.");
FURI_LOG_I(TAG, "Quickly press different keys to generate events.");
FURI_LOG_I(TAG, "Long press \"Back\" to exit app.");
// Run the application event loop. This call will block until the application is about to exit.
furi_event_loop_run(app->event_loop);
}
/*******************************************************************
* vvv START HERE vvv
*
* The application's entry point - referenced in application.fam
*******************************************************************/
int32_t example_event_loop_event_flags_app(void* arg) {
UNUSED(arg);
EventLoopEventFlagsApp* app = event_loop_event_flags_app_alloc();
event_loop_event_flags_app_run(app);
event_loop_event_flags_app_free(app);
return 0;
}

View File

@@ -52,7 +52,7 @@ typedef struct {
*/ */
// This function is executed each time the data is taken out of the stream buffer. It is used to restart the worker timer. // This function is executed each time the data is taken out of the stream buffer. It is used to restart the worker timer.
static bool static void
event_loop_multi_app_stream_buffer_worker_callback(FuriEventLoopObject* object, void* context) { event_loop_multi_app_stream_buffer_worker_callback(FuriEventLoopObject* object, void* context) {
furi_assert(context); furi_assert(context);
EventLoopMultiAppWorker* worker = context; EventLoopMultiAppWorker* worker = context;
@@ -62,8 +62,6 @@ static bool
FURI_LOG_I(TAG, "Data was removed from buffer"); FURI_LOG_I(TAG, "Data was removed from buffer");
// Restart the timer to generate another block of random data. // Restart the timer to generate another block of random data.
furi_event_loop_timer_start(worker->timer, WORKER_DATA_INTERVAL_MS); furi_event_loop_timer_start(worker->timer, WORKER_DATA_INTERVAL_MS);
return true;
} }
// This function is executed when the worker timer expires. The timer will NOT restart automatically // This function is executed when the worker timer expires. The timer will NOT restart automatically
@@ -152,7 +150,7 @@ static void event_loop_multi_app_input_callback(InputEvent* event, void* context
} }
// This function is executed each time new data is available in the stream buffer. // This function is executed each time new data is available in the stream buffer.
static bool static void
event_loop_multi_app_stream_buffer_callback(FuriEventLoopObject* object, void* context) { event_loop_multi_app_stream_buffer_callback(FuriEventLoopObject* object, void* context) {
furi_assert(context); furi_assert(context);
EventLoopMultiApp* app = context; EventLoopMultiApp* app = context;
@@ -172,12 +170,10 @@ static bool
FURI_LOG_I(TAG, "Received data: %s", furi_string_get_cstr(tmp_str)); FURI_LOG_I(TAG, "Received data: %s", furi_string_get_cstr(tmp_str));
furi_string_free(tmp_str); furi_string_free(tmp_str);
return true;
} }
// This function is executed each time a new message is inserted in the input queue. // This function is executed each time a new message is inserted in the input queue.
static bool event_loop_multi_app_input_queue_callback(FuriEventLoopObject* object, void* context) { static void event_loop_multi_app_input_queue_callback(FuriEventLoopObject* object, void* context) {
furi_assert(context); furi_assert(context);
EventLoopMultiApp* app = context; EventLoopMultiApp* app = context;
@@ -222,8 +218,6 @@ static bool event_loop_multi_app_input_queue_callback(FuriEventLoopObject* objec
// Not a long press, just print the key's name. // Not a long press, just print the key's name.
FURI_LOG_I(TAG, "Short press: %s", input_get_key_name(event.key)); FURI_LOG_I(TAG, "Short press: %s", input_get_key_name(event.key));
} }
return true;
} }
// This function is executed each time the countdown timer expires. // This function is executed each time the countdown timer expires.

View File

@@ -59,7 +59,7 @@ static int32_t event_loop_mutex_app_worker_thread(void* context) {
} }
// This function is being run each time when the mutex gets released // This function is being run each time when the mutex gets released
static bool event_loop_mutex_app_event_callback(FuriEventLoopObject* object, void* context) { static void event_loop_mutex_app_event_callback(FuriEventLoopObject* object, void* context) {
furi_assert(context); furi_assert(context);
EventLoopMutexApp* app = context; EventLoopMutexApp* app = context;
@@ -82,8 +82,6 @@ static bool event_loop_mutex_app_event_callback(FuriEventLoopObject* object, voi
MUTEX_EVENT_AND_FLAGS, MUTEX_EVENT_AND_FLAGS,
event_loop_mutex_app_event_callback, event_loop_mutex_app_event_callback,
app); app);
return true;
} }
static EventLoopMutexApp* event_loop_mutex_app_alloc(void) { static EventLoopMutexApp* event_loop_mutex_app_alloc(void) {

View File

@@ -54,7 +54,7 @@ static int32_t event_loop_stream_buffer_app_worker_thread(void* context) {
} }
// This function is being run each time when the number of bytes in the buffer is above its trigger level. // This function is being run each time when the number of bytes in the buffer is above its trigger level.
static bool static void
event_loop_stream_buffer_app_event_callback(FuriEventLoopObject* object, void* context) { event_loop_stream_buffer_app_event_callback(FuriEventLoopObject* object, void* context) {
furi_assert(context); furi_assert(context);
EventLoopStreamBufferApp* app = context; EventLoopStreamBufferApp* app = context;
@@ -76,8 +76,6 @@ static bool
FURI_LOG_I(TAG, "Received data: %s", furi_string_get_cstr(tmp_str)); FURI_LOG_I(TAG, "Received data: %s", furi_string_get_cstr(tmp_str));
furi_string_free(tmp_str); furi_string_free(tmp_str);
return true;
} }
static EventLoopStreamBufferApp* event_loop_stream_buffer_app_alloc(void) { static EventLoopStreamBufferApp* event_loop_stream_buffer_app_alloc(void) {

View File

@@ -218,6 +218,35 @@ App(
sources=["plugins/supported_cards/trt.c"], sources=["plugins/supported_cards/trt.c"],
) )
App(
appid="ndef_ul_parser",
apptype=FlipperAppType.PLUGIN,
cdefines=[("NDEF_PROTO", "NDEF_PROTO_UL")],
entry_point="ndef_plugin_ep",
targets=["f7"],
requires=["nfc"],
sources=["plugins/supported_cards/ndef.c"],
)
App(
appid="ndef_mfc_parser",
apptype=FlipperAppType.PLUGIN,
cdefines=[("NDEF_PROTO", "NDEF_PROTO_MFC")],
entry_point="ndef_plugin_ep",
targets=["f7"],
requires=["nfc"],
sources=["plugins/supported_cards/ndef.c"],
)
App(
appid="ndef_slix_parser",
apptype=FlipperAppType.PLUGIN,
cdefines=[("NDEF_PROTO", "NDEF_PROTO_SLIX")],
entry_point="ndef_plugin_ep",
targets=["f7"],
requires=["nfc"],
sources=["plugins/supported_cards/ndef.c"],
)
App( App(
appid="nfc_start", appid="nfc_start",
targets=["f7"], targets=["f7"],

View File

@@ -139,6 +139,19 @@ static const IdMapping actransit_zones[] = {
}; };
static const size_t kNumACTransitZones = COUNT(actransit_zones); static const size_t kNumACTransitZones = COUNT(actransit_zones);
// Instead of persisting individual Station IDs, Caltrain saves Zone numbers.
// https://www.caltrain.com/stations-zones
static const IdMapping caltrain_zones[] = {
{.id = 0x0001, .name = "Zone 1"},
{.id = 0x0002, .name = "Zone 2"},
{.id = 0x0003, .name = "Zone 3"},
{.id = 0x0004, .name = "Zone 4"},
{.id = 0x0005, .name = "Zone 5"},
{.id = 0x0006, .name = "Zone 6"},
};
static const size_t kNumCaltrainZones = COUNT(caltrain_zones);
// //
// Full agency+zone mapping. // Full agency+zone mapping.
// //
@@ -149,6 +162,7 @@ static const struct {
} agency_zone_map[] = { } agency_zone_map[] = {
{.agency_id = 0x0001, .zone_map = actransit_zones, .zone_count = kNumACTransitZones}, {.agency_id = 0x0001, .zone_map = actransit_zones, .zone_count = kNumACTransitZones},
{.agency_id = 0x0004, .zone_map = bart_zones, .zone_count = kNumBARTZones}, {.agency_id = 0x0004, .zone_map = bart_zones, .zone_count = kNumBARTZones},
{.agency_id = 0x0006, .zone_map = caltrain_zones, .zone_count = kNumCaltrainZones},
{.agency_id = 0x0012, .zone_map = muni_zones, .zone_count = kNumMUNIZones}}; {.agency_id = 0x0012, .zone_map = muni_zones, .zone_count = kNumMUNIZones}};
static const size_t kNumAgencyZoneMaps = COUNT(agency_zone_map); static const size_t kNumAgencyZoneMaps = COUNT(agency_zone_map);

File diff suppressed because it is too large Load Diff

View File

@@ -201,8 +201,9 @@ static bool plantain_read(Nfc* nfc, NfcDevice* device) {
static bool plantain_parse(const NfcDevice* device, FuriString* parsed_data) { static bool plantain_parse(const NfcDevice* device, FuriString* parsed_data) {
furi_assert(device); furi_assert(device);
size_t uid_len = 0;
const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic); const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
const uint8_t* uid = mf_classic_get_uid(data, &uid_len);
bool parsed = false; bool parsed = false;
@@ -220,12 +221,30 @@ static bool plantain_parse(const NfcDevice* device, FuriString* parsed_data) {
if(key != cfg.keys[cfg.data_sector].a) break; if(key != cfg.keys[cfg.data_sector].a) break;
furi_string_printf(parsed_data, "\e#Plantain card\n"); furi_string_printf(parsed_data, "\e#Plantain card\n");
const uint8_t* temp_ptr = &uid[0];
// UID is read from last to first byte
uint8_t card_number_tmp[uid_len];
if(uid_len == 4) {
for(size_t i = 0; i < 4; i++) {
card_number_tmp[i] = temp_ptr[3 - i];
}
} else if(uid_len == 7) {
for(size_t i = 0; i < 7; i++) {
card_number_tmp[i] = temp_ptr[6 - i];
}
} else {
break;
}
//UID is converted to a card number
uint64_t card_number = 0; uint64_t card_number = 0;
for(size_t i = 0; i < 7; i++) { for(size_t i = 0; i < uid_len; i++) {
card_number = (card_number << 8) | data->block[0].data[6 - i]; card_number = (card_number << 8) | card_number_tmp[i];
} }
// Print card number with 4-digit groups // Print card number with 4-digit groups. "3" in "3078" denotes a ticket type "3 - full ticket", will differ on discounted cards.
furi_string_cat_printf(parsed_data, "Number: "); furi_string_cat_printf(parsed_data, "Number: ");
FuriString* card_number_s = furi_string_alloc(); FuriString* card_number_s = furi_string_alloc();
furi_string_cat_printf(card_number_s, "%llu", card_number); furi_string_cat_printf(card_number_s, "%llu", card_number);
@@ -237,6 +256,7 @@ static bool plantain_parse(const NfcDevice* device, FuriString* parsed_data) {
furi_string_push_back(tmp_s, ' '); furi_string_push_back(tmp_s, ' ');
} }
furi_string_cat_printf(parsed_data, "%s\n", furi_string_get_cstr(tmp_s)); furi_string_cat_printf(parsed_data, "%s\n", furi_string_get_cstr(tmp_s));
// this works for 2K Plantain
if(data->type == MfClassicType1k) { if(data->type == MfClassicType1k) {
//balance //balance
uint32_t balance = 0; uint32_t balance = 0;
@@ -290,20 +310,70 @@ static bool plantain_parse(const NfcDevice* device, FuriString* parsed_data) {
last_payment_date.year, last_payment_date.year,
last_payment_date.hour, last_payment_date.hour,
last_payment_date.minute); last_payment_date.minute);
//payment summ //payment amount. This needs to be investigated more, currently it shows incorrect amount on some cards.
uint16_t last_payment = (data->block[18].data[9] << 8) | data->block[18].data[8]; uint16_t last_payment = (data->block[18].data[9] << 8) | data->block[18].data[8];
furi_string_cat_printf(parsed_data, "Amount: %d rub", last_payment / 100); furi_string_cat_printf(parsed_data, "Amount: %d rub", last_payment / 100);
furi_string_free(card_number_s); furi_string_free(card_number_s);
furi_string_free(tmp_s); furi_string_free(tmp_s);
//This is for 4K Plantains.
} else if(data->type == MfClassicType4k) { } else if(data->type == MfClassicType4k) {
//balance
uint32_t balance = 0;
for(uint8_t i = 0; i < 4; i++) {
balance = (balance << 8) | data->block[16].data[3 - i];
}
furi_string_cat_printf(parsed_data, "Balance: %ld rub\n", balance / 100);
//trips //trips
uint8_t trips_metro = data->block[36].data[0]; uint8_t trips_metro = data->block[21].data[0];
uint8_t trips_ground = data->block[36].data[1]; uint8_t trips_ground = data->block[21].data[1];
furi_string_cat_printf(parsed_data, "Trips: %d\n", trips_metro + trips_ground); furi_string_cat_printf(parsed_data, "Trips: %d\n", trips_metro + trips_ground);
//trip time
uint32_t last_trip_timestamp = 0;
for(uint8_t i = 0; i < 3; i++) {
last_trip_timestamp = (last_trip_timestamp << 8) | data->block[21].data[4 - i];
}
DateTime last_trip = {0};
from_minutes_to_datetime(last_trip_timestamp + 24 * 60, &last_trip, 2010);
furi_string_cat_printf(
parsed_data,
"Trip start: %02d.%02d.%04d %02d:%02d\n",
last_trip.day,
last_trip.month,
last_trip.year,
last_trip.hour,
last_trip.minute);
//validator
uint16_t validator = (data->block[20].data[5] << 8) | data->block[20].data[4];
furi_string_cat_printf(parsed_data, "Validator: %d\n", validator);
//tariff
uint16_t fare = (data->block[20].data[7] << 8) | data->block[20].data[6];
furi_string_cat_printf(parsed_data, "Tariff: %d rub\n", fare / 100);
//trips in metro //trips in metro
furi_string_cat_printf(parsed_data, "Trips (Metro): %d\n", trips_metro); furi_string_cat_printf(parsed_data, "Trips (Metro): %d\n", trips_metro);
//trips on ground //trips on ground
furi_string_cat_printf(parsed_data, "Trips (Ground): %d\n", trips_ground); furi_string_cat_printf(parsed_data, "Trips (Ground): %d\n", trips_ground);
//last payment
uint32_t last_payment_timestamp = 0;
for(uint8_t i = 0; i < 3; i++) {
last_payment_timestamp = (last_payment_timestamp << 8) |
data->block[18].data[4 - i];
}
DateTime last_payment_date = {0};
from_minutes_to_datetime(last_payment_timestamp + 24 * 60, &last_payment_date, 2010);
furi_string_cat_printf(
parsed_data,
"Last pay: %02d.%02d.%04d %02d:%02d\n",
last_payment_date.day,
last_payment_date.month,
last_payment_date.year,
last_payment_date.hour,
last_payment_date.minute);
//payment amount
uint16_t last_payment = (data->block[18].data[9] << 8) | data->block[18].data[8];
furi_string_cat_printf(parsed_data, "Amount: %d rub", last_payment / 100);
furi_string_free(card_number_s);
furi_string_free(tmp_s);
} }
parsed = true; parsed = true;
} while(false); } while(false);

File diff suppressed because it is too large Load Diff

View File

@@ -5,8 +5,8 @@
#define TAG "NfcMfClassicDictAttack" #define TAG "NfcMfClassicDictAttack"
// TODO: Fix lag when leaving the dictionary attack view after Hardnested // TODO FL-3926: Fix lag when leaving the dictionary attack view after Hardnested
// TODO: Re-enters backdoor detection between user and system dictionary if no backdoor is found // TODO FL-3926: Re-enters backdoor detection between user and system dictionary if no backdoor is found
typedef enum { typedef enum {
DictAttackStateCUIDDictInProgress, DictAttackStateCUIDDictInProgress,

View File

@@ -1,58 +1,59 @@
Filetype: Flipper SubGhz Keystore File Filetype: Flipper SubGhz Keystore File
Version: 0 Version: 0
Encryption: 1 Encryption: 1
IV: 59 65 73 2C 20 61 6E 64 20 79 6F 75 20 74 6F 6F IV: 43 68 65 63 6B 20 70 61 73 73 65 64 20 4F 77 4F
25F6594BB3FA40C929F9CF43E5E6649A42047B727398F057589EAAC7430169E5 00A92D8AE07E4998E826AF5C89AD659BD8C2BC6A40DA78B1AC05CF5B066243ED
B2D2A6E64C3A13406610C086DBF2D17F0C5643E44CD276D4F8933942B964DAA2 A3C71FA36145D0EE56D78C05DDDDA97E487BCDCA6BDAC2C6F87402A0B20EE3CB
76EE2549A4F62499533856346B3AAF535F248E23802111380D44B70014581E09 B9BDFBD18A63503580C18ABF84101B33D7F720900201510086EB3F0C1F533564
482D1CD99F0F9940195777F482B6EF78B0895FDEAF721544A349B705FCFDF3DE EAB736F80371447008CC3BE3CE952CE429E7BF743A70C7CC62FF415B9E38467B
A83ED14173781D96C7434892D9A092A8658EDDC105FC35FFB1D1C727E0A9FD13F8C07A79F6778D858C0002265C8D49ED 9C50D75C6E4A82F49AF285EC3545E58F8815FF4FCF5C9FFFCF0151FB693413FF13B594C8A28077450C8E561B0272C264
3675A174F916BDCBE056B5C9C7B6461AC885969A140DAB923137F80CEBBBF64D A54C7E6DD1CAE0758F7A123B187C6EDA5BF3789969FE1E5F5A167E2DA7719671
EE55B1DE8D6632FF91C090641032A290C15271E59C6B9E75328B7463F9029B9F 1461891D4AB3500B5BC0859166377CE098C04AAF6FD721F9C58A155F23F8E75A
8A81B9B366DD0F00D1829B3DF62A3EF912B4F492E9D6E99BDC33519C53A9A24D BD1FD5645367BA76D8A87C271FF71E71C407B276BB0B165AF8CF6317250B77A6
1C55C1195C5BFCC502448269211384EF13C979FABE39E4F3251E219F67C8DC4B B18718EA6EB53CCDF9C26FC46E36D17234D93EF578376123B1F3F9953302CF62
11F0FFEB6B9B308DFA9CFA0CD87F5D4EB2D0E48CC9D026C4FAE2C3D06EDE64E4 B633458C1948BC65357904F901F6DD5CD9D795887C176C6AA48E477F0EB693DB
11B3CC20AAD78DE62E835B922006635F6BA8D6D72088AEE51D6B0BA4DF8A87AB9A900F7B9D2AD87F0D9DC091EFFCE41A B1352AC0DE8EBDD838F5DF7E040B062CF8FBB73180F3E712C5B2BDBFE1257A2C18694C51F242BCEA62DF317708771AA1
5E57F7A3369B51636E91471C3873C4017B7694FC074E1620AD288548282201860304FA42804049A974E8D495C9DFDDC2 842294FA0BACCD5B7710F002E40E4BB6142BC7C3125B2E56D4431FD8D6DD1CCE3397A4EB502B9E4248FA68CE8013E93D
62E153886C36AD29DDFBE061800863C12DB1E04E10E082A4FED50BE36ACCF2D1 5379E56AD483C0F870D9446C9CDE65ACA40C3A699D0653EC2F356A076D72D6C2
B0699C0D88A3E91683D90D0B8E2BC78829E12691A9D44B771D0FCC6A8E6E5F34 8FD394F533CD2B03AE247B18F4A5214ED0AF64892F497362129FEF5837012BE4
5039B897208F5224F8147B443BF2E4AECBC4FF6BB8C5BA330094C7B1426E88EF FA8BAB9CD11B306F9FCD924B3A66C678A0316F048A30312B5FA69B9E86EDD8C9
3F1B2AE46A6467BFE35EA4003788A2F437A7AEB45B3EB0B53D4236445165CF93 35D6E447F3B8BA11331805027BCA1D972E9B29E07F1D2993C974DCAF5EE1EB76
CFB07A55170A6CC9DD578E294087FCCE356A5C11D6BDC4728BC3CE28C1AD4E0AF8CE283915464C2BC2BD47CF93B2FAE2 5FBF06E449C471D0AE2DECD141B937666C30EE72273CF58A99E19DCF5D2F69A703F17880B5FB1873480AFA82E2CE7674
EB7F7622C0A1C32B630F923CAB2A8014868212F7B3CE2DA3A81BC0BA11FE26EB 06C1463057544BDB86BD91B3BCC507FD3A5939CFF3315AFD252511FE4F6109EC
ABEBD5C5EF32E24841BDAD3A412F4CF841CB0FBC515E47F73DB55A8E3ABE8854 9AB5C03053AF5836E71FE27D0CD74515C967273EB240B7C37825B94D9D9F08AC
099226A6C87F2E543BEF871491CBB18F4CB3AC96B0629F45D79021183EC35B34C48E03E8C8EA5E11171D6ACC233293D7 66EB1C65F32D5DBDC37B5FF7ACD7C4273B88298E8F0BED08F533F41F38DE651010686B79623804F67398B020E109B9A4
AE78F4F9F800E1A2F96C2A1ADC2655576B723773D533DF2B6A9D98004F607990675648FEA66470FE70E32CCD071D7DEB 7B1846759EDD928EF35B2E8FB7E3FFFE545C9A8B349476A9CE2BDDE55CE0E97F53002A543D0FB738CA67490501629296
AC4065F0A4CD5947DE081FEC2B35F3C1A30A3C2A12908F59B7B4A1C2AA5A1C56 EB61CB652F414A70441ADACA46DBCC78A5AAC7A2ED4527AA2A93995482985867
15791F43991B06D729EAD37E9990D6FCF20F356D03661B4B96A5777D9FE6A7EA 1E119E453C38C3242EFE3D9A3BBB3D257D91D15710C47811C1ADA934515DBD9E
78AADF788C3AE3E619078B0AF71AAB39E125A6046B4FC362D7C8D51EC8C32451 6D8B689C37F7CFD52123BA77B6A4FABB16C3D22BF66FB78B4387BDBC975A3EE5
8B9000CF70198026159D56F7B4A530BCE0E247C00CD4D81EEB59ED52C57FEA03 997A46848917B76A1D728DA8C3A072F16F020AF50070BAD91AC2D841FA9805C1
A41F06335E36DDEED8E77A6773E75C8BB9E255DB48A070DDB63CCA4D8DA00336 C51BFD0B93FECC7234D342F1DA785736A9229A21ACA8C9AA171906AA8916856F
33DB24D54DA53E295467FAB962D863C7015DB706E4AF6248B9B046FB7B9899C2 86188B0DF2C25CA48DC0ED0B4524C17D93585873877FAFBBB55EB24A1D6FDABC
3C4C7C892BAA22AAEF6C92962C67CA0B0F18D8C0A88FD45A4D0A5A166FF1C766 8178B7DFF235094BDCA1E5D72E48B113F2290C0FEBC95CF842013AB9CD84E0AD
9B0850A575A27BA23E82E52EAECC6766B0D5FB3CDCBC60CD970AB90742B5BCFA 25AEF7C490788DCC142CBC96FBCAB598B7EA1D15B7DE4ABBF75DE70832B519E4
9329F044763196FC029639758C29F4186F77AF3DAF237CA434427A712A6BA890 8C5FB85C16C5C833E1D8CE13A9432009D98B0D1639EB0C7C86D0AF4ACD7EB694
35F89F373EA9F7EBAF1DE48BC21E14D7D1B28967C8F65A5D0228CED120ABD06F AC9422E946BB8482CFD808B8E17BE01D9F5D9AB49E1192174A04BB0F032F2182
319423ACE5A80A117C7521A1A0BF22B40181B62BEBDB800AEE139E94FE323298 793EEE939544D18C547E2198FA6F4A72D518C15C14146FC6CAD6AB642A3C9824
3C2D4F8700825C966004993A1191F1573CBF4A407CB7728937ED0E46320D4E9B C910617B5DC2C3137E96AC1869B7C5E90A1585181CC1B585C4CACC2624B7A72A
C1A9BF45B7E5AFE2753D7C8E04C80301EF54FBD9377F1A879C224A6CD0C841C9B8DAFEF2E12B4D0418D36B3AF8AFCBB7 0AAEBC3463300A0391C2B2B14865A68EF44EAE8B1C2462B2730E28B25881B6462B3CCA631DC8A750F97EF5003E6B3060
062E33ADC270AF5111536CADF329D9C78E3CC3FB0538ACF38E9E2A61B5A3B49E 0861D4A3FE5970BB36BF8B525C69009212520A3B79F48ACEC6CB8F6DF96D913F
BC6E26CCC82DB4C4AD1970CDCED894E85F838EFF0701A36BCFCBFA463A7D0482 6327F56523F609D4ABC552912EF808E5919EA42104137AD206EF93AA4B34C097
CDBD96D21F2CD5457863AE80240A435E478FEE7F7130ED22253B9FA77BED9B67 F88B71DAE547754731CD1C45ACBC355E52FE6D7B984A27B454DD7E8BFF12A023
21956DAF6FE1C313DBF310EC05A6EAD9930B6571AAEAB968C26CA4348ACECFE4 824025D56ED8B11BB65722F8C04168767D059BEB156B183D718CFB6AA38B9D81
F05A6468310D49A770A4FFB960EDD2F985646C83C7A7F5160E61CE9BDC578D38 66E60820D0A6C452D9E209FB56FE3F49CAEFCBDC6116177063E0759FA11FDC8F
7F7BEC5B03AFE530746A0F2E8277D17FAA743D678D0FDB69626FFA4AB07504B4 52D6ACA1A6928C52462BABD9A76628B25A2B913FC5BE316A9248C90B6F952529
92204CD4721F426DE2870D47FDDB54771BF8BF766AE2B88C1A2AE1A27ADAEBB6B50BB0D2A64E50725A8613DC702B4645 6FF5E9DAC956EF849FB58C11948C6F00743E157A8C2C631769EFCD80BAA3C048E10E682437EB53ED906C3060CCC7FECC
A3EA461CED048BA13FEFD357C04EC1CFD8C3608D5B5C81577C36B8C7B9A80759 42757F22BDC1059154C41EDEE74E227E6D980F39939D3AA78FEC3F4DC303BE87
960FA991AC2669E25D03AF0B52D04691172A00EECE05DC6DD59D008CEF220780 689C2694447C4FF56C85B60E9FA66CD70A7EB9703A7C0202AD34374E9B290178
EFE29D8222D2C319C93A0601410A1ED88563CD38E3E4E621617AFB72E3C100F3 E1A8E0ACF1E30714A7E580CA6380879C48992BA93A1637F24A66E15A03509B96
C55ADFA945416FFBD013F6B97914B4A902FABBFB4EFBB2583AEE11D93DC87B4D 5E1176A42E6C0DCF80E7B445372E3C054875A39D5F7A8B9C35985EDB46F65BB0
6D462090B80489D07337E034F0F27C881066321C58C553EDCBCE593A50B96A31 12A6349CB3EC6D79B3C85052D6155730A036AF7F54BCD8CC9EC8F36557005D5E
C2DB9833D0810D023E548EF4BEC218D916DB005EF2BE1122AE4BC756EBE4AFBF B6D3D039D8B6D6F298665B16EBCB908D5F64D37C31F583C4F660AC0535DEC2DD
D5E0EC7C772A9E39973EF35DA201C661BCD182DA2AAE3E3706781191666B7B4F 327BEE9B3E7A989B49787DE9D0C573C97343D79527FA6F442F8BDA0B9336E666
371463A5E41EEEA81D25217FDB97DACABC0B1892E0A5B36825B83330FEB900D3D89915D7B9AD5B3C3550C84384DF242E 1BDEDA4766FB3EFC1867836267E632B12020262B1EABA4BD31F9AB20DE49632A
BEE891192062A5AA254B6E3AD27D006A650D947EDF9010F6A78C78A4443F1D0C 8DE80C00CAD7F6E555713414DE8CE56B1C8603777F38F553F528907FA25E153EA5D3459739C66B46F6854A75F19A1511
D56C5498E65B3E2FE6B1A3C25B4D47650DD9E1BAE04AA9CD468026854D5B231B 3A07DD2876F794FDF920F5D7B50A6D15C618FCF02F064E5AD5871C5B098EE8EC
2CFF607EE43B721D9B6F564CF70034920716392BAD686A95518CD02ACFE6D232 66B7166156874006B35AF6997F61B84F016868FBAD283304B256F3DA065E65E3
2FABCA8EA65BF98CB8236AD4301B8D1C474FBB1F33B084C736C323A83B6DE336 1E6D841D11C065F88B3F52EF60862E579E717E6AEFABF6A79FCF90D0810B35BB
07AF12E594D474483BB6DB6CD2F7F722934DF2B9BECE2F0761FED2ECA2738C17FA8B03195A632D5395E9D2F4F8B84C91 B3B168E2532A6B659503736AA8933FB01A88DB9EA339C1C19EF7566600312B26
974628362FD1951C207BDC3F5363F2747BAFA14ED7155772ADAA2FE8D1F7DC702F8DDB8CB4C17DBA803EC0DB6CDA43AC

View File

@@ -212,7 +212,7 @@ static void dolphin_update_clear_limits_timer_period(void* context) {
FURI_LOG_D(TAG, "Daily limits reset in %lu ms", time_to_clear_limits); FURI_LOG_D(TAG, "Daily limits reset in %lu ms", time_to_clear_limits);
} }
static bool dolphin_process_event(FuriEventLoopObject* object, void* context) { static void dolphin_process_event(FuriEventLoopObject* object, void* context) {
UNUSED(object); UNUSED(object);
Dolphin* dolphin = context; Dolphin* dolphin = context;
@@ -264,8 +264,6 @@ static bool dolphin_process_event(FuriEventLoopObject* object, void* context) {
} }
dolphin_event_release(&event); dolphin_event_release(&event);
return true;
} }
static void dolphin_storage_callback(const void* message, void* context) { static void dolphin_storage_callback(const void* message, void* context) {

View File

@@ -376,7 +376,7 @@ void view_dispatcher_update(View* view, void* context) {
} }
} }
bool view_dispatcher_run_event_callback(FuriEventLoopObject* object, void* context) { void view_dispatcher_run_event_callback(FuriEventLoopObject* object, void* context) {
furi_assert(context); furi_assert(context);
ViewDispatcher* instance = context; ViewDispatcher* instance = context;
furi_assert(instance->event_queue == object); furi_assert(instance->event_queue == object);
@@ -384,11 +384,9 @@ bool view_dispatcher_run_event_callback(FuriEventLoopObject* object, void* conte
uint32_t event; uint32_t event;
furi_check(furi_message_queue_get(instance->event_queue, &event, 0) == FuriStatusOk); furi_check(furi_message_queue_get(instance->event_queue, &event, 0) == FuriStatusOk);
view_dispatcher_handle_custom_event(instance, event); view_dispatcher_handle_custom_event(instance, event);
return true;
} }
bool view_dispatcher_run_input_callback(FuriEventLoopObject* object, void* context) { void view_dispatcher_run_input_callback(FuriEventLoopObject* object, void* context) {
furi_assert(context); furi_assert(context);
ViewDispatcher* instance = context; ViewDispatcher* instance = context;
furi_assert(instance->input_queue == object); furi_assert(instance->input_queue == object);
@@ -396,6 +394,4 @@ bool view_dispatcher_run_input_callback(FuriEventLoopObject* object, void* conte
InputEvent input; InputEvent input;
furi_check(furi_message_queue_get(instance->input_queue, &input, 0) == FuriStatusOk); furi_check(furi_message_queue_get(instance->input_queue, &input, 0) == FuriStatusOk);
view_dispatcher_handle_input(instance, &input); view_dispatcher_handle_input(instance, &input);
return true;
} }

View File

@@ -57,7 +57,7 @@ void view_dispatcher_set_current_view(ViewDispatcher* view_dispatcher, View* vie
void view_dispatcher_update(View* view, void* context); void view_dispatcher_update(View* view, void* context);
/** ViewDispatcher run event loop event callback */ /** ViewDispatcher run event loop event callback */
bool view_dispatcher_run_event_callback(FuriEventLoopObject* object, void* context); void view_dispatcher_run_event_callback(FuriEventLoopObject* object, void* context);
/** ViewDispatcher run event loop input callback */ /** ViewDispatcher run event loop input callback */
bool view_dispatcher_run_input_callback(FuriEventLoopObject* object, void* context); void view_dispatcher_run_input_callback(FuriEventLoopObject* object, void* context);

View File

@@ -593,3 +593,7 @@ const NotificationSequence sequence_lcd_contrast_update = {
&message_lcd_contrast_update, &message_lcd_contrast_update,
NULL, NULL,
}; };
const NotificationSequence sequence_empty = {
NULL,
};

View File

@@ -145,6 +145,9 @@ extern const NotificationSequence sequence_audiovisual_alert;
// LCD // LCD
extern const NotificationSequence sequence_lcd_contrast_update; extern const NotificationSequence sequence_lcd_contrast_update;
// Wait for notification queue become empty
extern const NotificationSequence sequence_empty;
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@@ -191,7 +191,7 @@ static void power_handle_reboot(PowerBootMode mode) {
furi_hal_power_reset(); furi_hal_power_reset();
} }
static bool power_message_callback(FuriEventLoopObject* object, void* context) { static void power_message_callback(FuriEventLoopObject* object, void* context) {
furi_assert(context); furi_assert(context);
Power* power = context; Power* power = context;
@@ -223,8 +223,6 @@ static bool power_message_callback(FuriEventLoopObject* object, void* context) {
if(msg.lock) { if(msg.lock) {
api_lock_unlock(msg.lock); api_lock_unlock(msg.lock);
} }
return true;
} }
static void power_tick_callback(void* context) { static void power_tick_callback(void* context) {

View File

@@ -5,6 +5,7 @@ App(
provides=[ provides=[
"passport", "passport",
"system_settings", "system_settings",
"clock_settings",
"about", "about",
], ],
) )

View File

@@ -0,0 +1,17 @@
App(
appid="clock_settings",
name="Clock & Alarm",
apptype=FlipperAppType.SETTINGS,
entry_point="clock_settings",
requires=["gui"],
provides=["clock_settings_start"],
stack_size=1 * 1024,
order=90,
)
App(
appid="clock_settings_start",
apptype=FlipperAppType.STARTUP,
entry_point="clock_settings_start",
order=1000,
)

View File

@@ -0,0 +1,71 @@
#include "clock_settings.h"
#include <furi.h>
#include <furi_hal.h>
static bool clock_settings_custom_event_callback(void* context, uint32_t event) {
furi_assert(context);
ClockSettings* app = context;
return scene_manager_handle_custom_event(app->scene_manager, event);
}
static bool clock_settings_back_event_callback(void* context) {
furi_assert(context);
ClockSettings* app = context;
return scene_manager_handle_back_event(app->scene_manager);
}
ClockSettings* clock_settings_alloc() {
ClockSettings* app = malloc(sizeof(ClockSettings));
app->gui = furi_record_open(RECORD_GUI);
app->view_dispatcher = view_dispatcher_alloc();
app->scene_manager = scene_manager_alloc(&clock_settings_scene_handlers, app);
view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
view_dispatcher_set_custom_event_callback(
app->view_dispatcher, clock_settings_custom_event_callback);
view_dispatcher_set_navigation_event_callback(
app->view_dispatcher, clock_settings_back_event_callback);
view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
app->pwm_view =
clock_settings_module_alloc(view_dispatcher_get_event_loop(app->view_dispatcher));
view_dispatcher_add_view(
app->view_dispatcher, ClockSettingsViewPwm, clock_settings_module_get_view(app->pwm_view));
scene_manager_next_scene(app->scene_manager, ClockSettingsSceneStart);
return app;
}
void clock_settings_free(ClockSettings* app) {
furi_assert(app);
// Views
view_dispatcher_remove_view(app->view_dispatcher, ClockSettingsViewPwm);
clock_settings_module_free(app->pwm_view);
// View dispatcher
view_dispatcher_free(app->view_dispatcher);
scene_manager_free(app->scene_manager);
// Close records
furi_record_close(RECORD_GUI);
free(app);
}
int32_t clock_settings(void* p) {
UNUSED(p);
ClockSettings* clock_settings = clock_settings_alloc();
view_dispatcher_run(clock_settings->view_dispatcher);
clock_settings_free(clock_settings);
return 0;
}

View File

@@ -0,0 +1,31 @@
#pragma once
#include "scenes/clock_settings_scene.h"
#include <furi_hal_clock.h>
#include <furi_hal_pwm.h>
#include <gui/gui.h>
#include <gui/view_dispatcher.h>
#include <gui/scene_manager.h>
#include <gui/modules/submenu.h>
#include <gui/modules/variable_item_list.h>
#include <gui/modules/submenu.h>
#include "views/clock_settings_module.h"
typedef struct ClockSettings ClockSettings;
struct ClockSettings {
Gui* gui;
ViewDispatcher* view_dispatcher;
SceneManager* scene_manager;
ClockSettingsModule* pwm_view;
};
typedef enum {
ClockSettingsViewPwm,
} ClockSettingsView;
typedef enum {
ClockSettingsCustomEventNone,
} ClockSettingsCustomEvent;

View File

@@ -0,0 +1,177 @@
#include <furi.h>
#include <furi_hal.h>
#include <gui/gui.h>
#include <gui/view_port.h>
#include <notification/notification.h>
#include <notification/notification_messages.h>
#include <assets_icons.h>
#define TAG "ClockSettingsAlarm"
typedef struct {
DateTime now;
IconAnimation* icon;
} ClockSettingsAlramModel;
const NotificationSequence sequence_alarm = {
&message_force_speaker_volume_setting_1f,
&message_force_vibro_setting_on,
&message_force_display_brightness_setting_1f,
&message_vibro_on,
&message_display_backlight_on,
&message_note_c7,
&message_delay_250,
&message_display_backlight_off,
&message_note_c4,
&message_delay_250,
&message_display_backlight_on,
&message_note_c7,
&message_delay_250,
&message_display_backlight_off,
&message_note_c4,
&message_delay_250,
&message_sound_off,
&message_vibro_off,
NULL,
};
static void clock_settings_alarm_draw_callback(Canvas* canvas, void* ctx) {
ClockSettingsAlramModel* model = ctx;
char buffer[64] = {};
canvas_draw_icon_animation(canvas, 5, 6, model->icon);
canvas_set_font(canvas, FontBigNumbers);
snprintf(buffer, sizeof(buffer), "%02u:%02u", model->now.hour, model->now.minute);
canvas_draw_str(canvas, 58, 32, buffer);
canvas_set_font(canvas, FontPrimary);
snprintf(
buffer,
sizeof(buffer),
"%02u.%02u.%04u",
model->now.day,
model->now.month,
model->now.year);
canvas_draw_str(canvas, 60, 44, buffer);
}
static void clock_settings_alarm_input_callback(InputEvent* input_event, void* ctx) {
furi_assert(ctx);
FuriMessageQueue* event_queue = ctx;
furi_message_queue_put(event_queue, input_event, FuriWaitForever);
}
void clock_settings_alarm_animation_callback(IconAnimation* instance, void* context) {
UNUSED(instance);
ViewPort* view_port = context;
view_port_update(view_port);
}
int32_t clock_settings_alarm(void* p) {
UNUSED(p);
// View Model
ClockSettingsAlramModel model;
furi_hal_rtc_get_datetime(&model.now);
model.icon = icon_animation_alloc(&A_Alarm_47x39);
// Alloc message queue
FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
// Configure view port
ViewPort* view_port = view_port_alloc();
view_port_draw_callback_set(view_port, clock_settings_alarm_draw_callback, &model);
view_port_input_callback_set(view_port, clock_settings_alarm_input_callback, event_queue);
// Register view port in GUI
Gui* gui = furi_record_open(RECORD_GUI);
gui_add_view_port(gui, view_port, GuiLayerFullscreen);
NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
notification_message(notification, &sequence_alarm);
icon_animation_set_update_callback(
model.icon, clock_settings_alarm_animation_callback, view_port);
icon_animation_start(model.icon);
// Process events
InputEvent event;
bool running = true;
while(running) {
if(furi_message_queue_get(event_queue, &event, 2000) == FuriStatusOk) {
if(event.type == InputTypePress) {
running = false;
}
} else {
notification_message(notification, &sequence_alarm);
furi_hal_rtc_get_datetime(&model.now);
view_port_update(view_port);
}
}
icon_animation_stop(model.icon);
notification_message_block(notification, &sequence_empty);
furi_record_close(RECORD_NOTIFICATION);
view_port_enabled_set(view_port, false);
gui_remove_view_port(gui, view_port);
view_port_free(view_port);
furi_message_queue_free(event_queue);
furi_record_close(RECORD_GUI);
icon_animation_free(model.icon);
return 0;
}
FuriThread* clock_settings_alarm_thread = NULL;
static void clock_settings_alarm_thread_state_callback(
FuriThread* thread,
FuriThreadState state,
void* context) {
furi_assert(clock_settings_alarm_thread == thread);
UNUSED(context);
if(state == FuriThreadStateStopped) {
furi_thread_free(thread);
clock_settings_alarm_thread = NULL;
}
}
static void clock_settings_alarm_start(void* context, uint32_t arg) {
UNUSED(context);
UNUSED(arg);
FURI_LOG_I(TAG, "spawning alarm thread");
if(clock_settings_alarm_thread) return;
clock_settings_alarm_thread =
furi_thread_alloc_ex("ClockAlarm", 1024, clock_settings_alarm, NULL);
furi_thread_set_state_callback(
clock_settings_alarm_thread, clock_settings_alarm_thread_state_callback);
furi_thread_start(clock_settings_alarm_thread);
}
static void clock_settings_alarm_isr(void* context) {
UNUSED(context);
furi_timer_pending_callback(clock_settings_alarm_start, NULL, 0);
}
void clock_settings_start(void) {
#ifndef FURI_RAM_EXEC
furi_hal_rtc_set_alarm_callback(clock_settings_alarm_isr, NULL);
#endif
}

View File

@@ -0,0 +1,30 @@
#include "../clock_settings.h"
// Generate scene on_enter handlers array
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
void (*const clock_settings_scene_on_enter_handlers[])(void*) = {
#include "clock_settings_scene_config.h"
};
#undef ADD_SCENE
// Generate scene on_event handlers array
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
bool (*const clock_settings_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = {
#include "clock_settings_scene_config.h"
};
#undef ADD_SCENE
// Generate scene on_exit handlers array
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
void (*const clock_settings_scene_on_exit_handlers[])(void* context) = {
#include "clock_settings_scene_config.h"
};
#undef ADD_SCENE
// Initialize scene handlers configuration structure
const SceneManagerHandlers clock_settings_scene_handlers = {
.on_enter_handlers = clock_settings_scene_on_enter_handlers,
.on_event_handlers = clock_settings_scene_on_event_handlers,
.on_exit_handlers = clock_settings_scene_on_exit_handlers,
.scene_num = ClockSettingsSceneNum,
};

View File

@@ -0,0 +1,29 @@
#pragma once
#include <gui/scene_manager.h>
// Generate scene id and total number
#define ADD_SCENE(prefix, name, id) ClockSettingsScene##id,
typedef enum {
#include "clock_settings_scene_config.h"
ClockSettingsSceneNum,
} ClockSettingsScene;
#undef ADD_SCENE
extern const SceneManagerHandlers clock_settings_scene_handlers;
// Generate scene on_enter handlers declaration
#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
#include "clock_settings_scene_config.h"
#undef ADD_SCENE
// Generate scene on_event handlers declaration
#define ADD_SCENE(prefix, name, id) \
bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event);
#include "clock_settings_scene_config.h"
#undef ADD_SCENE
// Generate scene on_exit handlers declaration
#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context);
#include "clock_settings_scene_config.h"
#undef ADD_SCENE

View File

@@ -0,0 +1 @@
ADD_SCENE(clock_settings, start, Start)

View File

@@ -0,0 +1,32 @@
#include "../clock_settings.h"
#include <furi_hal.h>
#define TAG "SceneStart"
typedef enum {
SubmenuIndexPwm,
SubmenuIndexClockOutput,
} SubmenuIndex;
void clock_settings_scene_start_submenu_callback(void* context, uint32_t index) {
ClockSettings* app = context;
view_dispatcher_send_custom_event(app->view_dispatcher, index);
}
void clock_settings_scene_start_on_enter(void* context) {
ClockSettings* app = context;
view_dispatcher_switch_to_view(app->view_dispatcher, ClockSettingsViewPwm);
}
bool clock_settings_scene_start_on_event(void* context, SceneManagerEvent event) {
UNUSED(context);
UNUSED(event);
return false;
}
void clock_settings_scene_start_on_exit(void* context) {
UNUSED(context);
}

View File

@@ -0,0 +1,438 @@
#include "clock_settings_module.h"
#include <gui/elements.h>
#include <assets_icons.h>
#include <locale/locale.h>
#define TAG "ClockSettingsModule"
struct ClockSettingsModule {
FuriEventLoopTimer* timer;
View* view;
};
typedef struct {
DateTime current;
DateTime alarm;
bool alarm_enabled;
bool editing;
uint8_t row;
uint8_t column;
} ClockSettingsModuleViewModel;
typedef enum {
EditStateNone,
EditStateActive,
EditStateActiveEditing,
} EditState;
#define get_state(m, r, c) \
((m)->row == (r) && (m)->column == (c) ? \
((m)->editing ? EditStateActiveEditing : EditStateActive) : \
EditStateNone)
#define ROW_0_Y (4)
#define ROW_0_H (20)
#define ROW_1_Y (30)
#define ROW_1_H (12)
#define ROW_2_Y (48)
#define ROW_2_H (12)
#define ROW_COUNT 3
#define COLUMN_COUNT 3
static inline void clock_settings_module_cleanup_date(DateTime* dt) {
uint8_t day_per_month =
datetime_get_days_per_month(datetime_is_leap_year(dt->year), dt->month);
if(dt->day > day_per_month) {
dt->day = day_per_month;
}
}
static inline void clock_settings_module_draw_block(
Canvas* canvas,
int32_t x,
int32_t y,
size_t w,
size_t h,
Font font,
EditState state,
const char* text) {
canvas_set_color(canvas, ColorBlack);
if(state != EditStateNone) {
if(state == EditStateActiveEditing) {
canvas_draw_icon(canvas, x + w / 2 - 2, y - 1 - 3, &I_SmallArrowUp_3x5);
canvas_draw_icon(canvas, x + w / 2 - 2, y + h + 1, &I_SmallArrowDown_3x5);
}
canvas_draw_rbox(canvas, x, y, w, h, 1);
canvas_set_color(canvas, ColorWhite);
} else {
canvas_draw_rframe(canvas, x, y, w, h, 1);
}
canvas_set_font(canvas, font);
canvas_draw_str_aligned(canvas, x + w / 2, y + h / 2, AlignCenter, AlignCenter, text);
if(state != EditStateNone) {
canvas_set_color(canvas, ColorBlack);
}
}
static void
clock_settings_module_draw_time_callback(Canvas* canvas, ClockSettingsModuleViewModel* model) {
char buffer[64];
canvas_set_font(canvas, FontPrimary);
canvas_draw_str(canvas, 0, ROW_0_Y + 15, "Time");
snprintf(buffer, sizeof(buffer), "%02u", model->current.hour);
clock_settings_module_draw_block(
canvas, 32, ROW_0_Y, 28, ROW_0_H, FontBigNumbers, get_state(model, 0, 0), buffer);
canvas_draw_box(canvas, 62, ROW_0_Y + ROW_0_H - 7, 2, 2);
canvas_draw_box(canvas, 62, ROW_0_Y + ROW_0_H - 7 - 6, 2, 2);
snprintf(buffer, sizeof(buffer), "%02u", model->current.minute);
clock_settings_module_draw_block(
canvas, 66, ROW_0_Y, 28, ROW_0_H, FontBigNumbers, get_state(model, 0, 1), buffer);
canvas_draw_box(canvas, 96, ROW_0_Y + ROW_0_H - 7, 2, 2);
canvas_draw_box(canvas, 96, ROW_0_Y + ROW_0_H - 7 - 6, 2, 2);
snprintf(buffer, sizeof(buffer), "%02u", model->current.second);
clock_settings_module_draw_block(
canvas, 100, ROW_0_Y, 28, ROW_0_H, FontBigNumbers, get_state(model, 0, 2), buffer);
}
static void
clock_settings_module_draw_date_callback(Canvas* canvas, ClockSettingsModuleViewModel* model) {
char buffer[64];
canvas_set_font(canvas, FontPrimary);
canvas_draw_str(canvas, 0, ROW_1_Y + 9, "Date");
// Day
snprintf(buffer, sizeof(buffer), "%02u", model->current.day);
clock_settings_module_draw_block(
canvas, 44, ROW_1_Y, 17, ROW_1_H, FontPrimary, get_state(model, 1, 0), buffer);
canvas_draw_box(canvas, 71 - 6, ROW_1_Y + ROW_1_H - 4, 2, 2);
// Month
snprintf(buffer, sizeof(buffer), "%02u", model->current.month);
clock_settings_module_draw_block(
canvas, 71, ROW_1_Y, 17, ROW_1_H, FontPrimary, get_state(model, 1, 1), buffer);
canvas_draw_box(canvas, 98 - 6, ROW_1_Y + ROW_1_H - 4, 2, 2);
// Year
snprintf(buffer, sizeof(buffer), "%04u", model->current.year);
clock_settings_module_draw_block(
canvas, 98, ROW_1_Y, 30, ROW_1_H, FontPrimary, get_state(model, 1, 2), buffer);
}
static void
clock_settings_module_draw_alarm_callback(Canvas* canvas, ClockSettingsModuleViewModel* model) {
char buffer[64];
canvas_set_font(canvas, FontPrimary);
canvas_draw_str(canvas, 0, ROW_2_Y + 9, "Alarm");
snprintf(buffer, sizeof(buffer), "%02u", model->alarm.hour);
clock_settings_module_draw_block(
canvas, 58, ROW_2_Y, 17, ROW_2_H, FontPrimary, get_state(model, 2, 0), buffer);
canvas_draw_box(canvas, 81 - 4, ROW_2_Y + ROW_2_H - 4, 2, 2);
canvas_draw_box(canvas, 81 - 4, ROW_2_Y + ROW_2_H - 4 - 4, 2, 2);
snprintf(buffer, sizeof(buffer), "%02u", model->alarm.minute);
clock_settings_module_draw_block(
canvas, 81, ROW_2_Y, 17, ROW_2_H, FontPrimary, get_state(model, 2, 1), buffer);
clock_settings_module_draw_block(
canvas,
106,
ROW_2_Y,
22,
ROW_2_H,
FontPrimary,
get_state(model, 2, 2),
model->alarm_enabled ? "On" : "Off");
}
static void clock_settings_module_draw_callback(Canvas* canvas, void* _model) {
ClockSettingsModuleViewModel* model = _model;
clock_settings_module_draw_time_callback(canvas, model);
clock_settings_module_draw_date_callback(canvas, model);
clock_settings_module_draw_alarm_callback(canvas, model);
}
static bool clock_settings_module_input_navigation_callback(
InputEvent* event,
ClockSettingsModuleViewModel* model) {
if(event->key == InputKeyUp) {
if(model->row > 0) model->row--;
} else if(event->key == InputKeyDown) {
if(model->row < ROW_COUNT - 1) model->row++;
} else if(event->key == InputKeyOk) {
model->editing = !model->editing;
} else if(event->key == InputKeyRight) {
if(model->column < COLUMN_COUNT - 1) model->column++;
} else if(event->key == InputKeyLeft) {
if(model->column > 0) model->column--;
} else if(event->key == InputKeyBack && model->editing) {
model->editing = false;
} else {
return false;
}
return true;
}
static bool clock_settings_module_input_time_callback(
InputEvent* event,
ClockSettingsModuleViewModel* model) {
if(event->key == InputKeyUp) {
if(model->column == 0) {
model->current.hour++;
model->current.hour = model->current.hour % 24;
} else if(model->column == 1) {
model->current.minute++;
model->current.minute = model->current.minute % 60;
} else if(model->column == 2) {
model->current.second++;
model->current.second = model->current.second % 60;
} else {
furi_crash();
}
} else if(event->key == InputKeyDown) {
if(model->column == 0) {
if(model->current.hour > 0) {
model->current.hour--;
} else {
model->current.hour = 23;
}
model->current.hour = model->current.hour % 24;
} else if(model->column == 1) {
if(model->current.minute > 0) {
model->current.minute--;
} else {
model->current.minute = 59;
}
model->current.minute = model->current.minute % 60;
} else if(model->column == 2) {
if(model->current.second > 0) {
model->current.second--;
} else {
model->current.second = 59;
}
model->current.second = model->current.second % 60;
} else {
furi_crash();
}
} else {
return clock_settings_module_input_navigation_callback(event, model);
}
return true;
}
static bool clock_settings_module_input_date_callback(
InputEvent* event,
ClockSettingsModuleViewModel* model) {
if(event->key == InputKeyUp) {
if(model->column == 0) {
if(model->current.day < 31) model->current.day++;
} else if(model->column == 1) {
if(model->current.month < 12) {
model->current.month++;
}
} else if(model->column == 2) {
if(model->current.year < 2099) {
model->current.year++;
}
} else {
furi_crash();
}
} else if(event->key == InputKeyDown) {
if(model->column == 0) {
if(model->current.day > 1) {
model->current.day--;
}
} else if(model->column == 1) {
if(model->current.month > 1) {
model->current.month--;
}
} else if(model->column == 2) {
if(model->current.year > 2000) {
model->current.year--;
}
} else {
furi_crash();
}
} else {
return clock_settings_module_input_navigation_callback(event, model);
}
clock_settings_module_cleanup_date(&model->current);
return true;
}
static bool clock_settings_module_input_alarm_callback(
InputEvent* event,
ClockSettingsModuleViewModel* model) {
if(event->key == InputKeyUp) {
if(model->column == 0) {
model->alarm.hour++;
model->alarm.hour = model->alarm.hour % 24;
} else if(model->column == 1) {
model->alarm.minute++;
model->alarm.minute = model->alarm.minute % 60;
} else if(model->column == 2) {
model->alarm_enabled = !model->alarm_enabled;
} else {
furi_crash();
}
} else if(event->key == InputKeyDown) {
if(model->column == 0) {
if(model->alarm.hour > 0) {
model->alarm.hour--;
} else {
model->alarm.hour = 23;
}
model->alarm.hour = model->alarm.hour % 24;
} else if(model->column == 1) {
if(model->alarm.minute > 0) {
model->alarm.minute--;
} else {
model->alarm.minute = 59;
}
model->alarm.minute = model->alarm.minute % 60;
} else if(model->column == 2) {
model->alarm_enabled = !model->alarm_enabled;
} else {
furi_crash();
}
} else {
return clock_settings_module_input_navigation_callback(event, model);
}
return true;
}
static bool clock_settings_module_input_callback(InputEvent* event, void* context) {
furi_assert(context);
ClockSettingsModule* instance = context;
bool consumed = false;
with_view_model(
instance->view,
ClockSettingsModuleViewModel * model,
{
if(event->type == InputTypeShort || event->type == InputTypeRepeat) {
bool previous_editing = model->editing;
if(model->editing) {
if(model->row == 0) {
consumed = clock_settings_module_input_time_callback(event, model);
} else if(model->row == 1) {
consumed = clock_settings_module_input_date_callback(event, model);
} else if(model->row == 2) {
consumed = clock_settings_module_input_alarm_callback(event, model);
} else {
furi_crash();
}
} else {
consumed = clock_settings_module_input_navigation_callback(event, model);
}
// Switching between navigate/edit
if(model->editing != previous_editing) {
if(model->row == 2) {
if(!model->editing) {
// Disable alarm
furi_hal_rtc_set_alarm(NULL, false);
// Set new alarm
furi_hal_rtc_set_alarm(&model->alarm, model->alarm_enabled);
// Confirm
model->alarm_enabled = furi_hal_rtc_get_alarm(&model->alarm);
}
} else {
if(model->editing) {
// stop timer to prevent mess with current date time
furi_event_loop_timer_stop(instance->timer);
} else {
// save date time and restart timer
furi_hal_rtc_set_datetime(&model->current);
furi_event_loop_timer_start(instance->timer, 1000);
}
}
}
}
},
true);
return consumed;
}
static void clock_settings_module_timer_callback(void* context) {
furi_assert(context);
ClockSettingsModule* instance = context;
DateTime dt;
furi_hal_rtc_get_datetime(&dt);
with_view_model(
instance->view, ClockSettingsModuleViewModel * model, { model->current = dt; }, true);
}
static void clock_settings_module_view_enter_callback(void* context) {
furi_assert(context);
ClockSettingsModule* instance = context;
clock_settings_module_timer_callback(context);
DateTime alarm;
bool enabled = furi_hal_rtc_get_alarm(&alarm);
with_view_model(
instance->view,
ClockSettingsModuleViewModel * model,
{
model->alarm = alarm;
model->alarm_enabled = enabled;
},
true);
furi_event_loop_timer_start(instance->timer, 1000);
}
static void clock_settings_module_view_exit_callback(void* context) {
furi_assert(context);
ClockSettingsModule* instance = context;
furi_event_loop_timer_stop(instance->timer);
}
ClockSettingsModule* clock_settings_module_alloc(FuriEventLoop* event_loop) {
ClockSettingsModule* instance = malloc(sizeof(ClockSettingsModule));
instance->timer = furi_event_loop_timer_alloc(
event_loop, clock_settings_module_timer_callback, FuriEventLoopTimerTypePeriodic, instance);
instance->view = view_alloc();
view_set_enter_callback(instance->view, clock_settings_module_view_enter_callback);
view_set_exit_callback(instance->view, clock_settings_module_view_exit_callback);
view_allocate_model(
instance->view, ViewModelTypeLocking, sizeof(ClockSettingsModuleViewModel));
with_view_model(
instance->view, ClockSettingsModuleViewModel * model, { model->row = 0; }, false);
view_set_context(instance->view, instance);
view_set_draw_callback(instance->view, clock_settings_module_draw_callback);
view_set_input_callback(instance->view, clock_settings_module_input_callback);
return instance;
}
void clock_settings_module_free(ClockSettingsModule* instance) {
furi_assert(instance);
view_free(instance->view);
free(instance);
}
View* clock_settings_module_get_view(ClockSettingsModule* instance) {
furi_assert(instance);
return instance->view;
}

View File

@@ -0,0 +1,24 @@
#pragma once
#include <furi_hal.h>
#include <gui/view.h>
typedef struct ClockSettingsModule ClockSettingsModule;
typedef void (*ClockSettingsModuleViewCallback)(
uint8_t channel_id,
uint32_t freq,
uint8_t duty,
void* context);
ClockSettingsModule* clock_settings_module_alloc(FuriEventLoop* event_loop);
void clock_settings_module_free(ClockSettingsModule* instance);
View* clock_settings_module_get_view(ClockSettingsModule* instance);
void clock_settings_module_set(
ClockSettingsModule* instance,
const DateTime* datetime,
bool enabled);
bool clock_settings_module_get(ClockSettingsModule* instance, DateTime* datetime);

View File

@@ -5,8 +5,8 @@ App(
provides=[ provides=[
"updater_app", "updater_app",
"js_app", "js_app",
"js_app_start",
"mfkey", "mfkey",
"picopass",
# "archive", # "archive",
], ],
) )

View File

@@ -6,6 +6,16 @@ App(
stack_size=2 * 1024, stack_size=2 * 1024,
resources="examples", resources="examples",
order=0, order=0,
provides=["js_app_start"],
sources=[
"js_app.c",
"js_modules.c",
"js_thread.c",
"plugin_api/app_api_table.cpp",
"views/console_view.c",
"modules/js_flipper.c",
"modules/js_tests.c",
],
) )
App( App(
@@ -13,6 +23,7 @@ App(
apptype=FlipperAppType.STARTUP, apptype=FlipperAppType.STARTUP,
entry_point="js_app_on_system_start", entry_point="js_app_on_system_start",
order=160, order=160,
sources=["js_app.c"],
) )
App( App(
@@ -30,7 +41,7 @@ App(
appid="js_gui", appid="js_gui",
apptype=FlipperAppType.PLUGIN, apptype=FlipperAppType.PLUGIN,
entry_point="js_gui_ep", entry_point="js_gui_ep",
requires=["js_app", "js_event_loop"], requires=["js_app"],
sources=["modules/js_gui/js_gui.c", "modules/js_gui/js_gui_api_table.cpp"], sources=["modules/js_gui/js_gui.c", "modules/js_gui/js_gui_api_table.cpp"],
) )
@@ -38,7 +49,7 @@ App(
appid="js_gui__loading", appid="js_gui__loading",
apptype=FlipperAppType.PLUGIN, apptype=FlipperAppType.PLUGIN,
entry_point="js_view_loading_ep", entry_point="js_view_loading_ep",
requires=["js_app", "js_gui", "js_event_loop"], requires=["js_app"],
sources=["modules/js_gui/loading.c"], sources=["modules/js_gui/loading.c"],
) )
@@ -46,7 +57,7 @@ App(
appid="js_gui__empty_screen", appid="js_gui__empty_screen",
apptype=FlipperAppType.PLUGIN, apptype=FlipperAppType.PLUGIN,
entry_point="js_view_empty_screen_ep", entry_point="js_view_empty_screen_ep",
requires=["js_app", "js_gui", "js_event_loop"], requires=["js_app"],
sources=["modules/js_gui/empty_screen.c"], sources=["modules/js_gui/empty_screen.c"],
) )
@@ -54,7 +65,7 @@ App(
appid="js_gui__submenu", appid="js_gui__submenu",
apptype=FlipperAppType.PLUGIN, apptype=FlipperAppType.PLUGIN,
entry_point="js_view_submenu_ep", entry_point="js_view_submenu_ep",
requires=["js_app", "js_gui"], requires=["js_app"],
sources=["modules/js_gui/submenu.c"], sources=["modules/js_gui/submenu.c"],
) )
@@ -62,10 +73,18 @@ App(
appid="js_gui__text_input", appid="js_gui__text_input",
apptype=FlipperAppType.PLUGIN, apptype=FlipperAppType.PLUGIN,
entry_point="js_view_text_input_ep", entry_point="js_view_text_input_ep",
requires=["js_app", "js_gui", "js_event_loop"], requires=["js_app"],
sources=["modules/js_gui/text_input.c"], sources=["modules/js_gui/text_input.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( App(
appid="js_gui__text_box", appid="js_gui__text_box",
apptype=FlipperAppType.PLUGIN, apptype=FlipperAppType.PLUGIN,
@@ -82,6 +101,15 @@ App(
sources=["modules/js_gui/dialog.c"], 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( App(
appid="js_notification", appid="js_notification",
apptype=FlipperAppType.PLUGIN, apptype=FlipperAppType.PLUGIN,
@@ -110,7 +138,7 @@ App(
appid="js_gpio", appid="js_gpio",
apptype=FlipperAppType.PLUGIN, apptype=FlipperAppType.PLUGIN,
entry_point="js_gpio_ep", entry_point="js_gpio_ep",
requires=["js_app", "js_event_loop"], requires=["js_app"],
sources=["modules/js_gpio.c"], sources=["modules/js_gpio.c"],
) )

View File

@@ -13,7 +13,13 @@ let views = {
}), }),
}; };
badusb.setup({ vid: 0xAAAA, pid: 0xBBBB, mfrName: "Flipper", prodName: "Zero" }); 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) { eventLoop.subscribe(views.dialog.input, function (_sub, button, eventLoop, gui) {
if (button !== "center") if (button !== "center")
@@ -39,7 +45,13 @@ eventLoop.subscribe(views.dialog.input, function (_sub, button, eventLoop, gui)
badusb.println("Flipper Model: " + flipper.getModel()); badusb.println("Flipper Model: " + flipper.getModel());
badusb.println("Flipper Name: " + flipper.getName()); badusb.println("Flipper Name: " + flipper.getName());
badusb.println("Battery level: " + toString(flipper.getBatteryCharge()) + "%"); 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(); notify.success();
} else { } else {
@@ -47,6 +59,9 @@ eventLoop.subscribe(views.dialog.input, function (_sub, button, eventLoop, gui)
notify.error(); notify.error();
} }
// Optional, but allows to unlock usb interface to switch profile
badusb.quit();
eventLoop.stop(); eventLoop.stop();
}, eventLoop, gui); }, eventLoop, gui);

View File

@@ -19,7 +19,7 @@ eventLoop.subscribe(eventLoop.timer("periodic", 1000), function (_, _item, led,
// read potentiometer when button is pressed // read potentiometer when button is pressed
print("Press the button (PC1)"); print("Press the button (PC1)");
eventLoop.subscribe(button.interrupt(), function (_, _item, pot) { eventLoop.subscribe(button.interrupt(), function (_, _item, pot) {
print("PC0 is at", pot.read_analog(), "mV"); print("PC0 is at", pot.readAnalog(), "mV");
}, pot); }, pot);
// the program will just exit unless this is here // the program will just exit unless this is here

View File

@@ -5,8 +5,11 @@ let loadingView = require("gui/loading");
let submenuView = require("gui/submenu"); let submenuView = require("gui/submenu");
let emptyView = require("gui/empty_screen"); let emptyView = require("gui/empty_screen");
let textInputView = require("gui/text_input"); let textInputView = require("gui/text_input");
let byteInputView = require("gui/byte_input");
let textBoxView = require("gui/text_box"); let textBoxView = require("gui/text_box");
let dialogView = require("gui/dialog"); let dialogView = require("gui/dialog");
let filePicker = require("gui/file_picker");
let flipper = require("flipper");
// declare view instances // declare view instances
let views = { let views = {
@@ -16,9 +19,14 @@ let views = {
header: "Enter your name", header: "Enter your name",
minLength: 0, minLength: 0,
maxLength: 32, maxLength: 32,
defaultText: flipper.getName(),
defaultTextClear: true,
}), }),
helloDialog: dialogView.makeWith({ helloDialog: dialogView.make(),
center: "Hi Flipper! :)", 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({ 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.", 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.",
@@ -29,7 +37,9 @@ let views = {
"Hourglass screen", "Hourglass screen",
"Empty screen", "Empty screen",
"Text input & Dialog", "Text input & Dialog",
"Byte input",
"Text box", "Text box",
"File picker",
"Exit app", "Exit app",
], ],
}), }),
@@ -49,15 +59,28 @@ eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, v
} else if (index === 2) { } else if (index === 2) {
gui.viewDispatcher.switchTo(views.keyboard); gui.viewDispatcher.switchTo(views.keyboard);
} else if (index === 3) { } else if (index === 3) {
gui.viewDispatcher.switchTo(views.longText); gui.viewDispatcher.switchTo(views.bytekb);
} else if (index === 4) { } 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) {
eventLoop.stop(); eventLoop.stop();
} }
}, gui, eventLoop, views); }, gui, eventLoop, views);
// say hi after keyboard input // say hi after keyboard input
eventLoop.subscribe(views.keyboard.input, function (_sub, name, gui, views) { 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("text", "Hi " + name + "! :)");
views.helloDialog.set("center", "Hi Flipper! :)");
gui.viewDispatcher.switchTo(views.helloDialog); gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views); }, gui, views);
@@ -67,11 +90,27 @@ eventLoop.subscribe(views.helloDialog.input, function (_sub, button, gui, views)
gui.viewDispatcher.switchTo(views.demos); gui.viewDispatcher.switchTo(views.demos);
}, gui, views); }, gui, views);
// go to the demo chooser screen when the back key is pressed // show data after byte input
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, gui, views) { eventLoop.subscribe(views.bytekb.input, function (_sub, data, gui, views) {
gui.viewDispatcher.switchTo(views.demos); 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); }, 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);
// run UI // run UI
gui.viewDispatcher.switchTo(views.demos); gui.viewDispatcher.switchTo(views.demos);
eventLoop.run(); eventLoop.run();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -97,7 +97,7 @@ static void js_app_free(JsApp* app) {
int32_t js_app(void* arg) { int32_t js_app(void* arg) {
JsApp* app = js_app_alloc(); JsApp* app = js_app_alloc();
FuriString* script_path = furi_string_alloc_set(APP_ASSETS_PATH()); FuriString* script_path = furi_string_alloc_set(EXT_PATH("apps/Scripts"));
do { do {
if(arg != NULL && strlen(arg) > 0) { if(arg != NULL && strlen(arg) > 0) {
furi_string_set(script_path, (const char*)arg); furi_string_set(script_path, (const char*)arg);

View File

@@ -1,6 +1,8 @@
#include <core/common_defines.h> #include <core/common_defines.h>
#include "js_modules.h" #include "js_modules.h"
#include <m-array.h> #include <m-array.h>
#include <dialogs/dialogs.h>
#include <assets_icons.h>
#include "modules/js_flipper.h" #include "modules/js_flipper.h"
#ifdef FW_CFG_unit_tests #ifdef FW_CFG_unit_tests
@@ -76,6 +78,12 @@ JsModuleData* js_find_loaded_module(JsModules* instance, const char* name) {
} }
mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_len) { 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 // Check if module is already installed
JsModuleData* module_inst = js_find_loaded_module(modules, name); JsModuleData* module_inst = js_find_loaded_module(modules, name);
if(module_inst) { //-V547 if(module_inst) { //-V547
@@ -175,3 +183,133 @@ void* js_module_get(JsModules* modules, const char* name) {
furi_string_free(module_name); furi_string_free(module_name);
return module_inst ? module_inst->context : NULL; 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;
}
#define JS_SDK_COMPAT_ARGS \
int32_t major, minor; \
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&major), JS_ARG_INT32(&minor));
void js_sdk_compatibility_status(struct mjs* mjs) {
JS_SDK_COMPAT_ARGS;
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) {
JS_SDK_COMPAT_ARGS;
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) {
JS_SDK_COMPAT_ARGS;
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"
};
/**
* @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;
}
void js_does_sdk_support(struct mjs* mjs) {
mjs_val_t features;
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ARR(&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_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ARR(&features));
if(!js_internal_supports_all_of(mjs, features)) {
FURI_LOG_E(TAG, "Script requests unsupported features");
if(!js_internal_compat_ask_user("Unsupported Feature")) {
JS_ERROR_AND_RETURN(mjs, MJS_NOT_IMPLEMENTED_ERROR, "Incompatible script");
}
}
}

View File

@@ -9,6 +9,10 @@
#define PLUGIN_APP_ID "js" #define PLUGIN_APP_ID "js"
#define PLUGIN_API_VERSION 1 #define PLUGIN_API_VERSION 1
#define JS_SDK_VENDOR "flipperdevices"
#define JS_SDK_MAJOR 0
#define JS_SDK_MINOR 1
/** /**
* @brief Returns the foreign pointer in `obj["_"]` * @brief Returns the foreign pointer in `obj["_"]`
*/ */
@@ -275,3 +279,28 @@ mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_le
* @returns Pointer to module context, NULL if the module is not instantiated * @returns Pointer to module context, NULL if the module is not instantiated
*/ */
void* js_module_get(JsModules* modules, const char* name); 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);

View File

@@ -1,5 +1,7 @@
#include <common/cs_dbg.h> #include <common/cs_dbg.h>
#include <toolbox/path.h>
#include <toolbox/stream/file_stream.h> #include <toolbox/stream/file_stream.h>
#include <toolbox/strint.h>
#include <loader/firmware_api/firmware_api.h> #include <loader/firmware_api/firmware_api.h>
#include <flipper_application/api_hashtable/api_hashtable.h> #include <flipper_application/api_hashtable/api_hashtable.h>
#include <flipper_application/plugins/composite_resolver.h> #include <flipper_application/plugins/composite_resolver.h>
@@ -194,14 +196,25 @@ static void js_require(struct mjs* mjs) {
mjs_return(mjs, req_object); mjs_return(mjs, req_object);
} }
static void js_global_to_string(struct mjs* mjs) { static void js_parse_int(struct mjs* mjs) {
int base = 10; const char* str;
if(mjs_nargs(mjs) > 1) base = mjs_get_int(mjs, mjs_arg(mjs, 1)); JS_FETCH_ARGS_OR_RETURN(mjs, JS_AT_LEAST, JS_ARG_STR(&str));
double num = mjs_get_int(mjs, mjs_arg(mjs, 0));
char tmp_str[] = "-2147483648"; int32_t base = 10;
itoa(num, tmp_str, base); if(mjs_nargs(mjs) >= 2) {
mjs_val_t ret = mjs_mk_string(mjs, tmp_str, ~0, true); mjs_val_t base_arg = mjs_arg(mjs, 1);
mjs_return(mjs, ret); if(!mjs_is_number(base_arg)) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Base must be a number");
mjs_return(mjs, MJS_UNDEFINED);
}
base = mjs_get_int(mjs, base_arg);
}
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 #ifdef JS_DEBUG
@@ -231,18 +244,48 @@ static int32_t js_thread(void* arg) {
struct mjs* mjs = mjs_create(worker); struct mjs* mjs = mjs_create(worker);
worker->modules = js_modules_create(mjs, worker->resolver); worker->modules = js_modules_create(mjs, worker->resolver);
mjs_val_t global = mjs_get_global(mjs); mjs_val_t global = mjs_get_global(mjs);
mjs_set(mjs, global, "print", ~0, MJS_MK_FN(js_print));
mjs_set(mjs, global, "delay", ~0, MJS_MK_FN(js_delay));
mjs_set(mjs, global, "toString", ~0, MJS_MK_FN(js_global_to_string));
mjs_set(mjs, global, "ffi_address", ~0, MJS_MK_FN(js_ffi_address));
mjs_set(mjs, global, "require", ~0, MJS_MK_FN(js_require));
mjs_val_t console_obj = mjs_mk_object(mjs); mjs_val_t console_obj = mjs_mk_object(mjs);
mjs_set(mjs, console_obj, "log", ~0, MJS_MK_FN(js_console_log));
mjs_set(mjs, console_obj, "warn", ~0, MJS_MK_FN(js_console_warn)); if(worker->path) {
mjs_set(mjs, console_obj, "error", ~0, MJS_MK_FN(js_console_error)); FuriString* dirpath = furi_string_alloc();
mjs_set(mjs, console_obj, "debug", ~0, MJS_MK_FN(js_console_debug)); path_extract_dirname(furi_string_get_cstr(worker->path), dirpath);
mjs_set(mjs, global, "console", ~0, console_obj); 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_ffi_resolver(mjs, js_dlsym, worker->resolver);

View File

@@ -2,8 +2,11 @@
#include "../js_modules.h" #include "../js_modules.h"
#include <furi_hal.h> #include <furi_hal.h>
#define ASCII_TO_KEY(layout, x) (((uint8_t)x < 128) ? (layout[(uint8_t)x]) : HID_KEYBOARD_NONE)
typedef struct { typedef struct {
FuriHalUsbHidConfig* hid_cfg; FuriHalUsbHidConfig* hid_cfg;
uint16_t layout[128];
FuriHalUsbInterface* usb_if_prev; FuriHalUsbInterface* usb_if_prev;
uint8_t key_hold_cnt; uint8_t key_hold_cnt;
} JsBadusbInst; } JsBadusbInst;
@@ -64,9 +67,36 @@ static const struct {
{"F22", HID_KEYBOARD_F22}, {"F22", HID_KEYBOARD_F22},
{"F23", HID_KEYBOARD_F23}, {"F23", HID_KEYBOARD_F23},
{"F24", HID_KEYBOARD_F24}, {"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 bool setup_parse_params(struct mjs* mjs, mjs_val_t arg, FuriHalUsbHidConfig* hid_cfg) { 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)) { if(!mjs_is_object(arg)) {
return false; return false;
} }
@@ -74,6 +104,7 @@ static bool setup_parse_params(struct mjs* mjs, mjs_val_t arg, FuriHalUsbHidConf
mjs_val_t pid_obj = mjs_get(mjs, arg, "pid", ~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 mfr_obj = mjs_get(mjs, arg, "mfrName", ~0);
mjs_val_t prod_obj = mjs_get(mjs, arg, "prodName", ~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)) { if(mjs_is_number(vid_obj) && mjs_is_number(pid_obj)) {
hid_cfg->vid = mjs_get_int32(mjs, vid_obj); hid_cfg->vid = mjs_get_int32(mjs, vid_obj);
@@ -100,6 +131,25 @@ static bool setup_parse_params(struct mjs* mjs, mjs_val_t arg, FuriHalUsbHidConf
strlcpy(hid_cfg->product, str_temp, sizeof(hid_cfg->product)); 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; return true;
} }
@@ -122,7 +172,7 @@ static void js_badusb_setup(struct mjs* mjs) {
} else if(num_args == 1) { } else if(num_args == 1) {
badusb->hid_cfg = malloc(sizeof(FuriHalUsbHidConfig)); badusb->hid_cfg = malloc(sizeof(FuriHalUsbHidConfig));
// Parse argument object // Parse argument object
args_correct = setup_parse_params(mjs, mjs_arg(mjs, 0), badusb->hid_cfg); args_correct = setup_parse_params(badusb, mjs, mjs_arg(mjs, 0), badusb->hid_cfg);
} }
if(!args_correct) { if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
@@ -142,6 +192,22 @@ static void js_badusb_setup(struct mjs* mjs) {
mjs_return(mjs, MJS_UNDEFINED); 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) { 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); mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst); JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
@@ -157,9 +223,9 @@ static void js_badusb_is_connected(struct mjs* mjs) {
mjs_return(mjs, mjs_mk_boolean(mjs, is_connected)); mjs_return(mjs, mjs_mk_boolean(mjs, is_connected));
} }
uint16_t get_keycode_by_name(const char* key_name, size_t name_len) { uint16_t get_keycode_by_name(JsBadusbInst* badusb, const char* key_name, size_t name_len) {
if(name_len == 1) { // Single char if(name_len == 1) { // Single char
return HID_ASCII_TO_KEY(key_name[0]); return (ASCII_TO_KEY(badusb->layout, key_name[0]));
} }
for(size_t i = 0; i < COUNT_OF(key_codes); i++) { for(size_t i = 0; i < COUNT_OF(key_codes); i++) {
@@ -176,7 +242,7 @@ uint16_t get_keycode_by_name(const char* key_name, size_t name_len) {
return HID_KEYBOARD_NONE; return HID_KEYBOARD_NONE;
} }
static bool parse_keycode(struct mjs* mjs, size_t nargs, uint16_t* keycode) { static bool parse_keycode(JsBadusbInst* badusb, struct mjs* mjs, size_t nargs, uint16_t* keycode) {
uint16_t key_tmp = 0; uint16_t key_tmp = 0;
for(size_t i = 0; i < nargs; i++) { for(size_t i = 0; i < nargs; i++) {
mjs_val_t arg = mjs_arg(mjs, i); mjs_val_t arg = mjs_arg(mjs, i);
@@ -187,7 +253,7 @@ static bool parse_keycode(struct mjs* mjs, size_t nargs, uint16_t* keycode) {
// String error // String error
return false; return false;
} }
uint16_t str_key = get_keycode_by_name(key_name, name_len); uint16_t str_key = get_keycode_by_name(badusb, key_name, name_len);
if(str_key == HID_KEYBOARD_NONE) { if(str_key == HID_KEYBOARD_NONE) {
// Unknown key code // Unknown key code
return false; return false;
@@ -225,7 +291,7 @@ static void js_badusb_press(struct mjs* mjs) {
uint16_t keycode = HID_KEYBOARD_NONE; uint16_t keycode = HID_KEYBOARD_NONE;
size_t num_args = mjs_nargs(mjs); size_t num_args = mjs_nargs(mjs);
if(num_args > 0) { if(num_args > 0) {
args_correct = parse_keycode(mjs, num_args, &keycode); args_correct = parse_keycode(badusb, mjs, num_args, &keycode);
} }
if(!args_correct) { if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
@@ -251,7 +317,7 @@ static void js_badusb_hold(struct mjs* mjs) {
uint16_t keycode = HID_KEYBOARD_NONE; uint16_t keycode = HID_KEYBOARD_NONE;
size_t num_args = mjs_nargs(mjs); size_t num_args = mjs_nargs(mjs);
if(num_args > 0) { if(num_args > 0) {
args_correct = parse_keycode(mjs, num_args, &keycode); args_correct = parse_keycode(badusb, mjs, num_args, &keycode);
} }
if(!args_correct) { if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
@@ -290,7 +356,7 @@ static void js_badusb_release(struct mjs* mjs) {
mjs_return(mjs, MJS_UNDEFINED); mjs_return(mjs, MJS_UNDEFINED);
return; return;
} else { } else {
args_correct = parse_keycode(mjs, num_args, &keycode); args_correct = parse_keycode(badusb, mjs, num_args, &keycode);
} }
if(!args_correct) { if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
@@ -304,7 +370,35 @@ static void js_badusb_release(struct mjs* mjs) {
mjs_return(mjs, MJS_UNDEFINED); mjs_return(mjs, MJS_UNDEFINED);
} }
static void badusb_print(struct mjs* mjs, bool ln) { // 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); mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst); JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
furi_assert(badusb); furi_assert(badusb);
@@ -350,10 +444,20 @@ static void badusb_print(struct mjs* mjs, bool ln) {
return; return;
} }
if(alt) {
ducky_numlock_on();
}
for(size_t i = 0; i < text_len; i++) { for(size_t i = 0; i < text_len; i++) {
uint16_t keycode = HID_ASCII_TO_KEY(text_str[i]); if(alt) {
furi_hal_hid_kb_press(keycode); // Convert character to ascii numeric value
furi_hal_hid_kb_release(keycode); 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) { if(delay_val > 0) {
bool need_exit = js_delay_with_flags(mjs, delay_val); bool need_exit = js_delay_with_flags(mjs, delay_val);
if(need_exit) { if(need_exit) {
@@ -371,11 +475,19 @@ static void badusb_print(struct mjs* mjs, bool ln) {
} }
static void js_badusb_print(struct mjs* mjs) { static void js_badusb_print(struct mjs* mjs) {
badusb_print(mjs, false); badusb_print(mjs, false, false);
} }
static void js_badusb_println(struct mjs* mjs) { static void js_badusb_println(struct mjs* mjs) {
badusb_print(mjs, true); 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) { static void* js_badusb_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
@@ -384,25 +496,22 @@ static void* js_badusb_create(struct mjs* mjs, mjs_val_t* object, JsModules* mod
mjs_val_t badusb_obj = mjs_mk_object(mjs); 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, 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, "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, "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, "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, "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, "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, "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, "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; *object = badusb_obj;
return badusb; return badusb;
} }
static void js_badusb_destroy(void* inst) { static void js_badusb_destroy(void* inst) {
JsBadusbInst* badusb = inst; JsBadusbInst* badusb = inst;
if(badusb->usb_if_prev) { js_badusb_quit_free(badusb);
furi_hal_hid_kb_release_all();
furi_check(furi_hal_usb_set_config(badusb->usb_if_prev, NULL));
}
if(badusb->hid_cfg) {
free(badusb->hid_cfg);
}
free(badusb); free(badusb);
} }

View File

@@ -80,7 +80,7 @@ static void js_event_loop_callback_generic(void* param) {
/** /**
* @brief Handles non-timer events * @brief Handles non-timer events
*/ */
static bool js_event_loop_callback(void* object, void* param) { static void js_event_loop_callback(void* object, void* param) {
JsEventLoopCallbackContext* context = param; JsEventLoopCallbackContext* context = param;
if(context->transformer) { if(context->transformer) {
@@ -102,8 +102,6 @@ static bool js_event_loop_callback(void* object, void* param) {
} }
js_event_loop_callback_generic(param); js_event_loop_callback_generic(param);
return true;
} }
/** /**

View File

@@ -27,11 +27,19 @@ static void js_flipper_get_battery(struct mjs* mjs) {
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { void* js_flipper_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(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); mjs_val_t flipper_obj = mjs_mk_object(mjs);
mjs_set(mjs, flipper_obj, "getModel", ~0, MJS_MK_FN(js_flipper_get_model));
mjs_set(mjs, flipper_obj, "getName", ~0, MJS_MK_FN(js_flipper_get_name));
mjs_set(mjs, flipper_obj, "getBatteryCharge", ~0, MJS_MK_FN(js_flipper_get_battery));
*object = flipper_obj; *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, ~0, false));
JS_FIELD("jsSdkVersion", sdk_vsn);
}
return (void*)1; return (void*)1;
} }

View File

@@ -220,7 +220,7 @@ static void js_gpio_interrupt(struct mjs* mjs) {
* let gpio = require("gpio"); * let gpio = require("gpio");
* let pot = gpio.get("pc0"); * let pot = gpio.get("pc0");
* pot.init({ direction: "in", inMode: "analog" }); * pot.init({ direction: "in", inMode: "analog" });
* print("voltage:" pot.read_analog(), "mV"); * print("voltage:" pot.readAnalog(), "mV");
* ``` * ```
*/ */
static void js_gpio_read_analog(struct mjs* mjs) { static void js_gpio_read_analog(struct mjs* mjs) {
@@ -269,12 +269,11 @@ static void js_gpio_get(struct mjs* mjs) {
manager_data->interrupt_semaphore = furi_semaphore_alloc(UINT32_MAX, 0); manager_data->interrupt_semaphore = furi_semaphore_alloc(UINT32_MAX, 0);
manager_data->adc_handle = module->adc_handle; manager_data->adc_handle = module->adc_handle;
manager_data->adc_channel = pin_record->channel; manager_data->adc_channel = pin_record->channel;
mjs_own(mjs, &manager);
mjs_set(mjs, manager, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, manager_data)); mjs_set(mjs, manager, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, manager_data));
mjs_set(mjs, manager, "init", ~0, MJS_MK_FN(js_gpio_init)); mjs_set(mjs, manager, "init", ~0, MJS_MK_FN(js_gpio_init));
mjs_set(mjs, manager, "write", ~0, MJS_MK_FN(js_gpio_write)); mjs_set(mjs, manager, "write", ~0, MJS_MK_FN(js_gpio_write));
mjs_set(mjs, manager, "read", ~0, MJS_MK_FN(js_gpio_read)); mjs_set(mjs, manager, "read", ~0, MJS_MK_FN(js_gpio_read));
mjs_set(mjs, manager, "read_analog", ~0, MJS_MK_FN(js_gpio_read_analog)); mjs_set(mjs, manager, "readAnalog", ~0, MJS_MK_FN(js_gpio_read_analog));
mjs_set(mjs, manager, "interrupt", ~0, MJS_MK_FN(js_gpio_interrupt)); mjs_set(mjs, manager, "interrupt", ~0, MJS_MK_FN(js_gpio_interrupt));
mjs_return(mjs, manager); mjs_return(mjs, manager);

View File

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

View File

@@ -0,0 +1,47 @@
#include "../../js_modules.h"
#include <dialogs/dialogs.h>
#include <assets_icons.h>
static void js_gui_file_picker_pick_file(struct mjs* mjs) {
const char *base_path, *extension;
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&base_path), JS_ARG_STR(&extension));
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
const DialogsFileBrowserOptions browser_options = {
.extension = extension,
.icon = &I_file_10px,
.base_path = base_path,
};
FuriString* path = furi_string_alloc_set(base_path);
if(dialog_file_browser_show(dialogs, path, path, &browser_options)) {
mjs_return(mjs, mjs_mk_string(mjs, furi_string_get_cstr(path), ~0, true));
} else {
mjs_return(mjs, MJS_UNDEFINED);
}
furi_string_free(path);
furi_record_close(RECORD_DIALOGS);
}
static void* js_gui_file_picker_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
*object = mjs_mk_object(mjs);
mjs_set(mjs, *object, "pickFile", ~0, MJS_MK_FN(js_gui_file_picker_pick_file));
return NULL;
}
static const JsModuleDescriptor js_gui_file_picker_desc = {
"gui__file_picker",
js_gui_file_picker_create,
NULL,
NULL,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_gui_file_picker_desc,
};
const FlipperAppPluginDescriptor* js_gui_file_picker_ep(void) {
return &plugin_descriptor;
}

View File

@@ -101,8 +101,10 @@ static void js_gui_vd_switch_to(struct mjs* mjs) {
mjs_val_t view; mjs_val_t view;
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&view)); JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&view));
JsGuiViewData* view_data = JS_GET_INST(mjs, view); JsGuiViewData* view_data = JS_GET_INST(mjs, view);
JsGui* module = JS_GET_CONTEXT(mjs); mjs_val_t vd_obj = mjs_get_this(mjs);
JsGui* module = JS_GET_INST(mjs, vd_obj);
view_dispatcher_switch_to_view(module->dispatcher, (uint32_t)view_data->id); view_dispatcher_switch_to_view(module->dispatcher, (uint32_t)view_data->id);
mjs_set(mjs, vd_obj, "currentView", ~0, view);
} }
static void* js_gui_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { static void* js_gui_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
@@ -154,6 +156,7 @@ static void* js_gui_create(struct mjs* mjs, mjs_val_t* object, JsModules* module
JS_FIELD("switchTo", MJS_MK_FN(js_gui_vd_switch_to)); JS_FIELD("switchTo", MJS_MK_FN(js_gui_vd_switch_to));
JS_FIELD("custom", mjs_mk_foreign(mjs, &module->custom_contract)); JS_FIELD("custom", mjs_mk_foreign(mjs, &module->custom_contract));
JS_FIELD("navigation", mjs_mk_foreign(mjs, &module->navigation_contract)); JS_FIELD("navigation", mjs_mk_foreign(mjs, &module->navigation_contract));
JS_FIELD("currentView", MJS_NULL);
} }
// create API object // create API object
@@ -213,7 +216,21 @@ static bool
expected_type = "array"; expected_type = "array";
break; break;
} }
c_value = (JsViewPropValue){.array = value}; c_value = (JsViewPropValue){.term = value};
} break;
case JsViewPropTypeTypedArr: {
if(!mjs_is_typed_array(value)) {
expected_type = "typed_array";
break;
}
c_value = (JsViewPropValue){.term = value};
} break;
case JsViewPropTypeBool: {
if(!mjs_is_boolean(value)) {
expected_type = "bool";
break;
}
c_value = (JsViewPropValue){.boolean = mjs_get_bool(mjs, value)};
} break; } break;
} }

View File

@@ -9,12 +9,15 @@ typedef enum {
JsViewPropTypeString, JsViewPropTypeString,
JsViewPropTypeNumber, JsViewPropTypeNumber,
JsViewPropTypeArr, JsViewPropTypeArr,
JsViewPropTypeTypedArr,
JsViewPropTypeBool,
} JsViewPropType; } JsViewPropType;
typedef union { typedef union {
const char* string; const char* string;
int32_t number; int32_t number;
mjs_val_t array; bool boolean;
mjs_val_t term;
} JsViewPropValue; } JsViewPropValue;
/** /**

View File

@@ -33,9 +33,9 @@ static bool
static bool items_assign(struct mjs* mjs, Submenu* submenu, JsViewPropValue value, void* context) { static bool items_assign(struct mjs* mjs, Submenu* submenu, JsViewPropValue value, void* context) {
UNUSED(mjs); UNUSED(mjs);
submenu_reset(submenu); submenu_reset(submenu);
size_t len = mjs_array_length(mjs, value.array); size_t len = mjs_array_length(mjs, value.term);
for(size_t i = 0; i < len; i++) { for(size_t i = 0; i < len; i++) {
mjs_val_t item = mjs_array_get(mjs, value.array, i); mjs_val_t item = mjs_array_get(mjs, value.term, i);
if(!mjs_is_string(item)) return false; if(!mjs_is_string(item)) return false;
submenu_add_item(submenu, mjs_get_string(mjs, &item, NULL), i, choose_callback, context); submenu_add_item(submenu, mjs_get_string(mjs, &item, NULL), i, choose_callback, context);
} }

View File

@@ -8,7 +8,9 @@
typedef struct { typedef struct {
char* buffer; char* buffer;
size_t buffer_size; size_t buffer_size;
size_t default_text_size;
FuriString* header; FuriString* header;
bool default_text_clear;
FuriSemaphore* input_semaphore; FuriSemaphore* input_semaphore;
JsEventLoopContract contract; JsEventLoopContract contract;
} JsKbdContext; } JsKbdContext;
@@ -48,7 +50,14 @@ static bool max_len_assign(
JsViewPropValue value, JsViewPropValue value,
JsKbdContext* context) { JsKbdContext* context) {
UNUSED(mjs); UNUSED(mjs);
context->buffer_size = (size_t)(value.number + 1); size_t new_buffer_size = value.number + 1;
if(new_buffer_size < context->default_text_size) {
// Avoid confusing parameters from user
mjs_prepend_errorf(
mjs, MJS_BAD_ARGS_ERROR, "maxLength must be larger than defaultText length");
return false;
}
context->buffer_size = new_buffer_size;
context->buffer = realloc(context->buffer, context->buffer_size); //-V701 context->buffer = realloc(context->buffer, context->buffer_size); //-V701
text_input_set_result_callback( text_input_set_result_callback(
input, input,
@@ -56,17 +65,63 @@ static bool max_len_assign(
context, context,
context->buffer, context->buffer,
context->buffer_size, context->buffer_size,
true); context->default_text_clear);
return true;
}
static bool default_text_assign(
struct mjs* mjs,
TextInput* input,
JsViewPropValue value,
JsKbdContext* context) {
UNUSED(mjs);
UNUSED(input);
if(value.string) {
context->default_text_size = strlen(value.string) + 1;
if(context->buffer_size < context->default_text_size) {
// Ensure buffer is large enough for defaultData
context->buffer_size = context->default_text_size;
context->buffer = realloc(context->buffer, context->buffer_size); //-V701
}
// Also trim excess previous data with strlcpy()
strlcpy(context->buffer, value.string, context->buffer_size); //-V575
text_input_set_result_callback(
input,
(TextInputCallback)input_callback,
context,
context->buffer,
context->buffer_size,
context->default_text_clear);
}
return true;
}
static bool default_text_clear_assign(
struct mjs* mjs,
TextInput* input,
JsViewPropValue value,
JsKbdContext* context) {
UNUSED(mjs);
context->default_text_clear = value.boolean;
text_input_set_result_callback(
input,
(TextInputCallback)input_callback,
context,
context->buffer,
context->buffer_size,
context->default_text_clear);
return true; return true;
} }
static JsKbdContext* ctx_make(struct mjs* mjs, TextInput* input, mjs_val_t view_obj) { static JsKbdContext* ctx_make(struct mjs* mjs, TextInput* input, mjs_val_t view_obj) {
UNUSED(input);
JsKbdContext* context = malloc(sizeof(JsKbdContext)); JsKbdContext* context = malloc(sizeof(JsKbdContext));
*context = (JsKbdContext){ *context = (JsKbdContext){
.buffer_size = DEFAULT_BUF_SZ, .buffer_size = DEFAULT_BUF_SZ,
.buffer = malloc(DEFAULT_BUF_SZ), .buffer = malloc(DEFAULT_BUF_SZ),
.header = furi_string_alloc(), .header = furi_string_alloc(),
.default_text_clear = false,
.input_semaphore = furi_semaphore_alloc(1, 0), .input_semaphore = furi_semaphore_alloc(1, 0),
}; };
context->contract = (JsEventLoopContract){ context->contract = (JsEventLoopContract){
@@ -80,8 +135,13 @@ static JsKbdContext* ctx_make(struct mjs* mjs, TextInput* input, mjs_val_t view_
.transformer_context = context, .transformer_context = context,
}, },
}; };
UNUSED(mjs); text_input_set_result_callback(
UNUSED(view_obj); input,
(TextInputCallback)input_callback,
context,
context->buffer,
context->buffer_size,
context->default_text_clear);
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract)); mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
return context; return context;
} }
@@ -101,7 +161,7 @@ static const JsViewDescriptor view_descriptor = {
.get_view = (JsViewGetView)text_input_get_view, .get_view = (JsViewGetView)text_input_get_view,
.custom_make = (JsViewCustomMake)ctx_make, .custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy, .custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.prop_cnt = 3, .prop_cnt = 5,
.props = { .props = {
(JsViewPropDescriptor){ (JsViewPropDescriptor){
.name = "header", .name = "header",
@@ -115,6 +175,14 @@ static const JsViewDescriptor view_descriptor = {
.name = "maxLength", .name = "maxLength",
.type = JsViewPropTypeNumber, .type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)max_len_assign}, .assign = (JsViewPropAssign)max_len_assign},
(JsViewPropDescriptor){
.name = "defaultText",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)default_text_assign},
(JsViewPropDescriptor){
.name = "defaultTextClear",
.type = JsViewPropTypeBool,
.assign = (JsViewPropAssign)default_text_clear_assign},
}}; }};
JS_GUI_VIEW_DEF(text_input, &view_descriptor); JS_GUI_VIEW_DEF(text_input, &view_descriptor);

View File

@@ -308,7 +308,7 @@ void js_math_trunc(struct mjs* mjs) {
static void* js_math_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { static void* js_math_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules); UNUSED(modules);
mjs_val_t math_obj = mjs_mk_object(mjs); mjs_val_t math_obj = mjs_mk_object(mjs);
mjs_set(mjs, math_obj, "is_equal", ~0, MJS_MK_FN(js_math_is_equal)); mjs_set(mjs, math_obj, "isEqual", ~0, MJS_MK_FN(js_math_is_equal));
mjs_set(mjs, math_obj, "abs", ~0, MJS_MK_FN(js_math_abs)); mjs_set(mjs, math_obj, "abs", ~0, MJS_MK_FN(js_math_abs));
mjs_set(mjs, math_obj, "acos", ~0, MJS_MK_FN(js_math_acos)); mjs_set(mjs, math_obj, "acos", ~0, MJS_MK_FN(js_math_acos));
mjs_set(mjs, math_obj, "acosh", ~0, MJS_MK_FN(js_math_acosh)); mjs_set(mjs, math_obj, "acosh", ~0, MJS_MK_FN(js_math_acosh));

View File

@@ -1,4 +1,5 @@
#include <core/common_defines.h> #include <core/common_defines.h>
#include <expansion/expansion.h>
#include <furi_hal.h> #include <furi_hal.h>
#include "../js_modules.h" #include "../js_modules.h"
#include <m-array.h> #include <m-array.h>
@@ -89,16 +90,51 @@ static void js_serial_setup(struct mjs* mjs) {
return; return;
} }
serial->rx_stream = furi_stream_buffer_alloc(RX_BUF_LEN, 1); expansion_disable(furi_record_open(RECORD_EXPANSION));
furi_record_close(RECORD_EXPANSION);
serial->serial_handle = furi_hal_serial_control_acquire(serial_id); serial->serial_handle = furi_hal_serial_control_acquire(serial_id);
if(serial->serial_handle) { if(serial->serial_handle) {
serial->rx_stream = furi_stream_buffer_alloc(RX_BUF_LEN, 1);
furi_hal_serial_init(serial->serial_handle, baudrate); furi_hal_serial_init(serial->serial_handle, baudrate);
furi_hal_serial_async_rx_start( furi_hal_serial_async_rx_start(
serial->serial_handle, js_serial_on_async_rx, serial, false); serial->serial_handle, js_serial_on_async_rx, serial, false);
serial->setup_done = true; serial->setup_done = true;
} else {
expansion_enable(furi_record_open(RECORD_EXPANSION));
furi_record_close(RECORD_EXPANSION);
} }
} }
static void js_serial_deinit(JsSerialInst* js_serial) {
if(js_serial->setup_done) {
furi_hal_serial_async_rx_stop(js_serial->serial_handle);
furi_hal_serial_deinit(js_serial->serial_handle);
furi_hal_serial_control_release(js_serial->serial_handle);
js_serial->serial_handle = NULL;
furi_stream_buffer_free(js_serial->rx_stream);
expansion_enable(furi_record_open(RECORD_EXPANSION));
furi_record_close(RECORD_EXPANSION);
js_serial->setup_done = false;
}
}
static void js_serial_end(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsSerialInst* serial = mjs_get_ptr(mjs, obj_inst);
furi_assert(serial);
if(!serial->setup_done) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Serial is not configured");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
js_serial_deinit(serial);
}
static void js_serial_write(struct mjs* mjs) { static void js_serial_write(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsSerialInst* serial = mjs_get_ptr(mjs, obj_inst); JsSerialInst* serial = mjs_get_ptr(mjs, obj_inst);
@@ -346,6 +382,55 @@ static void js_serial_read_bytes(struct mjs* mjs) {
free(read_buf); free(read_buf);
} }
static char* js_serial_receive_any(JsSerialInst* serial, size_t* len, uint32_t timeout) {
uint32_t flags = ThreadEventCustomDataRx;
if(furi_stream_buffer_is_empty(serial->rx_stream)) {
flags = js_flags_wait(serial->mjs, ThreadEventCustomDataRx, timeout);
}
if(flags & ThreadEventCustomDataRx) { // New data received
*len = furi_stream_buffer_bytes_available(serial->rx_stream);
if(!*len) return NULL;
char* buf = malloc(*len);
furi_stream_buffer_receive(serial->rx_stream, buf, *len, 0);
return buf;
}
return NULL;
}
static void js_serial_read_any(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsSerialInst* serial = mjs_get_ptr(mjs, obj_inst);
furi_assert(serial);
if(!serial->setup_done) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Serial is not configured");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
uint32_t timeout = FuriWaitForever;
do {
size_t num_args = mjs_nargs(mjs);
if(num_args == 1) {
mjs_val_t timeout_arg = mjs_arg(mjs, 0);
if(!mjs_is_number(timeout_arg)) {
break;
}
timeout = mjs_get_int32(mjs, timeout_arg);
}
} while(0);
size_t bytes_read = 0;
char* read_buf = js_serial_receive_any(serial, &bytes_read, timeout);
mjs_val_t return_obj = MJS_UNDEFINED;
if(bytes_read > 0 && read_buf) {
return_obj = mjs_mk_string(mjs, read_buf, bytes_read, true);
}
mjs_return(mjs, return_obj);
free(read_buf);
}
static bool static bool
js_serial_expect_parse_string(struct mjs* mjs, mjs_val_t arg, PatternArray_t patterns) { js_serial_expect_parse_string(struct mjs* mjs, mjs_val_t arg, PatternArray_t patterns) {
size_t str_len = 0; size_t str_len = 0;
@@ -580,10 +665,12 @@ static void* js_serial_create(struct mjs* mjs, mjs_val_t* object, JsModules* mod
mjs_val_t serial_obj = mjs_mk_object(mjs); mjs_val_t serial_obj = mjs_mk_object(mjs);
mjs_set(mjs, serial_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, js_serial)); mjs_set(mjs, serial_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, js_serial));
mjs_set(mjs, serial_obj, "setup", ~0, MJS_MK_FN(js_serial_setup)); mjs_set(mjs, serial_obj, "setup", ~0, MJS_MK_FN(js_serial_setup));
mjs_set(mjs, serial_obj, "end", ~0, MJS_MK_FN(js_serial_end));
mjs_set(mjs, serial_obj, "write", ~0, MJS_MK_FN(js_serial_write)); mjs_set(mjs, serial_obj, "write", ~0, MJS_MK_FN(js_serial_write));
mjs_set(mjs, serial_obj, "read", ~0, MJS_MK_FN(js_serial_read)); mjs_set(mjs, serial_obj, "read", ~0, MJS_MK_FN(js_serial_read));
mjs_set(mjs, serial_obj, "readln", ~0, MJS_MK_FN(js_serial_readln)); mjs_set(mjs, serial_obj, "readln", ~0, MJS_MK_FN(js_serial_readln));
mjs_set(mjs, serial_obj, "readBytes", ~0, MJS_MK_FN(js_serial_read_bytes)); mjs_set(mjs, serial_obj, "readBytes", ~0, MJS_MK_FN(js_serial_read_bytes));
mjs_set(mjs, serial_obj, "readAny", ~0, MJS_MK_FN(js_serial_read_any));
mjs_set(mjs, serial_obj, "expect", ~0, MJS_MK_FN(js_serial_expect)); mjs_set(mjs, serial_obj, "expect", ~0, MJS_MK_FN(js_serial_expect));
*object = serial_obj; *object = serial_obj;
@@ -592,14 +679,7 @@ static void* js_serial_create(struct mjs* mjs, mjs_val_t* object, JsModules* mod
static void js_serial_destroy(void* inst) { static void js_serial_destroy(void* inst) {
JsSerialInst* js_serial = inst; JsSerialInst* js_serial = inst;
if(js_serial->setup_done) { js_serial_deinit(js_serial);
furi_hal_serial_async_rx_stop(js_serial->serial_handle);
furi_hal_serial_deinit(js_serial->serial_handle);
furi_hal_serial_control_release(js_serial->serial_handle);
js_serial->serial_handle = NULL;
}
furi_stream_buffer_free(js_serial->rx_stream);
free(js_serial); free(js_serial);
} }

View File

@@ -0,0 +1,20 @@
# Flipper Zero JavaScript SDK Wizard
This package contains an interactive wizard that lets you scaffold a JavaScript
application for Flipper Zero.
## Getting started
Create your application using the interactive wizard:
```shell
npx @flipperdevices/create-fz-app@latest
```
Then, enter the directory with your application and launch it:
```shell
cd my-flip-app
npm start
```
You are free to use `pnpm` or `yarn` instead of `npm`.
## Documentation
Check out the [JavaScript section in the Developer Documentation](https://developer.flipper.net/flipperzero/doxygen/js.html)

View File

@@ -0,0 +1,68 @@
#!/usr/bin/env node
import prompts from "prompts";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "url";
import { spawnSync } from "node:child_process";
import { replaceInFileSync } from "replace-in-file";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
(async () => {
const { name, pkgManager, confirm } = await prompts([
{
type: "text",
name: "name",
message: "What is the name of your project?",
initial: "my-flip-app"
},
{
type: "select",
name: "pkgManager",
message: "What package manager should your project use?",
choices: [
{ title: "npm", value: "npm" },
{ title: "pnpm", value: "pnpm" },
{ title: "yarn", value: "yarn" },
],
},
{
type: "confirm",
name: "confirm",
message: "Create project?",
initial: true,
},
]);
if (!confirm)
return;
if (fs.existsSync(name)) {
const { replace } = await prompts([
{
type: "confirm",
name: "replace",
message: `File or directory \`${name}\` already exists. Continue anyway?`,
initial: false,
},
]);
if (!replace)
return;
}
fs.rmSync(name, { recursive: true, force: true });
console.log("Copying files...");
fs.cpSync(path.resolve(__dirname, "template"), name, { recursive: true });
replaceInFileSync({ files: `${name}/**/*`, from: /<app_name>/g, to: name });
console.log("Installing packages...");
spawnSync("bash", ["-c", `cd ${name} && ${pkgManager} install`], {
cwd: process.cwd(),
detached: true,
stdio: "inherit",
});
console.log(`Done! Created ${name}. Run \`cd ${name} && ${pkgManager} start\` to run it on your Flipper.`);
})();

View File

@@ -0,0 +1,22 @@
{
"name": "@flipperdevices/create-fz-app",
"version": "0.1.0",
"description": "Template package for JS apps Flipper Zero",
"bin": "index.js",
"type": "module",
"keywords": [
"flipper",
"flipper zero"
],
"author": "Flipper Devices",
"license": "GPL-3.0-only",
"repository": {
"type": "git",
"url": "git+https://github.com/flipperdevices/flipperzero-firmware.git",
"directory": "applications/system/js_app/packages/create-fz-app"
},
"dependencies": {
"prompts": "^2.4.2",
"replace-in-file": "^8.2.0"
}
}

View File

@@ -0,0 +1,373 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
prompts:
specifier: ^2.4.2
version: 2.4.2
replace-in-file:
specifier: ^8.2.0
version: 8.2.0
packages:
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-regex@6.1.0:
resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
engines: {node: '>=12'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
ansi-styles@6.2.1:
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
engines: {node: '>=12'}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
brace-expansion@2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
chalk@5.3.0:
resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
foreground-child@3.3.0:
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
engines: {node: '>=14'}
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
glob@10.4.5:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
hasBin: true
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
kleur@3.0.3:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
path-scurry@1.11.1:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
prompts@2.4.2:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
replace-in-file@8.2.0:
resolution: {integrity: sha512-hMsQtdYHwWviQT5ZbNsgfu0WuCiNlcUSnnD+aHAL081kbU9dPkPocDaHlDvAHKydTWWpx1apfcEcmvIyQk3CpQ==}
engines: {node: '>=18'}
hasBin: true
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
string-width@5.1.2:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
strip-ansi@7.1.0:
resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
engines: {node: '>=12'}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
hasBin: true
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
wrap-ansi@8.1.0:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
yargs@17.7.2:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
snapshots:
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
string-width-cjs: string-width@4.2.3
strip-ansi: 7.1.0
strip-ansi-cjs: strip-ansi@6.0.1
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
'@pkgjs/parseargs@0.11.0':
optional: true
ansi-regex@5.0.1: {}
ansi-regex@6.1.0: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
ansi-styles@6.2.1: {}
balanced-match@1.0.2: {}
brace-expansion@2.0.1:
dependencies:
balanced-match: 1.0.2
chalk@5.3.0: {}
cliui@8.0.1:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
color-name@1.1.4: {}
cross-spawn@7.0.3:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
eastasianwidth@0.2.0: {}
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
escalade@3.2.0: {}
foreground-child@3.3.0:
dependencies:
cross-spawn: 7.0.3
signal-exit: 4.1.0
get-caller-file@2.0.5: {}
glob@10.4.5:
dependencies:
foreground-child: 3.3.0
jackspeak: 3.4.3
minimatch: 9.0.5
minipass: 7.1.2
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
is-fullwidth-code-point@3.0.0: {}
isexe@2.0.0: {}
jackspeak@3.4.3:
dependencies:
'@isaacs/cliui': 8.0.2
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
kleur@3.0.3: {}
lru-cache@10.4.3: {}
minimatch@9.0.5:
dependencies:
brace-expansion: 2.0.1
minipass@7.1.2: {}
package-json-from-dist@1.0.1: {}
path-key@3.1.1: {}
path-scurry@1.11.1:
dependencies:
lru-cache: 10.4.3
minipass: 7.1.2
prompts@2.4.2:
dependencies:
kleur: 3.0.3
sisteransi: 1.0.5
replace-in-file@8.2.0:
dependencies:
chalk: 5.3.0
glob: 10.4.5
yargs: 17.7.2
require-directory@2.1.1: {}
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
shebang-regex@3.0.0: {}
signal-exit@4.1.0: {}
sisteransi@1.0.5: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
string-width@5.1.2:
dependencies:
eastasianwidth: 0.2.0
emoji-regex: 9.2.2
strip-ansi: 7.1.0
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
strip-ansi@7.1.0:
dependencies:
ansi-regex: 6.1.0
which@2.0.2:
dependencies:
isexe: 2.0.0
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi@8.1.0:
dependencies:
ansi-styles: 6.2.1
string-width: 5.1.2
strip-ansi: 7.1.0
y18n@5.0.8: {}
yargs-parser@21.1.1: {}
yargs@17.7.2:
dependencies:
cliui: 8.0.1
escalade: 3.2.0
get-caller-file: 2.0.5
require-directory: 2.1.1
string-width: 4.2.3
y18n: 5.0.8
yargs-parser: 21.1.1

View File

@@ -0,0 +1,2 @@
/dist
node_modules/

View File

@@ -0,0 +1,23 @@
{
build: {
// Where to put the compiled file
output: "dist/<app_name>.js",
// Whether to reduce the final file size at the cost of readability and
// clarity of error messages
minify: false,
// Set this to `false` if you've thoroughly read the documentation and
// are sure that you can use manual version checks to your advantage
enforceSdkVersion: true,
},
upload: {
// Where to grab the file from. If you're not doing any extra processing
// after the SDK, this should match `build.output`
input: "dist/<app_name>.js",
// Where to put the file on the device
output: "/ext/apps/Scripts/<app_name>.js",
},
}

View File

@@ -0,0 +1,30 @@
// import modules
// caution: `eventLoop` HAS to be imported before `gui`, and `gui` HAS to be
// imported before any `gui` submodules.
import * as eventLoop from "@flipperdevices/fz-sdk/event_loop";
import * as gui from "@flipperdevices/fz-sdk/gui";
import * as dialog from "@flipperdevices/fz-sdk/gui/dialog";
// a common pattern is to declare all the views that your app uses on one object
const views = {
dialog: dialog.makeWith({
header: "Hello from <app_name>",
text: "Check out index.ts and\nchange something :)",
center: "Gonna do that!",
}),
};
// stop app on center button press
eventLoop.subscribe(views.dialog.input, (_sub, button, eventLoop) => {
if (button === "center")
eventLoop.stop();
}, eventLoop);
// stop app on back button press
eventLoop.subscribe(gui.viewDispatcher.navigation, (_sub, _item, eventLoop) => {
eventLoop.stop();
}, eventLoop);
// run app
gui.viewDispatcher.switchTo(views.dialog);
eventLoop.run();

View File

@@ -0,0 +1,12 @@
{
"name": "<app_name>",
"version": "1.0.0",
"scripts": {
"build": "tsc && node node_modules/@flipperdevices/fz-sdk/sdk.js build",
"start": "npm run build && node node_modules/@flipperdevices/fz-sdk/sdk.js upload"
},
"devDependencies": {
"@flipperdevices/fz-sdk": "^0.1",
"typescript": "^5.6.3"
}
}

View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"outDir": "dist",
"checkJs": true,
"module": "CommonJS",
"noLib": true,
"target": "ES2015",
},
"files": [
"./node_modules/@flipperdevices/fz-sdk/global.d.ts",
],
"include": [
"./**/*.ts",
"./**/*.js"
],
"exclude": [
"./node_modules/**/*",
"dist/**/*",
],
}

View File

@@ -0,0 +1 @@
docs/

View File

@@ -0,0 +1,31 @@
# Flipper Zero JavaScript SDK
This package contains official tooling and typings for developing Flipper Zero
applications in JavaScript.
## Getting started
Create your application using the interactive wizard:
```shell
npx @flipperdevices/create-fz-app@latest
```
Then, enter the directory with your application and launch it:
```shell
cd my-flip-app
npm start
```
You are free to use `pnpm` or `yarn` instead of `npm`.
## Versioning
For each version of this package, the major and minor components match those of
the Flipper Zero JS SDK version that that package version targets. This version
follows semver. For example, apps compiled with SDK version `0.1.0` will be
compatible with SDK versions `0.1`...`1.0` (not including `1.0`).
Every API has a version history reflected in its JSDoc comment. It is heavily
recommended to check SDK compatibility using a combination of
`sdkCompatibilityStatus`, `isSdkCompatible`, `assertSdkCompatibility` depending
on your use case.
## Documentation
Check out the [JavaScript section in the Developer Documentation](https://developer.flipper.net/flipperzero/doxygen/js.html)

View File

@@ -1,8 +1,10 @@
/** /**
* @brief Special key codes that this module recognizes * @brief Special key codes that this module recognizes
* @version Added in JS SDK 0.1
*/ */
export type ModifierKey = "CTRL" | "SHIFT" | "ALT" | "GUI"; export type ModifierKey = "CTRL" | "SHIFT" | "ALT" | "GUI";
/** @version Added in JS SDK 0.1 */
export type MainKey = export type MainKey =
"DOWN" | "LEFT" | "RIGHT" | "UP" | "DOWN" | "LEFT" | "RIGHT" | "UP" |
@@ -14,6 +16,9 @@ export type MainKey =
"F11" | "F12" | "F13" | "F14" | "F15" | "F16" | "F17" | "F18" | "F19" | "F11" | "F12" | "F13" | "F14" | "F15" | "F16" | "F17" | "F18" | "F19" |
"F20" | "F21" | "F22" | "F23" | "F24" | "F20" | "F21" | "F22" | "F23" | "F24" |
"NUM0" | "NUM1" | "NUM2" | "NUM3" | "NUM4" | "NUM5" | "NUM6" | "NUM7" |
"NUM8" | "NUM9" |
"\n" | " " | "!" | "\"" | "#" | "$" | "%" | "&" | "'" | "(" | ")" | "*" | "\n" | " " | "!" | "\"" | "#" | "$" | "%" | "&" | "'" | "(" | ")" | "*" |
"+" | "," | "-" | "." | "/" | ":" | ";" | "<" | ">" | "=" | "?" | "@" | "[" | "+" | "," | "-" | "." | "/" | ":" | ";" | "<" | ">" | "=" | "?" | "@" | "[" |
"]" | "\\" | "^" | "_" | "`" | "{" | "}" | "|" | "~" | "]" | "\\" | "^" | "_" | "`" | "{" | "}" | "|" | "~" |
@@ -28,16 +33,19 @@ export type MainKey =
"m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" |
"y" | "z"; "y" | "z";
/** @version Added in JS SDK 0.1 */
export type KeyCode = MainKey | ModifierKey | number; export type KeyCode = MainKey | ModifierKey | number;
/** /**
* @brief Initializes the module * @brief Initializes the module
* @param settings USB device settings. Omit to select default parameters * @param settings USB device settings. Omit to select default parameters
* @version Added in JS SDK 0.1
*/ */
export declare function setup(settings?: { vid: number, pid: number, mfrName?: string, prodName?: string }): void; export declare function setup(settings?: { vid: number, pid: number, mfrName?: string, prodName?: string, layoutPath?: string }): void;
/** /**
* @brief Tells whether the virtual USB HID device has successfully connected * @brief Tells whether the virtual USB HID device has successfully connected
* @version Added in JS SDK 0.1
*/ */
export declare function isConnected(): boolean; export declare function isConnected(): boolean;
@@ -46,6 +54,7 @@ export declare function isConnected(): boolean;
* @param keys The arguments represent a set of keys to. Out of that set, only * @param keys The arguments represent a set of keys to. Out of that set, only
* one of the keys may represent a "main key" (see `MainKey`), with * one of the keys may represent a "main key" (see `MainKey`), with
* the rest being modifier keys (see `ModifierKey`). * the rest being modifier keys (see `ModifierKey`).
* @version Added in JS SDK 0.1
*/ */
export declare function press(...keys: KeyCode[]): void; export declare function press(...keys: KeyCode[]): void;
@@ -54,6 +63,7 @@ export declare function press(...keys: KeyCode[]): void;
* @param keys The arguments represent a set of keys to. Out of that set, only * @param keys The arguments represent a set of keys to. Out of that set, only
* one of the keys may represent a "main key" (see `MainKey`), with * one of the keys may represent a "main key" (see `MainKey`), with
* the rest being modifier keys (see `ModifierKey`). * the rest being modifier keys (see `ModifierKey`).
* @version Added in JS SDK 0.1
*/ */
export declare function hold(...keys: KeyCode[]): void; export declare function hold(...keys: KeyCode[]): void;
@@ -62,6 +72,7 @@ export declare function hold(...keys: KeyCode[]): void;
* @param keys The arguments represent a set of keys to. Out of that set, only * @param keys The arguments represent a set of keys to. Out of that set, only
* one of the keys may represent a "main key" (see `MainKey`), with * one of the keys may represent a "main key" (see `MainKey`), with
* the rest being modifier keys (see `ModifierKey`). * the rest being modifier keys (see `ModifierKey`).
* @version Added in JS SDK 0.1
*/ */
export declare function release(...keys: KeyCode[]): void; export declare function release(...keys: KeyCode[]): void;
@@ -69,6 +80,7 @@ export declare function release(...keys: KeyCode[]): void;
* @brief Prints a string by repeatedly pressing and releasing keys * @brief Prints a string by repeatedly pressing and releasing keys
* @param string The string to print * @param string The string to print
* @param delay How many milliseconds to wait between key presses * @param delay How many milliseconds to wait between key presses
* @version Added in JS SDK 0.1
*/ */
export declare function print(string: string, delay?: number): void; export declare function print(string: string, delay?: number): void;
@@ -77,5 +89,29 @@ export declare function print(string: string, delay?: number): void;
* "Enter" after printing the string * "Enter" after printing the string
* @param string The string to print * @param string The string to print
* @param delay How many milliseconds to wait between key presses * @param delay How many milliseconds to wait between key presses
* @version Added in JS SDK 0.1
*/ */
export declare function println(string: string, delay?: number): void; export declare function println(string: string, delay?: number): void;
/**
* @brief Prints a string by Alt+Numpad method - works only on Windows!
* @param string The string to print
* @param delay How many milliseconds to wait between key presses
* @version Added in JS SDK 0.1
*/
export declare function altPrint(string: string, delay?: number): void;
/**
* @brief Prints a string by Alt+Numpad method - works only on Windows!
* Presses "Enter" after printing the string
* @param string The string to print
* @param delay How many milliseconds to wait between key presses
* @version Added in JS SDK 0.1
*/
export declare function altPrintln(string: string, delay?: number): void;
/**
* @brief Releases usb, optional, but allows to switch usb profile
* @version Added in JS SDK 0.1
*/
export declare function quit(): void;

View File

@@ -0,0 +1 @@
# Welcome

View File

@@ -0,0 +1,182 @@
/**
* Module for dealing with events
*
* ```js
* let eventLoop = require("event_loop");
* ```
*
* The event loop is central to event-based programming in many frameworks, and
* our JS subsystem is no exception. It is a good idea to familiarize yourself
* with the event loop first before using any of the advanced modules (e.g. GPIO
* and GUI).
*
* # Conceptualizing the event loop
* If you ever wrote JavaScript before, you have definitely seen callbacks. It's
* when a function accepts another function (usually an anonymous one) as one of
* the arguments, which it will call later on, e.g. when an event happens or
* when data becomes ready:
* ```js
* setTimeout(function() { console.log("Hello, World!") }, 1000);
* ```
*
* Many JavaScript engines employ a queue that the runtime fetches events from
* as they occur, subsequently calling the corresponding callbacks. This is done
* in a long-running loop, hence the name "event loop". Here's the pseudocode
* for a typical event loop:
* ```js
* while(loop_is_running()) {
* if(event_available_in_queue()) {
* let event = fetch_event_from_queue();
* let callback = get_callback_associated_with(event);
* if(callback)
* callback(get_extra_data_for(event));
* } else {
* // avoid wasting CPU time
* sleep_until_any_event_becomes_available();
* }
* }
* ```
*
* Most JS runtimes enclose the event loop within themselves, so that most JS
* programmers does not even need to be aware of its existence. This is not the
* case with our JS subsystem.
*
* # Example
* This is how one would write something similar to the `setTimeout` example
* above:
* ```js
* // import module
* let eventLoop = require("event_loop");
*
* // create an event source that will fire once 1 second after it has been created
* let timer = eventLoop.timer("oneshot", 1000);
*
* // subscribe a callback to the event source
* eventLoop.subscribe(timer, function(_subscription, _item, eventLoop) {
* print("Hello, World!");
* eventLoop.stop();
* }, eventLoop); // notice this extra argument. we'll come back to this later
*
* // run the loop until it is stopped
* eventLoop.run();
*
* // the previous line will only finish executing once `.stop()` is called, hence
* // the following line will execute only after "Hello, World!" is printed
* print("Stopped");
* ```
*
* I promised you that we'll come back to the extra argument after the callback
* function. Our JavaScript engine does not support closures (anonymous
* functions that access values outside of their arguments), so we ask
* `subscribe` to pass an outside value (namely, `eventLoop`) as an argument to
* the callback so that we can access it. We can modify this extra state:
* ```js
* // this timer will fire every second
* let timer = eventLoop.timer("periodic", 1000);
* eventLoop.subscribe(timer, function(_subscription, _item, counter, eventLoop) {
* print("Counter is at:", counter);
* if(counter === 10)
* eventLoop.stop();
* // modify the extra arguments that will be passed to us the next time
* return [counter + 1, eventLoop];
* }, 0, eventLoop);
* ```
*
* Because we have two extra arguments, if we return anything other than an
* array of length 2, the arguments will be kept as-is for the next call.
*
* The first two arguments that get passed to our callback are:
* - The subscription manager that lets us `.cancel()` our subscription
* - The event item, used for events that have extra data. Timer events do
* not, they just produce `undefined`.
*
* @version Added in JS SDK 0.1
* @module
*/
/**
* @ignore
*/
type Lit = undefined | null | {};
/**
* Subscription control interface
* @version Added in JS SDK 0.1
*/
export interface Subscription {
/**
* Cancels the subscription, preventing any future events managed by the
* subscription from firing
* @version Added in JS SDK 0.1
*/
cancel(): void;
}
/**
* Opaque event source identifier
* @version Added in JS SDK 0.1
*/
export type Contract<Item = undefined> = symbol & { "__tag__": "contract" };
// introducing a nominal type in a hacky way; the `__tag__` property doesn't really exist.
/**
* A callback can be assigned to an event loop to listen to an event. It may
* return an array with values that will be passed to it as arguments the next
* time that it is called. The first argument is always the subscription
* manager, and the second argument is always the item that trigged the event.
* The type of the item is defined by the event source.
* @version Added in JS SDK 0.1
*/
export type Callback<Item, Args extends Lit[]> = (subscription: Subscription, item: Item, ...args: Args) => Args | undefined | void;
/**
* Subscribes a callback to an event
* @param contract Event identifier
* @param callback Function to call when the event is triggered
* @param args Initial arguments passed to the callback
* @version Added in JS SDK 0.1
*/
export function subscribe<Item, Args extends Lit[]>(contract: Contract<Item>, callback: Callback<Item, Args>, ...args: Args): Subscription;
/**
* Runs the event loop until it is stopped (potentially never)
* @version Added in JS SDK 0.1
*/
export function run(): void | never;
/**
* Stops the event loop
* @version Added in JS SDK 0.1
*/
export function stop(): void;
/**
* Creates a timer event that can be subscribed to just like any other event
* @param mode Either `"oneshot"` or `"periodic"`
* @param interval Timer interval in milliseconds
* @version Added in JS SDK 0.1
*/
export function timer(mode: "oneshot" | "periodic", interval: number): Contract;
/**
* Message queue
* @version Added in JS SDK 0.1
*/
export declare class Queue<T> {
/**
* Message event
* @version Added in JS SDK 0.1
*/
input: Contract<T>;
/**
* Sends a message to the queue
* @param message message to send
* @version Added in JS SDK 0.1
*/
send(message: T): void;
}
/**
* Creates a message queue
* @param length maximum queue capacity
* @version Added in JS SDK 0.1
*/
export function queue<T>(length: number): Queue<T>;

View File

@@ -0,0 +1,41 @@
/**
* Module for querying device properties
* @version Added in JS SDK 0.1
* @module
*/
/**
* @brief Returns the device model
* @version Added in JS SDK 0.1
*/
export declare function getModel(): string;
/**
* @brief Returns the name of the virtual dolphin
* @version Added in JS SDK 0.1
*/
export declare function getName(): string;
/**
* @brief Returns the battery charge percentage
* @version Added in JS SDK 0.1
*/
export declare function getBatteryCharge(): number;
/**
* @warning Do **NOT** use this to check the presence or absence of features. If
* you do, I'm gonna be sad :( Instead, refer to `checkSdkFeatures` and
* other similar mechanisms.
* @note Original firmware reports `"flipperdevices"`.
* @version Added in JS SDK 0.1
*/
export declare const firmwareVendor: string;
/**
* @warning Do **NOT** use this to check the presence or absence of features. If
* you do, I'm gonna be sad :( Instead, refer to
* `checkSdkCompatibility` and other similar mechanisms.
* @note You're looking at JS SDK 0.1
* @version Added in JS SDK 0.1
*/
export declare const jsSdkVersion: [number, number];

View File

@@ -0,0 +1,402 @@
/**
* Things from this module are automatically available to you without having to
* explicitly import anything.
*
* # SDK versioning and features
*
* ## Motivation
* It is very important that you check that features are implemented before you
* use them. By adding the necessary checks, you ensure that your users get a
* clear warning instead of a cryptic error message when running the script.
*
* This system has been designed in collaboration with our community in order to
* make things better for everybody involved. You can find out more in this
* discussion: https://github.com/flipperdevices/flipperzero-firmware/pull/3961
*
* ## Community agreement
* Each interpreter implementation (aka "JS SDK", aka "JS API"), including
* those found in third-party firmware distributions, defines two markers for
* signaling what it supports: the **SDK version** and the
* **extra feature set**.
*
* The **SDK version** consists of two semver-like integer components: the major
* version and the minor version. Like semver, the major version is bumped when
* a breaking change is introduced (i.e. one that would require correction of
* apps by their developers), and the minor version is bumped when a new
* non-breaking feature is introduced. Because we have adopted TypeScript,
* the https://www.semver-ts.org/ standard is used to determine whether a change
* is breaking or not. The basis of `semver-ts` is the "no new red squiggles"
* rule.
*
* Every major version is associated with a set of **extra features** that are
* present in some firmware distributions but not others. Distributions may
* cross-port features between each other, until at some point they get ported
* into the upstream firmware distribution. With the next major release of the
* JS SDK, all extra features present in the upstream distribution are now
* declared **baseline features**, and thus no longer recognized as "extra
* features".
*
* Before using a feature, you must check that the interpreter that you're
* running on actually supports it. If you don't, the portability of your
* application will suffer.
*
* ## Implementation
* Use the following functions to check version compatibility:
* - `checkSdkCompatibility` when your script absolutely cannot function on an
* incompatible interpreter
* - `isSdkCompatible` when your script can leverage multiple interpreter
* editions to its advantage
* - `sdkCompatibilityStatus` when you need a detailed status on compatibility
*
* Use the following functions to check feature compatibility:
* - `checkSdkFeatures` when your script absolutely cannot function on an
* incompatible interpreter
* - `doesSdkSupport` when your script can leverage multiple interpreter
* editions to its advantage
*
* ## Automatic version enforcement
* The SDK will automatically insert a call to `checkSdkCompatibility` in the
* beginning of the resulting script. If you would like to disable this check
* and instead use other manual compatibility checking facilities, edit your
* `fz-sdk.config.json5`.
*
* # Standard library
* Standard library features are mostly unimplemented. This module defines,
* among other things, the features that _are_ implemented.
*
* @version Added in JS SDK 0.1
* @module
*/
/**
* @brief Checks compatibility between the script and the JS SDK that the
* firmware provides
*
* @note You're looking at JS SDK v0.1
*
* @param expectedMajor JS SDK major version expected by the script
* @param expectedMinor JS SDK minor version expected by the script
* @returns Compatibility status:
* - `"compatible"` if the script and the JS SDK are compatible
* - `"firmwareTooOld"` if the expected major version is larger than the
* version of the firmware, or if the expected minor version is larger than
* the version of the firmware
* - `"firmwareTooNew"` if the expected major version is lower than the
* version of the firmware
* @version Added in JS SDK 0.1
*/
declare function sdkCompatibilityStatus(expectedMajor: number, expectedMinor: number):
"compatible" | "firmwareTooOld" | "firmwareTooNew";
/**
* @brief Checks compatibility between the script and the JS SDK that the
* firmware provides in a boolean fashion
*
* @note You're looking at JS SDK v0.1
*
* @param expectedMajor JS SDK major version expected by the script
* @param expectedMinor JS SDK minor version expected by the script
* @returns `true` if the two are compatible, `false` otherwise
* @version Added in JS SDK 0.1
*/
declare function isSdkCompatible(expectedMajor: number, expectedMinor: number): boolean;
/**
* @brief Asks the user whether to continue executing the script if the versions
* are not compatible. Does nothing if they are.
*
* @note You're looking at JS SDK v0.1
*
* @param expectedMajor JS SDK major version expected by the script
* @param expectedMinor JS SDK minor version expected by the script
* @version Added in JS SDK 0.1
*/
declare function checkSdkCompatibility(expectedMajor: number, expectedMinor: number): void | never;
/**
* @brief Checks whether all of the specified extra features are supported by
* the interpreter.
* @warning This function will return `false` if a queried feature is now
* recognized as a baseline feature. For more info, consult the module
* documentation.
* @param features Array of named features to query
* @version Added in JS SDK 0.1
*/
declare function doesSdkSupport(features: string[]): boolean;
/**
* @brief Checks whether all of the specified extra features are supported by
* the interpreter, asking the user if they want to continue running the
* script if they're not.
* @warning This function will act as if the feature is not implemented for
* features that are now recognized as baseline features. For more
* info, consult the module documentation.
* @param features Array of named features to query
* @version Added in JS SDK 0.1
*/
declare function checkSdkFeatures(features: string[]): void | never;
/**
* @brief Pauses JavaScript execution for a while
* @param ms How many milliseconds to pause the execution for
* @version Added in JS SDK 0.1
*/
declare function delay(ms: number): void;
/**
* @brief Prints to the GUI console view
* @param args The arguments are converted to strings, concatenated without any
* spaces in between and printed to the console view
* @version Added in JS SDK 0.1
*/
declare function print(...args: any[]): void;
/**
* @brief Converts a string to a number
* @param text The string to convert to a number
* @param base Integer base (`2`...`16`), default: 10
* @version Added in JS SDK 0.1
*/
declare function parseInt(text: string, base?: number): number;
/**
* @brief Path to the directory containing the current script
* @version Added in JS SDK 0.1
*/
declare const __dirname: string;
/**
* @brief Path to the current script file
* @version Added in JS SDK 0.1
*/
declare const __filename: string;
/**
* @brief Runs a JS file and returns value from it
*
* Reads a file at the specified path and runs it as JS, returning the last evaluated value.
*
* The result is cached and this filepath will not re-evaluated on future
* load() calls for this session.
*
* @param path The path to the file
* @param scope An object to use as global scope while running this file
* @version Added in JS SDK 0.1
*/
declare function load(path: string, scope?: object): any;
/**
* @brief Return 1-byte string whose ASCII code is the integer `n`
*
* If `n` is not numeric or outside of `0-255` range, `null` is returned
*
* @param n The ASCII code to convert to string
* @version Added in JS SDK 0.1
*/
declare function chr(n: number): string | null;
/**
* @brief Loads a natively implemented module
* @param module The name of the module to load
* @version Added in JS SDK 0.1
*/
declare function require(module: string): any;
/**
* @brief mJS Foreign Pointer type
*
* JavaScript code cannot do anything with values of `RawPointer` type except
* acquire them from native code and pass them right back to other parts of
* native code. These values cannot be turned into something meaningful, nor can
* be they modified.
*
* @version Added in JS SDK 0.1
*/
declare type RawPointer = symbol & { "__tag__": "raw_ptr" };
// introducing a nominal type in a hacky way; the `__tag__` property doesn't really exist.
/**
* @brief Holds raw bytes
* @version Added in JS SDK 0.1
*/
declare class ArrayBuffer {
/**
* @brief The pointer to the byte buffer
* @note Like other `RawPointer` values, this value is essentially useless
* to JS code.
* @version Added in JS SDK 0.1
*/
getPtr: RawPointer;
/**
* @brief The length of the buffer in bytes
* @version Added in JS SDK 0.1
*/
byteLength: number;
/**
* @brief Creates an `ArrayBuffer` that contains a sub-part of the buffer
* @param start The index of the byte in the source buffer to be used as the
* start for the new buffer
* @param end The index of the byte in the source buffer that follows the
* byte to be used as the last byte for the new buffer
* @version Added in JS SDK 0.1
*/
slice(start: number, end?: number): ArrayBuffer;
}
declare function ArrayBuffer(): ArrayBuffer;
declare type ElementType = "u8" | "i8" | "u16" | "i16" | "u32" | "i32";
declare class TypedArray<E extends ElementType> {
/**
* @brief The length of the buffer in bytes
* @version Added in JS SDK 0.1
*/
byteLength: number;
/**
* @brief The length of the buffer in typed elements
* @version Added in JS SDK 0.1
*/
length: number;
/**
* @brief The underlying `ArrayBuffer`
* @version Added in JS SDK 0.1
*/
buffer: ArrayBuffer;
}
declare class Uint8Array extends TypedArray<"u8"> { }
declare class Int8Array extends TypedArray<"i8"> { }
declare class Uint16Array extends TypedArray<"u16"> { }
declare class Int16Array extends TypedArray<"i16"> { }
declare class Uint32Array extends TypedArray<"u32"> { }
declare class Int32Array extends TypedArray<"i32"> { }
declare function Uint8Array(data: ArrayBuffer | number | number[]): Uint8Array;
declare function Int8Array(data: ArrayBuffer | number | number[]): Int8Array;
declare function Uint16Array(data: ArrayBuffer | number | number[]): Uint16Array;
declare function Int16Array(data: ArrayBuffer | number | number[]): Int16Array;
declare function Uint32Array(data: ArrayBuffer | number | number[]): Uint32Array;
declare function Int32Array(data: ArrayBuffer | number | number[]): Int32Array;
declare const console: {
/**
* @brief Prints to the UART logs at the `[I]` level
* @param args The arguments are converted to strings, concatenated without any
* spaces in between and printed to the logs
* @version Added in JS SDK 0.1
*/
log(...args: any[]): void;
/**
* @brief Prints to the UART logs at the `[D]` level
* @param args The arguments are converted to strings, concatenated without any
* spaces in between and printed to the logs
* @version Added in JS SDK 0.1
*/
debug(...args: any[]): void;
/**
* @brief Prints to the UART logs at the `[W]` level
* @param args The arguments are converted to strings, concatenated without any
* spaces in between and printed to the logs
* @version Added in JS SDK 0.1
*/
warn(...args: any[]): void;
/**
* @brief Prints to the UART logs at the `[E]` level
* @param args The arguments are converted to strings, concatenated without any
* spaces in between and printed to the logs
* @version Added in JS SDK 0.1
*/
error(...args: any[]): void;
};
declare class Array<T> {
/**
* @brief Takes items out of the array
*
* Removes elements from the array and returns them in a new array
*
* @param start The index to start taking elements from
* @param deleteCount How many elements to take
* @returns The elements that were taken out of the original array as a new
* array
* @version Added in JS SDK 0.1
*/
splice(start: number, deleteCount: number): T[];
/**
* @brief Adds a value to the end of the array
* @param value The value to add
* @returns New length of the array
* @version Added in JS SDK 0.1
*/
push(value: T): number;
/**
* @brief How many elements there are in the array
* @version Added in JS SDK 0.1
*/
length: number;
}
declare class String {
/**
* @brief How many characters there are in the string
* @version Added in JS SDK 0.1
*/
length: number;
/**
* @brief Returns the character code at an index in the string
* @param index The index to consult
* @version Added in JS SDK 0.1
*/
charCodeAt(index: number): number;
/**
* See `charCodeAt`
* @version Added in JS SDK 0.1
*/
at(index: number): number;
/**
* @brief Return index of first occurrence of substr within the string or `-1` if not found
* @param substr The string to search for
* @param fromIndex The index to start searching from
* @version Added in JS SDK 0.1
*/
indexOf(substr: string, fromIndex?: number): number;
/**
* @brief Return a substring between two indices
* @param start The index to start substring at
* @param end The index to end substring at
* @version Added in JS SDK 0.1
*/
slice(start: number, end?: number): string;
/**
* @brief Return this string transformed to upper case
* @version Added in JS SDK 0.1
*/
toUpperCase(): string;
/**
* @brief Return this string transformed to lower case
* @version Added in JS SDK 0.1
*/
toLowerCase(): string;
}
declare class Boolean { }
declare class Function { }
declare class Number {
/**
* @brief Converts this number to a string
* @param base Integer base (`2`...`16`), default: 10
* @version Added in JS SDK 0.1
*/
toString(base?: number): string;
}
declare class Object { }
declare class RegExp { }
declare interface IArguments { }
declare type Partial<O extends object> = { [K in keyof O]?: O[K] };

View File

@@ -1,5 +1,37 @@
/**
* Module for accessing the GPIO (General Purpose Input/Output) ports
*
* ```js
* let eventLoop = require("event_loop");
* let gpio = require("gpio");
* ```
*
* This module depends on the `event_loop` module, so it _must_ only be imported
* after `event_loop` is imported.
*
* # Example
* ```js
* let eventLoop = require("event_loop");
* let gpio = require("gpio");
*
* let led = gpio.get("pc3");
* led.init({ direction: "out", outMode: "push_pull" });
*
* led.write(true);
* delay(1000);
* led.write(false);
* delay(1000);
* ```
*
* @version Added in JS SDK 0.1
* @module
*/
import type { Contract } from "../event_loop"; import type { Contract } from "../event_loop";
/**
* @version Added in JS SDK 0.1
*/
export interface Mode { export interface Mode {
direction: "in" | "out"; direction: "in" | "out";
outMode?: "push_pull" | "open_drain"; outMode?: "push_pull" | "open_drain";
@@ -8,31 +40,39 @@ export interface Mode {
pull?: "up" | "down"; pull?: "up" | "down";
} }
/**
* @version Added in JS SDK 0.1
*/
export interface Pin { export interface Pin {
/** /**
* Configures a pin. This may be done several times. * Configures a pin. This may be done several times.
* @param mode Pin configuration object * @param mode Pin configuration object
* @version Added in JS SDK 0.1
*/ */
init(mode: Mode): void; init(mode: Mode): void;
/** /**
* Sets the output value of a pin if it's been configured with * Sets the output value of a pin if it's been configured with
* `direction: "out"`. * `direction: "out"`.
* @param value Logic value to output * @param value Logic value to output
* @version Added in JS SDK 0.1
*/ */
write(value: boolean): void; write(value: boolean): void;
/** /**
* Gets the input value of a pin if it's been configured with * Gets the input value of a pin if it's been configured with
* `direction: "in"`, but not `inMode: "analog"`. * `direction: "in"`, but not `inMode: "analog"`.
* @version Added in JS SDK 0.1
*/ */
read(): boolean; read(): boolean;
/** /**
* Gets the input voltage of a pin in millivolts if it's been configured * Gets the input voltage of a pin in millivolts if it's been configured
* with `direction: "in"` and `inMode: "analog"` * with `direction: "in"` and `inMode: "analog"`
* @version Added in JS SDK 0.1
*/ */
read_analog(): number; readAnalog(): number;
/** /**
* Returns an `event_loop` event that can be used to listen to interrupts, * Returns an `event_loop` event that can be used to listen to interrupts,
* as configured by `init` * as configured by `init`
* @version Added in JS SDK 0.1
*/ */
interrupt(): Contract; interrupt(): Contract;
} }
@@ -41,5 +81,6 @@ export interface Pin {
* Returns an object that can be used to manage a GPIO pin. For the list of * Returns an object that can be used to manage a GPIO pin. For the list of
* available pins, see https://docs.flipper.net/gpio-and-modules#miFsS * available pins, see https://docs.flipper.net/gpio-and-modules#miFsS
* @param pin Pin name (e.g. `"PC3"`) or number (e.g. `7`) * @param pin Pin name (e.g. `"PC3"`) or number (e.g. `7`)
* @version Added in JS SDK 0.1
*/ */
export function get(pin: string | number): Pin; export function get(pin: string | number): Pin;

View File

@@ -0,0 +1,41 @@
/**
* Displays a byte input keyboard.
*
* <img src="../images/byte_input.png" width="200" alt="Sample screenshot of the view" />
*
* ```js
* let eventLoop = require("event_loop");
* let gui = require("gui");
* let byteInputView = require("gui/byte_input");
* ```
*
* This module depends on the `gui` module, which in turn depends on the
* `event_loop` module, so they _must_ be imported in this order. It is also
* recommended to conceptualize these modules first before using this one.
*
* # Example
* For an example refer to the `gui.js` example script.
*
* # View props
* - `header`: Text displayed at the top of the screen
* - `length`: Length of data to edit
* - `defaultData`: Data to show by default
*
* @version Added in JS SDK 0.1
* @module
*/
import type { View, ViewFactory } from ".";
import type { Contract } from "../event_loop";
type Props = {
header: string,
length: number,
defaultData: Uint8Array | ArrayBuffer,
}
declare class ByteInput extends View<Props> {
input: Contract<string>;
}
declare class ByteInputFactory extends ViewFactory<Props, ByteInput> { }
declare const factory: ByteInputFactory;
export = factory;

View File

@@ -0,0 +1,45 @@
/**
* Displays a dialog with up to three options.
*
* <img src="../images/dialog.png" width="200" alt="Sample screenshot of the view" />
*
* ```js
* let eventLoop = require("event_loop");
* let gui = require("gui");
* let dialogView = require("gui/dialog");
* ```
*
* This module depends on the `gui` module, which in turn depends on the
* `event_loop` module, so they _must_ be imported in this order. It is also
* recommended to conceptualize these modules first before using this one.
*
* # Example
* For an example refer to the `gui.js` example script.
*
* # View props
* - `header`: Text displayed in bold at the top of the screen
* - `text`: Text displayed in the middle of the string
* - `left`: Text for the left button
* - `center`: Text for the center button
* - `right`: Text for the right button
*
* @version Added in JS SDK 0.1
* @module
*/
import type { View, ViewFactory } from ".";
import type { Contract } from "../event_loop";
type Props = {
header: string,
text: string,
left: string,
center: string,
right: string,
}
declare class Dialog extends View<Props> {
input: Contract<"left" | "center" | "right">;
}
declare class DialogFactory extends ViewFactory<Props, Dialog> { }
declare const factory: DialogFactory;
export = factory;

View File

@@ -0,0 +1,32 @@
/**
* Displays nothing.
*
* <img src="../images/empty.png" width="200" alt="Sample screenshot of the view" />
*
* ```js
* let eventLoop = require("event_loop");
* let gui = require("gui");
* let emptyView = require("gui/empty_screen");
* ```
*
* This module depends on the `gui` module, which in turn depends on the
* `event_loop` module, so they _must_ be imported in this order. It is also
* recommended to conceptualize these modules first before using this one.
*
* # Example
* For an example refer to the GUI example.
*
* # View props
* This view does not have any props.
*
* @version Added in JS SDK 0.1
* @module
*/
import type { View, ViewFactory } from ".";
type Props = {};
declare class EmptyScreen extends View<Props> { }
declare class EmptyScreenFactory extends ViewFactory<Props, EmptyScreen> { }
declare const factory: EmptyScreenFactory;
export = factory;

View File

@@ -0,0 +1,7 @@
/**
* @brief Displays a file picker and returns the selected file, or undefined if cancelled
* @param basePath The path to start at
* @param extension The file extension(s) to show (like `.sub`, `.iso|.img`, `*`)
* @version Added in JS SDK 0.1
*/
export declare function pickFile(basePath: string, extension: string): string | undefined;

View File

@@ -0,0 +1,191 @@
/**
* ```js
* let eventLoop = require("event_loop");
* let gui = require("gui");
* ```
*
* This module depends on the `event_loop` module, so it _must_ only be imported
* after `event_loop` is imported.
*
* ## Conceptualizing GUI
* ### Event loop
* It is highly recommended to familiarize yourself with the event loop first
* before doing GUI-related things.
*
* ### Canvas
* The canvas is just a drawing area with no abstractions over it. Drawing on
* the canvas directly (i.e. not through a viewport) is useful in case you want
* to implement a custom design element, but this is rather uncommon.
*
* ### Viewport
* A viewport is a window into a rectangular portion of the canvas. Applications
* always access the canvas through a viewport.
*
* ### View
* In Flipper's terminology, a "View" is a fullscreen design element that
* assumes control over the entire viewport and all input events. Different
* types of views are available (not all of which are unfortunately currently
* implemented in JS):
* | View | Has JS adapter? |
* |----------------------|------------------|
* | `button_menu` | ❌ |
* | `button_panel` | ❌ |
* | `byte_input` | ✅ |
* | `dialog_ex` | ✅ (as `dialog`) |
* | `empty_screen` | ✅ |
* | `file_browser` | ❌ |
* | `loading` | ✅ |
* | `menu` | ❌ |
* | `number_input` | ❌ |
* | `popup` | ❌ |
* | `submenu` | ✅ |
* | `text_box` | ✅ |
* | `text_input` | ✅ |
* | `variable_item_list` | ❌ |
* | `widget` | ❌ |
*
* In JS, each view has its own set of properties (or just "props"). The
* programmer can manipulate these properties in two ways:
* - Instantiate a `View` using the `makeWith(props)` method, passing an
* object with the initial properties
* - Call `set(name, value)` to modify a property of an existing `View`
*
* ### View Dispatcher
* The view dispatcher holds references to all the views that an application
* needs and switches between them as the application makes requests to do so.
*
* ### Scene Manager
* The scene manager is an optional add-on to the view dispatcher that makes
* managing applications with complex navigation flows easier. It is currently
* inaccessible from JS.
*
* ### Approaches
* In total, there are three different approaches that you may take when writing
* a GUI application:
* | Approach | Use cases | Available from JS |
* |----------------|------------------------------------------------------------------------------|-------------------|
* | ViewPort only | Accessing the graphics API directly, without any of the nice UI abstractions | ❌ |
* | ViewDispatcher | Common UI elements that fit with the overall look of the system | ✅ |
* | SceneManager | Additional navigation flow management for complex applications | ❌ |
*
* # Example
* An example with three different views using the ViewDispatcher approach:
* ```js
* 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");
*
* // Common pattern: declare all the views in an object. This is absolutely not
* // required, but adds clarity to the script.
* let views = {
* // the view dispatcher auto-✨magically✨ remembers views as they are created
* loading: loadingView.make(),
* empty: emptyView.make(),
* demos: submenuView.makeWith({
* items: [
* "Hourglass screen",
* "Empty screen",
* "Exit app",
* ],
* }),
* };
*
* // go to different screens depending on what was selected
* eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, views) {
* if (index === 0) {
* gui.viewDispatcher.switchTo(views.loading);
* } else if (index === 1) {
* gui.viewDispatcher.switchTo(views.empty);
* } else if (index === 2) {
* eventLoop.stop();
* }
* }, gui, eventLoop, views);
*
* // go to the demo chooser screen when the back key is pressed
* eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, gui, views) {
* gui.viewDispatcher.switchTo(views.demos);
* }, gui, views);
*
* // run UI
* gui.viewDispatcher.switchTo(views.demos);
* eventLoop.run();
* ```
*
* @version Added in JS SDK 0.1
* @module
*/
import type { Contract } from "../event_loop";
type Properties = { [K: string]: any };
export declare class View<Props extends Properties> {
/**
* Assign value to property by name
* @param property Name of the property
* @param value Value to assign
* @version Added in JS SDK 0.1
*/
set<P extends keyof Props>(property: P, value: Props[P]): void;
}
export declare class ViewFactory<Props extends Properties, V extends View<Props>> {
/**
* Create view instance with default values, can be changed later with set()
* @version Added in JS SDK 0.1
*/
make(): V;
/**
* Create view instance with custom values, can be changed later with set()
* @param initial Dictionary of property names to values
* @version Added in JS SDK 0.1
*/
makeWith(initial: Partial<Props>): V;
}
/**
* @version Added in JS SDK 0.1
*/
declare class ViewDispatcher {
/**
* Event source for `sendCustom` events
* @version Added in JS SDK 0.1
*/
custom: Contract<number>;
/**
* Event source for navigation events (back key presses)
* @version Added in JS SDK 0.1
*/
navigation: Contract;
/**
* View object currently shown
* @version Added in JS SDK 0.1
*/
currentView: View<any>;
/**
* Sends a number to the custom event handler
* @param event number to send
* @version Added in JS SDK 0.1
*/
sendCustom(event: number): void;
/**
* Switches to a view
* @param assoc View-ViewDispatcher association as returned by `add`
* @version Added in JS SDK 0.1
*/
switchTo(assoc: View<any>): void;
/**
* Sends this ViewDispatcher to the front or back, above or below all other
* GUI viewports
* @param direction Either `"front"` or `"back"`
* @version Added in JS SDK 0.1
*/
sendTo(direction: "front" | "back"): void;
}
/**
* @version Added in JS SDK 0.1
*/
export const viewDispatcher: ViewDispatcher;

View File

@@ -0,0 +1,33 @@
/**
* Displays an animated hourglass icon. Suppresses all `navigation` events,
* making it impossible for the user to exit the view by pressing the back key.
*
* <img src="../images/loading.png" width="200" alt="Sample screenshot of the view" />
*
* ```js
* let eventLoop = require("event_loop");
* let gui = require("gui");
* let loadingView = require("gui/loading");
* ```
*
* This module depends on the `gui` module, which in turn depends on the
* `event_loop` module, so they _must_ be imported in this order. It is also
* recommended to conceptualize these modules first before using this one.
*
* # Example
* For an example refer to the GUI example.
*
* # View props
* This view does not have any props.
*
* @version Added in JS SDK 0.1
* @module
*/
import type { View, ViewFactory } from ".";
type Props = {};
declare class Loading extends View<Props> { }
declare class LoadingFactory extends ViewFactory<Props, Loading> { }
declare const factory: LoadingFactory;
export = factory;

View File

@@ -0,0 +1,39 @@
/**
* Displays a scrollable list of clickable textual entries.
*
* <img src="../images/submenu.png" width="200" alt="Sample screenshot of the view" />
*
* ```js
* let eventLoop = require("event_loop");
* let gui = require("gui");
* let submenuView = require("gui/submenu");
* ```
*
* This module depends on the `gui` module, which in turn depends on the
* `event_loop` module, so they _must_ be imported in this order. It is also
* recommended to conceptualize these modules first before using this one.
*
* # Example
* For an example refer to the GUI example.
*
* # View props
* - `header`: Text displayed at the top of the screen in bold
* - `items`: Array of selectable textual items
*
* @version Added in JS SDK 0.1
* @module
*/
import type { View, ViewFactory } from ".";
import type { Contract } from "../event_loop";
type Props = {
header: string,
items: string[],
};
declare class Submenu extends View<Props> {
chosen: Contract<number>;
}
declare class SubmenuFactory extends ViewFactory<Props, Submenu> { }
declare const factory: SubmenuFactory;
export = factory;

View File

@@ -0,0 +1,41 @@
/**
* Displays a scrollable read-only text field.
*
* <img src="text_box.png" width="200" alt="Sample screenshot of the view" />
*
* ```js
* let eventLoop = require("event_loop");
* let gui = require("gui");
* let textBoxView = require("gui/text_box");
* ```
*
* This module depends on the `gui` module, which in turn depends on the
* `event_loop` module, so they _must_ be imported in this order. It is also
* recommended to conceptualize these modules first before using this one.
*
* # Example
* For an example refer to the `gui.js` example script.
*
* # View props
* - `text`: Text in the text box
* - `font`: The font to display the text in (`"text"` or `"hex"`)
* - `focus`: The initial focus of the text box (`"start"` or `"end"`)
*
* @version Added in JS SDK 0.1
* @module
*/
import type { View, ViewFactory } from ".";
import type { Contract } from "../event_loop";
type Props = {
text: string,
font: "text" | "hex",
focus: "start" | "end",
}
declare class TextBox extends View<Props> {
chosen: Contract<number>;
}
declare class TextBoxFactory extends ViewFactory<Props, TextBox> { }
declare const factory: TextBoxFactory;
export = factory;

View File

@@ -0,0 +1,45 @@
/**
* Displays a text input keyboard.
*
* <img src="../images/text_input.png" width="200" alt="Sample screenshot of the view" />
*
* ```js
* let eventLoop = require("event_loop");
* let gui = require("gui");
* let textInputView = require("gui/text_input");
* ```
*
* This module depends on the `gui` module, which in turn depends on the
* `event_loop` module, so they _must_ be imported in this order. It is also
* recommended to conceptualize these modules first before using this one.
*
* # Example
* For an example refer to the `gui.js` example script.
*
* # View props
* - `header`: Text displayed at the top of the screen
* - `minLength`: Minimum allowed text length
* - `maxLength`: Maximum allowed text length
* - `defaultText`: Text to show by default
* - `defaultTextClear`: Whether to clear the default text on next character typed
*
* @version Added in JS SDK 0.1
* @module
*/
import type { View, ViewFactory } from ".";
import type { Contract } from "../event_loop";
type Props = {
header: string,
minLength: number,
maxLength: number,
defaultText: string,
defaultTextClear: boolean,
}
declare class TextInput extends View<Props> {
input: Contract<string>;
}
declare class TextInputFactory extends ViewFactory<Props, TextInput> { }
declare const factory: TextInputFactory;
export = factory;

View File

@@ -0,0 +1,60 @@
/**
* Math operations
* @version Added in JS SDK 0.1
* @module
*/
/** @version Added in JS SDK 0.1 */
export function isEqual(a: number, b: number, tolerance: number): boolean;
/** @version Added in JS SDK 0.1 */
export function abs(n: number): number;
/** @version Added in JS SDK 0.1 */
export function acos(n: number): number;
/** @version Added in JS SDK 0.1 */
export function acosh(n: number): number;
/** @version Added in JS SDK 0.1 */
export function asin(n: number): number;
/** @version Added in JS SDK 0.1 */
export function asinh(n: number): number;
/** @version Added in JS SDK 0.1 */
export function atan(n: number): number;
/** @version Added in JS SDK 0.1 */
export function atan2(a: number, b: number): number;
/** @version Added in JS SDK 0.1 */
export function atanh(n: number): number;
/** @version Added in JS SDK 0.1 */
export function cbrt(n: number): number;
/** @version Added in JS SDK 0.1 */
export function ceil(n: number): number;
/** @version Added in JS SDK 0.1 */
export function clz32(n: number): number;
/** @version Added in JS SDK 0.1 */
export function cos(n: number): number;
/** @version Added in JS SDK 0.1 */
export function exp(n: number): number;
/** @version Added in JS SDK 0.1 */
export function floor(n: number): number;
/** @version Added in JS SDK 0.1 */
export function log(n: number): number;
/** @version Added in JS SDK 0.1 */
export function max(n: number, m: number): number;
/** @version Added in JS SDK 0.1 */
export function min(n: number, m: number): number;
/** @version Added in JS SDK 0.1 */
export function pow(n: number, m: number): number;
/** @version Added in JS SDK 0.1 */
export function random(): number;
/** @version Added in JS SDK 0.1 */
export function sign(n: number): number;
/** @version Added in JS SDK 0.1 */
export function sin(n: number): number;
/** @version Added in JS SDK 0.1 */
export function sqrt(n: number): number;
/** @version Added in JS SDK 0.1 */
export function trunc(n: number): number;
/** @version Added in JS SDK 0.1 */
declare const PI: number;
/** @version Added in JS SDK 0.1 */
declare const E: number;
/** @version Added in JS SDK 0.1 */
declare const EPSILON: number;

View File

@@ -1,20 +1,32 @@
/**
* Module for using the color LED and vibration motor
* @version Added in JS SDK 0.1
* @module
*/
/** /**
* @brief Signals success to the user via the color LED, speaker and vibration * @brief Signals success to the user via the color LED, speaker and vibration
* motor * motor
* @version Added in JS SDK 0.1
*/ */
export declare function success(): void; export declare function success(): void;
/** /**
* @brief Signals failure to the user via the color LED, speaker and vibration * @brief Signals failure to the user via the color LED, speaker and vibration
* motor * motor
* @version Added in JS SDK 0.1
*/ */
export declare function error(): void; export declare function error(): void;
/**
* @version Added in JS SDK 0.1
*/
export type Color = "red" | "green" | "blue" | "yellow" | "cyan" | "magenta"; export type Color = "red" | "green" | "blue" | "yellow" | "cyan" | "magenta";
/** /**
* @brief Displays a basic color on the color LED * @brief Displays a basic color on the color LED
* @param color The color to display, see `Color` * @param color The color to display, see `Color`
* @param duration The duration, either `"short"` (10ms) or `"long"` (100ms) * @param duration The duration, either `"short"` (10ms) or `"long"` (100ms)
* @version Added in JS SDK 0.1
*/ */
export declare function blink(color: Color, duration: "short" | "long"): void; export declare function blink(color: Color, duration: "short" | "long"): void;

View File

@@ -0,0 +1,27 @@
{
"name": "@flipperdevices/fz-sdk",
"version": "0.1.1",
"description": "Type declarations and documentation for native JS modules available on Flipper Zero",
"keywords": [
"flipper",
"flipper zero",
"framework"
],
"author": "Flipper Devices",
"license": "GPL-3.0-only",
"repository": {
"type": "git",
"url": "git+https://github.com/flipperdevices/flipperzero-firmware.git",
"directory": "applications/system/js_app/packages/fz-sdk"
},
"type": "module",
"dependencies": {
"esbuild": "^0.24.0",
"esbuild-plugin-tsc": "^0.4.0",
"json5": "^2.2.3",
"typedoc": "^0.26.10",
"typedoc-material-theme": "^1.1.0",
"prompts": "^2.4.2",
"serialport": "^12.0.0"
}
}

View File

@@ -0,0 +1,896 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
prompts:
specifier: ^2.4.2
version: 2.4.2
serialport:
specifier: ^12.0.0
version: 12.0.0
devDependencies:
esbuild:
specifier: ^0.24.0
version: 0.24.0
esbuild-plugin-tsc:
specifier: ^0.4.0
version: 0.4.0(typescript@5.6.3)
json5:
specifier: ^2.2.3
version: 2.2.3
typedoc:
specifier: ^0.26.10
version: 0.26.10(typescript@5.6.3)
typedoc-material-theme:
specifier: ^1.1.0
version: 1.1.0(typedoc@0.26.10(typescript@5.6.3))
packages:
'@esbuild/aix-ppc64@0.24.0':
resolution: {integrity: sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.24.0':
resolution: {integrity: sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.24.0':
resolution: {integrity: sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.24.0':
resolution: {integrity: sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.24.0':
resolution: {integrity: sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.24.0':
resolution: {integrity: sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.24.0':
resolution: {integrity: sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.24.0':
resolution: {integrity: sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.24.0':
resolution: {integrity: sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.24.0':
resolution: {integrity: sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.24.0':
resolution: {integrity: sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.24.0':
resolution: {integrity: sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.24.0':
resolution: {integrity: sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.24.0':
resolution: {integrity: sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.24.0':
resolution: {integrity: sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.24.0':
resolution: {integrity: sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.24.0':
resolution: {integrity: sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-x64@0.24.0':
resolution: {integrity: sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.24.0':
resolution: {integrity: sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.24.0':
resolution: {integrity: sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/sunos-x64@0.24.0':
resolution: {integrity: sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.24.0':
resolution: {integrity: sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.24.0':
resolution: {integrity: sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.24.0':
resolution: {integrity: sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@material/material-color-utilities@0.2.7':
resolution: {integrity: sha512-0FCeqG6WvK4/Cc06F/xXMd/pv4FeisI0c1tUpBbfhA2n9Y8eZEv4Karjbmf2ZqQCPUWMrGp8A571tCjizxoTiQ==}
'@serialport/binding-mock@10.2.2':
resolution: {integrity: sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==}
engines: {node: '>=12.0.0'}
'@serialport/bindings-cpp@12.0.1':
resolution: {integrity: sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==}
engines: {node: '>=16.0.0'}
'@serialport/bindings-interface@1.2.2':
resolution: {integrity: sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==}
engines: {node: ^12.22 || ^14.13 || >=16}
'@serialport/parser-byte-length@12.0.0':
resolution: {integrity: sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==}
engines: {node: '>=12.0.0'}
'@serialport/parser-cctalk@12.0.0':
resolution: {integrity: sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==}
engines: {node: '>=12.0.0'}
'@serialport/parser-delimiter@11.0.0':
resolution: {integrity: sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==}
engines: {node: '>=12.0.0'}
'@serialport/parser-delimiter@12.0.0':
resolution: {integrity: sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==}
engines: {node: '>=12.0.0'}
'@serialport/parser-inter-byte-timeout@12.0.0':
resolution: {integrity: sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==}
engines: {node: '>=12.0.0'}
'@serialport/parser-packet-length@12.0.0':
resolution: {integrity: sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==}
engines: {node: '>=8.6.0'}
'@serialport/parser-readline@11.0.0':
resolution: {integrity: sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==}
engines: {node: '>=12.0.0'}
'@serialport/parser-readline@12.0.0':
resolution: {integrity: sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==}
engines: {node: '>=12.0.0'}
'@serialport/parser-ready@12.0.0':
resolution: {integrity: sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==}
engines: {node: '>=12.0.0'}
'@serialport/parser-regex@12.0.0':
resolution: {integrity: sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==}
engines: {node: '>=12.0.0'}
'@serialport/parser-slip-encoder@12.0.0':
resolution: {integrity: sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==}
engines: {node: '>=12.0.0'}
'@serialport/parser-spacepacket@12.0.0':
resolution: {integrity: sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==}
engines: {node: '>=12.0.0'}
'@serialport/stream@12.0.0':
resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==}
engines: {node: '>=12.0.0'}
'@shikijs/core@1.22.0':
resolution: {integrity: sha512-S8sMe4q71TJAW+qG93s5VaiihujRK6rqDFqBnxqvga/3LvqHEnxqBIOPkt//IdXVtHkQWKu4nOQNk0uBGicU7Q==}
'@shikijs/engine-javascript@1.22.0':
resolution: {integrity: sha512-AeEtF4Gcck2dwBqCFUKYfsCq0s+eEbCEbkUuFou53NZ0sTGnJnJ/05KHQFZxpii5HMXbocV9URYVowOP2wH5kw==}
'@shikijs/engine-oniguruma@1.22.0':
resolution: {integrity: sha512-5iBVjhu/DYs1HB0BKsRRFipRrD7rqjxlWTj4F2Pf+nQSPqc3kcyqFFeZXnBMzDf0HdqaFVvhDRAGiYNvyLP+Mw==}
'@shikijs/types@1.22.0':
resolution: {integrity: sha512-Fw/Nr7FGFhlQqHfxzZY8Cwtwk5E9nKDUgeLjZgt3UuhcM3yJR9xj3ZGNravZZok8XmEZMiYkSMTPlPkULB8nww==}
'@shikijs/vscode-textmate@9.3.0':
resolution: {integrity: sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==}
'@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
'@types/mdast@4.0.4':
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
'@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
'@ungap/structured-clone@1.2.0':
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
brace-expansion@2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
character-entities-html4@2.1.0:
resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
character-entities-legacy@3.0.0:
resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
esbuild-plugin-tsc@0.4.0:
resolution: {integrity: sha512-q9gWIovt1nkwchMLc2zhyksaiHOv3kDK4b0AUol8lkMCRhJ1zavgfb2fad6BKp7FT9rh/OHmEBXVjczLoi/0yw==}
peerDependencies:
typescript: ^4.0.0 || ^5.0.0
esbuild@0.24.0:
resolution: {integrity: sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==}
engines: {node: '>=18'}
hasBin: true
hast-util-to-html@9.0.3:
resolution: {integrity: sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==}
hast-util-whitespace@3.0.0:
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
json5@2.2.3:
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
engines: {node: '>=6'}
hasBin: true
kleur@3.0.3:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
lunr@2.3.9:
resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==}
markdown-it@14.1.0:
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
hasBin: true
mdast-util-to-hast@13.2.0:
resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==}
mdurl@2.0.0:
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
micromark-util-character@2.1.0:
resolution: {integrity: sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==}
micromark-util-encode@2.0.0:
resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==}
micromark-util-sanitize-uri@2.0.0:
resolution: {integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==}
micromark-util-symbol@2.0.0:
resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==}
micromark-util-types@2.0.0:
resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==}
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
node-addon-api@7.0.0:
resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==}
node-gyp-build@4.6.0:
resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==}
hasBin: true
oniguruma-to-js@0.4.3:
resolution: {integrity: sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==}
prompts@2.4.2:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
property-information@6.5.0:
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
punycode.js@2.3.1:
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
engines: {node: '>=6'}
regex@4.3.3:
resolution: {integrity: sha512-r/AadFO7owAq1QJVeZ/nq9jNS1vyZt+6t1p/E59B56Rn2GCya+gr1KSyOzNL/er+r+B7phv5jG2xU2Nz1YkmJg==}
serialport@12.0.0:
resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==}
engines: {node: '>=16.0.0'}
shiki@1.22.0:
resolution: {integrity: sha512-/t5LlhNs+UOKQCYBtl5ZsH/Vclz73GIqT2yQsCBygr8L/ppTdmpL4w3kPLoZJbMKVWtoG77Ue1feOjZfDxvMkw==}
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
space-separated-tokens@2.0.2:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
stringify-entities@4.0.4:
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
strip-comments@2.0.1:
resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==}
engines: {node: '>=10'}
trim-lines@3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
typedoc-material-theme@1.1.0:
resolution: {integrity: sha512-LLWGVb8w+i+QGnsu/a0JKjcuzndFQt/UeGVOQz0HFFGGocROEHv5QYudIACrj+phL2LDwH05tJx0Ob3pYYH2UA==}
engines: {node: '>=18.0.0', npm: '>=8.6.0'}
peerDependencies:
typedoc: ^0.25.13 || ^0.26.3
typedoc@0.26.10:
resolution: {integrity: sha512-xLmVKJ8S21t+JeuQLNueebEuTVphx6IrP06CdV7+0WVflUSW3SPmR+h1fnWVdAR/FQePEgsSWCUHXqKKjzuUAw==}
engines: {node: '>= 18'}
hasBin: true
peerDependencies:
typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x
typescript@5.6.3:
resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
engines: {node: '>=14.17'}
hasBin: true
uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
unist-util-is@6.0.0:
resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==}
unist-util-position@5.0.0:
resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
unist-util-stringify-position@4.0.0:
resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
unist-util-visit-parents@6.0.1:
resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==}
unist-util-visit@5.0.0:
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
vfile-message@4.0.2:
resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==}
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
yaml@2.6.0:
resolution: {integrity: sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==}
engines: {node: '>= 14'}
hasBin: true
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
snapshots:
'@esbuild/aix-ppc64@0.24.0':
optional: true
'@esbuild/android-arm64@0.24.0':
optional: true
'@esbuild/android-arm@0.24.0':
optional: true
'@esbuild/android-x64@0.24.0':
optional: true
'@esbuild/darwin-arm64@0.24.0':
optional: true
'@esbuild/darwin-x64@0.24.0':
optional: true
'@esbuild/freebsd-arm64@0.24.0':
optional: true
'@esbuild/freebsd-x64@0.24.0':
optional: true
'@esbuild/linux-arm64@0.24.0':
optional: true
'@esbuild/linux-arm@0.24.0':
optional: true
'@esbuild/linux-ia32@0.24.0':
optional: true
'@esbuild/linux-loong64@0.24.0':
optional: true
'@esbuild/linux-mips64el@0.24.0':
optional: true
'@esbuild/linux-ppc64@0.24.0':
optional: true
'@esbuild/linux-riscv64@0.24.0':
optional: true
'@esbuild/linux-s390x@0.24.0':
optional: true
'@esbuild/linux-x64@0.24.0':
optional: true
'@esbuild/netbsd-x64@0.24.0':
optional: true
'@esbuild/openbsd-arm64@0.24.0':
optional: true
'@esbuild/openbsd-x64@0.24.0':
optional: true
'@esbuild/sunos-x64@0.24.0':
optional: true
'@esbuild/win32-arm64@0.24.0':
optional: true
'@esbuild/win32-ia32@0.24.0':
optional: true
'@esbuild/win32-x64@0.24.0':
optional: true
'@material/material-color-utilities@0.2.7': {}
'@serialport/binding-mock@10.2.2':
dependencies:
'@serialport/bindings-interface': 1.2.2
debug: 4.3.4
transitivePeerDependencies:
- supports-color
'@serialport/bindings-cpp@12.0.1':
dependencies:
'@serialport/bindings-interface': 1.2.2
'@serialport/parser-readline': 11.0.0
debug: 4.3.4
node-addon-api: 7.0.0
node-gyp-build: 4.6.0
transitivePeerDependencies:
- supports-color
'@serialport/bindings-interface@1.2.2': {}
'@serialport/parser-byte-length@12.0.0': {}
'@serialport/parser-cctalk@12.0.0': {}
'@serialport/parser-delimiter@11.0.0': {}
'@serialport/parser-delimiter@12.0.0': {}
'@serialport/parser-inter-byte-timeout@12.0.0': {}
'@serialport/parser-packet-length@12.0.0': {}
'@serialport/parser-readline@11.0.0':
dependencies:
'@serialport/parser-delimiter': 11.0.0
'@serialport/parser-readline@12.0.0':
dependencies:
'@serialport/parser-delimiter': 12.0.0
'@serialport/parser-ready@12.0.0': {}
'@serialport/parser-regex@12.0.0': {}
'@serialport/parser-slip-encoder@12.0.0': {}
'@serialport/parser-spacepacket@12.0.0': {}
'@serialport/stream@12.0.0':
dependencies:
'@serialport/bindings-interface': 1.2.2
debug: 4.3.4
transitivePeerDependencies:
- supports-color
'@shikijs/core@1.22.0':
dependencies:
'@shikijs/engine-javascript': 1.22.0
'@shikijs/engine-oniguruma': 1.22.0
'@shikijs/types': 1.22.0
'@shikijs/vscode-textmate': 9.3.0
'@types/hast': 3.0.4
hast-util-to-html: 9.0.3
'@shikijs/engine-javascript@1.22.0':
dependencies:
'@shikijs/types': 1.22.0
'@shikijs/vscode-textmate': 9.3.0
oniguruma-to-js: 0.4.3
'@shikijs/engine-oniguruma@1.22.0':
dependencies:
'@shikijs/types': 1.22.0
'@shikijs/vscode-textmate': 9.3.0
'@shikijs/types@1.22.0':
dependencies:
'@shikijs/vscode-textmate': 9.3.0
'@types/hast': 3.0.4
'@shikijs/vscode-textmate@9.3.0': {}
'@types/hast@3.0.4':
dependencies:
'@types/unist': 3.0.3
'@types/mdast@4.0.4':
dependencies:
'@types/unist': 3.0.3
'@types/unist@3.0.3': {}
'@ungap/structured-clone@1.2.0': {}
argparse@2.0.1: {}
balanced-match@1.0.2: {}
brace-expansion@2.0.1:
dependencies:
balanced-match: 1.0.2
ccount@2.0.1: {}
character-entities-html4@2.1.0: {}
character-entities-legacy@3.0.0: {}
comma-separated-tokens@2.0.3: {}
debug@4.3.4:
dependencies:
ms: 2.1.2
dequal@2.0.3: {}
devlop@1.1.0:
dependencies:
dequal: 2.0.3
entities@4.5.0: {}
esbuild-plugin-tsc@0.4.0(typescript@5.6.3):
dependencies:
strip-comments: 2.0.1
typescript: 5.6.3
esbuild@0.24.0:
optionalDependencies:
'@esbuild/aix-ppc64': 0.24.0
'@esbuild/android-arm': 0.24.0
'@esbuild/android-arm64': 0.24.0
'@esbuild/android-x64': 0.24.0
'@esbuild/darwin-arm64': 0.24.0
'@esbuild/darwin-x64': 0.24.0
'@esbuild/freebsd-arm64': 0.24.0
'@esbuild/freebsd-x64': 0.24.0
'@esbuild/linux-arm': 0.24.0
'@esbuild/linux-arm64': 0.24.0
'@esbuild/linux-ia32': 0.24.0
'@esbuild/linux-loong64': 0.24.0
'@esbuild/linux-mips64el': 0.24.0
'@esbuild/linux-ppc64': 0.24.0
'@esbuild/linux-riscv64': 0.24.0
'@esbuild/linux-s390x': 0.24.0
'@esbuild/linux-x64': 0.24.0
'@esbuild/netbsd-x64': 0.24.0
'@esbuild/openbsd-arm64': 0.24.0
'@esbuild/openbsd-x64': 0.24.0
'@esbuild/sunos-x64': 0.24.0
'@esbuild/win32-arm64': 0.24.0
'@esbuild/win32-ia32': 0.24.0
'@esbuild/win32-x64': 0.24.0
hast-util-to-html@9.0.3:
dependencies:
'@types/hast': 3.0.4
'@types/unist': 3.0.3
ccount: 2.0.1
comma-separated-tokens: 2.0.3
hast-util-whitespace: 3.0.0
html-void-elements: 3.0.0
mdast-util-to-hast: 13.2.0
property-information: 6.5.0
space-separated-tokens: 2.0.2
stringify-entities: 4.0.4
zwitch: 2.0.4
hast-util-whitespace@3.0.0:
dependencies:
'@types/hast': 3.0.4
html-void-elements@3.0.0: {}
json5@2.2.3: {}
kleur@3.0.3: {}
linkify-it@5.0.0:
dependencies:
uc.micro: 2.1.0
lunr@2.3.9: {}
markdown-it@14.1.0:
dependencies:
argparse: 2.0.1
entities: 4.5.0
linkify-it: 5.0.0
mdurl: 2.0.0
punycode.js: 2.3.1
uc.micro: 2.1.0
mdast-util-to-hast@13.2.0:
dependencies:
'@types/hast': 3.0.4
'@types/mdast': 4.0.4
'@ungap/structured-clone': 1.2.0
devlop: 1.1.0
micromark-util-sanitize-uri: 2.0.0
trim-lines: 3.0.1
unist-util-position: 5.0.0
unist-util-visit: 5.0.0
vfile: 6.0.3
mdurl@2.0.0: {}
micromark-util-character@2.1.0:
dependencies:
micromark-util-symbol: 2.0.0
micromark-util-types: 2.0.0
micromark-util-encode@2.0.0: {}
micromark-util-sanitize-uri@2.0.0:
dependencies:
micromark-util-character: 2.1.0
micromark-util-encode: 2.0.0
micromark-util-symbol: 2.0.0
micromark-util-symbol@2.0.0: {}
micromark-util-types@2.0.0: {}
minimatch@9.0.5:
dependencies:
brace-expansion: 2.0.1
ms@2.1.2: {}
node-addon-api@7.0.0: {}
node-gyp-build@4.6.0: {}
oniguruma-to-js@0.4.3:
dependencies:
regex: 4.3.3
prompts@2.4.2:
dependencies:
kleur: 3.0.3
sisteransi: 1.0.5
property-information@6.5.0: {}
punycode.js@2.3.1: {}
regex@4.3.3: {}
serialport@12.0.0:
dependencies:
'@serialport/binding-mock': 10.2.2
'@serialport/bindings-cpp': 12.0.1
'@serialport/parser-byte-length': 12.0.0
'@serialport/parser-cctalk': 12.0.0
'@serialport/parser-delimiter': 12.0.0
'@serialport/parser-inter-byte-timeout': 12.0.0
'@serialport/parser-packet-length': 12.0.0
'@serialport/parser-readline': 12.0.0
'@serialport/parser-ready': 12.0.0
'@serialport/parser-regex': 12.0.0
'@serialport/parser-slip-encoder': 12.0.0
'@serialport/parser-spacepacket': 12.0.0
'@serialport/stream': 12.0.0
debug: 4.3.4
transitivePeerDependencies:
- supports-color
shiki@1.22.0:
dependencies:
'@shikijs/core': 1.22.0
'@shikijs/engine-javascript': 1.22.0
'@shikijs/engine-oniguruma': 1.22.0
'@shikijs/types': 1.22.0
'@shikijs/vscode-textmate': 9.3.0
'@types/hast': 3.0.4
sisteransi@1.0.5: {}
space-separated-tokens@2.0.2: {}
stringify-entities@4.0.4:
dependencies:
character-entities-html4: 2.1.0
character-entities-legacy: 3.0.0
strip-comments@2.0.1: {}
trim-lines@3.0.1: {}
typedoc-material-theme@1.1.0(typedoc@0.26.10(typescript@5.6.3)):
dependencies:
'@material/material-color-utilities': 0.2.7
typedoc: 0.26.10(typescript@5.6.3)
typedoc@0.26.10(typescript@5.6.3):
dependencies:
lunr: 2.3.9
markdown-it: 14.1.0
minimatch: 9.0.5
shiki: 1.22.0
typescript: 5.6.3
yaml: 2.6.0
typescript@5.6.3: {}
uc.micro@2.1.0: {}
unist-util-is@6.0.0:
dependencies:
'@types/unist': 3.0.3
unist-util-position@5.0.0:
dependencies:
'@types/unist': 3.0.3
unist-util-stringify-position@4.0.0:
dependencies:
'@types/unist': 3.0.3
unist-util-visit-parents@6.0.1:
dependencies:
'@types/unist': 3.0.3
unist-util-is: 6.0.0
unist-util-visit@5.0.0:
dependencies:
'@types/unist': 3.0.3
unist-util-is: 6.0.0
unist-util-visit-parents: 6.0.1
vfile-message@4.0.2:
dependencies:
'@types/unist': 3.0.3
unist-util-stringify-position: 4.0.0
vfile@6.0.3:
dependencies:
'@types/unist': 3.0.3
vfile-message: 4.0.2
yaml@2.6.0: {}
zwitch@2.0.4: {}

View File

@@ -0,0 +1,176 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { SerialPort } from "serialport";
import prompts from "prompts";
import esbuild from "esbuild";
import json5 from "json5";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
async function build(config) {
await esbuild.build({
entryPoints: ["./dist/index.js"],
outfile: config.output,
tsconfig: "./tsconfig.json",
format: "cjs",
bundle: true,
minify: config.minify,
external: [
"@flipperdevices/fz-sdk/*"
],
supported: {
"array-spread": false,
"arrow": false,
"async-await": false,
"async-generator": false,
"bigint": false,
"class": false,
"const-and-let": true,
"decorators": false,
"default-argument": false,
"destructuring": false,
"dynamic-import": false,
"exponent-operator": false,
"export-star-as": false,
"for-await": false,
"for-of": false,
"function-name-configurable": false,
"function-or-class-property-access": false,
"generator": false,
"hashbang": false,
"import-assertions": false,
"import-meta": false,
"inline-script": false,
"logical-assignment": false,
"nested-rest-binding": false,
"new-target": false,
"node-colon-prefix-import": false,
"node-colon-prefix-require": false,
"nullish-coalescing": false,
"object-accessors": false,
"object-extensions": false,
"object-rest-spread": false,
"optional-catch-binding": false,
"optional-chain": false,
"regexp-dot-all-flag": false,
"regexp-lookbehind-assertions": false,
"regexp-match-indices": false,
"regexp-named-capture-groups": false,
"regexp-set-notation": false,
"regexp-sticky-and-unicode-flags": false,
"regexp-unicode-property-escapes": false,
"rest-argument": false,
"template-literal": false,
"top-level-await": false,
"typeof-exotic-object-is-object": false,
"unicode-escapes": false,
"using": false,
},
});
let outContents = fs.readFileSync(config.output, "utf8");
outContents = "let exports = {};\n" + outContents;
if (config.enforceSdkVersion) {
const version = json5.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8")).version;
let [major, minor, _] = version.split(".");
outContents = `checkSdkCompatibility(${major}, ${minor});\n${outContents}`;
}
fs.writeFileSync(config.output, outContents);
}
async function upload(config) {
const appFile = fs.readFileSync(config.input, "utf8");
const flippers = (await SerialPort.list()).filter(x => x.serialNumber?.startsWith("flip_"));
if (!flippers) {
console.error("No Flippers found");
process.exit(1);
}
let portPath = flippers[0].path;
if (flippers.length > 1) {
port = (await prompts([{
type: "select",
name: "port",
message: "Select Flipper to run the app on",
choices: flippers.map(x => ({ title: x.serialNumber.replace("flip_", ""), value: x.path })),
}])).port;
}
console.log(`Connecting to Flipper at ${portPath}`);
let port = new SerialPort({ path: portPath, baudRate: 230400 });
let received = "";
let lastMatch = 0;
async function waitFor(string, timeoutMs) {
return new Promise((resolve, _reject) => {
let timeout = undefined;
if (timeoutMs) {
timeout = setTimeout(() => {
console.error("Error: timeout");
process.exit(1);
}, timeoutMs);
}
setInterval(() => {
let idx = received.indexOf(string, lastMatch);
if (idx !== -1) {
lastMatch = idx;
if (timeoutMs)
clearTimeout(timeout);
resolve();
}
}, 50);
});
}
port.on("data", (data) => {
received += data.toString();
});
await waitFor(">: ", 1000);
console.log("Uploading application file");
port.write(`storage remove ${config.output}\x0d`);
port.drain();
await waitFor(">: ", 1000);
port.write(`storage write_chunk ${config.output} ${appFile.length}\x0d`);
await waitFor("Ready", 1000);
port.write(appFile);
port.drain();
await waitFor(">: ", 1000);
console.log("Launching application");
port.write(`js ${config.output}\x0d`);
port.drain();
await waitFor("Running", 1000);
process.stdout.write(received.slice(lastMatch));
port.on("data", (data) => {
process.stdout.write(data.toString());
});
process.on("exit", () => {
port.write("\x03");
});
await waitFor("Script done!", 0);
process.exit(0);
}
(async () => {
const commands = {
"build": build,
"upload": upload,
};
const config = json5.parse(fs.readFileSync("./fz-sdk.config.json5", "utf8"));
const command = process.argv[2];
if (!Object.keys(commands).includes(command)) {
console.error(`Unknown command ${command}. Supported: ${Object.keys(commands).join(", ")}`);
process.exit(1);
}
await commands[command](config[command]);
})();

View File

@@ -1,7 +1,17 @@
/**
* Module for accessing the serial port
* @version Added in JS SDK 0.1
* @module
*/
/** /**
* @brief Initializes the serial port * @brief Initializes the serial port
*
* Automatically disables Expansion module service to prevent interference.
*
* @param port The port to initialize (`"lpuart"` or `"start"`) * @param port The port to initialize (`"lpuart"` or `"start"`)
* @param baudRate * @param baudRate
* @version Added in JS SDK 0.1
*/ */
export declare function setup(port: "lpuart" | "usart", baudRate: number): void; export declare function setup(port: "lpuart" | "usart", baudRate: number): void;
@@ -13,6 +23,7 @@ export declare function setup(port: "lpuart" | "usart", baudRate: number): void;
* - Arrays of numbers will get sent as a sequence of bytes. * - Arrays of numbers will get sent as a sequence of bytes.
* - `ArrayBuffer`s and `TypedArray`s will be sent as a sequence * - `ArrayBuffer`s and `TypedArray`s will be sent as a sequence
* of bytes. * of bytes.
* @version Added in JS SDK 0.1
*/ */
export declare function write<E extends ElementType>(value: string | number | number[] | ArrayBuffer | TypedArray<E>): void; export declare function write<E extends ElementType>(value: string | number | number[] | ArrayBuffer | TypedArray<E>): void;
@@ -24,6 +35,7 @@ export declare function write<E extends ElementType>(value: string | number | nu
* unset, the function will wait forever. * unset, the function will wait forever.
* @returns The received data interpreted as ASCII, or `undefined` if 0 bytes * @returns The received data interpreted as ASCII, or `undefined` if 0 bytes
* were read. * were read.
* @version Added in JS SDK 0.1
*/ */
export declare function read(length: number, timeout?: number): string | undefined; export declare function read(length: number, timeout?: number): string | undefined;
@@ -39,9 +51,24 @@ export declare function read(length: number, timeout?: number): string | undefin
* applies to characters, not entire strings. * applies to characters, not entire strings.
* @returns The received data interpreted as ASCII, or `undefined` if 0 bytes * @returns The received data interpreted as ASCII, or `undefined` if 0 bytes
* were read. * were read.
* @version Added in JS SDK 0.1
*/ */
export declare function readln(timeout?: number): string; export declare function readln(timeout?: number): string;
/**
* @brief Read any available amount of data from the serial port
*
* Can be useful to avoid starving your loop with small reads.
*
* @param timeout The number of time, in milliseconds, after which this function
* will give up and return nothing. If unset, the function will
* wait forever.
* @returns The received data interpreted as ASCII, or `undefined` if 0 bytes
* were read.
* @version Added in JS SDK 0.1
*/
export declare function readAny(timeout?: number): string | undefined;
/** /**
* @brief Reads data from the serial port * @brief Reads data from the serial port
* @param length The number of bytes to read * @param length The number of bytes to read
@@ -50,6 +77,7 @@ export declare function readln(timeout?: number): string;
* unset, the function will wait forever. * unset, the function will wait forever.
* @returns The received data as an ArrayBuffer, or `undefined` if 0 bytes were * @returns The received data as an ArrayBuffer, or `undefined` if 0 bytes were
* read. * read.
* @version Added in JS SDK 0.1
*/ */
export declare function readBytes(length: number, timeout?: number): ArrayBuffer; export declare function readBytes(length: number, timeout?: number): ArrayBuffer;
@@ -73,5 +101,12 @@ export declare function readBytes(length: number, timeout?: number): ArrayBuffer
* @returns The index of the matched pattern if multiple were provided, or 0 if * @returns The index of the matched pattern if multiple were provided, or 0 if
* only one was provided and it matched, or `undefined` if none of the * only one was provided and it matched, or `undefined` if none of the
* patterns matched. * patterns matched.
* @version Added in JS SDK 0.1
*/ */
export declare function expect(patterns: string | number[] | string[] | number[][], timeout?: number): number | undefined; export declare function expect(patterns: string | number[] | string[] | number[][], timeout?: number): number | undefined;
/**
* @brief Deinitializes the serial port, allowing multiple initializations per script run
* @version Added in JS SDK 0.1
*/
export declare function end(): void;

View File

@@ -1,8 +1,15 @@
/**
* Module for accessing the filesystem
* @version Added in JS SDK 0.1
* @module
*/
/** /**
* File readability mode: * File readability mode:
* - `"r"`: read-only * - `"r"`: read-only
* - `"w"`: write-only * - `"w"`: write-only
* - `"rw"`: read-write * - `"rw"`: read-write
* @version Added in JS SDK 0.1
*/ */
export type AccessMode = "r" | "w" | "rw"; export type AccessMode = "r" | "w" | "rw";
@@ -13,53 +20,78 @@ export type AccessMode = "r" | "w" | "rw";
* - `"open_append"`: open file and set r/w pointer to EOF, or create a new one if it doesn't exist * - `"open_append"`: open file and set r/w pointer to EOF, or create a new one if it doesn't exist
* - `"create_new"`: create new file or fail if it exists * - `"create_new"`: create new file or fail if it exists
* - `"create_always"`: truncate and open file, or create a new empty one if it doesn't exist * - `"create_always"`: truncate and open file, or create a new empty one if it doesn't exist
* @version Added in JS SDK 0.1
*/ */
export type OpenMode = "open_existing" | "open_always" | "open_append" | "create_new" | "create_always"; export type OpenMode = "open_existing" | "open_always" | "open_append" | "create_new" | "create_always";
/** Standard UNIX timestamp */ /**
* Standard UNIX timestamp
* @version Added in JS SDK 0.1
*/
export type Timestamp = number; export type Timestamp = number;
/** File information structure */ /**
* File information structure
* @version Added in JS SDK 0.1
*/
export declare class FileInfo { export declare class FileInfo {
/** /**
* Full path (e.g. "/ext/test", returned by `stat`) or file name * Full path (e.g. "/ext/test", returned by `stat`) or file name
* (e.g. "test", returned by `readDirectory`) * (e.g. "test", returned by `readDirectory`)
* @version Added in JS SDK 0.1
*/ */
path: string; path: string;
/** /**
* Is the file a directory? * Is the file a directory?
* @version Added in JS SDK 0.1
*/ */
isDirectory: boolean; isDirectory: boolean;
/** /**
* File size in bytes, or 0 in the case of directories * File size in bytes, or 0 in the case of directories
* @version Added in JS SDK 0.1
*/ */
size: number; size: number;
/** /**
* Time of last access as a UNIX timestamp * Time of last access as a UNIX timestamp
* @version Added in JS SDK 0.1
*/ */
accessTime: Timestamp; accessTime: Timestamp;
} }
/** Filesystem information structure */ /**
* Filesystem information structure
* @version Added in JS SDK 0.1
*/
export declare class FsInfo { export declare class FsInfo {
/** Total size of the filesystem, in bytes */ /**
* Total size of the filesystem, in bytes
* @version Added in JS SDK 0.1
*/
totalSpace: number; totalSpace: number;
/** Free space in the filesystem, in bytes */ /**
* Free space in the filesystem, in bytes
* @version Added in JS SDK 0.1
*/
freeSpace: number; freeSpace: number;
} }
// file operations // file operations
/** File class */ /**
* File class
* @version Added in JS SDK 0.1
*/
export declare class File { export declare class File {
/** /**
* Closes the file. After this method is called, all other operations * Closes the file. After this method is called, all other operations
* related to this file become unavailable. * related to this file become unavailable.
* @returns `true` on success, `false` on failure * @returns `true` on success, `false` on failure
* @version Added in JS SDK 0.1
*/ */
close(): boolean; close(): boolean;
/** /**
* Is the file currently open? * Is the file currently open?
* @version Added in JS SDK 0.1
*/ */
isOpen(): boolean; isOpen(): boolean;
/** /**
@@ -70,6 +102,7 @@ export declare class File {
* @returns an `ArrayBuf` if the mode is `"binary"`, a `string` if the mode * @returns an `ArrayBuf` if the mode is `"binary"`, a `string` if the mode
* is `ascii`. The number of bytes that was actually read may be * is `ascii`. The number of bytes that was actually read may be
* fewer than requested. * fewer than requested.
* @version Added in JS SDK 0.1
*/ */
read<T extends ArrayBuffer | string>(mode: T extends ArrayBuffer ? "binary" : "ascii", bytes: number): T; read<T extends ArrayBuffer | string>(mode: T extends ArrayBuffer ? "binary" : "ascii", bytes: number): T;
/** /**
@@ -77,36 +110,43 @@ export declare class File {
* @param data The data to write: a string that will be ASCII-encoded, or an * @param data The data to write: a string that will be ASCII-encoded, or an
* ArrayBuf * ArrayBuf
* @returns the amount of bytes that was actually written * @returns the amount of bytes that was actually written
* @version Added in JS SDK 0.1
*/ */
write(data: ArrayBuffer | string): number; write(data: ArrayBuffer | string): number;
/** /**
* Moves the R/W pointer forward * Moves the R/W pointer forward
* @param bytes How many bytes to move the pointer forward by * @param bytes How many bytes to move the pointer forward by
* @returns `true` on success, `false` on failure * @returns `true` on success, `false` on failure
* @version Added in JS SDK 0.1
*/ */
seekRelative(bytes: number): boolean; seekRelative(bytes: number): boolean;
/** /**
* Moves the R/W pointer to an absolute position inside the file * Moves the R/W pointer to an absolute position inside the file
* @param bytes The position inside the file * @param bytes The position inside the file
* @returns `true` on success, `false` on failure * @returns `true` on success, `false` on failure
* @version Added in JS SDK 0.1
*/ */
seekAbsolute(bytes: number): boolean; seekAbsolute(bytes: number): boolean;
/** /**
* Gets the absolute position of the R/W pointer in bytes * Gets the absolute position of the R/W pointer in bytes
* @version Added in JS SDK 0.1
*/ */
tell(): number; tell(): number;
/** /**
* Discards the data after the current position of the R/W pointer in a file * Discards the data after the current position of the R/W pointer in a file
* opened in either write-only or read-write mode. * opened in either write-only or read-write mode.
* @returns `true` on success, `false` on failure * @returns `true` on success, `false` on failure
* @version Added in JS SDK 0.1
*/ */
truncate(): boolean; truncate(): boolean;
/** /**
* Reads the total size of the file in bytes * Reads the total size of the file in bytes
* @version Added in JS SDK 0.1
*/ */
size(): number; size(): number;
/** /**
* Detects whether the R/W pointer has reached the end of the file * Detects whether the R/W pointer has reached the end of the file
* @version Added in JS SDK 0.1
*/ */
eof(): boolean; eof(): boolean;
/** /**
@@ -115,6 +155,7 @@ export declare class File {
* @param dest The file to copy the bytes into * @param dest The file to copy the bytes into
* @param bytes The number of bytes to copy * @param bytes The number of bytes to copy
* @returns `true` on success, `false` on failure * @returns `true` on success, `false` on failure
* @version Added in JS SDK 0.1
*/ */
copyTo(dest: File, bytes: number): boolean; copyTo(dest: File, bytes: number): boolean;
} }
@@ -126,12 +167,14 @@ export declare class File {
* @param openMode `"open_existing"`, `"open_always"`, `"open_append"`, * @param openMode `"open_existing"`, `"open_always"`, `"open_append"`,
* `"create_new"` or `"create_always"`; see `OpenMode` * `"create_new"` or `"create_always"`; see `OpenMode`
* @returns a `File` on success, or `undefined` on failure * @returns a `File` on success, or `undefined` on failure
* @version Added in JS SDK 0.1
*/ */
export declare function openFile(path: string, accessMode: AccessMode, openMode: OpenMode): File | undefined; export declare function openFile(path: string, accessMode: AccessMode, openMode: OpenMode): File | undefined;
/** /**
* Detects whether a file exists * Detects whether a file exists
* @param path The path to the file * @param path The path to the file
* @returns `true` on success, `false` on failure * @returns `true` on success, `false` on failure
* @version Added in JS SDK 0.1
*/ */
export declare function fileExists(path: string): boolean; export declare function fileExists(path: string): boolean;
@@ -142,17 +185,20 @@ export declare function fileExists(path: string): boolean;
* @param path The path to the directory * @param path The path to the directory
* @returns Array of `FileInfo` structures with directory entries, * @returns Array of `FileInfo` structures with directory entries,
* or `undefined` on failure * or `undefined` on failure
* @version Added in JS SDK 0.1
*/ */
export declare function readDirectory(path: string): FileInfo[] | undefined; export declare function readDirectory(path: string): FileInfo[] | undefined;
/** /**
* Detects whether a directory exists * Detects whether a directory exists
* @param path The path to the directory * @param path The path to the directory
* @version Added in JS SDK 0.1
*/ */
export declare function directoryExists(path: string): boolean; export declare function directoryExists(path: string): boolean;
/** /**
* Creates an empty directory * Creates an empty directory
* @param path The path to the new directory * @param path The path to the new directory
* @returns `true` on success, `false` on failure * @returns `true` on success, `false` on failure
* @version Added in JS SDK 0.1
*/ */
export declare function makeDirectory(path: string): boolean; export declare function makeDirectory(path: string): boolean;
@@ -161,24 +207,28 @@ export declare function makeDirectory(path: string): boolean;
/** /**
* Detects whether a file or a directory exists * Detects whether a file or a directory exists
* @param path The path to the file or directory * @param path The path to the file or directory
* @version Added in JS SDK 0.1
*/ */
export declare function fileOrDirExists(path: string): boolean; export declare function fileOrDirExists(path: string): boolean;
/** /**
* Acquires metadata about a file or directory * Acquires metadata about a file or directory
* @param path The path to the file or directory * @param path The path to the file or directory
* @returns A `FileInfo` structure or `undefined` on failure * @returns A `FileInfo` structure or `undefined` on failure
* @version Added in JS SDK 0.1
*/ */
export declare function stat(path: string): FileInfo | undefined; export declare function stat(path: string): FileInfo | undefined;
/** /**
* Removes a file or an empty directory * Removes a file or an empty directory
* @param path The path to the file or directory * @param path The path to the file or directory
* @returns `true` on success, `false` on failure * @returns `true` on success, `false` on failure
* @version Added in JS SDK 0.1
*/ */
export declare function remove(path: string): boolean; export declare function remove(path: string): boolean;
/** /**
* Removes a file or recursively removes a possibly non-empty directory * Removes a file or recursively removes a possibly non-empty directory
* @param path The path to the file or directory * @param path The path to the file or directory
* @returns `true` on success, `false` on failure * @returns `true` on success, `false` on failure
* @version Added in JS SDK 0.1
*/ */
export declare function rmrf(path: string): boolean; export declare function rmrf(path: string): boolean;
/** /**
@@ -187,6 +237,7 @@ export declare function rmrf(path: string): boolean;
* @param newPath The new path that the file or directory will become accessible * @param newPath The new path that the file or directory will become accessible
* under * under
* @returns `true` on success, `false` on failure * @returns `true` on success, `false` on failure
* @version Added in JS SDK 0.1
*/ */
export declare function rename(oldPath: string, newPath: string): boolean; export declare function rename(oldPath: string, newPath: string): boolean;
/** /**
@@ -194,11 +245,13 @@ export declare function rename(oldPath: string, newPath: string): boolean;
* @param oldPath The original path to the file or directory * @param oldPath The original path to the file or directory
* @param newPath The new path that the copy of the file or directory will be * @param newPath The new path that the copy of the file or directory will be
* accessible under * accessible under
* @version Added in JS SDK 0.1
*/ */
export declare function copy(oldPath: string, newPath: string): boolean; export declare function copy(oldPath: string, newPath: string): boolean;
/** /**
* Fetches generic information about a filesystem * Fetches generic information about a filesystem
* @param filesystem The path to the filesystem (e.g. `"/ext"` or `"/int"`) * @param filesystem The path to the filesystem (e.g. `"/ext"` or `"/int"`)
* @version Added in JS SDK 0.1
*/ */
export declare function fsInfo(filesystem: string): FsInfo | undefined; export declare function fsInfo(filesystem: string): FsInfo | undefined;
/** /**
@@ -218,6 +271,7 @@ export declare function fsInfo(filesystem: string): FsInfo | undefined;
* @param maxLen The maximum length of the filename with the numeric suffix * @param maxLen The maximum length of the filename with the numeric suffix
* @returns The base of the filename with the next available numeric suffix, * @returns The base of the filename with the next available numeric suffix,
* without the extension or the base directory. * without the extension or the base directory.
* @version Added in JS SDK 0.1
*/ */
export declare function nextAvailableFilename(dirPath: string, fileName: string, fileExt: string, maxLen: number): string; export declare function nextAvailableFilename(dirPath: string, fileName: string, fileExt: string, maxLen: number): string;
@@ -226,6 +280,7 @@ export declare function nextAvailableFilename(dirPath: string, fileName: string,
/** /**
* Determines whether the two paths are equivalent. Respects filesystem-defined * Determines whether the two paths are equivalent. Respects filesystem-defined
* path equivalence rules. * path equivalence rules.
* @version Added in JS SDK 0.1
*/ */
export declare function arePathsEqual(path1: string, path2: string): boolean; export declare function arePathsEqual(path1: string, path2: string): boolean;
/** /**
@@ -233,5 +288,6 @@ export declare function arePathsEqual(path1: string, path2: string): boolean;
* filesystem-defined path equivalence rules. * filesystem-defined path equivalence rules.
* @param parentPath The parent path * @param parentPath The parent path
* @param childPath The child path * @param childPath The child path
* @version Added in JS SDK 0.1
*/ */
export declare function isSubpathOf(parentPath: string, childPath: string): boolean; export declare function isSubpathOf(parentPath: string, childPath: string): boolean;

View File

@@ -1,6 +1,8 @@
/** /**
* Unit test module. Only available if the firmware has been configured with * Unit test module. Only available if the firmware has been configured with
* `FIRMWARE_APP_SET=unit_tests`. * `FIRMWARE_APP_SET=unit_tests`.
* @version Added in JS SDK 0.1
* @module
*/ */
export function fail(message: string): never; export function fail(message: string): never;

View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"checkJs": true,
"module": "CommonJS",
"noLib": true,
},
"include": [
"./**/*.d.ts"
],
"exclude": [
"node_modules",
]
}

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