Merge remote-tracking branch 'ofw/dev' into mntm-dev

This commit is contained in:
Willy-JL
2025-02-18 15:40:10 +00:00
119 changed files with 1701 additions and 214 deletions

View File

@@ -2,10 +2,21 @@
- UL: Desktop: Option to prevent Auto Lock when connected to USB/RPC (by @Dmitry422) - UL: Desktop: Option to prevent Auto Lock when connected to USB/RPC (by @Dmitry422)
- Desktop settings will be reset, need to reconfigure - Desktop settings will be reset, need to reconfigure
- Keybinds will remain configured - Keybinds will remain configured
- OFW: JS: New `gui/widget` view, replaces old `widget` module (by @portasynthinca3)
- Scripts using `widget` module will need to be updated
- Check the `gui.js` example for reference usage
### Added: ### Added:
- Apps: - Apps:
- Games: Quadrastic (by @ivanbarsukov) - Games: Quadrastic (by @ivanbarsukov)
- OFW: RFID: EM4305 support (by @Astrrra)
- Desktop:
- UL: Option to prevent Auto Lock when connected to USB/RPC (by @Dmitry422)
- OFW: Add the Showtime animation (by @Astrrra)
- OFW: JS: Features & bugfixes, SDK 0.2 (by @portasynthinca3)
- New `gui/widget` view, replaces old `widget` module
- Support for PWM in `gpio` module
- Stop `eventloop` on request and error
### Updated: ### Updated:
- Apps: - Apps:
@@ -17,11 +28,23 @@
- Metroflip: Big refactor with plugins and assets to save RAM, RavKav moved to Calypso parser (by @luu176), unified Calypso parser (by @DocSystem) - Metroflip: Big refactor with plugins and assets to save RAM, RavKav moved to Calypso parser (by @luu176), unified Calypso parser (by @DocSystem)
- Picopass: Added Save SR as legacy from saved menu, fix write key 'retry' when presented with new card (by @bettse) - Picopass: Added Save SR as legacy from saved menu, fix write key 'retry' when presented with new card (by @bettse)
- Pinball0: Prevent tilt before ball is in play, fixed Endless table by making bottom portal extend full width (by @rdefeo) - Pinball0: Prevent tilt before ball is in play, fixed Endless table by making bottom portal extend full width (by @rdefeo)
- OFW: Infrared: Increase max carrier limit to 1000000 (by @skotopes) - NFC:
- OFW: Added naming for DESFire cards + fix MF3ICD40 cards unable to be read (by @Demae)
- OFW: Enable MFUL sync poller to be provided with passwords (by @GMMan)
- Infrared:
- OFW: Add Fujitsu ASTG12LVCC to AC Universal Remote (by @KereruA0i)
- OFW: Increase max carrier limit to 1000000 (by @skotopes)
- OFW: API: Update mbedtls & expose AES (by @portasynthinca3)
### Fixed: ### Fixed:
- Asset Packs: Fix level-up animations not being themed (by @Willy-JL) - Asset Packs: Fix level-up animations not being themed (by @Willy-JL)
- About: Fix missing Prev. button when invoked from Device Info keybind (by @Willy-JL) - About: Fix missing Prev. button when invoked from Device Info keybind (by @Willy-JL)
- OFW: uFBT: Bumped action version in example github workflow for project template (by @hedger)
- OFW: NFC: ST25TB poller mode check (by @RebornedBrain)
- Furi:
- OFW: EventLoop unsubscribe fix (by @gsurkov & @portasynthinca3)
- OFW: Various bug fixes and improvements (by @skotopes)
- OFW: Ensure that `furi_record_create()` is passed a non-NULL data pointer (by @dcoles)
### Removed: ### Removed:
- Nothing - JS: Removed old `widget` module, replaced by new `gui/widget` view

View File

@@ -12,4 +12,4 @@ tests.assert_eq(false, doesSdkSupport(["abobus", "other-nonexistent-feature"]));
tests.assert_eq("momentum", flipper.firmwareVendor); tests.assert_eq("momentum", flipper.firmwareVendor);
tests.assert_eq(0, flipper.jsSdkVersion[0]); tests.assert_eq(0, flipper.jsSdkVersion[0]);
tests.assert_eq(1, flipper.jsSdkVersion[1]); tests.assert_eq(2, flipper.jsSdkVersion[1]);

View File

