Merge remote-tracking branch 'OFW/dev' into dev

This commit is contained in:
MX 2024-10-31 21:17:01 +03:00
commit 04c8093672
No known key found for this signature in database
GPG Key ID: 7CCC66B7DBDD1C83
130 changed files with 5492 additions and 544 deletions

5
.gitignore vendored
View File

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

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

@ -140,6 +140,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.
// //
@ -150,6 +163,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);

View File

@ -582,7 +582,7 @@ bool ndef_parse_record(
NdefTnf tnf, NdefTnf tnf,
const char* type, const char* type,
uint8_t type_len) { uint8_t type_len) {
FURI_LOG_D(TAG, "payload type: %.*s len: %d", type_len, type, len); FURI_LOG_D(TAG, "payload type: %.*s len: %hu", type_len, type, len);
if(!len) { if(!len) {
furi_string_cat(ndef->output, "Empty\n"); furi_string_cat(ndef->output, "Empty\n");
return true; return true;
@ -702,9 +702,9 @@ bool ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num,
pos += id_len; pos += id_len;
if(smart_poster) { if(smart_poster) {
furi_string_cat_printf(ndef->output, "\e*> SP-R%d: ", record_num); furi_string_cat_printf(ndef->output, "\e*> SP-R%zu: ", record_num);
} else { } else {
furi_string_cat_printf(ndef->output, "\e*> M%d-R%d: ", message_num, record_num); furi_string_cat_printf(ndef->output, "\e*> M%zu-R%zu: ", message_num, record_num);
} }
if(!ndef_parse_record(ndef, pos, payload_len, flags_tnf.type_name_format, type, type_len)) { if(!ndef_parse_record(ndef, pos, payload_len, flags_tnf.type_name_format, type, type_len)) {
if(type_was_allocated) free(type); if(type_was_allocated) free(type);
@ -721,7 +721,7 @@ bool ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num,
if(smart_poster) { if(smart_poster) {
furi_string_cat(ndef->output, "\e*> SP: Empty\n\n"); furi_string_cat(ndef->output, "\e*> SP: Empty\n\n");
} else { } else {
furi_string_cat_printf(ndef->output, "\e*> M%d: Empty\n\n", message_num); furi_string_cat_printf(ndef->output, "\e*> M%zu: Empty\n\n", message_num);
} }
} }
@ -949,7 +949,7 @@ static bool ndef_mfc_parse(const NfcDevice* device, FuriString* parsed_data) {
} else { } else {
data_block = 93 + (sector - 32) * 15; data_block = 93 + (sector - 32) * 15;
} }
FURI_LOG_D(TAG, "data_block: %d", data_block); FURI_LOG_D(TAG, "data_block: %zu", data_block);
size_t data_start = data_block * MF_CLASSIC_BLOCK_SIZE; size_t data_start = data_block * MF_CLASSIC_BLOCK_SIZE;
size_t parsed = ndef_parse_tlv(&ndef, data_start, data_size - data_start, total_parsed); size_t parsed = ndef_parse_tlv(&ndef, data_start, data_size - data_start, total_parsed);
@ -982,7 +982,7 @@ static bool ndef_slix_parse(const NfcDevice* device, FuriString* parsed_data) {
const uint16_t block_count = iso15693_3_get_block_count(data); const uint16_t block_count = iso15693_3_get_block_count(data);
const uint8_t* blocks = simple_array_cget_data(data->block_data); const uint8_t* blocks = simple_array_cget_data(data->block_data);
// TODO: Find some way to check for other iso15693 NDEF cards and // TODO(-nofl): Find some way to check for other iso15693 NDEF cards and
// split this to also support non-slix iso15693 NDEF tags // split this to also support non-slix iso15693 NDEF tags
// Rest of the code works on iso15693 too, but uses SLIX layout assumptions // Rest of the code works on iso15693 too, but uses SLIX layout assumptions
if(block_size != SLIX_BLOCK_SIZE) { if(block_size != SLIX_BLOCK_SIZE) {

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

@ -380,7 +380,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;
@ -412,8 +412,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,7 +5,6 @@ App(
provides=[ provides=[
"updater_app", "updater_app",
"js_app", "js_app",
"js_app_start",
# "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(
@ -96,6 +107,7 @@ App(
entry_point="js_gui_file_picker_ep", entry_point="js_gui_file_picker_ep",
requires=["js_app"], requires=["js_app"],
sources=["modules/js_gui/file_picker.c"], sources=["modules/js_gui/file_picker.c"],
fap_libs=["assets"],
) )
App( App(

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

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

@ -0,0 +1,3 @@
let math = load(__dirname + "/load_api.js");
let result = math.add(5, 10);
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

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

@ -269,11 +269,48 @@ static int32_t js_thread(void* arg) {
mjs_set(mjs, global, "parseInt", ~0, MJS_MK_FN(js_parse_int)); mjs_set(mjs, global, "parseInt", ~0, MJS_MK_FN(js_parse_int));
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("toString", MJS_MK_FN(js_global_to_string));
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

@ -204,6 +204,22 @@ static void js_badusb_quit(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);

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) {
@ -273,7 +273,7 @@ static void js_gpio_get(struct mjs* mjs) {
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

@ -1,17 +1,6 @@
#include "../../js_modules.h" #include "../../js_modules.h"
#include <dialogs/dialogs.h> #include <dialogs/dialogs.h>
#include <assets_icons.h>
// File icon
#include <gui/icon_i.h>
static const uint8_t _I_file_10px_0[] = {
0x00, 0x7f, 0x00, 0xa1, 0x00, 0x2d, 0x01, 0xe1, 0x01, 0x0d, 0x01,
0x01, 0x01, 0x7d, 0x01, 0x01, 0x01, 0x01, 0x01, 0xff, 0x01,
};
static const uint8_t* const _I_file_10px[] = {_I_file_10px_0};
static const Icon I_file_10px =
{.width = 10, .height = 10, .frame_count = 1, .frame_rate = 0, .frames = _I_file_10px};
// File icon end
static void js_gui_file_picker_pick_file(struct mjs* mjs) { static void js_gui_file_picker_pick_file(struct mjs* mjs) {
const char *base_path, *extension; const char *base_path, *extension;

View File

@ -85,7 +85,7 @@ static bool default_text_assign(
context->buffer = realloc(context->buffer, context->buffer_size); //-V701 context->buffer = realloc(context->buffer, context->buffer_size); //-V701
} }
// Also trim excess previous data with strlcpy() // Also trim excess previous data with strlcpy()
strlcpy(context->buffer, value.string, context->buffer_size); strlcpy(context->buffer, value.string, context->buffer_size); //-V575
text_input_set_result_callback( text_input_set_result_callback(
input, input,
(TextInputCallback)input_callback, (TextInputCallback)input_callback,

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" |
@ -31,6 +33,7 @@ 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;
/** /**
@ -39,11 +42,13 @@ export type KeyCode = MainKey | ModifierKey | number;
* Automatically unlocks USB profile, so qFlipper connection will be interrupted. * Automatically unlocks USB profile, so qFlipper connection will be interrupted.
* *
* @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, layoutPath?: 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;
@ -52,6 +57,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;
@ -60,6 +66,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;
@ -68,6 +75,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;
@ -75,6 +83,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;
@ -83,6 +92,7 @@ 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;
@ -90,6 +100,7 @@ export declare function println(string: string, delay?: number): void;
* @brief Prints a string by Alt+Numpad method - works only on Windows! * @brief Prints a string by Alt+Numpad method - works only on Windows!
* @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 altPrint(string: string, delay?: number): void; export declare function altPrint(string: string, delay?: number): void;
@ -98,10 +109,12 @@ export declare function altPrint(string: string, delay?: number): void;
* Presses "Enter" after printing the string * Presses "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 altPrintln(string: string, delay?: number): void; export declare function altPrintln(string: string, delay?: number): void;
/** /**
* @brief Releases usb, optional, but allows to switch usb profile * @brief Releases usb, optional, but allows to switch usb profile
* @version Added in JS SDK 0.1
*/ */
export declare function quit(): void; 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,367 @@
/**
* 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
*/
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
*/
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 Reads a JS value from a file
*
* Reads a file at the specified path, interprets it as a JS value and returns
* said value.
*
* @param path The path to the file
* @version Added in JS SDK 0.1
*/
declare function load(path: string): any;
/**
* @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,171 @@
/**
* ```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> {
set<P extends keyof Props>(property: P, value: Props[P]): void;
}
export declare class ViewFactory<Props extends Properties, V extends View<Props>> {
make(): V;
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;
/**
* 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

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

View File

@ -1,6 +1,13 @@
/**
* 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;
@ -10,11 +17,15 @@ export declare function success(): void;
*/ */
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,3 +1,9 @@
/**
* Module for accessing the serial port
* @version Added in JS SDK 0.1
* @module
*/
/** /**
* @brief Initializes the serial port * @brief Initializes the serial port
* *
@ -5,6 +11,7 @@
* *
* @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;
@ -16,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;
@ -27,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;
@ -42,6 +51,7 @@ 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;
@ -55,6 +65,7 @@ export declare function readln(timeout?: number): string;
* wait forever. * 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 readAny(timeout?: number): string | undefined; export declare function readAny(timeout?: number): string | undefined;
@ -66,6 +77,7 @@ export declare function readAny(timeout?: number): string | undefined;
* 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;
@ -89,10 +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 * @brief Deinitializes the serial port, allowing multiple initializations per script run
* @version Added in JS SDK 0.1
*/ */
export declare function end(): void; 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",
]
}

View File

@ -0,0 +1,19 @@
{
"$schema": "https://typedoc.org/schema.json",
"name": "Flipper Zero JS API",
"excludePrivate": true,
"entryPointStrategy": "expand",
"entryPoints": [
".",
],
"exclude": [
"node_modules"
],
"cleanOutputDir": true,
"out": "./docs",
"plugin": [
"typedoc-material-theme",
],
"readme": "./docs_readme.md",
"themeColor": "#ff8200",
}

View File

@ -1,70 +0,0 @@
type Lit = undefined | null | {};
/**
* Subscription control interface
*/
export interface Subscription {
/**
* Cancels the subscription, preventing any future events managed by the
* subscription from firing
*/
cancel(): void;
}
/**
* Opaque event source identifier
*/
export type Contract<Item = undefined> = symbol;
/**
* 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.
*/
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
*/
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)
*/
export function run(): void | never;
/**
* Stops the event loop
*/
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
*/
export function timer(mode: "oneshot" | "periodic", interval: number): Contract;
/**
* Message queue
*/
export interface Queue<T> {
/**
* Message event
*/
input: Contract<T>;
/**
* Sends a message to the queue
* @param message message to send
*/
send(message: T): void;
}
/**
* Creates a message queue
* @param length maximum queue capacity
*/
export function queue<T>(length: number): Queue<T>;

View File

@ -1,14 +0,0 @@
/**
* @brief Returns the device model
*/
export declare function getModel(): string;
/**
* @brief Returns the name of the virtual dolphin
*/
export declare function getName(): string;
/**
* @brief Returns the battery charge percentage
*/
export declare function getBatteryCharge(): number;

View File

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

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

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

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

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

@ -1,16 +0,0 @@
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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

View File

@ -0,0 +1 @@
2

View File

@ -20,7 +20,7 @@ To run the unit tests, follow these steps:
3. Launch the CLI session and run the `unit_tests` command. 3. Launch the CLI session and run the `unit_tests` command.
**NOTE:** To run a particular test (and skip all others), specify its name as the command argument. **NOTE:** To run a particular test (and skip all others), specify its name as the command argument.
See [test_index.c](https://github.com/flipperdevices/flipperzero-firmware/blob/dev/applications/debug/unit_tests/test_index.c) for the complete list of test names. Test names match application names defined [here](https://github.com/flipperdevices/flipperzero-firmware/blob/dev/applications/debug/unit_tests/application.fam).
## Adding unit tests ## Adding unit tests
@ -28,7 +28,7 @@ See [test_index.c](https://github.com/flipperdevices/flipperzero-firmware/blob/d
#### Entry point #### Entry point
The common entry point for all tests is the [unit_tests](https://github.com/flipperdevices/flipperzero-firmware/tree/dev/applications/debug/unit_tests) app. Test-specific code is placed into an arbitrarily named subdirectory and is then called from the [test_index.c](https://github.com/flipperdevices/flipperzero-firmware/tree/dev/applications/debug/unit_tests/test_index.c) source file. The common entry point for all tests is the [unit_tests](https://github.com/flipperdevices/flipperzero-firmware/tree/dev/applications/debug/unit_tests) app. Test-specific code is packaged as a `PLUGIN` app placed in a subdirectory of `tests` in the `unit_tests` mother-app and referenced in the common `application.fam`. Look at other tests for an example.
#### Test assets #### Test assets

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