From f5ca3ed2a16894f6b40938d11ff04724361c2643 Mon Sep 17 00:00:00 2001 From: Victor Nikitchuk Date: Thu, 4 Apr 2024 22:36:44 +0300 Subject: [PATCH 1/4] Status output !TX/RX on the GDO2 CC1101 pin --- .../drivers/subghz/cc1101_ext/cc1101_ext.c | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/applications/drivers/subghz/cc1101_ext/cc1101_ext.c b/applications/drivers/subghz/cc1101_ext/cc1101_ext.c index d27d114169..f109b3cf96 100644 --- a/applications/drivers/subghz/cc1101_ext/cc1101_ext.c +++ b/applications/drivers/subghz/cc1101_ext/cc1101_ext.c @@ -181,7 +181,15 @@ static bool subghz_device_cc1101_ext_check_init(void) { } furi_hal_gpio_init( subghz_device_cc1101_ext->g0_pin, GpioModeAnalog, GpioPullNo, GpioSpeedLow); - + + // Reset GDO2 (!TX/RX) to floating state + cc1101_status = cc1101_write_reg( + subghz_device_cc1101_ext->spi_bus_handle, CC1101_IOCFG2, CC1101IocfgHighImpedance); + if(cc1101_status.CHIP_RDYn != 0) { + //timeout or error + break; + } + // Go to sleep cc1101_status = cc1101_shutdown(subghz_device_cc1101_ext->spi_bus_handle); if(cc1101_status.CHIP_RDYn != 0) { @@ -410,6 +418,9 @@ void subghz_device_cc1101_ext_reset(void) { // Warning: push pull cc1101 clock output on GD0 cc1101_write_reg( subghz_device_cc1101_ext->spi_bus_handle, CC1101_IOCFG0, CC1101IocfgHighImpedance); + // Reset GDO2 (!TX/RX) to floating state + cc1101_write_reg( + subghz_device_cc1101_ext->spi_bus_handle, CC1101_IOCFG2, CC1101IocfgHighImpedance); furi_hal_spi_release(subghz_device_cc1101_ext->spi_bus_handle); } @@ -419,6 +430,9 @@ void subghz_device_cc1101_ext_idle(void) { //waiting for the chip to switch to IDLE mode furi_check(cc1101_wait_status_state( subghz_device_cc1101_ext->spi_bus_handle, CC1101StateIDLE, 10000)); + // Reset GDO2 (!TX/RX) to floating state + cc1101_write_reg( + subghz_device_cc1101_ext->spi_bus_handle, CC1101_IOCFG2, CC1101IocfgHighImpedance); furi_hal_spi_release(subghz_device_cc1101_ext->spi_bus_handle); if(subghz_device_cc1101_ext->power_amp) { furi_hal_gpio_write(SUBGHZ_DEVICE_CC1101_EXT_E07_AMP_GPIO, 0); @@ -431,6 +445,9 @@ void subghz_device_cc1101_ext_rx(void) { //waiting for the chip to switch to Rx mode furi_check( cc1101_wait_status_state(subghz_device_cc1101_ext->spi_bus_handle, CC1101StateRX, 10000)); + // Go GDO2 (!TX/RX) to high (RX state) + cc1101_write_reg( + subghz_device_cc1101_ext->spi_bus_handle, CC1101_IOCFG2, CC1101IocfgHW | CC1101_IOCFG_INV); furi_hal_spi_release(subghz_device_cc1101_ext->spi_bus_handle); if(subghz_device_cc1101_ext->power_amp) { furi_hal_gpio_write(SUBGHZ_DEVICE_CC1101_EXT_E07_AMP_GPIO, 0); @@ -444,6 +461,8 @@ bool subghz_device_cc1101_ext_tx(void) { //waiting for the chip to switch to Tx mode furi_check( cc1101_wait_status_state(subghz_device_cc1101_ext->spi_bus_handle, CC1101StateTX, 10000)); + // Go GDO2 (!TX/RX) to low (TX state) + cc1101_write_reg(subghz_device_cc1101_ext->spi_bus_handle, CC1101_IOCFG2, CC1101IocfgHW); furi_hal_spi_release(subghz_device_cc1101_ext->spi_bus_handle); if(subghz_device_cc1101_ext->power_amp) { furi_hal_gpio_write(SUBGHZ_DEVICE_CC1101_EXT_E07_AMP_GPIO, 1); From 45e7913435d683161f4c983c4ef26b19b6bbc77c Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 4 Apr 2024 22:55:39 +0300 Subject: [PATCH 2/4] after merge fixes add void --- applications/main/subghz_remote | 2 +- lib/nfc/protocols/emv/emv.c | 2 +- lib/nfc/protocols/emv/emv.h | 2 +- lib/subghz/blocks/custom_btn.c | 10 +++++----- lib/subghz/blocks/custom_btn.h | 8 ++++---- lib/subghz/blocks/custom_btn_i.h | 2 +- lib/subghz/protocols/alutech_at_4n.c | 4 ++-- lib/subghz/protocols/came_atomo.c | 4 ++-- lib/subghz/protocols/faac_slh.c | 2 +- lib/subghz/protocols/faac_slh.h | 2 +- lib/subghz/protocols/nice_flor_s.c | 4 ++-- lib/subghz/protocols/secplus_v2.c | 4 ++-- lib/subghz/protocols/somfy_telis.c | 4 ++-- targets/f7/furi_hal/furi_hal_subghz.c | 2 +- targets/f7/furi_hal/furi_hal_subghz.h | 2 +- 15 files changed, 27 insertions(+), 27 deletions(-) diff --git a/applications/main/subghz_remote b/applications/main/subghz_remote index 21b2c18d92..73ca3f2ac0 160000 --- a/applications/main/subghz_remote +++ b/applications/main/subghz_remote @@ -1 +1 @@ -Subproject commit 21b2c18d92d3d87e46f6381dce3de3c2818de151 +Subproject commit 73ca3f2ac02e313004aa3a724f2f8a0e4646ad10 diff --git a/lib/nfc/protocols/emv/emv.c b/lib/nfc/protocols/emv/emv.c index af208ea106..cfdad3c6e3 100644 --- a/lib/nfc/protocols/emv/emv.c +++ b/lib/nfc/protocols/emv/emv.c @@ -25,7 +25,7 @@ const NfcDeviceBase nfc_device_emv = { .get_base_data = (NfcDeviceGetBaseData)emv_get_base_data, }; -EmvData* emv_alloc() { +EmvData* emv_alloc(void) { EmvData* data = malloc(sizeof(EmvData)); data->iso14443_4a_data = iso14443_4a_alloc(); data->emv_application.pin_try_counter = 0xff; diff --git a/lib/nfc/protocols/emv/emv.h b/lib/nfc/protocols/emv/emv.h index e09eacaa47..638e93ddc2 100644 --- a/lib/nfc/protocols/emv/emv.h +++ b/lib/nfc/protocols/emv/emv.h @@ -119,7 +119,7 @@ extern const NfcDeviceBase nfc_device_emv; // Virtual methods -EmvData* emv_alloc(); +EmvData* emv_alloc(void); void emv_free(EmvData* data); diff --git a/lib/subghz/blocks/custom_btn.c b/lib/subghz/blocks/custom_btn.c index e33f798878..6a185c37bf 100644 --- a/lib/subghz/blocks/custom_btn.c +++ b/lib/subghz/blocks/custom_btn.c @@ -15,7 +15,7 @@ bool subghz_custom_btn_set(uint8_t btn_id) { } } -uint8_t subghz_custom_btn_get() { +uint8_t subghz_custom_btn_get(void) { return custom_btn_id; } @@ -23,7 +23,7 @@ void subghz_custom_btn_set_original(uint8_t btn_code) { custom_btn_original = btn_code; } -uint8_t subghz_custom_btn_get_original() { +uint8_t subghz_custom_btn_get_original(void) { return custom_btn_original; } @@ -31,14 +31,14 @@ void subghz_custom_btn_set_max(uint8_t b) { custom_btn_max_btns = b; } -void subghz_custom_btns_reset() { +void subghz_custom_btns_reset(void) { custom_btn_original = 0; custom_btn_max_btns = 0; controller_programming_mode = PROG_MODE_OFF; custom_btn_id = SUBGHZ_CUSTOM_BTN_OK; } -bool subghz_custom_btn_is_allowed() { +bool subghz_custom_btn_is_allowed(void) { return custom_btn_max_btns != 0; } @@ -46,6 +46,6 @@ void subghz_custom_btn_set_prog_mode(ProgMode prog_mode) { controller_programming_mode = prog_mode; } -ProgMode subghz_custom_btn_get_prog_mode() { +ProgMode subghz_custom_btn_get_prog_mode(void) { return controller_programming_mode; } diff --git a/lib/subghz/blocks/custom_btn.h b/lib/subghz/blocks/custom_btn.h index 45cd839d4e..8473fa8c27 100644 --- a/lib/subghz/blocks/custom_btn.h +++ b/lib/subghz/blocks/custom_btn.h @@ -17,13 +17,13 @@ extern "C" { bool subghz_custom_btn_set(uint8_t btn_id); -uint8_t subghz_custom_btn_get(); +uint8_t subghz_custom_btn_get(void); -uint8_t subghz_custom_btn_get_original(); +uint8_t subghz_custom_btn_get_original(void); -void subghz_custom_btns_reset(); +void subghz_custom_btns_reset(void); -bool subghz_custom_btn_is_allowed(); +bool subghz_custom_btn_is_allowed(void); #ifdef __cplusplus } diff --git a/lib/subghz/blocks/custom_btn_i.h b/lib/subghz/blocks/custom_btn_i.h index aafcc82da8..b800d7079b 100644 --- a/lib/subghz/blocks/custom_btn_i.h +++ b/lib/subghz/blocks/custom_btn_i.h @@ -15,4 +15,4 @@ void subghz_custom_btn_set_max(uint8_t b); void subghz_custom_btn_set_prog_mode(ProgMode prog_mode); -ProgMode subghz_custom_btn_get_prog_mode(); +ProgMode subghz_custom_btn_get_prog_mode(void); diff --git a/lib/subghz/protocols/alutech_at_4n.c b/lib/subghz/protocols/alutech_at_4n.c index 2a16cae850..d0c410e1ce 100644 --- a/lib/subghz/protocols/alutech_at_4n.c +++ b/lib/subghz/protocols/alutech_at_4n.c @@ -323,7 +323,7 @@ bool subghz_protocol_alutech_at_4n_create_data( * Basic set | 0x11 | 0x22 | 0xFF | 0x44 | 0x33 | * @return Button code */ -static uint8_t subghz_protocol_alutech_at_4n_get_btn_code(); +static uint8_t subghz_protocol_alutech_at_4n_get_btn_code(void); /** * Generating an upload from data. @@ -701,7 +701,7 @@ SubGhzProtocolStatus subghz_protocol_decoder_alutech_at_4n_deserialize( return ret; } -static uint8_t subghz_protocol_alutech_at_4n_get_btn_code() { +static uint8_t subghz_protocol_alutech_at_4n_get_btn_code(void) { uint8_t custom_btn_id = subghz_custom_btn_get(); uint8_t original_btn_code = subghz_custom_btn_get_original(); uint8_t btn = original_btn_code; diff --git a/lib/subghz/protocols/came_atomo.c b/lib/subghz/protocols/came_atomo.c index 87927e3628..d21440490c 100644 --- a/lib/subghz/protocols/came_atomo.c +++ b/lib/subghz/protocols/came_atomo.c @@ -78,7 +78,7 @@ static void subghz_protocol_came_atomo_remote_controller(SubGhzBlockGeneric* ins * Basic set | 0x0 | 0x2 | 0x4 | 0x6 | * @return Button code */ -static uint8_t subghz_protocol_came_atomo_get_btn_code(); +static uint8_t subghz_protocol_came_atomo_get_btn_code(void); void* subghz_protocol_encoder_came_atomo_alloc(SubGhzEnvironment* environment) { UNUSED(environment); @@ -614,7 +614,7 @@ void atomo_decrypt(uint8_t* buff) { } } -static uint8_t subghz_protocol_came_atomo_get_btn_code() { +static uint8_t subghz_protocol_came_atomo_get_btn_code(void) { uint8_t custom_btn_id = subghz_custom_btn_get(); uint8_t original_btn_code = subghz_custom_btn_get_original(); uint8_t btn = original_btn_code; diff --git a/lib/subghz/protocols/faac_slh.c b/lib/subghz/protocols/faac_slh.c index 2044d9d207..b095977e19 100644 --- a/lib/subghz/protocols/faac_slh.c +++ b/lib/subghz/protocols/faac_slh.c @@ -24,7 +24,7 @@ static uint32_t temp_counter_backup = 0; static bool faac_prog_mode = false; static bool allow_zero_seed = false; -void faac_slh_reset_prog_mode() { +void faac_slh_reset_prog_mode(void) { temp_fix_backup = 0; temp_counter_backup = 0; faac_prog_mode = false; diff --git a/lib/subghz/protocols/faac_slh.h b/lib/subghz/protocols/faac_slh.h index 16b6f031e6..66cbb70fa5 100644 --- a/lib/subghz/protocols/faac_slh.h +++ b/lib/subghz/protocols/faac_slh.h @@ -110,4 +110,4 @@ void subghz_protocol_decoder_faac_slh_get_string(void* context, FuriString* outp // Reset prog mode vars // TODO: Remake in proper way -void faac_slh_reset_prog_mode(); \ No newline at end of file +void faac_slh_reset_prog_mode(void); \ No newline at end of file diff --git a/lib/subghz/protocols/nice_flor_s.c b/lib/subghz/protocols/nice_flor_s.c index 3f56ba1078..2e0435bdbb 100644 --- a/lib/subghz/protocols/nice_flor_s.c +++ b/lib/subghz/protocols/nice_flor_s.c @@ -122,7 +122,7 @@ static void subghz_protocol_nice_one_get_data(uint8_t* p, uint8_t num_parcel, ui * Basic set | 0x1 | 0x2 | 0x4 | 0x8 | * @return Button code */ -static uint8_t subghz_protocol_nice_flor_s_get_btn_code(); +static uint8_t subghz_protocol_nice_flor_s_get_btn_code(void); /** * Generating an upload from data. @@ -751,7 +751,7 @@ SubGhzProtocolStatus return ret; } -static uint8_t subghz_protocol_nice_flor_s_get_btn_code() { +static uint8_t subghz_protocol_nice_flor_s_get_btn_code(void) { uint8_t custom_btn_id = subghz_custom_btn_get(); uint8_t original_btn_code = subghz_custom_btn_get_original(); uint8_t btn = original_btn_code; diff --git a/lib/subghz/protocols/secplus_v2.c b/lib/subghz/protocols/secplus_v2.c index e00cfba0e0..b58fe3df46 100644 --- a/lib/subghz/protocols/secplus_v2.c +++ b/lib/subghz/protocols/secplus_v2.c @@ -380,7 +380,7 @@ static uint64_t subghz_protocol_secplus_v2_encode_half(uint8_t roll_array[], uin * Basic set | 0x68 | 0x80 | 0x81 | 0xE2 | 0x78 * @return Button code */ -static uint8_t subghz_protocol_secplus_v2_get_btn_code(); +static uint8_t subghz_protocol_secplus_v2_get_btn_code(void); /** * Security+ 2.0 message encoding @@ -834,7 +834,7 @@ SubGhzProtocolStatus return ret; } -static uint8_t subghz_protocol_secplus_v2_get_btn_code() { +static uint8_t subghz_protocol_secplus_v2_get_btn_code(void) { uint8_t custom_btn_id = subghz_custom_btn_get(); uint8_t original_btn_code = subghz_custom_btn_get_original(); uint8_t btn = original_btn_code; diff --git a/lib/subghz/protocols/somfy_telis.c b/lib/subghz/protocols/somfy_telis.c index b198ce4913..a1308dd6d4 100644 --- a/lib/subghz/protocols/somfy_telis.c +++ b/lib/subghz/protocols/somfy_telis.c @@ -102,7 +102,7 @@ void subghz_protocol_encoder_somfy_telis_free(void* context) { * Basic set | 0x1 | 0x2 | 0x4 | 0x8 | * @return Button code */ -static uint8_t subghz_protocol_somfy_telis_get_btn_code(); +static uint8_t subghz_protocol_somfy_telis_get_btn_code(void); static bool subghz_protocol_somfy_telis_gen_data( SubGhzProtocolEncoderSomfyTelis* instance, @@ -668,7 +668,7 @@ SubGhzProtocolStatus subghz_protocol_somfy_telis_const.min_count_bit_for_found); } -static uint8_t subghz_protocol_somfy_telis_get_btn_code() { +static uint8_t subghz_protocol_somfy_telis_get_btn_code(void) { uint8_t custom_btn_id = subghz_custom_btn_get(); uint8_t original_btn_code = subghz_custom_btn_get_original(); uint8_t btn = original_btn_code; diff --git a/targets/f7/furi_hal/furi_hal_subghz.c b/targets/f7/furi_hal/furi_hal_subghz.c index ec85ff2c3c..29418bfc39 100644 --- a/targets/f7/furi_hal/furi_hal_subghz.c +++ b/targets/f7/furi_hal/furi_hal_subghz.c @@ -81,7 +81,7 @@ void furi_hal_subghz_set_ext_power_amp(bool enabled) { furi_hal_subghz.ext_power_amp = enabled; } -bool furi_hal_subghz_get_ext_power_amp() { +bool furi_hal_subghz_get_ext_power_amp(void) { return furi_hal_subghz.ext_power_amp; } diff --git a/targets/f7/furi_hal/furi_hal_subghz.h b/targets/f7/furi_hal/furi_hal_subghz.h index 864368120e..68c040928a 100644 --- a/targets/f7/furi_hal/furi_hal_subghz.h +++ b/targets/f7/furi_hal/furi_hal_subghz.h @@ -285,7 +285,7 @@ void furi_hal_subghz_stop_async_tx(void); // External CC1101 Ebytes power amplifier control void furi_hal_subghz_set_ext_power_amp(bool enabled); -bool furi_hal_subghz_get_ext_power_amp(); +bool furi_hal_subghz_get_ext_power_amp(void); #ifdef __cplusplus } From ec4b8b8f5e290fb957d3b4ef68a3462d83050598 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 5 Apr 2024 01:25:03 +0300 Subject: [PATCH 3/4] Various fixes fixes by Willy-JL nfc parser by zacharyweiss js widget and path globals by jamisonderek --- applications/main/nfc/application.fam | 9 + .../nfc/plugins/supported_cards/charliecard.c | 1059 +++++++++++++++++ applications/services/desktop/desktop.c | 7 +- applications/services/desktop/desktop_i.h | 3 +- applications/system/js_app/application.fam | 8 + .../js_app/examples/apps/Scripts/path.js | 9 + .../examples/apps/Scripts/widget-js.fxbm | Bin 0 -> 32 bytes .../js_app/examples/apps/Scripts/widget.js | 59 + applications/system/js_app/js_thread.c | 19 + applications/system/js_app/modules/js_math.c | 2 +- .../system/js_app/modules/js_widget.c | 956 +++++++++++++++ lib/infrared/worker/infrared_worker.c | 2 +- targets/f7/furi_hal/furi_hal_flash.c | 6 +- targets/furi_hal_include/furi_hal_random.h | 6 +- 14 files changed, 2137 insertions(+), 8 deletions(-) create mode 100644 applications/main/nfc/plugins/supported_cards/charliecard.c create mode 100644 applications/system/js_app/examples/apps/Scripts/path.js create mode 100644 applications/system/js_app/examples/apps/Scripts/widget-js.fxbm create mode 100644 applications/system/js_app/examples/apps/Scripts/widget.js create mode 100644 applications/system/js_app/modules/js_widget.c diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index f036feaeee..2b4b906d6d 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -128,6 +128,15 @@ App( sources=["plugins/supported_cards/metromoney.c"], ) +App( + appid="charliecard_parser", + apptype=FlipperAppType.PLUGIN, + entry_point="charliecard_plugin_ep", + targets=["f7"], + requires=["nfc"], + sources=["plugins/supported_cards/charliecard.c"], +) + App( appid="kazan_parser", apptype=FlipperAppType.PLUGIN, diff --git a/applications/main/nfc/plugins/supported_cards/charliecard.c b/applications/main/nfc/plugins/supported_cards/charliecard.c new file mode 100644 index 0000000000..7a405fffb2 --- /dev/null +++ b/applications/main/nfc/plugins/supported_cards/charliecard.c @@ -0,0 +1,1059 @@ +/* + * Parser for MBTA CharlieCard (Boston, MA, USA). + * + * Copyright 2024 Zachary Weiss + * + * Public security research on the MBTA's fare system stretches back to 2008, + * starting with Russel Ryan, Zack Anderson, and Alessandro Chiesa's + * "Anatomy of a Subway Hack", for which they were famously issued a gag order. + * A thorough history of research & researchers deserving of credit is + * detailed by @bobbyrsec in his 2022 blog post (& presentation): + * "Operation Charlie: Hacking the MBTA CharlieCard from 2008 to Present" + * https://medium.com/@bobbyrsec/operation-charlie-hacking-the-mbta-charliecard-from-2008-to-present-24ea9f0aaa38 + * + * Fare gate IDs, card types, and general assistance courtesy of the + * minds behind DEFCON 31's "Boston Infinite Money Glitch" presentation: + * — Matthew Harris; mattyharris.net + * — Zachary Bertocchi; zackbertocchi.com + * — Scott Campbell; josephscottcampbell.com + * — Noah Gibson; + * Talk available at: https://www.youtube.com/watch?v=1JT_lTfK69Q + * + * TODOs: + * — Reverse engineer passes (sectors 4 & 5?), impl. + * — Infer transaction flag meanings + * — Infer remaining unknown bytes in the balance sectors (2 & 3) + * – ASCII art &/or unified read function for the balance sectors, + * to improve readability / interpretability by others? + * — Improve string output formatting, esp. of transaction log + * — Continually gather data on fare gate ID mappings, update as collected; + * check locations this might be scrapable / inferrable from: + * [X] MBTA GTFS spec (https://www.mbta.com/developers/gtfs) features & IDs + * seem too-coarse-grained & uncorrelated + * [X] MBTA ArcGIS (https://mbta-massdot.opendata.arcgis.com/) & Tableau (https://public.tableau.com/app/profile/mbta.office.of.performance.management.and.innovation/vizzes) + * files don't seem to have anything of that resolution (only down to ridership by station) + * [X] (skim of) MBTA public GitHub (https://github.com/mbta) repos make no reference to fare-gate-level data + * [X] (skim of) MBTA public engineering docs (https://www.mbta.com/engineering) unfruitful; + * Closest mention spotted is 2014 "Ridership and Service Statistics" (https://cdn.mbta.com/sites/default/files/fmcb-meeting-docs/reports-policies/2014-07-mbta-bluebook-ed14.pdf) + * where on pg.40, "Equipment at Stations" is enumerated, and fare gates counts are given, + * listed as "AFC Gates" (presumably standing for "Automated Fare Control") + * [X] Josiah Zachery criminal trial public evidence — convicted partially on + * data on his CharlieCard, appeals partially on basis of legality of this search. + * Prev. court case (gag order mentioned in preamble) leaked some data in the files + * entered into evidence. Seemingly did not happen here; fare gate IDs unmentioned, + * only ever the nature of stored/saved data and methods of retrieval. + * Appelate case dockets 2019-P-0401, SJC-12952, SJ-2017-0390 (https://www.ma-appellatecourts.org/party) + * Trial court case 04/02/2015 #1584CR10265 @Suffolk County Criminal Superior Court (https://www.masscourts.org/eservices/home.page.16) + * [ ] FOIA / public records request? (https://massachusettsdot.mycusthelp.com/WEBAPP/_rs/(S(tbcygdlm0oojy35p1wv0y2y5))/supporthome.aspx) + * [ ] MBTA data blog? (https://www.massdottracker.com/datablog/) + * [ ] Other? + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nfc_supported_card_plugin.h" +#include + +#include + +#include +#include +#include +#include + +#define TAG "CharlieCard" + +// starts Wednesday 2003/1/1 @ midnight +#define CHARLIE_EPOCH \ + (DateTime) { \ + 0, 0, 0, 1, 1, 2003, 4 \ + } +// timestep is one minute +#define CHARLIE_TIME_DELTA_SECS 60 +#define CHARLIE_END_VALID_DELTA_SECS 60 * 8 +#define CHARLIE_N_TRIP_HISTORY 10 + +enum CharlieActiveSector { + CHARLIE_ACTIVE_SECTOR_2, + CHARLIE_ACTIVE_SECTOR_3, +}; + +typedef struct { + uint64_t a; + uint64_t b; +} MfClassicKeyPair; + +// always from the same set of keys (cf. default keys dict for list w/o multiplicity) +// we only care about the data in the first half of the sectors +// second half sectors keys seemingly change position sometimes across cards? +// no data stored there, but might want to impl some custom read function +// accounting for this such that reading is faster (else it seems to fall back on dict +// approach for remaining keys)... +static const MfClassicKeyPair charliecard_1k_keys[] = { + {.a = 0x3060206F5B0A, .b = 0xF1B9F5669CC8}, + {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89}, + {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89}, + {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89}, + {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89}, + {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89}, + {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89}, + {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89}, + {.a = 0x3A09594C8587, .b = 0x62387B8D250D}, + {.a = 0xF238D78FF48F, .b = 0x9DC282D46217}, + {.a = 0xAFD0BA94D624, .b = 0x92EE4DC87191}, + {.a = 0xB35A0E4ACC09, .b = 0x756EF55E2507}, + {.a = 0x447AB7FD5A6B, .b = 0x932B9CB730EF}, + {.a = 0x1F1A0A111B5B, .b = 0xAD9E0A1CA2F7}, + {.a = 0xD58023BA2BDC, .b = 0x62CED42A6D87}, + {.a = 0x2548A443DF28, .b = 0x2ED3B15E7C0F}, +}; + +typedef struct { + uint16_t dollars; + uint8_t cents; +} Money; + +#define FARE_BUS \ + (Money) { \ + 1, 70 \ + } +#define FARE_SUB \ + (Money) { \ + 2, 40 \ + } + +typedef struct { + DateTime date; + uint16_t gate; + uint8_t g_flag; + Money fare; + uint8_t f_flag; +} Trip; + +// IdMapping approach borrowed from Jeremy Cooper's 'clipper.c' +typedef struct { + uint16_t id; + const char* name; +} IdMapping; + +// this should be a complete accounting of types, +static const IdMapping charliecard_types[] = { + // Regular card types + {.id = 367, .name = "Adult"}, + {.id = 366, .name = "SV Adult"}, + {.id = 418, .name = "Student"}, + {.id = 419, .name = "Senior"}, + {.id = 420, .name = "Tap"}, + {.id = 417, .name = "Blind"}, + {.id = 426, .name = "Child"}, + {.id = 410, .name = "Employee ID Without Passback"}, + {.id = 414, .name = "Employee ID With Passback"}, + {.id = 415, .name = "Retiree"}, + {.id = 416, .name = "Police/Fire"}, + + // Passes + {.id = 135, .name = "30 Day Local Bus Pass"}, + {.id = 136, .name = "30 Day Inner Express Bus Pass"}, // + {.id = 137, .name = "30 Day Outer Express Bus Pass"}, // + {.id = 138, .name = "30 Day LinkPass"}, // + {.id = 139, .name = "30 Day Senior LinkPass"}, // + {.id = 148, .name = "30 Day TAP LinkPass"}, + {.id = 150, .name = "Monthly Student LinkPass"}, + {.id = 424, .name = "Monthly TAP LinkPass"}, // 0b0110101000 + {.id = 425, .name = "Monthly Senior LinkPass"}, // 0b0110101001 + {.id = 421, .name = "Senior TAP/Permit"}, // 0b0110100101 + {.id = 422, .name = "Senior TAP/Permit 30 Days"}, // 0b0110100110 + + // Commuter rail passes + {.id = 166, .name = "30 Day Commuter Rail Zone 1A Pass"}, + {.id = 167, .name = "30 Day Commuter Rail Zone 1 Pass"}, + {.id = 168, .name = "30 Day Commuter Rail Zone 2 Pass"}, + {.id = 169, .name = "30 Day Commuter Rail Zone 3 Pass"}, + {.id = 170, .name = "30 Day Commuter Rail Zone 4 Pass"}, + {.id = 171, .name = "30 Day Commuter Rail Zone 5 Pass"}, + {.id = 172, .name = "30 Day Commuter Rail Zone 6 Pass"}, + {.id = 173, .name = "30 Day Commuter Rail Zone 7 Pass"}, + {.id = 174, .name = "30 Day Commuter Rail Zone 8 Pass"}, + {.id = 175, .name = "30 Day Interzone 1 Pass"}, + {.id = 176, .name = "30 Day Interzone 2 Pass"}, + {.id = 177, .name = "30 Day Interzone 3 Pass"}, + {.id = 178, .name = "30 Day Interzone 4 Pass"}, + {.id = 179, .name = "30 Day Interzone 5 Pass"}, + {.id = 180, .name = "30 Day Interzone 6 Pass"}, + {.id = 181, .name = "30 Day Interzone 7 Pass"}, + {.id = 182, .name = "30 Day Interzone 8 Pass"}, + + {.id = 140, .name = "One Way Interzone Adult 1 Zone"}, + {.id = 141, .name = "One Way Interzone Adult 2 Zones"}, + {.id = 142, .name = "One Way Interzone Adult 3 Zones"}, + {.id = 143, .name = "One Way Interzone Adult 4 Zones"}, + {.id = 144, .name = "One Way Interzone Adult 5 Zones"}, + {.id = 145, .name = "One Way Interzone Adult 6 Zones"}, + {.id = 146, .name = "One Way Interzone Adult 7 Zones"}, + {.id = 147, .name = "One Way Interzone Adult 8 Zones"}, + + {.id = 428, .name = "One Way Half Fare Zone 1"}, + {.id = 429, .name = "One Way Half Fare Zone 2"}, + {.id = 430, .name = "One Way Half Fare Zone 3"}, + {.id = 431, .name = "One Way Half Fare Zone 4"}, + {.id = 432, .name = "One Way Half Fare Zone 5"}, + {.id = 433, .name = "One Way Half Fare Zone 6"}, + {.id = 434, .name = "One Way Half Fare Zone 7"}, + {.id = 435, .name = "One Way Half Fare Zone 8"}, + {.id = 436, .name = "One Way Interzone Half Fare 1 Zone"}, + {.id = 437, .name = "One Way Interzone Half Fare 2 Zones"}, + {.id = 438, .name = "One Way Interzone Half Fare 3 Zones"}, + {.id = 439, .name = "One Way Interzone Half Fare 4 Zones"}, + {.id = 440, .name = "One Way Interzone Half Fare 5 Zones"}, + {.id = 441, .name = "One Way Interzone Half Fare 6 Zones"}, + {.id = 442, .name = "One Way Interzone Half Fare 7 Zones"}, + {.id = 443, .name = "One Way Interzone Half Fare 8 Zones"}, + + {.id = 509, .name = "Group Interzone 1 Zones"}, + {.id = 510, .name = "Group Interzone 2 Zones"}, + {.id = 511, .name = "Group Interzone 3 Zones"}, + {.id = 512, .name = "Group Interzone 4 Zones"}, + {.id = 513, .name = "Group Interzone 5 Zones"}, + {.id = 514, .name = "Group Interzone 6 Zones"}, + {.id = 515, .name = "Group Interzone 7 Zones"}, + {.id = 516, .name = "Group Interzone 8 Zones"}, + + {.id = 952, .name = "Zone 1 Student Monthly Pass"}, + {.id = 953, .name = "Zone 2 Student Monthly Pass"}, + {.id = 954, .name = "Zone 3 Student Monthly Pass"}, + {.id = 955, .name = "Zone 4 Student Monthly Pass"}, + {.id = 956, .name = "Zone 5 Student Monthly Pass"}, + {.id = 957, .name = "Zone 6 Student Monthly Pass"}, + {.id = 958, .name = "Zone 7 Student Monthly Pass"}, + {.id = 959, .name = "Zone 8 Student Monthly Pass"}, + {.id = 960, .name = "Zone 9 Student Monthly Pass"}, + {.id = 961, .name = "Zone 10 Student Monthly Pass"}, + + {.id = 963, .name = "Interzone 1 Zone Student Monthly Pass"}, + {.id = 964, .name = "Interzone 2 Zone Student Monthly Pass"}, + {.id = 965, .name = "Interzone 3 Zone Student Monthly Pass"}, + {.id = 966, .name = "Interzone 4 Zone Student Monthly Pass"}, + {.id = 967, .name = "Interzone 5 Zone Student Monthly Pass"}, + {.id = 968, .name = "Interzone 6 Zone Student Monthly Pass"}, + {.id = 969, .name = "Interzone 7 Zone Student Monthly Pass"}, + {.id = 970, .name = "Interzone 8 Zone Student Monthly Pass"}, + {.id = 971, .name = "Interzone 9 Zone Student Monthly Pass"}, + {.id = 972, .name = "Interzone 10 Zone Student Monthly Pass"}, +}; +static const size_t kNumTypes = COUNT_OF(charliecard_types); + +// Incomplete, and subject to change +// Only covers Orange & Blue line stations +// Gathered manually, and provided courtesy of, DEFCON31 researchers +// as cited above. +static const IdMapping charliecard_fare_gate_ids[] = { + // Davis + {.id = 6766, .name = "Davis"}, + {.id = 6767, .name = "Davis"}, + {.id = 6768, .name = "Davis"}, + {.id = 6769, .name = "Davis"}, + {.id = 6770, .name = "Davis"}, + {.id = 6771, .name = "Davis"}, + {.id = 6772, .name = "Davis"}, + {.id = 2167, .name = "Davis"}, + {.id = 7020, .name = "Davis"}, + // Porter + {.id = 6781, .name = "Porter"}, + {.id = 6780, .name = "Porter"}, + {.id = 6779, .name = "Porter"}, + {.id = 6778, .name = "Porter"}, + {.id = 6777, .name = "Porter"}, + {.id = 6776, .name = "Porter"}, + {.id = 6775, .name = "Porter"}, + {.id = 2168, .name = "Porter"}, + {.id = 7021, .name = "Porter"}, + {.id = 6782, .name = "Porter"}, + // Oak Grove + {.id = 6640, .name = "Oak Grove"}, + {.id = 6641, .name = "Oak Grove"}, + {.id = 6639, .name = "Oak Grove"}, + {.id = 2036, .name = "Oak Grove"}, + {.id = 6642, .name = "Oak Grove"}, + {.id = 6979, .name = "Oak Grove"}, + // Downtown Crossing + {.id = 2091, .name = "Downtown Crossing"}, + {.id = 6995, .name = "Downtown Crossing"}, + {.id = 6699, .name = "Downtown Crossing"}, + {.id = 6700, .name = "Downtown Crossing"}, + {.id = 1926, .name = "Downtown Crossing"}, + {.id = 2084, .name = "Downtown Crossing"}, + {.id = 6994, .name = "Downtown Crossing"}, + {.id = 6695, .name = "Downtown Crossing"}, + {.id = 6694, .name = "Downtown Crossing"}, + {.id = 6696, .name = "Downtown Crossing"}, + {.id = 2336, .name = "Downtown Crossing"}, + {.id = 1056, .name = "Downtown Crossing"}, + {.id = 6814, .name = "Downtown Crossing"}, + {.id = 6813, .name = "Downtown Crossing"}, + {.id = 2212, .name = "Downtown Crossing"}, + {.id = 7038, .name = "Downtown Crossing"}, + // State + {.id = 7092, .name = "State"}, + {.id = 1844, .name = "State"}, + {.id = 6689, .name = "State"}, + {.id = 6988, .name = "State"}, + {.id = 6991, .name = "State"}, + {.id = 2083, .name = "State"}, + {.id = 6688, .name = "State"}, + {.id = 6687, .name = "State"}, + {.id = 6686, .name = "State"}, + {.id = 2078, .name = "State"}, + {.id = 6987, .name = "State"}, + {.id = 7090, .name = "State"}, + {.id = 1842, .name = "State"}, + // Haymarket + {.id = 6684, .name = "Haymarket"}, + {.id = 6683, .name = "Haymarket"}, + {.id = 6682, .name = "Haymarket"}, + {.id = 6681, .name = "Haymarket"}, + {.id = 2073, .name = "Haymarket"}, + {.id = 7074, .name = "Haymarket"}, + {.id = 6883, .name = "Haymarket"}, + {.id = 6884, .name = "Haymarket"}, + {.id = 6885, .name = "Haymarket"}, + {.id = 6886, .name = "Haymarket"}, + {.id = 2303, .name = "Haymarket"}, + {.id = 6986, .name = "Haymarket"}, + // North Station + {.id = 6985, .name = "North Station"}, + {.id = 2063, .name = "North Station"}, + {.id = 6671, .name = "North Station"}, + {.id = 6672, .name = "North Station"}, + {.id = 6673, .name = "North Station"}, + {.id = 6674, .name = "North Station"}, + {.id = 6675, .name = "North Station"}, + {.id = 6676, .name = "North Station"}, + {.id = 6677, .name = "North Station"}, + {.id = 6678, .name = "North Station"}, + {.id = 6984, .name = "North Station"}, + {.id = 2062, .name = "North Station"}, + {.id = 6668, .name = "North Station"}, + {.id = 6667, .name = "North Station"}, + {.id = 6666, .name = "North Station"}, + {.id = 6665, .name = "North Station"}, + {.id = 6664, .name = "North Station"}, + // Sullivan Square + {.id = 6654, .name = "Sullivan Square"}, + {.id = 6655, .name = "Sullivan Square"}, + {.id = 6656, .name = "Sullivan Square"}, + {.id = 6657, .name = "Sullivan Square"}, + {.id = 6658, .name = "Sullivan Square"}, + {.id = 6659, .name = "Sullivan Square"}, + {.id = 2053, .name = "Sullivan Square"}, + {.id = 6982, .name = "Sullivan Square"}, + // Community College + {.id = 6661, .name = "Community College"}, + {.id = 6662, .name = "Community College"}, + {.id = 2056, .name = "Community College"}, + {.id = 6983, .name = "Community College"}, + // Assembly + {.id = 3876, .name = "Assembly"}, + {.id = 3875, .name = "Assembly"}, + {.id = 6957, .name = "Assembly"}, + {.id = 6956, .name = "Assembly"}, + {.id = 6955, .name = "Assembly"}, + {.id = 6954, .name = "Assembly"}, + {.id = 6953, .name = "Assembly"}, + {.id = 7101, .name = "Assembly"}, + {.id = 3873, .name = "Assembly"}, + {.id = 3872, .name = "Assembly"}, + // Wellington + {.id = 6981, .name = "Wellington"}, + {.id = 2042, .name = "Wellington"}, + {.id = 6650, .name = "Wellington"}, + {.id = 6651, .name = "Wellington"}, + {.id = 6652, .name = "Wellington"}, + {.id = 6653, .name = "Wellington"}, + // Malden + {.id = 6980, .name = "Malden Center"}, + {.id = 2037, .name = "Malden Center"}, + {.id = 6645, .name = "Malden Center"}, + {.id = 6646, .name = "Malden Center"}, + {.id = 6647, .name = "Malden Center"}, + {.id = 6648, .name = "Malden Center"}, + // Chinatown + {.id = 6704, + .name = + "Malden Center"}, // Entry error? Placed after "Chinatown" divider, but with name Malden Center + {.id = 6705, .name = "Chinatown"}, + {.id = 2099, .name = "Chinatown"}, + {.id = 7003, .name = "Chinatown"}, + {.id = 7002, .name = "Chinatown"}, + {.id = 2096, .name = "Chinatown"}, + {.id = 6702, .name = "Chinatown"}, + {.id = 6701, .name = "Chinatown"}, + // Tufts Medical Center + {.id = 6707, .name = "Tufts Medical Center"}, + {.id = 6708, .name = "Tufts Medical Center"}, + {.id = 6709, .name = "Tufts Medical Center"}, + {.id = 6710, .name = "Tufts Medical Center"}, + {.id = 6711, .name = "Tufts Medical Center"}, + {.id = 2105, .name = "Tufts Medical Center"}, + {.id = 7004, .name = "Tufts Medical Center"}, + {.id = 1941, .name = "Tufts Medical Center"}, + {.id = 7006, .name = "Tufts Medical Center"}, + // Back Bay + {.id = 7007, .name = "Back Bay"}, + {.id = 1480, .name = "Back Bay"}, + {.id = 6714, .name = "Back Bay"}, + {.id = 6715, .name = "Back Bay"}, + {.id = 6716, .name = "Back Bay"}, + {.id = 6717, .name = "Back Bay"}, + {.id = 6718, .name = "Back Bay"}, + {.id = 6719, .name = "Back Bay"}, + {.id = 6720, .name = "Back Bay"}, + {.id = 1801, .name = "Back Bay"}, + {.id = 7009, .name = "Back Bay"}, + // Massachusetts Avenue + {.id = 7010, .name = "Massachusetts Avenue"}, + {.id = 2118, .name = "Massachusetts Avenue"}, + {.id = 6724, .name = "Massachusetts Avenue"}, + {.id = 6723, .name = "Massachusetts Avenue"}, + {.id = 6722, .name = "Massachusetts Avenue"}, + {.id = 6721, .name = "Massachusetts Avenue"}, + // Ruggles + {.id = 6726, .name = "Ruggles"}, + {.id = 6727, .name = "Ruggles"}, + {.id = 6728, .name = "Ruggles"}, + {.id = 2122, .name = "Ruggles"}, + {.id = 2123, .name = "Ruggles"}, + {.id = 2124, .name = "Ruggles"}, + {.id = 1804, .name = "Ruggles"}, + // Roxbury Crossing + {.id = 6737, .name = "Roxbury Crossing"}, + {.id = 6736, .name = "Roxbury Crossing"}, + {.id = 6735, .name = "Roxbury Crossing"}, + {.id = 6734, .name = "Roxbury Crossing"}, + {.id = 6733, .name = "Roxbury Crossing"}, + {.id = 2125, .name = "Roxbury Crossing"}, + {.id = 7012, .name = "Roxbury Crossing"}, + // Jackson Square + {.id = 6741, .name = "Jackson Square"}, + {.id = 6740, .name = "Jackson Square"}, + {.id = 6739, .name = "Jackson Square"}, + {.id = 2131, .name = "Jackson Square"}, + {.id = 7013, .name = "Jackson Square"}, + {.id = 7014, .name = "Jackson Square"}, + {.id = 2135, .name = "Jackson Square"}, + {.id = 6743, .name = "Jackson Square"}, + {.id = 6744, .name = "Jackson Square"}, + {.id = 6745, .name = "Jackson Square"}, + // Green Street + {.id = 6746, .name = "Green Street"}, + {.id = 6747, .name = "Green Street"}, + {.id = 6748, .name = "Green Street"}, + {.id = 2142, .name = "Green Street"}, + {.id = 7015, .name = "Green Street"}, + // Forest Hills + {.id = 6750, .name = "Forest Hills"}, + {.id = 6751, .name = "Forest Hills"}, + {.id = 6752, .name = "Forest Hills"}, + {.id = 6753, .name = "Forest Hills"}, + {.id = 6754, .name = "Forest Hills"}, + {.id = 6755, .name = "Forest Hills"}, + {.id = 2150, .name = "Forest Hills"}, + {.id = 7016, .name = "Forest Hills"}, + {.id = 6950, .name = "Forest Hills"}, + {.id = 6951, .name = "Forest Hills"}, + {.id = 604, .name = "Forest Hills"}, // Entry error? + {.id = 7096, .name = "Forest Hills"}, + // South Station + {.id = 7039, .name = "South Station"}, + {.id = 2215, .name = "South Station"}, + {.id = 6816, .name = "South Station"}, + {.id = 6817, .name = "South Station"}, + {.id = 6818, .name = "South Station"}, + {.id = 6819, .name = "South Station"}, + {.id = 6820, .name = "South Station"}, + {.id = 6821, .name = "South Station"}, + {.id = 6822, .name = "South Station"}, + {.id = 6823, .name = "South Station"}, + {.id = 7040, .name = "South Station"}, + {.id = 2228, .name = "South Station"}, + {.id = 6827, .name = "South Station"}, + {.id = 6826, .name = "South Station"}, + {.id = 6825, .name = "South Station"}, + {.id = 6824, .name = "South Station"}, + // Courthouse + {.id = 6929, .name = "Courthouse"}, + {.id = 2357, .name = "Courthouse"}, + {.id = 7079, .name = "Courthouse"}, + {.id = 6933, .name = "Courthouse"}, + {.id = 6932, .name = "Courthouse"}, + {.id = 2358, .name = "Courthouse"}, + {.id = 6792, .name = "Courthouse"}, + // Bowdoin + {.id = 6937, .name = "Bowdoin"}, + {.id = 2367, .name = "Bowdoin"}, + {.id = 7085, .name = "Bowdoin"}, + // Government Center + {.id = 6963, .name = "Government Center"}, + {.id = 6962, .name = "Government Center"}, + {.id = 6961, .name = "Government Center"}, + {.id = 6960, .name = "Government Center"}, + {.id = 6959, .name = "Government Center"}, + {.id = 6958, .name = "Government Center"}, + {.id = 5298, .name = "Government Center"}, + // Aquarium + {.id = 6609, .name = "Aquarium"}, + {.id = 6608, .name = "Aquarium"}, + {.id = 1877, .name = "Aquarium"}, + {.id = 6965, .name = "Aquarium"}, + {.id = 6610, .name = "Aquarium"}, + {.id = 1880, .name = "Aquarium"}, + {.id = 1871, .name = "Aquarium"}, + {.id = 6966, .name = "Aquarium"}, + // Maverick + {.id = 7088, .name = "Maverick"}, + {.id = 6944, .name = "Maverick"}, + {.id = 4384, .name = "Maverick"}, + {.id = 6946, .name = "Maverick"}, + {.id = 6947, .name = "Maverick"}, + {.id = 6948, .name = "Maverick"}, + {.id = 6949, .name = "Maverick"}, + {.id = 1840, .name = "Maverick"}, + {.id = 7083, .name = "Maverick"}, + // Airport + {.id = 6613, .name = "Airport"}, + {.id = 6612, .name = "Airport"}, + {.id = 6611, .name = "Airport"}, + {.id = 6968, .name = "Airport"}, + {.id = 2009, .name = "Airport"}, + {.id = 6616, .name = "Airport"}, + {.id = 6615, .name = "Airport"}, + {.id = 6614, .name = "Airport"}, + {.id = 6970, .name = "Airport"}, + {.id = 1847, .name = "Airport"}, + // Wood Island + {.id = 6618, .name = "Wood Island"}, + {.id = 6619, .name = "Wood Island"}, + {.id = 2010, .name = "Wood Island"}, + {.id = 6971, .name = "Wood Island"}, + // Orient Heights + {.id = 6621, .name = "Orient Heights"}, // marked as needs checking + {.id = 6622, .name = "Orient Heights"}, + {.id = 6623, .name = "Orient Heights"}, + {.id = 2014, .name = "Orient Heights"}, + {.id = 6972, .name = "Orient Heights"}, + {.id = 6974, .name = "Orient Heights"}, + {.id = 1868, .name = "Orient Heights"}, + // Suffolk Downs + {.id = 6625, .name = "Suffolk Downs"}, + {.id = 6626, .name = "Suffolk Downs"}, + {.id = 2017, .name = "Suffolk Downs"}, + {.id = 6975, .name = "Suffolk Downs"}, + // Beachmont + {.id = 6628, .name = "Beachmont"}, + {.id = 6629, .name = "Beachmont"}, + {.id = 6630, .name = "Beachmont"}, + {.id = 2021, .name = "Beachmont"}, + {.id = 6976, .name = "Beachmont"}, + // Revere Beach + {.id = 6632, .name = "Revere Beach"}, + {.id = 6633, .name = "Revere Beach"}, + {.id = 2024, .name = "Revere Beach"}, + {.id = 6977, .name = "Revere Beach"}, + // Wonderland + {.id = 6638, .name = "Wonderland"}, + {.id = 6637, .name = "Wonderland"}, + {.id = 6636, .name = "Wonderland"}, + {.id = 2025, .name = "Wonderland"}, + {.id = 6978, .name = "Wonderland"}, +}; +static const size_t kNumFareGateIds = COUNT_OF(charliecard_fare_gate_ids); + +static const uint8_t* + pos_to_ptr(const MfClassicData* data, uint8_t sector_num, uint8_t block_num, uint8_t byte_num) { + uint8_t block_offset = mf_classic_get_first_block_num_of_sector(sector_num); + return &data->block[block_offset + block_num].data[byte_num]; +} + +static uint64_t pos_to_num( + const MfClassicData* data, + uint8_t sector_num, + uint8_t block_num, + uint8_t byte_num, + uint8_t byte_len) { + return bit_lib_bytes_to_num_be(pos_to_ptr(data, sector_num, block_num, byte_num), byte_len); +} + +static DateTime dt_delta(DateTime dt, uint64_t delta_secs) { + DateTime dt_shifted = {0}; + datetime_timestamp_to_datetime(datetime_datetime_to_timestamp(&dt) + delta_secs, &dt_shifted); + + return dt_shifted; +} + +static bool get_map_item(uint16_t id, const IdMapping* map, size_t sz, const char** out) { + // code borrowed from Jeremy Cooper's 'clipper.c'. Used as follows: + // const char* s; if(!get_map_item(_,_,_,&s)) {s="Default str";} + // TODO: change to furistring out? + for(size_t i = 0; i < sz; i++) { + if(map[i].id == id) { + *out = map[i].name; + return true; + } + } + + return false; +} + +static bool charliecard_verify(Nfc* nfc) { + // does this suffice? Or should I check add'l keys/data/etc? + bool verified = false; + + do { + const uint8_t verify_sector = 1; + const uint8_t verify_block = mf_classic_get_first_block_num_of_sector(verify_sector) + 1; + FURI_LOG_D(TAG, "Verifying sector %u", verify_sector); + + MfClassicKey key = {0}; + bit_lib_num_to_bytes_be( + charliecard_1k_keys[verify_sector].a, COUNT_OF(key.data), key.data); + + MfClassicAuthContext auth_context; + MfClassicError error = + mf_classic_poller_sync_auth(nfc, verify_block, &key, MfClassicKeyTypeA, &auth_context); + if(error != MfClassicErrorNone) { + FURI_LOG_D(TAG, "Failed to read block %u: %d", verify_block, error); + break; + } + + verified = true; + } while(false); + + return verified; +} + +static bool charliecard_read(Nfc* nfc, NfcDevice* device) { + furi_assert(nfc); + furi_assert(device); + + bool is_read = false; + + MfClassicData* data = mf_classic_alloc(); + nfc_device_copy_data(device, NfcProtocolMfClassic, data); + + do { + MfClassicType type = MfClassicTypeMini; + MfClassicError error = mf_classic_poller_sync_detect_type(nfc, &type); + if(error != MfClassicErrorNone) break; + + data->type = type; + if(type != MfClassicType1k) break; + + MfClassicDeviceKeys keys = { + .key_a_mask = 0, + .key_b_mask = 0, + }; + for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) { + bit_lib_num_to_bytes_be( + charliecard_1k_keys[i].a, sizeof(MfClassicKey), keys.key_a[i].data); + FURI_BIT_SET(keys.key_a_mask, i); + bit_lib_num_to_bytes_be( + charliecard_1k_keys[i].b, sizeof(MfClassicKey), keys.key_b[i].data); + FURI_BIT_SET(keys.key_b_mask, i); + } + + error = mf_classic_poller_sync_read(nfc, &keys, data); + if(error == MfClassicErrorNotPresent) { + FURI_LOG_W(TAG, "Failed to read data"); + break; + } + + nfc_device_set_data(device, NfcProtocolMfClassic, data); + + is_read = (error == MfClassicErrorNone); + } while(false); + + mf_classic_free(data); + + return is_read; +} + +uint32_t time_now() { + return furi_hal_rtc_get_timestamp(); +} + +static Money money_parse( + const MfClassicData* data, + uint8_t sector_num, + uint8_t block_num, + uint8_t byte_num) { + // CharlieCards store all money values in two bytes as half-cents + // bitmask removes sign/flag, bitshift converts half-cents to cents, div & mod yield dollars & cents + uint16_t amt = (pos_to_num(data, sector_num, block_num, byte_num, 2) & 0x7FFF) >> 1; + return (Money){amt / 100, amt % 100}; +} + +static DateTime + date_parse(const MfClassicData* data, uint8_t sector_num, uint8_t block_num, uint8_t byte_num) { + // Dates are 3 bytes, in minutes since 2003/1/1 ("CHARLIE_EPOCH") + uint32_t ts_charlie = pos_to_num(data, sector_num, block_num, byte_num, 3); + return dt_delta(CHARLIE_EPOCH, ts_charlie * CHARLIE_TIME_DELTA_SECS); +} + +static DateTime + end_validity_parse(const MfClassicData* data, enum CharlieActiveSector active_sec) { + // End validity field is a bit odd; shares byte 1 with another variable (the card type field), + // occupying only the last 3 bits (and subsequent two bytes), hence bitmask + // TODO; what are the add'l 3 bits between type & end validity fields? + uint32_t ts_charlie_ev = + pos_to_num(data, (active_sec == CHARLIE_ACTIVE_SECTOR_2) ? 2 : 3, 1, 1, 3); + ts_charlie_ev = ts_charlie_ev & 0x1FFFFF; + + // additionally, instead of minute deltas, is in 8 minute increments + // relative to CHARLIE_EPOCH (2003/1/1), per DEFCON31 researcher's work + return dt_delta(CHARLIE_EPOCH, ts_charlie_ev * CHARLIE_END_VALID_DELTA_SECS); +} + +static Trip + trip_parse(const MfClassicData* data, uint8_t sector_num, uint8_t block_num, uint8_t byte_num) { + /* This function parses individual trips. Each trip packs 7 bytes, stored as follows: + + 0 1 2 3 4 5 6 + +----.----.----+----.--+-+----.----+ + | date | loc |f| amt | + +----.----.----+----.--+-+----.----+ + + Where date is in the typical format, loc represents the fare gate tapped, and amt is the fare amount. + Amount appears to contain some flag bits, however, it is unclear what precisely their function is. + + Gate ID ("loc") is only the first 13 bits of 0x3:0x5, the final three bits appear to be flags ("f"). + Least significant flag bit (ie "loc & 0x1") seems to indicate: + — When 0, fare (the amount by which balance is decremented) + — When 1, refill (the amount by which balance is incremented) + + On monthly pass cards, MSB of amt will be set: 0x8000 (negative zero) + Seemingly randomly (irrespective of card type, last trip, etc) 0x0001 will be set on amt in addition to + whatever the regular fare is (a half cent more). I am uncertain what this flag indicates. + */ + const DateTime date = date_parse(data, sector_num, block_num, byte_num); + const uint16_t gate = pos_to_num(data, sector_num, block_num, byte_num + 3, 2) >> 3; + const uint8_t g_flag = pos_to_num(data, sector_num, block_num, byte_num + 3, 2) & 0b111; + const Money fare = money_parse(data, sector_num, block_num, byte_num + 5); + const uint8_t f_flag = pos_to_num(data, sector_num, block_num, byte_num + 5, 2) & 0x8001; + return (Trip){date, gate, g_flag, fare, f_flag}; +} + +static bool date_ge(DateTime dt1, DateTime dt2) { + return datetime_datetime_to_timestamp(&dt1) >= datetime_datetime_to_timestamp(&dt2); +} + +static Trip* trips_parse(const MfClassicData* data) { + /* Sectors 6 & 7 store the last 10 trips. Overall layout as follows: + + 0 1 2 3 4 5 6 7 8 9 A B C D E F + +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ + 0x180 | trip0 | trip1 | crc1 | + +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ + ... ... ... ... + +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ + 0x1D0 | trip8 | trip9 | crc5 | + +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ + 0x1E0 | empty | crc6 | + +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ + + "empty" is all 0s. Trips are not sorted, rather, appear to get overwritten sequentially. (eg, sorted modulo array rotation) + */ + Trip* trips = malloc(sizeof(Trip) * CHARLIE_N_TRIP_HISTORY); + + // Parse each trip field using some modular math magic to get the offsets: + // move from sector 6 -> 7 after the first 6 trips + // move a block within a given sector every 2 trips, reset every 3 blocks (as sector has changed) + // alternate between a start byte of 0 and 7 with every iteration + for(size_t i = 0; i < CHARLIE_N_TRIP_HISTORY; i++) { + trips[i] = trip_parse(data, 6 + (i / 6), (i / 2) % 3, (i % 2) * 7); + } + + // Iterate through the array to find the maximum (newest) date value + int max_idx = 0; + for(int i = 1; i < CHARLIE_N_TRIP_HISTORY; i++) { + if(date_ge(trips[i].date, trips[max_idx].date)) { + max_idx = i; + } + } + + // Sort by rotating + for(int r = 0; r < (max_idx + 1); r++) { + // Store the first element + Trip temp = trips[0]; + // Shift elements to the left + for(int i = 0; i < CHARLIE_N_TRIP_HISTORY - 1; i++) { + trips[i] = trips[i + 1]; + } + // Move the first element to the last + trips[CHARLIE_N_TRIP_HISTORY - 1] = temp; + } + + // Reverse order, such that newest is first, oldest last + for(int i = 0; i < CHARLIE_N_TRIP_HISTORY / 2; i++) { + // Swap elements at index i and size - i - 1 + Trip temp = trips[i]; + trips[i] = trips[CHARLIE_N_TRIP_HISTORY - i - 1]; + trips[CHARLIE_N_TRIP_HISTORY - i - 1] = temp; + } + + return trips; +} + +static uint16_t n_uses(const MfClassicData* data, const enum CharlieActiveSector active_sector) { + /* First two bytes of applicable block (sector 1, block 1 or 2 depending on active_sector) + The *lower* of the two values *minus one* is the true use count, + per DEFCON31 researcher's findings + */ + return pos_to_num(data, 1, 1 + active_sector, 0, 2) - 1; +} + +static enum CharlieActiveSector get_active_sector(const MfClassicData* data) { + /* Card has two transaction sectors (2 & 3) containing balance data, with two + corresponding trip counters in 0x50:0x51 & 0x60:0x61 (sector 1, byte 0:1 of blocks 1 & 2). + + The *lower* count variable corresponds to the active sector + (0x5_ lower -> 2 active, 0x6_ lower -> 3 active) + + Sectors 2 & 3 are (largely) identical, save for trip data. + Card seems to alternate between the two, with active sector storing + the current balance & recent trip/transaction, & the inactive sector storing + the N-1 trip/transaction version of the same data. + + Here I check both the trip count and the stored transaction date, + for my own sanity, to confirm the active sector. + */ + + // active sector based on trip counters + const bool active_trip = n_uses(data, CHARLIE_ACTIVE_SECTOR_2) <= + n_uses(data, CHARLIE_ACTIVE_SECTOR_3); + + // active sector based on transaction date + DateTime ds2 = date_parse(data, 2, 0, 1); + DateTime ds3 = date_parse(data, 3, 0, 1); + const bool active_date = datetime_datetime_to_timestamp(&ds2) >= + datetime_datetime_to_timestamp(&ds3); + + // with all tested cards so far, this has been true + furi_assert(active_trip == active_date); + + return active_trip ? CHARLIE_ACTIVE_SECTOR_2 : CHARLIE_ACTIVE_SECTOR_3; +} + +static uint16_t type_parse(const MfClassicData* data) { + /* Card type data stored in the first 10bits of block 1 of sectors 2 & 3 (Block 9 & Block 13, from card start) + To my knowledge, card type should never change, so we can check either + without caring which is active. For my sanity, I check both, and assert equal. + */ + + // bitshift (2bytes = 16 bits) by 6bits for just first 10bits + const uint16_t type1 = pos_to_num(data, 2, 1, 0, 2) >> 6; + const uint16_t type2 = pos_to_num(data, 3, 1, 0, 2) >> 6; + furi_assert(type1 == type2); + + return type1; +} + +/* +static DateTime expiry(DateTime iss) { + // Per Metrodroid CharlieCard parser (https://github.com/metrodroid/metrodroid/blob/master/src/commonMain/kotlin/au/id/micolous/metrodroid/transit/charlie/CharlieCardTransitData.kt) + // Expiry not explicitly stored in card data; rather, calculated from date of issue + // Cards were first issued in 2006, expired in 5 years, w/ no printed expiry date + // Cards issued after 2011 expire in 10 years + // + // Per DEFCON31 researcher's work (cited above): + // Student cards last one school year and expire at the end of August the following year + // Pre-2011 issued cards expire in 7 years, not 5 as claimed by Metrodroid + // Post-2011 expire in 10 years, less one day + // Redundant function given the existance of the end validity field? + // Any important distinctions between the two? + + + // perhaps additionally clipping to 2030-12-__ in anticipation of upcoming system migration? + // need to get a new card to confirm. + + // TODO add card type logic for student card expiry + DateTime exp; + if(iss.year < 2011) { + // add 7 years; assumes average year of 8766 hrs (to account for leap years) + // may be off by a few hours as a result + exp = dt_delta(iss, 7 * 8766 * 60 * 60); + } else { + // add 10 years, subtract a day. Same assumption as above + exp = dt_delta(iss, ((10 * 8766) - 24) * 60 * 60); + } + + return exp; +}*/ + +static bool expired(DateTime expiry, DateTime last_trip) { + // if a card has sat unused for >2 years, expired (verify this claim?) + // else expired if current date > expiry date + + uint32_t ts_exp = datetime_datetime_to_timestamp(&expiry); + uint32_t ts_last = datetime_datetime_to_timestamp(&last_trip); + uint32_t ts_now = time_now(); + + return (ts_exp <= ts_now) | ((ts_now - ts_last) >= (2 * 365 * 24 * 60 * 60)); +} + +void locale_format_dt_cat(FuriString* out, const DateTime* dt) { + // helper to print datetimes + FuriString* s = furi_string_alloc(); + + LocaleDateFormat date_format = locale_get_date_format(); + const char* separator = (date_format == LocaleDateFormatDMY) ? "." : "/"; + locale_format_date(s, dt, date_format, separator); + furi_string_cat(out, s); + locale_format_time(s, dt, locale_get_time_format(), false); + furi_string_cat_printf(out, " "); + furi_string_cat(out, s); + + furi_string_free(s); +} + +void type_format_cat(FuriString* out, uint16_t type) { + const char* s; + if(!get_map_item(type, charliecard_types, kNumTypes, &s)) { + s = ""; + furi_string_cat_printf(out, "Unknown-%u", type); + } + + furi_string_cat_str(out, s); +} + +void money_format_cat(FuriString* out, Money money) { + furi_string_cat_printf(out, "$%u.%02u", money.dollars, money.cents); +} + +void trip_format_cat(FuriString* out, Trip trip) { + const char* sep = " "; + const char* sta; + + locale_format_dt_cat(out, &trip.date); + furi_string_cat_printf(out, "\n%s", !!(trip.g_flag & 0x1) ? "-" : "+"); + money_format_cat(out, trip.fare); + if(!!(trip.g_flag & 0x1) && (trip.fare.dollars == FARE_BUS.dollars) && + (trip.fare.cents == FARE_BUS.cents)) { + // if not a refill, and the fare amount is equal to bus fare (any better approach? flag bits for modality?) + // format for bus (gate ID on busses = posted bus #) + furi_string_cat_printf(out, "%sBus#%u", sep, trip.gate); + } else if(get_map_item(trip.gate, charliecard_fare_gate_ids, kNumFareGateIds, &sta)) { + // station found in fare gate ID map, append station name + furi_string_cat_str(out, sep); + furi_string_cat_str(out, sta); + } else { + // no found station in fare gate ID map & not a bus, just print ID w/o add'l info + furi_string_cat_printf(out, "%s%u", sep, trip.gate); + } + // print flags for debugging purposes + if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { + furi_string_cat_printf(out, "%s%u%s%u", sep, trip.g_flag, sep, trip.f_flag); + } +} + +static bool charliecard_parse(const NfcDevice* device, FuriString* parsed_data) { + furi_assert(device); + + const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic); + + bool parsed = false; + + do { + // Verify card type + if(data->type != MfClassicType1k) break; + + // Verify key + // arbitrary sector in the main data portion + const uint8_t verify_sector = 3; + const MfClassicSectorTrailer* sec_tr = + mf_classic_get_sector_trailer_by_sector(data, verify_sector); + + const uint64_t key_a = + bit_lib_bytes_to_num_be(sec_tr->key_a.data, COUNT_OF(sec_tr->key_a.data)); + const uint64_t key_b = + bit_lib_bytes_to_num_be(sec_tr->key_b.data, COUNT_OF(sec_tr->key_b.data)); + if(key_a != charliecard_1k_keys[verify_sector].a) break; + if(key_b != charliecard_1k_keys[verify_sector].b) break; + + // TODO: Verify add'l? + + const enum CharlieActiveSector active_sec_enum = get_active_sector(data); + const uint8_t active_sector = (active_sec_enum == CHARLIE_ACTIVE_SECTOR_2) ? 2 : 3; + + furi_string_cat_printf(parsed_data, "\e#CharlieCard"); + + size_t uid_len = 0; + const uint8_t* uid = mf_classic_get_uid(data, &uid_len); + uint32_t card_number = bit_lib_bytes_to_num_be(uid, 4); + furi_string_cat_printf(parsed_data, "\nSerial: 5-%lu", card_number); + + Money bal = money_parse(data, active_sector, 1, 5); + furi_string_cat_printf(parsed_data, "\nBal: "); + money_format_cat(parsed_data, bal); + + const uint16_t type = type_parse(data); + furi_string_cat_printf(parsed_data, "\nType: "); + type_format_cat(parsed_data, type); + + const uint16_t n_trips = n_uses(data, active_sec_enum); + furi_string_cat_printf(parsed_data, "\nTrip Count: %u", n_trips); + + const DateTime iss = date_parse(data, active_sector, 0, 6); + furi_string_cat_printf(parsed_data, "\nIssued: "); + locale_format_dt_cat(parsed_data, &iss); + + const DateTime e_v = end_validity_parse(data, active_sec_enum); + furi_string_cat_printf(parsed_data, "\nExpiry: "); + locale_format_dt_cat(parsed_data, &e_v); + + DateTime last = date_parse(data, active_sector, 0, 1); + furi_string_cat_printf(parsed_data, "\nExpired: %s", expired(e_v, last) ? "Yes" : "No"); + + Trip* trips = trips_parse(data); + furi_string_cat_printf(parsed_data, "\nTransactions:"); + for(size_t i = 0; i < CHARLIE_N_TRIP_HISTORY; i++) { + furi_string_cat_printf(parsed_data, "\n"); + trip_format_cat(parsed_data, trips[i]); + furi_string_cat_printf(parsed_data, "\n"); + } + free(trips); + + parsed = true; + } while(false); + + return parsed; +} + +/* Actual implementation of app<>plugin interface */ +static const NfcSupportedCardsPlugin charliecard_plugin = { + .protocol = NfcProtocolMfClassic, + .verify = charliecard_verify, + .read = charliecard_read, + .parse = charliecard_parse, +}; + +/* Plugin descriptor to comply with basic plugin specification */ +static const FlipperAppPluginDescriptor charliecard_plugin_descriptor = { + .appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID, + .ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION, + .entry_point = &charliecard_plugin, +}; + +/* Plugin entry point - must return a pointer to const descriptor */ +const FlipperAppPluginDescriptor* charliecard_plugin_ep(void) { + return &charliecard_plugin_descriptor; +} diff --git a/applications/services/desktop/desktop.c b/applications/services/desktop/desktop.c index 45bf6be6fa..3c1830ffa0 100644 --- a/applications/services/desktop/desktop.c +++ b/applications/services/desktop/desktop.c @@ -33,8 +33,10 @@ static void desktop_loader_callback(const void* message, void* context) { const LoaderEvent* event = message; if(event->type == LoaderEventTypeApplicationStarted) { + desktop->animation_lock = api_lock_alloc_locked(); view_dispatcher_send_custom_event(desktop->view_dispatcher, DesktopGlobalBeforeAppStarted); - furi_check(furi_semaphore_acquire(desktop->animation_semaphore, 3000) == FuriStatusOk); + api_lock_wait_unlock_and_free(desktop->animation_lock); + desktop->animation_lock = NULL; } else if(event->type == LoaderEventTypeApplicationStopped) { view_dispatcher_send_custom_event(desktop->view_dispatcher, DesktopGlobalAfterAppFinished); } @@ -126,7 +128,7 @@ static bool desktop_custom_event_callback(void* context, uint32_t event) { animation_manager_unload_and_stall_animation(desktop->animation_manager); } desktop_auto_lock_inhibit(desktop); - furi_semaphore_release(desktop->animation_semaphore); + api_lock_unlock(desktop->animation_lock); return true; case DesktopGlobalAfterAppFinished: animation_manager_load_and_continue_animation(desktop->animation_manager); @@ -276,7 +278,6 @@ void desktop_set_stealth_mode_state(Desktop* desktop, bool enabled) { Desktop* desktop_alloc(void) { Desktop* desktop = malloc(sizeof(Desktop)); - desktop->animation_semaphore = furi_semaphore_alloc(1, 0); desktop->animation_manager = animation_manager_alloc(); desktop->gui = furi_record_open(RECORD_GUI); desktop->scene_thread = furi_thread_alloc(); diff --git a/applications/services/desktop/desktop_i.h b/applications/services/desktop/desktop_i.h index c0b29f922d..06ca70bdad 100644 --- a/applications/services/desktop/desktop_i.h +++ b/applications/services/desktop/desktop_i.h @@ -20,6 +20,7 @@ #include #include +#include #define STATUS_BAR_Y_SHIFT 13 @@ -81,7 +82,7 @@ struct Desktop { bool in_transition : 1; - FuriSemaphore* animation_semaphore; + FuriApiLock animation_lock; }; Desktop* desktop_alloc(void); diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index 63b84fd502..8088f232fc 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -109,3 +109,11 @@ App( requires=["js_app"], sources=["modules/js_textbox.c"], ) + +App( + appid="js_widget", + apptype=FlipperAppType.PLUGIN, + entry_point="js_widget_ep", + requires=["js_app"], + sources=["modules/js_widget.c"], +) diff --git a/applications/system/js_app/examples/apps/Scripts/path.js b/applications/system/js_app/examples/apps/Scripts/path.js new file mode 100644 index 0000000000..0381150d29 --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/path.js @@ -0,0 +1,9 @@ +let storage = require("storage"); + +print("script has __dirpath of" + __dirpath); +print("script has __filepath of" + __filepath); +if (storage.exists(__dirpath + "/math.js")) { + print("math.js exist here."); +} else { + print("math.js does not exist here."); +} \ No newline at end of file diff --git a/applications/system/js_app/examples/apps/Scripts/widget-js.fxbm b/applications/system/js_app/examples/apps/Scripts/widget-js.fxbm new file mode 100644 index 0000000000000000000000000000000000000000..9ba5783cecd9b4f30b96d935b4cbb902f5114e6f GIT binary patch literal 32 icmb1PU|`^a;(7)Sh7*h$3>pk488{i)7#SEJFaQ85*aM6J literal 0 HcmV?d00001 diff --git a/applications/system/js_app/examples/apps/Scripts/widget.js b/applications/system/js_app/examples/apps/Scripts/widget.js new file mode 100644 index 0000000000..4ff11e4410 --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/widget.js @@ -0,0 +1,59 @@ +let widget = require("widget"); + +let demo_seconds = 30; + +print("Loading file", __filepath); +print("From directory", __dirpath); + +// addText supports "Primary" and "Secondary" font sizes. +widget.addText(10, 10, "Primary", "Example JS widget"); +widget.addText(10, 20, "Secondary", "Example widget from JS!"); + +// load a Xbm file from the same directory as this script. +widget.addText(0, 30, "Secondary", __filepath); +let logo = widget.loadImageXbm(__dirpath + "/widget-js.fxbm"); + +// add a line (x1, y1, x2, y2) +widget.addLine(10, 35, 120, 35); + +// add a circle/disc (x, y, radius) +widget.addCircle(12, 52, 10); +widget.addDisc(12, 52, 5); + +// add a frame/box (x, y, width, height) +widget.addFrame(30, 45, 10, 10); +widget.addBox(32, 47, 6, 6); + +// add a rounded frame/box (x, y, width, height, radius) +widget.addRframe(50, 45, 15, 15, 3); +widget.addRbox(53, 48, 6, 6, 2); + +// add a dot (x, y) +widget.addDot(100, 45); +widget.addDot(102, 44); +widget.addDot(104, 43); + +// add a glyph (x, y, glyph) +widget.addGlyph(115, 50, "#".charCodeAt(0)); + +// Show the widget (drawing the layers in the orderer they were added) +widget.show(); + +let i = 1; +let bitmap = undefined; +while (widget.isOpen() && i <= demo_seconds) { + // Print statements will only show up once the widget is closed. + print("count is at", i++); + + // You can call remove on any added item, it does not impact the other ids. + if (bitmap) { widget.remove(bitmap); bitmap = undefined; } + // All of the addXXX functions return an id that can be used to remove the item. + else { bitmap = widget.addXbm(77, 45, logo); } + + delay(1000); +} + +// If user did not press the back button, close the widget. +if (widget.isOpen()) { + widget.close(); +} \ No newline at end of file diff --git a/applications/system/js_app/js_thread.c b/applications/system/js_app/js_thread.c index 86b9a3ed97..1dd34d57c4 100644 --- a/applications/system/js_app/js_thread.c +++ b/applications/system/js_app/js_thread.c @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -319,6 +320,24 @@ static int32_t js_thread(void* arg) { struct mjs* mjs = mjs_create(worker); worker->modules = js_modules_create(mjs, worker->resolver); mjs_val_t global = mjs_get_global(mjs); + if(worker->path) { + FuriString* dirpath = furi_string_alloc(); + path_extract_dirname(furi_string_get_cstr(worker->path), dirpath); + mjs_set( + mjs, + global, + "__filepath", + ~0, + mjs_mk_string( + mjs, furi_string_get_cstr(worker->path), furi_string_size(worker->path), true)); + mjs_set( + mjs, + global, + "__dirpath", + ~0, + mjs_mk_string(mjs, furi_string_get_cstr(dirpath), furi_string_size(dirpath), true)); + furi_string_free(dirpath); + } mjs_set(mjs, global, "print", ~0, MJS_MK_FN(js_print)); mjs_set(mjs, global, "delay", ~0, MJS_MK_FN(js_delay)); mjs_set(mjs, global, "to_string", ~0, MJS_MK_FN(js_global_to_string)); diff --git a/applications/system/js_app/modules/js_math.c b/applications/system/js_app/modules/js_math.c index 80d97fb9cf..e7daae41b8 100644 --- a/applications/system/js_app/modules/js_math.c +++ b/applications/system/js_app/modules/js_math.c @@ -211,7 +211,7 @@ void js_math_random(struct mjs* mjs) { mjs_return(mjs, MJS_UNDEFINED); } const uint32_t random_val = furi_hal_random_get(); - double rnd = (double)random_val / RAND_MAX; + double rnd = (double)random_val / FURI_HAL_RANDOM_MAX; mjs_return(mjs, mjs_mk_number(mjs, rnd)); } diff --git a/applications/system/js_app/modules/js_widget.c b/applications/system/js_app/modules/js_widget.c new file mode 100644 index 0000000000..382177bbd4 --- /dev/null +++ b/applications/system/js_app/modules/js_widget.c @@ -0,0 +1,956 @@ +#include +#include +#include +#include +#include +#include +#include "../js_modules.h" + +typedef struct WidgetComponent WidgetComponent; +ARRAY_DEF(ComponentArray, WidgetComponent*, M_PTR_OPLIST); + +typedef struct XbmImage XbmImage; +LIST_DEF(XbmImageList, XbmImage*, M_POD_OPLIST); + +struct WidgetComponent { + void (*draw)(Canvas* canvas, void* model); + void (*free)(WidgetComponent* component); + void* model; + uint32_t id; +}; + +struct XbmImage { + uint32_t width; + uint32_t height; + uint8_t data[]; +}; + +typedef struct { + uint8_t x; + uint8_t y; + uint8_t w; + uint8_t h; +} BoxElement; + +typedef struct { + uint8_t x; + uint8_t y; + uint8_t r; +} CircleElement; + +typedef struct { + uint8_t x; + uint8_t y; + uint8_t r; +} DiscElement; + +typedef struct { + uint8_t x; + uint8_t y; +} DotElement; + +typedef struct { + uint8_t x; + uint8_t y; + const Icon* icon; +} IconElement; + +typedef struct { + uint8_t x; + uint8_t y; + uint8_t w; + uint8_t h; +} FrameElement; + +typedef struct { + uint8_t x; + uint8_t y; + uint16_t ch; +} GlyphElement; + +typedef struct { + uint8_t x1; + uint8_t y1; + uint8_t x2; + uint8_t y2; +} LineElement; + +typedef struct { + uint8_t x; + uint8_t y; + uint8_t w; + uint8_t h; + uint8_t r; +} RboxElement; + +typedef struct { + uint8_t x; + uint8_t y; + uint8_t w; + uint8_t h; + uint8_t r; +} RframeElement; + +typedef struct { + uint8_t x; + uint8_t y; + Font font; + FuriString* text; +} TextElement; + +typedef struct { + uint8_t x; + uint8_t y; + uint32_t index; + View* view; +} XbmElement; + +typedef struct { + ComponentArray_t component; + XbmImageList_t image; + uint32_t max_assigned_id; +} WidgetModel; + +typedef struct { + View* view; + ViewDispatcher* view_dispatcher; + FuriThread* thread; +} JsWidgetInst; + +static JsWidgetInst* get_this_ctx(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsWidgetInst* widget = mjs_get_ptr(mjs, obj_inst); + furi_assert(widget); + return widget; +} + +static void ret_bad_args(struct mjs* mjs, const char* error) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error); + mjs_return(mjs, MJS_UNDEFINED); +} + +static bool check_arg_count(struct mjs* mjs, size_t count) { + size_t num_args = mjs_nargs(mjs); + if(num_args != count) { + ret_bad_args(mjs, "Wrong argument count"); + return false; + } + return true; +} + +static void js_widget_load_image_xbm(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 1)) { + return; + } + + mjs_val_t path_arg = mjs_arg(mjs, 0); + size_t path_len = 0; + const char* path = mjs_get_string(mjs, &path_arg, &path_len); + if(!path) { + ret_bad_args(mjs, "Path must be a string"); + return; + } + + Storage* storage = furi_record_open(RECORD_STORAGE); + File* file = storage_file_alloc(storage); + XbmImage* xbm = NULL; + + do { + if(!storage_file_open(file, path, FSAM_READ, FSOM_OPEN_EXISTING)) { + ret_bad_args(mjs, "Failed to open file"); + break; + } + + uint32_t size = 0; + if(storage_file_read(file, &size, sizeof(size)) != sizeof(size)) { + ret_bad_args(mjs, "Failed to get file size"); + break; + } + + xbm = malloc(size); + if(storage_file_read(file, xbm, size) != size) { + ret_bad_args(mjs, "Failed to load entire file"); + free(xbm); + xbm = NULL; + break; + } + } while(false); + + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + + if(xbm == NULL) { + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + uint32_t count = 0; + with_view_model( + widget->view, + WidgetModel * model, + { + count = XbmImageList_size(model->image); + XbmImageList_push_back(model->image, xbm); + }, + false); + + mjs_return(mjs, mjs_mk_number(mjs, count)); +} + +static void js_widget_remove(struct mjs* mjs) { + bool removed = false; + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 1)) { + return; + } + + with_view_model( + widget->view, + WidgetModel * model, + { + uint32_t id = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + ComponentArray_it_t it; + ComponentArray_it(it, model->component); + while(!ComponentArray_end_p(it)) { + WidgetComponent* component = *ComponentArray_ref(it); + if(component->id == id) { + if(component->free) { + component->free(component); + } + ComponentArray_remove(model->component, it); + removed = true; + break; + } + ComponentArray_next(it); + } + }, + true); + + mjs_return(mjs, mjs_mk_boolean(mjs, removed)); +} + +static void widget_box_draw(Canvas* canvas, void* model) { + BoxElement* element = model; + canvas_draw_box(canvas, element->x, element->y, element->w, element->h); +} + +static void widget_box_free(WidgetComponent* component) { + BoxElement* element = component->model; + free(element); + free(component); +} + +static void js_widget_add_box(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 4)) return; + + int32_t x = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + int32_t y = mjs_get_int32(mjs, mjs_arg(mjs, 1)); + int32_t w = mjs_get_int32(mjs, mjs_arg(mjs, 2)); + int32_t h = mjs_get_int32(mjs, mjs_arg(mjs, 3)); + + WidgetComponent* component = malloc(sizeof(WidgetComponent)); + component->draw = widget_box_draw; + component->free = widget_box_free; + component->model = malloc(sizeof(BoxElement)); + BoxElement* element = component->model; + element->x = x; + element->y = y; + element->w = w; + element->h = h; + + with_view_model( + widget->view, + WidgetModel * model, + { + ++model->max_assigned_id; + component->id = model->max_assigned_id; + ComponentArray_push_back(model->component, component); + }, + true); + + mjs_return(mjs, mjs_mk_number(mjs, component->id)); +} + +static void widget_circle_draw(Canvas* canvas, void* model) { + CircleElement* element = model; + canvas_draw_circle(canvas, element->x, element->y, element->r); +} + +static void widget_circle_free(WidgetComponent* component) { + CircleElement* element = component->model; + free(element); + free(component); +} + +static void js_widget_add_circle(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 3)) return; + + int32_t x = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + int32_t y = mjs_get_int32(mjs, mjs_arg(mjs, 1)); + int32_t r = mjs_get_int32(mjs, mjs_arg(mjs, 2)); + + WidgetComponent* component = malloc(sizeof(WidgetComponent)); + component->draw = widget_circle_draw; + component->free = widget_circle_free; + component->model = malloc(sizeof(CircleElement)); + CircleElement* element = component->model; + element->x = x; + element->y = y; + element->r = r; + + with_view_model( + widget->view, + WidgetModel * model, + { + ++model->max_assigned_id; + component->id = model->max_assigned_id; + ComponentArray_push_back(model->component, component); + }, + true); + + mjs_return(mjs, mjs_mk_number(mjs, component->id)); +} + +static void widget_disc_draw(Canvas* canvas, void* model) { + DiscElement* element = model; + canvas_draw_disc(canvas, element->x, element->y, element->r); +} + +static void widget_disc_free(WidgetComponent* component) { + DiscElement* element = component->model; + free(element); + free(component); +} + +static void js_widget_add_disc(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 3)) return; + + int32_t x = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + int32_t y = mjs_get_int32(mjs, mjs_arg(mjs, 1)); + int32_t r = mjs_get_int32(mjs, mjs_arg(mjs, 2)); + + WidgetComponent* component = malloc(sizeof(WidgetComponent)); + component->draw = widget_disc_draw; + component->free = widget_disc_free; + component->model = malloc(sizeof(DiscElement)); + DiscElement* element = component->model; + element->x = x; + element->y = y; + element->r = r; + + with_view_model( + widget->view, + WidgetModel * model, + { + ++model->max_assigned_id; + component->id = model->max_assigned_id; + ComponentArray_push_back(model->component, component); + }, + true); + + mjs_return(mjs, mjs_mk_number(mjs, component->id)); +} + +static void widget_dot_draw(Canvas* canvas, void* model) { + DotElement* element = model; + canvas_draw_dot(canvas, element->x, element->y); +} + +static void widget_dot_free(WidgetComponent* component) { + DotElement* element = component->model; + free(element); + free(component); +} + +static void js_widget_add_dot(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 2)) return; + + int32_t x = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + int32_t y = mjs_get_int32(mjs, mjs_arg(mjs, 1)); + + WidgetComponent* component = malloc(sizeof(WidgetComponent)); + component->draw = widget_dot_draw; + component->free = widget_dot_free; + component->model = malloc(sizeof(DotElement)); + DotElement* element = component->model; + element->x = x; + element->y = y; + + with_view_model( + widget->view, + WidgetModel * model, + { + ++model->max_assigned_id; + component->id = model->max_assigned_id; + ComponentArray_push_back(model->component, component); + }, + true); + + mjs_return(mjs, mjs_mk_number(mjs, component->id)); +} + +static void widget_frame_draw(Canvas* canvas, void* model) { + FrameElement* element = model; + canvas_draw_frame(canvas, element->x, element->y, element->w, element->h); +} + +static void widget_frame_free(WidgetComponent* component) { + FrameElement* element = component->model; + free(element); + free(component); +} + +static void js_widget_add_frame(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 4)) return; + + int32_t x = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + int32_t y = mjs_get_int32(mjs, mjs_arg(mjs, 1)); + int32_t w = mjs_get_int32(mjs, mjs_arg(mjs, 2)); + int32_t h = mjs_get_int32(mjs, mjs_arg(mjs, 3)); + + WidgetComponent* component = malloc(sizeof(WidgetComponent)); + component->draw = widget_frame_draw; + component->free = widget_frame_free; + component->model = malloc(sizeof(FrameElement)); + FrameElement* element = component->model; + element->x = x; + element->y = y; + element->w = w; + element->h = h; + + with_view_model( + widget->view, + WidgetModel * model, + { + ++model->max_assigned_id; + component->id = model->max_assigned_id; + ComponentArray_push_back(model->component, component); + }, + true); + + mjs_return(mjs, mjs_mk_number(mjs, component->id)); +} + +static void widget_glyph_draw(Canvas* canvas, void* model) { + GlyphElement* element = model; + canvas_draw_glyph(canvas, element->x, element->y, element->ch); +} + +static void widget_glyph_free(WidgetComponent* component) { + GlyphElement* element = component->model; + free(element); + free(component); +} + +static void js_widget_add_glyph(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 3)) return; + + int32_t x = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + int32_t y = mjs_get_int32(mjs, mjs_arg(mjs, 1)); + int32_t ch = mjs_get_int32(mjs, mjs_arg(mjs, 2)); + + WidgetComponent* component = malloc(sizeof(WidgetComponent)); + component->draw = widget_glyph_draw; + component->free = widget_glyph_free; + component->model = malloc(sizeof(GlyphElement)); + GlyphElement* element = component->model; + element->x = x; + element->y = y; + element->ch = ch; + + with_view_model( + widget->view, + WidgetModel * model, + { + ++model->max_assigned_id; + component->id = model->max_assigned_id; + ComponentArray_push_back(model->component, component); + }, + true); + + mjs_return(mjs, mjs_mk_number(mjs, component->id)); +} + +static void widget_line_draw(Canvas* canvas, void* model) { + LineElement* element = model; + canvas_draw_line(canvas, element->x1, element->y1, element->x2, element->y2); +} + +static void widget_line_free(WidgetComponent* component) { + LineElement* element = component->model; + free(element); + free(component); +} + +static void js_widget_add_line(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 4)) return; + + int32_t x1 = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + int32_t y1 = mjs_get_int32(mjs, mjs_arg(mjs, 1)); + int32_t x2 = mjs_get_int32(mjs, mjs_arg(mjs, 2)); + int32_t y2 = mjs_get_int32(mjs, mjs_arg(mjs, 3)); + + WidgetComponent* component = malloc(sizeof(WidgetComponent)); + component->draw = widget_line_draw; + component->free = widget_line_free; + component->model = malloc(sizeof(LineElement)); + LineElement* element = component->model; + element->x1 = x1; + element->y1 = y1; + element->x2 = x2; + element->y2 = y2; + + with_view_model( + widget->view, + WidgetModel * model, + { + ++model->max_assigned_id; + component->id = model->max_assigned_id; + ComponentArray_push_back(model->component, component); + }, + true); + + mjs_return(mjs, mjs_mk_number(mjs, component->id)); +} + +static void widget_rbox_draw(Canvas* canvas, void* model) { + RboxElement* element = model; + canvas_draw_rbox(canvas, element->x, element->y, element->w, element->h, element->r); +} + +static void widget_rbox_free(WidgetComponent* component) { + BoxElement* element = component->model; + free(element); + free(component); +} + +static void js_widget_add_rbox(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 5)) return; + + int32_t x = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + int32_t y = mjs_get_int32(mjs, mjs_arg(mjs, 1)); + int32_t w = mjs_get_int32(mjs, mjs_arg(mjs, 2)); + int32_t h = mjs_get_int32(mjs, mjs_arg(mjs, 3)); + int32_t r = mjs_get_int32(mjs, mjs_arg(mjs, 4)); + + WidgetComponent* component = malloc(sizeof(WidgetComponent)); + component->draw = widget_rbox_draw; + component->free = widget_rbox_free; + component->model = malloc(sizeof(RboxElement)); + RboxElement* element = component->model; + element->x = x; + element->y = y; + element->w = w; + element->h = h; + element->r = r; + + with_view_model( + widget->view, + WidgetModel * model, + { + ++model->max_assigned_id; + component->id = model->max_assigned_id; + ComponentArray_push_back(model->component, component); + }, + true); + + mjs_return(mjs, mjs_mk_number(mjs, component->id)); +} + +static void widget_rframe_draw(Canvas* canvas, void* model) { + RframeElement* element = model; + canvas_draw_rframe(canvas, element->x, element->y, element->w, element->h, element->r); +} + +static void widget_rframe_free(WidgetComponent* component) { + RframeElement* element = component->model; + free(element); + free(component); +} + +static void js_widget_add_rframe(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 5)) return; + + int32_t x = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + int32_t y = mjs_get_int32(mjs, mjs_arg(mjs, 1)); + int32_t w = mjs_get_int32(mjs, mjs_arg(mjs, 2)); + int32_t h = mjs_get_int32(mjs, mjs_arg(mjs, 3)); + int32_t r = mjs_get_int32(mjs, mjs_arg(mjs, 4)); + + WidgetComponent* component = malloc(sizeof(WidgetComponent)); + component->draw = widget_rframe_draw; + component->free = widget_rframe_free; + component->model = malloc(sizeof(RframeElement)); + RframeElement* element = component->model; + element->x = x; + element->y = y; + element->w = w; + element->h = h; + element->r = r; + + with_view_model( + widget->view, + WidgetModel * model, + { + ++model->max_assigned_id; + component->id = model->max_assigned_id; + ComponentArray_push_back(model->component, component); + }, + true); + + mjs_return(mjs, mjs_mk_number(mjs, component->id)); +} + +static void widget_text_draw(Canvas* canvas, void* model) { + TextElement* element = model; + canvas_set_font(canvas, element->font); + canvas_draw_str(canvas, element->x, element->y, furi_string_get_cstr(element->text)); +} + +static void widget_text_free(WidgetComponent* component) { + TextElement* element = component->model; + furi_string_free(element->text); + free(element); + free(component); +} + +static void js_widget_add_text(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 4)) return; + + int32_t x = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + int32_t y = mjs_get_int32(mjs, mjs_arg(mjs, 1)); + + mjs_val_t font_arg = mjs_arg(mjs, 2); + size_t font_name_len = 0; + const char* font_name_text = mjs_get_string(mjs, &font_arg, &font_name_len); + if(!font_name_text) { + ret_bad_args(mjs, "Font name must be a string"); + return; + } + + Font font = FontTotalNumber; + size_t cmp_str_len = strlen("Primary"); + if(font_name_len == cmp_str_len && strncmp(font_name_text, "Primary", cmp_str_len) == 0) { + font = FontPrimary; + } else { + cmp_str_len = strlen("Secondary"); + if(font_name_len == cmp_str_len && + strncmp(font_name_text, "Secondary", cmp_str_len) == 0) { + font = FontSecondary; + } + } + if(font == FontTotalNumber) { + ret_bad_args(mjs, "Unknown font name"); + return; + } + + mjs_val_t text_arg = mjs_arg(mjs, 3); + size_t text_len = 0; + const char* text = mjs_get_string(mjs, &text_arg, &text_len); + if(!text) { + ret_bad_args(mjs, "Text must be a string"); + return; + } + FuriString* text_str = furi_string_alloc_set(text); + + WidgetComponent* component = malloc(sizeof(WidgetComponent)); + component->draw = widget_text_draw; + component->free = widget_text_free; + component->model = malloc(sizeof(TextElement)); + TextElement* element = component->model; + element->x = x; + element->y = y; + element->font = font; + element->text = text_str; + + with_view_model( + widget->view, + WidgetModel * model, + { + ++model->max_assigned_id; + component->id = model->max_assigned_id; + ComponentArray_push_back(model->component, component); + }, + true); + + mjs_return(mjs, mjs_mk_number(mjs, component->id)); +} + +static void widget_xbm_draw(Canvas* canvas, void* model) { + XbmElement* element = model; + XbmImage* image = NULL; + + with_view_model( + element->view, + WidgetModel * widget_model, + { image = *XbmImageList_get(widget_model->image, element->index); }, + false); + + if(image) { + canvas_draw_xbm(canvas, element->x, element->y, image->width, image->height, image->data); + } +} + +static void widget_xbm_free(WidgetComponent* component) { + XbmElement* element = component->model; + free(element); + free(component); +} + +static void js_widget_add_xbm(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 3)) return; + + int32_t x = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + int32_t y = mjs_get_int32(mjs, mjs_arg(mjs, 1)); + int32_t index = mjs_get_int32(mjs, mjs_arg(mjs, 2)); + with_view_model( + widget->view, + WidgetModel * widget_model, + { + size_t count = XbmImageList_size(widget_model->image); + if(index < 0 || index >= (int32_t)count) { + ret_bad_args(mjs, "Invalid image index"); + return; + } + }, + false); + + WidgetComponent* component = malloc(sizeof(WidgetComponent)); + component->draw = widget_xbm_draw; + component->free = widget_xbm_free; + component->model = malloc(sizeof(XbmElement)); + XbmElement* element = component->model; + element->x = x; + element->y = y; + element->index = index; + + with_view_model( + widget->view, + WidgetModel * model, + { + ++model->max_assigned_id; + component->id = model->max_assigned_id; + element->view = widget->view; + ComponentArray_push_back(model->component, component); + }, + true); + + mjs_return(mjs, mjs_mk_number(mjs, component->id)); +} + +static void js_widget_is_open(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 0)) return; + + mjs_return(mjs, mjs_mk_boolean(mjs, !!widget->thread)); +} + +static void widget_deinit(void* context) { + JsWidgetInst* widget = context; + if(widget->thread) { + furi_thread_join(widget->thread); + furi_thread_free(widget->thread); + widget->thread = NULL; + + furi_assert(widget->view_dispatcher); + view_dispatcher_remove_view(widget->view_dispatcher, 0); + view_dispatcher_free(widget->view_dispatcher); + widget->view_dispatcher = NULL; + + furi_record_close(RECORD_GUI); + } +} + +static void widget_callback(void* context, uint32_t arg) { + UNUSED(arg); + widget_deinit(context); +} + +static bool widget_exit(void* context) { + JsWidgetInst* widget = context; + view_dispatcher_stop(widget->view_dispatcher); + furi_timer_pending_callback(widget_callback, widget, 0); + return true; +} + +static int32_t widget_thread(void* context) { + ViewDispatcher* view_dispatcher = context; + view_dispatcher_run(view_dispatcher); + return 0; +} + +static void js_widget_show(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 0)) return; + + Gui* gui = furi_record_open(RECORD_GUI); + + widget->view_dispatcher = view_dispatcher_alloc(); + view_dispatcher_enable_queue(widget->view_dispatcher); + view_dispatcher_add_view(widget->view_dispatcher, 0, widget->view); + view_dispatcher_set_event_callback_context(widget->view_dispatcher, widget); + view_dispatcher_set_navigation_event_callback(widget->view_dispatcher, widget_exit); + view_dispatcher_attach_to_gui(widget->view_dispatcher, gui, ViewDispatcherTypeFullscreen); + view_dispatcher_switch_to_view(widget->view_dispatcher, 0); + + widget->thread = + furi_thread_alloc_ex("JsWidget", 1024, widget_thread, widget->view_dispatcher); + furi_thread_start(widget->thread); + + mjs_return(mjs, MJS_UNDEFINED); +} + +static void js_widget_close(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 0)) return; + + if(widget->thread) { + view_dispatcher_stop(widget->view_dispatcher); + widget_deinit(widget); + } + + mjs_return(mjs, MJS_UNDEFINED); +} + +static void widget_draw_callback(Canvas* canvas, void* model) { + WidgetModel* widget_model = model; + canvas_clear(canvas); + + ComponentArray_it_t it; + ComponentArray_it(it, widget_model->component); + while(!ComponentArray_end_p(it)) { + WidgetComponent* component = *ComponentArray_ref(it); + if(component->draw != NULL) { + component->draw(canvas, component->model); + } + ComponentArray_next(it); + } +} + +static void widget_remove_view(void* context) { + JsWidgetInst* widget = context; + + if(widget->view) { + with_view_model( + widget->view, + WidgetModel * model, + { + ComponentArray_it_t it; + ComponentArray_it(it, model->component); + while(!ComponentArray_end_p(it)) { + WidgetComponent* component = *ComponentArray_ref(it); + if(component->free) { + component->free(component); + component->free = NULL; + } + ComponentArray_next(it); + } + ComponentArray_reset(model->component); + ComponentArray_clear(model->component); + }, + false); + with_view_model( + widget->view, WidgetModel * model, { XbmImageList_clear(model->image); }, false); + view_free(widget->view); + widget->view = NULL; + } +} + +static JsWidgetInst* widget_alloc(void) { + JsWidgetInst* widget = malloc(sizeof(JsWidgetInst)); + widget->thread = NULL; + widget->view_dispatcher = NULL; + + widget->view = view_alloc(); + view_allocate_model(widget->view, ViewModelTypeLockFree, sizeof(WidgetModel)); + view_set_draw_callback(widget->view, widget_draw_callback); + with_view_model( + widget->view, + WidgetModel * model, + { + ComponentArray_init(model->component); + XbmImageList_init(model->image); + model->max_assigned_id = 0; + }, + true); + + return widget; +} + +static void* js_widget_create(struct mjs* mjs, mjs_val_t* object) { + JsWidgetInst* widget = widget_alloc(); + mjs_val_t widget_obj = mjs_mk_object(mjs); + mjs_set(mjs, widget_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, widget)); + // addBox(x: number, y: number, w: number, h: number): number (returns id of the added component) + mjs_set(mjs, widget_obj, "addBox", ~0, MJS_MK_FN(js_widget_add_box)); + // addCircle(x: number, y: number, r: number): number (returns id of the added component) + mjs_set(mjs, widget_obj, "addCircle", ~0, MJS_MK_FN(js_widget_add_circle)); + // addDisc(x: number, y: number, r: number): number (returns id of the added component) + mjs_set(mjs, widget_obj, "addDisc", ~0, MJS_MK_FN(js_widget_add_disc)); + // addDot(x: number, y: number): number (returns id of the added component) + mjs_set(mjs, widget_obj, "addDot", ~0, MJS_MK_FN(js_widget_add_dot)); + // addFrame(x: number, y: number, w: number, h: number): number (returns id of the added component) + mjs_set(mjs, widget_obj, "addFrame", ~0, MJS_MK_FN(js_widget_add_frame)); + // addGlyph(x: number, y: number, ch: number): number (returns id of the added component) + mjs_set(mjs, widget_obj, "addGlyph", ~0, MJS_MK_FN(js_widget_add_glyph)); + // addLine(x1: number, y1: number, x2: number, y2: number): number (returns id of the added component) + mjs_set(mjs, widget_obj, "addLine", ~0, MJS_MK_FN(js_widget_add_line)); + // addRbox(x: number, y: number, w: number, h: number, r: number): number (returns id of the added component) + mjs_set(mjs, widget_obj, "addRbox", ~0, MJS_MK_FN(js_widget_add_rbox)); + // addRframe(x: number, y: number, w: number, h: number, r: number): number (returns id of the added component) + mjs_set(mjs, widget_obj, "addRframe", ~0, MJS_MK_FN(js_widget_add_rframe)); + // addText(x: number, y: number, font: string, text: string): number (returns id of the added component) + mjs_set(mjs, widget_obj, "addText", ~0, MJS_MK_FN(js_widget_add_text)); + // addXbm(x: number, y: number, index: number): number (returns id of the added component) + mjs_set(mjs, widget_obj, "addXbm", ~0, MJS_MK_FN(js_widget_add_xbm)); + // loadImageXbm(path: string): number (returns index of the loaded image) + mjs_set(mjs, widget_obj, "loadImageXbm", ~0, MJS_MK_FN(js_widget_load_image_xbm)); + // remove(id: number): boolean (returns true if the component was removed) + mjs_set(mjs, widget_obj, "remove", ~0, MJS_MK_FN(js_widget_remove)); + // isOpen(): boolean (returns true if the widget is open) + mjs_set(mjs, widget_obj, "isOpen", ~0, MJS_MK_FN(js_widget_is_open)); + // show(): void (shows the widget) + mjs_set(mjs, widget_obj, "show", ~0, MJS_MK_FN(js_widget_show)); + // close(): void (closes the widget) + mjs_set(mjs, widget_obj, "close", ~0, MJS_MK_FN(js_widget_close)); + *object = widget_obj; + return widget; +} + +static void js_widget_destroy(void* inst) { + JsWidgetInst* widget = inst; + if(widget->thread) { + view_dispatcher_stop(widget->view_dispatcher); + widget_deinit(widget); + } + widget_remove_view(widget); + free(widget); +} + +static const JsModuleDescriptor js_widget_desc = { + "widget", + js_widget_create, + js_widget_destroy, +}; + +static const FlipperAppPluginDescriptor widget_plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_widget_desc, +}; + +const FlipperAppPluginDescriptor* js_widget_ep(void) { + return &widget_plugin_descriptor; +} diff --git a/lib/infrared/worker/infrared_worker.c b/lib/infrared/worker/infrared_worker.c index a867542e01..89f351eb92 100644 --- a/lib/infrared/worker/infrared_worker.c +++ b/lib/infrared/worker/infrared_worker.c @@ -612,7 +612,7 @@ void infrared_worker_set_raw_signal( furi_check(timings); furi_check(timings_cnt > 0); furi_check((frequency <= INFRARED_MAX_FREQUENCY) && (frequency >= INFRARED_MIN_FREQUENCY)); - furi_check((duty_cycle < 1.0f) && (duty_cycle > 0.0f)); + furi_check((duty_cycle <= 1.0f) && (duty_cycle > 0.0f)); size_t max_copy_num = COUNT_OF(instance->signal.raw.timings) - 1; furi_check(timings_cnt <= max_copy_num); diff --git a/targets/f7/furi_hal/furi_hal_flash.c b/targets/f7/furi_hal/furi_hal_flash.c index 138e07eab7..63932ac613 100644 --- a/targets/f7/furi_hal/furi_hal_flash.c +++ b/targets/f7/furi_hal/furi_hal_flash.c @@ -41,7 +41,11 @@ > If for any reason this test is never passed, this means there is a failure in the system and there is no other > way to recover than applying a device reset. */ -#define FURI_HAL_FLASH_C2_LOCK_TIMEOUT_MS (3000U) /* 3 seconds */ +// Was previously 3000U, 3 seconds +// Changing furi_assert() to furi_check() brought timeout crashes +// Internal storage is very slow, and "big" files will often cause a "timeout" with 3 seconds +// 10 seconds seems fine, the file operations complete successfully, albeit slowly +#define FURI_HAL_FLASH_C2_LOCK_TIMEOUT_MS (10000U) /* 10 seconds */ #define IS_ADDR_ALIGNED_64BITS(__VALUE__) (((__VALUE__) & 0x7U) == (0x00UL)) #define IS_FLASH_PROGRAM_ADDRESS(__VALUE__) \ diff --git a/targets/furi_hal_include/furi_hal_random.h b/targets/furi_hal_include/furi_hal_random.h index 051b6f928d..20c6c3357d 100644 --- a/targets/furi_hal_include/furi_hal_random.h +++ b/targets/furi_hal_include/furi_hal_random.h @@ -6,12 +6,16 @@ extern "C" { #endif +#define FURI_HAL_RANDOM_MAX 0xFFFFFFFF + /** Initialize random subsystem */ void furi_hal_random_init(void); /** Get random value + * furi_hal_random_get() gives up to FURI_HAL_RANDOM_MAX + * rand() and random() give up to RAND_MAX * - * @return random value + * @return 32 bit random value (up to FURI_HAL_RANDOM_MAX) */ uint32_t furi_hal_random_get(void); From 5ba6e3225b6480cd4a965db376161ddb98a18ee7 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 5 Apr 2024 02:46:04 +0300 Subject: [PATCH 4/4] allow external apps to use infrared settings by Willy-JL --- applications/main/infrared/infrared_app.c | 9 --------- applications/main/infrared/infrared_app.h | 12 ++++++++++++ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/applications/main/infrared/infrared_app.c b/applications/main/infrared/infrared_app.c index 38332021fe..346e5e640a 100644 --- a/applications/main/infrared/infrared_app.c +++ b/applications/main/infrared/infrared_app.c @@ -12,15 +12,6 @@ #define INFRARED_TX_MIN_INTERVAL_MS (50U) #define INFRARED_TASK_STACK_SIZE (2048UL) -#define INFRARED_SETTINGS_PATH EXT_PATH("infrared/.infrared.settings") -#define INFRARED_SETTINGS_VERSION (1) -#define INFRARED_SETTINGS_MAGIC (0x1F) - -typedef struct { - FuriHalInfraredTxPin tx_pin; - bool otg_enabled; -} InfraredSettings; - static const NotificationSequence* infrared_notification_sequences[InfraredNotificationMessageCount] = { &sequence_success, diff --git a/applications/main/infrared/infrared_app.h b/applications/main/infrared/infrared_app.h index a6f87402a9..6b7b8821a9 100644 --- a/applications/main/infrared/infrared_app.h +++ b/applications/main/infrared/infrared_app.h @@ -13,3 +13,15 @@ * @brief InfraredApp opaque type declaration. */ typedef struct InfraredApp InfraredApp; + +#include +#include + +#define INFRARED_SETTINGS_PATH EXT_PATH("infrared/.infrared.settings") +#define INFRARED_SETTINGS_VERSION (1) +#define INFRARED_SETTINGS_MAGIC (0x1F) + +typedef struct { + FuriHalInfraredTxPin tx_pin; + bool otg_enabled; +} InfraredSettings;