@@ -446,6 +446,55 @@ static int32_t test_furi_event_loop_consumer(void* p) {
return 0; return 0;
} }
typedef struct {
FuriEventLoop* event_loop;
FuriSemaphore* semaphore;
size_t counter;
} SelfUnsubTestTimerContext;
static void test_self_unsub_semaphore_callback(FuriEventLoopObject* object, void* context) {
furi_event_loop_unsubscribe(context, object); // shouldn't crash here
}
static void test_self_unsub_timer_callback(void* arg) {
SelfUnsubTestTimerContext* context = arg;
if(context->counter == 0) {
furi_semaphore_release(context->semaphore);
} else if(context->counter == 1) {
furi_event_loop_stop(context->event_loop);
}
context->counter++;
}
void test_furi_event_loop_self_unsubscribe(void) {
FuriEventLoop* event_loop = furi_event_loop_alloc();
FuriSemaphore* semaphore = furi_semaphore_alloc(1, 0);
furi_event_loop_subscribe_semaphore(
event_loop,
semaphore,
FuriEventLoopEventIn,
test_self_unsub_semaphore_callback,
event_loop);
SelfUnsubTestTimerContext timer_context = {
.event_loop = event_loop,
.semaphore = semaphore,
.counter = 0,
};
FuriEventLoopTimer* timer = furi_event_loop_timer_alloc(
event_loop, test_self_unsub_timer_callback, FuriEventLoopTimerTypePeriodic, &timer_context);
furi_event_loop_timer_start(timer, furi_ms_to_ticks(20));
furi_event_loop_run(event_loop);
furi_event_loop_timer_free(timer);
furi_semaphore_free(semaphore);
furi_event_loop_free(event_loop);
}
void test_furi_event_loop(void) { void test_furi_event_loop(void) {
TestFuriEventLoopData data = {}; TestFuriEventLoopData data = {};

View File

@@ -8,6 +8,7 @@ void test_furi_concurrent_access(void);
void test_furi_pubsub(void); 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_furi_event_loop_self_unsubscribe(void);
void test_errno_saving(void); void test_errno_saving(void);
void test_furi_primitives(void); void test_furi_primitives(void);
void test_stdin(void); void test_stdin(void);
@@ -46,6 +47,10 @@ MU_TEST(mu_test_furi_event_loop) {
test_furi_event_loop(); test_furi_event_loop();
} }
MU_TEST(mu_test_furi_event_loop_self_unsubscribe) {
test_furi_event_loop_self_unsubscribe();
}
MU_TEST(mu_test_errno_saving) { MU_TEST(mu_test_errno_saving) {
test_errno_saving(); test_errno_saving();
} }
@@ -68,6 +73,7 @@ MU_TEST_SUITE(test_suite) {
MU_RUN_TEST(mu_test_furi_pubsub); MU_RUN_TEST(mu_test_furi_pubsub);
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_furi_event_loop_self_unsubscribe);
MU_RUN_TEST(mu_test_stdio); MU_RUN_TEST(mu_test_stdio);
MU_RUN_TEST(mu_test_errno_saving); MU_RUN_TEST(mu_test_errno_saving);
MU_RUN_TEST(mu_test_furi_primitives); MU_RUN_TEST(mu_test_furi_primitives);

View File

@@ -262,7 +262,7 @@ static void mf_ultralight_reader_test(const char* path) {
nfc_listener_start(mfu_listener, NULL, NULL); nfc_listener_start(mfu_listener, NULL, NULL);
MfUltralightData* mfu_data = mf_ultralight_alloc(); MfUltralightData* mfu_data = mf_ultralight_alloc();
MfUltralightError error = mf_ultralight_poller_sync_read_card(poller, mfu_data); MfUltralightError error = mf_ultralight_poller_sync_read_card(poller, mfu_data, NULL);
mu_assert(error == MfUltralightErrorNone, "mf_ultralight_poller_sync_read_card() failed"); mu_assert(error == MfUltralightErrorNone, "mf_ultralight_poller_sync_read_card() failed");
nfc_listener_stop(mfu_listener); nfc_listener_stop(mfu_listener);
@@ -315,7 +315,7 @@ MU_TEST(ntag_213_locked_reader) {
nfc_listener_start(mfu_listener, NULL, NULL); nfc_listener_start(mfu_listener, NULL, NULL);
MfUltralightData* mfu_data = mf_ultralight_alloc(); MfUltralightData* mfu_data = mf_ultralight_alloc();
MfUltralightError error = mf_ultralight_poller_sync_read_card(poller, mfu_data); MfUltralightError error = mf_ultralight_poller_sync_read_card(poller, mfu_data, NULL);
mu_assert(error == MfUltralightErrorNone, "mf_ultralight_poller_sync_read_card() failed"); mu_assert(error == MfUltralightErrorNone, "mf_ultralight_poller_sync_read_card() failed");
nfc_listener_stop(mfu_listener); nfc_listener_stop(mfu_listener);
@@ -353,7 +353,7 @@ static void mf_ultralight_write(void) {
MfUltralightData* mfu_data = mf_ultralight_alloc(); MfUltralightData* mfu_data = mf_ultralight_alloc();
// Initial read // Initial read
MfUltralightError error = mf_ultralight_poller_sync_read_card(poller, mfu_data); MfUltralightError error = mf_ultralight_poller_sync_read_card(poller, mfu_data, NULL);
mu_assert(error == MfUltralightErrorNone, "mf_ultralight_poller_sync_read_card() failed"); mu_assert(error == MfUltralightErrorNone, "mf_ultralight_poller_sync_read_card() failed");
mu_assert( mu_assert(
@@ -371,7 +371,7 @@ static void mf_ultralight_write(void) {
} }
// Verification read // Verification read
error = mf_ultralight_poller_sync_read_card(poller, mfu_data); error = mf_ultralight_poller_sync_read_card(poller, mfu_data, NULL);
mu_assert(error == MfUltralightErrorNone, "mf_ultralight_poller_sync_read_card() failed"); mu_assert(error == MfUltralightErrorNone, "mf_ultralight_poller_sync_read_card() failed");
nfc_listener_stop(mfu_listener); nfc_listener_stop(mfu_listener);

View File

@@ -185,7 +185,7 @@ static int32_t usb_uart_worker(void* context) {
usb_uart->usb_mutex = furi_mutex_alloc(FuriMutexTypeNormal); usb_uart->usb_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
usb_uart->tx_thread = usb_uart->tx_thread =
furi_thread_alloc_ex("UsbUartTxWorker", 512, usb_uart_tx_thread, usb_uart); furi_thread_alloc_ex("UsbUartTxWorker", 768, usb_uart_tx_thread, usb_uart);
usb_uart_vcp_init(usb_uart, usb_uart->cfg.vcp_ch); usb_uart_vcp_init(usb_uart, usb_uart->cfg.vcp_ch);
usb_uart_serial_init(usb_uart, usb_uart->cfg.uart_ch); usb_uart_serial_init(usb_uart, usb_uart->cfg.uart_ch);
@@ -293,8 +293,6 @@ static int32_t usb_uart_worker(void* context) {
furi_hal_serial_send_break(usb_uart->serial_handle); furi_hal_serial_send_break(usb_uart->serial_handle);
} }
} }
usb_uart_vcp_deinit(usb_uart, usb_uart->cfg.vcp_ch);
usb_uart_serial_deinit(usb_uart);
furi_hal_gpio_init(USB_USART_DE_RE_PIN, GpioModeAnalog, GpioPullNo, GpioSpeedLow); furi_hal_gpio_init(USB_USART_DE_RE_PIN, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
@@ -307,6 +305,9 @@ static int32_t usb_uart_worker(void* context) {
furi_thread_join(usb_uart->tx_thread); furi_thread_join(usb_uart->tx_thread);
furi_thread_free(usb_uart->tx_thread); furi_thread_free(usb_uart->tx_thread);
usb_uart_vcp_deinit(usb_uart, usb_uart->cfg.vcp_ch);
usb_uart_serial_deinit(usb_uart);
furi_stream_buffer_free(usb_uart->rx_stream); furi_stream_buffer_free(usb_uart->rx_stream);
furi_mutex_free(usb_uart->usb_mutex); furi_mutex_free(usb_uart->usb_mutex);
furi_semaphore_free(usb_uart->tx_sem); furi_semaphore_free(usb_uart->tx_sem);

View File

@@ -2079,3 +2079,42 @@ type: raw
frequency: 38000 frequency: 38000
duty_cycle: 0.330000 duty_cycle: 0.330000
data: 9024 4481 655 551 655 1653 654 550 656 1652 655 1652 656 550 656 550 656 550 656 550 656 551 655 1652 655 551 655 1652 655 550 656 551 655 1654 654 550 656 550 656 551 655 1652 655 550 656 551 654 550 656 1652 655 1651 657 550 655 551 655 550 656 1654 654 550 656 1653 654 551 654 551 655 1651 656 551 655 19984 655 550 656 551 655 550 656 551 655 550 655 551 655 551 655 550 656 1654 653 1653 655 1653 655 550 655 551 655 550 656 551 655 551 655 550 656 551 655 551 655 551 655 551 655 551 655 550 656 550 656 550 656 551 655 551 655 551 655 1652 656 550 656 551 655 550 656 39996 8999 4479 656 551 655 1652 656 550 656 1653 655 1653 655 550 656 551 655 550 656 551 655 551 655 1652 655 551 655 1652 655 550 656 551 655 1653 655 551 655 550 656 550 656 1652 655 551 654 551 655 551 655 1652 655 1652 656 551 655 551 655 552 654 551 655 1653 655 1653 655 551 655 549 656 1653 655 552 654 19984 655 1652 655 551 655 550 656 1652 656 551 655 551 655 551 655 1652 655 1652 655 551 656 1652 656 1653 655 1653 655 551 655 1652 655 551 655 551 655 551 654 551 654 551 655 551 655 1653 655 550 656 551 655 1652 656 1653 654 551 655 551 655 551 655 550 655 550 656 551 655 data: 9024 4481 655 551 655 1653 654 550 656 1652 655 1652 656 550 656 550 656 550 656 550 656 551 655 1652 655 551 655 1652 655 550 656 551 655 1654 654 550 656 550 656 551 655 1652 655 550 656 551 654 550 656 1652 655 1651 657 550 655 551 655 550 656 1654 654 550 656 1653 654 551 654 551 655 1651 656 551 655 19984 655 550 656 551 655 550 656 551 655 550 655 551 655 551 655 550 656 1654 653 1653 655 1653 655 550 655 551 655 550 656 551 655 551 655 550 656 551 655 551 655 551 655 551 655 551 655 550 656 550 656 550 656 551 655 551 655 551 655 1652 656 550 656 551 655 550 656 39996 8999 4479 656 551 655 1652 656 550 656 1653 655 1653 655 550 656 551 655 550 656 551 655 551 655 1652 655 551 655 1652 655 550 656 551 655 1653 655 551 655 550 656 550 656 1652 655 551 654 551 655 551 655 1652 655 1652 656 551 655 551 655 552 654 551 655 1653 655 1653 655 551 655 549 656 1653 655 552 654 19984 655 1652 655 551 655 550 656 1652 656 551 655 551 655 551 655 1652 655 1652 655 551 656 1652 656 1653 655 1653 655 551 655 1652 655 551 655 551 655 551 654 551 654 551 655 551 655 1653 655 550 656 551 655 1652 656 1653 654 551 655 551 655 551 655 550 655 550 656 551 655
#
# Model: Fujitsu ASTG12LVCC
#
name: Off
type: raw
frequency: 38000
duty_cycle: 0.330000
data: 3258 1573 427 404 426 404 425 1180 428 403 427 1183 425 402 427 402 428 402 427 1180 428 1181 427 404 426 403 427 402 428 1181 427 1181 427 402 427 405 425 402 427 402 427 403 427 402 428 402 428 403 426 401 429 403 427 402 428 403 427 403 427 1180 428 401 428 404 425 401 428 402 427 402 427 402 427 402 428 1180 427 401 428 403 427 402 427 401 428 1180 427 401 428 402 427 402 428 402 427 401 428 403 427 1180 427 402 427 1180 427 1180 427 1177 429 1179 427 1179 427 1178 428
#
name: Dh
type: raw
frequency: 38000
duty_cycle: 0.330000
data: 39677 99167 3233 1570 425 405 425 404 425 1184 424 405 425 1182 426 405 424 404 426 404 425 1181 427 1182 426 403 427 403 426 404 426 1183 425 1183 425 403 426 404 426 406 424 404 425 405 425 402 427 405 425 404 425 403 426 404 426 404 425 402 427 405 424 1182 425 402 427 404 426 403 426 404 425 404 426 404 425 404 425 1183 424 406 423 404 426 403 426 404 425 1181 427 1182 426 1181 426 1181 426 1181 425 1182 425 1181 426 1182 426 403 426 404 425 1182 426 404 425 404 425 405 425 404 426 403 426 403 427 404 426 404 426 1182 426 1182 426 403 426 404 426 1182 426 405 424 404 426 403 426 1182 426 405 425 403 426 1182 426 404 426 1183 425 403 426 403 426 404 425 403 426 405 425 403 426 1182 425 1182 425 403 427 404 425 1181 426 403 427 403 426 404 425 406 424 404 426 404 425 404 426 404 425 404 426 404 426 404 426 404 426 404 425 404 426 404 426 403 426 403 427 404 425 402 427 405 425 403 426 404 425 404 425 404 425 405 425 404 425 404 425 404 426 403 426 402 427 403 427 403 426 1182 425 404 426 404 425 403 426 1182 425 403 426 1181 426 403 427 403 426 404 425 405 425
#
name: Cool_hi
type: raw
frequency: 38000
duty_cycle: 0.330000
data: 39674 99137 3228 1573 422 407 421 408 422 1185 423 408 422 1187 421 410 419 409 421 408 421 1186 422 1187 421 408 421 408 422 409 421 1187 420 1186 422 408 421 410 419 407 424 408 421 407 421 410 420 409 421 408 422 408 421 408 422 407 422 410 419 408 422 1187 420 408 421 408 422 408 421 408 421 408 421 408 420 409 421 1187 444 382 422 408 422 408 421 408 445 1162 419 1187 420 1184 423 1185 421 1184 423 1186 421 1186 421 1187 422 409 419 409 420 1186 422 407 420 409 422 409 420 407 422 407 422 411 419 406 421 409 422 1185 446 1162 420 409 421 409 421 1189 418 407 421 408 422 407 422 409 420 409 421 408 420 412 417 1187 421 407 422 408 420 410 421 408 421 409 421 409 445 384 420 410 421 407 421 407 422 409 421 1187 420 409 419 409 421 408 422 408 421 410 419 409 420 410 419 410 420 407 422 409 420 408 421 407 422 408 421 408 421 410 419 409 420 407 423 407 422 409 421 410 419 411 418 408 421 408 422 410 420 407 421 409 419 409 421 409 419 408 422 407 422 407 422 409 420 1188 419 409 421 409 420 409 419 1189 419 1186 421 1188 419 1187 420 408 421 407 422 1188 419
#
name: Cool_lo
type: raw
frequency: 38000
duty_cycle: 0.330000
data: 39689 99188 3229 1576 421 407 422 409 420 1188 419 409 421 1187 422 408 422 409 419 410 419 1187 422 1186 423 410 419 409 421 409 420 1187 420 1188 420 410 447 382 420 409 422 410 446 383 422 409 420 407 422 410 395 435 420 408 422 407 422 410 420 409 445 1162 420 410 420 409 420 410 420 409 421 410 419 409 421 409 419 1189 420 407 422 409 395 437 419 410 418 1186 422 1186 423 1187 420 1185 422 1188 420 1184 421 1188 419 1188 419 408 420 410 419 1186 421 408 420 410 419 410 419 409 419 411 418 409 421 410 419 409 420 1187 445 1163 419 412 417 409 420 1188 419 409 419 410 420 409 444 1164 418 1187 419 1189 419 409 419 1187 420 408 422 409 419 410 420 409 419 410 419 434 393 411 420 409 421 408 421 409 419 409 421 1188 418 410 419 410 420 410 418 412 417 409 445 385 419 409 420 410 420 408 419 409 421 410 419 411 419 408 446 382 421 409 420 409 420 410 418 409 420 409 419 410 419 412 442 384 419 411 416 412 419 409 420 410 419 410 419 410 418 410 420 409 420 409 420 434 394 1187 419 412 417 410 418 410 420 1188 419 1187 420 1188 419 410 419 1188 419 411 418 434 396
#
name: Heat_hi
type: raw
frequency: 38000
duty_cycle: 0.330000
data: 39692 99118 3226 1571 423 406 423 405 423 1185 421 405 424 1183 423 406 422 408 422 405 423 1184 446 1160 422 409 420 406 424 405 424 1185 422 1183 424 404 425 406 422 408 421 406 423 407 422 406 423 407 421 406 424 407 422 407 422 404 425 407 421 409 420 1185 422 406 423 408 421 404 424 405 424 407 447 383 421 406 424 1184 447 380 424 406 422 409 421 407 423 1183 447 1159 424 1185 422 1185 421 1184 422 1185 422 1185 421 1185 423 407 423 406 423 1185 423 406 424 406 423 408 446 381 424 406 423 408 421 406 424 406 423 1185 423 1184 422 407 423 407 422 1187 421 408 421 407 423 407 423 405 424 1185 422 1186 421 1184 423 407 422 407 422 1186 422 407 422 406 423 408 422 405 423 408 447 383 420 409 421 406 423 407 423 1184 423 407 423 407 422 408 421 408 423 406 424 406 422 409 422 406 423 408 421 408 421 406 422 406 424 407 422 406 423 409 421 407 422 408 423 406 423 406 423 409 446 382 447 384 420 407 423 405 424 406 423 406 423 407 423 407 422 406 423 405 422 407 424 406 422 1185 422 406 423 407 422 1183 423 1184 422 407 422 1185 423 1186 421 1184 424 407 422 1185 422
#
name: Heat_lo
type: raw
frequency: 38000
duty_cycle: 0.330000
data: 39670 99106 3227 1570 424 406 422 407 422 1183 424 405 424 1184 448 380 423 407 421 406 424 1183 424 1185 421 406 423 404 424 405 424 1184 423 1182 425 407 448 380 423 407 422 405 423 406 422 406 424 405 423 407 421 406 423 407 422 405 424 405 423 406 423 1184 422 408 421 408 422 405 424 406 421 407 422 406 423 405 423 1183 424 406 423 405 423 405 423 405 423 1186 421 1184 422 1184 422 1185 422 1184 447 1159 423 1184 422 1184 422 408 421 407 423 1184 421 407 448 381 422 405 423 409 421 406 422 406 422 407 422 406 423 1183 423 1185 422 406 423 405 424 1184 423 408 421 405 424 405 424 1184 422 1185 422 1184 422 407 423 408 420 409 420 1185 447 382 423 405 423 408 421 406 423 407 422 406 423 406 423 408 421 406 423 1183 424 407 422 406 424 405 424 406 423 407 423 406 423 408 422 407 422 405 424 408 421 407 422 407 422 406 423 406 423 407 422 406 423 406 422 408 421 407 422 408 421 407 422 406 423 408 422 406 423 405 423 409 422 406 422 406 423 406 423 407 422 407 423 405 424 1184 423 407 421 406 424 1184 423 1184 422 407 423 1183 423 405 424 1184 423 409 420 407 422
#

View File

@@ -131,6 +131,23 @@ App(
fap_libs=[], fap_libs=[],
) )
App(
appid="js_gui__widget",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_widget_ep",
requires=["js_app"],
sources=["modules/js_gui/widget.c"],
)
App(
appid="js_gui__icon",
apptype=FlipperAppType.PLUGIN,
entry_point="js_gui_icon_ep",
requires=["js_app"],
sources=["modules/js_gui/icon.c"],
fap_libs=["assets"],
)
App( App(
appid="js_notification", appid="js_notification",
apptype=FlipperAppType.PLUGIN, apptype=FlipperAppType.PLUGIN,

View File

@@ -3,6 +3,7 @@ let gpio = require("gpio");
// initialize pins // initialize pins
let led = gpio.get("pc3"); // same as `gpio.get(7)` let led = gpio.get("pc3"); // same as `gpio.get(7)`
let led2 = gpio.get("pa7"); // same as `gpio.get(2)`
let pot = gpio.get("pc0"); // same as `gpio.get(16)` let pot = gpio.get("pc0"); // same as `gpio.get(16)`
let button = gpio.get("pc1"); // same as `gpio.get(15)` let button = gpio.get("pc1"); // same as `gpio.get(15)`
led.init({ direction: "out", outMode: "push_pull" }); led.init({ direction: "out", outMode: "push_pull" });
@@ -16,6 +17,13 @@ eventLoop.subscribe(eventLoop.timer("periodic", 1000), function (_, _item, led,
return [led, !state]; return [led, !state];
}, led, true); }, led, true);
// cycle led pwm
print("Commencing PWM (PA7)");
eventLoop.subscribe(eventLoop.timer("periodic", 10), function (_, _item, led2, state) {
led2.pwmWrite(10000, state);
return [led2, (state + 1) % 101];
}, led2, 0);
// read potentiometer when button is pressed // 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) {

View File

@@ -9,8 +9,23 @@ let byteInputView = require("gui/byte_input");
let textBoxView = require("gui/text_box"); let textBoxView = require("gui/text_box");
let dialogView = require("gui/dialog"); let dialogView = require("gui/dialog");
let filePicker = require("gui/file_picker"); let filePicker = require("gui/file_picker");
let widget = require("gui/widget");
let icon = require("gui/icon");
let flipper = require("flipper"); let flipper = require("flipper");
// declare clock widget children
let cuteDolphinWithWatch = icon.getBuiltin("DolphinWait_59x54");
let jsLogo = icon.getBuiltin("js_script_10px");
let stopwatchWidgetElements = [
{ element: "string", x: 67, y: 44, align: "bl", font: "big_numbers", text: "00 00" },
{ element: "string", x: 77, y: 22, align: "bl", font: "primary", text: "Stopwatch" },
{ element: "frame", x: 64, y: 27, w: 28, h: 20, radius: 3 },
{ element: "frame", x: 100, y: 27, w: 28, h: 20, radius: 3 },
{ element: "icon", x: 0, y: 5, iconData: cuteDolphinWithWatch },
{ element: "icon", x: 64, y: 13, iconData: jsLogo },
{ element: "button", button: "right", text: "Back" },
];
// declare view instances // declare view instances
let views = { let views = {
loading: loadingView.make(), loading: loadingView.make(),
@@ -31,6 +46,7 @@ let views = {
longText: textBoxView.makeWith({ longText: textBoxView.makeWith({
text: "This is a very long string that demonstrates the TextBox view. Use the D-Pad to scroll backwards and forwards.\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rhoncus est malesuada quam egestas ultrices. Maecenas non eros a nulla eleifend vulputate et ut risus. Quisque in mauris mattis, venenatis risus eget, aliquam diam. Fusce pretium feugiat mauris, ut faucibus ex volutpat in. Phasellus volutpat ex sed gravida consectetur. Aliquam sed lectus feugiat, tristique lectus et, bibendum lacus. Ut sit amet augue eu sapien elementum aliquam quis vitae tortor. Vestibulum quis commodo odio. In elementum fermentum massa, eu pellentesque nibh cursus at. Integer eleifend lacus nec purus elementum sodales. Nulla elementum neque urna, non vulputate massa semper sed. Fusce ut nisi vitae dui blandit congue pretium vitae turpis.", text: "This is a very long string that demonstrates the TextBox view. Use the D-Pad to scroll backwards and forwards.\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rhoncus est malesuada quam egestas ultrices. Maecenas non eros a nulla eleifend vulputate et ut risus. Quisque in mauris mattis, venenatis risus eget, aliquam diam. Fusce pretium feugiat mauris, ut faucibus ex volutpat in. Phasellus volutpat ex sed gravida consectetur. Aliquam sed lectus feugiat, tristique lectus et, bibendum lacus. Ut sit amet augue eu sapien elementum aliquam quis vitae tortor. Vestibulum quis commodo odio. In elementum fermentum massa, eu pellentesque nibh cursus at. Integer eleifend lacus nec purus elementum sodales. Nulla elementum neque urna, non vulputate massa semper sed. Fusce ut nisi vitae dui blandit congue pretium vitae turpis.",
}), }),
stopwatchWidget: widget.makeWith({}, stopwatchWidgetElements),
demos: submenuView.makeWith({ demos: submenuView.makeWith({
header: "Choose a demo", header: "Choose a demo",
items: [ items: [
@@ -40,6 +56,7 @@ let views = {
"Byte input", "Byte input",
"Text box", "Text box",
"File picker", "File picker",
"Widget",
"Exit app", "Exit app",
], ],
}), }),
@@ -78,6 +95,8 @@ eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, v
views.helloDialog.set("center", "Nice!"); views.helloDialog.set("center", "Nice!");
gui.viewDispatcher.switchTo(views.helloDialog); gui.viewDispatcher.switchTo(views.helloDialog);
} else if (index === 6) { } else if (index === 6) {
gui.viewDispatcher.switchTo(views.stopwatchWidget);
} else if (index === 7) {
eventLoop.stop(); eventLoop.stop();
} }
}, gui, eventLoop, views); }, gui, eventLoop, views);
@@ -117,6 +136,31 @@ eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, gui, views
gui.viewDispatcher.switchTo(views.demos); gui.viewDispatcher.switchTo(views.demos);
}, gui, views, eventLoop); }, gui, views, eventLoop);
// go to the demo chooser screen when the right key is pressed on the widget screen
eventLoop.subscribe(views.stopwatchWidget.button, function (_sub, buttonId, gui, views) {
if (buttonId === "right")
gui.viewDispatcher.switchTo(views.demos);
}, gui, views);
// count time
eventLoop.subscribe(eventLoop.timer("periodic", 500), function (_sub, _item, views, stopwatchWidgetElements, halfSeconds) {
let text = (halfSeconds / 2 / 60).toString();
if (halfSeconds < 10 * 60 * 2)
text = "0" + text;
text += (halfSeconds % 2 === 0) ? ":" : " ";
if (((halfSeconds / 2) % 60) < 10)
text += "0";
text += ((halfSeconds / 2) % 60).toString();
stopwatchWidgetElements[0].text = text;
views.stopwatchWidget.setChildren(stopwatchWidgetElements);
halfSeconds++;
return [views, stopwatchWidgetElements, halfSeconds];
}, views, stopwatchWidgetElements, 0);
// run UI // run UI
gui.viewDispatcher.switchTo(views.demos); gui.viewDispatcher.switchTo(views.demos);
eventLoop.run(); eventLoop.run();

View File

@@ -267,6 +267,8 @@ void js_check_sdk_compatibility(struct mjs* mjs) {
static const char* extra_features[] = { static const char* extra_features[] = {
"baseline", // dummy "feature" "baseline", // dummy "feature"
"gpio-pwm",
"gui-widget",
// extra modules // extra modules
"blebeacon", "blebeacon",

View File

@@ -12,7 +12,7 @@
#define JS_SDK_VENDOR_FIRMWARE "momentum" #define JS_SDK_VENDOR_FIRMWARE "momentum"
#define JS_SDK_VENDOR "flipperdevices" #define JS_SDK_VENDOR "flipperdevices"
#define JS_SDK_MAJOR 0 #define JS_SDK_MAJOR 0
#define JS_SDK_MINOR 1 #define JS_SDK_MINOR 2
/** /**
* @brief Returns the foreign pointer in `obj["_"]` * @brief Returns the foreign pointer in `obj["_"]`
@@ -255,6 +255,18 @@ static inline void
return; \ return; \
} while(0) } while(0)
/**
* @brief Prepends an error, sets the JS return value to `undefined` and returns
* a value C function
* @warning This macro executes `return;` by design
*/
#define JS_ERROR_AND_RETURN_VAL(mjs, error_code, ret_val, ...) \
do { \
mjs_prepend_errorf(mjs, error_code, __VA_ARGS__); \
mjs_return(mjs, MJS_UNDEFINED); \
return ret_val; \
} while(0)
typedef struct JsModules JsModules; typedef struct JsModules JsModules;
typedef void* (*JsModuleConstructor)(struct mjs* mjs, mjs_val_t* object, JsModules* modules); typedef void* (*JsModuleConstructor)(struct mjs* mjs, mjs_val_t* object, JsModules* modules);

View File

@@ -92,7 +92,7 @@ static void js_console_debug(struct mjs* mjs) {
} }
static void js_exit_flag_poll(struct mjs* mjs) { static void js_exit_flag_poll(struct mjs* mjs) {
uint32_t flags = furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny, 0); uint32_t flags = furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny | FuriFlagNoClear, 0);
if(flags & FuriFlagError) { if(flags & FuriFlagError) {
return; return;
} }
@@ -102,7 +102,8 @@ static void js_exit_flag_poll(struct mjs* mjs) {
} }
bool js_delay_with_flags(struct mjs* mjs, uint32_t time) { bool js_delay_with_flags(struct mjs* mjs, uint32_t time) {
uint32_t flags = furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny, time); uint32_t flags =
furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny | FuriFlagNoClear, time);
if(flags & FuriFlagError) { if(flags & FuriFlagError) {
return false; return false;
} }
@@ -124,7 +125,7 @@ uint32_t js_flags_wait(struct mjs* mjs, uint32_t flags_mask, uint32_t timeout) {
uint32_t flags = furi_thread_flags_get(); uint32_t flags = furi_thread_flags_get();
furi_check((flags & FuriFlagError) == 0); furi_check((flags & FuriFlagError) == 0);
if(flags == 0) { if(flags == 0) {
flags = furi_thread_flags_wait(flags_mask, FuriFlagWaitAny, timeout); flags = furi_thread_flags_wait(flags_mask, FuriFlagWaitAny | FuriFlagNoClear, timeout);
} else { } else {
uint32_t state = furi_thread_flags_clear(flags & flags_mask); uint32_t state = furi_thread_flags_clear(flags & flags_mask);
furi_check((state & FuriFlagError) == 0); furi_check((state & FuriFlagError) == 0);

View File

@@ -12,6 +12,7 @@
* @brief Context passed to the generic event callback * @brief Context passed to the generic event callback
*/ */
typedef struct { typedef struct {
FuriEventLoop* event_loop;
JsEventLoopObjectType object_type; JsEventLoopObjectType object_type;
struct mjs* mjs; struct mjs* mjs;
@@ -36,11 +37,6 @@ typedef struct {
void* subscriptions; // SubscriptionArray_t, which we can't reference in this definition void* subscriptions; // SubscriptionArray_t, which we can't reference in this definition
} JsEventLoopSubscription; } JsEventLoopSubscription;
typedef struct {
FuriEventLoop* loop;
struct mjs* mjs;
} JsEventLoopTickContext;
ARRAY_DEF(SubscriptionArray, JsEventLoopSubscription*, M_PTR_OPLIST); //-V575 ARRAY_DEF(SubscriptionArray, JsEventLoopSubscription*, M_PTR_OPLIST); //-V575
ARRAY_DEF(ContractArray, JsEventLoopContract*, M_PTR_OPLIST); //-V575 ARRAY_DEF(ContractArray, JsEventLoopContract*, M_PTR_OPLIST); //-V575
@@ -51,7 +47,6 @@ struct JsEventLoop {
FuriEventLoop* loop; FuriEventLoop* loop;
SubscriptionArray_t subscriptions; SubscriptionArray_t subscriptions;
ContractArray_t owned_contracts; //<! Contracts that were produced by this module ContractArray_t owned_contracts; //<! Contracts that were produced by this module
JsEventLoopTickContext* tick_context;
}; };
/** /**
@@ -60,7 +55,7 @@ struct JsEventLoop {
static void js_event_loop_callback_generic(void* param) { static void js_event_loop_callback_generic(void* param) {
JsEventLoopCallbackContext* context = param; JsEventLoopCallbackContext* context = param;
mjs_val_t result; mjs_val_t result;
mjs_apply( mjs_err_t error = mjs_apply(
context->mjs, context->mjs,
&result, &result,
context->callback, context->callback,
@@ -68,6 +63,12 @@ static void js_event_loop_callback_generic(void* param) {
context->arity, context->arity,
context->arguments); context->arguments);
bool is_error = strcmp(mjs_strerror(context->mjs, error), "NO_ERROR") != 0;
bool asked_to_stop = js_flags_wait(context->mjs, ThreadEventStop, 0) & ThreadEventStop;
if(is_error || asked_to_stop) {
furi_event_loop_stop(context->event_loop);
}
// save returned args for next call // save returned args for next call
if(mjs_array_length(context->mjs, result) != context->arity - SYSTEM_ARGS) return; if(mjs_array_length(context->mjs, result) != context->arity - SYSTEM_ARGS) return;
for(size_t i = 0; i < context->arity - SYSTEM_ARGS; i++) { for(size_t i = 0; i < context->arity - SYSTEM_ARGS; i++) {
@@ -111,11 +112,14 @@ static void js_event_loop_subscription_cancel(struct mjs* mjs) {
JsEventLoopSubscription* subscription = JS_GET_CONTEXT(mjs); JsEventLoopSubscription* subscription = JS_GET_CONTEXT(mjs);
if(subscription->object_type == JsEventLoopObjectTypeTimer) { if(subscription->object_type == JsEventLoopObjectTypeTimer) {
// timer operations are deferred, which creates lifetime issues
// just stop the timer and let the cleanup routine free everything when the script is done
furi_event_loop_timer_stop(subscription->object); furi_event_loop_timer_stop(subscription->object);
} else { return;
furi_event_loop_unsubscribe(subscription->loop, subscription->object);
} }
furi_event_loop_unsubscribe(subscription->loop, subscription->object);
free(subscription->context->arguments); free(subscription->context->arguments);
free(subscription->context); free(subscription->context);
@@ -158,6 +162,7 @@ static void js_event_loop_subscribe(struct mjs* mjs) {
mjs_set(mjs, subscription_obj, "cancel", ~0, MJS_MK_FN(js_event_loop_subscription_cancel)); mjs_set(mjs, subscription_obj, "cancel", ~0, MJS_MK_FN(js_event_loop_subscription_cancel));
// create callback context // create callback context
context->event_loop = module->loop;
context->object_type = contract->object_type; context->object_type = contract->object_type;
context->arity = mjs_nargs(mjs) - SYSTEM_ARGS + 2; context->arity = mjs_nargs(mjs) - SYSTEM_ARGS + 2;
context->arguments = calloc(context->arity, sizeof(mjs_val_t)); context->arguments = calloc(context->arity, sizeof(mjs_val_t));
@@ -333,37 +338,22 @@ static void js_event_loop_queue(struct mjs* mjs) {
mjs_return(mjs, queue); mjs_return(mjs, queue);
} }
static void js_event_loop_tick(void* param) {
JsEventLoopTickContext* context = param;
uint32_t flags = furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny | FuriFlagNoClear, 0);
if(flags & FuriFlagError) {
return;
}
if(flags & ThreadEventStop) {
furi_event_loop_stop(context->loop);
mjs_exit(context->mjs);
}
}
static void* js_event_loop_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { static void* js_event_loop_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules); UNUSED(modules);
mjs_val_t event_loop_obj = mjs_mk_object(mjs); mjs_val_t event_loop_obj = mjs_mk_object(mjs);
JsEventLoop* module = malloc(sizeof(JsEventLoop)); JsEventLoop* module = malloc(sizeof(JsEventLoop));
JsEventLoopTickContext* tick_ctx = malloc(sizeof(JsEventLoopTickContext));
module->loop = furi_event_loop_alloc(); module->loop = furi_event_loop_alloc();
tick_ctx->loop = module->loop;
tick_ctx->mjs = mjs;
module->tick_context = tick_ctx;
furi_event_loop_tick_set(module->loop, 10, js_event_loop_tick, tick_ctx);
SubscriptionArray_init(module->subscriptions); SubscriptionArray_init(module->subscriptions);
ContractArray_init(module->owned_contracts); ContractArray_init(module->owned_contracts);
mjs_set(mjs, event_loop_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, module)); JS_ASSIGN_MULTI(mjs, event_loop_obj) {
mjs_set(mjs, event_loop_obj, "subscribe", ~0, MJS_MK_FN(js_event_loop_subscribe)); JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, module));
mjs_set(mjs, event_loop_obj, "run", ~0, MJS_MK_FN(js_event_loop_run)); JS_FIELD("subscribe", MJS_MK_FN(js_event_loop_subscribe));
mjs_set(mjs, event_loop_obj, "stop", ~0, MJS_MK_FN(js_event_loop_stop)); JS_FIELD("run", MJS_MK_FN(js_event_loop_run));
mjs_set(mjs, event_loop_obj, "timer", ~0, MJS_MK_FN(js_event_loop_timer)); JS_FIELD("stop", MJS_MK_FN(js_event_loop_stop));
mjs_set(mjs, event_loop_obj, "queue", ~0, MJS_MK_FN(js_event_loop_queue)); JS_FIELD("timer", MJS_MK_FN(js_event_loop_timer));
JS_FIELD("queue", MJS_MK_FN(js_event_loop_queue));
}
*object = event_loop_obj; *object = event_loop_obj;
return module; return module;
@@ -418,7 +408,6 @@ static void js_event_loop_destroy(void* inst) {
ContractArray_clear(module->owned_contracts); ContractArray_clear(module->owned_contracts);
furi_event_loop_free(module->loop); furi_event_loop_free(module->loop);
free(module->tick_context);
free(module); free(module);
} }
} }

View File

@@ -1,6 +1,7 @@
#include "../js_modules.h" // IWYU pragma: keep #include "../js_modules.h" // IWYU pragma: keep
#include "./js_event_loop/js_event_loop.h" #include "./js_event_loop/js_event_loop.h"
#include <furi_hal_gpio.h> #include <furi_hal_gpio.h>
#include <furi_hal_pwm.h>
#include <furi_hal_resources.h> #include <furi_hal_resources.h>
#include <expansion/expansion.h> #include <expansion/expansion.h>
#include <limits.h> #include <limits.h>
@@ -17,6 +18,7 @@ typedef struct {
FuriSemaphore* interrupt_semaphore; FuriSemaphore* interrupt_semaphore;
JsEventLoopContract* interrupt_contract; JsEventLoopContract* interrupt_contract;
FuriHalAdcChannel adc_channel; FuriHalAdcChannel adc_channel;
FuriHalPwmOutputId pwm_output;
FuriHalAdcHandle* adc_handle; FuriHalAdcHandle* adc_handle;
} JsGpioPinInst; } JsGpioPinInst;
@@ -231,6 +233,88 @@ static void js_gpio_read_analog(struct mjs* mjs) {
mjs_return(mjs, mjs_mk_number(mjs, (double)millivolts)); mjs_return(mjs, mjs_mk_number(mjs, (double)millivolts));
} }
/**
* @brief Determines whether this pin supports PWM
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* assert_eq(true, gpio.get("pa4").isPwmSupported());
* assert_eq(false, gpio.get("pa5").isPwmSupported());
* ```
*/
static void js_gpio_is_pwm_supported(struct mjs* mjs) {
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
mjs_return(mjs, mjs_mk_boolean(mjs, manager_data->pwm_output != FuriHalPwmOutputIdNone));
}
/**
* @brief Sets PWM parameters and starts the PWM
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* let pa4 = gpio.get("pa4");
* pa4.pwmWrite(10000, 50);
* ```
*/
static void js_gpio_pwm_write(struct mjs* mjs) {
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
int32_t frequency, duty;
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&frequency), JS_ARG_INT32(&duty));
if(manager_data->pwm_output == FuriHalPwmOutputIdNone) {
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "PWM is not supported on this pin");
}
if(furi_hal_pwm_is_running(manager_data->pwm_output)) {
furi_hal_pwm_set_params(manager_data->pwm_output, frequency, duty);
} else {
furi_hal_pwm_start(manager_data->pwm_output, frequency, duty);
}
}
/**
* @brief Determines whether PWM is running
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* assert_eq(false, gpio.get("pa4").isPwmRunning());
* ```
*/
static void js_gpio_is_pwm_running(struct mjs* mjs) {
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
if(manager_data->pwm_output == FuriHalPwmOutputIdNone) {
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "PWM is not supported on this pin");
}
mjs_return(mjs, mjs_mk_boolean(mjs, furi_hal_pwm_is_running(manager_data->pwm_output)));
}
/**
* @brief Stops PWM
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* let pa4 = gpio.get("pa4");
* pa4.pwmWrite(10000, 50);
* pa4.pwmStop();
* ```
*/
static void js_gpio_pwm_stop(struct mjs* mjs) {
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
if(manager_data->pwm_output != FuriHalPwmOutputIdNone) {
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "PWM is not supported on this pin");
}
furi_hal_pwm_stop(manager_data->pwm_output);
}
/** /**
* @brief Returns an object that manages a specified pin. * @brief Returns an object that manages a specified pin.
* *
@@ -269,12 +353,19 @@ static void js_gpio_get(struct mjs* mjs) {
manager_data->interrupt_semaphore = furi_semaphore_alloc(UINT32_MAX, 0); manager_data->interrupt_semaphore = furi_semaphore_alloc(UINT32_MAX, 0);
manager_data->adc_handle = module->adc_handle; manager_data->adc_handle = module->adc_handle;
manager_data->adc_channel = pin_record->channel; manager_data->adc_channel = pin_record->channel;
mjs_set(mjs, manager, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, manager_data)); manager_data->pwm_output = pin_record->pwm_output;
mjs_set(mjs, manager, "init", ~0, MJS_MK_FN(js_gpio_init)); JS_ASSIGN_MULTI(mjs, manager) {
mjs_set(mjs, manager, "write", ~0, MJS_MK_FN(js_gpio_write)); JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, manager_data));
mjs_set(mjs, manager, "read", ~0, MJS_MK_FN(js_gpio_read)); JS_FIELD("init", MJS_MK_FN(js_gpio_init));
mjs_set(mjs, manager, "readAnalog", ~0, MJS_MK_FN(js_gpio_read_analog)); JS_FIELD("write", MJS_MK_FN(js_gpio_write));
mjs_set(mjs, manager, "interrupt", ~0, MJS_MK_FN(js_gpio_interrupt)); JS_FIELD("read", MJS_MK_FN(js_gpio_read));
JS_FIELD("readAnalog", MJS_MK_FN(js_gpio_read_analog));
JS_FIELD("interrupt", MJS_MK_FN(js_gpio_interrupt));
JS_FIELD("isPwmSupported", MJS_MK_FN(js_gpio_is_pwm_supported));
JS_FIELD("pwmWrite", MJS_MK_FN(js_gpio_pwm_write));
JS_FIELD("isPwmRunning", MJS_MK_FN(js_gpio_is_pwm_running));
JS_FIELD("pwmStop", MJS_MK_FN(js_gpio_pwm_stop));
}
mjs_return(mjs, manager); mjs_return(mjs, manager);
// remember pin // remember pin

View File

@@ -0,0 +1,61 @@
#include "../../js_modules.h"
#include <assets_icons.h>
typedef struct {
const char* name;
const Icon* data;
} IconDefinition;
#define ICON_DEF(icon) \
(IconDefinition) { \
.name = #icon, .data = &I_##icon \
}
static const IconDefinition builtin_icons[] = {
ICON_DEF(DolphinWait_59x54),
ICON_DEF(js_script_10px),
};
static void js_gui_icon_get_builtin(struct mjs* mjs) {
const char* icon_name;
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&icon_name));
for(size_t i = 0; i < COUNT_OF(builtin_icons); i++) {
if(strcmp(icon_name, builtin_icons[i].name) == 0) {
mjs_return(mjs, mjs_mk_foreign(mjs, (void*)builtin_icons[i].data));
return;
}
}
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "no such built-in icon");
}
static void* js_gui_icon_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
*object = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, *object) {
JS_FIELD("getBuiltin", MJS_MK_FN(js_gui_icon_get_builtin));
}
return NULL;
}
static void js_gui_icon_destroy(void* inst) {
UNUSED(inst);
}
static const JsModuleDescriptor js_gui_icon_desc = {
"gui__icon",
js_gui_icon_create,
js_gui_icon_destroy,
NULL,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_gui_icon_desc,
};
const FlipperAppPluginDescriptor* js_gui_icon_ep(void) {
return &plugin_descriptor;
}

View File

@@ -247,6 +247,22 @@ static bool
return false; return false;
} }
/**
* @brief Sets the list of children. Not available from JS.
*/
static bool
js_gui_view_internal_set_children(struct mjs* mjs, mjs_val_t children, JsGuiViewData* data) {
data->descriptor->reset_children(data->specific_view, data->custom_data);
for(size_t i = 0; i < mjs_array_length(mjs, children); i++) {
mjs_val_t child = mjs_array_get(mjs, children, i);
if(!data->descriptor->add_child(mjs, data->specific_view, data->custom_data, child))
return false;
}
return true;
}
/** /**
* @brief `View.set` * @brief `View.set`
*/ */
@@ -260,6 +276,46 @@ static void js_gui_view_set(struct mjs* mjs) {
mjs_return(mjs, MJS_UNDEFINED); mjs_return(mjs, MJS_UNDEFINED);
} }
/**
* @brief `View.addChild`
*/
static void js_gui_view_add_child(struct mjs* mjs) {
JsGuiViewData* data = JS_GET_CONTEXT(mjs);
if(!data->descriptor->add_child || !data->descriptor->reset_children)
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children");
mjs_val_t child;
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&child));
bool success = data->descriptor->add_child(mjs, data->specific_view, data->custom_data, child);
UNUSED(success);
mjs_return(mjs, MJS_UNDEFINED);
}
/**
* @brief `View.resetChildren`
*/
static void js_gui_view_reset_children(struct mjs* mjs) {
JsGuiViewData* data = JS_GET_CONTEXT(mjs);
if(!data->descriptor->add_child || !data->descriptor->reset_children)
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children");
data->descriptor->reset_children(data->specific_view, data->custom_data);
mjs_return(mjs, MJS_UNDEFINED);
}
/**
* @brief `View.setChildren`
*/
static void js_gui_view_set_children(struct mjs* mjs) {
JsGuiViewData* data = JS_GET_CONTEXT(mjs);
if(!data->descriptor->add_child || !data->descriptor->reset_children)
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children");
mjs_val_t children;
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ARR(&children));
js_gui_view_internal_set_children(mjs, children, data);
}
/** /**
* @brief `View` destructor * @brief `View` destructor
*/ */
@@ -283,7 +339,12 @@ static mjs_val_t js_gui_make_view(struct mjs* mjs, const JsViewDescriptor* descr
// generic view API // generic view API
mjs_val_t view_obj = mjs_mk_object(mjs); mjs_val_t view_obj = mjs_mk_object(mjs);
mjs_set(mjs, view_obj, "set", ~0, MJS_MK_FN(js_gui_view_set)); JS_ASSIGN_MULTI(mjs, view_obj) {
JS_FIELD("set", MJS_MK_FN(js_gui_view_set));
JS_FIELD("addChild", MJS_MK_FN(js_gui_view_add_child));
JS_FIELD("resetChildren", MJS_MK_FN(js_gui_view_reset_children));
JS_FIELD("setChildren", MJS_MK_FN(js_gui_view_set_children));
}
// object data // object data
JsGuiViewData* data = malloc(sizeof(JsGuiViewData)); JsGuiViewData* data = malloc(sizeof(JsGuiViewData));
@@ -314,7 +375,7 @@ static void js_gui_vf_make(struct mjs* mjs) {
*/ */
static void js_gui_vf_make_with(struct mjs* mjs) { static void js_gui_vf_make_with(struct mjs* mjs) {
mjs_val_t props; mjs_val_t props;
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&props)); JS_FETCH_ARGS_OR_RETURN(mjs, JS_AT_LEAST, JS_ARG_OBJ(&props));
const JsViewDescriptor* descriptor = JS_GET_CONTEXT(mjs); const JsViewDescriptor* descriptor = JS_GET_CONTEXT(mjs);
// make the object like normal // make the object like normal
@@ -334,6 +395,18 @@ static void js_gui_vf_make_with(struct mjs* mjs) {
} }
} }
// assign children
if(mjs_nargs(mjs) >= 2) {
if(!data->descriptor->add_child || !data->descriptor->reset_children)
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children");
mjs_val_t children = mjs_arg(mjs, 1);
if(!mjs_is_array(children))
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "argument 1: expected array");
if(!js_gui_view_internal_set_children(mjs, children, data)) return;
}
mjs_return(mjs, view_obj); mjs_return(mjs, view_obj);
} }

View File

@@ -50,6 +50,11 @@ typedef void (*JsViewFree)(void* specific_view);
typedef void* (*JsViewCustomMake)(struct mjs* mjs, void* specific_view, mjs_val_t view_obj); typedef void* (*JsViewCustomMake)(struct mjs* mjs, void* specific_view, mjs_val_t view_obj);
/** @brief Context destruction for glue code */ /** @brief Context destruction for glue code */
typedef void (*JsViewCustomDestroy)(void* specific_view, void* custom_state, FuriEventLoop* loop); typedef void (*JsViewCustomDestroy)(void* specific_view, void* custom_state, FuriEventLoop* loop);
/** @brief `addChild` callback for glue code */
typedef bool (
*JsViewAddChild)(struct mjs* mjs, void* specific_view, void* custom_state, mjs_val_t child_obj);
/** @brief `resetChildren` callback for glue code */
typedef void (*JsViewResetChildren)(void* specific_view, void* custom_state);
/** /**
* @brief Descriptor for a JS view * @brief Descriptor for a JS view
@@ -66,15 +71,22 @@ typedef struct {
JsViewAlloc alloc; JsViewAlloc alloc;
JsViewGetView get_view; JsViewGetView get_view;
JsViewFree free; JsViewFree free;
JsViewCustomMake custom_make; // <! May be NULL JsViewCustomMake custom_make; // <! May be NULL
JsViewCustomDestroy custom_destroy; // <! May be NULL JsViewCustomDestroy custom_destroy; // <! May be NULL
JsViewAddChild add_child; // <! May be NULL
JsViewResetChildren reset_children; // <! May be NULL
size_t prop_cnt; //<! Number of properties visible from JS size_t prop_cnt; //<! Number of properties visible from JS
JsViewPropDescriptor props[]; // <! Descriptors of properties visible from JS JsViewPropDescriptor props[]; // <! Descriptors of properties visible from JS
} JsViewDescriptor; } JsViewDescriptor;
// Callback ordering: // Callback ordering:
// alloc -> get_view -> [custom_make (if set)] -> props[i].assign -> [custom_destroy (if_set)] -> free // +-> add_child -+
// \_______________ creation ________________/ \___ usage ___/ \_________ destruction _________/ // +-> reset_children -+
// alloc -> get_view -> custom_make -+-> props[i].assign -+> custom_destroy -> free
// \__________ creation __________/ \____ use ____/ \___ destruction ____/
/** /**
* @brief Creates a JS `ViewFactory` object * @brief Creates a JS `ViewFactory` object

View File

@@ -0,0 +1,281 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/widget.h>
typedef struct {
FuriMessageQueue* queue;
JsEventLoopContract contract;
} JsWidgetCtx;
#define QUEUE_LEN 2
/**
* @brief Parses position (X and Y) from an element declaration object
*/
static bool element_get_position(struct mjs* mjs, mjs_val_t element, int32_t* x, int32_t* y) {
mjs_val_t x_in = mjs_get(mjs, element, "x", ~0);
mjs_val_t y_in = mjs_get(mjs, element, "y", ~0);
if(!mjs_is_number(x_in) || !mjs_is_number(y_in)) return false;
*x = mjs_get_int32(mjs, x_in);
*y = mjs_get_int32(mjs, y_in);
return true;
}
/**
* @brief Parses size (W and h) from an element declaration object
*/
static bool element_get_size(struct mjs* mjs, mjs_val_t element, int32_t* w, int32_t* h) {
mjs_val_t w_in = mjs_get(mjs, element, "w", ~0);
mjs_val_t h_in = mjs_get(mjs, element, "h", ~0);
if(!mjs_is_number(w_in) || !mjs_is_number(h_in)) return false;
*w = mjs_get_int32(mjs, w_in);
*h = mjs_get_int32(mjs, h_in);
return true;
}
/**
* @brief Parses alignment (V and H) from an element declaration object
*/
static bool
element_get_alignment(struct mjs* mjs, mjs_val_t element, Align* align_v, Align* align_h) {
mjs_val_t align_in = mjs_get(mjs, element, "align", ~0);
const char* align = mjs_get_string(mjs, &align_in, NULL);
if(!align) return false;
if(strlen(align) != 2) return false;
if(align[0] == 't') {
*align_v = AlignTop;
} else if(align[0] == 'c') {
*align_v = AlignCenter;
} else if(align[0] == 'b') {
*align_v = AlignBottom;
} else {
return false;
}
if(align[1] == 'l') {
*align_h = AlignLeft;
} else if(align[1] == 'm') { // m = middle
*align_h = AlignCenter;
} else if(align[1] == 'r') {
*align_h = AlignRight;
} else {
return false;
}
return true;
}
/**
* @brief Parses font from an element declaration object
*/
static bool element_get_font(struct mjs* mjs, mjs_val_t element, Font* font) {
mjs_val_t font_in = mjs_get(mjs, element, "font", ~0);
const char* font_str = mjs_get_string(mjs, &font_in, NULL);
if(!font_str) return false;
if(strcmp(font_str, "primary") == 0) {
*font = FontPrimary;
} else if(strcmp(font_str, "secondary") == 0) {
*font = FontSecondary;
} else if(strcmp(font_str, "keyboard") == 0) {
*font = FontKeyboard;
} else if(strcmp(font_str, "big_numbers") == 0) {
*font = FontBigNumbers;
} else {
return false;
}
return true;
}
/**
* @brief Parses text from an element declaration object
*/
static bool element_get_text(struct mjs* mjs, mjs_val_t element, mjs_val_t* text) {
*text = mjs_get(mjs, element, "text", ~0);
return mjs_is_string(*text);
}
/**
* @brief Widget button element callback
*/
static void js_widget_button_callback(GuiButtonType result, InputType type, JsWidgetCtx* context) {
UNUSED(type);
furi_check(furi_message_queue_put(context->queue, &result, 0) == FuriStatusOk);
}
#define DESTRUCTURE_OR_RETURN(mjs, child_obj, part, ...) \
if(!element_get_##part(mjs, child_obj, __VA_ARGS__)) \
JS_ERROR_AND_RETURN_VAL(mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element " #part);
static bool js_widget_add_child(
struct mjs* mjs,
Widget* widget,
JsWidgetCtx* context,
mjs_val_t child_obj) {
UNUSED(context);
if(!mjs_is_object(child_obj))
JS_ERROR_AND_RETURN_VAL(mjs, MJS_BAD_ARGS_ERROR, false, "child must be an object");
mjs_val_t element_type_term = mjs_get(mjs, child_obj, "element", ~0);
const char* element_type = mjs_get_string(mjs, &element_type_term, NULL);
if(!element_type)
JS_ERROR_AND_RETURN_VAL(
mjs, MJS_BAD_ARGS_ERROR, false, "child object must have `element` property");
if((strcmp(element_type, "string") == 0) || (strcmp(element_type, "string_multiline") == 0)) {
int32_t x, y;
Align align_v, align_h;
Font font;
mjs_val_t text;
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
DESTRUCTURE_OR_RETURN(mjs, child_obj, alignment, &align_v, &align_h);
DESTRUCTURE_OR_RETURN(mjs, child_obj, font, &font);
DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text);
if(strcmp(element_type, "string") == 0) {
widget_add_string_element(
widget, x, y, align_h, align_v, font, mjs_get_string(mjs, &text, NULL));
} else {
widget_add_string_multiline_element(
widget, x, y, align_h, align_v, font, mjs_get_string(mjs, &text, NULL));
}
} else if(strcmp(element_type, "text_box") == 0) {
int32_t x, y, w, h;
Align align_v, align_h;
Font font;
mjs_val_t text;
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
DESTRUCTURE_OR_RETURN(mjs, child_obj, size, &w, &h);
DESTRUCTURE_OR_RETURN(mjs, child_obj, alignment, &align_v, &align_h);
DESTRUCTURE_OR_RETURN(mjs, child_obj, font, &font);
DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text);
mjs_val_t strip_to_dots_in = mjs_get(mjs, child_obj, "stripToDots", ~0);
if(!mjs_is_boolean(strip_to_dots_in))
JS_ERROR_AND_RETURN_VAL(
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element stripToDots");
bool strip_to_dots = mjs_get_bool(mjs, strip_to_dots_in);
widget_add_text_box_element(
widget, x, y, w, h, align_h, align_v, mjs_get_string(mjs, &text, NULL), strip_to_dots);
} else if(strcmp(element_type, "text_scroll") == 0) {
int32_t x, y, w, h;
mjs_val_t text;
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
DESTRUCTURE_OR_RETURN(mjs, child_obj, size, &w, &h);
DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text);
widget_add_text_scroll_element(widget, x, y, w, h, mjs_get_string(mjs, &text, NULL));
} else if(strcmp(element_type, "button") == 0) {
mjs_val_t btn_in = mjs_get(mjs, child_obj, "button", ~0);
const char* btn_name = mjs_get_string(mjs, &btn_in, NULL);
if(!btn_name)
JS_ERROR_AND_RETURN_VAL(
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element button");
GuiButtonType btn_type;
if(strcmp(btn_name, "left") == 0) {
btn_type = GuiButtonTypeLeft;
} else if(strcmp(btn_name, "center") == 0) {
btn_type = GuiButtonTypeCenter;
} else if(strcmp(btn_name, "right") == 0) {
btn_type = GuiButtonTypeRight;
} else {
JS_ERROR_AND_RETURN_VAL(mjs, MJS_BAD_ARGS_ERROR, false, "incorrect button type");
}
mjs_val_t text;
DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text);
widget_add_button_element(
widget,
btn_type,
mjs_get_string(mjs, &text, NULL),
(ButtonCallback)js_widget_button_callback,
context);
} else if(strcmp(element_type, "icon") == 0) {
int32_t x, y;
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
mjs_val_t icon_data_in = mjs_get(mjs, child_obj, "iconData", ~0);
if(!mjs_is_foreign(icon_data_in))
JS_ERROR_AND_RETURN_VAL(
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element iconData");
const Icon* icon = mjs_get_ptr(mjs, icon_data_in);
widget_add_icon_element(widget, x, y, icon);
} else if(strcmp(element_type, "frame") == 0) {
int32_t x, y, w, h;
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
DESTRUCTURE_OR_RETURN(mjs, child_obj, size, &w, &h);
mjs_val_t radius_in = mjs_get(mjs, child_obj, "radius", ~0);
if(!mjs_is_number(radius_in))
JS_ERROR_AND_RETURN_VAL(
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element radius");
int32_t radius = mjs_get_int32(mjs, radius_in);
widget_add_frame_element(widget, x, y, w, h, radius);
}
return true;
}
static void js_widget_reset_children(Widget* widget, void* state) {
UNUSED(state);
widget_reset(widget);
}
static mjs_val_t js_widget_button_event_transformer(
struct mjs* mjs,
FuriMessageQueue* queue,
JsWidgetCtx* context) {
UNUSED(context);
GuiButtonType btn_type;
furi_check(furi_message_queue_get(queue, &btn_type, 0) == FuriStatusOk);
const char* btn_name;
if(btn_type == GuiButtonTypeLeft) {
btn_name = "left";
} else if(btn_type == GuiButtonTypeCenter) {
btn_name = "center";
} else if(btn_type == GuiButtonTypeRight) {
btn_name = "right";
} else {
furi_crash();
}
return mjs_mk_string(mjs, btn_name, ~0, false);
}
static void* js_widget_custom_make(struct mjs* mjs, Widget* widget, mjs_val_t view_obj) {
UNUSED(widget);
JsWidgetCtx* context = malloc(sizeof(JsWidgetCtx));
context->queue = furi_message_queue_alloc(QUEUE_LEN, sizeof(GuiButtonType));
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
.object = context->queue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)js_widget_button_event_transformer,
},
};
mjs_set(mjs, view_obj, "button", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void js_widget_custom_destroy(Widget* widget, JsWidgetCtx* context, FuriEventLoop* loop) {
UNUSED(widget);
furi_event_loop_maybe_unsubscribe(loop, context->queue);
furi_message_queue_free(context->queue);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)widget_alloc,
.free = (JsViewFree)widget_free,
.get_view = (JsViewGetView)widget_get_view,
.custom_make = (JsViewCustomMake)js_widget_custom_make,
.custom_destroy = (JsViewCustomDestroy)js_widget_custom_destroy,
.add_child = (JsViewAddChild)js_widget_add_child,
.reset_children = (JsViewResetChildren)js_widget_reset_children,
.prop_cnt = 0,
.props = {},
};
JS_GUI_VIEW_DEF(widget, &view_descriptor);

View File

@@ -75,6 +75,34 @@ export interface Pin {
* @version Added in JS SDK 0.1 * @version Added in JS SDK 0.1
*/ */
interrupt(): Contract; interrupt(): Contract;
/**
* Determines whether this pin supports PWM. If `false`, all other
* PWM-related methods on this pin will throw an error when called.
* @note On Flipper Zero only pins PA4 and PA7 support PWM
* @version Added in JS SDK 0.2, extra feature `"gpio-pwm"`
*/
isPwmSupported(): boolean;
/**
* Sets PWM parameters and starts the PWM. Configures the pin with
* `{ direction: "out", outMode: "push_pull" }`. Throws an error if PWM is
* not supported on this pin.
* @param freq Frequency in Hz
* @param duty Duty cycle in %
* @version Added in JS SDK 0.2, extra feature `"gpio-pwm"`
*/
pwmWrite(freq: number, duty: number): void;
/**
* Determines whether PWM is running. Throws an error if PWM is not
* supported on this pin.
* @version Added in JS SDK 0.2, extra feature `"gpio-pwm"`
*/
isPwmRunning(): boolean;
/**
* Stops PWM. Does not restore previous pin configuration. Throws an error
* if PWM is not supported on this pin.
* @version Added in JS SDK 0.2, extra feature `"gpio-pwm"`
*/
pwmStop(): void;
} }
/** /**

View File

@@ -33,9 +33,10 @@ type Props = {
length: number, length: number,
defaultData: Uint8Array | ArrayBuffer, defaultData: Uint8Array | ArrayBuffer,
} }
declare class ByteInput extends View<Props> { type Child = never;
declare class ByteInput extends View<Props, Child> {
input: Contract<string>; input: Contract<string>;
} }
declare class ByteInputFactory extends ViewFactory<Props, ByteInput> { } declare class ByteInputFactory extends ViewFactory<Props, Child, ByteInput> { }
declare const factory: ByteInputFactory; declare const factory: ByteInputFactory;
export = factory; export = factory;

View File

@@ -37,9 +37,10 @@ type Props = {
center: string, center: string,
right: string, right: string,
} }
declare class Dialog extends View<Props> { type Child = never;
declare class Dialog extends View<Props, Child> {
input: Contract<"left" | "center" | "right">; input: Contract<"left" | "center" | "right">;
} }
declare class DialogFactory extends ViewFactory<Props, Dialog> { } declare class DialogFactory extends ViewFactory<Props, Child, Dialog> { }
declare const factory: DialogFactory; declare const factory: DialogFactory;
export = factory; export = factory;

View File

@@ -26,7 +26,8 @@
import type { View, ViewFactory } from "."; import type { View, ViewFactory } from ".";
type Props = {}; type Props = {};
declare class EmptyScreen extends View<Props> { } type Child = never;
declare class EmptyScreenFactory extends ViewFactory<Props, EmptyScreen> { } declare class EmptyScreen extends View<Props, Child> { }
declare class EmptyScreenFactory extends ViewFactory<Props, Child, EmptyScreen> { }
declare const factory: EmptyScreenFactory; declare const factory: EmptyScreenFactory;
export = factory; export = factory;

View File

@@ -0,0 +1,11 @@
export type BuiltinIcon = "DolphinWait_59x54" | "js_script_10px";
export type IconData = symbol & { "__tag__": "icon" };
// introducing a nominal type in a hacky way; the `__tag__` property doesn't really exist.
/**
* Gets a built-in firmware icon for use in GUI
* @param icon Name of the icon
* @version Added in JS SDK 0.2, extra feature `"gui-widget"`
*/
export declare function getBuiltin(icon: BuiltinIcon): IconData;

View File

@@ -26,23 +26,23 @@
* assumes control over the entire viewport and all input events. Different * assumes control over the entire viewport and all input events. Different
* types of views are available (not all of which are unfortunately currently * types of views are available (not all of which are unfortunately currently
* implemented in JS): * implemented in JS):
* | View | Has JS adapter? | * | View | Has JS adapter? |
* |----------------------|------------------| * |----------------------|-----------------------|
* | `button_menu` | ❌ | * | `button_menu` | ❌ |
* | `button_panel` | ❌ | * | `button_panel` | ❌ |
* | `byte_input` | ✅ | * | `byte_input` | ✅ |
* | `dialog_ex` | ✅ (as `dialog`) | * | `dialog_ex` | ✅ (as `dialog`) |
* | `empty_screen` | ✅ | * | `empty_screen` | ✅ |
* | `file_browser` | | * | `file_browser` | ✅ (as `file_picker`) |
* | `loading` | ✅ | * | `loading` | ✅ |
* | `menu` | ❌ | * | `menu` | ❌ |
* | `number_input` | ❌ | * | `number_input` | ❌ |
* | `popup` | ❌ | * | `popup` | ❌ |
* | `submenu` | ✅ | * | `submenu` | ✅ |
* | `text_box` | ✅ | * | `text_box` | ✅ |
* | `text_input` | ✅ | * | `text_input` | ✅ |
* | `variable_item_list` | ❌ | * | `variable_item_list` | ❌ |
* | `widget` | | * | `widget` | |
* *
* In JS, each view has its own set of properties (or just "props"). The * In JS, each view has its own set of properties (or just "props"). The
* programmer can manipulate these properties in two ways: * programmer can manipulate these properties in two ways:
@@ -121,7 +121,7 @@ import type { Contract } from "../event_loop";
type Properties = { [K: string]: any }; type Properties = { [K: string]: any };
export declare class View<Props extends Properties> { export declare class View<Props extends Properties, Child> {
/** /**
* Assign value to property by name * Assign value to property by name
* @param property Name of the property * @param property Name of the property
@@ -129,9 +129,26 @@ export declare class View<Props extends Properties> {
* @version Added in JS SDK 0.1 * @version Added in JS SDK 0.1
*/ */
set<P extends keyof Props>(property: P, value: Props[P]): void; set<P extends keyof Props>(property: P, value: Props[P]): void;
/**
* Adds a child to the View
* @param child Child to add
* @version Added in JS SDK 0.2, extra feature `"gui-widget"`
*/
addChild<C extends Child>(child: C): void;
/**
* Removes all children from the View
* @version Added in JS SDK 0.2, extra feature `"gui-widget"`
*/
resetChildren(): void;
/**
* Removes all previous children from the View and assigns new children
* @param children The list of children to assign
* @version Added in JS SDK 0.2, extra feature `"gui-widget"`
*/
setChildren(children: Child[]): void;
} }
export declare class ViewFactory<Props extends Properties, V extends View<Props>> { export declare class ViewFactory<Props extends Properties, Child, V extends View<Props, Child>> {
/** /**
* Create view instance with default values, can be changed later with set() * Create view instance with default values, can be changed later with set()
* @version Added in JS SDK 0.1 * @version Added in JS SDK 0.1
@@ -140,9 +157,10 @@ export declare class ViewFactory<Props extends Properties, V extends View<Props>
/** /**
* Create view instance with custom values, can be changed later with set() * Create view instance with custom values, can be changed later with set()
* @param initial Dictionary of property names to values * @param initial Dictionary of property names to values
* @version Added in JS SDK 0.1 * @param children Optional list of children to add to the view
* @version Added in JS SDK 0.1; amended in JS SDK 0.2, extra feature `"gui-widget"`
*/ */
makeWith(initial: Partial<Props>): V; makeWith(initial: Partial<Props>, children?: Child[]): V;
} }
/** /**
@@ -163,7 +181,7 @@ declare class ViewDispatcher {
* View object currently shown * View object currently shown
* @version Added in JS SDK 0.1 * @version Added in JS SDK 0.1
*/ */
currentView: View<any>; currentView: View<any, any>;
/** /**
* Sends a number to the custom event handler * Sends a number to the custom event handler
* @param event number to send * @param event number to send
@@ -175,7 +193,7 @@ declare class ViewDispatcher {
* @param assoc View-ViewDispatcher association as returned by `add` * @param assoc View-ViewDispatcher association as returned by `add`
* @version Added in JS SDK 0.1 * @version Added in JS SDK 0.1
*/ */
switchTo(assoc: View<any>): void; switchTo(assoc: View<any, any>): void;
/** /**
* Sends this ViewDispatcher to the front or back, above or below all other * Sends this ViewDispatcher to the front or back, above or below all other
* GUI viewports * GUI viewports

View File

@@ -27,7 +27,8 @@
import type { View, ViewFactory } from "."; import type { View, ViewFactory } from ".";
type Props = {}; type Props = {};
declare class Loading extends View<Props> { } type Child = never;
declare class LoadingFactory extends ViewFactory<Props, Loading> { } declare class Loading extends View<Props, Child> { }
declare class LoadingFactory extends ViewFactory<Props, Child, Loading> { }
declare const factory: LoadingFactory; declare const factory: LoadingFactory;
export = factory; export = factory;

View File

@@ -31,9 +31,10 @@ type Props = {
header: string, header: string,
items: string[], items: string[],
}; };
declare class Submenu extends View<Props> { type Child = never;
declare class Submenu extends View<Props, Child> {
chosen: Contract<number>; chosen: Contract<number>;
} }
declare class SubmenuFactory extends ViewFactory<Props, Submenu> { } declare class SubmenuFactory extends ViewFactory<Props, Child, Submenu> { }
declare const factory: SubmenuFactory; declare const factory: SubmenuFactory;
export = factory; export = factory;

View File

@@ -33,9 +33,10 @@ type Props = {
font: "text" | "hex", font: "text" | "hex",
focus: "start" | "end", focus: "start" | "end",
} }
declare class TextBox extends View<Props> { type Child = never;
declare class TextBox extends View<Props, Child> {
chosen: Contract<number>; chosen: Contract<number>;
} }
declare class TextBoxFactory extends ViewFactory<Props, TextBox> { } declare class TextBoxFactory extends ViewFactory<Props, Child, TextBox> { }
declare const factory: TextBoxFactory; declare const factory: TextBoxFactory;
export = factory; export = factory;

View File

@@ -39,9 +39,10 @@ type Props = {
defaultTextClear: boolean, defaultTextClear: boolean,
illegalSymbols: boolean, illegalSymbols: boolean,
} }
declare class TextInput extends View<Props> { type Child = never;
declare class TextInput extends View<Props, Child> {
input: Contract<string>; input: Contract<string>;
} }
declare class TextInputFactory extends ViewFactory<Props, TextInput> { } declare class TextInputFactory extends ViewFactory<Props, Child, TextInput> { }
declare const factory: TextInputFactory; declare const factory: TextInputFactory;
export = factory; export = factory;

View File

@@ -0,0 +1,66 @@
/**
* Displays a combination of custom elements on one screen.
*
* <img src="../images/widget.png" width="200" alt="Sample screenshot of the view" />
*
* ```js
* let eventLoop = require("event_loop");
* let gui = require("gui");
* let emptyView = require("gui/widget");
* ```
*
* 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.
*
* # Children
* This view has the elements as its children.
*
* @version Added in JS SDK 0.2, extra feature `"gui-widget"`
* @module
*/
import type { View, ViewFactory } from ".";
import type { IconData } from "./icon";
import type { Contract } from "../event_loop";
type Position = { x: number, y: number };
type Size = { w: number, h: number };
type Alignment = { align: `${"t" | "c" | "b"}${"l" | "m" | "r"}` };
type Font = { font: "primary" | "secondary" | "keyboard" | "big_numbers" };
type Text = { text: string };
type StringMultilineElement = { element: "string_multiline" } & Position & Alignment & Font & Text;
type StringElement = { element: "string" } & Position & Alignment & Font & Text;
type TextBoxElement = { element: "text_box", stripToDots: boolean } & Position & Size & Alignment & Text;
type TextScrollElement = { element: "text_scroll" } & Position & Size & Text;
type ButtonElement = { element: "button", button: "left" | "center" | "right" } & Text;
type IconElement = { element: "icon", iconData: IconData } & Position;
type FrameElement = { element: "frame", radius: number } & Position & Size;
type Element = StringMultilineElement
| StringElement
| TextBoxElement
| TextScrollElement
| ButtonElement
| IconElement
| FrameElement;
type Props = {};
type Child = Element;
declare class Widget extends View<Props, Child> {
/**
* Event source for buttons. Only gets fired if there's a corresponding
* button element.
*/
button: Contract<"left" | "center" | "right">;
}
declare class WidgetFactory extends ViewFactory<Props, Child, Widget> { }
declare const factory: WidgetFactory;
export = factory;

View File

@@ -1,6 +1,6 @@
{ {
"name": "@next-flip/fz-sdk-mntm", "name": "@next-flip/fz-sdk-mntm",
"version": "0.1.4", "version": "0.2.0",
"description": "Type declarations and documentation for native JS modules available on Momentum Custom Firmware for Flipper Zero", "description": "Type declarations and documentation for native JS modules available on Momentum Custom Firmware for Flipper Zero",
"keywords": [ "keywords": [
"momentum", "momentum",

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 775 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 713 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 913 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

View File

@@ -0,0 +1,23 @@
Filetype: Flipper Animation
Version: 1
Width: 128
Height: 64
Passive frames: 26
Active frames: 26
Frames order: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 41 42 43 44 45 46 47 48 49
Active cycles: 1
Frame rate: 2
Duration: 360
Active cooldown: 7
Bubble slots: 1
Slot: 0
X: 69
Y: 47
Text: SHOWTIME!
AlignH: Left
AlignV: Center
StartFrame: 41
EndFrame: 44

View File

@@ -237,7 +237,7 @@ Min butthurt: 0
Max butthurt: 14 Max butthurt: 14
Min level: 16 Min level: 16
Max level: 30 Max level: 30
Weight: 4 Weight: 3
Name: L1_Sleigh_ride_128x64 Name: L1_Sleigh_ride_128x64
Min butthurt: 0 Min butthurt: 0
@@ -245,3 +245,10 @@ Max butthurt: 14
Min level: 9 Min level: 9
Max level: 30 Max level: 30
Weight: 4 Weight: 4
Name: L1_Showtime_128x64
Min butthurt: 0
Max butthurt: 10
Min level: 16
Max level: 30
Weight: 4

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -27,23 +27,23 @@ always access the canvas through a viewport.
In Flipper's terminology, a "View" is a fullscreen design element that assumes 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 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): are available (not all of which are unfortunately currently implemented in JS):
| View | Has JS adapter? | | View | Has JS adapter? |
|----------------------|------------------| |----------------------|-----------------------|
| `button_menu` | ❌ | | `button_menu` | ❌ |
| `button_panel` | ❌ | | `button_panel` | ❌ |
| `byte_input` | | | `byte_input` | |
| `dialog_ex` | ✅ (as `dialog`) | | `dialog_ex` | ✅ (as `dialog`) |
| `empty_screen` | ✅ | | `empty_screen` | ✅ |
| `file_browser` | | | `file_browser` | ✅ (as `file_picker`) |
| `loading` | ✅ | | `loading` | ✅ |
| `menu` | ❌ | | `menu` | ❌ |
| `number_input` | ❌ | | `number_input` | ❌ |
| `popup` | ❌ | | `popup` | ❌ |
| `submenu` | ✅ | | `submenu` | ✅ |
| `text_box` | ✅ | | `text_box` | ✅ |
| `text_input` | ✅ | | `text_input` | ✅ |
| `variable_item_list` | ❌ | | `variable_item_list` | ❌ |
| `widget` | | | `widget` | |
In JS, each view has its own set of properties (or just "props"). The programmer In JS, each view has its own set of properties (or just "props"). The programmer
can manipulate these properties in two ways: can manipulate these properties in two ways:

View File

@@ -0,0 +1,25 @@
# js_gui__widget {#js_gui__widget}
# Widget GUI view
Displays a combination of custom elements on one screen
<img src="widget.png" width="200" alt="Sample screenshot of the view" />
```js
let eventLoop = require("event_loop");
let gui = require("gui");
let widgetView = require("gui/widget");
```
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
This view does not have any props.
# Children
This view has the elements as its children.

View File

@@ -32,14 +32,6 @@ static void furi_event_loop_item_notify(FuriEventLoopItem* instance);
static bool furi_event_loop_item_is_waiting(FuriEventLoopItem* instance); static bool furi_event_loop_item_is_waiting(FuriEventLoopItem* instance);
static void furi_event_loop_process_pending_callbacks(FuriEventLoop* instance) {
for(; !PendingQueue_empty_p(instance->pending_queue);
PendingQueue_pop_back(NULL, instance->pending_queue)) {
const FuriEventLoopPendingQueueItem* item = PendingQueue_back(instance->pending_queue);
item->callback(item->context);
}
}
static bool furi_event_loop_signal_callback(uint32_t signal, void* arg, void* context) { static bool furi_event_loop_signal_callback(uint32_t signal, void* arg, void* context) {
furi_assert(context); furi_assert(context);
FuriEventLoop* instance = context; FuriEventLoop* instance = context;
@@ -130,12 +122,16 @@ static inline FuriEventLoopProcessStatus
furi_event_loop_unsubscribe(instance, item->object); furi_event_loop_unsubscribe(instance, item->object);
} }
instance->current_item = item;
if(item->event & FuriEventLoopEventFlagEdge) { if(item->event & FuriEventLoopEventFlagEdge) {
status = furi_event_loop_process_edge_event(item); status = furi_event_loop_process_edge_event(item);
} else { } else {
status = furi_event_loop_process_level_event(item); status = furi_event_loop_process_level_event(item);
} }
instance->current_item = NULL;
if(item->owner == NULL) { if(item->owner == NULL) {
status = FuriEventLoopProcessStatusFreeLater; status = FuriEventLoopProcessStatusFreeLater;
} }
@@ -193,6 +189,14 @@ static void furi_event_loop_process_waiting_list(FuriEventLoop* instance) {
furi_event_loop_sync_flags(instance); furi_event_loop_sync_flags(instance);
} }
static void furi_event_loop_process_pending_callbacks(FuriEventLoop* instance) {
for(; !PendingQueue_empty_p(instance->pending_queue);
PendingQueue_pop_back(NULL, instance->pending_queue)) {
const FuriEventLoopPendingQueueItem* item = PendingQueue_back(instance->pending_queue);
item->callback(item->context);
}
}
static void furi_event_loop_restore_flags(FuriEventLoop* instance, uint32_t flags) { static void furi_event_loop_restore_flags(FuriEventLoop* instance, uint32_t flags) {
if(flags) { if(flags) {
xTaskNotifyIndexed( xTaskNotifyIndexed(
@@ -203,7 +207,6 @@ static void furi_event_loop_restore_flags(FuriEventLoop* instance, uint32_t flag
void furi_event_loop_run(FuriEventLoop* instance) { void furi_event_loop_run(FuriEventLoop* instance) {
furi_check(instance); furi_check(instance);
furi_check(instance->thread_id == furi_thread_get_current_id()); furi_check(instance->thread_id == furi_thread_get_current_id());
FuriThread* thread = furi_thread_get_current(); FuriThread* thread = furi_thread_get_current();
// Set the default signal callback if none was previously set // Set the default signal callback if none was previously set
@@ -213,9 +216,9 @@ void furi_event_loop_run(FuriEventLoop* instance) {
furi_event_loop_init_tick(instance); furi_event_loop_init_tick(instance);
while(true) { instance->state = FuriEventLoopStateRunning;
instance->state = FuriEventLoopStateIdle;
while(true) {
const TickType_t ticks_to_sleep = const TickType_t ticks_to_sleep =
MIN(furi_event_loop_get_timer_wait_time(instance), MIN(furi_event_loop_get_timer_wait_time(instance),
furi_event_loop_get_tick_wait_time(instance)); furi_event_loop_get_tick_wait_time(instance));
@@ -224,8 +227,6 @@ void furi_event_loop_run(FuriEventLoop* instance) {
BaseType_t ret = xTaskNotifyWaitIndexed( BaseType_t ret = xTaskNotifyWaitIndexed(
FURI_EVENT_LOOP_FLAG_NOTIFY_INDEX, 0, FuriEventLoopFlagAll, &flags, ticks_to_sleep); FURI_EVENT_LOOP_FLAG_NOTIFY_INDEX, 0, FuriEventLoopFlagAll, &flags, ticks_to_sleep);
instance->state = FuriEventLoopStateProcessing;
if(ret == pdTRUE) { if(ret == pdTRUE) {
if(flags & FuriEventLoopFlagStop) { if(flags & FuriEventLoopFlagStop) {
instance->state = FuriEventLoopStateStopped; instance->state = FuriEventLoopStateStopped;
@@ -448,7 +449,7 @@ void furi_event_loop_unsubscribe(FuriEventLoop* instance, FuriEventLoopObject* o
WaitingList_unlink(item); WaitingList_unlink(item);
} }
if(instance->state == FuriEventLoopStateProcessing) { if(instance->current_item == item) {
furi_event_loop_item_free_later(item); furi_event_loop_item_free_later(item);
} else { } else {
furi_event_loop_item_free(item); furi_event_loop_item_free(item);

View File

@@ -64,8 +64,7 @@ typedef enum {
typedef enum { typedef enum {
FuriEventLoopStateStopped, FuriEventLoopStateStopped,
FuriEventLoopStateIdle, FuriEventLoopStateRunning,
FuriEventLoopStateProcessing,
} FuriEventLoopState; } FuriEventLoopState;
typedef struct { typedef struct {
@@ -81,6 +80,7 @@ struct FuriEventLoop {
// Poller state // Poller state
volatile FuriEventLoopState state; volatile FuriEventLoopState state;
volatile FuriEventLoopItem* current_item;
// Event handling // Event handling
FuriEventLoopTree_t tree; FuriEventLoopTree_t tree;

View File

@@ -80,6 +80,7 @@ bool furi_record_exists(const char* name) {
void furi_record_create(const char* name, void* data) { void furi_record_create(const char* name, void* data) {
furi_check(furi_record); furi_check(furi_record);
furi_check(name); furi_check(name);
furi_check(data);
furi_record_lock(); furi_record_lock();

View File

@@ -27,7 +27,7 @@ bool furi_record_exists(const char* name);
/** Create record /** Create record
* *
* @param name record name * @param name record name
* @param data data pointer * @param data data pointer (not NULL)
* @note Thread safe. Create and destroy must be executed from the same * @note Thread safe. Create and destroy must be executed from the same
* thread. * thread.
*/ */

View File

@@ -25,6 +25,8 @@
#define THREAD_MAX_STACK_SIZE (UINT16_MAX * sizeof(StackType_t)) #define THREAD_MAX_STACK_SIZE (UINT16_MAX * sizeof(StackType_t))
#define THREAD_STACK_WATERMARK_MIN (256u)
typedef struct { typedef struct {
FuriThreadStdoutWriteCallback write_callback; FuriThreadStdoutWriteCallback write_callback;
FuriString* buffer; FuriString* buffer;
@@ -117,6 +119,18 @@ static void furi_thread_body(void* context) {
furi_check(!thread->is_service, "Service threads MUST NOT return"); furi_check(!thread->is_service, "Service threads MUST NOT return");
size_t stack_watermark = furi_thread_get_stack_space(thread);
if(stack_watermark < THREAD_STACK_WATERMARK_MIN) {
#ifdef FURI_DEBUG
furi_crash("Stack watermark is dangerously low");
#endif
FURI_LOG_E( //-V779
thread->name ? thread->name : "Thread",
"Stack watermark is too low %zu < " STRINGIFY(
THREAD_STACK_WATERMARK_MIN) ". Increase stack size.",
stack_watermark);
}
if(thread->heap_trace_enabled == true) { if(thread->heap_trace_enabled == true) {
furi_delay_ms(33); furi_delay_ms(33);
thread->heap_size = memmgr_heap_get_thread_memory((FuriThreadId)thread); thread->heap_size = memmgr_heap_get_thread_memory((FuriThreadId)thread);

View File

@@ -510,9 +510,6 @@ static void lfrfid_worker_mode_emulate_process(LFRFIDWorker* worker) {
static void lfrfid_worker_mode_write_process(LFRFIDWorker* worker) { static void lfrfid_worker_mode_write_process(LFRFIDWorker* worker) {
LFRFIDProtocol protocol = worker->protocol; LFRFIDProtocol protocol = worker->protocol;
LFRFIDWriteRequest* request = malloc(sizeof(LFRFIDWriteRequest)); LFRFIDWriteRequest* request = malloc(sizeof(LFRFIDWriteRequest));
request->write_type = LFRFIDWriteTypeT5577;
bool can_be_written = protocol_dict_get_write_data(worker->protocols, protocol, request);
uint32_t write_start_time = furi_get_tick(); uint32_t write_start_time = furi_get_tick();
bool too_long = false; bool too_long = false;
@@ -521,63 +518,88 @@ static void lfrfid_worker_mode_write_process(LFRFIDWorker* worker) {
size_t data_size = protocol_dict_get_data_size(worker->protocols, protocol); size_t data_size = protocol_dict_get_data_size(worker->protocols, protocol);
uint8_t* verify_data = malloc(data_size); uint8_t* verify_data = malloc(data_size);
uint8_t* read_data = malloc(data_size); uint8_t* read_data = malloc(data_size);
protocol_dict_get_data(worker->protocols, protocol, verify_data, data_size); protocol_dict_get_data(worker->protocols, protocol, verify_data, data_size);
if(can_be_written) { while(!lfrfid_worker_check_for_stop(worker)) {
while(!lfrfid_worker_check_for_stop(worker)) { FURI_LOG_D(TAG, "Data write");
FURI_LOG_D(TAG, "Data write"); uint16_t skips = 0;
t5577_write(&request->t5577); for(size_t i = 0; i < LFRFIDWriteTypeMax; i++) {
memset(request, 0, sizeof(LFRFIDWriteRequest));
LFRFIDWriteType write_type = i;
request->write_type = write_type;
ProtocolId read_result = PROTOCOL_NO; protocol_dict_set_data(worker->protocols, protocol, verify_data, data_size);
LFRFIDWorkerReadState state = lfrfid_worker_read_internal(
worker,
protocol_dict_get_features(worker->protocols, protocol),
LFRFID_WORKER_WRITE_VERIFY_TIME_MS,
&read_result);
if(state == LFRFIDWorkerReadOK) { bool can_be_written =
bool read_success = false; protocol_dict_get_write_data(worker->protocols, protocol, request);
if(read_result == protocol) { if(!can_be_written) {
protocol_dict_get_data(worker->protocols, protocol, read_data, data_size); skips++;
if(skips == LFRFIDWriteTypeMax) {
if(memcmp(read_data, verify_data, data_size) == 0) {
read_success = true;
}
}
if(read_success) {
if(worker->write_cb) { if(worker->write_cb) {
worker->write_cb(LFRFIDWorkerWriteOK, worker->cb_ctx); worker->write_cb(LFRFIDWorkerWriteProtocolCannotBeWritten, worker->cb_ctx);
} }
break; break;
} else { }
unsuccessful_reads++; continue;
}
if(unsuccessful_reads == LFRFID_WORKER_WRITE_MAX_UNSUCCESSFUL_READS) { memset(read_data, 0, data_size);
if(worker->write_cb) {
worker->write_cb(LFRFIDWorkerWriteFobCannotBeWritten, worker->cb_ctx); if(request->write_type == LFRFIDWriteTypeT5577) {
} t5577_write(&request->t5577);
} else if(request->write_type == LFRFIDWriteTypeEM4305) {
em4305_write(&request->em4305);
} else {
furi_crash("Unknown write type");
}
}
ProtocolId read_result = PROTOCOL_NO;
LFRFIDWorkerReadState state = lfrfid_worker_read_internal(
worker,
protocol_dict_get_features(worker->protocols, protocol),
LFRFID_WORKER_WRITE_VERIFY_TIME_MS,
&read_result);
if(state == LFRFIDWorkerReadOK) {
bool read_success = false;
if(read_result == protocol) {
protocol_dict_get_data(worker->protocols, protocol, read_data, data_size);
if(memcmp(read_data, verify_data, data_size) == 0) {
read_success = true;
}
}
if(read_success) {
if(worker->write_cb) {
worker->write_cb(LFRFIDWorkerWriteOK, worker->cb_ctx);
}
break;
} else {
unsuccessful_reads++;
if(unsuccessful_reads == LFRFID_WORKER_WRITE_MAX_UNSUCCESSFUL_READS) {
if(worker->write_cb) {
worker->write_cb(LFRFIDWorkerWriteFobCannotBeWritten, worker->cb_ctx);
} }
} }
} else if(state == LFRFIDWorkerReadExit) {
break;
} }
} else if(state == LFRFIDWorkerReadExit) {
break;
}
if(!too_long && if(!too_long &&
(furi_get_tick() - write_start_time) > LFRFID_WORKER_WRITE_TOO_LONG_TIME_MS) { (furi_get_tick() - write_start_time) > LFRFID_WORKER_WRITE_TOO_LONG_TIME_MS) {
too_long = true; too_long = true;
if(worker->write_cb) { if(worker->write_cb) {
worker->write_cb(LFRFIDWorkerWriteTooLongToWrite, worker->cb_ctx); worker->write_cb(LFRFIDWorkerWriteTooLongToWrite, worker->cb_ctx);
}
} }
}
lfrfid_worker_delay(worker, LFRFID_WORKER_WRITE_DROP_TIME_MS); lfrfid_worker_delay(worker, LFRFID_WORKER_WRITE_DROP_TIME_MS);
}
} else {
if(worker->write_cb) {
worker->write_cb(LFRFIDWorkerWriteProtocolCannotBeWritten, worker->cb_ctx);
}
} }
free(request); free(request);

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <toolbox/protocols/protocol.h> #include <toolbox/protocols/protocol.h>
#include "../tools/t5577.h" #include "../tools/t5577.h"
#include "../tools/em4305.h"
typedef enum { typedef enum {
LFRFIDFeatureASK = 1 << 0, /** ASK Demodulation */ LFRFIDFeatureASK = 1 << 0, /** ASK Demodulation */
@@ -32,6 +33,7 @@ typedef enum {
LFRFIDProtocolSecurakey, LFRFIDProtocolSecurakey,
LFRFIDProtocolGProxII, LFRFIDProtocolGProxII,
LFRFIDProtocolInstaFob, LFRFIDProtocolInstaFob,
LFRFIDProtocolMax, LFRFIDProtocolMax,
} LFRFIDProtocol; } LFRFIDProtocol;
@@ -39,11 +41,15 @@ extern const ProtocolBase* lfrfid_protocols[];
typedef enum { typedef enum {
LFRFIDWriteTypeT5577, LFRFIDWriteTypeT5577,
LFRFIDWriteTypeEM4305,
LFRFIDWriteTypeMax,
} LFRFIDWriteType; } LFRFIDWriteType;
typedef struct { typedef struct {
LFRFIDWriteType write_type; LFRFIDWriteType write_type;
union { union {
LFRFIDT5577 t5577; LFRFIDT5577 t5577;
LFRFIDEM4305 em4305;
}; };
} LFRFIDWriteRequest; } LFRFIDWriteRequest;

View File

@@ -407,6 +407,24 @@ bool protocol_electra_write_data(ProtocolElectra* protocol, void* data) {
request->t5577.blocks_to_write = 5; request->t5577.blocks_to_write = 5;
result = true; result = true;
} }
if(request->write_type == LFRFIDWriteTypeEM4305) {
request->em4305.word[4] =
(EM4x05_MODULATION_MANCHESTER | EM4x05_SET_BITRATE(64) | (8 << EM4x05_MAXBLOCK_SHIFT));
uint64_t encoded_data_reversed = 0;
uint64_t encoded_epilogue_reversed = 0;
for(uint8_t i = 0; i < 64; i++) {
encoded_data_reversed = (encoded_data_reversed << 1) |
((protocol->encoded_base_data >> i) & 1);
encoded_epilogue_reversed = (encoded_epilogue_reversed << 1) |
((protocol->encoded_epilogue >> i) & 1);
}
request->em4305.word[5] = encoded_data_reversed & 0xFFFFFFFF;
request->em4305.word[6] = encoded_data_reversed >> 32;
request->em4305.word[7] = encoded_epilogue_reversed & 0xFFFFFFFF;
request->em4305.word[8] = encoded_epilogue_reversed >> 32;
request->em4305.mask = 0x01F0;
result = true;
}
return result; return result;
} }

View File

@@ -69,6 +69,19 @@ uint32_t protocol_em4100_get_t5577_bitrate(ProtocolEM4100* proto) {
} }
} }
uint32_t protocol_em4100_get_em4305_bitrate(ProtocolEM4100* proto) {
switch(proto->clock_per_bit) {
case 64:
return EM4x05_SET_BITRATE(64);
case 32:
return EM4x05_SET_BITRATE(32);
case 16:
return EM4x05_SET_BITRATE(16);
default:
return EM4x05_SET_BITRATE(64);
}
}
uint16_t protocol_em4100_get_short_time_low(ProtocolEM4100* proto) { uint16_t protocol_em4100_get_short_time_low(ProtocolEM4100* proto) {
return EM_READ_SHORT_TIME_BASE / protocol_em4100_get_time_divisor(proto) - return EM_READ_SHORT_TIME_BASE / protocol_em4100_get_time_divisor(proto) -
EM_READ_JITTER_TIME_BASE / protocol_em4100_get_time_divisor(proto); EM_READ_JITTER_TIME_BASE / protocol_em4100_get_time_divisor(proto);
@@ -339,6 +352,19 @@ bool protocol_em4100_write_data(ProtocolEM4100* protocol, void* data) {
request->t5577.block[2] = protocol->encoded_data; request->t5577.block[2] = protocol->encoded_data;
request->t5577.blocks_to_write = 3; request->t5577.blocks_to_write = 3;
result = true; result = true;
} else if(request->write_type == LFRFIDWriteTypeEM4305) {
request->em4305.word[4] =
(EM4x05_MODULATION_MANCHESTER | protocol_em4100_get_em4305_bitrate(protocol) |
(6 << EM4x05_MAXBLOCK_SHIFT));
uint64_t encoded_data_reversed = 0;
for(uint8_t i = 0; i < 64; i++) {
encoded_data_reversed = (encoded_data_reversed << 1) |
((protocol->encoded_data >> i) & 1);
}
request->em4305.word[5] = encoded_data_reversed;
request->em4305.word[6] = encoded_data_reversed >> 32;
request->em4305.mask = 0x70;
result = true;
} }
return result; return result;
} }

View File

@@ -264,6 +264,20 @@ bool protocol_gallagher_write_data(ProtocolGallagher* protocol, void* data) {
request->t5577.block[3] = bit_lib_get_bits_32(protocol->encoded_data, 64, 32); request->t5577.block[3] = bit_lib_get_bits_32(protocol->encoded_data, 64, 32);
request->t5577.blocks_to_write = 4; request->t5577.blocks_to_write = 4;
result = true; result = true;
} else if(request->write_type == LFRFIDWriteTypeEM4305) {
request->em4305.word[4] =
(EM4x05_MODULATION_MANCHESTER | EM4x05_SET_BITRATE(32) | (7 << EM4x05_MAXBLOCK_SHIFT));
uint32_t encoded_data_reversed[3] = {0};
for(uint8_t i = 0; i < (32 * 3); i++) {
encoded_data_reversed[i / 32] =
(encoded_data_reversed[i / 32] << 1) |
(bit_lib_get_bit(protocol->encoded_data, ((32 * 3) - i)) & 1);
}
request->em4305.word[5] = encoded_data_reversed[2];
request->em4305.word[6] = encoded_data_reversed[1];
request->em4305.word[7] = encoded_data_reversed[0];
request->em4305.mask = 0xF0;
result = true;
} }
return result; return result;
} }

View File

@@ -171,6 +171,19 @@ bool protocol_viking_write_data(ProtocolViking* protocol, void* data) {
request->t5577.block[2] = bit_lib_get_bits_32(protocol->encoded_data, 32, 32); request->t5577.block[2] = bit_lib_get_bits_32(protocol->encoded_data, 32, 32);
request->t5577.blocks_to_write = 3; request->t5577.blocks_to_write = 3;
result = true; result = true;
} else if(request->write_type == LFRFIDWriteTypeEM4305) {
request->em4305.word[4] =
(EM4x05_MODULATION_MANCHESTER | EM4x05_SET_BITRATE(32) | (6 << EM4x05_MAXBLOCK_SHIFT));
uint32_t encoded_data_reversed[2] = {0};
for(uint8_t i = 0; i < 64; i++) {
encoded_data_reversed[i / 32] =
(encoded_data_reversed[i / 32] << 1) |
(bit_lib_get_bit(protocol->encoded_data, (63 - i)) & 1);
}
request->em4305.word[5] = encoded_data_reversed[1];
request->em4305.word[6] = encoded_data_reversed[0];
request->em4305.mask = 0x70;
result = true;
} }
return result; return result;
} }

152
lib/lfrfid/tools/em4305.c Normal file
View File

@@ -0,0 +1,152 @@
#include "em4305.h"
#include <furi.h>
#include <furi_hal_rfid.h>
#define TAG "EM4305"
#define EM4305_TIMING_1 (32)
#define EM4305_TIMING_0_OFF (23)
#define EM4305_TIMING_0_ON (18)
#define EM4305_FIELD_STOP_OFF_CYCLES (55)
#define EM4305_FIELD_STOP_ON_CYCLES (18)
#define EM4305_TIMING_POWER_CHECK (1480)
#define EM4305_TIMING_EEPROM_WRITE (9340)
static bool em4305_line_parity(uint8_t data) {
uint8_t parity = 0;
for(uint8_t i = 0; i < 8; i++) {
parity ^= (data >> i) & 1;
}
return parity;
}
static uint64_t em4305_prepare_data(uint32_t data) {
uint8_t i, j;
uint64_t data_with_parity = 0;
// 4 lines of 8 bits of data
// line even parity at bits 8 17 26 35
// column even parity at bits 36-43
// bit 44 is always 0
// final table is 5 lines of 9 bits
// line parity
for(i = 0; i < 4; i++) {
for(j = 0; j < 8; j++) {
data_with_parity = (data_with_parity << 1) | ((data >> (i * 8 + j)) & 1);
}
data_with_parity = (data_with_parity << 1) | (uint64_t)em4305_line_parity(data >> (i * 8));
}
// column parity
for(i = 0; i < 8; i++) {
uint8_t column_parity = 0;
for(j = 0; j < 4; j++) {
column_parity ^= (data >> (j * 8 + i)) & 1;
}
data_with_parity = (data_with_parity << 1) | column_parity;
}
// bit 44
data_with_parity = (data_with_parity << 1) | 0;
return data_with_parity;
}
static void em4305_start(void) {
furi_hal_rfid_tim_read_start(125000, 0.5);
// do not ground the antenna
furi_hal_rfid_pin_pull_release();
}
static void em4305_stop(void) {
furi_hal_rfid_tim_read_stop();
furi_hal_rfid_pins_reset();
}
static void em4305_write_bit(bool value) {
if(value) {
furi_delay_us(EM4305_TIMING_1 * 8);
} else {
furi_hal_rfid_tim_read_pause();
furi_delay_us(EM4305_TIMING_0_OFF * 8);
furi_hal_rfid_tim_read_continue();
furi_delay_us(EM4305_TIMING_0_ON * 8);
}
}
static void em4305_write_opcode(uint8_t value) {
// 3 bit opcode
for(uint8_t i = 0; i < 3; i++) {
em4305_write_bit((value >> i) & 1);
}
// parity
bool parity = 0;
for(uint8_t i = 0; i < 3; i++) {
parity ^= (value >> i) & 1;
}
em4305_write_bit(parity);
}
static void em4305_field_stop() {
furi_hal_rfid_tim_read_pause();
furi_delay_us(EM4305_FIELD_STOP_OFF_CYCLES * 8);
furi_hal_rfid_tim_read_continue();
furi_delay_us(EM4305_FIELD_STOP_ON_CYCLES * 8);
}
static void em4305_write_word(uint8_t address, uint32_t data) {
// parity
uint64_t data_with_parity = em4305_prepare_data(data);
// power up the tag
furi_delay_us(8000);
// field stop
em4305_field_stop();
// start bit
em4305_write_bit(0);
// opcode
em4305_write_opcode(EM4x05_OPCODE_WRITE);
// address
bool address_parity = 0;
for(uint8_t i = 0; i < 4; i++) {
em4305_write_bit((address >> (i)) & 1);
address_parity ^= (address >> (i)) & 1;
}
em4305_write_bit(0);
em4305_write_bit(0);
em4305_write_bit(address_parity);
// data
for(uint8_t i = 0; i < 45; i++) {
em4305_write_bit((data_with_parity >> (44 - i)) & 1);
}
// wait for power check and eeprom write
furi_delay_us(EM4305_TIMING_POWER_CHECK);
furi_delay_us(EM4305_TIMING_EEPROM_WRITE);
}
void em4305_write(LFRFIDEM4305* data) {
furi_check(data);
em4305_start();
FURI_CRITICAL_ENTER();
for(uint8_t i = 0; i < EM4x05_WORD_COUNT; i++) {
if(data->mask & (1 << i)) {
em4305_write_word(i, data->word[i]);
}
}
FURI_CRITICAL_EXIT();
em4305_stop();
}

61
lib/lfrfid/tools/em4305.h Normal file
View File

@@ -0,0 +1,61 @@
#pragma once
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
// EM4305/4205 chip config definitions, thanks proxmark3!
#define EM4x05_GET_BITRATE(x) ((((x) & 0x3F) * 2) + 2)
// Note: only data rates 8, 16, 32, 40(*) and 64 are supported. (*) only with EM4305 330pF
#define EM4x05_SET_BITRATE(x) (((x) - 2) / 2)
#define EM4x05_MODULATION_NRZ (0x00000000)
#define EM4x05_MODULATION_MANCHESTER (0x00000040)
#define EM4x05_MODULATION_BIPHASE (0x00000080)
#define EM4x05_MODULATION_MILLER (0x000000C0) // not supported by all 4x05/4x69 chips
#define EM4x05_MODULATION_PSK1 (0x00000100) // not supported by all 4x05/4x69 chips
#define EM4x05_MODULATION_PSK2 (0x00000140) // not supported by all 4x05/4x69 chips
#define EM4x05_MODULATION_PSK3 (0x00000180) // not supported by all 4x05/4x69 chips
#define EM4x05_MODULATION_FSK1 (0x00000200) // not supported by all 4x05/4x69 chips
#define EM4x05_MODULATION_FSK2 (0x00000240) // not supported by all 4x05/4x69 chips
#define EM4x05_PSK_RF_2 (0)
#define EM4x05_PSK_RF_4 (0x00000400)
#define EM4x05_PSK_RF_8 (0x00000800)
#define EM4x05_MAXBLOCK_SHIFT (14)
#define EM4x05_FIRST_USER_BLOCK (5)
#define EM4x05_SET_NUM_BLOCKS(x) \
(((x) + 4) << 14) // number of blocks sent during default read mode
#define EM4x05_GET_NUM_BLOCKS(x) ((((x) >> 14) & 0xF) - 4)
#define EM4x05_READ_LOGIN_REQ (1 << 18)
#define EM4x05_READ_HK_LOGIN_REQ (1 << 19)
#define EM4x05_WRITE_LOGIN_REQ (1 << 20)
#define EM4x05_WRITE_HK_LOGIN_REQ (1 << 21)
#define EM4x05_READ_AFTER_WRITE (1 << 22)
#define EM4x05_DISABLE_ALLOWED (1 << 23)
#define EM4x05_READER_TALK_FIRST (1 << 24)
#define EM4x05_INVERT (1 << 25)
#define EM4x05_PIGEON (1 << 26)
#define EM4x05_WORD_COUNT (16)
#define EM4x05_OPCODE_LOGIN (0b001)
#define EM4x05_OPCODE_WRITE (0b010)
#define EM4x05_OPCODE_READ (0b100)
#define EM4x05_OPCODE_PROTECT (0b110)
#define EM4x05_OPCODE_DISABLE (0b101)
typedef struct {
uint32_t word[EM4x05_WORD_COUNT]; /**< Word data to write */
uint16_t mask; /**< Word mask */
} LFRFIDEM4305;
/** Write EM4305 tag data to tag
*
* @param data The data to write (mask is taken from that data)
*/
void em4305_write(LFRFIDEM4305* data);
#ifdef __cplusplus
}
#endif

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