From fd93bcf3b63d3bab33f25d87c58c7970131e3ac4 Mon Sep 17 00:00:00 2001 From: Matthew <113921492+MatthewKuKanich@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:51:50 -0400 Subject: [PATCH] merging dev to findmy-battery (#99) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Notes / comments * Minor gate ID mapping corrections * add tlsf as submodule * libs: tlsf * Furi: tlsf as allocator * Furi: heap walker * shmal fixshesh * f18: tlsf * PVS: ignore tlsf * rework subghz settings, enable tx-rx state on unused gpio pin by default * SubGHz: Refactor last settings logic * better subghz settings and more anim unload fixes by Willy-JL * cleanup unused debug, set proper log levels * format * Ext power amp is always true now * JS: Add badusb layout support * js add badusb layout support by Willy-JL * fix favorites lockup * JS: Refactor storage to use array buffers * JS: Add storage copy() move() mkdir() * fix apps loading logic * RFID: Fix success title icon overlap * Sync apps * Fix build * Oops (#75 #79) * Oops (semaphore vs apilock) This reverts commit f89f775d787f51fb147175b2b9156f94e07316d2. * Desktop: Unload animations before FAP is loaded * Loader: Add API to start app detached (returns instantly, queues app start) * Desktop: Fix early animation unload deadlocks * Update symbols * Fix build and cleanup * Fix --no-build * Desktop: Unload animations before FAP is loaded * Loader: Add API to start detached (returns instantly, queues event) * Desktop: Fix early animation unload deadlocks * JS: Fix default layout handling * Storage: Default volume label DOLPHIN added to virtual storage * desktop animations unload apilock revert + js layout fix and lfrfid ui fix by Willy-JL * update readme and fix hex uppercase * update changelog * Storage: Virtual volume label is now set from Image Name * Storage: Updated virtual storage label to use strlcpy * Improve error handling, use correct drive id * Fix FATfs drive IDs (0=/ext 1=/mnt) * Cleanup import * Update of NFC emulation pict Visual edits, perspective enhancement and re-centering on y axis. -> NFC code edit to follow * Change y value for graphic asset in nfc_protocol_support.c Set y=0 for NFC_dolphin_emulation_51x64 asset so it is coherent with the other uses of this pict. * MNTM Pack - adding iButton (success) yappy asset * MNTM Pack - Adding "wait" yappy asset Note - Only used so far in : iButton / Wardriver / SimonSays * MNTM Pack - Adding Warning yappy assets (normal & flip vers.) * IR: Fix crash on duty_cycle=1 (#3568) * IR: Fix crash on duty_cycle=1 * Infrared: use float around duty_cycle Co-authored-by: あく * Furi: Add "out of memory" and "malloc(0)" crash messages (#3574) Co-authored-by: あく * Explain RNG differences, add FURI_HAL_RANDOM_MAX (#3565) * Explain RNG differences, add FURI_HAL_RANDOM_MAX * Mark FURI_HAL_RANDOM_MAX unsigned Co-authored-by: あく * MNTM Pack - Adding Save yappy asset * Move crypto1 to helpers, add it to the public API (#3567) * Move crypto1 to helpers, add it to the public API * F18 API version bump Co-authored-by: あく * MNTM Pack - Adding Success yappy asset * Re-upload of Success asset - oops Wrong sized vers previously uploaded * MNTM: Refactor device name to Spoof submenu * MNTM: Add flipper Shell Color spoofing support * Misc cleanup/refactor + passes testing * ASCII of known layouts, pass testing * I like to moving * merge upcoming changes * Parsing function readability refactor * Ble: set max connection interal same as min, kinda speedups everything * memmgr: alloc aligned, realloc * MNTM Pack - Adding Mafia (erf...) yappy asset * Remove old workflows --nobuild * Furi: distinct name for auxiliary memory pool * Furi: put idle and timer thread to mem2 * Furi: fix smal things in allocator * More cleanup, pass testing * Archive: Fix .txt file type for /any paths * MNTM Pack - Adding "done" Yappy (PI) asset :P * Update radio stack to v1.19.0 (#3545) * Update radio stack to v1.19.0 * Ble: set max connection interal same as min, kinda speedups everything * JS Documentation (#3535) * Initial JS documentation * Spelling fix Co-authored-by: あく * Fix BLE Spam #85 * Furi: remove aligned_free. Use free instead. * Desktop/Loader: Unload animations before loading FAPs (#3573) * Desktop: Unload animations before FAP is loaded * Loader: Add API to start detached (returns instantly, queues event) * Desktop: Fix early animation unload deadlocks * Loader: remove redundant event * Bump api symbols Co-authored-by: あく Co-authored-by: SG * aligned_malloc -> aligned_alloc * aligned_alloc, parameters order * Status output !TX/RX on the GDO2 CC1101 pin (#3571) * Status output !TX/RX on the GDO2 CC1101 pin\ * Fix PVS warnings Co-authored-by: あく * Fix build * update changelog * MNTM Pack - Adding IR reading yappy asset * Struct&var refactor for clarity * Only print end validity when present Same might be worth doing for type & balance, keeping those as is for now; expiry/EV specifically given the switching logic as it has most potential for misinterpretation of null/0 value * Typo * Add Dolphin 3d printing and wardriving animation (#86) * Add files via upload * Add files via upload * Update manifest.txt * Update manifest.txt * Rebalance levels * Naming consistency * Consistent cycle time --------- Co-authored-by: Willy-JL <49810075+Willy-JL@users.noreply.github.com> * fkin windows with case insensitive paths * [FL-3772] Felica poller (#3570) * New types for felica poller * New functions for felica data transmissions * Felica memory map extended with new fields * Init/deinit of mbedtls context added for felica encryption * Functions for session key and mac calculations added * Raw felica_poller implementation added * Removed MAC type parameter from check_mac function * Replaced all data fields needed for auth with context structure * Clean up felica_poller.c * Now RC block is filled with random numbers * New parameter for counting well-read blocks * Some cleanups * Felica file save and load logic added * Now we use card key from context for session key calculation * Copying card key to card block from auth context when both authentications succeeded, otherwise decrement blocks count by 1 * New felica poller event added * Moved some data structions to public namespace * FelicaAuthenticationContext struct moved to felica.h * Field type and name changed for better ones * Helper functions for felica_auth added to the app * New scene for felica card key input added * Logic for felica key input added * Auth context request processing added * Added block index definitions and replaced all index numbers with them * More macro defines * Replace nesting with do while block * New function for write operations mac calculation added * Replace nesting with do while block * Make functions static for now because they are used internally * Wrote some comments * Raw felica render implementation * New felica scenes * Adjusted felica dump rendering according design requirements * New felica scene added * Helper for switching scene during unlock added * Added warning scene and transfer to it * Moved unlock scene logic to separate files * Magic number changed * New felica render logic * Felica scenes adjusted according to design requirements * Felica poller cleanups * Some asserts added and some fixed * Replcaed asserts to checks in public api * Fixed pvs warnings in felica_poller * New event for felica_poller added for incomplete read actions * Handling of new poller event added * Update SConscript with felica files * Update api_symbols.csv with felica functions * Sync API versions Co-authored-by: あく * subghz add manually fixes * update nfc parser by zacharyweiss * update changelog * aligned_alloc: check that alignment is correct * unit test: malloc * unit tests: realloc and test with memory fragmentation * unit tests: aligned_alloc * update api * Allow setting view dispatcher callbacks to NULL * JS: Fix badusb double free crash with quit() * Sync apps - Picopass: Save as LFRFID and improvements (by bettse) - NFC Magic: Gen4 sync and fixes (by xMasterX) * Format * js fix badusb double free crash with quit() by Willy-JL * Allow no prefix usage of name_generator_make_detailed_datetime Remove extra check for NULL since check is present in code already * BLE Spam: Fix back event deadlocks * MNTM Pack - Adding RFID Receive/Send yappy graphic assets Last 2 ones of the static graphic assets ! O_oV * [FL-3750] Mf Desfire multiple file rights support (#3576) * mf desfire: remove unused type * mf desfire: continue reading after failed get free mem cmd * mf desfire: fix processing read master key settings command * mf desfire: don't read applications if they are auth protected * mf desfire: handle multiple rights * mf desfire: fix PVS warnings * mf desfire: fix print format * mf desfire: fix logs * mf classic: add send frame functions to poller * unit tests: add test from mfc crypto frame exchange * mf classic: add documentation * mf classic: fix incorrect name * target: fix api version * Allow empty prefix in name generator * BLE: Allow bonding with GapPairingNone * BadKB: Fix mac address for remember on/off * BadKB: Improve BT Remember handling * BadKB: Choose BT Pairing security mode (YesNo, PIN Type, Pin Y/N) * Impl basic favorite ability for WAVs Namely just for the sake of favorite-ing the cart lock/unlock. Requires changes to WAV player ext fap, as to be PR'd from the Momentum-Apps repo. * Update WAV fap for compatibility * Delete assets/dolphin/internal/L1_NoSd_128x49/frame_1.png * Delete assets/dolphin/internal/L1_NoSd_128x49/frame_2.png * Delete assets/dolphin/internal/L1_NoSd_128x49/frame_3.png * Delete assets/dolphin/internal/L1_NoSd_128x49/frame_4.png * Delete assets/dolphin/internal/L1_NoSd_128x49/frame_5.png * Replacing frame_0 & meta * [FL-2969] FuriHal: add ADC API (#3583) * Examples: remove unused context * FuriHal: add simple ADC API * Examples: add ADC example app * FuriHal: add extended configuration options for ADC API * FuriHal: add ADC clock configuration, fix calibration routine for single ended mode, new optimized parameters, documentation. * FuriHal: add FuriHalAdcChannelTEMPSENSOR sampling time note * FuriHal: update FuriHalAdcChannelVBAT description. * FuriHal: use insomnia while ADC is acquired. * Examples: cleanup example_adc a little bit * Sync WAV Player * [FL-3679] iButton new UI (#3471) * iButton new UI * UI final touches * Satisfy PVS Co-authored-by: Aleksandr Kutuzov * Update tv.ir (#3584) Co-authored-by: あく * Allow setting view_dispatcher callbacks to NULL again (#3580) Co-authored-by: あく * nfc app: fix false positive verification in bip plugin (#3595) Co-authored-by: あく * BLE: Add GapPairingNone support (#3596) * BLE: Add GapPairingNone support * FuriHal: cleanup naming in ble gap, remove useless config options Co-authored-by: あく * Update symbols --------- Co-authored-by: Zachary Weiss Co-authored-by: SG Co-authored-by: MX <10697207+xMasterX@users.noreply.github.com> Co-authored-by: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Co-authored-by: Nick Shaw Co-authored-by: Kuronons <110337784+Kuronons@users.noreply.github.com> Co-authored-by: あく Co-authored-by: Astra <93453568+Astrrra@users.noreply.github.com> Co-authored-by: Nikolay Minaylov Co-authored-by: Victor Nikitchuk Co-authored-by: Chonk_m <143907552+Davim09@users.noreply.github.com> Co-authored-by: RebornedBrain <138568282+RebornedBrain@users.noreply.github.com> Co-authored-by: gornekich Co-authored-by: KRukus9 <65518085+KRukus9@users.noreply.github.com> --- .github/workflows/webhook.yml | 2 - .gitmodules | 3 + .pvsoptions | 2 +- .../debug/unit_tests/furi/furi_memmgr_test.c | 262 +++++- .../debug/unit_tests/furi/furi_test.c | 2 + applications/debug/unit_tests/nfc/nfc_test.c | 127 ++- .../drivers/subghz/cc1101_ext/cc1101_ext.c | 6 +- .../examples/example_adc/application.fam | 9 + .../examples/example_adc/example_adc.c | 176 ++++ .../example_custom_font/example_custom_font.c | 2 +- .../examples/example_images/example_images.c | 2 +- applications/external | 2 +- .../main/archive/helpers/archive_browser.h | 1 + .../main/archive/helpers/archive_files.c | 8 +- .../main/archive/helpers/archive_files.h | 1 + .../archive/scenes/archive_scene_browser.c | 2 + .../main/archive/views/archive_browser_view.c | 1 + applications/main/bad_kb/bad_kb_app.c | 40 +- applications/main/bad_kb/bad_kb_app_i.h | 1 - .../main/bad_kb/helpers/ducky_script.c | 4 + .../main/bad_kb/scenes/bad_kb_scene_config.c | 51 +- .../ibutton/scenes/ibutton_scene_config.h | 1 + .../scenes/ibutton_scene_delete_confirm.c | 22 +- .../ibutton/scenes/ibutton_scene_emulate.c | 17 +- .../main/ibutton/scenes/ibutton_scene_info.c | 21 +- .../main/ibutton/scenes/ibutton_scene_read.c | 6 +- .../ibutton/scenes/ibutton_scene_read_error.c | 7 +- .../scenes/ibutton_scene_read_exit_confirm.c | 59 ++ .../scenes/ibutton_scene_read_key_menu.c | 18 +- .../scenes/ibutton_scene_read_success.c | 8 +- .../scenes/ibutton_scene_saved_key_menu.c | 8 +- .../main/ibutton/scenes/ibutton_scene_write.c | 21 +- .../lfrfid/scenes/lfrfid_scene_read_success.c | 6 +- .../lfrfid/scenes/lfrfid_scene_save_name.c | 2 +- .../scenes/momentum_app_scene_config.h | 3 +- .../scenes/momentum_app_scene_misc.c | 15 +- .../scenes/momentum_app_scene_misc_spoof.c | 77 ++ ...c => momentum_app_scene_misc_spoof_name.c} | 20 +- applications/main/nfc/helpers/felica_auth.c | 21 + applications/main/nfc/helpers/felica_auth.h | 17 + .../helpers/protocol_support/felica/felica.c | 123 ++- .../protocol_support/felica/felica_render.c | 98 ++- .../protocol_support/felica/felica_render.h | 12 + .../protocol_support/mf_desfire/mf_desfire.c | 8 +- .../mf_desfire/mf_desfire_render.c | 94 ++- .../mf_ultralight/mf_ultralight.c | 38 +- .../protocol_support/nfc_protocol_support.c | 2 +- .../nfc_protocol_support_unlock_helper.c | 38 + .../nfc_protocol_support_unlock_helper.h | 9 + applications/main/nfc/nfc_app.c | 2 + applications/main/nfc/nfc_app_i.h | 2 + .../nfc/plugins/supported_cards/all_in_one.c | 10 +- .../nfc/plugins/supported_cards/charliecard.c | 761 +++++++++++------- .../plugins/supported_cards/social_moscow.c | 4 +- .../main/nfc/scenes/nfc_scene_config.h | 2 + .../nfc/scenes/nfc_scene_felica_key_input.c | 46 ++ .../nfc/scenes/nfc_scene_felica_unlock_warn.c | 59 ++ .../scenes/nfc_scene_mf_desfire_more_info.c | 2 +- .../scenes/subghz_scene_frequency_analyzer.c | 6 - .../scenes/subghz_scene_radio_settings.c | 37 - .../subghz/scenes/subghz_scene_read_raw.c | 3 +- .../subghz/scenes/subghz_scene_receiver.c | 4 - .../scenes/subghz_scene_receiver_config.c | 13 +- .../main/subghz/scenes/subghz_scene_set_cnt.c | 4 +- .../main/subghz/scenes/subghz_scene_set_fix.c | 2 +- .../subghz/scenes/subghz_scene_set_seed.c | 2 +- .../subghz/scenes/subghz_scene_set_type.c | 8 +- applications/main/subghz/subghz.c | 7 - .../main/subghz/subghz_extended_freq.c | 3 - .../main/subghz/subghz_last_settings.c | 436 ++++------ .../main/subghz/subghz_last_settings.h | 18 +- .../subghz/views/subghz_frequency_analyzer.c | 29 +- applications/services/cli/cli_commands.c | 48 +- applications/services/desktop/desktop.c | 7 +- applications/services/desktop/desktop_i.h | 3 +- applications/services/gui/view_dispatcher.c | 3 - applications/services/loader/loader.c | 46 +- applications/services/loader/loader.h | 9 +- applications/services/loader/loader_i.h | 2 +- .../services/power/power_service/power.c | 6 +- applications/services/storage/storage.c | 4 +- applications/services/storage/storage_glue.c | 6 + applications/services/storage/storage_glue.h | 1 + .../services/storage/storages/storage_ext.c | 15 + .../examples/apps/Scripts/badusb_demo.js | 8 +- .../js_app/examples/apps/Scripts/storage.js | 22 +- .../system/js_app/modules/js_badusb.c | 54 +- .../system/js_app/modules/js_storage.c | 143 +++- .../L1_3d_printing_128x64/frame_0.png | Bin 0 -> 1449 bytes .../L1_3d_printing_128x64/frame_1.png | Bin 0 -> 1470 bytes .../L1_3d_printing_128x64/frame_10.png | Bin 0 -> 1266 bytes .../L1_3d_printing_128x64/frame_11.png | Bin 0 -> 1275 bytes .../L1_3d_printing_128x64/frame_12.png | Bin 0 -> 1302 bytes .../L1_3d_printing_128x64/frame_13.png | Bin 0 -> 1266 bytes .../L1_3d_printing_128x64/frame_14.png | Bin 0 -> 1266 bytes .../L1_3d_printing_128x64/frame_15.png | Bin 0 -> 1284 bytes .../L1_3d_printing_128x64/frame_16.png | Bin 0 -> 1284 bytes .../L1_3d_printing_128x64/frame_17.png | Bin 0 -> 1257 bytes .../L1_3d_printing_128x64/frame_18.png | Bin 0 -> 1281 bytes .../L1_3d_printing_128x64/frame_19.png | Bin 0 -> 1299 bytes .../L1_3d_printing_128x64/frame_2.png | Bin 0 -> 1269 bytes .../L1_3d_printing_128x64/frame_20.png | Bin 0 -> 1287 bytes .../L1_3d_printing_128x64/frame_21.png | Bin 0 -> 1287 bytes .../L1_3d_printing_128x64/frame_22.png | Bin 0 -> 1278 bytes .../L1_3d_printing_128x64/frame_23.png | Bin 0 -> 1314 bytes .../L1_3d_printing_128x64/frame_24.png | Bin 0 -> 1287 bytes .../L1_3d_printing_128x64/frame_25.png | Bin 0 -> 1281 bytes .../L1_3d_printing_128x64/frame_26.png | Bin 0 -> 1299 bytes .../L1_3d_printing_128x64/frame_27.png | Bin 0 -> 1293 bytes .../L1_3d_printing_128x64/frame_28.png | Bin 0 -> 1302 bytes .../L1_3d_printing_128x64/frame_29.png | Bin 0 -> 1302 bytes .../L1_3d_printing_128x64/frame_3.png | Bin 0 -> 1233 bytes .../L1_3d_printing_128x64/frame_30.png | Bin 0 -> 1293 bytes .../L1_3d_printing_128x64/frame_31.png | Bin 0 -> 1266 bytes .../L1_3d_printing_128x64/frame_32.png | Bin 0 -> 1314 bytes .../L1_3d_printing_128x64/frame_33.png | Bin 0 -> 1299 bytes .../L1_3d_printing_128x64/frame_34.png | Bin 0 -> 1293 bytes .../L1_3d_printing_128x64/frame_35.png | Bin 0 -> 1287 bytes .../L1_3d_printing_128x64/frame_36.png | Bin 0 -> 1302 bytes .../L1_3d_printing_128x64/frame_37.png | Bin 0 -> 1284 bytes .../L1_3d_printing_128x64/frame_38.png | Bin 0 -> 1308 bytes .../L1_3d_printing_128x64/frame_39.png | Bin 0 -> 1299 bytes .../L1_3d_printing_128x64/frame_4.png | Bin 0 -> 1251 bytes .../L1_3d_printing_128x64/frame_40.png | Bin 0 -> 1293 bytes .../L1_3d_printing_128x64/frame_41.png | Bin 0 -> 1269 bytes .../L1_3d_printing_128x64/frame_42.png | Bin 0 -> 1314 bytes .../L1_3d_printing_128x64/frame_43.png | Bin 0 -> 1332 bytes .../L1_3d_printing_128x64/frame_44.png | Bin 0 -> 1305 bytes .../L1_3d_printing_128x64/frame_45.png | Bin 0 -> 1287 bytes .../L1_3d_printing_128x64/frame_5.png | Bin 0 -> 1269 bytes .../L1_3d_printing_128x64/frame_6.png | Bin 0 -> 1287 bytes .../L1_3d_printing_128x64/frame_7.png | Bin 0 -> 1284 bytes .../L1_3d_printing_128x64/frame_8.png | Bin 0 -> 1284 bytes .../L1_3d_printing_128x64/frame_9.png | Bin 0 -> 1266 bytes .../external/L1_3d_printing_128x64/meta.txt | 14 + .../external/L1_Wardriving_128x64/frame_0.png | Bin 0 -> 1536 bytes .../external/L1_Wardriving_128x64/frame_1.png | Bin 0 -> 1569 bytes .../external/L1_Wardriving_128x64/frame_2.png | Bin 0 -> 1575 bytes .../external/L1_Wardriving_128x64/frame_3.png | Bin 0 -> 1587 bytes .../external/L1_Wardriving_128x64/frame_4.png | Bin 0 -> 1560 bytes .../external/L1_Wardriving_128x64/frame_5.png | Bin 0 -> 1590 bytes .../external/L1_Wardriving_128x64/frame_6.png | Bin 0 -> 1578 bytes .../external/L1_Wardriving_128x64/frame_7.png | Bin 0 -> 1539 bytes .../external/L1_Wardriving_128x64/meta.txt | 23 + assets/dolphin/external/manifest.txt | 14 + .../internal/L1_NoSd_128x49/frame_0.png | Bin 1411 -> 4374 bytes .../internal/L1_NoSd_128x49/frame_1.png | Bin 1410 -> 0 bytes .../internal/L1_NoSd_128x49/frame_2.png | Bin 1416 -> 0 bytes .../internal/L1_NoSd_128x49/frame_3.png | Bin 1415 -> 0 bytes .../internal/L1_NoSd_128x49/frame_4.png | Bin 1401 -> 0 bytes .../internal/L1_NoSd_128x49/frame_5.png | Bin 1412 -> 0 bytes .../dolphin/internal/L1_NoSd_128x49/meta.txt | 17 +- assets/icons/Dolphin/DolphinWait_59x54.png | Bin 0 -> 1539 bytes .../Icons/Dolphin/DolphinDone_80x58.png | Bin 0 -> 2602 bytes .../Icons/Dolphin/DolphinMafia_119x62.png | Bin 0 -> 2058 bytes .../Dolphin/DolphinReadingSuccess_59x63.png | Bin 0 -> 2880 bytes .../Icons/Dolphin/DolphinSaved_92x58.png | Bin 0 -> 2336 bytes .../Icons/Dolphin/DolphinSuccess_91x55.png | Bin 0 -> 3254 bytes .../Icons/Dolphin/DolphinWait_61x59.png | Bin 0 -> 3671 bytes .../Dolphin/WarningDolphinFlip_45x42.png | Bin 0 -> 2108 bytes .../Icons/Dolphin/WarningDolphin_45x42.png | Bin 0 -> 2113 bytes .../Icons/NFC/NFC_dolphin_emulation_51x64.png | Bin 2479 -> 2156 bytes .../Icons/RFID/RFIDDolphinReceive_97x61.png | Bin 0 -> 2374 bytes .../Icons/RFID/RFIDDolphinSend_97x61.png | Bin 0 -> 2378 bytes .../iButtonDolphinVerySuccess_92x55.png | Bin 0 -> 3372 bytes documentation/JavaScript.md | 1 + documentation/doxygen/index.dox | 1 + documentation/doxygen/js.dox | 18 + documentation/js/js_badusb.md | 144 ++++ documentation/js/js_builtin.md | 56 ++ documentation/js/js_data_types.md | 13 + documentation/js/js_dialog.md | 49 ++ documentation/js/js_notification.md | 36 + documentation/js/js_serial.md | 107 +++ furi/core/memmgr.c | 39 +- furi/core/memmgr.h | 25 +- furi/core/memmgr_heap.c | 746 +++++------------ furi/core/memmgr_heap.h | 12 +- furi/core/thread.c | 4 +- furi/flipper.c | 13 +- lib/SConscript | 1 + lib/ble_profile/extra_profiles/hid_profile.c | 4 +- lib/flipper_application/elf/elf_file.c | 8 +- lib/ibutton/ibutton_protocols.c | 11 + lib/ibutton/ibutton_protocols.h | 11 + lib/ibutton/protocols/dallas/dallas_common.c | 34 +- lib/ibutton/protocols/dallas/dallas_common.h | 2 + .../protocols/dallas/protocol_dallas_base.h | 1 + .../protocols/dallas/protocol_ds1971.c | 16 +- .../protocols/dallas/protocol_ds1990.c | 10 + .../protocols/dallas/protocol_ds1992.c | 16 +- .../protocols/dallas/protocol_ds1996.c | 16 +- .../protocols/dallas/protocol_ds_generic.c | 10 +- .../protocols/dallas/protocol_group_dallas.c | 19 + lib/ibutton/protocols/misc/protocol_cyfral.c | 9 + lib/ibutton/protocols/misc/protocol_metakom.c | 8 +- lib/ibutton/protocols/protocol_group_base.h | 1 + lib/momentum/momentum.h | 2 + lib/momentum/settings.c | 2 + lib/nfc/SConscript | 3 + .../mf_classic => helpers}/crypto1.c | 0 .../mf_classic => helpers}/crypto1.h | 0 lib/nfc/protocols/felica/felica.c | 211 ++++- lib/nfc/protocols/felica/felica.h | 101 +++ lib/nfc/protocols/felica/felica_poller.c | 233 +++++- lib/nfc/protocols/felica/felica_poller.h | 21 +- lib/nfc/protocols/felica/felica_poller_i.c | 107 ++- lib/nfc/protocols/felica/felica_poller_i.h | 104 ++- .../mf_classic/mf_classic_listener_i.h | 2 +- .../protocols/mf_classic/mf_classic_poller.h | 83 ++ .../mf_classic/mf_classic_poller_i.c | 74 ++ .../mf_classic/mf_classic_poller_i.h | 2 +- lib/nfc/protocols/mf_desfire/mf_desfire.h | 11 +- lib/nfc/protocols/mf_desfire/mf_desfire_i.c | 137 ++-- lib/nfc/protocols/mf_desfire/mf_desfire_i.h | 46 ++ .../protocols/mf_desfire/mf_desfire_poller.c | 22 +- .../mf_desfire/mf_desfire_poller_i.c | 65 +- .../mf_desfire/mf_desfire_poller_i.h | 2 + lib/subghz/devices/devices.c | 2 +- lib/tlsf | 1 + lib/tlsf.scons | 21 + lib/toolbox/name_generator.c | 73 +- lib/toolbox/protocols/protocol.h | 1 + targets/f18/api_symbols.csv | 24 +- targets/f18/furi_hal/furi_hal.c | 1 + targets/f18/furi_hal/furi_hal_resources.c | 168 +++- targets/f18/furi_hal/furi_hal_resources.h | 5 +- targets/f18/target.json | 3 +- targets/f7/api_symbols.csv | 62 +- targets/f7/ble_glue/app_conf.h | 6 - targets/f7/ble_glue/extra_beacon.c | 3 +- targets/f7/ble_glue/gap.c | 16 +- targets/f7/ble_glue/gap.h | 1 + targets/f7/ble_glue/profiles/serial_profile.c | 4 +- targets/f7/fatfs/sector_cache.c | 2 +- targets/f7/furi_hal/furi_hal.c | 1 + targets/f7/furi_hal/furi_hal_adc.c | 281 +++++++ targets/f7/furi_hal/furi_hal_clock.c | 1 + targets/f7/furi_hal/furi_hal_infrared.c | 6 +- targets/f7/furi_hal/furi_hal_resources.c | 90 ++- targets/f7/furi_hal/furi_hal_resources.h | 14 +- targets/f7/furi_hal/furi_hal_subghz.c | 10 - targets/f7/furi_hal/furi_hal_subghz.h | 49 +- targets/f7/furi_hal/furi_hal_version.c | 7 +- targets/f7/target.json | 1 + targets/furi_hal_include/furi_hal.h | 1 + targets/furi_hal_include/furi_hal_adc.h | 229 ++++++ targets/furi_hal_include/furi_hal_random.h | 2 +- targets/furi_hal_include/furi_hal_version.h | 1 + 249 files changed, 5260 insertions(+), 1875 deletions(-) create mode 100644 applications/examples/example_adc/application.fam create mode 100644 applications/examples/example_adc/example_adc.c create mode 100644 applications/main/ibutton/scenes/ibutton_scene_read_exit_confirm.c create mode 100644 applications/main/momentum_app/scenes/momentum_app_scene_misc_spoof.c rename applications/main/momentum_app/scenes/{momentum_app_scene_misc_rename.c => momentum_app_scene_misc_spoof_name.c} (70%) create mode 100644 applications/main/nfc/helpers/felica_auth.c create mode 100644 applications/main/nfc/helpers/felica_auth.h create mode 100644 applications/main/nfc/helpers/protocol_support/nfc_protocol_support_unlock_helper.c create mode 100644 applications/main/nfc/helpers/protocol_support/nfc_protocol_support_unlock_helper.h create mode 100644 applications/main/nfc/scenes/nfc_scene_felica_key_input.c create mode 100644 applications/main/nfc/scenes/nfc_scene_felica_unlock_warn.c create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_0.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_1.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_10.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_11.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_12.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_13.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_14.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_15.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_16.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_17.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_18.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_19.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_2.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_20.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_21.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_22.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_23.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_24.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_25.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_26.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_27.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_28.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_29.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_3.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_30.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_31.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_32.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_33.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_34.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_35.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_36.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_37.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_38.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_39.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_4.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_40.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_41.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_42.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_43.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_44.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_45.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_5.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_6.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_7.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_8.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/frame_9.png create mode 100644 assets/dolphin/external/L1_3d_printing_128x64/meta.txt create mode 100644 assets/dolphin/external/L1_Wardriving_128x64/frame_0.png create mode 100644 assets/dolphin/external/L1_Wardriving_128x64/frame_1.png create mode 100644 assets/dolphin/external/L1_Wardriving_128x64/frame_2.png create mode 100644 assets/dolphin/external/L1_Wardriving_128x64/frame_3.png create mode 100644 assets/dolphin/external/L1_Wardriving_128x64/frame_4.png create mode 100644 assets/dolphin/external/L1_Wardriving_128x64/frame_5.png create mode 100644 assets/dolphin/external/L1_Wardriving_128x64/frame_6.png create mode 100644 assets/dolphin/external/L1_Wardriving_128x64/frame_7.png create mode 100644 assets/dolphin/external/L1_Wardriving_128x64/meta.txt delete mode 100644 assets/dolphin/internal/L1_NoSd_128x49/frame_1.png delete mode 100644 assets/dolphin/internal/L1_NoSd_128x49/frame_2.png delete mode 100644 assets/dolphin/internal/L1_NoSd_128x49/frame_3.png delete mode 100644 assets/dolphin/internal/L1_NoSd_128x49/frame_4.png delete mode 100644 assets/dolphin/internal/L1_NoSd_128x49/frame_5.png create mode 100644 assets/icons/Dolphin/DolphinWait_59x54.png create mode 100644 assets/packs/Momentum/Icons/Dolphin/DolphinDone_80x58.png create mode 100644 assets/packs/Momentum/Icons/Dolphin/DolphinMafia_119x62.png create mode 100644 assets/packs/Momentum/Icons/Dolphin/DolphinReadingSuccess_59x63.png create mode 100644 assets/packs/Momentum/Icons/Dolphin/DolphinSaved_92x58.png create mode 100644 assets/packs/Momentum/Icons/Dolphin/DolphinSuccess_91x55.png create mode 100644 assets/packs/Momentum/Icons/Dolphin/DolphinWait_61x59.png create mode 100644 assets/packs/Momentum/Icons/Dolphin/WarningDolphinFlip_45x42.png create mode 100644 assets/packs/Momentum/Icons/Dolphin/WarningDolphin_45x42.png create mode 100644 assets/packs/Momentum/Icons/RFID/RFIDDolphinReceive_97x61.png create mode 100644 assets/packs/Momentum/Icons/RFID/RFIDDolphinSend_97x61.png create mode 100644 assets/packs/Momentum/Icons/iButton/iButtonDolphinVerySuccess_92x55.png create mode 100644 documentation/doxygen/js.dox create mode 100644 documentation/js/js_badusb.md create mode 100644 documentation/js/js_builtin.md create mode 100644 documentation/js/js_data_types.md create mode 100644 documentation/js/js_dialog.md create mode 100644 documentation/js/js_notification.md create mode 100644 documentation/js/js_serial.md rename lib/nfc/{protocols/mf_classic => helpers}/crypto1.c (100%) rename lib/nfc/{protocols/mf_classic => helpers}/crypto1.h (100%) create mode 160000 lib/tlsf create mode 100644 lib/tlsf.scons create mode 100644 targets/f7/furi_hal/furi_hal_adc.c create mode 100644 targets/furi_hal_include/furi_hal_adc.h diff --git a/.github/workflows/webhook.yml b/.github/workflows/webhook.yml index 7409e201e6..003442a1fb 100644 --- a/.github/workflows/webhook.yml +++ b/.github/workflows/webhook.yml @@ -7,8 +7,6 @@ on: - "Build" - "Lint" - "Release" - - "SonarCloud" - - "Submodules" types: - "completed" issues: diff --git a/.gitmodules b/.gitmodules index a52b87b855..f6bb3b18a3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -44,3 +44,6 @@ [submodule "documentation/doxygen/doxygen-awesome-css"] path = documentation/doxygen/doxygen-awesome-css url = https://github.com/jothepro/doxygen-awesome-css.git +[submodule "lib/tlsf"] + path = lib/tlsf + url = https://github.com/espressif/tlsf diff --git a/.pvsoptions b/.pvsoptions index 8606eef154..590a34de8b 100644 --- a/.pvsoptions +++ b/.pvsoptions @@ -1 +1 @@ ---ignore-ccache -C gccarm --rules-config .pvsconfig -e lib/cmsis_core -e lib/fatfs -e lib/fnv1a-hash -e lib/FreeRTOS-Kernel -e lib/heatshrink -e lib/libusb_stm32 -e lib/littlefs -e lib/mbedtls -e lib/microtar -e lib/mlib -e lib/stm32wb_cmsis -e lib/stm32wb_copro -e lib/stm32wb_hal -e lib/u8g2 -e lib/nanopb -e lib/mjs -e */arm-none-eabi/* +--ignore-ccache -C gccarm --rules-config .pvsconfig -e lib/cmsis_core -e lib/tlsf -e lib/fatfs -e lib/fnv1a-hash -e lib/FreeRTOS-Kernel -e lib/heatshrink -e lib/libusb_stm32 -e lib/littlefs -e lib/mbedtls -e lib/microtar -e lib/mlib -e lib/stm32wb_cmsis -e lib/stm32wb_copro -e lib/stm32wb_hal -e lib/u8g2 -e lib/nanopb -e lib/mjs -e */arm-none-eabi/* diff --git a/applications/debug/unit_tests/furi/furi_memmgr_test.c b/applications/debug/unit_tests/furi/furi_memmgr_test.c index 01e2c17f66..399e2d4188 100644 --- a/applications/debug/unit_tests/furi/furi_memmgr_test.c +++ b/applications/debug/unit_tests/furi/furi_memmgr_test.c @@ -1,8 +1,5 @@ #include "../minunit.h" -#include -#include -#include -#include +#include void test_furi_memmgr(void) { void* ptr; @@ -37,3 +34,260 @@ void test_furi_memmgr(void) { } free(ptr); } + +static void test_memmgr_malloc(const size_t allocation_size) { + uint8_t* ptr = NULL; + const char* error_message = NULL; + + FURI_CRITICAL_ENTER(); + + ptr = malloc(allocation_size); + + // test that we can allocate memory + if(ptr == NULL) { + error_message = "malloc failed"; + } + + // test that memory is zero-initialized after allocation + for(size_t i = 0; i < allocation_size; i++) { + if(ptr[i] != 0) { + error_message = "memory is not zero-initialized after malloc"; + break; + } + } + memset(ptr, 0x55, allocation_size); + free(ptr); + + // test that memory is zero-initialized after free + // we know that allocator can use this memory for inner purposes + // so we check that memory at least partially zero-initialized + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wuse-after-free" + + size_t zero_count = 0; + for(size_t i = 0; i < allocation_size; i++) { + if(ptr[i] == 0) { + zero_count++; + } + } + +#pragma GCC diagnostic pop + + // check that at least 75% of memory is zero-initialized + if(zero_count < (allocation_size * 0.75)) { + error_message = "seems that memory is not zero-initialized after free (malloc)"; + } + + FURI_CRITICAL_EXIT(); + + if(error_message != NULL) { + mu_fail(error_message); + } +} + +static void test_memmgr_realloc(const size_t allocation_size) { + uint8_t* ptr = NULL; + const char* error_message = NULL; + + FURI_CRITICAL_ENTER(); + + ptr = realloc(ptr, allocation_size); + + // test that we can allocate memory + if(ptr == NULL) { + error_message = "realloc(NULL) failed"; + } + + // test that memory is zero-initialized after allocation + for(size_t i = 0; i < allocation_size; i++) { + if(ptr[i] != 0) { + error_message = "memory is not zero-initialized after realloc(NULL)"; + break; + } + } + + memset(ptr, 0x55, allocation_size); + + ptr = realloc(ptr, allocation_size * 2); + + // test that we can reallocate memory + if(ptr == NULL) { + error_message = "realloc failed"; + } + + // test that memory content is preserved + for(size_t i = 0; i < allocation_size; i++) { + if(ptr[i] != 0x55) { + error_message = "memory is not reallocated after realloc"; + break; + } + } + + // test that remaining memory is zero-initialized + size_t non_zero_count = 0; + for(size_t i = allocation_size; i < allocation_size * 2; i++) { + if(ptr[i] != 0) { + non_zero_count += 1; + } + } + + // check that at most of memory is zero-initialized + // we know that allocator not always can restore content size from a pointer + // so we check against small threshold + if(non_zero_count > 4) { + error_message = "seems that memory is not zero-initialized after realloc"; + } + + uint8_t* null_ptr = realloc(ptr, 0); + + // test that we can free memory + if(null_ptr != NULL) { + error_message = "realloc(0) failed"; + } + + // test that memory is zero-initialized after realloc(0) + // we know that allocator can use this memory for inner purposes + // so we check that memory at least partially zero-initialized + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wuse-after-free" + + size_t zero_count = 0; + for(size_t i = 0; i < allocation_size; i++) { + if(ptr[i] == 0) { + zero_count++; + } + } + +#pragma GCC diagnostic pop + + // check that at least 75% of memory is zero-initialized + if(zero_count < (allocation_size * 0.75)) { + error_message = "seems that memory is not zero-initialized after realloc(0)"; + } + + FURI_CRITICAL_EXIT(); + + if(error_message != NULL) { + mu_fail(error_message); + } +} + +static void test_memmgr_alloc_aligned(const size_t allocation_size, const size_t alignment) { + uint8_t* ptr = NULL; + const char* error_message = NULL; + + FURI_CRITICAL_ENTER(); + + ptr = aligned_alloc(alignment, allocation_size); + + // test that we can allocate memory + if(ptr == NULL) { + error_message = "aligned_alloc failed"; + } + + // test that memory is aligned + if(((uintptr_t)ptr % alignment) != 0) { + error_message = "memory is not aligned after aligned_alloc"; + } + + // test that memory is zero-initialized after allocation + for(size_t i = 0; i < allocation_size; i++) { + if(ptr[i] != 0) { + error_message = "memory is not zero-initialized after aligned_alloc"; + break; + } + } + memset(ptr, 0x55, allocation_size); + free(ptr); + + // test that memory is zero-initialized after free + // we know that allocator can use this memory for inner purposes + // so we check that memory at least partially zero-initialized + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wuse-after-free" + + size_t zero_count = 0; + for(size_t i = 0; i < allocation_size; i++) { + if(ptr[i] == 0) { + zero_count++; + } + } + +#pragma GCC diagnostic pop + + // check that at least 75% of memory is zero-initialized + if(zero_count < (allocation_size * 0.75)) { + error_message = "seems that memory is not zero-initialized after free (aligned_alloc)"; + } + + FURI_CRITICAL_EXIT(); + + if(error_message != NULL) { + mu_fail(error_message); + } +} + +void test_furi_memmgr_advanced(void) { + const size_t sizes[] = {50, 100, 500, 1000, 5000, 10000}; + const size_t sizes_count = sizeof(sizes) / sizeof(sizes[0]); + const size_t alignments[] = {4, 8, 16, 32, 64, 128, 256, 512, 1024}; + const size_t alignments_count = sizeof(alignments) / sizeof(alignments[0]); + + // do test without memory fragmentation + { + for(size_t i = 0; i < sizes_count; i++) { + test_memmgr_malloc(sizes[i]); + } + + for(size_t i = 0; i < sizes_count; i++) { + test_memmgr_realloc(sizes[i]); + } + + for(size_t i = 0; i < sizes_count; i++) { + for(size_t j = 0; j < alignments_count; j++) { + test_memmgr_alloc_aligned(sizes[i], alignments[j]); + } + } + } + + // do test with memory fragmentation + { + void* blocks[sizes_count]; + void* guards[sizes_count - 1]; + + // setup guards + for(size_t i = 0; i < sizes_count; i++) { + blocks[i] = malloc(sizes[i]); + if(i < sizes_count - 1) { + guards[i] = malloc(sizes[i]); + } + } + + for(size_t i = 0; i < sizes_count; i++) { + free(blocks[i]); + } + + // do test + for(size_t i = 0; i < sizes_count; i++) { + test_memmgr_malloc(sizes[i]); + } + + for(size_t i = 0; i < sizes_count; i++) { + test_memmgr_realloc(sizes[i]); + } + + for(size_t i = 0; i < sizes_count; i++) { + for(size_t j = 0; j < alignments_count; j++) { + test_memmgr_alloc_aligned(sizes[i], alignments[j]); + } + } + + // cleanup guards + for(size_t i = 0; i < sizes_count - 1; i++) { + free(guards[i]); + } + } +} \ No newline at end of file diff --git a/applications/debug/unit_tests/furi/furi_test.c b/applications/debug/unit_tests/furi/furi_test.c index e287f9927f..e0b5916d55 100644 --- a/applications/debug/unit_tests/furi/furi_test.c +++ b/applications/debug/unit_tests/furi/furi_test.c @@ -9,6 +9,7 @@ void test_furi_concurrent_access(void); void test_furi_pubsub(void); void test_furi_memmgr(void); +void test_furi_memmgr_advanced(void); static int foo = 0; @@ -37,6 +38,7 @@ MU_TEST(mu_test_furi_memmgr) { // this test is not accurate, but gives a basic understanding // that memory management is working fine test_furi_memmgr(); + test_furi_memmgr_advanced(); } MU_TEST_SUITE(test_suite) { diff --git a/applications/debug/unit_tests/nfc/nfc_test.c b/applications/debug/unit_tests/nfc/nfc_test.c index 4b6503b728..c6304d53ca 100644 --- a/applications/debug/unit_tests/nfc/nfc_test.c +++ b/applications/debug/unit_tests/nfc/nfc_test.c @@ -7,10 +7,13 @@ #include #include #include +#include #include #include #include #include +#include +#include #include #include @@ -22,6 +25,23 @@ #define NFC_TEST_NFC_DEV_PATH EXT_PATH("unit_tests/nfc/nfc_device_test.nfc") #define NFC_APP_MF_CLASSIC_DICT_UNIT_TEST_PATH EXT_PATH("unit_tests/mf_dict.nfc") +#define NFC_TEST_FLAG_WORKER_DONE (1) + +typedef enum { + NfcTestMfClassicSendFrameTestStateAuth, + NfcTestMfClassicSendFrameTestStateReadBlock, + + NfcTestMfClassicSendFrameTestStateFail, + NfcTestMfClassicSendFrameTestStateSuccess, +} NfcTestMfClassicSendFrameTestState; + +typedef struct { + NfcTestMfClassicSendFrameTestState state; + BitBuffer* tx_buf; + BitBuffer* rx_buf; + FuriThreadId thread_id; +} NfcTestMfClassicSendFrameTest; + typedef struct { Storage* storage; } NfcTest; @@ -435,6 +455,109 @@ static void mf_classic_value_block(void) { nfc_free(poller); } +NfcCommand mf_classic_poller_send_frame_callback(NfcGenericEventEx event, void* context) { + furi_check(event.poller); + furi_check(event.parent_event_data); + furi_check(context); + + NfcCommand command = NfcCommandContinue; + MfClassicPoller* instance = event.poller; + NfcTestMfClassicSendFrameTest* frame_test = context; + Iso14443_3aPollerEvent* iso3_event = event.parent_event_data; + + MfClassicError error = MfClassicErrorNone; + if(iso3_event->type == Iso14443_3aPollerEventTypeReady) { + if(frame_test->state == NfcTestMfClassicSendFrameTestStateAuth) { + MfClassicKey key = { + .data = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + }; + error = mf_classic_poller_auth(instance, 0, &key, MfClassicKeyTypeA, NULL); + frame_test->state = (error == MfClassicErrorNone) ? + NfcTestMfClassicSendFrameTestStateReadBlock : + NfcTestMfClassicSendFrameTestStateFail; + } else if(frame_test->state == NfcTestMfClassicSendFrameTestStateReadBlock) { + do { + const uint8_t read_block_cmd[] = { + 0x30, + 0x01, + 0x8b, + 0xb9, + }; + bit_buffer_copy_bytes(frame_test->tx_buf, read_block_cmd, sizeof(read_block_cmd)); + + error = mf_classic_poller_send_encrypted_frame( + instance, frame_test->tx_buf, frame_test->rx_buf, 200000); + if(error != MfClassicErrorNone) break; + if(bit_buffer_get_size_bytes(frame_test->rx_buf) != 18) { + error = MfClassicErrorProtocol; + break; + } + + const uint8_t* rx_data = bit_buffer_get_data(frame_test->rx_buf); + const uint8_t rx_data_ref[16] = {0}; + if(memcmp(rx_data, rx_data_ref, sizeof(rx_data_ref)) != 0) { + error = MfClassicErrorProtocol; + break; + } + } while(false); + + frame_test->state = (error == MfClassicErrorNone) ? + NfcTestMfClassicSendFrameTestStateSuccess : + NfcTestMfClassicSendFrameTestStateFail; + } else if(frame_test->state == NfcTestMfClassicSendFrameTestStateSuccess) { + command = NfcCommandStop; + } else if(frame_test->state == NfcTestMfClassicSendFrameTestStateFail) { + command = NfcCommandStop; + } + } else { + frame_test->state = NfcTestMfClassicSendFrameTestStateFail; + command = NfcCommandStop; + } + + if(command == NfcCommandStop) { + furi_thread_flags_set(frame_test->thread_id, NFC_TEST_FLAG_WORKER_DONE); + } + + return command; +} + +MU_TEST(mf_classic_send_frame_test) { + Nfc* poller = nfc_alloc(); + Nfc* listener = nfc_alloc(); + + NfcDevice* nfc_device = nfc_device_alloc(); + nfc_data_generator_fill_data(NfcDataGeneratorTypeMfClassic4k_7b, nfc_device); + NfcListener* mfc_listener = nfc_listener_alloc( + listener, NfcProtocolMfClassic, nfc_device_get_data(nfc_device, NfcProtocolMfClassic)); + nfc_listener_start(mfc_listener, NULL, NULL); + + NfcPoller* mfc_poller = nfc_poller_alloc(poller, NfcProtocolMfClassic); + NfcTestMfClassicSendFrameTest context = { + .state = NfcTestMfClassicSendFrameTestStateAuth, + .thread_id = furi_thread_get_current_id(), + .tx_buf = bit_buffer_alloc(32), + .rx_buf = bit_buffer_alloc(32), + }; + nfc_poller_start_ex(mfc_poller, mf_classic_poller_send_frame_callback, &context); + + uint32_t flag = + furi_thread_flags_wait(NFC_TEST_FLAG_WORKER_DONE, FuriFlagWaitAny, FuriWaitForever); + mu_assert(flag == NFC_TEST_FLAG_WORKER_DONE, "Wrong thread flag"); + nfc_poller_stop(mfc_poller); + nfc_poller_free(mfc_poller); + + mu_assert( + context.state == NfcTestMfClassicSendFrameTestStateSuccess, "Wrong test state at the end"); + + bit_buffer_free(context.tx_buf); + bit_buffer_free(context.rx_buf); + nfc_listener_stop(mfc_listener); + nfc_listener_free(mfc_listener); + nfc_device_free(nfc_device); + nfc_free(listener); + nfc_free(poller); +} + MU_TEST(mf_classic_dict_test) { Storage* storage = furi_record_open(RECORD_STORAGE); if(storage_common_stat(storage, NFC_APP_MF_CLASSIC_DICT_UNIT_TEST_PATH, NULL) == FSE_OK) { @@ -538,11 +661,11 @@ MU_TEST_SUITE(nfc) { MU_RUN_TEST(mf_classic_1k_7b_file_test); MU_RUN_TEST(mf_classic_4k_4b_file_test); MU_RUN_TEST(mf_classic_4k_7b_file_test); - MU_RUN_TEST(mf_classic_reader); + MU_RUN_TEST(mf_classic_reader); MU_RUN_TEST(mf_classic_write); MU_RUN_TEST(mf_classic_value_block); - + MU_RUN_TEST(mf_classic_send_frame_test); MU_RUN_TEST(mf_classic_dict_test); nfc_test_free(); diff --git a/applications/drivers/subghz/cc1101_ext/cc1101_ext.c b/applications/drivers/subghz/cc1101_ext/cc1101_ext.c index 19dcf04eb2..420c9a097b 100644 --- a/applications/drivers/subghz/cc1101_ext/cc1101_ext.c +++ b/applications/drivers/subghz/cc1101_ext/cc1101_ext.c @@ -238,10 +238,7 @@ bool subghz_device_cc1101_ext_alloc(SubGhzDeviceConf* conf) { &furi_hal_spi_bus_handle_external_extra); // this is needed if multiple SPI devices are connected to the same bus but with different CS pins - if(momentum_settings.spi_cc1101_handle == SpiDefault && !furi_hal_subghz_get_ext_power_amp()) { - furi_hal_gpio_init_simple(&gpio_ext_pc3, GpioModeOutputPushPull); - furi_hal_gpio_write(&gpio_ext_pc3, true); - } else if(momentum_settings.spi_cc1101_handle == SpiExtra) { + if(momentum_settings.spi_cc1101_handle == SpiExtra) { furi_hal_gpio_init_simple(&gpio_ext_pa4, GpioModeOutputPushPull); furi_hal_gpio_write(&gpio_ext_pa4, true); } @@ -468,6 +465,7 @@ void subghz_device_cc1101_ext_rx(void) { // 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_E07M20S_AMP_GPIO, 0); diff --git a/applications/examples/example_adc/application.fam b/applications/examples/example_adc/application.fam new file mode 100644 index 0000000000..68aa2ac4d4 --- /dev/null +++ b/applications/examples/example_adc/application.fam @@ -0,0 +1,9 @@ +App( + appid="example_adc", + name="Example: ADC", + apptype=FlipperAppType.EXTERNAL, + entry_point="example_adc_main", + requires=["gui"], + stack_size=1 * 1024, + fap_category="Examples", +) diff --git a/applications/examples/example_adc/example_adc.c b/applications/examples/example_adc/example_adc.c new file mode 100644 index 0000000000..53f8d5c12d --- /dev/null +++ b/applications/examples/example_adc/example_adc.c @@ -0,0 +1,176 @@ +/** + * @file example_adc.c + * @brief ADC example. + */ +#include +#include + +#include +#include +#include + +const uint8_t font[] = + "`\2\3\2\3\4\1\2\4\5\11\0\376\6\376\7\377\1M\2\263\3\370 \6\315\364\371\6!\12\315" + "\364\201\260\35\312Q\0\42\11\315tJI\316\13\0#\14\315\264\223dP*\203R'\1$\15\315\264" + "\262A\311\266D\251l\71\0%\15\315\264\7%\61)J\42\345 \0&\14\315\264\263$\13\223\266$" + "\7\1'\10\315\364\201\60\347\10(\10\315\364\32[\313\0)\11\315\64\322b[\35\2*\12\315\264\263" + "(\222j\71\15+\11\315\364I\331\226\23\1,\10\315\364\271\205Y\10-\10\315\364\31t\26\0.\10" + "\315\364\71\346(\0/\14\315\364\221\60\13\263\60\13C\0\60\13\315\264\245Jb)E:\12\61\12\315" + "\364\201Ll\333A\0\62\12\315\264\245bV\33r\20\63\13\315\264\245Z\232D\221\216\2\64\14\315\364" + "\201LJ\242!\313v\20\65\14\315t\207$\134\223(\322Q\0\66\13\315\264\245p\252D\221\216\2\67" + "\12\315t\207\60+\326a\0\70\13\315\264\245\222T\211\42\35\5\71\13\315\264\245J\24\215\221\216\2:" + "\11\315\364i\71!G\1;\12\315\364I\71!\314B\0<\11\315\364\341\254Z\7\1=\12\315\364)" + "C<\344$\0>\11\315\364\301\264V\207\1\77\12\315\264\245Z\35\312a\0@\14\315\264\245J\242$" + "J\272\203\0A\15\315\264\245J\224\14I\224D\71\10B\13\315t\247\312T\211\222\35\5C\12\315\264" + "\245JX\212t\24D\15\315t\247J\224DI\224\354(\0E\14\315t\207$\234\302p\310A\0F" + "\12\315t\207$\234\302:\1G\14\315\264\245J\230(Q\244\243\0H\17\315t\243$J\206$J\242" + "$\312A\0I\11\315\264\267\260m\7\1J\12\315\364\221\260%\212t\24K\14\315t\243\244\244iI" + "T\7\1L\11\315t\303\216C\16\2M\17\315t\243dH\206$J\242$\312A\0N\16\315t\243" + "D\251(Q\22%Q\16\2O\15\315\264\245J\224DI\24\351(\0P\12\315t\247J\224LaN" + "Q\15\315\264\245J\224DI\42\251\61\0R\14\315t\247J\224L\225(\7\1S\13\315\264\245\222\232" + "D\221\216\2T\10\315\264\267\260;\12U\16\315t\243$J\242$J\242HG\1V\15\315t\243$" + "J\242$Jj\71\14W\17\315t\243$J\242dH\206$\312A\0X\15\315t\243$\212\64\251\22" + "\345 \0Y\13\315t\243$Jja\35\6Z\12\315t\207\60k\34r\20[\10\315\264\264\260G\31" + "\134\12\315\264\303\64L\303\64\14]\10\315t\304\276\351\0^\11\315\364\201,\311\271\1_\7\315\364y" + "\35\4`\10\315t\322\234'\0a\14\315\364IK\224$R\222\203\0b\13\315t\303p\252D\311\216" + "\2c\12\315\364IR%\335A\0d\14\315\364\221\60Z\242$\212v\20e\12\315\364I\322\220\244;" + "\10f\12\315\364\221,\333\302:\12g\14\315\364IK\224D\321\30I\0h\14\315t\303p\252DI" + "\224\203\0i\12\315\364\201\34\21k;\10j\12\315\364\201\34\21\273e\0k\13\315t\303J\244%Q" + "\35\4l\10\315\264\305n;\10m\14\315\364)CRQ\22\245\216\1n\13\315\364)%\245\224D\71" + "\10o\12\315\364IR%\212t\24p\13\315\364)S%J\246\60\4q\13\315\364IK\224D\321X" + "\1r\11\315\364)%\245\230\23s\12\315\364I\313\232\354(\0t\13\315\364\201\60\333\302\64\7\1u" + "\15\315\364)Q\22%\211\224\344 \0v\13\315\364)Q\22%\265\34\6w\13\315\364)\25%Q\272" + "\203\0x\12\315\364)Q\244Iu\20y\15\315\364)Q\22%Q\64F\22\0z\12\315\364)CV" + "\33r\20{\12\315\364\212\265\64\254&\0|\7\315\264\302~\7}\12\315t\322\260\232\205\265\14~\11" + "\315\364II;\13\0\177\6\315\364\371\6\0\0\0\4\377\377\0"; + +#define FONT_HEIGHT (8u) + +typedef float (*ValueConverter)(FuriHalAdcHandle* handle, uint16_t value); + +typedef struct { + const GpioPinRecord* pin; + float value; + ValueConverter converter; + const char* suffix; +} DataItem; + +typedef struct { + size_t count; + DataItem* items; +} Data; + +const GpioPinRecord item_vref = {.name = "VREF", .channel = FuriHalAdcChannelVREFINT}; +const GpioPinRecord item_temp = {.name = "TEMP", .channel = FuriHalAdcChannelTEMPSENSOR}; +const GpioPinRecord item_vbat = {.name = "VBAT", .channel = FuriHalAdcChannelVBAT}; + +static void app_draw_callback(Canvas* canvas, void* ctx) { + furi_assert(ctx); + Data* data = ctx; + + canvas_set_custom_u8g2_font(canvas, font); + char buffer[64]; + int32_t x = 0, y = FONT_HEIGHT; + for(size_t i = 0; i < data->count; i++) { + if(i == canvas_height(canvas) / FONT_HEIGHT) { + x = 64; + y = FONT_HEIGHT; + } + + snprintf( + buffer, + sizeof(buffer), + "%4s: %4.0f%s\n", + data->items[i].pin->name, + (double)data->items[i].value, + data->items[i].suffix); + canvas_draw_str(canvas, x, y, buffer); + y += FONT_HEIGHT; + } +} + +static void app_input_callback(InputEvent* input_event, void* ctx) { + furi_assert(ctx); + FuriMessageQueue* event_queue = ctx; + furi_message_queue_put(event_queue, input_event, FuriWaitForever); +} + +int32_t example_adc_main(void* p) { + UNUSED(p); + + // Data + Data data = {}; + for(size_t i = 0; i < gpio_pins_count; i++) { + if(gpio_pins[i].channel != FuriHalAdcChannelNone) { + data.count++; + } + } + data.count += 3; // Special channels + data.items = malloc(data.count * sizeof(DataItem)); + size_t item_pos = 0; + for(size_t i = 0; i < gpio_pins_count; i++) { + if(gpio_pins[i].channel != FuriHalAdcChannelNone) { + furi_hal_gpio_init(gpio_pins[i].pin, GpioModeAnalog, GpioPullNo, GpioSpeedLow); + data.items[item_pos].pin = &gpio_pins[i]; + data.items[item_pos].converter = furi_hal_adc_convert_to_voltage; + data.items[item_pos].suffix = "mV"; + item_pos++; + } + } + data.items[item_pos].pin = &item_vref; + data.items[item_pos].converter = furi_hal_adc_convert_vref; + data.items[item_pos].suffix = "mV"; + item_pos++; + data.items[item_pos].pin = &item_temp; + data.items[item_pos].converter = furi_hal_adc_convert_temp; + data.items[item_pos].suffix = "C"; + item_pos++; + data.items[item_pos].pin = &item_vbat; + data.items[item_pos].converter = furi_hal_adc_convert_vbat; + data.items[item_pos].suffix = "mV"; + item_pos++; + furi_assert(item_pos == data.count); + + // Alloc message queue + FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); + + // Configure view port + ViewPort* view_port = view_port_alloc(); + view_port_draw_callback_set(view_port, app_draw_callback, &data); + view_port_input_callback_set(view_port, app_input_callback, event_queue); + + // Register view port in GUI + Gui* gui = furi_record_open(RECORD_GUI); + gui_add_view_port(gui, view_port, GuiLayerFullscreen); + + // Initialize ADC + FuriHalAdcHandle* adc_handle = furi_hal_adc_acquire(); + furi_hal_adc_configure(adc_handle); + + // Process events + InputEvent event; + bool running = true; + while(running) { + if(furi_message_queue_get(event_queue, &event, 100) == FuriStatusOk) { + if(event.type == InputTypePress && event.key == InputKeyBack) { + running = false; + } + } else { + for(size_t i = 0; i < data.count; i++) { + data.items[i].value = data.items[i].converter( + adc_handle, furi_hal_adc_read(adc_handle, data.items[i].pin->channel)); + } + view_port_update(view_port); + } + } + + furi_hal_adc_release(adc_handle); + view_port_enabled_set(view_port, false); + gui_remove_view_port(gui, view_port); + view_port_free(view_port); + furi_message_queue_free(event_queue); + furi_record_close(RECORD_GUI); + free(data.items); + + return 0; +} diff --git a/applications/examples/example_custom_font/example_custom_font.c b/applications/examples/example_custom_font/example_custom_font.c index 405db46e30..a175838e31 100644 --- a/applications/examples/example_custom_font/example_custom_font.c +++ b/applications/examples/example_custom_font/example_custom_font.c @@ -94,7 +94,7 @@ int32_t example_custom_font_main(void* p) { // Configure view port ViewPort* view_port = view_port_alloc(); - view_port_draw_callback_set(view_port, app_draw_callback, view_port); + view_port_draw_callback_set(view_port, app_draw_callback, NULL); view_port_input_callback_set(view_port, app_input_callback, event_queue); // Register view port in GUI diff --git a/applications/examples/example_images/example_images.c b/applications/examples/example_images/example_images.c index ba9a354f60..60269a81f7 100644 --- a/applications/examples/example_images/example_images.c +++ b/applications/examples/example_images/example_images.c @@ -39,7 +39,7 @@ int32_t example_images_main(void* p) { // Configure view port ViewPort* view_port = view_port_alloc(); - view_port_draw_callback_set(view_port, app_draw_callback, view_port); + view_port_draw_callback_set(view_port, app_draw_callback, NULL); view_port_input_callback_set(view_port, app_input_callback, event_queue); // Register view port in GUI diff --git a/applications/external b/applications/external index 5890d08342..68b1a9af7c 160000 --- a/applications/external +++ b/applications/external @@ -1 +1 @@ -Subproject commit 5890d08342cf621f5dba8160b8ee87d53c055988 +Subproject commit 68b1a9af7c8ff1cdb3b9bb269fab014810376f22 diff --git a/applications/main/archive/helpers/archive_browser.h b/applications/main/archive/helpers/archive_browser.h index 6966dc8de5..be5c6a4d0e 100644 --- a/applications/main/archive/helpers/archive_browser.h +++ b/applications/main/archive/helpers/archive_browser.h @@ -33,6 +33,7 @@ static const char* known_ext[] = { [ArchiveFileTypeSubghzRemote] = ".txt", [ArchiveFileTypeInfraredRemote] = ".txt", [ArchiveFileTypeBadKb] = ".txt", + [ArchiveFileTypeWAV] = ".wav", [ArchiveFileTypeU2f] = "?", [ArchiveFileTypeApplication] = ".fap", [ArchiveFileTypeJS] = ".js", diff --git a/applications/main/archive/helpers/archive_files.c b/applications/main/archive/helpers/archive_files.c index 10b89c3b5c..3406ceea90 100644 --- a/applications/main/archive/helpers/archive_files.c +++ b/applications/main/archive/helpers/archive_files.c @@ -36,7 +36,13 @@ void archive_set_file_type(ArchiveFile_t* file, const char* path, bool is_folder txt_path = archive_get_default_path(ArchiveTabBadKb); break; } - if(txt_path != NULL && furi_string_start_with_str(file->path, txt_path)) { + if(txt_path != NULL) { + size_t len = strlen(txt_path); + if(furi_string_size(file->path) < len) continue; + // Compare but ignore /ext or /any, continue if different (memcmp() != 0) + if(memcmp(furi_string_get_cstr(file->path) + 4, txt_path + 4, len - 4)) { + continue; + } file->type = i; return; } diff --git a/applications/main/archive/helpers/archive_files.h b/applications/main/archive/helpers/archive_files.h index 61232f9434..2872105ef7 100644 --- a/applications/main/archive/helpers/archive_files.h +++ b/applications/main/archive/helpers/archive_files.h @@ -19,6 +19,7 @@ typedef enum { ArchiveFileTypeSubghzRemote, ArchiveFileTypeInfraredRemote, ArchiveFileTypeBadKb, + ArchiveFileTypeWAV, ArchiveFileTypeU2f, ArchiveFileTypeApplication, ArchiveFileTypeJS, diff --git a/applications/main/archive/scenes/archive_scene_browser.c b/applications/main/archive/scenes/archive_scene_browser.c index 3f37990d11..6942144aa4 100644 --- a/applications/main/archive/scenes/archive_scene_browser.c +++ b/applications/main/archive/scenes/archive_scene_browser.c @@ -32,6 +32,8 @@ const char* archive_get_flipper_app_name(ArchiveFileTypeEnum file_type) { return EXT_PATH("apps/Infrared/ir_remote.fap"); case ArchiveFileTypeBadKb: return "Bad KB"; + case ArchiveFileTypeWAV: + return EXT_PATH("apps/Media/wav_player.fap"); case ArchiveFileTypeU2f: return "U2F"; case ArchiveFileTypeUpdateManifest: diff --git a/applications/main/archive/views/archive_browser_view.c b/applications/main/archive/views/archive_browser_view.c index c29dec9476..8894496e89 100644 --- a/applications/main/archive/views/archive_browser_view.c +++ b/applications/main/archive/views/archive_browser_view.c @@ -34,6 +34,7 @@ static const Icon* ArchiveItemIcons[] = { [ArchiveFileTypeSubghzRemote] = &I_subrem_10px, [ArchiveFileTypeInfraredRemote] = &I_ir_scope_10px, [ArchiveFileTypeBadKb] = &I_badkb_10px, + [ArchiveFileTypeWAV] = &I_music_10px, [ArchiveFileTypeU2f] = &I_u2f_10px, [ArchiveFileTypeApplication] = &I_Apps_10px, [ArchiveFileTypeJS] = &I_js_script_10px, diff --git a/applications/main/bad_kb/bad_kb_app.c b/applications/main/bad_kb/bad_kb_app.c index 551eb24a28..21e160264d 100644 --- a/applications/main/bad_kb/bad_kb_app.c +++ b/applications/main/bad_kb/bad_kb_app.c @@ -36,6 +36,7 @@ void bad_kb_load_settings(BadKbApp* app) { FlipperFormat* file = flipper_format_file_alloc(storage); if(flipper_format_file_open_existing(file, BAD_KB_SETTINGS_PATH)) { FuriString* tmp_str = furi_string_alloc(); + uint32_t tmp_uint = 0; if(!flipper_format_read_string(file, "Keyboard_Layout", app->keyboard_layout)) { furi_string_reset(app->keyboard_layout); @@ -47,11 +48,17 @@ void bad_kb_load_settings(BadKbApp* app) { flipper_format_rewind(file); } - if(!flipper_format_read_bool(file, "Bt_Remember", &app->bt_remember, 1)) { - app->bt_remember = false; + if(!flipper_format_read_bool(file, "Bt_Remember", &cfg->ble.bonding, 1)) { + cfg->ble.bonding = false; flipper_format_rewind(file); } + if(!flipper_format_read_uint32(file, "Bt_Pairing", &tmp_uint, 1)) { + tmp_uint = GapPairingNone; + flipper_format_rewind(file); + } + cfg->ble.pairing = tmp_uint; + if(flipper_format_read_string(file, "Bt_Name", tmp_str)) { strlcpy(cfg->ble.name, furi_string_get_cstr(tmp_str), sizeof(cfg->ble.name)); } else { @@ -115,9 +122,12 @@ static void bad_kb_save_settings(BadKbApp* app) { Storage* storage = furi_record_open(RECORD_STORAGE); FlipperFormat* file = flipper_format_file_alloc(storage); if(flipper_format_file_open_always(file, BAD_KB_SETTINGS_PATH)) { + uint32_t tmp_uint = 0; flipper_format_write_string(file, "Keyboard_Layout", app->keyboard_layout); flipper_format_write_bool(file, "Is_Bt", &app->is_bt, 1); - flipper_format_write_bool(file, "Bt_Remember", &app->bt_remember, 1); + flipper_format_write_bool(file, "Bt_Remember", &cfg->ble.bonding, 1); + tmp_uint = cfg->ble.pairing; + flipper_format_write_uint32(file, "Bt_Pairing", &tmp_uint, 1); flipper_format_write_string_cstr(file, "Bt_Name", cfg->ble.name); flipper_format_write_hex(file, "Bt_Mac", (uint8_t*)&cfg->ble.mac, sizeof(cfg->ble.mac)); flipper_format_write_string_cstr(file, "Usb_Manuf", cfg->usb.manuf); @@ -143,22 +153,21 @@ void bad_kb_app_show_loading_popup(BadKbApp* app, bool show) { int32_t bad_kb_conn_apply(BadKbApp* app) { if(app->is_bt) { - bt_timeout = bt_hid_delays[LevelRssi39_0]; - bt_disconnect(app->bt); - furi_delay_ms(200); - bt_keys_storage_set_storage_path(app->bt, BAD_KB_KEYS_PATH); - - // Setup new config + // Setup profile config BadKbConfig* cfg = app->set_bt_id ? &app->id_config : &app->config; memcpy(&app->cur_ble_cfg, &cfg->ble, sizeof(cfg->ble)); - app->cur_ble_cfg.bonding = app->bt_remember; - if(app->bt_remember) { - app->cur_ble_cfg.pairing = GapPairingPinCodeVerifyYesNo; - } else { - app->cur_ble_cfg.pairing = GapPairingNone; + if(app->cur_ble_cfg.bonding) { + // Hardcode mac for remember mode + // Change in config copy to preserve user choice for non-remember mode memcpy(app->cur_ble_cfg.mac, BAD_KB_BOUND_MAC, sizeof(BAD_KB_BOUND_MAC)); } + // Prepare for new profile + bt_timeout = bt_hid_delays[LevelRssi39_0]; + bt_disconnect(app->bt); + furi_delay_ms(200); + bt_keys_storage_set_storage_path(app->bt, BAD_KB_KEYS_PATH); + // Set profile app->ble_hid = bt_profile_start(app->bt, ble_profile_hid, &app->cur_ble_cfg); furi_check(app->ble_hid); @@ -246,7 +255,8 @@ void bad_kb_config_refresh(BadKbApp* app) { bad_kb_conn_reset(app); } else { BleProfileHidParams* cur = &app->cur_ble_cfg; - apply = apply || cfg->ble.bonding != app->bt_remember; + apply = apply || cfg->ble.bonding != cur->bonding; + apply = apply || cfg->ble.pairing != cur->pairing; apply = apply || strncmp(cfg->ble.name, cur->name, sizeof(cfg->ble.name)); apply = apply || memcmp(cfg->ble.mac, cur->mac, sizeof(cfg->ble.mac)); } diff --git a/applications/main/bad_kb/bad_kb_app_i.h b/applications/main/bad_kb/bad_kb_app_i.h index e009f0e602..a7d3844ace 100644 --- a/applications/main/bad_kb/bad_kb_app_i.h +++ b/applications/main/bad_kb/bad_kb_app_i.h @@ -72,7 +72,6 @@ struct BadKbApp { Bt* bt; bool is_bt; - bool bt_remember; BadKbConfig config; // User options BadKbConfig id_config; // ID and BT_ID values diff --git a/applications/main/bad_kb/helpers/ducky_script.c b/applications/main/bad_kb/helpers/ducky_script.c index 4e7e48e223..925bce5d45 100644 --- a/applications/main/bad_kb/helpers/ducky_script.c +++ b/applications/main/bad_kb/helpers/ducky_script.c @@ -344,6 +344,10 @@ static bool ducky_set_bt_id(BadKbScript* bad_kb, const char* line) { strlcpy(cfg->ble.name, line + mac_len, sizeof(cfg->ble.name)); FURI_LOG_D(WORKER_TAG, "set bt id: %s", line); + + // Can't set bonding and pairing via BT_ID, sync with user choice instead + cfg->ble.bonding = bad_kb->app->config.ble.bonding; + cfg->ble.pairing = bad_kb->app->config.ble.pairing; return true; } diff --git a/applications/main/bad_kb/scenes/bad_kb_scene_config.c b/applications/main/bad_kb/scenes/bad_kb_scene_config.c index cc7cefdc02..52e12af9cd 100644 --- a/applications/main/bad_kb/scenes/bad_kb_scene_config.c +++ b/applications/main/bad_kb/scenes/bad_kb_scene_config.c @@ -8,6 +8,7 @@ enum VarItemListIndex { enum VarItemListIndexBt { VarItemListIndexBtRemember = VarItemListIndexConnection + 1, + VarItemListIndexBtPairing, VarItemListIndexBtDeviceName, VarItemListIndexBtMacAddress, VarItemListIndexBtRandomizeMac, @@ -29,11 +30,35 @@ void bad_kb_scene_config_connection_callback(VariableItem* item) { void bad_kb_scene_config_bt_remember_callback(VariableItem* item) { BadKbApp* bad_kb = variable_item_get_context(item); - bad_kb->bt_remember = variable_item_get_current_value_index(item); - variable_item_set_current_value_text(item, bad_kb->bt_remember ? "ON" : "OFF"); + bool value = variable_item_get_current_value_index(item); + // Set user config and remember + bad_kb->config.ble.bonding = value; + // Apply to ID config so its temporarily overridden (currently can't set bonding with BT_ID anyway) + if(bad_kb->set_bt_id) { + bad_kb->id_config.ble.bonding = value; + } + variable_item_set_current_value_text(item, value ? "ON" : "OFF"); view_dispatcher_send_custom_event(bad_kb->view_dispatcher, VarItemListIndexBtRemember); } +const char* const bt_pairing_names[GapPairingCount] = { + "YesNo", + "PIN Type", + "PIN Y/N", +}; +void bad_kb_scene_config_bt_pairing_callback(VariableItem* item) { + BadKbApp* bad_kb = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + // Set user config and remember + bad_kb->config.ble.pairing = index; + // Apply to ID config so its temporarily overridden (currently can't set pairing with BT_ID anyway) + if(bad_kb->set_bt_id) { + bad_kb->id_config.ble.pairing = index; + } + variable_item_set_current_value_text(item, bt_pairing_names[index]); + view_dispatcher_send_custom_event(bad_kb->view_dispatcher, VarItemListIndexBtPairing); +} + void bad_kb_scene_config_var_item_list_callback(void* context, uint32_t index) { BadKbApp* bad_kb = context; view_dispatcher_send_custom_event(bad_kb->view_dispatcher, index); @@ -52,20 +77,31 @@ void bad_kb_scene_config_on_enter(void* context) { variable_item_set_current_value_text(item, bad_kb->is_bt ? "BT" : "USB"); if(bad_kb->is_bt) { + BadKbConfig* cfg = bad_kb->set_bt_id ? &bad_kb->id_config : &bad_kb->config; + item = variable_item_list_add( var_item_list, "BT Remember", 2, bad_kb_scene_config_bt_remember_callback, bad_kb); - variable_item_set_current_value_index(item, bad_kb->bt_remember); - variable_item_set_current_value_text(item, bad_kb->bt_remember ? "ON" : "OFF"); + variable_item_set_current_value_index(item, cfg->ble.bonding); + variable_item_set_current_value_text(item, cfg->ble.bonding ? "ON" : "OFF"); + + item = variable_item_list_add( + var_item_list, + "BT Pairing", + GapPairingCount, + bad_kb_scene_config_bt_pairing_callback, + bad_kb); + variable_item_set_current_value_index(item, cfg->ble.pairing); + variable_item_set_current_value_text(item, bt_pairing_names[cfg->ble.pairing]); item = variable_item_list_add(var_item_list, "BT Device Name", 0, NULL, bad_kb); item = variable_item_list_add(var_item_list, "BT MAC Address", 0, NULL, bad_kb); - if(bad_kb->bt_remember) { + if(cfg->ble.bonding) { variable_item_set_locked(item, true, "Remember\nmust be Off!"); } item = variable_item_list_add(var_item_list, "Randomize BT MAC", 0, NULL, bad_kb); - if(bad_kb->bt_remember) { + if(cfg->ble.bonding) { variable_item_set_locked(item, true, "Remember\nmust be Off!"); } } else { @@ -109,6 +145,9 @@ bool bad_kb_scene_config_on_event(void* context, SceneManagerEvent event) { case VarItemListIndexBtRemember: bad_kb_config_refresh(bad_kb); break; + case VarItemListIndexBtPairing: + bad_kb_config_refresh(bad_kb); + break; case VarItemListIndexBtDeviceName: scene_manager_next_scene(bad_kb->scene_manager, BadKbSceneConfigBtName); break; diff --git a/applications/main/ibutton/scenes/ibutton_scene_config.h b/applications/main/ibutton/scenes/ibutton_scene_config.h index 79f6791b3b..f3fd0fff77 100644 --- a/applications/main/ibutton/scenes/ibutton_scene_config.h +++ b/applications/main/ibutton/scenes/ibutton_scene_config.h @@ -17,5 +17,6 @@ ADD_SCENE(ibutton, delete_confirm, DeleteConfirm) ADD_SCENE(ibutton, delete_success, DeleteSuccess) ADD_SCENE(ibutton, retry_confirm, RetryConfirm) ADD_SCENE(ibutton, exit_confirm, ExitConfirm) +ADD_SCENE(ibutton, read_exit_confirm, ReadExitConfirm) ADD_SCENE(ibutton, view_data, ViewData) ADD_SCENE(ibutton, rpc, Rpc) diff --git a/applications/main/ibutton/scenes/ibutton_scene_delete_confirm.c b/applications/main/ibutton/scenes/ibutton_scene_delete_confirm.c index b293af952c..a7eba672a3 100644 --- a/applications/main/ibutton/scenes/ibutton_scene_delete_confirm.c +++ b/applications/main/ibutton/scenes/ibutton_scene_delete_confirm.c @@ -7,23 +7,33 @@ void ibutton_scene_delete_confirm_on_enter(void* context) { Widget* widget = ibutton->widget; FuriString* tmp = furi_string_alloc(); + FuriString* uid = furi_string_alloc(); widget_add_button_element(widget, GuiButtonTypeLeft, "Back", ibutton_widget_callback, context); widget_add_button_element( widget, GuiButtonTypeRight, "Delete", ibutton_widget_callback, context); - furi_string_printf(tmp, "\e#Delete %s?\e#", ibutton->key_name); + furi_string_printf(tmp, "\e#Delete %s?\e#\n", ibutton->key_name); + + ibutton_protocols_render_uid(ibutton->protocols, key, uid); + + furi_string_cat_printf( + uid, + "\n%s %s", + ibutton_protocols_get_manufacturer(ibutton->protocols, ibutton_key_get_protocol_id(key)), + ibutton_protocols_get_name(ibutton->protocols, ibutton_key_get_protocol_id(key))); + + furi_string_cat(tmp, uid); + widget_add_text_box_element( - widget, 0, 0, 128, 23, AlignCenter, AlignCenter, furi_string_get_cstr(tmp), false); + widget, 0, 0, 128, 64, AlignCenter, AlignTop, furi_string_get_cstr(tmp), false); furi_string_reset(tmp); - ibutton_protocols_render_brief_data(ibutton->protocols, key, tmp); - - widget_add_string_multiline_element( - widget, 128 / 2, 24, AlignCenter, AlignTop, FontSecondary, furi_string_get_cstr(tmp)); + furi_string_reset(uid); view_dispatcher_switch_to_view(ibutton->view_dispatcher, iButtonViewWidget); furi_string_free(tmp); + furi_string_free(uid); } bool ibutton_scene_delete_confirm_on_event(void* context, SceneManagerEvent event) { diff --git a/applications/main/ibutton/scenes/ibutton_scene_emulate.c b/applications/main/ibutton/scenes/ibutton_scene_emulate.c index 3cfddb8d04..070f229109 100644 --- a/applications/main/ibutton/scenes/ibutton_scene_emulate.c +++ b/applications/main/ibutton/scenes/ibutton_scene_emulate.c @@ -21,17 +21,20 @@ void ibutton_scene_emulate_on_enter(void* context) { widget_add_icon_element(widget, 3, 10, &I_iButtonKey_49x44); - furi_string_printf( - tmp, - "[%s]\n%s", - ibutton_protocols_get_name(ibutton->protocols, ibutton_key_get_protocol_id(key)), - furi_string_empty(ibutton->file_path) ? "Unsaved Key" : ibutton->key_name); + if(furi_string_empty(ibutton->file_path)) { + furi_string_printf( + tmp, + "Unsaved\n%s", + ibutton_protocols_get_name(ibutton->protocols, ibutton_key_get_protocol_id(key))); + } else { + furi_string_printf(tmp, "%s", ibutton->key_name); + } widget_add_text_box_element( - widget, 52, 30, 75, 40, AlignCenter, AlignCenter, furi_string_get_cstr(tmp), true); + widget, 52, 23, 75, 26, AlignCenter, AlignTop, furi_string_get_cstr(tmp), false); widget_add_string_multiline_element( - widget, 88, 5, AlignCenter, AlignTop, FontPrimary, "iButton\nemulating"); + widget, 88, 10, AlignCenter, AlignTop, FontPrimary, "Emulating"); ibutton_worker_emulate_set_callback(ibutton->worker, ibutton_scene_emulate_callback, ibutton); ibutton_worker_emulate_start(ibutton->worker, key); diff --git a/applications/main/ibutton/scenes/ibutton_scene_info.c b/applications/main/ibutton/scenes/ibutton_scene_info.c index 50dc328a5f..ba72de6ca7 100644 --- a/applications/main/ibutton/scenes/ibutton_scene_info.c +++ b/applications/main/ibutton/scenes/ibutton_scene_info.c @@ -8,19 +8,24 @@ void ibutton_scene_info_on_enter(void* context) { const iButtonProtocolId protocol_id = ibutton_key_get_protocol_id(key); FuriString* tmp = furi_string_alloc(); - FuriString* keynumber = furi_string_alloc(); - - ibutton_protocols_render_brief_data(ibutton->protocols, key, keynumber); + FuriString* brief_data = furi_string_alloc(); furi_string_printf( tmp, - "\e#%s\n[%s]\e#\n%s", + "Name:%s\n\e#%s %s\e#\n", ibutton->key_name, - ibutton_protocols_get_name(ibutton->protocols, protocol_id), - furi_string_get_cstr(keynumber)); + ibutton_protocols_get_manufacturer(ibutton->protocols, protocol_id), + ibutton_protocols_get_name(ibutton->protocols, protocol_id)); + + ibutton_protocols_render_brief_data(ibutton->protocols, key, brief_data); + + furi_string_cat(tmp, brief_data); widget_add_text_box_element( - widget, 0, 2, 128, 64, AlignLeft, AlignTop, furi_string_get_cstr(tmp), true); + widget, 0, 0, 128, 64, AlignLeft, AlignTop, furi_string_get_cstr(tmp), false); + + furi_string_reset(tmp); + furi_string_reset(brief_data); if(ibutton_protocols_get_features(ibutton->protocols, protocol_id) & iButtonProtocolFeatureExtData) { @@ -30,7 +35,7 @@ void ibutton_scene_info_on_enter(void* context) { view_dispatcher_switch_to_view(ibutton->view_dispatcher, iButtonViewWidget); furi_string_free(tmp); - furi_string_free(keynumber); + furi_string_free(brief_data); } bool ibutton_scene_info_on_event(void* context, SceneManagerEvent event) { diff --git a/applications/main/ibutton/scenes/ibutton_scene_read.c b/applications/main/ibutton/scenes/ibutton_scene_read.c index f360c3ac43..f490488f5f 100644 --- a/applications/main/ibutton/scenes/ibutton_scene_read.c +++ b/applications/main/ibutton/scenes/ibutton_scene_read.c @@ -12,9 +12,9 @@ void ibutton_scene_read_on_enter(void* context) { iButtonKey* key = ibutton->key; iButtonWorker* worker = ibutton->worker; - popup_set_header(popup, "iButton", 95, 26, AlignCenter, AlignBottom); - popup_set_text(popup, "Apply key to\nFlipper's back", 95, 30, AlignCenter, AlignTop); - popup_set_icon(popup, 0, 5, &I_DolphinWait_61x59); + popup_set_header(popup, "Reading", 95, 26, AlignCenter, AlignBottom); + popup_set_text(popup, "Connect key\nwith pogo pins", 95, 30, AlignCenter, AlignTop); + popup_set_icon(popup, 0, 10, &I_DolphinWait_59x54); view_dispatcher_switch_to_view(ibutton->view_dispatcher, iButtonViewPopup); diff --git a/applications/main/ibutton/scenes/ibutton_scene_read_error.c b/applications/main/ibutton/scenes/ibutton_scene_read_error.c index e966384bfc..6655657521 100644 --- a/applications/main/ibutton/scenes/ibutton_scene_read_error.c +++ b/applications/main/ibutton/scenes/ibutton_scene_read_error.c @@ -14,13 +14,10 @@ void ibutton_scene_read_error_on_enter(void* context) { widget_add_button_element( widget, GuiButtonTypeRight, "More", ibutton_widget_callback, context); - widget_add_string_element( - widget, 128 / 2, 2, AlignCenter, AlignTop, FontPrimary, "Read Error"); - ibutton_protocols_render_error(ibutton->protocols, key, tmp); - widget_add_string_multiline_element( - widget, 128 / 2, 16, AlignCenter, AlignTop, FontSecondary, furi_string_get_cstr(tmp)); + widget_add_text_box_element( + widget, 0, 0, 128, 48, AlignCenter, AlignTop, furi_string_get_cstr(tmp), false); ibutton_notification_message(ibutton, iButtonNotificationMessageError); ibutton_notification_message(ibutton, iButtonNotificationMessageRedOn); diff --git a/applications/main/ibutton/scenes/ibutton_scene_read_exit_confirm.c b/applications/main/ibutton/scenes/ibutton_scene_read_exit_confirm.c new file mode 100644 index 0000000000..4077de9a63 --- /dev/null +++ b/applications/main/ibutton/scenes/ibutton_scene_read_exit_confirm.c @@ -0,0 +1,59 @@ +#include "../ibutton_i.h" + +static void ibutton_scene_read_exit_confirm_widget_callback( + GuiButtonType result, + InputType type, + void* context) { + iButton* ibutton = context; + if(type == InputTypeShort) { + view_dispatcher_send_custom_event(ibutton->view_dispatcher, result); + } +} + +void ibutton_scene_read_exit_confirm_on_enter(void* context) { + iButton* ibutton = context; + Widget* widget = ibutton->widget; + + widget_add_button_element( + widget, + GuiButtonTypeLeft, + "Exit", + ibutton_scene_read_exit_confirm_widget_callback, + ibutton); + widget_add_button_element( + widget, + GuiButtonTypeRight, + "Stay", + ibutton_scene_read_exit_confirm_widget_callback, + ibutton); + widget_add_string_element( + widget, 64, 19, AlignCenter, AlignBottom, FontPrimary, "Retry Reading?"); + widget_add_string_element( + widget, 64, 31, AlignCenter, AlignBottom, FontSecondary, "All unsaved data will be lost!"); + + view_dispatcher_switch_to_view(ibutton->view_dispatcher, iButtonViewWidget); +} + +bool ibutton_scene_read_exit_confirm_on_event(void* context, SceneManagerEvent event) { + iButton* ibutton = context; + SceneManager* scene_manager = ibutton->scene_manager; + bool consumed = false; + + if(event.type == SceneManagerEventTypeBack) { + consumed = true; // Ignore Back button presses + } else if(event.type == SceneManagerEventTypeCustom) { + consumed = true; + if(event.event == GuiButtonTypeLeft) { + scene_manager_search_and_switch_to_previous_scene(scene_manager, iButtonSceneRead); + } else if(event.event == GuiButtonTypeRight) { + scene_manager_previous_scene(scene_manager); + } + } + + return consumed; +} + +void ibutton_scene_read_exit_confirm_on_exit(void* context) { + iButton* ibutton = context; + widget_reset(ibutton->widget); +} diff --git a/applications/main/ibutton/scenes/ibutton_scene_read_key_menu.c b/applications/main/ibutton/scenes/ibutton_scene_read_key_menu.c index 1555f2cc20..890e0a2848 100644 --- a/applications/main/ibutton/scenes/ibutton_scene_read_key_menu.c +++ b/applications/main/ibutton/scenes/ibutton_scene_read_key_menu.c @@ -30,29 +30,29 @@ void ibutton_scene_read_key_menu_on_enter(void* context) { ibutton_scene_read_key_menu_submenu_callback, ibutton); - if(features & iButtonProtocolFeatureExtData) { + if(features & iButtonProtocolFeatureWriteBlank) { submenu_add_item( submenu, - "View Data", - SubmenuIndexViewData, + "Write ID", + SubmenuIndexWriteBlank, ibutton_scene_read_key_menu_submenu_callback, ibutton); } - if(features & iButtonProtocolFeatureWriteBlank) { + if(features & iButtonProtocolFeatureWriteCopy) { submenu_add_item( submenu, - "Write Blank", - SubmenuIndexWriteBlank, + "Full Write on Same Type", + SubmenuIndexWriteCopy, ibutton_scene_read_key_menu_submenu_callback, ibutton); } - if(features & iButtonProtocolFeatureWriteCopy) { + if(features & iButtonProtocolFeatureExtData) { submenu_add_item( submenu, - "Write Copy", - SubmenuIndexWriteCopy, + "Data Info", + SubmenuIndexViewData, ibutton_scene_read_key_menu_submenu_callback, ibutton); } diff --git a/applications/main/ibutton/scenes/ibutton_scene_read_success.c b/applications/main/ibutton/scenes/ibutton_scene_read_success.c index 2e50bc9964..6dd06ca526 100644 --- a/applications/main/ibutton/scenes/ibutton_scene_read_success.c +++ b/applications/main/ibutton/scenes/ibutton_scene_read_success.c @@ -18,9 +18,9 @@ void ibutton_scene_read_success_on_enter(void* context) { furi_string_printf( tmp, - "%s[%s]", - ibutton_protocols_get_name(ibutton->protocols, protocol_id), - ibutton_protocols_get_manufacturer(ibutton->protocols, protocol_id)); + "%s %s", + ibutton_protocols_get_manufacturer(ibutton->protocols, protocol_id), + ibutton_protocols_get_name(ibutton->protocols, protocol_id)); widget_add_string_element( widget, 0, 2, AlignLeft, AlignTop, FontPrimary, furi_string_get_cstr(tmp)); @@ -44,7 +44,7 @@ bool ibutton_scene_read_success_on_event(void* context, SceneManagerEvent event) if(event.type == SceneManagerEventTypeBack) { consumed = true; - scene_manager_next_scene(scene_manager, iButtonSceneExitConfirm); + scene_manager_next_scene(scene_manager, iButtonSceneReadExitConfirm); } else if(event.type == SceneManagerEventTypeCustom) { consumed = true; if(event.event == GuiButtonTypeRight) { diff --git a/applications/main/ibutton/scenes/ibutton_scene_saved_key_menu.c b/applications/main/ibutton/scenes/ibutton_scene_saved_key_menu.c index fc0cf42e2f..1547a647bf 100644 --- a/applications/main/ibutton/scenes/ibutton_scene_saved_key_menu.c +++ b/applications/main/ibutton/scenes/ibutton_scene_saved_key_menu.c @@ -21,12 +21,16 @@ void ibutton_scene_saved_key_menu_on_enter(void* context) { if(features & iButtonProtocolFeatureWriteBlank) { submenu_add_item( - submenu, "Write Blank", SubmenuIndexWriteBlank, ibutton_submenu_callback, ibutton); + submenu, "Write ID", SubmenuIndexWriteBlank, ibutton_submenu_callback, ibutton); } if(features & iButtonProtocolFeatureWriteCopy) { submenu_add_item( - submenu, "Write Copy", SubmenuIndexWriteCopy, ibutton_submenu_callback, ibutton); + submenu, + "Full Write on Same Type", + SubmenuIndexWriteCopy, + ibutton_submenu_callback, + ibutton); } submenu_add_item(submenu, "Edit", SubmenuIndexEdit, ibutton_submenu_callback, ibutton); diff --git a/applications/main/ibutton/scenes/ibutton_scene_write.c b/applications/main/ibutton/scenes/ibutton_scene_write.c index 80bf13447a..465b063010 100644 --- a/applications/main/ibutton/scenes/ibutton_scene_write.c +++ b/applications/main/ibutton/scenes/ibutton_scene_write.c @@ -40,30 +40,29 @@ void ibutton_scene_write_on_enter(void* context) { widget_add_icon_element(widget, 3, 10, &I_iButtonKey_49x44); - furi_string_printf( - tmp, - "[%s]\n%s ", - ibutton_protocols_get_name(ibutton->protocols, protocol_id), - ibutton->key_name); + if(furi_string_empty(ibutton->file_path)) { + furi_string_printf( + tmp, "Unsaved\n%s", ibutton_protocols_get_name(ibutton->protocols, protocol_id)); + } else { + furi_string_printf(tmp, "%s", ibutton->key_name); + } widget_add_text_box_element( - widget, 52, 30, 75, 40, AlignCenter, AlignCenter, furi_string_get_cstr(tmp), true); + widget, 52, 23, 75, 26, AlignCenter, AlignTop, furi_string_get_cstr(tmp), false); ibutton_worker_write_set_callback(worker, ibutton_scene_write_callback, ibutton); - furi_string_set(tmp, "iButton\nwriting "); - if(ibutton->write_mode == iButtonWriteModeBlank) { - furi_string_cat(tmp, "Blank"); + furi_string_set(tmp, "Writing ID"); ibutton_worker_write_blank_start(worker, key); } else if(ibutton->write_mode == iButtonWriteModeCopy) { - furi_string_cat(tmp, "Copy"); + furi_string_set(tmp, "Full Writing"); ibutton_worker_write_copy_start(worker, key); } widget_add_string_multiline_element( - widget, 88, 5, AlignCenter, AlignTop, FontPrimary, furi_string_get_cstr(tmp)); + widget, 88, 10, AlignCenter, AlignTop, FontPrimary, furi_string_get_cstr(tmp)); ibutton_notification_message(ibutton, iButtonNotificationMessageEmulateStart); view_dispatcher_switch_to_view(ibutton->view_dispatcher, iButtonViewWidget); diff --git a/applications/main/lfrfid/scenes/lfrfid_scene_read_success.c b/applications/main/lfrfid/scenes/lfrfid_scene_read_success.c index eb773f41b1..b0e373ea52 100644 --- a/applications/main/lfrfid/scenes/lfrfid_scene_read_success.c +++ b/applications/main/lfrfid/scenes/lfrfid_scene_read_success.c @@ -15,8 +15,10 @@ void lfrfid_scene_read_success_on_enter(void* context) { } else { furi_string_printf(display_text, "\e#%s\e#", protocol); } + widget_add_text_box_element( + widget, 16, 2, 112, 14, AlignLeft, AlignTop, furi_string_get_cstr(display_text), true); - furi_string_cat(display_text, "\nHex: "); + furi_string_set(display_text, "Hex: "); const size_t data_size = protocol_dict_get_data_size(app->dict, app->protocol_id); uint8_t* data = malloc(data_size); @@ -40,7 +42,7 @@ void lfrfid_scene_read_success_on_enter(void* context) { furi_string_free(rendered_data); widget_add_text_box_element( - widget, 0, 0, 128, 52, AlignLeft, AlignTop, furi_string_get_cstr(display_text), true); + widget, 0, 16, 128, 52, AlignLeft, AlignTop, furi_string_get_cstr(display_text), true); widget_add_button_element(widget, GuiButtonTypeLeft, "Retry", lfrfid_widget_callback, app); widget_add_button_element(widget, GuiButtonTypeRight, "More", lfrfid_widget_callback, app); diff --git a/applications/main/lfrfid/scenes/lfrfid_scene_save_name.c b/applications/main/lfrfid/scenes/lfrfid_scene_save_name.c index 70c47e8a43..2f223ae87f 100644 --- a/applications/main/lfrfid/scenes/lfrfid_scene_save_name.c +++ b/applications/main/lfrfid/scenes/lfrfid_scene_save_name.c @@ -36,7 +36,7 @@ void lfrfid_scene_save_name_on_enter(void* context) { LFRFID_TEXT_STORE_SIZE, key_name_is_empty); - FURI_LOG_I("", "%s %s", furi_string_get_cstr(folder_path), app->text_store); + FURI_LOG_D("", "%s %s", furi_string_get_cstr(folder_path), app->text_store); ValidatorIsFile* validator_is_file = validator_is_file_alloc_init( furi_string_get_cstr(folder_path), diff --git a/applications/main/momentum_app/scenes/momentum_app_scene_config.h b/applications/main/momentum_app/scenes/momentum_app_scene_config.h index 77eb464325..419d9b27d8 100644 --- a/applications/main/momentum_app/scenes/momentum_app_scene_config.h +++ b/applications/main/momentum_app/scenes/momentum_app_scene_config.h @@ -18,6 +18,7 @@ ADD_SCENE(momentum_app, misc, Misc) ADD_SCENE(momentum_app, misc_screen, MiscScreen) ADD_SCENE(momentum_app, misc_screen_color, MiscScreenColor) ADD_SCENE(momentum_app, misc_dolphin, MiscDolphin) +ADD_SCENE(momentum_app, misc_spoof, MiscSpoof) +ADD_SCENE(momentum_app, misc_spoof_name, MiscSpoofName) ADD_SCENE(momentum_app, misc_vgm, MiscVgm) ADD_SCENE(momentum_app, misc_vgm_color, MiscVgmColor) -ADD_SCENE(momentum_app, misc_rename, MiscRename) diff --git a/applications/main/momentum_app/scenes/momentum_app_scene_misc.c b/applications/main/momentum_app/scenes/momentum_app_scene_misc.c index ba9c1077e2..64b9900f2b 100644 --- a/applications/main/momentum_app/scenes/momentum_app_scene_misc.c +++ b/applications/main/momentum_app/scenes/momentum_app_scene_misc.c @@ -3,8 +3,8 @@ enum VarItemListIndex { VarItemListIndexScreen, VarItemListIndexDolphin, + VarItemListIndexSpoof, VarItemListIndexVgm, - VarItemListIndexChangeDeviceName, VarItemListIndexChargeCap, VarItemListIndexShowMomentumIntro, }; @@ -37,10 +37,11 @@ void momentum_app_scene_misc_on_enter(void* context) { item = variable_item_list_add(var_item_list, "Dolphin", 0, NULL, app); variable_item_set_current_value_text(item, ">"); - item = variable_item_list_add(var_item_list, "VGM Options", 0, NULL, app); + item = variable_item_list_add(var_item_list, "Spoofing Options", 0, NULL, app); variable_item_set_current_value_text(item, ">"); - variable_item_list_add(var_item_list, "Change Device Name", 0, NULL, app); + item = variable_item_list_add(var_item_list, "VGM Options", 0, NULL, app); + variable_item_set_current_value_text(item, ">"); char cap_str[6]; value_index = momentum_settings.charge_cap / CHARGE_CAP_INTV; @@ -81,14 +82,14 @@ bool momentum_app_scene_misc_on_event(void* context, SceneManagerEvent event) { scene_manager_set_scene_state(app->scene_manager, MomentumAppSceneMiscDolphin, 0); scene_manager_next_scene(app->scene_manager, MomentumAppSceneMiscDolphin); break; + case VarItemListIndexSpoof: + scene_manager_set_scene_state(app->scene_manager, MomentumAppSceneMiscSpoof, 0); + scene_manager_next_scene(app->scene_manager, MomentumAppSceneMiscSpoof); + break; case VarItemListIndexVgm: scene_manager_set_scene_state(app->scene_manager, MomentumAppSceneMiscVgm, 0); scene_manager_next_scene(app->scene_manager, MomentumAppSceneMiscVgm); break; - case VarItemListIndexChangeDeviceName: - scene_manager_set_scene_state(app->scene_manager, MomentumAppSceneMiscRename, 0); - scene_manager_next_scene(app->scene_manager, MomentumAppSceneMiscRename); - break; case VarItemListIndexShowMomentumIntro: { for(int i = 0; i < 10; i++) { if(storage_common_copy( diff --git a/applications/main/momentum_app/scenes/momentum_app_scene_misc_spoof.c b/applications/main/momentum_app/scenes/momentum_app_scene_misc_spoof.c new file mode 100644 index 0000000000..a9aea7efb7 --- /dev/null +++ b/applications/main/momentum_app/scenes/momentum_app_scene_misc_spoof.c @@ -0,0 +1,77 @@ +#include "../momentum_app.h" + +enum VarItemListIndex { + VarItemListIndexFlipperName, // TODO: Split into name, mac, serial + VarItemListIndexShellColor, +}; + +const char* const shell_color_names[FuriHalVersionColorCount] = { + "Real", + "Black", + "White", + "Transparent", +}; +static void momentum_app_scene_misc_spoof_shell_color_changed(VariableItem* item) { + MomentumApp* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, shell_color_names[index]); + momentum_settings.spoof_color = index; + app->save_settings = true; + app->require_reboot = true; +} + +void momentum_app_scene_misc_spoof_var_item_list_callback(void* context, uint32_t index) { + MomentumApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, index); +} + +void momentum_app_scene_misc_spoof_on_enter(void* context) { + MomentumApp* app = context; + VariableItemList* var_item_list = app->var_item_list; + VariableItem* item; + + item = variable_item_list_add(var_item_list, "Flipper Name", 0, NULL, app); + variable_item_set_current_value_text(item, app->device_name); + + item = variable_item_list_add( + var_item_list, + "Shell Color", + FuriHalVersionColorCount, + momentum_app_scene_misc_spoof_shell_color_changed, + app); + variable_item_set_current_value_index(item, momentum_settings.spoof_color); + variable_item_set_current_value_text(item, shell_color_names[momentum_settings.spoof_color]); + + variable_item_list_set_enter_callback( + var_item_list, momentum_app_scene_misc_spoof_var_item_list_callback, app); + + variable_item_list_set_selected_item( + var_item_list, + scene_manager_get_scene_state(app->scene_manager, MomentumAppSceneMiscSpoof)); + + view_dispatcher_switch_to_view(app->view_dispatcher, MomentumAppViewVarItemList); +} + +bool momentum_app_scene_misc_spoof_on_event(void* context, SceneManagerEvent event) { + MomentumApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + scene_manager_set_scene_state(app->scene_manager, MomentumAppSceneMiscSpoof, event.event); + consumed = true; + switch(event.event) { + case VarItemListIndexFlipperName: + scene_manager_next_scene(app->scene_manager, MomentumAppSceneMiscSpoofName); + break; + default: + break; + } + } + + return consumed; +} + +void momentum_app_scene_misc_spoof_on_exit(void* context) { + MomentumApp* app = context; + variable_item_list_reset(app->var_item_list); +} diff --git a/applications/main/momentum_app/scenes/momentum_app_scene_misc_rename.c b/applications/main/momentum_app/scenes/momentum_app_scene_misc_spoof_name.c similarity index 70% rename from applications/main/momentum_app/scenes/momentum_app_scene_misc_rename.c rename to applications/main/momentum_app/scenes/momentum_app_scene_misc_spoof_name.c index 5f0127dfd2..1ba6aed7bd 100644 --- a/applications/main/momentum_app/scenes/momentum_app_scene_misc_rename.c +++ b/applications/main/momentum_app/scenes/momentum_app_scene_misc_spoof_name.c @@ -4,7 +4,7 @@ enum TextInputIndex { TextInputResultOk, }; -static void momentum_app_scene_misc_rename_text_input_callback(void* context) { +static void momentum_app_scene_misc_spoof_name_text_input_callback(void* context) { MomentumApp* app = context; app->save_name = true; @@ -12,8 +12,10 @@ static void momentum_app_scene_misc_rename_text_input_callback(void* context) { view_dispatcher_send_custom_event(app->view_dispatcher, TextInputResultOk); } -static bool - momentum_app_scene_misc_rename_validator(const char* text, FuriString* error, void* context) { +static bool momentum_app_scene_misc_spoof_name_validator( + const char* text, + FuriString* error, + void* context) { UNUSED(context); for(; *text; ++text) { @@ -27,19 +29,19 @@ static bool return true; } -void momentum_app_scene_misc_rename_on_enter(void* context) { +void momentum_app_scene_misc_spoof_name_on_enter(void* context) { MomentumApp* app = context; TextInput* text_input = app->text_input; - text_input_set_header_text(text_input, "Leave empty for default"); + text_input_set_header_text(text_input, "Leave empty for real name"); - text_input_set_validator(text_input, momentum_app_scene_misc_rename_validator, NULL); + text_input_set_validator(text_input, momentum_app_scene_misc_spoof_name_validator, NULL); text_input_set_minimum_length(text_input, 0); text_input_set_result_callback( text_input, - momentum_app_scene_misc_rename_text_input_callback, + momentum_app_scene_misc_spoof_name_text_input_callback, app, app->device_name, FURI_HAL_VERSION_ARRAY_NAME_LENGTH, @@ -48,7 +50,7 @@ void momentum_app_scene_misc_rename_on_enter(void* context) { view_dispatcher_switch_to_view(app->view_dispatcher, MomentumAppViewTextInput); } -bool momentum_app_scene_misc_rename_on_event(void* context, SceneManagerEvent event) { +bool momentum_app_scene_misc_spoof_name_on_event(void* context, SceneManagerEvent event) { MomentumApp* app = context; bool consumed = false; @@ -66,7 +68,7 @@ bool momentum_app_scene_misc_rename_on_event(void* context, SceneManagerEvent ev return consumed; } -void momentum_app_scene_misc_rename_on_exit(void* context) { +void momentum_app_scene_misc_spoof_name_on_exit(void* context) { MomentumApp* app = context; text_input_reset(app->text_input); } diff --git a/applications/main/nfc/helpers/felica_auth.c b/applications/main/nfc/helpers/felica_auth.c new file mode 100644 index 0000000000..a8cd0929ba --- /dev/null +++ b/applications/main/nfc/helpers/felica_auth.c @@ -0,0 +1,21 @@ +#include "felica_auth.h" + +FelicaAuthenticationContext* felica_auth_alloc() { + FelicaAuthenticationContext* instance = malloc(sizeof(FelicaAuthenticationContext)); + memset(instance->card_key.data, 0, FELICA_DATA_BLOCK_SIZE); + instance->skip_auth = true; + return instance; +} + +void felica_auth_free(FelicaAuthenticationContext* instance) { + furi_assert(instance); + free(instance); +} + +void felica_auth_reset(FelicaAuthenticationContext* instance) { + furi_assert(instance); + memset(instance->card_key.data, 0, FELICA_DATA_BLOCK_SIZE); + instance->skip_auth = true; + instance->auth_status.external = 0; + instance->auth_status.internal = 0; +} \ No newline at end of file diff --git a/applications/main/nfc/helpers/felica_auth.h b/applications/main/nfc/helpers/felica_auth.h new file mode 100644 index 0000000000..3d99f1f9cd --- /dev/null +++ b/applications/main/nfc/helpers/felica_auth.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +FelicaAuthenticationContext* felica_auth_alloc(); + +void felica_auth_free(FelicaAuthenticationContext* instance); + +void felica_auth_reset(FelicaAuthenticationContext* instance); + +#ifdef __cplusplus +} +#endif diff --git a/applications/main/nfc/helpers/protocol_support/felica/felica.c b/applications/main/nfc/helpers/protocol_support/felica/felica.c index affa33b865..b0660f3e62 100644 --- a/applications/main/nfc/helpers/protocol_support/felica/felica.c +++ b/applications/main/nfc/helpers/protocol_support/felica/felica.c @@ -7,6 +7,11 @@ #include "../nfc_protocol_support_common.h" #include "../nfc_protocol_support_gui_common.h" +#include "../nfc_protocol_support_unlock_helper.h" + +enum { + SubmenuIndexUnlock = SubmenuIndexCommonMax, +}; static void nfc_scene_info_on_enter_felica(NfcApp* instance) { const NfcDevice* device = instance->nfc_device; @@ -18,6 +23,35 @@ static void nfc_scene_info_on_enter_felica(NfcApp* instance) { temp_str, "\e#%s\n", nfc_device_get_name(device, NfcDeviceNameTypeFull)); nfc_render_felica_info(data, NfcProtocolFormatTypeFull, temp_str); + widget_add_text_scroll_element( + instance->widget, 0, 0, 128, 48, furi_string_get_cstr(temp_str)); + + widget_add_button_element( + instance->widget, + GuiButtonTypeRight, + "More", + nfc_protocol_support_common_widget_callback, + instance); + furi_string_free(temp_str); +} + +static bool nfc_scene_info_on_event_felica(NfcApp* instance, SceneManagerEvent event) { + if(event.type == SceneManagerEventTypeCustom && event.event == GuiButtonTypeRight) { + scene_manager_next_scene(instance->scene_manager, NfcSceneMoreInfo); + return true; + } + + return false; +} + +static void nfc_scene_more_info_on_enter_felica(NfcApp* instance) { + const NfcDevice* device = instance->nfc_device; + const FelicaData* data = nfc_device_get_data(device, NfcProtocolFelica); + + FuriString* temp_str = furi_string_alloc(); + + nfc_render_felica_dump(data, temp_str); + widget_add_text_scroll_element( instance->widget, 0, 0, 128, 64, furi_string_get_cstr(temp_str)); @@ -29,29 +63,75 @@ static NfcCommand nfc_scene_read_poller_callback_felica(NfcGenericEvent event, v NfcApp* instance = context; const FelicaPollerEvent* felica_event = event.event_data; + NfcCommand command = NfcCommandContinue; if(felica_event->type == FelicaPollerEventTypeReady) { nfc_device_set_data( instance->nfc_device, NfcProtocolFelica, nfc_poller_get_data(instance->poller)); view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventPollerSuccess); - return NfcCommandStop; + command = NfcCommandStop; + } else if( + felica_event->type == FelicaPollerEventTypeError || + felica_event->type == FelicaPollerEventTypeIncomplete) { + nfc_device_set_data( + instance->nfc_device, NfcProtocolFelica, nfc_poller_get_data(instance->poller)); + view_dispatcher_send_custom_event( + instance->view_dispatcher, NfcCustomEventPollerIncomplete); + command = NfcCommandStop; + } else if(felica_event->type == FelicaPollerEventTypeRequestAuthContext) { + view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventCardDetected); + FelicaAuthenticationContext* ctx = felica_event->data->auth_context; + ctx->skip_auth = instance->felica_auth->skip_auth; + memcpy(ctx->card_key.data, instance->felica_auth->card_key.data, FELICA_DATA_BLOCK_SIZE); } - return NfcCommandContinue; + return command; } static void nfc_scene_read_on_enter_felica(NfcApp* instance) { + nfc_unlock_helper_setup_from_state(instance); nfc_poller_start(instance->poller, nfc_scene_read_poller_callback_felica, instance); } +bool nfc_scene_read_on_event_felica(NfcApp* instance, SceneManagerEvent event) { + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == NfcCustomEventCardDetected) { + nfc_unlock_helper_card_detected_handler(instance); + } else if(event.event == NfcCustomEventPollerIncomplete) { + notification_message(instance->notifications, &sequence_semi_success); + scene_manager_next_scene(instance->scene_manager, NfcSceneReadSuccess); + dolphin_deed(DolphinDeedNfcReadSuccess); + } + } + return true; +} + static void nfc_scene_read_success_on_enter_felica(NfcApp* instance) { const NfcDevice* device = instance->nfc_device; const FelicaData* data = nfc_device_get_data(device, NfcProtocolFelica); FuriString* temp_str = furi_string_alloc(); - furi_string_cat_printf( - temp_str, "\e#%s\n", nfc_device_get_name(device, NfcDeviceNameTypeFull)); - nfc_render_felica_info(data, NfcProtocolFormatTypeShort, temp_str); + + if(!scene_manager_has_previous_scene(instance->scene_manager, NfcSceneFelicaUnlockWarn)) { + furi_string_cat_printf( + temp_str, "\e#%s\n", nfc_device_get_name(device, NfcDeviceNameTypeFull)); + nfc_render_felica_info(data, NfcProtocolFormatTypeShort, temp_str); + } else { + bool all_unlocked = data->blocks_read == data->blocks_total; + furi_string_cat_printf( + temp_str, + "\e#%s\n", + all_unlocked ? "All Blocks Are Unlocked" : "Some Blocks Are Locked"); + nfc_render_felica_idm(data, NfcProtocolFormatTypeShort, temp_str); + uint8_t* ck_data = instance->felica_auth->card_key.data; + furi_string_cat_printf(temp_str, "Key:"); + for(uint8_t i = 0; i < 7; i++) { + furi_string_cat_printf(temp_str, " %02X", ck_data[i]); + if(i == 6) furi_string_cat_printf(temp_str, "..."); + } + nfc_render_felica_blocks_count(data, temp_str, false); + } + felica_auth_reset(instance->felica_auth); widget_add_text_scroll_element( instance->widget, 0, 0, 128, 52, furi_string_get_cstr(temp_str)); @@ -74,23 +154,50 @@ static void nfc_scene_emulate_on_enter_felica(NfcApp* instance) { nfc_listener_start(instance->listener, NULL, NULL); } +static void nfc_scene_read_menu_on_enter_felica(NfcApp* instance) { + const FelicaData* data = nfc_device_get_data(instance->nfc_device, NfcProtocolFelica); + if(data->blocks_read != data->blocks_total) { + submenu_add_item( + instance->submenu, + "Unlock", + SubmenuIndexUnlock, + nfc_protocol_support_common_submenu_callback, + instance); + } +} + +static bool nfc_scene_read_menu_on_event_felica(NfcApp* instance, SceneManagerEvent event) { + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == SubmenuIndexUnlock) { + scene_manager_next_scene(instance->scene_manager, NfcSceneFelicaKeyInput); + return true; + } + } + return false; +} + const NfcProtocolSupportBase nfc_protocol_support_felica = { .features = NfcProtocolFeatureEmulateUid, .scene_info = { .on_enter = nfc_scene_info_on_enter_felica, + .on_event = nfc_scene_info_on_event_felica, + }, + .scene_more_info = + { + .on_enter = nfc_scene_more_info_on_enter_felica, .on_event = nfc_protocol_support_common_on_event_empty, }, .scene_read = { .on_enter = nfc_scene_read_on_enter_felica, - .on_event = nfc_protocol_support_common_on_event_empty, + .on_event = nfc_scene_read_on_event_felica, }, .scene_read_menu = { - .on_enter = nfc_protocol_support_common_on_enter_empty, - .on_event = nfc_protocol_support_common_on_event_empty, + .on_enter = nfc_scene_read_menu_on_enter_felica, + .on_event = nfc_scene_read_menu_on_event_felica, }, .scene_read_success = { diff --git a/applications/main/nfc/helpers/protocol_support/felica/felica_render.c b/applications/main/nfc/helpers/protocol_support/felica/felica_render.c index 3142b2c6db..6c57fb24b6 100644 --- a/applications/main/nfc/helpers/protocol_support/felica/felica_render.c +++ b/applications/main/nfc/helpers/protocol_support/felica/felica_render.c @@ -1,19 +1,107 @@ #include "felica_render.h" -void nfc_render_felica_info( +void nfc_render_felica_blocks_count( + const FelicaData* data, + FuriString* str, + bool render_auth_notification) { + furi_string_cat_printf(str, "\nBlocks Read: %u/%u", data->blocks_read, data->blocks_total); + if(render_auth_notification && data->blocks_read != data->blocks_total) { + furi_string_cat_printf(str, "\nAuth-protected blocks!"); + } +} + +void nfc_render_felica_idm( const FelicaData* data, NfcProtocolFormatType format_type, FuriString* str) { - furi_string_cat_printf(str, "IDm:"); + furi_string_cat_printf(str, (format_type == NfcProtocolFormatTypeFull) ? "IDm:\n" : "IDm:"); for(size_t i = 0; i < FELICA_IDM_SIZE; i++) { - furi_string_cat_printf(str, " %02X", data->idm.data[i]); + furi_string_cat_printf( + str, + (format_type == NfcProtocolFormatTypeFull) ? "%02X " : " %02X", + data->idm.data[i]); + } +} + +void nfc_render_felica_info( + const FelicaData* data, + NfcProtocolFormatType format_type, + FuriString* str) { + if(format_type == NfcProtocolFormatTypeFull) { + furi_string_cat_printf(str, "Tech: JIS X 6319-4,\nISO 18092 [NFC-F]\n"); } + nfc_render_felica_idm(data, format_type, str); + if(format_type == NfcProtocolFormatTypeFull) { - furi_string_cat_printf(str, "\nPMm:"); + furi_string_cat_printf(str, "\nPMm:\n"); for(size_t i = 0; i < FELICA_PMM_SIZE; ++i) { - furi_string_cat_printf(str, " %02X", data->pmm.data[i]); + furi_string_cat_printf(str, "%02X ", data->pmm.data[i]); + } + } + nfc_render_felica_blocks_count(data, str, true); +} + +static void nfc_render_felica_block_name( + const char* name, + FuriString* str, + uint8_t prefix_separator_cnt, + uint8_t suffix_separator_cnt) { + for(uint8_t i = 0; i < prefix_separator_cnt; i++) { + furi_string_cat_printf(str, ":"); + } + furi_string_cat_printf(str, "[ %s ]", name); + for(uint8_t i = 0; i < suffix_separator_cnt; i++) { + furi_string_cat_printf(str, ":"); + } +} + +static void nfc_render_felica_block_data(const FelicaBlock* block, FuriString* str) { + furi_string_cat_printf(str, "\nSF1=%02X; SF2=%02X\n", block->SF1, block->SF2); + for(size_t j = 0; j < FELICA_DATA_BLOCK_SIZE; j++) { + if((j != 0) && (j % 8 == 0)) furi_string_cat_printf(str, "\n"); + furi_string_cat_printf(str, "%02X ", block->data[j]); + } + furi_string_cat_printf(str, "\n"); +} + +static void nfc_render_felica_block( + const FelicaBlock* block, + FuriString* str, + const char* name, + uint8_t prefix_separator_cnt, + uint8_t suffix_separator_cnt) { + nfc_render_felica_block_name(name, str, prefix_separator_cnt, suffix_separator_cnt); + nfc_render_felica_block_data(block, str); +} + +void nfc_render_felica_dump(const FelicaData* data, FuriString* str) { + FuriString* name = furi_string_alloc(); + for(size_t i = 0; i < 14; i++) { + furi_string_printf(name, "S_PAD%d", i); + uint8_t suf_cnt = 18; + if(i == 1) { + suf_cnt = 19; + } else if((i == 10) || (i == 12) || (i == 13)) { + suf_cnt = 16; } + nfc_render_felica_block( + &data->data.fs.spad[i], str, furi_string_get_cstr(name), 20, suf_cnt); } + furi_string_free(name); + nfc_render_felica_block(&data->data.fs.reg, str, "REG", 23, 23); + nfc_render_felica_block(&data->data.fs.rc, str, "RC", 25, 25); + nfc_render_felica_block(&data->data.fs.mac, str, "MAC", 23, 23); + nfc_render_felica_block(&data->data.fs.id, str, "ID", 25, 25); + nfc_render_felica_block(&data->data.fs.d_id, str, "D_ID", 22, 24); + nfc_render_felica_block(&data->data.fs.ser_c, str, "SER_C", 20, 21); + nfc_render_felica_block(&data->data.fs.sys_c, str, "SYS_C", 20, 21); + nfc_render_felica_block(&data->data.fs.ckv, str, "CKV", 23, 23); + nfc_render_felica_block(&data->data.fs.ck, str, "CK", 25, 25); + nfc_render_felica_block(&data->data.fs.mc, str, "MC", 25, 24); + nfc_render_felica_block(&data->data.fs.wcnt, str, "WCNT", 22, 20); + nfc_render_felica_block(&data->data.fs.mac_a, str, "MAC_A", 20, 20); + nfc_render_felica_block(&data->data.fs.state, str, "STATE", 20, 21); + nfc_render_felica_block(&data->data.fs.crc_check, str, "CRC_CHCK", 15, 17); } diff --git a/applications/main/nfc/helpers/protocol_support/felica/felica_render.h b/applications/main/nfc/helpers/protocol_support/felica/felica_render.h index 6d9816fc66..3d32e8d14e 100644 --- a/applications/main/nfc/helpers/protocol_support/felica/felica_render.h +++ b/applications/main/nfc/helpers/protocol_support/felica/felica_render.h @@ -4,7 +4,19 @@ #include "../nfc_protocol_support_render_common.h" +void nfc_render_felica_blocks_count( + const FelicaData* data, + FuriString* str, + bool render_auth_notification); + void nfc_render_felica_info( const FelicaData* data, NfcProtocolFormatType format_type, FuriString* str); + +void nfc_render_felica_dump(const FelicaData* data, FuriString* str); + +void nfc_render_felica_idm( + const FelicaData* data, + NfcProtocolFormatType format_type, + FuriString* str); \ No newline at end of file diff --git a/applications/main/nfc/helpers/protocol_support/mf_desfire/mf_desfire.c b/applications/main/nfc/helpers/protocol_support/mf_desfire/mf_desfire.c index ef51d98e0b..deba1bca28 100644 --- a/applications/main/nfc/helpers/protocol_support/mf_desfire/mf_desfire.c +++ b/applications/main/nfc/helpers/protocol_support/mf_desfire/mf_desfire.c @@ -34,6 +34,8 @@ static void nfc_scene_more_info_on_enter_mf_desfire(NfcApp* instance) { static NfcCommand nfc_scene_read_poller_callback_mf_desfire(NfcGenericEvent event, void* context) { furi_assert(event.protocol == NfcProtocolMfDesfire); + NfcCommand command = NfcCommandContinue; + NfcApp* instance = context; const MfDesfirePollerEvent* mf_desfire_event = event.event_data; @@ -41,10 +43,12 @@ static NfcCommand nfc_scene_read_poller_callback_mf_desfire(NfcGenericEvent even nfc_device_set_data( instance->nfc_device, NfcProtocolMfDesfire, nfc_poller_get_data(instance->poller)); view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventPollerSuccess); - return NfcCommandStop; + command = NfcCommandStop; + } else if(mf_desfire_event->type == MfDesfirePollerEventTypeReadFailed) { + command = NfcCommandReset; } - return NfcCommandContinue; + return command; } static void nfc_scene_read_on_enter_mf_desfire(NfcApp* instance) { diff --git a/applications/main/nfc/helpers/protocol_support/mf_desfire/mf_desfire_render.c b/applications/main/nfc/helpers/protocol_support/mf_desfire/mf_desfire_render.c index 94b333f552..f8eacd51a2 100644 --- a/applications/main/nfc/helpers/protocol_support/mf_desfire/mf_desfire_render.c +++ b/applications/main/nfc/helpers/protocol_support/mf_desfire/mf_desfire_render.c @@ -11,24 +11,29 @@ void nfc_render_mf_desfire_info( const uint32_t bytes_total = 1UL << (data->version.sw_storage >> 1); const uint32_t bytes_free = data->free_memory.is_present ? data->free_memory.bytes_free : 0; - furi_string_cat_printf(str, "\n%lu", bytes_total); + if(data->master_key_settings.is_free_directory_list) { + const uint32_t app_count = simple_array_get_count(data->applications); + uint32_t file_count = 0; + + for(uint32_t i = 0; i < app_count; ++i) { + const MfDesfireApplication* app = simple_array_cget(data->applications, i); + if(app->key_settings.is_free_directory_list) { + file_count += simple_array_get_count(app->file_ids); + } + } - if(data->version.sw_storage & 1) { - furi_string_push_back(str, '+'); + furi_string_cat_printf(str, "\n%lu Application%s", app_count, app_count != 1 ? "s" : ""); + furi_string_cat_printf(str, ", %lu File%s", file_count, file_count != 1 ? "s" : ""); + } else { + furi_string_cat_printf(str, "\nAuth required to read apps!"); } - furi_string_cat_printf(str, " bytes, %lu bytes free\n", bytes_free); - - const uint32_t app_count = simple_array_get_count(data->applications); - uint32_t file_count = 0; + furi_string_cat_printf(str, "\n%lu", bytes_total); - for(uint32_t i = 0; i < app_count; ++i) { - const MfDesfireApplication* app = simple_array_cget(data->applications, i); - file_count += simple_array_get_count(app->file_ids); + if(data->version.sw_storage & 1) { + furi_string_push_back(str, '+'); } - - furi_string_cat_printf(str, "%lu Application%s", app_count, app_count != 1 ? "s" : ""); - furi_string_cat_printf(str, ", %lu File%s", file_count, file_count != 1 ? "s" : ""); + furi_string_cat_printf(str, " bytes, %lu bytes free", bytes_free); if(format_type != NfcProtocolFormatTypeFull) return; @@ -101,17 +106,29 @@ void nfc_render_mf_desfire_free_memory(const MfDesfireFreeMemory* data, FuriStri } void nfc_render_mf_desfire_key_settings(const MfDesfireKeySettings* data, FuriString* str) { - furi_string_cat_printf(str, "changeKeyID %d\n", data->change_key_id); - furi_string_cat_printf(str, "configChangeable %d\n", data->is_config_changeable); - furi_string_cat_printf(str, "freeCreateDelete %d\n", data->is_free_create_delete); - furi_string_cat_printf(str, "freeDirectoryList %d\n", data->is_free_directory_list); - furi_string_cat_printf(str, "masterChangeable %d\n", data->is_master_key_changeable); + if(data->is_free_directory_list) { + furi_string_cat_printf(str, "changeKeyID %d\n", data->change_key_id); + furi_string_cat_printf(str, "configChangeable %d\n", data->is_config_changeable); + furi_string_cat_printf(str, "freeCreateDelete %d\n", data->is_free_create_delete); + furi_string_cat_printf(str, "freeDirectoryList %d\n", data->is_free_directory_list); + furi_string_cat_printf(str, "masterChangeable %d\n", data->is_master_key_changeable); + } else { + furi_string_cat_printf(str, "changeKeyID ??\n"); + furi_string_cat_printf(str, "configChangeable ??\n"); + furi_string_cat_printf(str, "freeCreateDelete ??\n"); + furi_string_cat_printf(str, "freeDirectoryList 0\n"); + furi_string_cat_printf(str, "masterChangeable ??\n"); + } if(data->flags) { furi_string_cat_printf(str, "flags %d\n", data->flags); } - furi_string_cat_printf(str, "maxKeys %d\n", data->max_keys); + if(data->is_free_directory_list) { + furi_string_cat_printf(str, "maxKeys %d\n", data->max_keys); + } else { + furi_string_cat_printf(str, "maxKeys ??\n"); + } } void nfc_render_mf_desfire_key_version( @@ -123,14 +140,16 @@ void nfc_render_mf_desfire_key_version( void nfc_render_mf_desfire_application_id(const MfDesfireApplicationId* data, FuriString* str) { const uint8_t* app_id = data->data; - furi_string_cat_printf(str, "Application %02x%02x%02x\n", app_id[0], app_id[1], app_id[2]); + furi_string_cat_printf(str, "Application %02x%02x%02x\n", app_id[2], app_id[1], app_id[0]); } void nfc_render_mf_desfire_application(const MfDesfireApplication* data, FuriString* str) { nfc_render_mf_desfire_key_settings(&data->key_settings, str); - for(uint32_t i = 0; i < simple_array_get_count(data->key_versions); ++i) { - nfc_render_mf_desfire_key_version(simple_array_cget(data->key_versions, i), i, str); + if(data->key_settings.is_free_directory_list) { + for(uint32_t i = 0; i < simple_array_get_count(data->key_versions); ++i) { + nfc_render_mf_desfire_key_version(simple_array_cget(data->key_versions, i), i, str); + } } } @@ -179,13 +198,16 @@ void nfc_render_mf_desfire_file_settings_data( } furi_string_cat_printf(str, "%s %s\n", type, comm); - furi_string_cat_printf( - str, - "r %d w %d rw %d c %d\n", - settings->access_rights >> 12 & 0xF, - settings->access_rights >> 8 & 0xF, - settings->access_rights >> 4 & 0xF, - settings->access_rights & 0xF); + + for(size_t i = 0; i < settings->access_rights_len; i++) { + furi_string_cat_printf( + str, + "r %d w %d rw %d c %d\n", + settings->access_rights[i] >> 12 & 0xF, + settings->access_rights[i] >> 8 & 0xF, + settings->access_rights[i] >> 4 & 0xF, + settings->access_rights[i] & 0xF); + } uint32_t record_count = 1; uint32_t record_size = 0; @@ -217,6 +239,20 @@ void nfc_render_mf_desfire_file_settings_data( break; } + bool is_auth_required = true; + for(size_t i = 0; i < settings->access_rights_len; i++) { + uint8_t read_rights = (settings->access_rights[i] >> 12) & 0x0f; + uint8_t read_write_rights = (settings->access_rights[i] >> 4) & 0x0f; + if((read_rights == 0x0e) || (read_write_rights == 0x0e)) { + is_auth_required = false; + break; + } + } + if(is_auth_required) { + furi_string_cat_printf(str, "Auth required to read file data\n"); + return; + } + if(simple_array_get_count(data->data) == 0) { return; } diff --git a/applications/main/nfc/helpers/protocol_support/mf_ultralight/mf_ultralight.c b/applications/main/nfc/helpers/protocol_support/mf_ultralight/mf_ultralight.c index bd2015889d..cd16374bc8 100644 --- a/applications/main/nfc/helpers/protocol_support/mf_ultralight/mf_ultralight.c +++ b/applications/main/nfc/helpers/protocol_support/mf_ultralight/mf_ultralight.c @@ -8,6 +8,7 @@ #include "../nfc_protocol_support_common.h" #include "../nfc_protocol_support_gui_common.h" +#include "../nfc_protocol_support_unlock_helper.h" enum { SubmenuIndexUnlock = SubmenuIndexCommonMax, @@ -157,48 +158,15 @@ static NfcCommand return NfcCommandContinue; } -enum { - NfcSceneMfUltralightReadMenuStateCardSearch, - NfcSceneMfUltralightReadMenuStateCardFound, -}; - -static void nfc_scene_read_setup_view(NfcApp* instance) { - Popup* popup = instance->popup; - popup_reset(popup); - uint32_t state = scene_manager_get_scene_state(instance->scene_manager, NfcSceneRead); - - if(state == NfcSceneMfUltralightReadMenuStateCardSearch) { - popup_set_icon(instance->popup, 0, 8, &I_NFC_manual_60x50); - popup_set_header(instance->popup, "Unlocking", 97, 15, AlignCenter, AlignTop); - popup_set_text( - instance->popup, "Hold card next\nto Flipper's back", 94, 27, AlignCenter, AlignTop); - } else { - popup_set_header(instance->popup, "Don't move", 85, 27, AlignCenter, AlignTop); - popup_set_icon(instance->popup, 12, 20, &A_Loading_24); - } - - view_dispatcher_switch_to_view(instance->view_dispatcher, NfcViewPopup); -} - static void nfc_scene_read_on_enter_mf_ultralight(NfcApp* instance) { - bool unlocking = - scene_manager_has_previous_scene(instance->scene_manager, NfcSceneMfUltralightUnlockWarn); - - uint32_t state = unlocking ? NfcSceneMfUltralightReadMenuStateCardSearch : - NfcSceneMfUltralightReadMenuStateCardFound; - - scene_manager_set_scene_state(instance->scene_manager, NfcSceneRead, state); - - nfc_scene_read_setup_view(instance); + nfc_unlock_helper_setup_from_state(instance); nfc_poller_start(instance->poller, nfc_scene_read_poller_callback_mf_ultralight, instance); } bool nfc_scene_read_on_event_mf_ultralight(NfcApp* instance, SceneManagerEvent event) { if(event.type == SceneManagerEventTypeCustom) { if(event.event == NfcCustomEventCardDetected) { - scene_manager_set_scene_state( - instance->scene_manager, NfcSceneRead, NfcSceneMfUltralightReadMenuStateCardFound); - nfc_scene_read_setup_view(instance); + nfc_unlock_helper_card_detected_handler(instance); } else if((event.event == NfcCustomEventPollerIncomplete)) { notification_message(instance->notifications, &sequence_semi_success); scene_manager_next_scene(instance->scene_manager, NfcSceneReadSuccess); diff --git a/applications/main/nfc/helpers/protocol_support/nfc_protocol_support.c b/applications/main/nfc/helpers/protocol_support/nfc_protocol_support.c index 5f41a0d892..3cfbebf67c 100644 --- a/applications/main/nfc/helpers/protocol_support/nfc_protocol_support.c +++ b/applications/main/nfc/helpers/protocol_support/nfc_protocol_support.c @@ -574,7 +574,7 @@ static void nfc_protocol_support_scene_emulate_on_enter(NfcApp* instance) { FuriString* temp_str = furi_string_alloc(); const NfcProtocol protocol = nfc_device_get_protocol(instance->nfc_device); - widget_add_icon_element(widget, 0, 3, &I_NFC_dolphin_emulation_51x64); + widget_add_icon_element(widget, 0, 0, &I_NFC_dolphin_emulation_51x64); if(nfc_protocol_support_has_feature(protocol, NfcProtocolFeatureEmulateUid)) { widget_add_string_element( diff --git a/applications/main/nfc/helpers/protocol_support/nfc_protocol_support_unlock_helper.c b/applications/main/nfc/helpers/protocol_support/nfc_protocol_support_unlock_helper.c new file mode 100644 index 0000000000..f1d504d248 --- /dev/null +++ b/applications/main/nfc/helpers/protocol_support/nfc_protocol_support_unlock_helper.c @@ -0,0 +1,38 @@ +#include "nfc_protocol_support_unlock_helper.h" + +static void nfc_scene_read_setup_view(NfcApp* instance) { + Popup* popup = instance->popup; + popup_reset(popup); + uint32_t state = scene_manager_get_scene_state(instance->scene_manager, NfcSceneRead); + + if(state == NfcSceneReadMenuStateCardSearch) { + popup_set_icon(instance->popup, 0, 8, &I_NFC_manual_60x50); + popup_set_header(instance->popup, "Unlocking", 97, 15, AlignCenter, AlignTop); + popup_set_text( + instance->popup, "Hold card next\nto Flipper's back", 94, 27, AlignCenter, AlignTop); + } else { + popup_set_header(instance->popup, "Don't move", 85, 27, AlignCenter, AlignTop); + popup_set_icon(instance->popup, 12, 20, &A_Loading_24); + } + + view_dispatcher_switch_to_view(instance->view_dispatcher, NfcViewPopup); +} + +void nfc_unlock_helper_setup_from_state(NfcApp* instance) { + bool unlocking = + scene_manager_has_previous_scene( + instance->scene_manager, NfcSceneMfUltralightUnlockWarn) || + scene_manager_has_previous_scene(instance->scene_manager, NfcSceneFelicaUnlockWarn); + + uint32_t state = unlocking ? NfcSceneReadMenuStateCardSearch : NfcSceneReadMenuStateCardFound; + + scene_manager_set_scene_state(instance->scene_manager, NfcSceneRead, state); + + nfc_scene_read_setup_view(instance); +} + +void nfc_unlock_helper_card_detected_handler(NfcApp* instance) { + scene_manager_set_scene_state( + instance->scene_manager, NfcSceneRead, NfcSceneReadMenuStateCardFound); + nfc_scene_read_setup_view(instance); +} \ No newline at end of file diff --git a/applications/main/nfc/helpers/protocol_support/nfc_protocol_support_unlock_helper.h b/applications/main/nfc/helpers/protocol_support/nfc_protocol_support_unlock_helper.h new file mode 100644 index 0000000000..65da332402 --- /dev/null +++ b/applications/main/nfc/helpers/protocol_support/nfc_protocol_support_unlock_helper.h @@ -0,0 +1,9 @@ +#include "nfc/nfc_app_i.h" + +typedef enum { + NfcSceneReadMenuStateCardSearch, + NfcSceneReadMenuStateCardFound, +} NfcSceneUnlockReadState; + +void nfc_unlock_helper_setup_from_state(NfcApp* instance); +void nfc_unlock_helper_card_detected_handler(NfcApp* instance); \ No newline at end of file diff --git a/applications/main/nfc/nfc_app.c b/applications/main/nfc/nfc_app.c index bcfe7d0374..7dca264090 100644 --- a/applications/main/nfc/nfc_app.c +++ b/applications/main/nfc/nfc_app.c @@ -51,6 +51,7 @@ NfcApp* nfc_app_alloc(void) { instance->nfc = nfc_alloc(); + instance->felica_auth = felica_auth_alloc(); instance->mf_ul_auth = mf_ultralight_auth_alloc(); instance->slix_unlock = slix_unlock_alloc(); instance->mfc_key_cache = mf_classic_key_cache_alloc(); @@ -142,6 +143,7 @@ void nfc_app_free(NfcApp* instance) { nfc_free(instance->nfc); + felica_auth_free(instance->felica_auth); mf_ultralight_auth_free(instance->mf_ul_auth); slix_unlock_free(instance->slix_unlock); mf_classic_key_cache_free(instance->mfc_key_cache); diff --git a/applications/main/nfc/nfc_app_i.h b/applications/main/nfc/nfc_app_i.h index df11c985f4..d38e3eab6c 100644 --- a/applications/main/nfc/nfc_app_i.h +++ b/applications/main/nfc/nfc_app_i.h @@ -33,6 +33,7 @@ #include "helpers/nfc_emv_parser.h" #include "helpers/mf_classic_key_cache.h" #include "helpers/nfc_supported_cards.h" +#include "helpers/felica_auth.h" #include "helpers/slix_unlock.h" #include @@ -130,6 +131,7 @@ struct NfcApp { NfcScanner* scanner; NfcListener* listener; + FelicaAuthenticationContext* felica_auth; MfUltralightAuth* mf_ul_auth; SlixUnlock* slix_unlock; NfcMfClassicDictAttackContext nfc_dict_context; diff --git a/applications/main/nfc/plugins/supported_cards/all_in_one.c b/applications/main/nfc/plugins/supported_cards/all_in_one.c index 9e20b92d1f..f85d7ad2e8 100644 --- a/applications/main/nfc/plugins/supported_cards/all_in_one.c +++ b/applications/main/nfc/plugins/supported_cards/all_in_one.c @@ -20,8 +20,8 @@ static AllInOneLayoutType all_in_one_get_layout(const MfUltralightData* data) { const uint8_t layout_byte = data->page[5].data[2]; const uint8_t layout_half_byte = data->page[5].data[2] & 0x0F; - FURI_LOG_I(TAG, "Layout byte: %02x", layout_byte); - FURI_LOG_I(TAG, "Layout half-byte: %02x", layout_half_byte); + FURI_LOG_D(TAG, "Layout byte: %02x", layout_byte); + FURI_LOG_D(TAG, "Layout half-byte: %02x", layout_half_byte); switch(layout_half_byte) { // If it is A, the layout type is a type A layout @@ -32,7 +32,7 @@ static AllInOneLayoutType all_in_one_get_layout(const MfUltralightData* data) { case 0x02: return AllInOneLayoutType2; default: - FURI_LOG_I(TAG, "Unknown layout type: %d", layout_half_byte); + FURI_LOG_E(TAG, "Unknown layout type: %d", layout_half_byte); return AllInOneLayoutTypeUnknown; } } @@ -47,7 +47,7 @@ static bool all_in_one_parse(const NfcDevice* device, FuriString* parsed_data) { do { if(data->page[4].data[0] != 0x45 || data->page[4].data[1] != 0xD9) { - FURI_LOG_I(TAG, "Pass not verified"); + FURI_LOG_E(TAG, "Pass not verified"); break; } @@ -63,7 +63,7 @@ static bool all_in_one_parse(const NfcDevice* device, FuriString* parsed_data) { // If the layout is D, the ride count is stored in the second byte of page 9 ride_count = data->page[9].data[1]; } else { - FURI_LOG_I(TAG, "Unknown layout: %d", layout_type); + FURI_LOG_E(TAG, "Unknown layout: %d", layout_type); ride_count = 137; } diff --git a/applications/main/nfc/plugins/supported_cards/charliecard.c b/applications/main/nfc/plugins/supported_cards/charliecard.c index 7a405fffb2..9880ca3ad7 100644 --- a/applications/main/nfc/plugins/supported_cards/charliecard.c +++ b/applications/main/nfc/plugins/supported_cards/charliecard.c @@ -23,29 +23,41 @@ * — 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 + * — Mapping of buses to garages, and subsequently, route subsets via + * http://roster.transithistory.org/ data + * — Mapping of stations to lines + * — Add'l data fields for side of station fare gates are on? Some stations + * separate inbound & outbound sides, so direction could be inferred + * from gates used. * — 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) + * [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) + * 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") + * listed as "AFC Gates" (presumably standing for "Automated Fare Collection") * [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/) + * Appelate case dockets 2019-P-0401, SJC-12952, SJ-2017-0390 + * (https://www.ma-appellatecourts.org/party) + * Trial court indictment 04/02/2015, Case# 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) + * [X] MBTA data blog? (https://www.massdottracker.com/datablog/) + * [ ] MassDOT developers Google group? (https://groups.google.com/g/massdotdevelopers) + * [X] preexisting posts + * [ ] ask directly? * [ ] Other? * * This program is free software: you can redistribute it and/or modify it @@ -82,12 +94,8 @@ // 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, -}; +#define CHARLIE_N_TRANSACTION_HISTORY 10 +#define CHARLIE_N_PASSES 4 typedef struct { uint64_t a; @@ -138,8 +146,27 @@ typedef struct { uint16_t gate; uint8_t g_flag; Money fare; - uint8_t f_flag; -} Trip; + uint16_t f_flag; +} Transaction; + +typedef struct { + bool valid; + uint16_t pre; + uint16_t post; + DateTime date; +} Pass; + +typedef struct { + uint16_t n_uses; + uint8_t active_balance_sector; +} CounterSector; + +typedef struct { + Money balance; + uint16_t type; + DateTime issued; + DateTime end_validity; +} BalanceSector; // IdMapping approach borrowed from Jeremy Cooper's 'clipper.c' typedef struct { @@ -147,14 +174,14 @@ typedef struct { const char* name; } IdMapping; -// this should be a complete accounting of types, +// this should be a complete accounting of types, (1 and 7 day pass types maybe missing?) 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 = 420, .name = "TAP"}, {.id = 417, .name = "Blind"}, {.id = 426, .name = "Child"}, {.id = 410, .name = "Employee ID Without Passback"}, @@ -164,16 +191,16 @@ static const IdMapping charliecard_types[] = { // 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 = 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 + {.id = 424, .name = "Monthly TAP LinkPass"}, + {.id = 425, .name = "Monthly Senior LinkPass"}, + {.id = 421, .name = "Senior TAP/Permit"}, + {.id = 422, .name = "Senior TAP/Permit 30 Days"}, // Commuter rail passes {.id = 166, .name = "30 Day Commuter Rail Zone 1A Pass"}, @@ -388,9 +415,7 @@ static const IdMapping charliecard_fare_gate_ids[] = { {.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 = 6704, .name = "Chinatown"}, {.id = 6705, .name = "Chinatown"}, {.id = 2099, .name = "Chinatown"}, {.id = 7003, .name = "Chinatown"}, @@ -471,7 +496,7 @@ static const IdMapping charliecard_fare_gate_ids[] = { {.id = 7016, .name = "Forest Hills"}, {.id = 6950, .name = "Forest Hills"}, {.id = 6951, .name = "Forest Hills"}, - {.id = 604, .name = "Forest Hills"}, // Entry error? + {.id = 604, .name = "Forest Hills"}, {.id = 7096, .name = "Forest Hills"}, // South Station {.id = 7039, .name = "South Station"}, @@ -546,7 +571,7 @@ static const IdMapping charliecard_fare_gate_ids[] = { {.id = 2010, .name = "Wood Island"}, {.id = 6971, .name = "Wood Island"}, // Orient Heights - {.id = 6621, .name = "Orient Heights"}, // marked as needs checking + {.id = 6621, .name = "Orient Heights"}, {.id = 6622, .name = "Orient Heights"}, {.id = 6623, .name = "Orient Heights"}, {.id = 2014, .name = "Orient Heights"}, @@ -578,8 +603,13 @@ static const IdMapping charliecard_fare_gate_ids[] = { }; static const size_t kNumFareGateIds = COUNT_OF(charliecard_fare_gate_ids); +// ********************************************************** +// ********************* MISC HELPERS *********************** +// ********************************************************** + static const uint8_t* pos_to_ptr(const MfClassicData* data, uint8_t sector_num, uint8_t block_num, uint8_t byte_num) { + // returns pointer to specified sector/block/byte of MFClassic card data uint8_t block_offset = mf_classic_get_first_block_num_of_sector(sector_num); return &data->block[block_offset + block_num].data[byte_num]; } @@ -590,16 +620,29 @@ static uint64_t pos_to_num( uint8_t block_num, uint8_t byte_num, uint8_t byte_len) { + // returns numeric values at specified card location, for given byte length. + // assumes big endian. 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) { + // returns shifted DateTime, from initial DateTime and time offset in seconds DateTime dt_shifted = {0}; datetime_timestamp_to_datetime(datetime_datetime_to_timestamp(&dt) + delta_secs, &dt_shifted); return dt_shifted; } +static bool dt_ge(DateTime dt1, DateTime dt2) { + // compares two DateTimes + return datetime_datetime_to_timestamp(&dt1) >= datetime_datetime_to_timestamp(&dt2); +} + +static bool dt_eq(DateTime dt1, DateTime dt2) { + // compares two DateTimes + return datetime_datetime_to_timestamp(&dt1) == datetime_datetime_to_timestamp(&dt2); +} + 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";} @@ -614,82 +657,17 @@ static bool get_map_item(uint16_t id, const IdMapping* map, size_t sz, const cha 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; +uint32_t time_now() { + return furi_hal_rtc_get_timestamp(); } -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; +static bool is_debug() { + return furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug); } -uint32_t time_now() { - return furi_hal_rtc_get_timestamp(); -} +// ********************************************************** +// ******************** FIELD PARSING *********************** +// ********************************************************** static Money money_parse( const MfClassicData* data, @@ -709,83 +687,249 @@ static DateTime 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; +static DateTime end_validity_parse( + const MfClassicData* data, + uint8_t sector_num, + uint8_t block_num, + uint8_t byte_num) { + // End validity field is weird; shares first byte with another variable (the card type field), + // occupying the last 5 bits (and subsequent two bytes), hence bitmask + uint32_t ts_charlie_ev = pos_to_num(data, sector_num, block_num, byte_num, 3) & 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 Pass + pass_parse(const MfClassicData* data, uint8_t sector_num, uint8_t block_num, uint8_t byte_num) { + // WIP; testing only. Speculating it may be structured as follows + // Sub-byte field divisions not drawn to scale, see code for exact bit offsets + // + // 0 1 2 3 4 5 + // +----.----.----.----+----.----+ + // | uk1 | date | uk2 | + // +----.----.----.----+----.----+ + // + // "Blank" entries are as follows: + // 0 1 2 3 4 5 + // +----.----.----.----.----.----+ + // | 00 20 00 00 00 00 | + // +----.----.----.----.----.----+ + // + // even when not blank, uk1 LSB seems to always be set to 1... + // the sole bit set to 1 on the blank entry seems to divide + // the uk1 and date fields, and is always set to 1 regardless + // same is true of type & end-validity split found in balance sector + // + // likely fields incl + // — type #, + // — a secondary date field (eg start/end, end validity or normal format) + // — ID of FVM from which the pass was loaded + + // check for empty, if so, return struct filled w/ 0s + // (incl "valid" field: hence, "valid" is false-y) + if(pos_to_num(data, sector_num, block_num, byte_num, 6) == 0x002000000000) { + return (Pass){0}; + } + + // const DateTime start = date_parse(data, sector_num, block_num, byte_num + 1); + + const uint16_t pre = pos_to_num(data, sector_num, block_num, byte_num, 2) >> 6; + const uint16_t post = (pos_to_num(data, sector_num, block_num, byte_num + 4, 2) >> 2) & 0x3ff; + + // these values make sense for a date, but implied position of type + // before end validity, as seen in balance sector, doesn't seem + // to produce sensible values + const DateTime date = end_validity_parse(data, sector_num, block_num, byte_num + 1); + + // DateTime start = date_parse(data, sector_num, block_num, byte_num); + // uint16_t type = 0; // pos_to_num(data, sector_num, block_num, byte_num + 3, 2) >> 6; + + return (Pass){true, pre, post, date}; } -static bool date_ge(DateTime dt1, DateTime dt2) { - return datetime_datetime_to_timestamp(&dt1) >= datetime_datetime_to_timestamp(&dt2); +static Transaction + transaction_parse(const MfClassicData* data, uint8_t sector, uint8_t block, uint8_t byte) { + // This function parses individual transactions. Each transaction 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 seems to indicate: + // — When f & 1 == 1, fare (the amount by which balance is decremented) + // — When f & 1 == 0, refill (the amount by which balance is incremented) + // MSB (sign bit) of amt seems to serve the same role, just inverted, ie + // — When amt & 0x8000 == 0, fare + // — When amt & 0x8000 == 0x8000, refill + // Only contradiction between the two observed is on cards w/ passes; + // MSB of amt seems to be set for every transaction when (remaining bits of) amt is 0 on a card w/ a pass + // Hence, using f's LSB as method for inferring fare v. refill + // + // Remaining unknown bits: + // — f & 0b100; seems to be set on fares where the card has a pass, and amt is 0 + // — f & 0b010 + // — amt & 1; does not seem to correspond with card type, last transaction, first transaction, refill v. fare, etc + + const DateTime date = date_parse(data, sector, block, byte); + const uint16_t gate = pos_to_num(data, sector, block, byte + 3, 2) >> 3; + const uint8_t g_flag = pos_to_num(data, sector, block, byte + 3, 2) & 0b111; + const Money fare = money_parse(data, sector, block, byte + 5); + const uint16_t f_flag = pos_to_num(data, sector, block, byte + 5, 2) & 0x8001; + return (Transaction){date, gate, g_flag, fare, f_flag}; } -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); +// ********************************************************** +// ******************* SECTOR PARSING *********************** +// ********************************************************** + +static uint32_t mfg_sector_parse(const MfClassicData* data) { + // Manufacturer data (Sector 0) + // + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + // +----.----.----.----+----+----.----.----.----+----+----.----.----.----.----+----+ + // 0x000 | UID | rc | 88 04 00 C8 | uk | 00 20 00 00 00 | uk | + // +----.----.----.----+----+----.----.----.----+----+----.----.----.----.----+----+ + // 0x010 | 4E 0F 04 10 04 10 04 10 04 10 04 10 04 10 04 10 | + // +----.----.----.----.----.----.----.----.----.----.----.----.----.----.----.----+ + // 0x020 | ... 00 00 ... | + // +----.----.----.----.----.----.----.----.----.----.----.----.----.----.----.----+ + // + // rc := "redundancy check" (lrc / bcc) + // uk := "unknown" - // 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) + size_t uid_len = 0; + const uint8_t* uid = mf_classic_get_uid(data, &uid_len); + const uint32_t card_number = bit_lib_bytes_to_num_be(uid, 4); + + return card_number; +} + +static CounterSector counter_sector_parse(const MfClassicData* data) { + // Trip/transaction counters (Sector 1) + // + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + // +----.----.----.----.----.----.----.----.----.----.----.----.----.----.----.----+ + // 0x040 | 04 10 23 45 66 77 ... 00 00 ... | + // +----.----+----+----.----.----.----.----.----.----.----.----.----.----.----.----+ + // 0x050 | uses1 | uk | ... 00 00 ... | + // +----.----+----+----.----.----.----.----.----.----.----.----.----.----.----.----+ + // 0x060 | uses2 | uk | ... 00 00 ... | + // +----.----+----+----.----.----.----.----.----.----.----.----.----.----.----.----+ + // + // uk := "unknown"; if nonzero, seems to only occupy the first 4 bits (ie, uk & 0xF0 == uk), + // with the remaining 4 zero + + // Card has two 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* of the two values *minus one* is the true use count, + // and corresponds to the active balance sector, + // (0x50 counter lower -> sector 2 active, 0x60 counter lower -> 3 active) + // per DEFCON31 researcher's findings + + const uint16_t n_uses1 = pos_to_num(data, 1, 1, 0, 2); + const uint16_t n_uses2 = pos_to_num(data, 1, 2, 0, 2); + + const bool is_sec2_active = n_uses1 <= n_uses2; + const uint8_t active_sector = is_sec2_active ? 2 : 3; + const uint16_t n_uses = (is_sec2_active ? n_uses1 : n_uses2) - 1; + + return (CounterSector){n_uses, active_sector}; +} + +static BalanceSector balance_sector_parse(const MfClassicData* data, uint8_t active_sector) { + // Balance & misc card info (Sector 2 or 3) + // + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + // +----+----.----.----+----.----+----.----.----+----.----+----.----+----+----.----+ + // 0x080 | 11 | date last | loc last| date issued | 65 00 | unknown | 00 | crc | 0x0C0 + // +----+----.----.----+----+----+----+----+----+----.----+----.----+----+----.----+ + // 0x090 | type |end validity| uk | balance | 00 | unknown | crc | 0x0D0 + // +----.----.----.----+----+----.----+----+----.----.----.----.----.----+----.----+ + // 0x0A0 | 20 ... 00 00 ... 04 | crc | 0x0E0 + // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ + // + // "Active" balance sector alternates between 2 and 3 + // Last trip/transaction info in balance sector ("date last" & "loc last") + // is also included in transaction log, hence don't bother to read here + // + // Inactive balance sector represent the transaction N-1 version + // (where active sector represents data from transaction N). + + const DateTime issued = date_parse(data, active_sector, 0, 6); + const DateTime end_validity = end_validity_parse(data, active_sector, 1, 1); + // Card type data stored in the first 10bits of block 1 + // (0x90 or 0xD0 depending on active sector) + // bitshift (2bytes = 16 bits) by 6bits for just first 10bits + const uint16_t type = pos_to_num(data, active_sector, 1, 0, 2) >> 6; + const Money bal = money_parse(data, active_sector, 1, 5); + + return (BalanceSector){bal, type, issued, end_validity}; +} + +static Pass* passes_parse(const MfClassicData* data) { + // Passes, speculative (Sectors 4 &/or 5) + // + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + // +----.----.----.----.----.----+----+----.----.----.----.----.----+----+----.----+ + // 0x100 | pass0/2? | 00 | pass1/3? | 00 | crc | 0x140 + // +----.----.----.----.----.----+----+----.----.----.----.----.----+----+----.----+ + // 0x110 | ... 00 00 ... | crc | 0x150 + // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ + // 0x120 | ... 00 ... 05 | crc | 0x160 + // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ + // + // WIP. Read in all speculative passes into array + // 4 separate fields? active vs inactive sector for 2 passes? + // something else entirely? + + Pass* passes = malloc(sizeof(Pass) * CHARLIE_N_PASSES); + + for(size_t i = 0; i < CHARLIE_N_PASSES; i++) { + passes[i] = pass_parse(data, 4 + (i / 2), 0, (i % 2) * 7); + } + + return passes; +} + +static Transaction* transactions_parse(const MfClassicData* data) { + // Transaction history (Sectors 6–7) + // + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ + // 0x180 | transaction0 | transaction1 | crc | + // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ + // ... ... ... ... + // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ + // 0x1D0 | transaction8 | transaction9 | crc | + // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ + // 0x1E0 | ... 00 00 ... | crc | + // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ + // + // Transactions are not sorted, rather, appear to get overwritten + // sequentially. (eg, sorted modulo array rotation) + + Transaction* transactions = malloc(sizeof(Transaction) * CHARLIE_N_TRANSACTION_HISTORY); + + // Parse each transaction field using some modular math magic to get the offsets: + // move from sector 6 -> 7 after the first 6 transactions + // move a block within a given sector every 2 transactions, 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); + for(size_t i = 0; i < CHARLIE_N_TRANSACTION_HISTORY; i++) { + transactions[i] = transaction_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)) { + for(int i = 1; i < CHARLIE_N_TRANSACTION_HISTORY; i++) { + if(dt_ge(transactions[i].date, transactions[max_idx].date)) { max_idx = i; } } @@ -793,78 +937,24 @@ static Trip* trips_parse(const MfClassicData* data) { // Sort by rotating for(int r = 0; r < (max_idx + 1); r++) { // Store the first element - Trip temp = trips[0]; + Transaction temp = transactions[0]; // Shift elements to the left - for(int i = 0; i < CHARLIE_N_TRIP_HISTORY - 1; i++) { - trips[i] = trips[i + 1]; + for(int i = 0; i < CHARLIE_N_TRANSACTION_HISTORY - 1; i++) { + transactions[i] = transactions[i + 1]; } // Move the first element to the last - trips[CHARLIE_N_TRIP_HISTORY - 1] = temp; + transactions[CHARLIE_N_TRANSACTION_HISTORY - 1] = temp; } // Reverse order, such that newest is first, oldest last - for(int i = 0; i < CHARLIE_N_TRIP_HISTORY / 2; i++) { + for(int i = 0; i < CHARLIE_N_TRANSACTION_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; + Transaction temp = transactions[i]; + transactions[i] = transactions[CHARLIE_N_TRANSACTION_HISTORY - i - 1]; + transactions[CHARLIE_N_TRANSACTION_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; + return transactions; } /* @@ -897,18 +987,23 @@ static DateTime expiry(DateTime iss) { } return exp; -}*/ +} -static bool expired(DateTime expiry, DateTime last_trip) { +static bool expired(DateTime expiry, DateTime last_transaction) { // 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_last = datetime_datetime_to_timestamp(&last_transaction); uint32_t ts_now = time_now(); return (ts_exp <= ts_now) | ((ts_now - ts_last) >= (2 * 365 * 24 * 60 * 60)); } +*/ + +// ********************************************************** +// ****************** STRING FORMATTING ********************* +// ********************************************************** void locale_format_dt_cat(FuriString* out, const DateTime* dt) { // helper to print datetimes @@ -935,36 +1030,84 @@ void type_format_cat(FuriString* out, uint16_t type) { furi_string_cat_str(out, s); } +void pass_format_cat(FuriString* out, Pass pass) { + furi_string_cat_printf(out, "\n-Pre: %b", pass.pre); + // type_format_cat(out, pass.type); + furi_string_cat_printf(out, "\n-Post: "); + type_format_cat(out, pass.post); + // locale_format_dt_cat(out, &pass.start); + furi_string_cat_printf(out, "\n-Date: "); + locale_format_dt_cat(out, &pass.date); +} + +void passes_format_cat(FuriString* out, Pass* passes) { + // only print passes if DEBUG on + if(!is_debug()) { + return; + } + + // only print if there is at least 1 valid pass to print + bool any_valid = false; + for(size_t i = 0; i < CHARLIE_N_PASSES; i++) { + any_valid |= passes[i].valid; + } + if(!any_valid) { + return; + } + + furi_string_cat_printf(out, "\nPasses (DEBUG / WIP):"); + for(size_t i = 0; i < CHARLIE_N_PASSES; i++) { + if(passes[i].valid) { + furi_string_cat_printf(out, "\nPass %u", i + 1); + pass_format_cat(out, passes[i]); + furi_string_cat_printf(out, "\n"); + } + } +} + 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) { +void transaction_format_cat(FuriString* out, Transaction transaction) { 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)) { + locale_format_dt_cat(out, &transaction.date); + furi_string_cat_printf(out, "\n%s", !!(transaction.g_flag & 0x1) ? "-" : "+"); + money_format_cat(out, transaction.fare); + if(!!(transaction.g_flag & 0x1) && (transaction.fare.dollars == FARE_BUS.dollars) && + (transaction.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)) { + // format for bus — supposedly some correlation between gate ID & bus #, haven't investigated + furi_string_cat_printf(out, "%s#%u", sep, transaction.gate); + } else if(get_map_item(transaction.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); + furi_string_cat_printf(out, "%s#%u", sep, transaction.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); + if(is_debug()) { + furi_string_cat_printf(out, "%s%x%s%x", sep, transaction.g_flag, sep, transaction.f_flag); } } +void transactions_format_cat(FuriString* out, Transaction* transactions) { + furi_string_cat_printf(out, "\nTransactions:"); + for(size_t i = 0; i < CHARLIE_N_TRANSACTION_HISTORY; i++) { + furi_string_cat_printf(out, "\n"); + transaction_format_cat(out, transactions[i]); + furi_string_cat_printf(out, "\n"); + } +} + +// ********************************************************** +// **************** NFC PLUGIN BOILERPLATE ****************** +// ********************************************************** + static bool charliecard_parse(const NfcDevice* device, FuriString* parsed_data) { furi_assert(device); @@ -989,48 +1132,50 @@ static bool charliecard_parse(const NfcDevice* device, FuriString* parsed_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; + // parse card data + const uint32_t card_number = mfg_sector_parse(data); + const CounterSector counter_sector = counter_sector_parse(data); + const BalanceSector balance_sector = + balance_sector_parse(data, counter_sector.active_balance_sector); + Pass* passes = passes_parse(data); + Transaction* transactions = transactions_parse(data); + // print/append card data 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); + // Type and balance 0 on some (Perq) cards + // (ie no "main" type / balance / end validity, + // essentially only pass & trip info) + // skip/change formatting for that case? furi_string_cat_printf(parsed_data, "\nBal: "); - money_format_cat(parsed_data, bal); + money_format_cat(parsed_data, balance_sector.balance); - const uint16_t type = type_parse(data); furi_string_cat_printf(parsed_data, "\nType: "); - type_format_cat(parsed_data, type); + type_format_cat(parsed_data, balance_sector.type); - const uint16_t n_trips = n_uses(data, active_sec_enum); - furi_string_cat_printf(parsed_data, "\nTrip Count: %u", n_trips); + furi_string_cat_printf(parsed_data, "\nTrip Count: %u", counter_sector.n_uses); - const DateTime iss = date_parse(data, active_sector, 0, 6); furi_string_cat_printf(parsed_data, "\nIssued: "); - locale_format_dt_cat(parsed_data, &iss); + locale_format_dt_cat(parsed_data, &balance_sector.issued); + + if(!dt_eq(balance_sector.end_validity, CHARLIE_EPOCH) & + dt_ge(balance_sector.end_validity, balance_sector.issued)) { + // sometimes (seen on Perq cards) end validity field is all 0 + // When this is the case, calc'd end validity is equal to CHARLIE_EPOCH). + // Only print if not 0, & end validity after issuance date + furi_string_cat_printf(parsed_data, "\nExpiry: "); + locale_format_dt_cat(parsed_data, &balance_sector.end_validity); + } - 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); + // const DateTime last = date_parse(data, active_sector, 0, 1); + // furi_string_cat_printf(parsed_data, "\nExpired: %s", expired(e_v, last) ? "Yes" : "No"); - DateTime last = date_parse(data, active_sector, 0, 1); - furi_string_cat_printf(parsed_data, "\nExpired: %s", expired(e_v, last) ? "Yes" : "No"); + transactions_format_cat(parsed_data, transactions); + free(transactions); - 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); + passes_format_cat(parsed_data, passes); + free(passes); parsed = true; } while(false); @@ -1038,6 +1183,78 @@ static bool charliecard_parse(const NfcDevice* device, FuriString* parsed_data) return parsed; } +static bool charliecard_verify(Nfc* nfc) { + 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; +} + /* Actual implementation of app<>plugin interface */ static const NfcSupportedCardsPlugin charliecard_plugin = { .protocol = NfcProtocolMfClassic, diff --git a/applications/main/nfc/plugins/supported_cards/social_moscow.c b/applications/main/nfc/plugins/supported_cards/social_moscow.c index 6123c00292..3e97d0a9a7 100644 --- a/applications/main/nfc/plugins/supported_cards/social_moscow.c +++ b/applications/main/nfc/plugins/supported_cards/social_moscow.c @@ -82,7 +82,7 @@ void from_minutes_to_datetime(uint32_t minutes, DateTime* datetime, uint16_t sta bool parse_transport_block(const MfClassicBlock* block, FuriString* result) { uint16_t transport_departament = bit_lib_get_bits_16(block->data, 0, 10); - FURI_LOG_I(TAG, "Transport departament: %x", transport_departament); + FURI_LOG_D(TAG, "Transport departament: %x", transport_departament); uint16_t layout_type = bit_lib_get_bits_16(block->data, 52, 4); if(layout_type == 0xE) { @@ -91,7 +91,7 @@ bool parse_transport_block(const MfClassicBlock* block, FuriString* result) { layout_type = bit_lib_get_bits_16(block->data, 52, 14); } - FURI_LOG_I(TAG, "Layout type %x", layout_type); + FURI_LOG_D(TAG, "Layout type %x", layout_type); uint16_t card_view = 0; uint16_t card_type = 0; diff --git a/applications/main/nfc/scenes/nfc_scene_config.h b/applications/main/nfc/scenes/nfc_scene_config.h index e522cd059a..2457fb1561 100644 --- a/applications/main/nfc/scenes/nfc_scene_config.h +++ b/applications/main/nfc/scenes/nfc_scene_config.h @@ -33,6 +33,8 @@ ADD_SCENE(nfc, mf_ultralight_unlock_menu, MfUltralightUnlockMenu) ADD_SCENE(nfc, mf_ultralight_unlock_warn, MfUltralightUnlockWarn) ADD_SCENE(nfc, mf_ultralight_key_input, MfUltralightKeyInput) ADD_SCENE(nfc, mf_ultralight_capture_pass, MfUltralightCapturePass) +ADD_SCENE(nfc, felica_key_input, FelicaKeyInput) +ADD_SCENE(nfc, felica_unlock_warn, FelicaUnlockWarn) ADD_SCENE(nfc, mf_desfire_more_info, MfDesfireMoreInfo) ADD_SCENE(nfc, mf_desfire_app, MfDesfireApp) diff --git a/applications/main/nfc/scenes/nfc_scene_felica_key_input.c b/applications/main/nfc/scenes/nfc_scene_felica_key_input.c new file mode 100644 index 0000000000..b04f12dae8 --- /dev/null +++ b/applications/main/nfc/scenes/nfc_scene_felica_key_input.c @@ -0,0 +1,46 @@ +#include "../nfc_app_i.h" + +void nfc_scene_felica_key_input_byte_input_callback(void* context) { + NfcApp* nfc = context; + + view_dispatcher_send_custom_event(nfc->view_dispatcher, NfcCustomEventByteInputDone); +} + +void nfc_scene_felica_key_input_on_enter(void* context) { + NfcApp* nfc = context; + + // Setup view + ByteInput* byte_input = nfc->byte_input; + byte_input_set_header_text(byte_input, "Enter key in hex"); + byte_input_set_result_callback( + byte_input, + nfc_scene_felica_key_input_byte_input_callback, + NULL, + nfc, + nfc->felica_auth->card_key.data, + FELICA_DATA_BLOCK_SIZE); + view_dispatcher_switch_to_view(nfc->view_dispatcher, NfcViewByteInput); +} + +bool nfc_scene_felica_key_input_on_event(void* context, SceneManagerEvent event) { + NfcApp* nfc = context; + UNUSED(event); + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == NfcCustomEventByteInputDone) { + nfc->felica_auth->skip_auth = false; + scene_manager_next_scene(nfc->scene_manager, NfcSceneFelicaUnlockWarn); + consumed = true; + } + } + return consumed; +} + +void nfc_scene_felica_key_input_on_exit(void* context) { + NfcApp* nfc = context; + + // Clear view + byte_input_set_result_callback(nfc->byte_input, NULL, NULL, NULL, NULL, 0); + byte_input_set_header_text(nfc->byte_input, ""); +} diff --git a/applications/main/nfc/scenes/nfc_scene_felica_unlock_warn.c b/applications/main/nfc/scenes/nfc_scene_felica_unlock_warn.c new file mode 100644 index 0000000000..15b61dfa51 --- /dev/null +++ b/applications/main/nfc/scenes/nfc_scene_felica_unlock_warn.c @@ -0,0 +1,59 @@ +#include "../nfc_app_i.h" + +void nfc_scene_felica_unlock_warn_dialog_callback(DialogExResult result, void* context) { + NfcApp* nfc = context; + + view_dispatcher_send_custom_event(nfc->view_dispatcher, result); +} + +void nfc_scene_felica_unlock_warn_on_enter(void* context) { + NfcApp* nfc = context; + + const char* message = "Risky Action!"; + DialogEx* dialog_ex = nfc->dialog_ex; + dialog_ex_set_context(dialog_ex, nfc); + dialog_ex_set_result_callback(dialog_ex, nfc_scene_felica_unlock_warn_dialog_callback); + + dialog_ex_set_header(dialog_ex, message, 64, 0, AlignCenter, AlignTop); + + FuriString* str = furi_string_alloc(); + furi_string_cat_printf(str, "Unlock with key: "); + for(uint8_t i = 0; i < FELICA_DATA_BLOCK_SIZE; i++) + furi_string_cat_printf(str, "%02X ", nfc->felica_auth->card_key.data[i]); + furi_string_cat_printf(str, "?"); + + nfc_text_store_set(nfc, furi_string_get_cstr(str)); + furi_string_free(str); + + dialog_ex_set_text(dialog_ex, nfc->text_store, 0, 12, AlignLeft, AlignTop); + + dialog_ex_set_left_button_text(dialog_ex, "Cancel"); + dialog_ex_set_right_button_text(dialog_ex, "Unlock"); + + view_dispatcher_switch_to_view(nfc->view_dispatcher, NfcViewDialogEx); +} + +bool nfc_scene_felica_unlock_warn_on_event(void* context, SceneManagerEvent event) { + NfcApp* nfc = context; + UNUSED(event); + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == DialogExResultRight) { + nfc->felica_auth->skip_auth = false; + scene_manager_next_scene(nfc->scene_manager, NfcSceneRead); + consumed = true; + } else if(event.event == DialogExResultLeft) { + scene_manager_previous_scene(nfc->scene_manager); + consumed = true; + } + } + return consumed; +} + +void nfc_scene_felica_unlock_warn_on_exit(void* context) { + NfcApp* nfc = context; + + dialog_ex_reset(nfc->dialog_ex); + nfc_text_store_clear(nfc); +} diff --git a/applications/main/nfc/scenes/nfc_scene_mf_desfire_more_info.c b/applications/main/nfc/scenes/nfc_scene_mf_desfire_more_info.c index 76834e3f4f..3e111c723c 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_desfire_more_info.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_desfire_more_info.c @@ -35,7 +35,7 @@ void nfc_scene_mf_desfire_more_info_on_enter(void* context) { for(uint32_t i = 0; i < simple_array_get_count(data->application_ids); ++i) { const MfDesfireApplicationId* app_id = simple_array_cget(data->application_ids, i); furi_string_printf( - label, "App %02x%02x%02x", app_id->data[0], app_id->data[1], app_id->data[2]); + label, "App %02x%02x%02x", app_id->data[2], app_id->data[1], app_id->data[0]); submenu_add_item( submenu, furi_string_get_cstr(label), diff --git a/applications/main/subghz/scenes/subghz_scene_frequency_analyzer.c b/applications/main/subghz/scenes/subghz_scene_frequency_analyzer.c index 308f6dbb33..4343040244 100644 --- a/applications/main/subghz/scenes/subghz_scene_frequency_analyzer.c +++ b/applications/main/subghz/scenes/subghz_scene_frequency_analyzer.c @@ -60,9 +60,6 @@ bool subghz_scene_frequency_analyzer_on_event(void* context, SceneManagerEvent e subghz_frequency_analyzer_get_frequency_to_save(subghz->subghz_frequency_analyzer); if(frequency > 0) { subghz->last_settings->frequency = frequency; -#ifdef FURI_DEBUG - subghz_last_settings_log(subghz->last_settings); -#endif // Disable Hopping before opening the receiver scene! if(subghz->last_settings->enable_hopping) { subghz->last_settings->enable_hopping = false; @@ -73,9 +70,6 @@ bool subghz_scene_frequency_analyzer_on_event(void* context, SceneManagerEvent e return true; } else if(event.event == SubGhzCustomEventViewFreqAnalOkLong) { // Don't need to save, we already saved on short event -#ifdef FURI_DEBUG - FURI_LOG_W(TAG, "Goto next scene!"); -#endif //scene_manager_set_scene_state(subghz->scene_manager, SubGhzSceneStart, 10); scene_manager_next_scene(subghz->scene_manager, SubGhzSceneReceiver); return true; diff --git a/applications/main/subghz/scenes/subghz_scene_radio_settings.c b/applications/main/subghz/scenes/subghz_scene_radio_settings.c index f293bc86c7..270d999c0c 100644 --- a/applications/main/subghz/scenes/subghz_scene_radio_settings.c +++ b/applications/main/subghz/scenes/subghz_scene_radio_settings.c @@ -20,12 +20,6 @@ const char* const timestamp_names_text[TIMESTAMP_NAMES_COUNT] = { "ON", }; -#define EXT_MOD_POWER_AMP_COUNT 2 -const char* const ext_mod_power_amp_text[EXT_MOD_POWER_AMP_COUNT] = { - "OFF", - "ON", -}; - #define DEBUG_P_COUNT 2 const char* const debug_pin_text[DEBUG_P_COUNT] = { "OFF", @@ -104,27 +98,6 @@ static void subghz_scene_receiver_config_set_debug_counter(VariableItem* item) { furi_hal_subghz_set_rolling_counter_mult(debug_counter_val[index]); } -static void subghz_scene_reciever_config_set_ext_mod_power_amp_text(VariableItem* item) { - SubGhz* subghz = variable_item_get_context(item); - uint8_t index = variable_item_get_current_value_index(item); - - variable_item_set_current_value_text(item, ext_mod_power_amp_text[index]); - - subghz->last_settings->external_module_power_amp = index == 1; - - // Set globally in furi hal - furi_hal_subghz_set_ext_power_amp(subghz->last_settings->external_module_power_amp); - - subghz_last_settings_save(subghz->last_settings); - - // reinit external device - const SubGhzRadioDeviceType current = subghz_txrx_radio_device_get(subghz->txrx); - if(current != SubGhzRadioDeviceTypeInternal) { - subghz_txrx_radio_device_set(subghz->txrx, SubGhzRadioDeviceTypeInternal); - subghz_txrx_radio_device_set(subghz->txrx, current); - } -} - static void subghz_scene_receiver_config_set_gps(VariableItem* item) { SubGhz* subghz = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); @@ -194,16 +167,6 @@ void subghz_scene_radio_settings_on_enter(void* context) { variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, radio_device_text[value_index]); - item = variable_item_list_add( - variable_item_list, - "Ext Power Amp", - EXT_MOD_POWER_AMP_COUNT, - subghz_scene_reciever_config_set_ext_mod_power_amp_text, - subghz); - value_index = subghz->last_settings->external_module_power_amp ? 1 : 0; - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, ext_mod_power_amp_text[value_index]); - item = variable_item_list_add( variable_item_list, "GPS Baudrate", diff --git a/applications/main/subghz/scenes/subghz_scene_read_raw.c b/applications/main/subghz/scenes/subghz_scene_read_raw.c index ca12689178..874c3b3ed5 100644 --- a/applications/main/subghz/scenes/subghz_scene_read_raw.c +++ b/applications/main/subghz/scenes/subghz_scene_read_raw.c @@ -104,14 +104,13 @@ void subghz_scene_read_raw_on_enter(void* context) { if(subghz_rx_key_state_get(subghz) != SubGhzRxKeyStateBack) { subghz_rx_key_state_set(subghz, SubGhzRxKeyStateIDLE); -#if SUBGHZ_LAST_SETTING_SAVE_PRESET + if(furi_string_empty(file_name)) { subghz_txrx_set_preset_internal( subghz->txrx, subghz->last_settings->frequency, subghz->last_settings->preset_index); } -#endif } subghz_scene_read_raw_update_statusbar(subghz); diff --git a/applications/main/subghz/scenes/subghz_scene_receiver.c b/applications/main/subghz/scenes/subghz_scene_receiver.c index cd300e7c13..32ad1d2c95 100644 --- a/applications/main/subghz/scenes/subghz_scene_receiver.c +++ b/applications/main/subghz/scenes/subghz_scene_receiver.c @@ -250,12 +250,8 @@ void subghz_scene_receiver_on_enter(void* context) { FuriString* item_time = furi_string_alloc(); if(subghz_rx_key_state_get(subghz) == SubGhzRxKeyStateIDLE) { -#if SUBGHZ_LAST_SETTING_SAVE_PRESET subghz_txrx_set_preset_internal( subghz->txrx, subghz->last_settings->frequency, subghz->last_settings->preset_index); -#else - subghz_txrx_set_default_preset(subghz->txrx, subghz->last_settings->frequency); -#endif subghz->filter = subghz->last_settings->filter; subghz_txrx_receiver_set_filter(subghz->txrx, subghz->filter); diff --git a/applications/main/subghz/scenes/subghz_scene_receiver_config.c b/applications/main/subghz/scenes/subghz_scene_receiver_config.c index ce0d82ee6b..7986cbe661 100644 --- a/applications/main/subghz/scenes/subghz_scene_receiver_config.c +++ b/applications/main/subghz/scenes/subghz_scene_receiver_config.c @@ -410,14 +410,11 @@ static void subghz_scene_receiver_config_var_list_enter_callback(void* context, subghz->view_dispatcher, SubGhzCustomEventSceneSettingLock); } else if(index == SubGhzSettingIndexResetToDefault) { // Reset all values to default state! -#if SUBGHZ_LAST_SETTING_SAVE_PRESET subghz_txrx_set_preset_internal( subghz->txrx, SUBGHZ_LAST_SETTING_DEFAULT_FREQUENCY, SUBGHZ_LAST_SETTING_DEFAULT_PRESET); -#else - subghz_txrx_set_default_preset(subghz->txrx, SUBGHZ_LAST_SETTING_DEFAULT_FREQUENCY); -#endif + SubGhzSetting* setting = subghz_txrx_get_setting(subghz->txrx); SubGhzRadioPreset preset = subghz_txrx_get_preset(subghz->txrx); const char* preset_name = furi_string_get_cstr(preset.name); @@ -449,9 +446,7 @@ static void subghz_scene_receiver_config_var_list_enter_callback(void* context, variable_item_list_set_selected_item(subghz->variable_item_list, default_index); variable_item_list_reset(subghz->variable_item_list); -#ifdef FURI_DEBUG - subghz_last_settings_log(subghz->last_settings); -#endif + subghz_last_settings_save(subghz->last_settings); view_dispatcher_send_custom_event( @@ -745,9 +740,7 @@ void subghz_scene_receiver_config_on_exit(void* context) { variable_item_list_set_selected_item(subghz->variable_item_list, 0); variable_item_list_reset(subghz->variable_item_list); -#ifdef FURI_DEBUG - subghz_last_settings_log(subghz->last_settings); -#endif + subghz_last_settings_save(subghz->last_settings); scene_manager_set_scene_state( subghz->scene_manager, SubGhzSceneReadRAW, SubGhzCustomEventManagerNoSet); diff --git a/applications/main/subghz/scenes/subghz_scene_set_cnt.c b/applications/main/subghz/scenes/subghz_scene_set_cnt.c index 4952c2b14e..b202d6d91e 100644 --- a/applications/main/subghz/scenes/subghz_scene_set_cnt.c +++ b/applications/main/subghz/scenes/subghz_scene_set_cnt.c @@ -18,7 +18,7 @@ void subghz_scene_set_cnt_on_enter(void* context) { switch(state) { case SetTypeBFTClone: - byte_input_set_header_text(byte_input, "Enter COUNTER in Hex"); + byte_input_set_header_text(byte_input, "Enter COUNTER in hex"); byte_input_set_result_callback( byte_input, subghz_scene_set_cnt_byte_input_callback, @@ -29,7 +29,7 @@ void subghz_scene_set_cnt_on_enter(void* context) { break; case SetTypeFaacSLH_Manual_433: case SetTypeFaacSLH_Manual_868: - byte_input_set_header_text(byte_input, "Enter COUNTER in Hex 20 bits"); + byte_input_set_header_text(byte_input, "Enter COUNTER in hex 20 bits"); byte_input_set_result_callback( byte_input, subghz_scene_set_cnt_byte_input_callback, diff --git a/applications/main/subghz/scenes/subghz_scene_set_fix.c b/applications/main/subghz/scenes/subghz_scene_set_fix.c index b58f39ddd0..7564fb24d7 100644 --- a/applications/main/subghz/scenes/subghz_scene_set_fix.c +++ b/applications/main/subghz/scenes/subghz_scene_set_fix.c @@ -13,7 +13,7 @@ void subghz_scene_set_fix_on_enter(void* context) { // Setup view ByteInput* byte_input = subghz->byte_input; - byte_input_set_header_text(byte_input, "Enter FIX in Hex"); + byte_input_set_header_text(byte_input, "Enter FIX in hex"); byte_input_set_result_callback( byte_input, subghz_scene_set_fix_byte_input_callback, diff --git a/applications/main/subghz/scenes/subghz_scene_set_seed.c b/applications/main/subghz/scenes/subghz_scene_set_seed.c index a22daf9196..c8301745fa 100644 --- a/applications/main/subghz/scenes/subghz_scene_set_seed.c +++ b/applications/main/subghz/scenes/subghz_scene_set_seed.c @@ -14,7 +14,7 @@ void subghz_scene_set_seed_on_enter(void* context) { // Setup view ByteInput* byte_input = subghz->byte_input; - byte_input_set_header_text(byte_input, "Enter SEED in Hex"); + byte_input_set_header_text(byte_input, "Enter SEED in hex"); byte_input_set_result_callback( byte_input, subghz_scene_set_seed_byte_input_callback, diff --git a/applications/main/subghz/scenes/subghz_scene_set_type.c b/applications/main/subghz/scenes/subghz_scene_set_type.c index 90b30f0768..b88e449b15 100644 --- a/applications/main/subghz/scenes/subghz_scene_set_type.c +++ b/applications/main/subghz/scenes/subghz_scene_set_type.c @@ -445,8 +445,8 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) { .type = GenKeeloq, .mod = "FM476", .freq = 434420000, - .keeloq.serial = key & 0x0FFFFFFF, - .keeloq.btn = 0x04, + .keeloq.serial = (key & 0x0000FFFF) | 0x07150000, + .keeloq.btn = 0x02, .keeloq.cnt = 0x03, .keeloq.manuf = "Sommer(fsk476)"}; break; @@ -455,8 +455,8 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) { .type = GenKeeloq, .mod = "FM476", .freq = 868800000, - .keeloq.serial = key & 0x0FFFFFFF, - .keeloq.btn = 0x04, + .keeloq.serial = (key & 0x0000FFFF) | 0x07150000, + .keeloq.btn = 0x02, .keeloq.cnt = 0x03, .keeloq.manuf = "Sommer(fsk476)"}; break; diff --git a/applications/main/subghz/subghz.c b/applications/main/subghz/subghz.c index a884759dd6..7d21bb2cf3 100644 --- a/applications/main/subghz/subghz.c +++ b/applications/main/subghz/subghz.c @@ -207,16 +207,9 @@ SubGhz* subghz_alloc(bool alloc_for_tx_only) { subghz->last_settings = subghz_last_settings_alloc(); size_t preset_count = subghz_setting_get_preset_count(setting); subghz_last_settings_load(subghz->last_settings, preset_count); -#ifdef FURI_DEBUG - subghz_last_settings_log(subghz->last_settings); -#endif if(!alloc_for_tx_only) { -#if SUBGHZ_LAST_SETTING_SAVE_PRESET subghz_txrx_set_preset_internal( subghz->txrx, subghz->last_settings->frequency, subghz->last_settings->preset_index); -#else - subghz_txrx_set_default_preset(subghz->txrx, subghz->last_settings->frequency); -#endif subghz->history = subghz_history_alloc(); } diff --git a/applications/main/subghz/subghz_extended_freq.c b/applications/main/subghz/subghz_extended_freq.c index d7f41a25bf..eebcc5856c 100644 --- a/applications/main/subghz/subghz_extended_freq.c +++ b/applications/main/subghz/subghz_extended_freq.c @@ -26,8 +26,5 @@ void subghz_extended_freq() { SubGhzLastSettings* last_settings = subghz_last_settings_alloc(); subghz_last_settings_load(last_settings, 0); - // Set globally in furi hal - furi_hal_subghz_set_ext_power_amp(last_settings->external_module_power_amp); - subghz_last_settings_free(last_settings); } diff --git a/applications/main/subghz/subghz_last_settings.c b/applications/main/subghz/subghz_last_settings.c index 0ab6213067..4d1e44dc6e 100644 --- a/applications/main/subghz/subghz_last_settings.c +++ b/applications/main/subghz/subghz_last_settings.c @@ -4,26 +4,24 @@ #define TAG "SubGhzLastSettings" #define SUBGHZ_LAST_SETTING_FILE_TYPE "Flipper SubGhz Last Setting File" -#define SUBGHZ_LAST_SETTING_FILE_VERSION 1 +#define SUBGHZ_LAST_SETTING_FILE_VERSION 3 #define SUBGHZ_LAST_SETTINGS_PATH EXT_PATH("subghz/assets/last_subghz.settings") #define SUBGHZ_LAST_SETTING_FIELD_FREQUENCY "Frequency" #define SUBGHZ_LAST_SETTING_FIELD_PRESET "Preset" // AKA Modulation #define SUBGHZ_LAST_SETTING_FIELD_FREQUENCY_ANALYZER_FEEDBACK_LEVEL "FeedbackLevel" #define SUBGHZ_LAST_SETTING_FIELD_FREQUENCY_ANALYZER_TRIGGER "FATrigger" -#define SUBGHZ_LAST_SETTING_FIELD_EXTERNAL_MODULE_ENABLED "External" -#define SUBGHZ_LAST_SETTING_FIELD_EXTERNAL_MODULE_POWER "ExtPower" -#define SUBGHZ_LAST_SETTING_FIELD_PROTOCOL_FILE_NAMES "TimestampNames" -#define SUBGHZ_LAST_SETTING_FIELD_EXTERNAL_MODULE_POWER_AMP "ExtPowerAmp" -#define SUBGHZ_LAST_SETTING_FIELD_GPS "Gps" +#define SUBGHZ_LAST_SETTING_FIELD_PROTOCOL_FILE_NAMES "ProtocolNames" #define SUBGHZ_LAST_SETTING_FIELD_HOPPING_ENABLE "Hopping" -#define SUBGHZ_LAST_SETTING_FIELD_REMOVE_DUPLICATES "RemoveDuplicates" #define SUBGHZ_LAST_SETTING_FIELD_IGNORE_FILTER "IgnoreFilter" #define SUBGHZ_LAST_SETTING_FIELD_FILTER "Filter" #define SUBGHZ_LAST_SETTING_FIELD_RSSI_THRESHOLD "RSSI" +#define SUBGHZ_LAST_SETTING_FIELD_DELETE_OLD "DelOldSignals" + +#define SUBGHZ_LAST_SETTING_FIELD_GPS_BAUDRATE "GpsBaudrate" +#define SUBGHZ_LAST_SETTING_FIELD_REMOVE_DUPLICATES "RemoveDuplicates" #define SUBGHZ_LAST_SETTING_FIELD_REPEATER "Repeater" #define SUBGHZ_LAST_SETTING_FIELD_ENABLE_SOUND "Sound" -#define SUBGHZ_LAST_SETTING_FIELD_DELETE_OLD "DelOldSignals" #define SUBGHZ_LAST_SETTING_FIELD_AUTOSAVE "Autosave" SubGhzLastSettings* subghz_last_settings_alloc(void) { @@ -39,202 +37,145 @@ void subghz_last_settings_free(SubGhzLastSettings* instance) { void subghz_last_settings_load(SubGhzLastSettings* instance, size_t preset_count) { furi_assert(instance); + // Default values (all others set to 0, if read from file fails these are used) + instance->frequency = SUBGHZ_LAST_SETTING_DEFAULT_FREQUENCY; + instance->preset_index = SUBGHZ_LAST_SETTING_DEFAULT_PRESET; + instance->frequency_analyzer_feedback_level = + SUBGHZ_LAST_SETTING_FREQUENCY_ANALYZER_FEEDBACK_LEVEL; + instance->frequency_analyzer_trigger = SUBGHZ_LAST_SETTING_FREQUENCY_ANALYZER_TRIGGER; + // See bin_raw_value in scenes/subghz_scene_receiver_config.c + instance->filter = SubGhzProtocolFlag_Decodable; + instance->rssi = SUBGHZ_RAW_THRESHOLD_MIN; + Storage* storage = furi_record_open(RECORD_STORAGE); FlipperFormat* fff_data_file = flipper_format_file_alloc(storage); - uint32_t temp_frequency = 0; - uint32_t temp_frequency_analyzer_feedback_level = 0; - float temp_frequency_analyzer_trigger = 0; - bool temp_external_module_enabled = false; - bool temp_external_module_power_5v_disable = false; - bool temp_external_module_power_amp = false; - bool temp_protocol_file_names = false; - bool temp_enable_hopping = false; - bool temp_enable_sound = false; - uint32_t temp_repeater_state; - bool temp_remove_duplicates = false; - bool temp_delete_old_sig = false; - bool temp_autosave = false; - uint32_t temp_ignore_filter = 0; - uint32_t temp_filter = 0; - float temp_rssi = 0; - uint32_t temp_preset = 0; - - bool preset_was_read = false; - bool rssi_was_read = false; - bool filter_was_read = false; - bool ignore_filter_was_read = false; - bool remove_duplicates_was_read = false; - bool frequency_analyzer_feedback_level_was_read = false; - bool frequency_analyzer_trigger_was_read = false; - bool repeater_was_read = false; - bool enable_sound_was_read = false; + FuriString* temp_str = furi_string_alloc(); + uint32_t config_version = 0; - uint32_t temp_gps_baudrate = 0; - - if(FSE_OK == storage_sd_status(storage) && SUBGHZ_LAST_SETTINGS_PATH && + if(FSE_OK == storage_sd_status(storage) && flipper_format_file_open_existing(fff_data_file, SUBGHZ_LAST_SETTINGS_PATH)) { - preset_was_read = flipper_format_read_uint32( - fff_data_file, SUBGHZ_LAST_SETTING_FIELD_PRESET, (uint32_t*)&temp_preset, 1); - flipper_format_read_uint32( - fff_data_file, SUBGHZ_LAST_SETTING_FIELD_FREQUENCY, (uint32_t*)&temp_frequency, 1); - frequency_analyzer_feedback_level_was_read = flipper_format_read_uint32( - fff_data_file, - SUBGHZ_LAST_SETTING_FIELD_FREQUENCY_ANALYZER_FEEDBACK_LEVEL, - (uint32_t*)&temp_frequency_analyzer_feedback_level, - 1); - frequency_analyzer_trigger_was_read = flipper_format_read_float( - fff_data_file, - SUBGHZ_LAST_SETTING_FIELD_FREQUENCY_ANALYZER_TRIGGER, - (float*)&temp_frequency_analyzer_trigger, - 1); - flipper_format_read_bool( - fff_data_file, - SUBGHZ_LAST_SETTING_FIELD_EXTERNAL_MODULE_ENABLED, - (bool*)&temp_external_module_enabled, - 1); - flipper_format_read_bool( - fff_data_file, - SUBGHZ_LAST_SETTING_FIELD_EXTERNAL_MODULE_POWER, - (bool*)&temp_external_module_power_5v_disable, - 1); - flipper_format_read_bool( - fff_data_file, - SUBGHZ_LAST_SETTING_FIELD_PROTOCOL_FILE_NAMES, - (bool*)&temp_protocol_file_names, - 1); - flipper_format_read_bool( - fff_data_file, - SUBGHZ_LAST_SETTING_FIELD_EXTERNAL_MODULE_POWER_AMP, - (bool*)&temp_external_module_power_amp, - 1); - flipper_format_read_uint32( - fff_data_file, SUBGHZ_LAST_SETTING_FIELD_GPS, (uint32_t*)&temp_gps_baudrate, 1); - flipper_format_read_bool( - fff_data_file, - SUBGHZ_LAST_SETTING_FIELD_HOPPING_ENABLE, - (bool*)&temp_enable_hopping, - 1); - rssi_was_read = flipper_format_read_float( - fff_data_file, SUBGHZ_LAST_SETTING_FIELD_RSSI_THRESHOLD, (float*)&temp_rssi, 1); - remove_duplicates_was_read = flipper_format_read_bool( - fff_data_file, - SUBGHZ_LAST_SETTING_FIELD_REMOVE_DUPLICATES, - (bool*)&temp_remove_duplicates, - 1); - ignore_filter_was_read = flipper_format_read_uint32( - fff_data_file, - SUBGHZ_LAST_SETTING_FIELD_IGNORE_FILTER, - (uint32_t*)&temp_ignore_filter, - 1); - filter_was_read = flipper_format_read_uint32( - fff_data_file, SUBGHZ_LAST_SETTING_FIELD_FILTER, (uint32_t*)&temp_filter, 1); - repeater_was_read = flipper_format_read_uint32( - fff_data_file, SUBGHZ_LAST_SETTING_FIELD_REPEATER, (uint32_t*)&temp_repeater_state, 1); - enable_sound_was_read = flipper_format_read_bool( - fff_data_file, SUBGHZ_LAST_SETTING_FIELD_ENABLE_SOUND, (bool*)&temp_enable_sound, 1); - flipper_format_read_bool( - fff_data_file, SUBGHZ_LAST_SETTING_FIELD_DELETE_OLD, (bool*)&temp_delete_old_sig, 1); - flipper_format_read_bool( - fff_data_file, SUBGHZ_LAST_SETTING_FIELD_AUTOSAVE, (bool*)&temp_autosave, 1); - + do { + if(!flipper_format_read_header(fff_data_file, temp_str, &config_version)) break; + if((strcmp(furi_string_get_cstr(temp_str), SUBGHZ_LAST_SETTING_FILE_TYPE) != 0) || + (config_version != SUBGHZ_LAST_SETTING_FILE_VERSION)) { + break; + } + + if(!flipper_format_read_uint32( + fff_data_file, SUBGHZ_LAST_SETTING_FIELD_FREQUENCY, &instance->frequency, 1)) { + flipper_format_rewind(fff_data_file); + } + if(!flipper_format_read_uint32( + fff_data_file, SUBGHZ_LAST_SETTING_FIELD_PRESET, &instance->preset_index, 1)) { + flipper_format_rewind(fff_data_file); + } + if(!flipper_format_read_uint32( + fff_data_file, + SUBGHZ_LAST_SETTING_FIELD_FREQUENCY_ANALYZER_FEEDBACK_LEVEL, + &instance->frequency_analyzer_feedback_level, + 1)) { + flipper_format_rewind(fff_data_file); + } + if(!flipper_format_read_float( + fff_data_file, + SUBGHZ_LAST_SETTING_FIELD_FREQUENCY_ANALYZER_TRIGGER, + &instance->frequency_analyzer_trigger, + 1)) { + flipper_format_rewind(fff_data_file); + } + if(!flipper_format_read_bool( + fff_data_file, + SUBGHZ_LAST_SETTING_FIELD_PROTOCOL_FILE_NAMES, + &instance->protocol_file_names, + 1)) { + flipper_format_rewind(fff_data_file); + } + if(!flipper_format_read_bool( + fff_data_file, + SUBGHZ_LAST_SETTING_FIELD_HOPPING_ENABLE, + &instance->enable_hopping, + 1)) { + flipper_format_rewind(fff_data_file); + } + if(!flipper_format_read_uint32( + fff_data_file, + SUBGHZ_LAST_SETTING_FIELD_IGNORE_FILTER, + &instance->ignore_filter, + 1)) { + flipper_format_rewind(fff_data_file); + } + if(!flipper_format_read_uint32( + fff_data_file, SUBGHZ_LAST_SETTING_FIELD_FILTER, &instance->filter, 1)) { + flipper_format_rewind(fff_data_file); + } + if(!flipper_format_read_float( + fff_data_file, SUBGHZ_LAST_SETTING_FIELD_RSSI_THRESHOLD, &instance->rssi, 1)) { + flipper_format_rewind(fff_data_file); + } + if(!flipper_format_read_bool( + fff_data_file, + SUBGHZ_LAST_SETTING_FIELD_DELETE_OLD, + &instance->delete_old_signals, + 1)) { + flipper_format_rewind(fff_data_file); + } + + if(!flipper_format_read_uint32( + fff_data_file, + SUBGHZ_LAST_SETTING_FIELD_GPS_BAUDRATE, + &instance->gps_baudrate, + 1)) { + flipper_format_rewind(fff_data_file); + } + if(!flipper_format_read_bool( + fff_data_file, + SUBGHZ_LAST_SETTING_FIELD_REMOVE_DUPLICATES, + &instance->remove_duplicates, + 1)) { + flipper_format_rewind(fff_data_file); + } + if(!flipper_format_read_uint32( + fff_data_file, + SUBGHZ_LAST_SETTING_FIELD_REPEATER, + &instance->repeater_state, + 1)) { + flipper_format_rewind(fff_data_file); + } + if(!flipper_format_read_bool( + fff_data_file, + SUBGHZ_LAST_SETTING_FIELD_ENABLE_SOUND, + &instance->enable_sound, + 1)) { + flipper_format_rewind(fff_data_file); + } + if(!flipper_format_read_bool( + fff_data_file, SUBGHZ_LAST_SETTING_FIELD_AUTOSAVE, &instance->autosave, 1)) { + flipper_format_rewind(fff_data_file); + } + } while(0); } else { FURI_LOG_E(TAG, "Error open file %s", SUBGHZ_LAST_SETTINGS_PATH); } - if(temp_frequency == 0 || !furi_hal_subghz_is_tx_allowed(temp_frequency)) { - FURI_LOG_W(TAG, "Last used frequency not found or can't be used!"); - - instance->frequency = SUBGHZ_LAST_SETTING_DEFAULT_FREQUENCY; - instance->preset_index = SUBGHZ_LAST_SETTING_DEFAULT_PRESET; - instance->frequency_analyzer_feedback_level = - SUBGHZ_LAST_SETTING_FREQUENCY_ANALYZER_FEEDBACK_LEVEL; - instance->frequency_analyzer_trigger = SUBGHZ_LAST_SETTING_FREQUENCY_ANALYZER_TRIGGER; - instance->external_module_enabled = false; - instance->protocol_file_names = false; - instance->external_module_power_amp = false; - instance->gps_baudrate = 0; - instance->enable_hopping = false; - instance->remove_duplicates = false; - instance->repeater_state = 0; - instance->enable_sound = 0; - instance->delete_old_signals = false; - instance->autosave = false; - instance->ignore_filter = 0x00; - // See bin_raw_value in applications/main/subghz/scenes/subghz_scene_receiver_config.c - instance->filter = SubGhzProtocolFlag_Decodable; - instance->rssi = SUBGHZ_RAW_THRESHOLD_MIN; - } else { - instance->frequency = temp_frequency; - instance->frequency_analyzer_feedback_level = - frequency_analyzer_feedback_level_was_read ? - temp_frequency_analyzer_feedback_level : - SUBGHZ_LAST_SETTING_FREQUENCY_ANALYZER_FEEDBACK_LEVEL; - - instance->frequency_analyzer_trigger = frequency_analyzer_trigger_was_read ? - temp_frequency_analyzer_trigger : - SUBGHZ_LAST_SETTING_FREQUENCY_ANALYZER_TRIGGER; - - if(!preset_was_read) { - FURI_LOG_W(TAG, "Preset was not read. Set default"); - instance->preset_index = SUBGHZ_LAST_SETTING_DEFAULT_PRESET; - } else if(temp_preset > (uint32_t)preset_count - 1) { - FURI_LOG_W( - TAG, - "Last used preset out of range. Preset to set: %ld, Max index: %ld. Set default", - temp_preset, - (uint32_t)preset_count - 1); - instance->preset_index = SUBGHZ_LAST_SETTING_DEFAULT_PRESET; - } else { - instance->preset_index = temp_preset; - } - instance->external_module_enabled = temp_external_module_enabled; - - instance->external_module_power_5v_disable = temp_external_module_power_5v_disable; - - instance->protocol_file_names = temp_protocol_file_names; - - instance->delete_old_signals = temp_delete_old_sig; - - instance->autosave = temp_autosave; - - // External power amp CC1101 - instance->external_module_power_amp = temp_external_module_power_amp; - - instance->rssi = rssi_was_read ? temp_rssi : SUBGHZ_RAW_THRESHOLD_MIN; - instance->enable_hopping = temp_enable_hopping; - instance->repeater_state = repeater_was_read ? temp_repeater_state : 0; - instance->enable_sound = enable_sound_was_read ? temp_enable_sound : false; - instance->remove_duplicates = remove_duplicates_was_read ? temp_remove_duplicates : false; - instance->ignore_filter = ignore_filter_was_read ? temp_ignore_filter : 0x00; -#if SUBGHZ_LAST_SETTING_SAVE_BIN_RAW - instance->filter = filter_was_read ? temp_filter : SubGhzProtocolFlag_Decodable; -#else - if(filter_was_read) { - instance->filter = temp_filter != SubGhzProtocolFlag_Decodable ? - SubGhzProtocolFlag_Decodable : - temp_filter; - } else { - instance->filter = SubGhzProtocolFlag_Decodable; - } -#endif - // Set globally in furi hal - furi_hal_subghz_set_ext_power_amp(instance->external_module_power_amp); - - instance->gps_baudrate = temp_gps_baudrate; - } + furi_string_free(temp_str); flipper_format_file_close(fff_data_file); flipper_format_free(fff_data_file); furi_record_close(RECORD_STORAGE); + + if(instance->frequency == 0 || !furi_hal_subghz_is_tx_allowed(instance->frequency)) { + instance->frequency = SUBGHZ_LAST_SETTING_DEFAULT_FREQUENCY; + } + + if(instance->preset_index > (uint32_t)preset_count - 1) { + instance->preset_index = SUBGHZ_LAST_SETTING_DEFAULT_PRESET; + } } bool subghz_last_settings_save(SubGhzLastSettings* instance) { furi_assert(instance); -#if SUBGHZ_LAST_SETTING_SAVE_BIN_RAW != true - instance->filter = SubGhzProtocolFlag_Decodable; -#endif bool saved = false; Storage* storage = furi_record_open(RECORD_STORAGE); FlipperFormat* file = flipper_format_file_alloc(storage); @@ -251,96 +192,76 @@ bool subghz_last_settings_save(SubGhzLastSettings* instance) { if(!flipper_format_write_header_cstr( file, SUBGHZ_LAST_SETTING_FILE_TYPE, SUBGHZ_LAST_SETTING_FILE_VERSION)) break; - if(!flipper_format_insert_or_update_uint32( - file, SUBGHZ_LAST_SETTING_FIELD_PRESET, &instance->preset_index, 1)) { + if(!flipper_format_write_uint32( + file, SUBGHZ_LAST_SETTING_FIELD_FREQUENCY, &instance->frequency, 1)) { break; } - if(!flipper_format_insert_or_update_uint32( - file, SUBGHZ_LAST_SETTING_FIELD_FREQUENCY, &instance->frequency, 1)) { + if(!flipper_format_write_uint32( + file, SUBGHZ_LAST_SETTING_FIELD_PRESET, &instance->preset_index, 1)) { break; } - if(!flipper_format_insert_or_update_uint32( + if(!flipper_format_write_uint32( file, SUBGHZ_LAST_SETTING_FIELD_FREQUENCY_ANALYZER_FEEDBACK_LEVEL, &instance->frequency_analyzer_feedback_level, 1)) { break; } - if(!flipper_format_insert_or_update_float( + if(!flipper_format_write_float( file, SUBGHZ_LAST_SETTING_FIELD_FREQUENCY_ANALYZER_TRIGGER, &instance->frequency_analyzer_trigger, 1)) { break; } - if(!flipper_format_insert_or_update_bool( + if(!flipper_format_write_bool( file, - SUBGHZ_LAST_SETTING_FIELD_EXTERNAL_MODULE_ENABLED, - &instance->external_module_enabled, + SUBGHZ_LAST_SETTING_FIELD_PROTOCOL_FILE_NAMES, + &instance->protocol_file_names, 1)) { break; } - if(!flipper_format_insert_or_update_bool( - file, - SUBGHZ_LAST_SETTING_FIELD_EXTERNAL_MODULE_POWER, - &instance->external_module_power_5v_disable, - 1)) { + if(!flipper_format_write_bool( + file, SUBGHZ_LAST_SETTING_FIELD_HOPPING_ENABLE, &instance->enable_hopping, 1)) { break; } - if(!flipper_format_insert_or_update_bool( - file, - SUBGHZ_LAST_SETTING_FIELD_PROTOCOL_FILE_NAMES, - &instance->protocol_file_names, - 1)) { + if(!flipper_format_write_uint32( + file, SUBGHZ_LAST_SETTING_FIELD_IGNORE_FILTER, &instance->ignore_filter, 1)) { break; } - if(!flipper_format_insert_or_update_bool( - file, - SUBGHZ_LAST_SETTING_FIELD_EXTERNAL_MODULE_POWER_AMP, - &instance->external_module_power_amp, - 1)) { + if(!flipper_format_write_uint32( + file, SUBGHZ_LAST_SETTING_FIELD_FILTER, &instance->filter, 1)) { break; } - if(!flipper_format_insert_or_update_uint32( - file, SUBGHZ_LAST_SETTING_FIELD_GPS, &instance->gps_baudrate, 1)) { + if(!flipper_format_write_float( + file, SUBGHZ_LAST_SETTING_FIELD_RSSI_THRESHOLD, &instance->rssi, 1)) { break; } - if(!flipper_format_insert_or_update_bool( - file, SUBGHZ_LAST_SETTING_FIELD_HOPPING_ENABLE, &instance->enable_hopping, 1)) { + if(!flipper_format_write_bool( + file, SUBGHZ_LAST_SETTING_FIELD_DELETE_OLD, &instance->delete_old_signals, 1)) { break; } - if(!flipper_format_insert_or_update_float( - file, SUBGHZ_LAST_SETTING_FIELD_RSSI_THRESHOLD, &instance->rssi, 1)) { + + if(!flipper_format_write_uint32( + file, SUBGHZ_LAST_SETTING_FIELD_GPS_BAUDRATE, &instance->gps_baudrate, 1)) { break; } - if(!flipper_format_insert_or_update_bool( + if(!flipper_format_write_bool( file, SUBGHZ_LAST_SETTING_FIELD_REMOVE_DUPLICATES, &instance->remove_duplicates, 1)) { break; } - if(!flipper_format_insert_or_update_uint32( - file, SUBGHZ_LAST_SETTING_FIELD_IGNORE_FILTER, &instance->ignore_filter, 1)) { - break; - } - if(!flipper_format_insert_or_update_uint32( - file, SUBGHZ_LAST_SETTING_FIELD_FILTER, &instance->filter, 1)) { - break; - } - if(!flipper_format_insert_or_update_uint32( + if(!flipper_format_write_uint32( file, SUBGHZ_LAST_SETTING_FIELD_REPEATER, &instance->repeater_state, 1)) { break; } - if(!flipper_format_insert_or_update_bool( + if(!flipper_format_write_bool( file, SUBGHZ_LAST_SETTING_FIELD_ENABLE_SOUND, &instance->enable_sound, 1)) { break; } - if(!flipper_format_insert_or_update_bool( - file, SUBGHZ_LAST_SETTING_FIELD_DELETE_OLD, &instance->delete_old_signals, 1)) { - break; - } - if(!flipper_format_insert_or_update_bool( + if(!flipper_format_write_bool( file, SUBGHZ_LAST_SETTING_FIELD_AUTOSAVE, &instance->autosave, 1)) { break; } @@ -357,54 +278,3 @@ bool subghz_last_settings_save(SubGhzLastSettings* instance) { return saved; } - -const char* LOG_ON = "ON"; -const char* LOG_OFF = "OFF"; - -static inline const char* - subghz_last_settings_log_filter_get_index(uint32_t filter, uint32_t flag) { - return READ_BIT(filter, flag) > 0 ? LOG_ON : LOG_OFF; -} - -static inline const char* bool_to_char(bool value) { - return value ? LOG_ON : LOG_OFF; -} - -void subghz_last_settings_log(SubGhzLastSettings* instance) { - furi_assert(instance); - - FURI_LOG_I( - TAG, - "Frequency: %03ld.%02ld, FeedbackLevel: %ld, FATrigger: %.2f, External: %s, ExtPower: %s, TimestampNames: %s, ExtPowerAmp: %s,\n" - "GPSBaudrate: %ld, Hopping: %s,\nPreset: %ld, RSSI: %.2f, " - "BinRAW: %s, Repeater: %lu, Duplicates: %s, Autosave: %s, Starline: %s, Cars: %s, Magellan: %s, NiceFloR-S: %s, Weather: %s, TPMS: %s, Sound: %s", - instance->frequency / 1000000 % 1000, - instance->frequency / 10000 % 100, - instance->frequency_analyzer_feedback_level, - (double)instance->frequency_analyzer_trigger, - bool_to_char(instance->external_module_enabled), - bool_to_char(instance->external_module_power_5v_disable), - bool_to_char(instance->protocol_file_names), - bool_to_char(instance->external_module_power_amp), - instance->gps_baudrate, - bool_to_char(instance->enable_hopping), - instance->preset_index, - (double)instance->rssi, - subghz_last_settings_log_filter_get_index(instance->filter, SubGhzProtocolFlag_BinRAW), - instance->repeater_state, - bool_to_char(instance->remove_duplicates), - bool_to_char(instance->autosave), - subghz_last_settings_log_filter_get_index( - instance->ignore_filter, SubGhzProtocolFilter_StarLine), - subghz_last_settings_log_filter_get_index( - instance->ignore_filter, SubGhzProtocolFilter_AutoAlarms), - subghz_last_settings_log_filter_get_index( - instance->ignore_filter, SubGhzProtocolFilter_Magellan), - subghz_last_settings_log_filter_get_index( - instance->ignore_filter, SubGhzProtocolFilter_NiceFlorS), - subghz_last_settings_log_filter_get_index( - instance->ignore_filter, SubGhzProtocolFilter_Weather), - subghz_last_settings_log_filter_get_index( - instance->ignore_filter, SubGhzProtocolFilter_TPMS), - bool_to_char(instance->enable_sound)); -} diff --git a/applications/main/subghz/subghz_last_settings.h b/applications/main/subghz/subghz_last_settings.h index 3c03c0f116..91dfbef3a8 100644 --- a/applications/main/subghz/subghz_last_settings.h +++ b/applications/main/subghz/subghz_last_settings.h @@ -7,8 +7,6 @@ #include #define SUBGHZ_LAST_SETTING_FREQUENCY_ANALYZER_TRIGGER (-93.0f) -#define SUBGHZ_LAST_SETTING_SAVE_BIN_RAW true -#define SUBGHZ_LAST_SETTING_SAVE_PRESET true // 1 = "AM650" // "AM270", "AM650", "FM238", "FM476", #define SUBGHZ_LAST_SETTING_DEFAULT_PRESET 1 @@ -20,21 +18,17 @@ typedef struct { uint32_t preset_index; // AKA Modulation uint32_t frequency_analyzer_feedback_level; float frequency_analyzer_trigger; - // TODO not using but saved so as not to change the version - bool external_module_enabled; - bool external_module_power_5v_disable; - bool external_module_power_amp; - // saved so as not to change the version bool protocol_file_names; - uint32_t gps_baudrate; bool enable_hopping; - uint32_t repeater_state; - bool enable_sound; - bool remove_duplicates; uint32_t ignore_filter; uint32_t filter; float rssi; bool delete_old_signals; + + uint32_t gps_baudrate; + bool remove_duplicates; + uint32_t repeater_state; + bool enable_sound; bool autosave; } SubGhzLastSettings; @@ -45,5 +39,3 @@ void subghz_last_settings_free(SubGhzLastSettings* instance); void subghz_last_settings_load(SubGhzLastSettings* instance, size_t preset_count); bool subghz_last_settings_save(SubGhzLastSettings* instance); - -void subghz_last_settings_log(SubGhzLastSettings* instance); diff --git a/applications/main/subghz/views/subghz_frequency_analyzer.c b/applications/main/subghz/views/subghz_frequency_analyzer.c index bc472f2339..f16a38d3e5 100644 --- a/applications/main/subghz/views/subghz_frequency_analyzer.c +++ b/applications/main/subghz/views/subghz_frequency_analyzer.c @@ -228,9 +228,7 @@ uint32_t subghz_frequency_find_correct(uint32_t input) { uint32_t prev_freq = 0; uint32_t current = 0; uint32_t result = 0; -#ifdef FURI_DEBUG - FURI_LOG_D(TAG, "input: %ld", input); -#endif + for(size_t i = 0; i < sizeof(subghz_frequency_list); i++) { current = subghz_frequency_list[i]; if(current == input) { @@ -281,7 +279,7 @@ bool subghz_frequency_analyzer_input(InputEvent* event, void* context) { break; } subghz_frequency_analyzer_worker_set_trigger_level(instance->worker, trigger_level); - FURI_LOG_I(TAG, "trigger = %.1f", (double)trigger_level); + FURI_LOG_D(TAG, "trigger = %.1f", (double)trigger_level); need_redraw = true; } else if(event->type == InputTypePress && event->key == InputKeyUp) { if(instance->feedback_level == 0) { @@ -289,9 +287,7 @@ bool subghz_frequency_analyzer_input(InputEvent* event, void* context) { } else { instance->feedback_level--; } -#ifdef FURI_DEBUG - FURI_LOG_D(TAG, "feedback_level = %d", instance->feedback_level); -#endif + need_redraw = true; } else if( ((event->type == InputTypePress) || (event->type == InputTypeRepeat)) && @@ -324,13 +320,6 @@ bool subghz_frequency_analyzer_input(InputEvent* event, void* context) { } if(frequency_candidate > 0 && frequency_candidate != model->frequency_to_save) { -#ifdef FURI_DEBUG - FURI_LOG_D( - TAG, - "frequency_to_save: %ld, candidate: %ld", - model->frequency_to_save, - frequency_candidate); -#endif model->frequency_to_save = frequency_candidate; updated = true; } @@ -372,24 +361,12 @@ bool subghz_frequency_analyzer_input(InputEvent* event, void* context) { }, true); -#ifdef FURI_DEBUG - FURI_LOG_I( - TAG, - "updated: %d, long: %d, type: %d", - updated, - (event->type == InputTypeLong), - event->type); -#endif - if(updated) { instance->callback(SubGhzCustomEventViewFreqAnalOkShort, instance->context); } // First device receive short, then when user release button we get long if(event->type == InputTypeLong && frequency_to_save > 0) { -#ifdef FURI_DEBUG - FURI_LOG_I(TAG, "Long press!"); -#endif // Stop worker if(subghz_frequency_analyzer_worker_is_running(instance->worker)) { subghz_frequency_analyzer_worker_stop(instance->worker); diff --git a/applications/services/cli/cli_commands.c b/applications/services/cli/cli_commands.c index 9805e7f104..1a91e2622a 100644 --- a/applications/services/cli/cli_commands.c +++ b/applications/services/cli/cli_commands.c @@ -435,8 +435,34 @@ void cli_command_free(Cli* cli, FuriString* args, void* context) { printf("Minimum heap size: %zu\r\n", memmgr_get_minimum_free_heap()); printf("Maximum heap block: %zu\r\n", memmgr_heap_get_max_free_block()); - printf("Pool free: %zu\r\n", memmgr_pool_get_free()); - printf("Maximum pool block: %zu\r\n", memmgr_pool_get_max_block()); + printf("Aux pool total free: %zu\r\n", memmgr_aux_pool_get_free()); + printf("Aux pool max free block: %zu\r\n", memmgr_pool_get_max_block()); +} + +typedef struct { + void* addr; + size_t size; +} FreeBlockInfo; + +#define FREE_BLOCK_INFO_MAX 128 + +typedef struct { + FreeBlockInfo free_blocks[FREE_BLOCK_INFO_MAX]; + size_t free_blocks_count; +} FreeBlockContext; + +static bool free_block_walker(void* pointer, size_t size, bool used, void* context) { + FreeBlockContext* free_blocks = (FreeBlockContext*)context; + if(!used) { + if(free_blocks->free_blocks_count < FREE_BLOCK_INFO_MAX) { + free_blocks->free_blocks[free_blocks->free_blocks_count].addr = pointer; + free_blocks->free_blocks[free_blocks->free_blocks_count].size = size; + free_blocks->free_blocks_count++; + } else { + return false; + } + } + return true; } void cli_command_free_blocks(Cli* cli, FuriString* args, void* context) { @@ -444,7 +470,23 @@ void cli_command_free_blocks(Cli* cli, FuriString* args, void* context) { UNUSED(args); UNUSED(context); - memmgr_heap_printf_free_blocks(); + FreeBlockContext* free_blocks = malloc(sizeof(FreeBlockContext)); + free_blocks->free_blocks_count = 0; + + memmgr_heap_walk_blocks(free_block_walker, free_blocks); + + for(size_t i = 0; i < free_blocks->free_blocks_count; i++) { + printf( + "A %p S %zu\r\n", + (void*)free_blocks->free_blocks[i].addr, + free_blocks->free_blocks[i].size); + } + + if(free_blocks->free_blocks_count == FREE_BLOCK_INFO_MAX) { + printf("... and more\r\n"); + } + + free(free_blocks); } void cli_command_i2c(Cli* cli, FuriString* args, void* context) { diff --git a/applications/services/desktop/desktop.c b/applications/services/desktop/desktop.c index a042dc56b0..38914f43ae 100644 --- a/applications/services/desktop/desktop.c +++ b/applications/services/desktop/desktop.c @@ -34,10 +34,8 @@ static void desktop_loader_callback(const void* message, void* context) { const LoaderEvent* event = message; if(event->type == LoaderEventTypeApplicationBeforeLoad) { - desktop->animation_lock = api_lock_alloc_locked(); view_dispatcher_send_custom_event(desktop->view_dispatcher, DesktopGlobalBeforeAppStarted); - api_lock_wait_unlock_and_free(desktop->animation_lock); - desktop->animation_lock = NULL; + furi_check(furi_semaphore_acquire(desktop->animation_semaphore, 3000) == FuriStatusOk); } else if( event->type == LoaderEventTypeApplicationLoadFailed || event->type == LoaderEventTypeApplicationStopped) { @@ -125,7 +123,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); - api_lock_unlock(desktop->animation_lock); + furi_semaphore_release(desktop->animation_semaphore); return true; case DesktopGlobalAfterAppFinished: animation_manager_load_and_continue_animation(desktop->animation_manager); @@ -282,6 +280,7 @@ 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 c06ba40a25..1c1598c796 100644 --- a/applications/services/desktop/desktop_i.h +++ b/applications/services/desktop/desktop_i.h @@ -20,7 +20,6 @@ #include #include -#include #define STATUS_BAR_Y_SHIFT 13 @@ -81,7 +80,7 @@ struct Desktop { bool in_transition : 1; - FuriApiLock animation_lock; + FuriSemaphore* animation_semaphore; Keybinds keybinds; diff --git a/applications/services/gui/view_dispatcher.c b/applications/services/gui/view_dispatcher.c index 0bc08cd27c..a94d627cef 100644 --- a/applications/services/gui/view_dispatcher.c +++ b/applications/services/gui/view_dispatcher.c @@ -53,7 +53,6 @@ void view_dispatcher_set_navigation_event_callback( ViewDispatcher* view_dispatcher, ViewDispatcherNavigationEventCallback callback) { furi_check(view_dispatcher); - furi_check(callback); view_dispatcher->navigation_event_callback = callback; } @@ -61,7 +60,6 @@ void view_dispatcher_set_custom_event_callback( ViewDispatcher* view_dispatcher, ViewDispatcherCustomEventCallback callback) { furi_check(view_dispatcher); - furi_check(callback); view_dispatcher->custom_event_callback = callback; } @@ -70,7 +68,6 @@ void view_dispatcher_set_tick_event_callback( ViewDispatcherTickEventCallback callback, uint32_t tick_period) { furi_check(view_dispatcher); - furi_check(callback); view_dispatcher->tick_event_callback = callback; view_dispatcher->tick_period = tick_period; } diff --git a/applications/services/loader/loader.c b/applications/services/loader/loader.c index 03de06b305..446a6b10d7 100644 --- a/applications/services/loader/loader.c +++ b/applications/services/loader/loader.c @@ -24,14 +24,14 @@ static const char* loader_find_external_application_by_name(const char* app_name, FlipperApplicationFlag* flags) { for(size_t i = 0; i < FLIPPER_EXTERNAL_APPS_COUNT; i++) { if(strcmp(FLIPPER_EXTERNAL_APPS[i].name, app_name) == 0) { - *flags = FLIPPER_EXTERNAL_APPS[i].flags; + if(flags) *flags = FLIPPER_EXTERNAL_APPS[i].flags; return FLIPPER_EXTERNAL_APPS[i].path; } } for(size_t i = 0; i < FLIPPER_SETTINGS_APPS_COUNT; i++) { if(strcmp(FLIPPER_SETTINGS_APPS[i].name, app_name) == 0) { - *flags = FLIPPER_SETTINGS_APPS[i].flags; + if(flags) *flags = FLIPPER_SETTINGS_APPS[i].flags; return FLIPPER_SETTINGS_APPS[i].path; } } @@ -61,10 +61,24 @@ LoaderStatus return result.value; } -static void loader_show_gui_error(LoaderStatus status, FuriString* error_message) { - // TODO FL-3522: we have many places where we can emit a double start, ex: desktop, menu - // so i prefer to not show LoaderStatusErrorAppStarted error message for now - if(status == LoaderStatusErrorUnknownApp || status == LoaderStatusErrorInternal) { +static void + loader_show_gui_error(LoaderStatus status, const char* name, FuriString* error_message) { + if(status == LoaderStatusErrorUnknownApp && + loader_find_external_application_by_name(name, NULL) != NULL) { + // Special case for external apps + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + DialogMessage* message = dialog_message_alloc(); + dialog_message_set_header(message, "Update needed", 64, 3, AlignCenter, AlignTop); + dialog_message_set_buttons(message, NULL, NULL, NULL); + dialog_message_set_icon(message, &I_WarningDolphinFlip_45x42, 83, 22); + dialog_message_set_text( + message, "Update firmware\nto run this app", 3, 26, AlignLeft, AlignTop); + dialog_message_show(dialogs, message); + dialog_message_free(message); + furi_record_close(RECORD_DIALOGS); + } else if(status == LoaderStatusErrorUnknownApp || status == LoaderStatusErrorInternal) { + // TODO FL-3522: we have many places where we can emit a double start, ex: desktop, menu + // so i prefer to not show LoaderStatusErrorAppStarted error message for now DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); DialogMessage* message = dialog_message_alloc(); dialog_message_set_header(message, "Error", 64, 0, AlignCenter, AlignTop); @@ -90,7 +104,7 @@ LoaderStatus loader_start_with_gui_error(Loader* loader, const char* name, const FuriString* error_message = furi_string_alloc(); LoaderStatus status = loader_start(loader, name, args, error_message); - loader_show_gui_error(status, error_message); + loader_show_gui_error(status, name, error_message); furi_string_free(error_message); return status; } @@ -99,11 +113,11 @@ void loader_start_detached_with_gui_error(Loader* loader, const char* name, cons furi_check(loader); furi_check(name); - LoaderMessage message; - - message.type = LoaderMessageTypeStartByNameDetachedWithGuiError; - message.start.name = name ? strdup(name) : NULL; - message.start.args = args ? strdup(args) : NULL; + LoaderMessage message = { + .type = LoaderMessageTypeStartByNameDetachedWithGuiError, + .start.name = strdup(name), + .start.args = args ? strdup(args) : NULL, + }; furi_message_queue_put(loader->queue, &message, FuriWaitForever); } @@ -196,11 +210,7 @@ static void loader_thread_state_callback(FuriThreadState thread_state, void* con Loader* loader = context; - if(thread_state == FuriThreadStateRunning) { - LoaderEvent event; - event.type = LoaderEventTypeApplicationStarted; - furi_pubsub_publish(loader->pubsub, &event); - } else if(thread_state == FuriThreadStateStopped) { + if(thread_state == FuriThreadStateStopped) { LoaderMessage message; message.type = LoaderMessageTypeAppClosed; furi_message_queue_put(loader->queue, &message, FuriWaitForever); @@ -742,7 +752,7 @@ int32_t loader_srv(void* p) { FuriString* error_message = furi_string_alloc(); LoaderStatus status = loader_do_start_by_name( loader, message.start.name, message.start.args, error_message); - loader_show_gui_error(status, error_message); + loader_show_gui_error(status, message.start.name, error_message); if(message.start.name) free((void*)message.start.name); if(message.start.args) free((void*)message.start.args); furi_string_free(error_message); diff --git a/applications/services/loader/loader.h b/applications/services/loader/loader.h index 66669bc745..0223cfffd3 100644 --- a/applications/services/loader/loader.h +++ b/applications/services/loader/loader.h @@ -20,7 +20,6 @@ typedef enum { typedef enum { LoaderEventTypeApplicationBeforeLoad, LoaderEventTypeApplicationLoadFailed, - LoaderEventTypeApplicationStarted, LoaderEventTypeApplicationStopped } LoaderEventType; @@ -34,7 +33,7 @@ typedef struct { * @param[in] name application name or id * @param[in] args application arguments * @param[out] error_message detailed error message, can be NULL - * @return LoaderStatus + * @return LoaderStatus */ LoaderStatus loader_start(Loader* instance, const char* name, const char* args, FuriString* error_message); @@ -44,19 +43,19 @@ LoaderStatus * @param[in] instance loader instance * @param[in] name application name or id * @param[in] args application arguments - * @return LoaderStatus + * @return LoaderStatus */ LoaderStatus loader_start_with_gui_error(Loader* loader, const char* name, const char* args); /** * @brief Start application detached with GUI error message * @param[in] instance loader instance - * @param[in] name application name + * @param[in] name application name or id * @param[in] args application arguments */ void loader_start_detached_with_gui_error(Loader* loader, const char* name, const char* args); -/** +/** * @brief Lock application start * @param[in] instance loader instance * @return true on success diff --git a/applications/services/loader/loader_i.h b/applications/services/loader/loader_i.h index d8b4d235c7..e6c93b316b 100644 --- a/applications/services/loader/loader_i.h +++ b/applications/services/loader/loader_i.h @@ -33,8 +33,8 @@ typedef enum { LoaderMessageTypeLock, LoaderMessageTypeUnlock, LoaderMessageTypeIsLocked, - LoaderMessageTypeStartByNameDetachedWithGuiError, + LoaderMessageTypeShowSettings, } LoaderMessageType; diff --git a/applications/services/power/power_service/power.c b/applications/services/power/power_service/power.c index 7badb7103a..39724910c8 100644 --- a/applications/services/power/power_service/power.c +++ b/applications/services/power/power_service/power.c @@ -300,9 +300,11 @@ static void power_loader_callback(const void* message, void* context) { Power* power = context; const LoaderEvent* event = message; - if(event->type == LoaderEventTypeApplicationStarted) { + if(event->type == LoaderEventTypeApplicationBeforeLoad) { power_auto_shutdown_inhibit(power); - } else if(event->type == LoaderEventTypeApplicationStopped) { + } else if( + event->type == LoaderEventTypeApplicationLoadFailed || + event->type == LoaderEventTypeApplicationStopped) { power_auto_shutdown_arm(power); } } diff --git a/applications/services/storage/storage.c b/applications/services/storage/storage.c index f2997ffcf4..b51c0df6c4 100644 --- a/applications/services/storage/storage.c +++ b/applications/services/storage/storage.c @@ -43,10 +43,12 @@ Storage* storage_app_alloc(void) { } #ifndef FURI_RAM_EXEC - storage_mnt_init(&app->storage[ST_MNT]); storage_int_init(&app->storage[ST_INT]); #endif storage_ext_init(&app->storage[ST_EXT]); +#ifndef FURI_RAM_EXEC + storage_mnt_init(&app->storage[ST_MNT]); +#endif // sd icon gui app->sd_gui.enabled = false; diff --git a/applications/services/storage/storage_glue.c b/applications/services/storage/storage_glue.c index 41da6c3f48..727af765fe 100644 --- a/applications/services/storage/storage_glue.c +++ b/applications/services/storage/storage_glue.c @@ -156,3 +156,9 @@ size_t storage_open_files_count(StorageData* storage) { size_t count = StorageFileList_size(storage->files); return count; } + +const char* storage_file_get_path(File* file, StorageData* storage) { + StorageFile* storage_file_ref = storage_get_file(file, storage); + if(!storage_file_ref) return ""; + return furi_string_get_cstr(storage_file_ref->path); +} \ No newline at end of file diff --git a/applications/services/storage/storage_glue.h b/applications/services/storage/storage_glue.h index fbc08ebbf9..9a19ae815a 100644 --- a/applications/services/storage/storage_glue.h +++ b/applications/services/storage/storage_glue.h @@ -35,6 +35,7 @@ void storage_file_init(StorageFile* obj); void storage_file_init_set(StorageFile* obj, const StorageFile* src); void storage_file_set(StorageFile* obj, const StorageFile* src); void storage_file_clear(StorageFile* obj); +const char* storage_file_get_path(File* file, StorageData* storage); void storage_data_init(StorageData* storage); StorageStatus storage_data_status(StorageData* storage); diff --git a/applications/services/storage/storages/storage_ext.c b/applications/services/storage/storages/storage_ext.c index 0bb3133358..971f1df171 100644 --- a/applications/services/storage/storages/storage_ext.c +++ b/applications/services/storage/storages/storage_ext.c @@ -4,6 +4,7 @@ #include #include "sd_notify.h" #include +#include typedef FIL SDFile; typedef DIR SDDir; @@ -741,6 +742,20 @@ FS_Error storage_process_virtual_format(StorageData* storage) { SDError error = f_mkfs(sd_data->path, FM_ANY, 0, work, _MAX_SS); free(work); if(error != FR_OK) return FSE_INTERNAL; + + if(storage_process_virtual_mount(storage) == FSE_OK) { + // Image file path + const char* img_path = storage_file_get_path(mnt_image, mnt_image_storage); + // Image file name + FuriString* img_name = furi_string_alloc(); + path_extract_filename_no_ext(img_path, img_name); + // Label with drive id prefix + char* label = storage_ext_drive_path(storage, furi_string_get_cstr(img_name)); + furi_string_free(img_name); + f_setlabel(label); + free(label); + storage_process_virtual_unmount(storage); + } return FSE_OK; #endif } diff --git a/applications/system/js_app/examples/apps/Scripts/badusb_demo.js b/applications/system/js_app/examples/apps/Scripts/badusb_demo.js index 8605020233..be94a64d2a 100644 --- a/applications/system/js_app/examples/apps/Scripts/badusb_demo.js +++ b/applications/system/js_app/examples/apps/Scripts/badusb_demo.js @@ -3,7 +3,13 @@ let notify = require("notification"); let flipper = require("flipper"); let dialog = require("dialog"); -badusb.setup({ vid: 0xAAAA, pid: 0xBBBB, mfr_name: "Flipper", prod_name: "Zero" }); +badusb.setup({ + vid: 0xAAAA, + pid: 0xBBBB, + mfr_name: "Flipper", + prod_name: "Zero", + layout_path: "/ext/badusb/assets/layouts/en-US.kl" +}); dialog.message("BadUSB demo", "Press OK to start"); if (badusb.isConnected()) { diff --git a/applications/system/js_app/examples/apps/Scripts/storage.js b/applications/system/js_app/examples/apps/Scripts/storage.js index c19f0f003b..fd2ba4a4fb 100644 --- a/applications/system/js_app/examples/apps/Scripts/storage.js +++ b/applications/system/js_app/examples/apps/Scripts/storage.js @@ -1,19 +1,36 @@ let storage = require("storage"); let path = "/ext/storage.test"; +function arraybuf_to_string(arraybuf) { + let string = ""; + let data_view = Uint8Array(arraybuf); + for (let i = 0; i < data_view.length; i++) { + string += chr(data_view[i]); + } + return string; +} + print("File exists:", storage.exists(path)); print("Writing..."); +// write(path, data, offset) +// If offset is specified, the file is not cleared, content is kept and data is written at specified offset +// Takes both strings and array buffers storage.write(path, "Hello "); print("File exists:", storage.exists(path)); // Append will create the file even if it doesnt exist! +// Takes both strings and array buffers storage.append(path, "World!"); print("Reading..."); +// read(path, size, offset) +// If no size specified, total filesize is used +// If offset is specified, size is capped at (filesize - offset) let data = storage.read(path); -print(data); +// read returns an array buffer, to allow proper usage of raw binary data +print(arraybuf_to_string(data)); print("Removing...") storage.remove(path); @@ -21,6 +38,9 @@ storage.remove(path); print("Done") // There's also: +// storage.copy(old_path, new_path); +// storage.move(old_path, new_path); +// storage.mkdir(path); // storage.virtualInit(path); // storage.virtualMount(); // storage.virtualQuit(); \ No newline at end of file diff --git a/applications/system/js_app/modules/js_badusb.c b/applications/system/js_app/modules/js_badusb.c index a380141e84..d2e30b3342 100644 --- a/applications/system/js_app/modules/js_badusb.c +++ b/applications/system/js_app/modules/js_badusb.c @@ -2,8 +2,11 @@ #include "../js_modules.h" #include +#define ASCII_TO_KEY(layout, x) (((uint8_t)x < 128) ? (layout[(uint8_t)x]) : HID_KEYBOARD_NONE) + typedef struct { FuriHalUsbHidConfig* hid_cfg; + uint16_t layout[128]; FuriHalUsbInterface* usb_if_prev; uint8_t key_hold_cnt; } JsBadusbInst; @@ -85,10 +88,15 @@ static void js_badusb_quit_free(JsBadusbInst* badusb) { } if(badusb->hid_cfg) { free(badusb->hid_cfg); + badusb->hid_cfg = NULL; } } -static bool setup_parse_params(struct mjs* mjs, mjs_val_t arg, FuriHalUsbHidConfig* hid_cfg) { +static bool setup_parse_params( + JsBadusbInst* badusb, + struct mjs* mjs, + mjs_val_t arg, + FuriHalUsbHidConfig* hid_cfg) { if(!mjs_is_object(arg)) { return false; } @@ -96,6 +104,7 @@ static bool setup_parse_params(struct mjs* mjs, mjs_val_t arg, FuriHalUsbHidConf mjs_val_t pid_obj = mjs_get(mjs, arg, "pid", ~0); mjs_val_t mfr_obj = mjs_get(mjs, arg, "mfr_name", ~0); mjs_val_t prod_obj = mjs_get(mjs, arg, "prod_name", ~0); + mjs_val_t layout_obj = mjs_get(mjs, arg, "layout_path", ~0); if(mjs_is_number(vid_obj) && mjs_is_number(pid_obj)) { hid_cfg->vid = mjs_get_int32(mjs, vid_obj); @@ -122,6 +131,25 @@ static bool setup_parse_params(struct mjs* mjs, mjs_val_t arg, FuriHalUsbHidConf strlcpy(hid_cfg->product, str_temp, sizeof(hid_cfg->product)); } + if(mjs_is_string(layout_obj)) { + size_t str_len = 0; + const char* str_temp = mjs_get_string(mjs, &layout_obj, &str_len); + if((str_len == 0) || (str_temp == NULL)) { + return false; + } + File* file = storage_file_alloc(furi_record_open(RECORD_STORAGE)); + bool layout_loaded = storage_file_open(file, str_temp, FSAM_READ, FSOM_OPEN_EXISTING) && + storage_file_read(file, badusb->layout, sizeof(badusb->layout)) == + sizeof(badusb->layout); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + if(!layout_loaded) { + return false; + } + } else { + memcpy(badusb->layout, hid_asciimap, MIN(sizeof(hid_asciimap), sizeof(badusb->layout))); + } + return true; } @@ -144,7 +172,7 @@ static void js_badusb_setup(struct mjs* mjs) { } else if(num_args == 1) { badusb->hid_cfg = malloc(sizeof(FuriHalUsbHidConfig)); // Parse argument object - args_correct = setup_parse_params(mjs, mjs_arg(mjs, 0), badusb->hid_cfg); + args_correct = setup_parse_params(badusb, mjs, mjs_arg(mjs, 0), badusb->hid_cfg); } if(!args_correct) { mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); @@ -191,9 +219,9 @@ static void js_badusb_is_connected(struct mjs* mjs) { mjs_return(mjs, mjs_mk_boolean(mjs, is_connected)); } -uint16_t get_keycode_by_name(const char* key_name, size_t name_len) { +uint16_t get_keycode_by_name(JsBadusbInst* badusb, const char* key_name, size_t name_len) { if(name_len == 1) { // Single char - return (HID_ASCII_TO_KEY(key_name[0])); + return (ASCII_TO_KEY(badusb->layout, key_name[0])); } for(size_t i = 0; i < COUNT_OF(key_codes); i++) { @@ -210,7 +238,7 @@ uint16_t get_keycode_by_name(const char* key_name, size_t name_len) { return HID_KEYBOARD_NONE; } -static bool parse_keycode(struct mjs* mjs, size_t nargs, uint16_t* keycode) { +static bool parse_keycode(JsBadusbInst* badusb, struct mjs* mjs, size_t nargs, uint16_t* keycode) { uint16_t key_tmp = 0; for(size_t i = 0; i < nargs; i++) { mjs_val_t arg = mjs_arg(mjs, i); @@ -221,7 +249,7 @@ static bool parse_keycode(struct mjs* mjs, size_t nargs, uint16_t* keycode) { // String error return false; } - uint16_t str_key = get_keycode_by_name(key_name, name_len); + uint16_t str_key = get_keycode_by_name(badusb, key_name, name_len); if(str_key == HID_KEYBOARD_NONE) { // Unknown key code return false; @@ -259,7 +287,7 @@ static void js_badusb_press(struct mjs* mjs) { uint16_t keycode = HID_KEYBOARD_NONE; size_t num_args = mjs_nargs(mjs); if(num_args > 0) { - args_correct = parse_keycode(mjs, num_args, &keycode); + args_correct = parse_keycode(badusb, mjs, num_args, &keycode); } if(!args_correct) { mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); @@ -285,7 +313,7 @@ static void js_badusb_hold(struct mjs* mjs) { uint16_t keycode = HID_KEYBOARD_NONE; size_t num_args = mjs_nargs(mjs); if(num_args > 0) { - args_correct = parse_keycode(mjs, num_args, &keycode); + args_correct = parse_keycode(badusb, mjs, num_args, &keycode); } if(!args_correct) { mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); @@ -324,7 +352,7 @@ static void js_badusb_release(struct mjs* mjs) { mjs_return(mjs, MJS_UNDEFINED); return; } else { - args_correct = parse_keycode(mjs, num_args, &keycode); + args_correct = parse_keycode(badusb, mjs, num_args, &keycode); } if(!args_correct) { mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); @@ -347,14 +375,14 @@ static void ducky_numlock_on() { } // Simulate pressing a character using ALT+Numpad ASCII code -static void ducky_altchar(const char* ascii_code) { +static void ducky_altchar(JsBadusbInst* badusb, const char* ascii_code) { // Hold the ALT key furi_hal_hid_kb_press(KEY_MOD_LEFT_ALT); // Press the corresponding numpad key for each digit of the ASCII code for(size_t i = 0; ascii_code[i] != '\0'; i++) { char digitChar[5] = {'N', 'U', 'M', ascii_code[i], '\0'}; // Construct the numpad key name - uint16_t numpad_keycode = get_keycode_by_name(digitChar, strlen(digitChar)); + uint16_t numpad_keycode = get_keycode_by_name(badusb, digitChar, strlen(digitChar)); if(numpad_keycode == HID_KEYBOARD_NONE) { continue; // Skip if keycode not found } @@ -420,9 +448,9 @@ static void badusb_print(struct mjs* mjs, bool ln, bool alt) { // Convert character to ascii numeric value char ascii_str[4]; snprintf(ascii_str, sizeof(ascii_str), "%u", (uint8_t)text_str[i]); - ducky_altchar(ascii_str); + ducky_altchar(badusb, ascii_str); } else { - uint16_t keycode = HID_ASCII_TO_KEY(text_str[i]); + uint16_t keycode = ASCII_TO_KEY(badusb->layout, text_str[i]); furi_hal_hid_kb_press(keycode); furi_hal_hid_kb_release(keycode); } diff --git a/applications/system/js_app/modules/js_storage.c b/applications/system/js_app/modules/js_storage.c index 05f8ffd40a..67c17c486f 100644 --- a/applications/system/js_app/modules/js_storage.c +++ b/applications/system/js_app/modules/js_storage.c @@ -32,8 +32,8 @@ static bool check_arg_count(struct mjs* mjs, size_t count) { return true; } -static bool get_path_arg(struct mjs* mjs, const char** path) { - mjs_val_t path_obj = mjs_arg(mjs, 0); +static bool get_path_arg(struct mjs* mjs, const char** path, size_t index) { + mjs_val_t path_obj = mjs_arg(mjs, index); if(!mjs_is_string(path_obj)) { ret_bad_args(mjs, "Path must be a string"); return false; @@ -49,10 +49,9 @@ static bool get_path_arg(struct mjs* mjs, const char** path) { static void js_storage_read(struct mjs* mjs) { JsStorageInst* storage = get_this_ctx(mjs); - if(!check_arg_count(mjs, 1)) return; const char* path; - if(!get_path_arg(mjs, &path)) return; + if(!get_path_arg(mjs, &path, 0)) return; File* file = storage_file_alloc(storage->api); do { @@ -62,15 +61,26 @@ static void js_storage_read(struct mjs* mjs) { } uint64_t size = storage_file_size(file); - if(size > 128 * 1024) { - ret_int_err(mjs, "File too large"); + mjs_val_t size_arg = mjs_arg(mjs, 1); + if(mjs_is_number(size_arg)) { + size = mjs_get_int32(mjs, size_arg); + } + + mjs_val_t seek_arg = mjs_arg(mjs, 2); + if(mjs_is_number(seek_arg)) { + storage_file_seek(file, mjs_get_int32(mjs, seek_arg), true); + size = MIN(size, storage_file_size(file) - storage_file_tell(file)); + } + + if(size > memmgr_heap_get_max_free_block()) { + ret_int_err(mjs, "Read size too large"); break; } uint8_t* data = malloc(size); size_t read = storage_file_read(file, data, size); if(read == size) { - mjs_return(mjs, mjs_mk_string(mjs, (const char*)data, size, true)); + mjs_return(mjs, mjs_mk_array_buf(mjs, (char*)data, size)); } else { ret_int_err(mjs, "File read failed"); } @@ -81,27 +91,41 @@ static void js_storage_read(struct mjs* mjs) { static void js_storage_write(struct mjs* mjs) { JsStorageInst* storage = get_this_ctx(mjs); - if(!check_arg_count(mjs, 2)) return; const char* path; - if(!get_path_arg(mjs, &path)) return; + if(!get_path_arg(mjs, &path, 0)) return; - mjs_val_t data_obj = mjs_arg(mjs, 1); - if(!mjs_is_string(data_obj)) { - ret_bad_args(mjs, "Data must be a string"); + mjs_val_t data_arg = mjs_arg(mjs, 1); + if(!mjs_is_typed_array(data_arg) && !mjs_is_string(data_arg)) { + ret_bad_args(mjs, "Data must be string, arraybuf or dataview"); return; } + if(mjs_is_data_view(data_arg)) { + data_arg = mjs_dataview_get_buf(mjs, data_arg); + } size_t data_len = 0; - const char* data = mjs_get_string(mjs, &data_obj, &data_len); - if((data_len == 0) || (data == NULL)) { - ret_bad_args(mjs, "Bad data argument"); - return; + const char* data = NULL; + if(mjs_is_string(data_arg)) { + data = mjs_get_string(mjs, &data_arg, &data_len); + } else if(mjs_is_typed_array(data_arg)) { + data = mjs_array_buf_get_ptr(mjs, data_arg, &data_len); } + mjs_val_t seek_arg = mjs_arg(mjs, 2); + File* file = storage_file_alloc(storage->api); - if(!storage_file_open(file, path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { + if(!storage_file_open( + file, + path, + FSAM_WRITE, + mjs_is_number(seek_arg) ? FSOM_OPEN_ALWAYS : FSOM_CREATE_ALWAYS)) { ret_int_err(mjs, storage_file_get_error_desc(file)); + } else { + if(mjs_is_number(seek_arg)) { + storage_file_seek(file, mjs_get_int32(mjs, seek_arg), true); + } + size_t write = storage_file_write(file, data, data_len); mjs_return(mjs, mjs_mk_boolean(mjs, write == data_len)); } @@ -113,18 +137,22 @@ static void js_storage_append(struct mjs* mjs) { if(!check_arg_count(mjs, 2)) return; const char* path; - if(!get_path_arg(mjs, &path)) return; + if(!get_path_arg(mjs, &path, 0)) return; - mjs_val_t data_obj = mjs_arg(mjs, 1); - if(!mjs_is_string(data_obj)) { - ret_bad_args(mjs, "Data must be a string"); + mjs_val_t data_arg = mjs_arg(mjs, 1); + if(!mjs_is_typed_array(data_arg) && !mjs_is_string(data_arg)) { + ret_bad_args(mjs, "Data must be string, arraybuf or dataview"); return; } + if(mjs_is_data_view(data_arg)) { + data_arg = mjs_dataview_get_buf(mjs, data_arg); + } size_t data_len = 0; - const char* data = mjs_get_string(mjs, &data_obj, &data_len); - if((data_len == 0) || (data == NULL)) { - ret_bad_args(mjs, "Bad data argument"); - return; + const char* data = NULL; + if(mjs_is_string(data_arg)) { + data = mjs_get_string(mjs, &data_arg, &data_len); + } else if(mjs_is_typed_array(data_arg)) { + data = mjs_array_buf_get_ptr(mjs, data_arg, &data_len); } File* file = storage_file_alloc(storage->api); @@ -142,7 +170,7 @@ static void js_storage_exists(struct mjs* mjs) { if(!check_arg_count(mjs, 1)) return; const char* path; - if(!get_path_arg(mjs, &path)) return; + if(!get_path_arg(mjs, &path, 0)) return; mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_exists(storage->api, path))); } @@ -152,17 +180,63 @@ static void js_storage_remove(struct mjs* mjs) { if(!check_arg_count(mjs, 1)) return; const char* path; - if(!get_path_arg(mjs, &path)) return; + if(!get_path_arg(mjs, &path, 0)) return; mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_remove(storage->api, path))); } +static void js_storage_copy(struct mjs* mjs) { + JsStorageInst* storage = get_this_ctx(mjs); + if(!check_arg_count(mjs, 2)) return; + + const char* old_path; + if(!get_path_arg(mjs, &old_path, 0)) return; + + const char* new_path; + if(!get_path_arg(mjs, &new_path, 1)) return; + + FS_Error error = storage_common_copy(storage->api, old_path, new_path); + if(error == FSE_OK) { + mjs_return(mjs, MJS_UNDEFINED); + } else { + ret_int_err(mjs, storage_error_get_desc(error)); + } +} + +static void js_storage_move(struct mjs* mjs) { + JsStorageInst* storage = get_this_ctx(mjs); + if(!check_arg_count(mjs, 2)) return; + + const char* old_path; + if(!get_path_arg(mjs, &old_path, 0)) return; + + const char* new_path; + if(!get_path_arg(mjs, &new_path, 1)) return; + + FS_Error error = storage_common_rename(storage->api, old_path, new_path); + if(error == FSE_OK) { + mjs_return(mjs, MJS_UNDEFINED); + } else { + ret_int_err(mjs, storage_error_get_desc(error)); + } +} + +static void js_storage_mkdir(struct mjs* mjs) { + JsStorageInst* storage = get_this_ctx(mjs); + if(!check_arg_count(mjs, 1)) return; + + const char* path; + if(!get_path_arg(mjs, &path, 0)) return; + + mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_mkdir(storage->api, path))); +} + static void js_storage_virtual_init(struct mjs* mjs) { JsStorageInst* storage = get_this_ctx(mjs); if(!check_arg_count(mjs, 1)) return; const char* path; - if(!get_path_arg(mjs, &path)) return; + if(!get_path_arg(mjs, &path, 0)) return; if(storage->virtual) { ret_int_err(mjs, "Virtual already setup"); @@ -202,11 +276,6 @@ static void js_storage_virtual_mount(struct mjs* mjs) { return; } - if(storage->virtual) { - storage_file_free(storage->virtual); - storage->virtual = NULL; - } - mjs_return(mjs, MJS_UNDEFINED); } @@ -219,6 +288,11 @@ static void js_storage_virtual_quit(struct mjs* mjs) { return; } + if(storage->virtual) { + storage_file_free(storage->virtual); + storage->virtual = NULL; + } + mjs_return(mjs, MJS_UNDEFINED); } @@ -231,6 +305,9 @@ static void* js_storage_create(struct mjs* mjs, mjs_val_t* object) { mjs_set(mjs, storage_obj, "append", ~0, MJS_MK_FN(js_storage_append)); mjs_set(mjs, storage_obj, "exists", ~0, MJS_MK_FN(js_storage_exists)); mjs_set(mjs, storage_obj, "remove", ~0, MJS_MK_FN(js_storage_remove)); + mjs_set(mjs, storage_obj, "copy", ~0, MJS_MK_FN(js_storage_copy)); + mjs_set(mjs, storage_obj, "move", ~0, MJS_MK_FN(js_storage_move)); + mjs_set(mjs, storage_obj, "mkdir", ~0, MJS_MK_FN(js_storage_mkdir)); mjs_set(mjs, storage_obj, "virtualInit", ~0, MJS_MK_FN(js_storage_virtual_init)); mjs_set(mjs, storage_obj, "virtualMount", ~0, MJS_MK_FN(js_storage_virtual_mount)); mjs_set(mjs, storage_obj, "virtualQuit", ~0, MJS_MK_FN(js_storage_virtual_quit)); diff --git a/assets/dolphin/external/L1_3d_printing_128x64/frame_0.png b/assets/dolphin/external/L1_3d_printing_128x64/frame_0.png new file mode 100644 index 0000000000000000000000000000000000000000..753497fae07bafdd109fab5b92aede933de1acbc GIT binary patch literal 1449 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 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_3d_printing_128x64/frame_15.png b/assets/dolphin/external/L1_3d_printing_128x64/frame_15.png new file mode 100644 index 0000000000000000000000000000000000000000..351f2c9b82e06f0b0ddaaf6a09d2a5a5c7e481d4 GIT binary patch literal 1284 zcmV+f1^fDmP)Px(yh%hsRCr$PoY8LNAPhx!{{N$=O0^jw3ckl+2$`^NRU8by2iwqeU+?$({crz! zYy@5#fX8b8yYLObv-RHqJY4?`zy@G9fsbZj_W+NUe>Z^-mwyAW0oeBd&t_ov0MFKc z9|Iq*{{~oFRxV{|#RP@wt!%&CeHUON0T4EkHrRU^bwqL%_ z>J|Xhge}Qo*?PwLKB>zqS{{HF(KZAe-=zU)5&vBJDiByb3zwi2d4ly3M=06aCHyb~b? z6(Yh{ItKN-JOHKLUFrdH@#)VhyYvb}h3NH@zfb)x1%Q?Qt>e_u15m;9)V~3k(Fy~+ zYs99hx;6{}z^auaLqK;T2$9Q#`duai5CCf3ehB~?t(^fz;!jmDZtkc+H9MZ}|g{HLMHK0~uao(X{Y(ah0&M+CE!W=I-5uW@M;7%^9RMDqG7l@ZHVMA_-hY{%bL zT?W8x@ka??%K%odaz2a9K&8^C#{U3dUBl*-(KD@;0cMC_@~;Bo-Sw)GR2hK!qcKlm zBw1AgkT*1*`+5REv>>YNMEmOjpcGp=f7L_KY=;0Klv?$#ltD(;zfu=o>&D62sExM= z$agFCLo;$%1|Spj06->2d@7sLE*S}4D*zU-YK^LNtBiM922cwd`JARl?;LsnXo0{y z-u?f-6EFbY#g^=?`|cSj*t>>k5EKBazI*3iv-dREG6T>~nv?)VV^jdV<3xd=0H6(Z zDI1X8o|aa+CJ6wY^qm37o3U0<^BBz9@d|i!6WF@wG}Y zRw1A9y8s1%cc*y-AkEXr`KcQ_x)zU}$EPd-5vKqZ;Iz>swlo+NEK>97Sb0N9=Q`4^ zy9p#jks}aUq&Lvkm{*OpO6pHzQ@t@fOF-|=i);d~60R4f6@|~&^yj1&34q$+6td_* zQojlSwZ1z(|NSN~V#ld5NS#dssK)aI+v~eDcI13Kb02N{0U*MW+G!pkJhpW1JtFqJ zOXqim-SDr1^F?`t9T9ZW(B_%w_*ubyg?JG@rvs25lhiqVRN(VI<6Ew=y--{R068y@ u62DgC$+5{B;MMIf2VnI8ef#`=0Gm3MfT$ppl>h($07*qoM6N<$f&c&*BT0Dx literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_3d_printing_128x64/frame_16.png b/assets/dolphin/external/L1_3d_printing_128x64/frame_16.png new file mode 100644 index 0000000000000000000000000000000000000000..e77cb28ecf1924de4b41fe8631093f238e38d821 GIT binary patch literal 1284 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 Ju0mjf006G3QqBMX literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_3d_printing_128x64/frame_2.png b/assets/dolphin/external/L1_3d_printing_128x64/frame_2.png new file mode 100644 index 0000000000000000000000000000000000000000..67b294b31632b9280c3c1f79db284d659577159e GIT binary patch literal 1269 zcmVPx(tw}^dRCr$PoZGJBAPhw(|Nqe)wNfg?!FbsiLK@Fo6$fAT_Knld@p`>p|MK5s zBXA@E9*h0Q@g%^r@lOIg9RDOh5+H}bM{^)&fJeihL*T>VPXZ(X@*d#X9LO2q+4$!g z_;CD_07-zn2Y56GZao7W$0353yk@+<9RVolrCgFhCc|wAa01!`^DWbQPTpnt@_SOZ zAbl`ZbL#l2S-@(K6=+GG>b)HSS~q+r0W3YAyb}Qi z1tLOMc?Q+HJpo$T-I@Vv@abo*x{Lxtff&_OzfbjUMSx!U@2yks8Gr&_m;NNcicuKQ zTViZks-n4CjBW2otrxfkR;b@9jAFS zLqIdp5Go`}^T+1l}XlpaD1a%e( zC=eD~i7eH-lmHr#y&msfb4ak>GD!d$DW{wU{`=UOy;w6l37|!2;5h-G62J=-u_2Ox zL|-qYqqc0*0xW@`PE!(;y0x?EnIsR&;8P&Vb7{=TUqV&CXa6>kIzubK87L{R7SQXU zYr(RDf6f+G-#mJ}Gt%@Sa25e-BAlPCAbK)X0;oRAOGZKfZE&T!Z%+UkENQ*85~ioG zY$G-CBlVP(MXD}T|NEykVgFBH?Ig+m^BQ+XnWULILL#Y+5Z|Npl5@mMwkrv+8vMv| zuT_-W5ukSsTRTfHY z5-HtU8zVD77u&N1j0VO+fRR7n(x6G?8}*Y8f%TR$LvQcyg0{Qw5IAa}g#fDtzE%cC zUih?jZ)p(r#FxwwTI<&IUP1uPD4OM7Ihq~T^Lt4^d?tXGHj%ci>0Q1CY6e+@J|Z7Y z1dRYzfW5w125hwg&`FxK01Nn3uQy2b@D|aQGy;$doO*URKa*@PeW2u42|$gGBtXmf znY}w|V5@ChmGN2^L8*tQ9m;8MpxePdE2*vW%PN4? fu#{gDf&Vi%gMgoK%7p*`002ovPDHLkV1fVu&#FYU literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_3d_printing_128x64/frame_20.png b/assets/dolphin/external/L1_3d_printing_128x64/frame_20.png new file mode 100644 index 0000000000000000000000000000000000000000..7a80cf5657f40e946881e8f889c386c45dc3c795 GIT binary patch literal 1287 zcmV+i1^D`jP)Px(z)3_wRCr$Po7;{fAq)jK|Nqe$B~p(N3qECY?P>OH)qsspVQiFnz2EQmzw__0 z5qKQ{9;^L#;|G9e>wf@vxc&!#1HdT)AI-q20Uj;?DFPoZ{{i3taNYr)&A_Pvo~{3B z10Syc0pI{|-T@xXz^!Y5*Xvak?{Lkyz8wHm^mN>Tp$@}s0JsFT#QcmYeJ69-dHH*z zZUI0|*peL9SkF3tPwMiHwg(_0+JS)MyEOn2@vp7#0)bVtaGByGRsFLq04?Hwf9h1J z2M8eXaQ{E^01%9(5|AvrLl#(7fKt0#W&l;qr{b%wzyd|3M*^7|LxoW5c)wG-w*kO{ zBHH@($TCV8Dui0c`<>d|4uFyAKDE0Q09N|9+Nn#iIb0(ufbh+IRc-EA@e0iagxmjIyAIvHRj{!|6y<&Fwe8)sb|mwP=! zKyD4MITV7{fS0<9D3ocOyx^pM5c|_IfD&JkN`;J$ z8x26zuU_-2*XxW7K*Xm2M1>+gL{D3Oe_vfmFY$V;vkU+dcyBdMA@HP9x~3AN_HN;U z%i}kx0j$F{{0bNWz-L3&yMY&|Ydw!hRDXrqyAfoM9 zsmN{diQ2tA04gP0-+QI~yaOnmM6Z~wzF`19ZDLChtyY7i|FmH81}W+{4UTvHl4wy| zdF;Hgp>{uiTeS>;+2Yfnc!iH<0L!2009enkc|hocjg^t{&-0D23eg4)|&=?%bB1LPGy^&7cz@opDL0XF(e;)xx|Oa=08mbO0uUKX z)1!Co762ldfLz-u*3-KH<&Zb%E#rHGpa4+!d&^$(cAV}_W&m1AlPh2+@jZna6-EJ& zpK@}7tu7#KKl5?j0H99a8Gt;HQBd<3%sTN3P!qISMPM|jYI7sL)6@r?7>lvhBHE>dgY|tBMYt2<_?UMS_*i>H(ckBXI3D*kKj>4;YKwbgl)aGvmwK+7s_YNer zs{o*}xYM}rPr`bfIJE}3XVbu`^*llL+Agi#(;tuTqsRUN(8G{AX&xawW2x^gBDTz> z{;e1{{70erraZ!q2)d_58r7Yw5V~e_Iskc_q`D84RieSn>x|kPhSm8*ZSOR182~v5 xrJ#%wzjnuywrMBvdib&&fF7GTYs4e)146%~fVvLcZU6uP07*qoM6N<$f&e-Px(z)3_wRCr$PoZ*t|AP9wT-~XX|C!I~kvA{VXF{WYv?ZhB*J`inpU+?$({jYu= z8-Z5=@L26X3oihkt$zXVaQzE_0-&0}M>9}8z@z1_Ch+0%7XSr7-2*(Ef$9OCt$!T@ zAFh7^Pyp0Dz@r(sbr0}*y;SjvYmV#N0lgHSV(W$@!1(inzGBk!20@?9CXXm{Q02UO{ zqhCgqLBg;?AUodY?7YhXAc=y6YM-+UpVbR^{qYK|jc4b*9RSh|-wuFh&u8Ar5Q7yW zW3JW)JMZ!UNZBp*0KNF^@2I+r3d0I9I#2&TJMU5eSmob3PHjB^D|lb}3xFA|FoCy5 zY+I@|GYkOMUU_B+*i0ls>=)*%iR5RIE?ixKSu?34kRq3Z5n; zBgkm5`{t{)#|jZ?;H;Ri^IlQ`G*L7m7;&YbvzI`bRX}T6y^xLi)W+DnESvqcaMa96 zqA^)4oeWUburk;4u4Je8*#iJ89#aAKF=}$n1ON%?*#g)S#|k-`ank96oqNW(%K<>* zXEZ(|fh+(DU+ep-Y0l-9jaLj0s znXPGg0JdAl!8jhTYxG<8_iJ3X3C!>{Dhirg0xCM!NE2yokzdeW3BYXeqhcW8`#XSk zyfr8DjA`T~|Jb!|e_Z#e0M<2ZKLLATV>N&?#FysJIO?GzpceoJFgE6WZ-mwh0cgt! z0>!fhjC_J;1q_`!P(TNfji!0h1as$@kR&=pkU^-Hy|$ z1JhSg6@YQd0|4U~Gbb7&WyzV~jWmHM^R>02ts4P<2>{3`B<$#Owmf?0837uuf+JAUS{L-n~P?28}%h%*uF_1SUM_9sn6{gJ&HBQT8*(8f1g@ z6^)C6utR#4@a@qW#q-eZY5>?^&!_-B7_4u71=&mSY!H?OVC0-?RTz}XS49{%Z%n+RV?N&r%*Zq=D?GtvN^Eqo8I4^=tTO!{wRHe6YG`}k`ecfQ zU}k{5J<3eqGW1|1XrAqJ)QqA3x71gSN?G* xyDenVuJr(!&Fh)Io?&}h1vta`vN;}s|K^XgfP$*Ki~s-t07*qoM6N<$f&gh@ND2S| literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_3d_printing_128x64/frame_22.png b/assets/dolphin/external/L1_3d_printing_128x64/frame_22.png new file mode 100644 index 0000000000000000000000000000000000000000..c83f6d7412494d01c78c8183ca17e2684acdba21 GIT binary patch literal 1278 zcmVPx(xJg7oRCr$Pn(fl+APj_$_kYp8qi1j!OtPC0T597@OCe+*@_{~{&*$@BeQxW4 zM*(nK?cW}=JFQ+1wh>c+?#=_0q(7TwShO+ zzW^uz>K@?M3|zVfcsw4ec*Qlx_2mFyMeoNI3_1*#0iX$LiTN5MJ!jtK&dcwOx zu#z0sSkFAaC$+qz?E#30RuJ&`ZVf<0{I>O7Ah2o{UZ(g+RR3%Xz>N6szd9-P00T(e z-QRB(06}je0hwuchyrI7pzOL^MgUaJ_u`|jzybw@M+P!9h7|(M<9*Jqdl>*MD59-j zMwCIqutK1DywBNnw*x>DrT=cssW0)R=j`cT4FM~JS70=rUH4J|G_j<@Y6M|)&?t02 z7Yhq=x^^$|`hMjaGwW^-z$laifh4{r_}B$h1UMQ^;}}`t-+THCfd4;~A$hln09wwH zBEZNH69F{tDgq!xB8e~XPBs9n3E2Qh^$BZ>HZCI3<%&Sm=6O><0<`Z{-yR77I+@yyUi7BV1I=mxJhrSDdpZC}d?sJ5aLjlw zFuR6TtIWyJP z`iRE(b$J)C0)W0a8c%K1=@|e>E#GV#*!Jp;PVBmu1wbO*drB;jP;{|6fc_aPL^R5w z$brDQJOB})vI{?JzU={c{a!HR#Qt3?9BXUz1rwq*%g(4MXniS-MAsTqr`I>^y04#% zP6Oa*@mZ%Jp{=m19RlxN#^C^XZsFJq8+7Y;gdn3>-jn2k!5aZ)3uLQsWqtdJ0G4Bz z%G%nz(J4XPmlVSXA)n17O>3P zOxi}iKD-M+1~D7Y8j}qI0s!UrtbKX9D;NPJqoZBGPU1`9ch)`xfK>EZ=>+9@sfOrN z9g}Jy0IbnW4KVBAtP_Hq9<_7y!BHn3`Nd2X?II|;>7GLCCUACsw8_lp(bUiYI7|4x zyFzW^N4H-FKof$6(c3YMPCpuiZ2@2%o_UH}>hfN}N|kxk{-XiV{70pL4Ci}q!L0O= zVGlaNY^X=`ShljEIAgz7QK;5ECb(ol6qIWAlo!R0Loy6|A;` zX@Qu{c^!>T9|K>uUAu&Srj4Z9z^H?w;crI>7@Kw!e$?(OO`{aRh(F#HM2eCAooq*D zM1gFZSii0w_x?*5LpL%6&B5f^5rPcidN^n1L;FMi+4cDcfD8kXgFO&<#}Sy6;AioB_4av5O8+Q!vPrEB-DMNs1kL+nA3md8r$oS(*R)lmNBydue=OO;fS0#Z^Z)<=07*qoM6N<$f;%5k@Bjb+ literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_3d_printing_128x64/frame_23.png b/assets/dolphin/external/L1_3d_printing_128x64/frame_23.png new file mode 100644 index 0000000000000000000000000000000000000000..f3a007fb3b375eadf50ef9065d1b1ce446c90245 GIT binary patch literal 1314 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 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_3d_printing_128x64/frame_25.png b/assets/dolphin/external/L1_3d_printing_128x64/frame_25.png new file mode 100644 index 0000000000000000000000000000000000000000..266d3cd7db086ef12f7b100c1151221e8ce71d71 GIT binary patch literal 1281 zcmV+c1^)VpP)Px(xk*GpRCr$Poa>S+Aqa#|-v6Pqm7FqV8K4`yPKN!NQDNv$1UI{n=kxjemp`|S zz#{>;t@iI1PXO+%e*$oG{S$x$AdA3TGmtgFt>w=m@aFO-00}_e1KgW|tO4$=f3|@) z*FOPB0P-H-)(l*_26#Lks(8sY$Mxj^U`4OTB@8+YmjR#%YKi$8BRyy4a{J}?MqL5` zO;|||YpiFT-;-K?qvHXHh?WrW_#O>FMEvsVr$AuUEWAwdk*NMT7Jwe{-(Pi7>H!9j zc(}i3CIG=`A_19YPsjqh3Q*SWkr@D0^R@V>E3iO8>5+j9jbVjA>v*5Db}s{f1x2*= z8bcA=Y{!p+Kx*#8N%(y^vRT`+lr6lH=9j(2@YsHm;>==8>h zwYwF7)#9^3@d|IXl9o;88B@q<#}RWRegN>?!m$@N=+8GJIMosHv{w*$kx%o z#ufrl7fKdU*2_ylEW3ZSASkG#?e*3GZP&v3kGh?q05H421HdftrRw|S44`!~OuXwc zJy+#a24EcW0KlA0j7>B~+9i>S8rg}~01U9mHafad#@hk_atgh4?{hXidhK{?fOdV9 z{yo`@eaGcpz;5xST{p@n1b|myFMxH&;06Gt66VZ7_W+(iOTqHY_XLj(7FrVlz!uc) zICNxcM;)5?wo@*Gtl*-Gvp75o07;OUXLnnSML}zQT(#p7n5MQ20LTCX017ICYy|+l zAQT^W314?tD4w5p-3fco=DFn8>JDIO6&*i<|s{%z(OQUtQqI8y;fUosN-s4X;)^<`HbAo)*afSFwY zDgdpZvjIoKq30euXzWSim-klN{1ruD6aW-xBr5ydYeyS@qVR1DzK-dU0B8Y^0I)ik z9v@fx#)2Ff_x?_xcL#V1q{!NlIgyut&GFjuj*t4aYTgHc5eBX6WOj$G46wH&S?Svd zJ(xA7e=C^XWnM9umqTM%L3j9ud>{3E2>==dJ9^|ga0q~37XEW*u=9^Y*<)c8?ON?J rvUr{8>x{LRWq=vZ=bHTy_ydIq(SQrq8YBP!002ovPDHLkV1fVuCht*g literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_3d_printing_128x64/frame_26.png b/assets/dolphin/external/L1_3d_printing_128x64/frame_26.png new file mode 100644 index 0000000000000000000000000000000000000000..66e30789820f80ede3a0ea937f2bc68760263e9f GIT binary patch literal 1299 zcmV+u1?>8XP)Px(%Sl8*RCr$Po#B$(APj_?_kZYgoT+ap2ii{pV~FX$8v_!%E3r7a>+Ah~zyIui zk450M1@KtzKO5fycsBoA01xMX3t$Uio4`kFV0(Z^)4xsN!|C4w*aFz^0iLaa?E#+6 z|Gox3oc}F=Er9(V;L#db+5@~^uPS+4*G$*T34oHGj@x3Wi(wf7oP=6r9%D++$y`=1 z-#2Or0n~^s(P54Ctn+0Oa);Rfm8h0uISnc1sP8}YA5}vpIEr1bL z7|6S(*tAvGmLUSL&dQz;SV9Fb6o*9eTdo5T0o2@nNdOe=-W{OSiH=+%Bp&l9k;@+e zH6kcM)_f&~C^@BdYo4Vg2F0Lc0+sk~@c^|pSp$iXo6aR|I3M3aP!9arkxFJ&iLqA6 zib!joHFvlGl$onll?S*8nUN~9Isg%&XHTtTtHm|)_W~3s))^l?<0x=j02Haef15Y8T{G&(Yk^)GpD+k6ydaZI5$+hRDJA3pVAI;&MN8>IlfPACgF5(4> z5K`(Ql!#IVpW=>mCmMHo0T5!WiuJZuE8da{jN89nsb)ODky@t&SmH9$?f}Pvx2d-YX|1fM}JIy`*D2w<~!Bl#-6#0(t@M1w@Uf z)k4;(6#+GGHD5rgJw00Ko}>UuPPGIC@c^?5AO}5~XO6+BidVuk#iQN>sKrz8dVMfM zV_r3mR!<)P6u@1?r&sHe0WKEqHw&Of1g&uIjUfkpv=F8h00q2vMb`-QRJd0J=^jQ4 zzjf&&cDAY+_Ik9pTcL>=RX4Ik)Q(D zLxg86&Ao-hmbo;)JH}o9rOPx(#Ysd#RCr$Po7-~aAPhuj{{N#V<*9OtB9gi#Fqfcx+cj81ebB|5yg#4M=aqks zjleqrI9B`b!4rV9^-ll}*FOPB0I~@@nt|*Aj+Q^0z{BNF01|-w4sbRD*#n%de~y8N z>z@E50Qnu@Xa=_K0p9PoDqeEUalIV?tmx&sgh7X48vt%WEitb((sO1ncVGVBsx1J} zgq7s5)_UIa|D={rv^)S2(Gmh4-=zVFh<_h_6$q@Jg_kM567@gJ0?;D<_eY(Set-ca z9`3(86M$eek$}u=SI7dh8c^2mk{JMX^QHJ`DzHF7>5+j9tzm^g@9{oo?QR2r1x0l9 z>yc%UFsu;hJ>KW6-Q@s~L_tE8&)I{I>IJ<1c!k#1vv#)wK>EUW1K_Fo%$^=%utM~( zRlb9@yF38W?Us6gT735JD7&-@!wS)Ar{2%nT?zmz{afSI)dR4C*Qq}N7|{w7cx%L# zsakst1AtX4dxpRQl?a2lB;wz88Gr#m>-Hr8*kJe008%Epe2Iv7?PG=9z65AOumV~8 z5e!jrr1xq)ON$LQ22vA<;G5C|XlYUl5+iQ8mr29*`WAuez~323YE}`9+D%46TKlZM z!vR3eoOY``fs2S4xn))cU?B9IsdcS-xkmn801Ap#4O{}iMk=@tiSJEuK{wQG zPXiFiN@Wx1jZYo#sTqI~pFLnyD8`4VNKy~bB3`7Ki}ntW_W(@bZ|7M7SO`kn=-uo) zQReAA2Hqec|Dz>xO90B-RR_i+x^{Ds$l7yhW{=MCt-i5#w*}x15L@d!qO|~^0U5c$ z_nt_0ItGBKSkcsK*^`7~g^*JC5CE>Plr2Fl_gd*#vr&FD@!pcC-%(MJz1{#=v1NQ= z8tdzKtEK@kTKuPkFUygq1X%uzDs8_2Sf62Q+Sm^^=&RodK^WiBZDOB$GeGNvWc3DY zY#{*bJZl|Z3Si6s;GsHfJ%6&ycma@Ni=Zg`g8Q7>g>;pG*1&iGcuM?wbke|MZ?tz^ z1b`X>uT4$lt{H%F$^!sHCSwz=k-iB;Mk-_{+5<4aBHO5Rql-vS z03Z_C+t@phtX-5%(D`W_;eA<4!egMokwtyyTgS7wv002ovPDHLkV1fVu DklS5m literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_3d_printing_128x64/frame_28.png b/assets/dolphin/external/L1_3d_printing_128x64/frame_28.png new file mode 100644 index 0000000000000000000000000000000000000000..897dcb051e1a5119d1cdea6dbc8ff89c55b1797f GIT binary patch literal 1302 zcmV+x1?l>UP)Px(&q+iYj;ZpK;3)|KAH+Fq9A!>NQTz1K%jlR&sn>-A%I1S z=;${>We_kd5NIFobJp&51du?f|JK&j7x>b1HoIq&fCa(}Fj~*ry%hlpSW;uv5}^%H z3v|5~OBNJ#?Ox*b_n9_&>~2qh7AT1X68IY6?G%s-u$r1i8d>u1&Hg07{}0L#yxS;% zRs&TmjbI<^_*e!+ARoxGSeyn(p(<}Tbr9bS&EuUbk@-Tqx~)?027cA z0H8o1;G*wK_Y3u2p0oX231GlyvpHI?86P|#F+_?28GTQhBv4$ZH-SvxQKKTsKLeI0 zkWpeG-`H^s?c-G@*6u9{@Ct@?Z{Q_V*^eO=(|OnXus~=%PUI|Be7@|=o&shlfF^ZF z?zHAF@*dkr_7I@HPw9y@Jb>JSkJyv8DW92NRjKL9~`KXOEVEHd{Tm>b06W3{wEcF%JP4#~7Pvjg%#kjvCqNB|xhLtUW7^ z+be+QkfYbK!SUMl_5khvNd7(j4131qW8lmh906>k8X*FB0rsA!E^}@Ite65k;7h^s zyzdDf8!WUZB7oHNX?uFcK&{j<+0P7rT0#Q&A$yN}cmk7njRZ=hY1d~?qp(XKZBGltGN~q5nS13Tw zoNA3&tUC3sV@cZE)%W_Ty-z8?2;0TafTM?u z1U_mDy~p~pTK+A+DFvAMSpaGPE$7*QBjC_;j~%pjC-BR;)iM979smKNjh4)N-ZcnN zH10{#>-AyP`mZuQk^qq=M0;m-7I{=^wQn=Y@%~DfHv>EkQe^MQJJBF{ZF$#6{aW?j zhX5lCTGz>JhOG>+cOqHo+Xy_EJ*Iytl&$4nFqp%kH7uZ4_=bER^?V5dG!k4fBiDgL z1o&m(KaK0`e;3dOy#Adn3!~Vs^)4fu*O|V~*n61*%y2&U?2o`7UF+9?6!>&c00000 MNkvXXu0mjf090L8RR910 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_3d_printing_128x64/frame_29.png b/assets/dolphin/external/L1_3d_printing_128x64/frame_29.png new file mode 100644 index 0000000000000000000000000000000000000000..0aa3c865ac7dee6acbe7532a58a17e50aeeb98b2 GIT binary patch literal 1302 zcmV+x1?l>UP)Px(&`Cr=RCr$Poa=7vAPj`N@Bh$SWhE*^ZO_;^glv3%ZU`99=L;x3ulM`?{;7YD z^}wqDc&zrHjTZpV*1rIFxc&t|0Z>igqZz0k;L-9|6ZmlX3xEQk?g5_7K=lC6*1wK{ z57)l{C;;jn;L!}+x(9f@UaEM-HOKYs0ANM$#}y1Z47UNG32KRXjFFx*?{e|-eWPvx zfF`UYhc(tS&-Y0!?`V4fBBB)pJic245D~v^eHRF$%AKoX_@ZLFz>_|kJWy3dAy6~Zep8qcnKD*&2UQe!oOFgj=y zx}S@M1vy>2mpFbubB&pGw+CPpN`gQVUlV*h1ylxDjiwPsR{Hlw|5X5F9s6cwxm^j+ z43;v$$PkkOH14u9fCQem1hC>+C5fG5n-Y)>09&um1fb6b-iV&Hi(Pvw0JP&=AK~5x zkV(m2aZuznd)fO6sbOuuMQxK#OJcKl#aF?aDB5PVj$NbNEBb7-^^UcA6*eoKF;4aE zY5**8EC4)Ci9}5n1O))-^1WBHnlNmW!-8`*03ImW`i)8vQ-YTD0N%9B3YRr58-T;E zsF2b3#!|%e5laAI#AlC6QK1+cJWZ z_#UJJ!1#1qy3j$QBEmhQN1$TrZWaJc4YxAU*Jc2%)-f9%9TtsH$?;MaiO%2870FKQ zy0u=36nlL1K-<@Z#mz@EY zlz=M%SS>yq6%0ffCBU+0Y@WTavU~l@0G?Yo>(1V)&m+;Sku!qKG@9OZy%}Kkgk+Tl z*0&IVK4n@>mbH1wk?0E^uk**#f%d*Pl00^J0g!x)py>O8W}6m}OjAq&z&ODJz**um z*=IKZMhF;RGg&P)129H;0AR>u)o0Lkd+DIm+yUWcIZY_u@*pbQ|D zC23yIF;FXYOsRzcNS&%?FuDlZ6@c*s5w&y70IMP%foYbCb`ccaG}8bvB%)2Md>+jR z4S=hJ?@w2Bruk+7G$B~ny(fmz>8nB5768`a*-@?u=!tMI1k7DT_CC>kR0%+nFwao~ zq|GtoWXmozFAD;i2iee`uL=Y<{eSv9R01+i0nEl(F&Gf7zQpRd(Yc!0$JkJM>^*BN zl>yM^5yj{X%x-Qb+Qv1mnrn6KXMC&a3o`&Sc{?gv$3SoNc*dg?oG}*z(~iQAzWJvS zDghn<&{$T8Y)7)|B9PGj?)d!kNf<*n+8mn0TjSAu$R4R1p8+A$9*^y3kNpE6V~cHYBsR;&f0dx`^y1nn`9#V%%~gt zjI0Ls3K3`4ybOR51ZfjYmYCVR)F-sxR<513hsZpa0}wH~-pTX8FT1qVfFaO6EC2ui M07*qoM6N<$g5zdby#N3J literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_3d_printing_128x64/frame_3.png b/assets/dolphin/external/L1_3d_printing_128x64/frame_3.png new file mode 100644 index 0000000000000000000000000000000000000000..7d3365ee4d4e78bbfa85d5cc9eefa9e110fe106f GIT binary patch literal 1233 zcmV;?1TOoDP)Px(iAh93RCr$Pob7TOAq++9_kU=2>?s+B0{4jT4eGy+wMgh5gtT$;dcWWAKmPC9 z47?nGYqkGw+yS^-e+S@l{T%=Yz?;CU8Soz9YWckhyj*?g6+O0d7|Jj_27n-FP0Wu??sM`iJ1@V_>Jb2x zgw4rOv$d@A`=rkA=y(8HL~{r@zDEPlB7WHVDG=0p7A{k}rRsl<1)xX#?^m4){Qv=^ zJ>CC5=>RllQwd0xJ)sKhYCx&qBP)RF=40{IR8Rv&l}7@ZvY|q#b*!ILzmEZ+21V=W zH=@cYVW<#l9qZ@R?{NU+L{UP;pVN)6>IJOvScNXxQ@@V`AoqqZ0ARWE$ulFwphArB z6|X`49uGk7bmw}2RDAk>Rb6_8p+fZfNuQ^Fj{=}p{%hmZrw5>dr={NkSkVdtyi3H! zrMk2X0ibqQ&P)M4i6BKL6YBSv3P1oTb^AF0Xtulp%*3CnPLj3!R zy8@L$fc-udfRyysa!M0L5?>O3_VuPFr|Nemo(=#Jrerhf)W-}i;6$3@~Ze72}zW9=DCjhI(A6f28Ww%rTdc+&? ztOTI;8a54pzObpCf)(QD{Hs#jlk8drP=2>x0rbn2miP4p0F@Z4Agc6^_7}&O_7oOT zEw&^lbiJP*pf@qOhN>h+&yR1(=HoGuR4=-wva98Q<3MR0TL){1Fu)U+k<)c`b23oCvWpaAGevO1BZx0x9qJ&B|uQ5%a-*#wT50#t%6b1O|kSaXzbE2O8K zel-b50O;sKMj#@dHPJm4fW#9|2n41yUlt!TZHv>}iBrId5RjNoI`y<4Kox+HNjAUh z1W~`Lnvgs^&F1@yA%t$VF|~%J^S0dIDy~ueh1ws9<3yVu07e9-PMQS*i!F`4N5r0I zX?#2Emi}s(^)${%x@APN#=AqnUZ2YWh`VYzqKH5C*t7YxXGyD<6_lP>c0RiefE0vW v64aG4GI0usk-kn^YdZiZCF90e{sOgNdVmw*)Exi-002ovPDHLkV1fVuECoI0 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_3d_printing_128x64/frame_30.png b/assets/dolphin/external/L1_3d_printing_128x64/frame_30.png new file mode 100644 index 0000000000000000000000000000000000000000..1142b205b5a36cee02a31d543800a4089632551c GIT binary patch literal 1293 zcmV+o1@iidP)Px(#Ysd#RCr$PThVeOAq>0x|3~L$Go`~&+mel8*}%PYF3WUzYLW@z_37|dA!frb(bT61WNr`d!}CCOV8Qr9!&xk2rs~BJiG3r2vES1j8#j7 zHbE`W^;|4jP|~$~iR<%`YxJzUJONsuBoavAYk;?1z)^tN)HKS-l7DaY-$j5CZC?bU z#~lG$z>)&A9AXqe<6X7_NW|+o0$3oWBac~knIj;Z0PJ}^k^r?DM5?&w(u!SsDFSHi zTwUQ_1IS2n4>+3Hl)4p`l=V0kNKFCC=h_}`A%F&o8syKlt|qUo)OkVAQ~}GYL;+jeqk0?<+gzV6Ku)iaKbO_FVj^yLGxoyz9JCfX+R*!v=bkm831rctBy}%#e01u9d1@%g3K% zInCFujrW1pM+&pnpG8^ffVz@OKr2*~W$0FjZ!{sM5@7UZ&iwui{0Bv>-GFcvvzY(@002ovPDHLkV1fVu DA$U-1 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_3d_printing_128x64/frame_31.png b/assets/dolphin/external/L1_3d_printing_128x64/frame_31.png new file mode 100644 index 0000000000000000000000000000000000000000..f1c13be32f10d04ede633eb7904276d265c3aac6 GIT binary patch literal 1266 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 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_3d_printing_128x64/frame_36.png b/assets/dolphin/external/L1_3d_printing_128x64/frame_36.png new file mode 100644 index 0000000000000000000000000000000000000000..e6b6569239eeef1dba1309ba297fc43a817c078c GIT binary patch literal 1302 zcmV+x1?l>UP)Px(&q+iZ_QmwyAW0odOG?#;mN0q(8; z9s_T#{{~8 zs!IT13EPvyTdVBzeNy*Nv^)SA(KZAe-=zV_h+mGr3Itxy!ez>@jQXEt0XQQ5&#yWe z{QvrUDO?C_NI$qBT^AXdm@CwR;%= zJSeiG--s*|2}6a5_EDcxyUPL46Q%yUwx+(s?>(ot`)mlP5URk@dTRGl02Hx$ja3Um z8=zL`dM^q~6!g-3l67=V*0-+#pcSeIK~H>3@OBCqodJ4wUp6yBQW32WV#MxJ8Nf^b z>g`_vfI3rU03hS6l}V`WWdN{f?`44Y&?EzRfy#5&Jb1Eb148UQA{WQeb%)athU)vMIV_3tX?Gp z8Li`&DgmB29sna~_x2bCLA}or0o##M1&G?7+5<#4(<=eJxt@$U6r3{vs6|u-Y{yl4 zj`}?kpJ@O@;uB}pASmls1mJ7F0}$~k09m1k57}E7R&2>tIRM zWX~w8i~^vIo9Y|Q9;cUpGXU^vt0KC0b0eX>&btg8Cj2RI9W_Mm46iU83o zPe%YKAZ5xPwR_nNKm>`_WCE*>l2iz<%w_zfc3=OrX4wA|*w!g>`|I!KYMqM&ww}}A zsQZu30JVKJHuQ`i1z@)LG$^X@bvd#wf%T2UntrA=fh7YdU*YHn8!rRQ5Wi<%y-5lT zY6c(~((7%!(m-K10E&F(b_1eD*;eXGbIyH|07L?0uTk=nxn15 zoF#rc)>+_@^JLbj8NixkRNHEK`7cgU382noG63B>yRLUjG95Lwp9P@J1k|3@Kh*n0 z84r~J;*bJBJwqd|xOi0b>1v()b@4R)9T zXeUiFfFeG%J0fVKK!^b7HGP)<${1*sI(t>0x1AJLC&n*Z8JvwY{hGR zoOR=)OW>9Y0zhPd01yQ=f@}r=M?ok)eoFXyx{Bf{Wq>mQpy_ue09g>)pw9+jSpbgA zskInU79pT!)=)@$B_U&P^gE~oj4(>RgJ**IiQC_cN5&7Dibur1&0Is8K-!Iy4mf+r zOyIM&qW7pTv(vxlH_8Cyqk>1&Spd+0N5VzV6+6+|mBcUSHhAw_8Z<#6vJ>ZDj{0ZMioR91=1po_zPt3@5 zP@)_C&BB*BOaq`mPz#_mUWDe%8n2;S!N1Izly{~Apg6YI&WXTZjO6Knp}2Zl00000 MNkvXXu0mjf0J;8GoB#j- literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_3d_printing_128x64/frame_37.png b/assets/dolphin/external/L1_3d_printing_128x64/frame_37.png new file mode 100644 index 0000000000000000000000000000000000000000..dfde848c2a3b60fee042db787574f47e027cead2 GIT binary patch literal 1284 zcmV+f1^fDmP)Px(yh%hsRCr$PoZFJ)APht&|NqgMN-CbB7>RBPf#q2HwgGcdA6>xSeLSDf=TH5) ztpy$hz-_gEZ@d7wxBdmd&Gjz;3Vfl6o5#;%N2nmaYg_z#`hX)rrKtXo1P-BjbJm;13kMn0wB-SZuGo2eHLhjduopR~2t-gXj{qwGXe;D?gBH!YJTyT{kRtvU^W-Qu%GK|+rTFAD&p_3TP*e*jo+;n)kCQ31F^d})5GCt6^z z)&R2w@+gI|utNaaihC5MN1K<7D80h97#kgbMkyy^9%&3K0FrGH6zs<)p#U%jvjDJ4 z{JtR5z+?8~>Ei*Qhrl!M%vCuR0hmQu0N7RVNp}L#m4DPJ0C~POqNR@j3{h|t?$m&c0=$&Wj=d z+TRu1-=Bmrbfe9oF_=8NsxY+9EuNRb|2l^3lePU1fQSVmPqPSN>C3JiL5d;(+8?`) zO#POB6xwgrBJ7M{nvt!?GRt~13Cpf!I{^JL37tMxd0k&;WV^*=1)S}78UW0Bd6oED uj&Fb8IrprHGtZw6z|7+7*?9~60&ybafO~PbbN~PV07*qoM6N<$f&c*QZ8RYO literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_3d_printing_128x64/frame_38.png b/assets/dolphin/external/L1_3d_printing_128x64/frame_38.png new file mode 100644 index 0000000000000000000000000000000000000000..873f7650d12ed4f92a6debba1da01c0fb440daea GIT binary patch literal 1308 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 Ju0mjf004gtS-AiJ literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_3d_printing_128x64/frame_4.png b/assets/dolphin/external/L1_3d_printing_128x64/frame_4.png new file mode 100644 index 0000000000000000000000000000000000000000..b5dd2c1b9518e40bb0598a2b032b9d0b09d70dff GIT binary patch literal 1251 zcmV<91RVQ`P)Px(n@L1LRCr$Po!!#gAPhvC_kZYgoDLa=B6e4n0mtfP%#Y9>3CYQPeZAlB_n-Xt zSP#4sfX8b8Id}r_Z2c2}hwGmJBmh|iKAM570Uj-X7J(0!KLJPp@;$(_8OR#o+4^T2 z_;CFbfCM1l13a36Th{=u*UJ`v(`3xG8Q`HRvCbG2;&H^P>c=C{>brd zHbFwPc17C21l8qN0OR=LRQB*J1XhvQ6OmmL0niGL0I1iCvY|luvjB_^sE8izd`o;r zi0C?m^}{VQKr7st`&5ZgCi+VjQ&T%D7_y-}XZOE70IWllJ zhWNexkq~S|F;|Krsxp?bkzMcj()e=)j2y!()@xxH0HOs!sW*B*)JZ5=XGt5#0KgL0 zI@c1T_x(Myhcafgx3Z2hb`%6xf@ftT6xg5Ea|Hm5VT$9evAxU{8D;IhX8=H%%Q_>v zx)FFw06<1Xh(B6GNy~~YX8`y_KURzKme!$tLG`> zl>!@P09Hh%2t+it1y$<+WxnFPC=d_;ZIm++T#Kq+b<9-@0bq@e1fVC#m3Mb>>{$?6 z8)rp4f_(_utRfIaFKRVs0B{zBV&f{|>+TBq^Ly6S0I*Rv696v=tMKA1f@dna~9HGTjmW?ZTwOpZHtrbVUWh(F2~#6LDUdvCd*-+u0REa78~xt> zSI_$b5MhW?A6Wa%7N4nE{uBT#2v+pS zN=x_4!hf^c%>Oj1EuNIZizQkeH!A3MuunaYI(}LPPz}rRb3E`D#>k$4X8caj00000 NNkvXXu0mjf008siI9LDx literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_3d_printing_128x64/frame_40.png b/assets/dolphin/external/L1_3d_printing_128x64/frame_40.png new file mode 100644 index 0000000000000000000000000000000000000000..c6603542e14061c089d5d01e81a18fa03e515475 GIT binary patch literal 1293 zcmV+o1@iidP)Px(#z{m$RCr$Pob7VsAPhw}@Bh&0IJ0yZinvDtV;r;pw#FcIk3Jx2U+?$({crz! ztOs5jfX8b8+4u(F+4^q)9@q*!KXBX5iL6!0Yug#oJsnu5Skb6+IufVX$Gi4FE~dmYBzw+H>+Qik zbqfG2VQX@<#(L)YKB?DIl(@$)m+=vjAp07{{15Y)uC1TRkkI|Ix{vk^wE^zV)Ss{j~r>>DA; z?Mi@Ua4iFr43P|AaW^^x)WGwU04kg+M|7TLO2B9Ul!MN?5V9IpPgIY9GS5WKqw*?W z3bi&J%>cQ;3I>ljYA&l((KV=W#13*_a!CfT>ZIi)t7EsEMe1GL@8Yyc>fUt0o{_?}MLBP<;| z8h~Tnh?q4$QFzLXmi~Wv6G+6T07Qi%Hbfj<0@iyBOyH!WE%i&y4)X5lO(1bRAvIF3 zJrNiM>ns4udA)Eha}RI^0C{>X9pe!_0#&P|)SOO*up-zS9QkSFYcqgV>&S+qsgm>( zwJg#DK!Q7xo#?u^ECECeGA80!l>w}b6iMMz060FWB+epZTItw&zqi>TcAXjZs*KP` zysQLV3BYXeqhe6Vyd|JzPuV*~G6*S&$1m4?GJxk6j$YWPTfZ5C^mfx!{M49wNXsuQv20uXD9%~39Zf7qHuHrrfO*P zI7RlJ#zX)pA@Ho%+SAeiklGY+L<@qRj+)vM1)v84Rk~HiLnVMXz?%WcoM(-p(cxXc z2Y?6!%{%`8e>(vK;N5I(Jm~?HK_l#-bBqQ-0ifzTwOftzoUUXBs2LqS1$e}#>ndlE z=c7PS0Mwd3+RTz@&r2)alLUYoO-cX-pl1BpO=G@KYGH8*dqpPWscj`V37**?EDM0uGkBtG zMhQ^$*qec717O*YN&txnHHKV+dZQ}UX0P*=t&!&u2x%&g*rR|Y0VbP3+KrPAIJ(J* z+Xy|#9H)ONlt#2y46->jh6;LxZOZmh&!+%jL2$(xId_}_;G2a{ zewYS8d7M<|(@})0d0Gt}En8}hm7h-qU}Z-<)wUn(uHb;?_rrGp0000Px(uSrBfRCr$PoKceFAPhv;_dj%|oLZ(RMxt9FFc|G;OhBj~jlh|`KA+F$U;H~Z z16KfWtoGlD2LNa59{?P#e*h2wL=kv215pDUEq@e&hsz%T1ORala5e)`1DvgY90L#6 zKL7{-;vV2=2DYvNuIp083$8h?w*!C`J#QB<=rC*pKoYbj=G{hm&U}|!FYjx$1pu0` zk{q?RmVVwRwLGKc0ca5|AmH&`8h{q@)6`dipjNZ+GPSov_0O^Z^oalcsgqI_np3d=u)?AFNyV~Q zE3ERPP)&$d2PiFd)_p*2t3slUe69ibhy!Sak{aayYh7tyPf3&ZT~>5Y7YG0V#!b+0u3d0CybhajaHIvhGp#!Q^xb0F3xduqmNE31RDk7Ko(yjt77hp9R1IfyGW& zbAS>+EA%XQ!yRA*05xm1i0)kkN=avhCkI)LRCj=iv{6@W9um&P9nn$-b%3*_1s#=;H($SZCHie(FsUg5T^ zYE(V<9;YlZSpkr8i=bd%HM;`9B%wBWNd)f#j0IsO2hdJ3OukWlurvTQC$Ubx)?T_3 z7ztjB1N1_Gwr&JGwFNMuSRH`rb5t94by&ym0ifItn6Yc(vUB_efEf_AVQK(GBpSo> zIh!PQOauVh-`R0cJ4=BL9e}N*83$NN{5*+XARqwPf?9F|X0_*AD?29$02}nq0F?W1 zE2w1-X03R}5wtcG0BC>&No@;gO)4|CcN=!~l;b7J};mw zan0WQ8FD>DiU`aNL&#Nz{pB05K^YiDV<_Q22gI$f@iDbt`Zi42wRIm7ifMcKdV0f1;8UfB$x#POItSf9ua%K%f|OayWzhImWsvu zGX#rPSM(-{y&)^Bc_!Z8*q-?QbO0=g9l7etI^!+Z*j9)h1E2&!3Vi0OLqJ#)yG#4W f0oWx`Z;kT@=X>CQab4k}00000NkvXXu0mjf!Tc$Q literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_3d_printing_128x64/frame_42.png b/assets/dolphin/external/L1_3d_printing_128x64/frame_42.png new file mode 100644 index 0000000000000000000000000000000000000000..3e3415af66fd99906d2d0fb24b2ffce515b3b7bd GIT binary patch literal 1314 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 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_3d_printing_128x64/frame_44.png b/assets/dolphin/external/L1_3d_printing_128x64/frame_44.png new file mode 100644 index 0000000000000000000000000000000000000000..4b3e29fe106ddc62c21a8630151619a38f40b693 GIT binary patch literal 1305 zcmV+!1?KvRP)Px((n&-?RCr$Pn^|(>FbqV;_dm2#aurh?hCp|Nizt)%6&EZIo8-LS@Av!P`P zx&IFU2Y~Y)U~2{*-2=Q{FVlI4YmV#30l+#vUw2@zVR#Gx$)TPzuQj#j%w8^DzHikd z0I(cebBDK9Ip_OaUDs%N03uF15b*de4M4>CN%d7A@Ol>BP4She|5+A*7UzF{>SXi- z3?OlH|J`{22wD?4kU4gREO1u?%HF$V20-0>?tC;Ac%Y#4$UuhHu#Q0IsL$DZ9|M2~ zMRfG*k!4E9^iKIvNFNJ8%_+!H`EyqBQN2JZt13?p)Qr8$WdH;~9$_gu(jKMeGkesu zTQ;9HS=BQU{MLEw-OB?|i|$$vP>Z}geOk|>2KEep-CLG|NGfNYXuS|)dZncR@S@+V zt;yhw!^_k^3jj6pd#|?uTm}HE-sP#Az*Yn!%nWfQ@1=Yoe?+B?B08lDA+E6I+ ztwyp1fbwG(Bg1w8C`LgYd&^mm1>kyz(yF)>eN6q&znrOcE-H64D$#wlG7`P3w4G`j z_TKA%t8N2ew)0t|AV*uzRRBDBrUT%;hGjZZPx+d`W;nl=fz(J+V2A?1+~Iu)tLwANM06_JynNkY@V1@1pK+Pc|Yj>G~6;R7Mp6{I%@hAw~QjuN+MU~EtSDt}J6{`tS zXI^9g^?5WSBuMXad_G;Vwx==xV?dMvn!8o-@7|1z>KYY<(&)26SQdbmIn~QBszame zRS=?GeN;ZQk17G^5azj60%~6g?!B=JIWhViIh4&v zD>}ZE{ZEDTfos@mQ z04ST(NLP%etZkK99q(q!)~Xx3fB4VBFF_c?sm@>(^wu$QCHcJuxQ8rvoiPOf3j$?c z9bq5@z#UMI(9;cqng1SMHcv|9HOeqvL$`u_qx4SaPRjtJ6*=d}Ebs$UQrduY#JR@+ P0000Px(zez+vRCr$Po7K@=|2DYvNUayxbUUAKFy&V9o=v_-jN!{Pk@&H6cD+qXemj)mr{-0S1t` zyZ@hA00g~>1Y};jLKJwa0A<%*G6JA#z7-#J1r{hMJTj1>F{}{iJ>KW+y4wI?K@n~J zGNOzWCOhOuA>9^$o+wDD_Bp%pyJ`VxtJj_us2OvYivS3K_Jm!h=W3V&pLsvx%KVQafK@M`xz_W{Hc)mI9=q5U za(WGbiaDmDMUIG`w#YmbiC7`fp1jZ5b*BPgi7!vFtk!#u*5-JfHFDAl0Mgq?{7f-P zll~c&RSagP0QMAHQ4>YADG`7Xp9LT)6k`K&76VoW0J4>g*u}zgI{=L1FUPVQWg+m~ zq_OWr5M%)$*I|9rrcUNiZ3)1YvwIu#h~7Fm1AyMCV^5-1<9H9pF#yPo-t8!K^0h*8 z1OWN5i?QK206b1X75m7$wgupGhcdcx?esDEzx{IN&2v$^qo)$B*GnVOwWRO7zG2t> z{BPA`0L&JjbqW$%d+r6mvS&H~)-^0sky@MYIc$dby$IwzNgf!Y05EG>uP=IUrz`%$-(3IIc>Cj#6hzHIBX$iwU>GR7ML zv`)smw)GVh0Z&B$#vl&>7wyb@N5=F{Nk+e)RRb`vu3>-3TF*&rYQP<|iTOFP$7V8ax^R?mUjA#o@W-RM0RyZaiu(LVDhZ2pfiNasxQ#~kbhQv{tG~cfk-e91fH?1 z?IR+N%w_F+V%+UN3zwVn2z!Uqy=|Gt-X{bcU2{4BvQ0wW2Z}0D2go|(TduLa{&)-k x2|~~CyTtFUg@Eunc9-#w1F(yuzSYjZsgu!w5AD;=00000NkvXXu0mjf003m!P3Qms literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_3d_printing_128x64/frame_5.png b/assets/dolphin/external/L1_3d_printing_128x64/frame_5.png new file mode 100644 index 0000000000000000000000000000000000000000..544848d81ee566fd5006a15327b63ae0fbffe31f GIT binary patch literal 1269 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 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_3d_printing_128x64/frame_8.png b/assets/dolphin/external/L1_3d_printing_128x64/frame_8.png new file mode 100644 index 0000000000000000000000000000000000000000..0e018b6df2060d0ee0ac5e3278d0eb96490b3491 GIT binary patch literal 1284 zcmV+f1^fDmP)Px(yh%hsRCr$Pob7VsAPhw}@Bh%*NoMG<6m*Zohw+;J>ll#GJxIby`+C3M??30~ zu@QJ303NITcjE_uXX}3ec)0!tfCIp30w2x5=>Z-s|7ijrF8=}G0C3&|Jez^j13X** za}9jB{s({qzI#`Wz0prYsF4h(G=ZUaCP)DrVEruLl7W#{Gp z8+8i+TEf=ku*Q1U`TwM@?`V4fBBC7#IKEp05D`CZeHRF<(Grss@zW-7*8HZax=Z-3lyFRC*+ksWDUtwT|~WwR;-? zEGVK^zY$qR2}6ZY>v*42yW0U!6GaJ?Kc^dC)eCt2@e17A zphAqWm9IhVZVy21bk}--QhfTm$}YXaP$7Ell=G?GtpKpnzjdAZ^Z-=wGW8z-RHkGx4V?7;o;VK&^3B=}7L) z3;{ifj!>bY)b1r2pe0I62qJDi*DE@O3`M>eM1_c~ZOx%}mjj@hx!R_PptYNUC#bU^ zph9?jC1a`Gr2r@)tr{P_=8#~0#vA}>q=Ms=_@8BG)?(%MNC3!Qdovc<4~glL3_t@y z0Eh}jd?5Bo-OU59QZez%y8c?%+bsa_Y>)~-A;@j3wdtA)fL1fA?5h)9D}nTrP;=bu zzYUxLK&sg_!abtrK^+Z1x$ltxkUdiS(@VfC0FrogqrIt;oX{L1&%Vjpytg#>li>0@a(DRIibqTQASZP2=;oRhI#f-ros?FT0h9x+ zj#`xfvTo0qS{+oWv)5h(01H^OFv3JP%&00V1cX*07bQ)RRG|-?ed35E1}a z37_AtR6KulyBYu*>=hZHgn{~2W{?E@Y7n*spy!@yWtg5*?wRWap$3G<-srKY1dK3B z?!b{?UUB+crNLW^c7I95dEJR%>G(<8Swn~ u9Zr4>&GJS7cXatfcPx(t4TybRCr$Po9%MkFbGAn@Bh%7@y-03e3IM>7yJz@z1lA@Jey2LJ&;d&Tr;k32LKg4wF?;PFx&=!Q&3CH!>06{yvxqZ_gUQn zfSRx+IV@YxI^QRCc}Lp=&>~tu!13K0fEMx3t?vSXHM4M;+AY=mvn>EU;(vbBsWJ}` zK-$y&|C<3oV>Xq5WZ506z*Pe%^}A&SP}6)WzPc4yps4alAX7F}2(^y)IrV!R04yk4 zuYMz{j1q5Xz4uH}Zz7qgX&nNGU5Q7Ra z!dJQm^}9U)rPE!S0dn!_-&J+#6^07Y>nDGo`rQfutNdHnsbdD9g6E|_09Y{!1H5a* zrlq>J3<1FEl`~U7Pa;T>%Y^#frUDQEYSVrR0Gh3+05kEYDi~kxs6aJ4t96{=%}fD3 ziH=kuQ|kAU3Q!ZJCIk^Teb*~Gg$!jrGonJYtZltR{VoSU4RfVU5kX5g15Z+CK|qD@ z#7b+Jn*RRKmOq*W*q!w3NL z#9OzU9Lf>?)9)Mvvgs{=B;GrXQwUPuD!Ft`PbIm+e-40x;FbWKJ-f6yPtsE{M*xtX z_8ffr-_{CL1z-sP3xeAM;N55~akN*G#HSE>Dj!+9*QR>Kp8eawosjpQL z&%SBAJ$6+9j^Vp!;q=|y2}#2F8CUhHQ88NWqt}O30k&-cR|2qF{E_9p1WR5~=sZI0 zJb=KUEV-_f%BfWWR)}BnKeNqPIZ%FQv?EegB>-v3RG@fTK+pf+StmiY z9VLY4l;yT90JK&mb;9Yo2mq24PX(AIem;~MNW78n5UW=VNnJ%oAp zFd75}fSOOd@mpZfy~zqdMIB-+cbFiw^=l;{%mAWSWLI2GF)P$fgdT$JQ z(pQ79EdVs(wAxU+nt+}Y?uDRds;B$%+MyQUUDR_6PyqBK*@|1OOF>ZUs2I-YDi0$x zpfek80V8e!WaCr}Pm-u`#5Y|lA1FQX&ay=nzRC_iPvvLV=YIf<2v8;11A)hu#@-`h&$~3fE9|EKRB&lnJpN6=a-Ljb=Sdv> zLY7bSqvPEkWuH1b1 c{`vU@je~=L>E+pV00000NkvXXu0mjf0EU!KmjD0& literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_3d_printing_128x64/meta.txt b/assets/dolphin/external/L1_3d_printing_128x64/meta.txt new file mode 100644 index 0000000000..2fa7c72d27 --- /dev/null +++ b/assets/dolphin/external/L1_3d_printing_128x64/meta.txt @@ -0,0 +1,14 @@ +Filetype: Flipper Animation +Version: 1 + +Width: 128 +Height: 64 +Passive frames: 46 +Active frames: 0 +Frames order: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 +Active cycles: 0 +Frame rate: 2 +Duration: 360 +Active cooldown: 0 + +Bubble slots: 0 diff --git a/assets/dolphin/external/L1_Wardriving_128x64/frame_0.png b/assets/dolphin/external/L1_Wardriving_128x64/frame_0.png new file mode 100644 index 0000000000000000000000000000000000000000..db2fc0b46cfd5d94d463d6917061cae41ac2d162 GIT binary patch literal 1536 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 To!}pZ00000NkvXXu0mjf)g#sv literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Wardriving_128x64/frame_2.png b/assets/dolphin/external/L1_Wardriving_128x64/frame_2.png new file mode 100644 index 0000000000000000000000000000000000000000..8a33d5543a3daa50a7c85ba7cf1221571d4edd45 GIT binary patch literal 1575 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 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Wardriving_128x64/frame_4.png b/assets/dolphin/external/L1_Wardriving_128x64/frame_4.png new file mode 100644 index 0000000000000000000000000000000000000000..15678ed0754441ba6fce84c077c64542c5449dc4 GIT binary patch literal 1560 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 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Wardriving_128x64/frame_7.png b/assets/dolphin/external/L1_Wardriving_128x64/frame_7.png new file mode 100644 index 0000000000000000000000000000000000000000..6431f94353bc4c836b658d1cacea05febab2c1bf GIT binary patch literal 1539 zcmV+e2K@PnP)Px)yGcYrRCr$PT)?w3_r`w_;O6)*0xSZoBJkE6ST(?{;a^4I&Ea1JSOi%20Qcs=ssS#B z|F&(f9cOEIZ`>ll#R%}az1y}l0s1&io*@E!eSJaqJsuBevkY<`IGg|s@cKxL2sw}= zYfS)#5K~fs1$+dowqGR3lYjvm5g@7?=oBylrX0ISkRw3{ua9cQA47_6kBUZ$00_WL zN>~KQ5x}#SB=8gffls{-_P|G^@HSo@H^*(iYnD*j$#yYR01K@CyBU!@bFSY7Eq5BX zup_~T3LpX66RbVw=3IFVE2|#R_X3RU%hS3FFjDko-MI*m_1I5)j~r-biU2d@9&MyO zZ$$XiTFic~WHXfjM;2;}1bNz3!FOwbBMGpSAV&ZVejA;6QDmloGvwr@1egfA;L8?( zPyvq2$BO_l0W7em5a4L}m&aZtut_kZ|9@W<>o0<&)6W3F8TNq1_;VH>_{~o}_Yc(o zqXfvlF+{GrI|1~azg6Tl8)snuT3 z(Mz9Bio*$D$$xjsNJnvZ0(iul-UeCMtd5%j((AJ1zq4n}hvq>=+-_~4SFEAR{*weidqK{vJl~4| zmI6fg`tq4Yk|70PE`Tg)_f*1Cjcg>dU}xvekYF$Hqssn$Nn zV73)xi0I8fQfOLp%HBFdnyl@tb;<&d*%HRQDZe-xPLJTuptN`MGVivY8np8=n(5i)Z|fUxyIRG_uJShsmI zlwcYGB$yTvswp4?glFp&qP;$0RSe~PotHCt8jZE)nxO@8k8~n*8Xw z7c9~JL0E(I?*W)jAd+9bhzB@|AgLA=(jbyV(jS402q5=bRRE6+NFilc(QR4vcz+Yf z5WtFTl&dR%WPsPXzoG&NTq((j!0o)*8bC{TMFl>h0FnR**b0a!1zBXX0MC*D0hZ0l zUXf=-C+a%rNLIIo^ten4$X4162^gD50%s7w0vLf9?J3jJSp@jF0(2*`U}PgKl1-!} zD8+akMs|-Z37CAGwHZkPTKe_cNC{pwz}^JNIs`?$E)*>iRS0k-3Hq6{(LYRpZQGui zmpa)rFbn*stBBgxio#emfz01tOE>a+J%9TdMXy!;rxidGfN@wgx+-Kq-AS~Uk$s}b zs+^@V#v?&?f9^c;-vzJ^W9=#j?qjUF!TJ&jeWu0`TJE#0%S7JD*(zh@p0eOu-N!rS zoQVKTkFCL~5CE-H0qCk@zFh>d(K1T`vOviFF?;ZAn6Xy{G_&vKheHVv$-O*C|I&DG z0yJsu^2!85hDecYS5gkTwq}aKHvuvS*|Q7Kir@jx?l+?V+M&@{6lIz`=&Bh#m+PT6 zQ4&qp4Vq{#0>~}^9ZW_n6Cvz;jGerzVAo~|(4DF~4m}?IZ21SZl<#5;{0Gopi>b*W zf7V|e>(=sH3SdAungDJ6QSdDV)8z8VhU^eYnRgBv2XwcBj2kVBw?iFTlT+}07xyE z0*^^GXdRCzOworTe`*(i7%iVy51m=VvdyfEY|-=4yc^|BMYpt=k~x~?M}npgdv<$00000NkvXXu0mjf0091cyXgP` literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Wardriving_128x64/meta.txt b/assets/dolphin/external/L1_Wardriving_128x64/meta.txt new file mode 100644 index 0000000000..5a492afc63 --- /dev/null +++ b/assets/dolphin/external/L1_Wardriving_128x64/meta.txt @@ -0,0 +1,23 @@ +Filetype: Flipper Animation +Version: 1 + +Width: 128 +Height: 64 +Passive frames: 9 +Active frames: 0 +Frames order: 0 1 2 3 4 5 6 7 7 +Active cycles: 0 +Frame rate: 2 +Duration: 360 +Active cooldown: 0 + +Bubble slots: 1 + +Slot: 0 +X: 55 +Y: 14 +Text: Pwned! +AlignH: Left +AlignV: Center +StartFrame: 7 +EndFrame: 8 diff --git a/assets/dolphin/external/manifest.txt b/assets/dolphin/external/manifest.txt index 50689ded19..d5e24b143c 100644 --- a/assets/dolphin/external/manifest.txt +++ b/assets/dolphin/external/manifest.txt @@ -189,3 +189,17 @@ Max butthurt: 14 Min level: 16 Max level: 30 Weight: 5 + +Name: L1_3d_printing_128x64 +Min butthurt: 0 +Max butthurt: 14 +Min level: 6 +Max level: 30 +Weight: 6 + +Name: L1_Wardriving_128x64 +Min butthurt: 0 +Max butthurt: 14 +Min level: 11 +Max level: 30 +Weight: 6 diff --git a/assets/dolphin/internal/L1_NoSd_128x49/frame_0.png b/assets/dolphin/internal/L1_NoSd_128x49/frame_0.png index 9a48d15f7a61949a492799c5098957738b9fe837..c804c6a152883af706448ceccc36aa9678a97525 100644 GIT binary patch literal 4374 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 CGO#uP delta 782 zcmbQH)XY6WB9Midfq~&c+a3)d#aJBV?!>U}oXkrghb7(7*O7r?V?XzwL{=c5Ex;$l z_5c6>KxXKn%XL7Cv%n*=n1O-s5C}7hYIrp;Ffh-bXywnu%Qdmf#@t-bP&e7Yz(B#= zQZF?n(J09{EiqX~!N|bKP~Q-U%ykXTt&EJV3@sI)K*^4mOJU-7CsrV(pfK5zal&K{ zCe_JLm?9?sVd9uv!mFU7q)?Gt;OlGUnO9trn3tUD>0+w{6w%AfOtErtGj%jDvoM}q z!z(d)8?zAy$T$#~9Kx!nn_^&UWSN>~q??p#Y_4lyWMZgmX_0EKYnhZ{Y-nbZY;I&~ zqNGq<5|o-|l``3oS+U;4%s9=|*fLeu$kY<3*wVyIH_61pK-bVDF)bxE$=t#yIZ;VL zA8Lz@eo;!Al}l=Ia#3bMNoIZ?SR5i6^+%_13LRG9aQ$iidn&mUGosD5@_BJ_%>vO&8ZpX}G2#;jmW3$l3icRKq z%*yLCHin#%bSV3?qUeGqL%;lFcQ0?L_8Eo?$|gk~I4ipLhnzzcLnYIMX|`-3zq$P5 p?;N{xiE*w!eM}o=7=K$n1RP43b_>X=C zon=d$_{UuQ182}Mr&%!Bh+?8i5Ty%Hzm zJL^kJ>QX#Hh(lArQt>|3;8Pxa1(-+c%r=Q*D=KEM2lr+?VjbZK=F<`obK7Y`q9_V; zb9N{TjOd67(hiKF7@DNf>n3P7&p3FR!?F*KJYrZ~LXPom{-&%)Bzf>2)6{vAOeT}| zq{FTyx=EVjIFe#Wh9Qs!VGO9IkRnuL>ncbV#t#iCq3EWfsaOV95Vd~OgQK8Vvx)2B z@EW>mWQ&H%N2Uawr0o z(eS$y3c4Vn&#y`Saj2RhzXwMrc3F{m8A>b*Ws%^Vl1RuR>m)caMiU$>$}BCr7&q(kt?QPyye|O- zQ%giOtuIH2NRMV}MvtarjKfZ2)q53H){;g|?xN6Dt!D0r3FRP^n-W?aTd6g#tP_V! zIT;Sd7(#>&H$gEhO1~DmQ6Xdp?P4W2<7Dxyy?j#*S80oOP%e5dB%XCSVonDK3C77G zAdYnrB3nxlG%LhpD7tGINx(63g_tN%GDD)9NVQ7Xm0iTxOv8|C9g~sPx(=$S)e@+u zn6T|E>h`XX-y02nFf&qI{ytU|xKcRw&%sGd+S+jL^)&*ICrvK!XwO&=Cm_74xG8XkQ0Qo-j-_v{}1 zX!5YN=DU}{6PNMy&0zE0Q2AbB+O^tdrR zP>3xaKRfYzcsuA0?n}?U`$X|5m{OL_rHV^0+_xDVB!7JDtK=w{zN_JyMQCLwE8C+4;=mv&OgyK diff --git a/assets/dolphin/internal/L1_NoSd_128x49/frame_2.png b/assets/dolphin/internal/L1_NoSd_128x49/frame_2.png deleted file mode 100644 index 5b474fff262306ecccbdc2a0ace273bd26e55219..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1416 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 O|8@a?$Tw2cdEh_m_R@*~ diff --git a/assets/dolphin/internal/L1_NoSd_128x49/frame_5.png b/assets/dolphin/internal/L1_NoSd_128x49/frame_5.png deleted file mode 100644 index d7f8c6402a27c5a2eb565d12b595a3a438ed94ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1412 zcmaJ>eM}o=7{5Y6#+TX5+!*L?H~(N}*Xv!cJ?_>DwAVqE0>LhTBeU1Lw{R)FYwnIw z(71*v(P3uHf;h|sKcbjL5cULiX=$*GC0X_l#L@>h-#L%EmjY+*Ks$^|><` zoJRwKp=tqD?amUSxl1)vy-SUul+8k-m94TOslEEX>_x$=TF;yZntT*U2Q@W{rfbc~ zo5Uem?Gy{b6dnRL2Def)Ouq^+un>}sbkHJ0*=a0eFWphYb=txmm`f}h5=T31VY`h5 zIAv!c5KBAo5M5=(Nm>X?AjDKrqJW`9nwT(9B1NJ*NVQJb^e$vUiVb+e1H{KK5(m=OgYNm#>cY!8O>bKA%qGXf&B1f0zTbUp ztB~hVa^vWndH-Z^dvT2ek>^jwO@V>4!&`z2hYL7k%KxKjrmbiPTIL)~ymq7b;;y7| zw&L4*UlxT@2>>MiwkDa!Npt}*?G%*eCWOC z?n^7NduEN+iG5>DBL@;A&zK)K zy!~bWnZLK?86y+<&$sT8iOY@uEPTTv#?hsm7q1HKb1NNJ3U)Sp(p&c=BAxXjSoO2r V#OII6-|b2L_8?x@D=!CanvlS37m0^u7O6DJd#U+#XghA8u=x zLmoyiU#_vDR-X|Rc$Sv&Ah#^bPL~zZ1S4OR2Bii_Nr4xL6cq3lu>u*;%SZK+k{mup zssZ>Mb zk6g0IGcM9*GLPGnc6xb%DEdeQ@%#M>ze>UJZbV5C1cG7+hQSg77J{ru1z=W4ifAwa zf#y9v(ZjJ&SR>`+N<_U}@^&l?ug_|IqL>xNlO?4M2~a*nsX!60Hyqn2S`cmEUpJnL z7VJSEKx{zZN_bkTi7P2Wmh$&(Lt&ug4Vle*q{2`|CXO!g0#>w`^m6G&!FU)F01ZJi zDgwqR+y$$(Din5NDgfg&p&^t`EpV!w5j{`is|~mcSE4!`H={~rMh2n9v2>%!Y*wo= zL%NoTa4oDLQY;N3wmp*VajsOmXK+a)4=9o2?HpGW>4cmDPUM6F&Ie%{1q!8DDcZw^ zPv(cKG#0c8@Sak@n0d|%ji#COJjDeJCkRT(;4r1cl&~6OG_Vus)G&iF1gZlX7e!0q zkMWuRn?6Kx3JLehe|jf!BsFAMd@>Ev=E*z(RvIE+nwHy%w~xzY6CsPqU=Lh4yJDcZ z6Po&U^y9~;h!=VtW^kh+xhHRS#n8kB1;N^zTOU-}jt^9|rp!z^xht{srQ)bM)0q#e zk7XJYs}HTYad7eWrjx~6qgb}NbTISa$Ok|3gAR9UWnYJ8T_dt?-;{SRM=k&TY~QI` zxM$?Z)bgaRydCqi&?B{1Hx=0P`#ZN>SakCW8$*W%^6qrp&s7bbn}^zx8n#bjV(w+T z=B(O(%O4Y~>WFP?FjcLMHs4CX@7yi9^68}**^hB>>!Z(e5-Y0JulH^F)Y;Qlo&y4!5XJy&P2X^f>R%j~fugQDrUhJKFb&o3!JHFXY<^gwY|2tX4{xoBiF>$rUelbShQFVSm%Y{Es8*(jTEf3&OGcKdF+D%@4+0EB A-~a#s literal 0 HcmV?d00001 diff --git a/assets/packs/Momentum/Icons/Dolphin/DolphinDone_80x58.png b/assets/packs/Momentum/Icons/Dolphin/DolphinDone_80x58.png new file mode 100644 index 0000000000000000000000000000000000000000..81a0e29ba55b02e1281dff350820dcfeb1199727 GIT binary patch literal 2602 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 NhX#eK56@nj@fS5W8v_6U literal 0 HcmV?d00001 diff --git a/assets/packs/Momentum/Icons/Dolphin/DolphinReadingSuccess_59x63.png b/assets/packs/Momentum/Icons/Dolphin/DolphinReadingSuccess_59x63.png new file mode 100644 index 0000000000000000000000000000000000000000..b700ba94da72901250f3c77a62155286b77ef47c GIT binary patch literal 2880 zcmbVO3v3j}8NSj4#XKcQR0KsV2f_rH+nIgu#b|Ns3n|39;JY0bUk$4njL@p#5pS4C>+^&WaGykitSzjwO)D!r7YtCrgy&)CWC z@jcJ3-BUcCJKj&!H9C#)#eQrheVSqE#FtB^Dca+ySeQ#|xP>^3PMQ*_fcNjO{@%+Z zjDWX6jKg?3Oqvr_>oR27x|%w?t_7=xcVQ({k@Hi6Bylt*muyYh{#?MD=k?Qj_ciNf z@(`yb;0?MCna221CTwL0Bl#e}5b}(y`j8}uoVr89CnbnQT zC^aft58rq$umq_-n+~KsTrDB3Y51-+Q8{wZg376aVu}FsDiw|e5$K8{0Ag@NR4}i} zJg#8fLZK8xHTdmZ8bOfo2*?o9lO~un0Z;*ofD8#KNJ4}wm8+CwmdvHgs;tWxh^mPI z(F6nN0xtta7d6$CAkBbLDwiT5gmh#AQzs%&WB~%5U;+e5G9*EkOaxJ>T$%!TPR24& zH9GSYO!K8n5Cg?Th?r261g>N*EJ97xMF@z@DYSjMTm}_tDx9Q9#DKcaml_vVblxyT z8DI%M0f~tab zRE>lsC`S+;Q9lP+<*C* zJF|0}$_DRhm=t^Mc)0iOi(hwty654(*XC*$r|q46VL|yu_MxriYaXzAk9N+iTU2p= z|GAgz=d6FaKhXS2e`?Qup`UB`x_jcB`P(2ppZ&x3N zGq$`*`mV2d=|IbZnB8&e@p228gw=vJZcwyyV9>QO#?0v!RcUG&)&!5dT;ukmHTdxC>93wY`-{%5X-5YZzp#9QuYB~oe;DmKxbsX$yA`YL z;`^p-Z^+(`zMuR&cg49c{MnVZ9Z#=5(b@F;o~kF+-~D(JKUm3ce&P6;>1DkWf3j$6 za8|$U+#{MjAI(qW8}-b&vKIZ7P2COuTt4;EgNHVKb#3rd{L+pkA0K%A$ODhql^-sC j=T9qpo*QhRvB5Lh^Ml7O9DlUO-Q!kAYa)9?t2X}|U}xX< literal 0 HcmV?d00001 diff --git a/assets/packs/Momentum/Icons/Dolphin/DolphinSaved_92x58.png b/assets/packs/Momentum/Icons/Dolphin/DolphinSaved_92x58.png new file mode 100644 index 0000000000000000000000000000000000000000..4b3aedaa991717bf81b3b18812af95a6d456b83d GIT binary patch literal 2336 zcmcImeQXnD9PUg8V|)pWLjzT4ihZtK>$&e&15MK%H9KHh83wRh$2O1mj4 zOCU}JOeCP-`0xWi{vc+eqL3JYL^4JWs%SRi7I)AzczAdX2iHg~=EuFw0t z&+qqpe)qOBR9`!>u(;6Sa7?TV_#5DL89e!dyWn>>@@@cL#>E3Iro%CQntkRuw!e0d z!!edto0_fW;G?{x$DD$!i@=$R#Ua|^D62@t1*sKShzP=}=0z_b{0v1@*^4e=gG4Z1 z4I*lw-2jd4^-WTHtHjA@MLANI;vqo{SOSuYMKzO8dC@d456AXn97WO)tJRD8>;;kL zU0(&Qy#S#Iioi*Duo&s)84piWNalmW)eKqT8~hJv)`E9l zG-6qC9>Iut> zLZ!nY#Ns(x&CH|;CJavrah!A#c1T%ZFgQ9i7R#Yct7bJ^C8KvlVza3&4)6wG>Ip*v zHLHPU&B`t&OS!r6gb__gDoZ$sf*6FEP#Bq8Djv}--HhnBi6i%OE}A%BZ2-a2jV4`> zW(yU{vLF=YB$3i3swV46bDpgzGXeMo3wTjjH3lQ#;cg;Xp5l0ddx)TTf*6Jdp$}xi z68;W$@f3tHe*x>Vs@bReYQoaBW%{ELtjdg{Dv~xGFG#l6 zy{Kdt8OUhnu{uKb?o8(}Z88EN=_th?hMBrzB?SZc!Z6eSXfFJA;!R=o=+Z?7)>`5u zOcYoGlihaVC62({3a1c)NRy(6%ZC2HNYA8IiU?X5z!xWu{)b@h$i=s-!!3RFZ|X2| zFu47`9CjFftNCu&G}{0^*qXLA%}hfZmRJln9)*!412942Sd8Vmd|{`M^1R@6zlcn^?ojVcXt>0&Wpi=zaCi5pD6avFWI?vY7r-$Y`s=eRb2A= z-o~vJv-9&;oW7Fip_;dL4DwT2N{o@!kzWYomuQIE=#k{0N?4qpsfW1Bvg=m{?Ms9snltREnd?28xZuG*duc3${yU|M4 zOL(KRK~O1LsDpbJmXu2iLlP&WxigU&^*j_10j7x5N5ZPX*Spb}Tpo_C*EovAAZEyo z=2;4nDsL$=ThjsJuoIX>5H#ZC?4*NXDd%*AA}9hU;lX00lV@B!O(BUN3bpC7pDz;% z6T0A&8x5Lfl*jS9x;lFuZP)YwPI4TF6BJHS7(`&k0@W1jG1bUUN)Uh{=}Od8G!?NV ziaxEzbfeJHxC@bJN~~%m!UO|`*Nah{v=f#~F`z7^aM2n)98)e!I0%CXP)!43$rKhw z$?HwQhZGx$q|k<0PzxEc;}C#X9JiYJIT)C-G97ESU0z!br=Bu{bw1J-24zu=lsS>pYg9uZ-Pm53Ms zcvKCR}Qm4HWZqtH~lqR6~VIXNfE5g5$@C*~p<7beQA3tCP1IEtnHv`b9R z7c{BHO20Kf-U+fMK^dvI@tnj^G6@KbbT~*%1`LI9Br9Q1gGiIKJ}uL>mKk>ViZ=8SQYZ3WHpK#m8>KK8BM%ahKSSkSrivr7X(n~Rgxcv8Jgd$ z6LpXmfHA#hapA)mZ-}*5H=UC?7B~qSBP76JE{4zE?^1c#+c z0Wme?O5qQyM+UkwC~SkZq)b45L4DUul0-|a!{KK!j^;Rwbutc^B-oLR&m}pWz~=x2 z`)f(kHJ!OWQW>UMen@`5T*cwmiWsK>{D%w?e-`3pQD_xfY^C9c;)3gKD7f_SkRel? z`Qz9vu3!7PENQhssyp~3+2+Azz#>N@!7BM;AjK~;hvs(CMbMN}a$=kaY276;e$3|} zB+TU}N#G|~2SbrFVj&JIP*!Xd7mHtAe+)Tj^A&=;An|7eF z@sQ0nV!lVnD-WJJzy8hDJ;)g0()XK7Iy|$sRop$j{YV?mPCe^YX}0@gG{w%twF!O?m~^+&6lAS`YTnjzt|0uFc-@(p~E|g@0Vd8Fsz6{|n+pf?{YdOCeqqkg}jS9NAxuK3sH_ILKRHOQT> z>?qG}tKXPj&8BUe+M-5Y%Q_Gl-R${lpSx#zMrjsx^7YKkon>P`ZvSm_aAdf$Z}O{Y zB-%A`O?BqQi_>#kGuE$}bKB8rd_^|eTy=ER)Pl){;aqY7zH;lb@KbGNSgP(X{{ocr9GGc6~_oGCeRsqMtMzSB*M2Htw^?SnULnD)YC z^!*9*eZYBOgC@*!pMTDJT&- I@*jNszte(n&j0`b literal 0 HcmV?d00001 diff --git a/assets/packs/Momentum/Icons/Dolphin/DolphinWait_61x59.png b/assets/packs/Momentum/Icons/Dolphin/DolphinWait_61x59.png new file mode 100644 index 0000000000000000000000000000000000000000..c4ca5a174eda02a9e50aaeec9285d2988b8d182d GIT binary patch literal 3671 zcmd5<4R93I9Y3f}3Bq7uY#org-Oxdxd%JIMZ+CCWfk`eQha6xCNCuetZujlomb=}} z?&fl#g40M1vNrjiqf{-^AWUVbrG*e@Y*OfyfmYP2Op?C6kLAV* zBr~Nm-t66e{r=y-_y51!x_s%P$rGnf^!a>~qjiybd=BI9{0Zan@1n*#_Ty8TS=VU$ ze78+=zvFxzd+)>-Lt4XXXLW3eBpYczRE#+CXVWG|`+OA(vnG_+BZr8igrAa7VNhZJ(*0-_=-VFhXAKq8w?>9&*&lR3Q-zIGqe zB$0zS>%(NFYmitSTTWCP79w~*L&*#iAcT+~@LZ4;en7Ac%g_KHK?(>GCrSa9D11n4 z%~DjUK2lq-h3~>-(s4|QrdwKC{4D{$VI^n~3WaEfrCFB32+D5N9hjwbyS%6&f^6B+ zOh+?x!qo`lMza$pv8VYE(x#_Yw+m^)38S;nq=BDtL&^aa*~6L5Rw`#)k!h4dX{0+g z#sUwEvlNSY@DbIf(;nJ(YBJcPLTn?V?S@tp(e=nSnk^aCWU$5ZqGPsGkFKR=4B{B> z37bj7G3=yqog{d#y;PV|wS}N#SPh1eDpq-UQ47HWKTF*6fTk-(i#^A6u^@mV&_Q7m zmmE+Gi;o7Dm6)Iez&yZ643me(Fo_iCKo87O3_gSghRN}RpT}USBG^zgwRKdeB1@`a zr6EqSmWByLn|dN=Ef$lay6r$+M$t%^#IE`^O_7)Y5`$3SC;=eXSB{}z2!<#n1k{iK zAPNDYC?7H8X1DyVe10V;hKzN1d6NVdDTrkt#l%sN5)>{(K>-9Pzy|=Q00y7LqPAt0 zh8H7Dm1=dpQm{sZL<|*p3Nflk0gj7PaX!dW0wW;A3qceYWRkdMx7tVm{6JIEnE%J2uyG8A=yWUPw+tJ%Mn0rpfCD zBO7`KStYeFp#^bQMwT5WRm(^d9{mZ&AfRccG#R>8q%*ow=zJc5u5U*{mR6FsQY)Sd z#7(p}YLS4*B2{E$TqKsG;)r7@sBi&_Q{1ZWw;r~=5+TGve zdo}&lHlDwa*Jys6ljf8~foi_oCvudmh(v>&<*O}+`L$7N|#nD-b zhq^n#>_Y3Wf(dU3EEsuGLy-xvf-F!W+%hOp=2SdkaaR#l0FcTAd5#4YxfG|gYr2Xt z!=Q}f0K~Dhs^B5PVL>4-QmPsk0FVKPc)mnBrzh_eePi!)$dXMgHl2HfcgF6ewE^9Oytal_F_Wka&>L`T$mmzY%3Jbd5m!_S?0aIo^juEy7oY?#nE z?d5&F_i#I2X*=-Oo@Gr_e!uL=Z`MrD#7~{x+w}?f4ZY`=KiSC>C%;qn(%jFDT`z2! z+C3}TGc|kZw(vklMY3!1qlcTe-QHc1tbMZomMPtnt{gl!VZSxCyN#cD*X`Y@3tNU? zJ2_wf;M3`q?XQx@PweUBzSvgVvA18pt7>Q23*+uEJKsO?-Btg7=#}^F55d9SL2WT? zls@?R+9TAQ`8%(k+O+X+7bd;8;K<}ZzIO3m``5F&uMF&}=X;MI{QDow&bF1e{&?>8 zrn~uFQ!bTphkjMp@vmo;YCd8}hFVcFrEE>1AdL<5%q1GY3AmYX8Ycht7ZY zqwMqQ(6jfS-P60~>|o|npZ3N(=METeFWx-kXMJX1?&bHM_(pGP-SI@GNq_d~_Q7de zXS`0_`Nm&{qMaZ2-1-aWO8+M>PM^2^)n^`C`_P%wd;cWvJkvgK>C~CPJO6mt*!ab~ z-j6pvazAnO&{J1uc8Ps=a}BGGzIk=xyN{r2idJ&DCnmkodK z=urC>bK&O;BjvtT$Lcp98-BRzv%dCmE6cZvovn>j;FfilPY(R{D#G&>~eESa* X`=+k#FNfcE|N2F1mPTH#da&)kn@##p literal 0 HcmV?d00001 diff --git a/assets/packs/Momentum/Icons/Dolphin/WarningDolphinFlip_45x42.png b/assets/packs/Momentum/Icons/Dolphin/WarningDolphinFlip_45x42.png new file mode 100644 index 0000000000000000000000000000000000000000..5595b606c797b2ff9d871d1fc6bf1a9d4eb6374d GIT binary patch literal 2108 zcmcIlYitx%7~LY#QbU6QA_9q%Awn&ko%b^=xZT;(MYdGfNJ)I%z4z{R(%qTq%#_`d z5QS=tip7|Q5Q6#xQRD|2Bu0XP2u4f{KEM~o#2BK9XdwY(f+%=rU!Z~7KW=7r?#z7m zeCIpgnR}nM0a=#uD2WXZC!3*Z0_s4UJ~c8orM=m_+Sg?Jg9HT3^1{sHsS(>VZ1OqWT9wwyv z(!Er|@i3)CIRnta*g20dn#vir=P-bj@K^?)wKJ+X%upFBT5kb}&3Su;2 zS}mdIHehYPCKC~iEd!V3qg+^0Jusb~IJ8)7MEgNY=~24j3%U_mW2$Q&2l2-5rYLa$ArdWvOpu`G=&F}Ea^za6<%b% z>A;~-iEw_^w#b15qoJmuqFheR0foVez%z;>iHxplDubY|N?6tb6k#20m1mJJ2NbSH z4Ot)y5ZXQu?7#;Mg@zin9cw&^m>ejVUsbUYmcoPs5d#DRoGHL zCZH2EzZ&K{xu66*9M2P_|I=LT&B*&;{lwHK*ZBXDdQDc?59~Z93nxp5#jxQLQ*8?S z?|xnDcil5_;^wfMeocl>kZ%aG^u`wDvzn%fqQ)RGt1==-td*5zg+V!-Qy|Jp8i%Va z3f48Ox-q$AJkh1gy26P(&nFXcRhIRnpvcKClPu2q&1Wi#qRD2$R3+7{%{+s@)fg{*xPR-JW!jd$tuL(@edx2nMHjc> zkqh6g7|l$1@5IBSopaCb{x#8&+|jyO&;4~v(}`7h{u^TlX1$Y|e|#|b^!~Qzj-LM6 z+jDXR{kHJ7wwYfZe{#kn_oW{_zw0gklX>Ub$M#+Q=HpG5ceUR=GWG2XU$p-;YxEF1 zRj{}8Ha(5TzSg&iy9>qNy~2xohg;{(Ik!e@v7i63`OH_-xjRqKUU0?Lp1rhgqu+6A zbm0B%<-={!{@gLre4!?k2H@`o!Fx=l$$sTiG=VP1y1{OEHI{*Lx literal 0 HcmV?d00001 diff --git a/assets/packs/Momentum/Icons/Dolphin/WarningDolphin_45x42.png b/assets/packs/Momentum/Icons/Dolphin/WarningDolphin_45x42.png new file mode 100644 index 0000000000000000000000000000000000000000..b254b6bca2550a948c98692c328b7ff9b0b2acb9 GIT binary patch literal 2113 zcmcIlYitx%7@hJ%9;LNWqLmOc8PxFT>^yefu;6xgOEq9_K1)C7$|3DT-0nm|naBP2#?G(;grON>N~-q{yup!ScOnVmZ` z=brC;=lkZ~o!wn)n&vH>M^RK$x-;2Bt}Hp1&6!31-`t!UBA2;t=LVmm8gCEJ25QgV zR*JgioYmVG^kp72pi_td3_u+*t5?x0O9mk*-~iC2Lf-a`Qj96{8e|+k<`}vR2?kuKpMj%|ltEC)rVrMebru ze-O9^#}$jkNHH35yc{R!y3X+;CyFdVu>P_g9T1Iw}LkP(>9P!MB? z(n<;iw@z#ORhx)mTnV_G5aGj=YQRipdT60gNBco)6NysQdqZNsci6>T5B8lQ58~7& zYzK>L!Q6f)aQuGfCfe$+|Dh1UdO&x`%a`pzki&UgAlyE|3T1t+k?=4G9Iw}L^0h*B z*CNuQ7!l|t>n$5OMSp3is5${BL4acnshY&{1eJRG0Ij!SfYphV%gf0hk%s zK~{EnCeTdANZWn@Y>3mz7(+xwEDIS(Q)MLUkd+ayu~{e~R)5NiSnv!;R}D?+TPFpCsZRmxcswud~6d^sS0 zJ!_~65k^@BA^>8P6#(f;F)N9zt_eIV%281lO@Jhc@LY?VaPp3qaS(Ag%FqZJS;s2? z(mz%KE|nD1nRBJglLN1Xo8=HhN9-Upkemp-$?|BuqEw!(g3=P+3~IVMbo3zwKm zQMiBC>sq~QJrgJ099Gk>+RzSibwgI&xJ3n1)3j(*V^P#p*(fhT)|6$1MOmCxATlM5 z#}yTM>l#vN-EEpQ$K{CMOd~RZ^4XnH_Sx zUEw8J5jtcdR6|ob z8Sm|zxcu_@<_UU!;_6@fzrOo$`t`Q1OA9|e_{*1#+Y*xpcl>(pwRcA+x9H~%J!Z$J z{Jpb}oIN$`i$z!32c$)#A6__ie%G^Y2c*J+%!>yOt@`-qdoHw2eg4tbd(FAetij8* zM_Y{3e=fRf`}knroHLI+tXr|G{uq6yr=j`E6FbCL zS2vHWkk`C7*;<(3^d}qoP6-R2=QiK*RM6NwcKPDbsn52Y9sEjLvPbtR9sZ`}4b#9h4{ literal 0 HcmV?d00001 diff --git a/assets/packs/Momentum/Icons/NFC/NFC_dolphin_emulation_51x64.png b/assets/packs/Momentum/Icons/NFC/NFC_dolphin_emulation_51x64.png index c8d8ff8ef41f1657a925c174525b50b86fbf3cfb..853e95202aa792748cde67d9ada9adce2b02f9a9 100644 GIT binary patch delta 800 zcmZ24{6=7c1ruA^Myq%xF7prrQ!4{QD^v5yixfhrek-5K_l$8uEO)Qg=&2^K_ z&C_(#OiWC5lT0m*bj?!J%q%PuO^p)MCOa@|tE8nSnV1?F80n^%7@O!?n3|aBCK@JL z>L!_+C7CBDrKK7eT2Ag~R#7oYGcz(ZHZaz;NJ~xBO))n~)J;rFP0=+rPct!0HL$R> zG`5_4owQl^qf*u}<^G4$C?p3W*Zxr!}OB?ag@!(nfT-=LAF7idO zUHBsGwal7h=FYn;%IS}k)>{=jFS{TR@%^X3%~P9YBUEmFSLQ2kdgC|QMki@v%0u&f z#d_^lsp(%-^P_Ua+%}|ao}VEc{Cdw1C%f6xoy57H`AEMzd+mez=g9cJrzfNMRgEJv zTu)b>sA+rhusEf1)3upr%!+Sj`b{v)-xZ}ZPsc{6ryw@PYH2Qe$oU`le(~r?9^pGx zz!q}yfc=--JqDf7q}y4ExMK+UZYOdf-rP^5$1l&twUoc{HhU&8m_$e~#X_UK^sdVa|HJW!KZL zF1z#W?CFxHn?((!z4v-%Nd9Je*JJ+i?&le2xwc2traZD=P_DrqV6)Y2@^RIpPY*o% rp?Z6s)?E$Bo0~PHH+}EbP0l+XkK3?V!^ delta 983 zcma)2eN0tl9L9SuA38A21=42ez1J+|mGgd_^S#D&t&3xXv$jP9HDW)2b8RLMd1V(v`0f}H)kwm5$5SzQLPA8K@QCAEB zNHUUuC=(s128DpDgzH>kGDO6@Y~B98XKcN&*J{bmO1PPo2#_|8%Oe@$`X;IUl7c!r-k{r(45K znQM0U#)Jl2E9(OvAFNqACLP}sem2AZMPv0)zjxNIU}~+s~eA<76TKRJW1)<6epFQ%zg>OriJ=C|qSi5cb%waR{x_4+> z*OlQ=p?4vV3pE}2>P+v^0aSbK^VGn*4;~izH^k>}K6s#|u>F2g)68y1b)dbcD&L7q z@8SEd4Gotf^Q=`{U)q<{q9?`Iw|kOP(stYp*ECFR;W$0mGqh*lkBJMOO8F#Mc%%Q7 z2m5{Xp0$0}8G#!5{xyA9UTb<@d-2r!D@D^HsV&t3o!$WrTQ_<{S67l(!Jr~QNQZ);e{@gK?_J*jihU)(%?7s?(0*)fsdg>sb1hBxpNI`KL*C_r2ZU z`+nd1e&4>A`ssO7<6=j~>U6reoNTTDUI9ETG12f$r7RD^%RpcD>xxb{Xoz-1>DH_r zq0_yvQF0WiMfO~lmpytQ$WBoo^!Q-3PB&?C&-iKT2XBkFiV*4Q-KvnrBB2Q zL`5$5^P;Un^s32`WWG{am6cNY8Qgjw|AV2x_a^j}`^$Rd5qMlI6Fm@Hfw5!{KObxL zi$InA4p}aX6l!`TBSKMn5=kkPyn-B1CTNVp14Is}q6LLjqcMWUNZLUVEJ3l9X(Bum zL@3k_eINi8^e56PVLw zCNain1`NqlBp`$yUT{1o^W~Zn+W3%d1(}B!y=t;13Vum5bSiBUF_)8ukVe6XnOwBV zXyzG0pvVYXzF&fG4k&vPRbwSUL$8;>< zHLqJxUMsQ)XCJyPJw?6#@eGX*l!_3vkK%{I6xpQ)fM3jTL#6-IT==udE1;rp>isMH z|42P7D_;t{ZV@h=II2ws*Or)2QSg7)>v6p!p2>i2)~cz5Wyl2XUP02IVNu#lQX&H| z28cW+P_z@e-sHr1#+k+&X(vyZnGlOI=`mKsm?onw%}g0HI0!Cy-+eg+*CGn9$2T-Md#*l8IIuj zTWc}~w;?gM*6ywAj*YR;8I~TiB(7!L^&dNLytsIF=gDu+&Obl;#JcYv&R%g~L@{!J zja@2rw51GxdB?k}XXVvzi;rJxT=wbW0}UL$WUJI&{O5l+{*!;Zka#}9-$D*#A1{Lq$n>AGqB%XXN zc6)ZNeg3w_Ydg-?T&W+sp|0f4JKGw!+$Q$A6SkOcFF4e=<Y~KczwJf`lq7ZJP*sh0-~MjjbKJ*6Ge_aNpYQGMNJ<*oq@MZpO7h(gp5Hb4(7mW} z56=Wgzn8G?*cXrHN*6B2Oc;2;$qfjk&q!}bn1^0-bsMv8&K-8qc>Pi}Rp9u<5V!Ga zV*UA_IzKwz)G&7|#}_yzKTPbHHc+~lbo6-swBY!ZBZ=ELbuE2RaJJddx}hoMR=ZL< z>J(_;R&#H3paZH$>{&goFmpq*xh~-#n0T&c)aLDEZr7fe2kO=-$bBj|4daDA>i_@% literal 0 HcmV?d00001 diff --git a/assets/packs/Momentum/Icons/RFID/RFIDDolphinSend_97x61.png b/assets/packs/Momentum/Icons/RFID/RFIDDolphinSend_97x61.png new file mode 100644 index 0000000000000000000000000000000000000000..fd2ead2e3d727a71a29170aa6c77739c28a63f56 GIT binary patch literal 2378 zcmcImdu$VR94{;&<27(R#zZ}?fNbn~k6y36+X`&G;Wkz_Mv;L6UhjTwFKc_XciXxl zNCt*MCQLF70-2b>UPD24)Ra3CsVJi{Z9U@#aS3d$qQ4E){J4m5!LW7F%sKEKcR z^ZR^1_q#4Dp8EQLzNvi;2E%~-JiY|(1-RM!^n}0ExcUg(C5G~5sRl#8WPR&lShFt0 zVCcPGc9m+S&O%O90wzIH+`tqGgdo~r$eI`l31S)05I6A3K|A_;^lKE6B|AFP;v}3Q z2k^;x)nPEbda6sTE)!V^oj3u>ig1u105k!K1pGmji`dalUJf4XmvI#7glJ`UG+Qr- zlsbzMhY|(|V83nge zuGvwjv}+21P&aK*joAbZ!y`fnCryMtr8v;(d^R)?=tiqrZUw9o)B8kX)m0S&cnMII z@~{YUD?m^)#*2k~il(SOG1@`5kwc% z33DI`n(%yJMUp*L&jWQjx%{B22|*F$^L7-9GRd;U+1!*Qxfv026IOuHHk!p)o8-nM zfg~vgSQrm$?$iax^NLumJE4#7imjxGkfYm8&PD={n*}UJnk6d+JT!^1g4KdafR!u& zFbptT<7`D?89q6J|8Z7bl>`~x6zMh#5{p?qq<}FZ&0qq_m@$SX7{Wus@KZF3JgVzZ z{7TrVNYGuVRuVan5)KG3KXO3u0z4G->S>1Ta29#83R_nddp@RvcVb6=8R-liCy091 z?Wm|X8AxdCvit=1o=;~NZKV%D(q}Zk3#KX_tx^bsY%g^BKf{GTO}r{pJX`wnHU58; z9S2|+J_3nz~1lfm^RrmHFVzx(y5-|@_3!!+yNRAV;e2;Oc(COkQ!3~eJRzzQ&! zK*S`9c4Ib?abqIuwu%!mI~-<*nI;K_ zr_GOz?3_uLApF_NJCTnqvvC{X+N~dUzck=U_iG0Pp=DwCQVFEYU1u=#EY9b%U6FG) z)=t^BnCPFo^P%&~fb>zwvIQLpOT8=8zi7LByM4;J_QU5+Ri91z@spz+`_OWNn^ai7 zYj|_*sgjI=B@ag?7hO8P`e^jc8x4YXH_DXG9+pI=I*xxoCIRo){3X+PY{-aftCU-7 z78;}d+Y_!-zLwgw_}(u+_i9*Lm6qb1e*fhBv30crR&JkFI(TmgyESOQGgUMlcwqCj3X6cS_SM$C2e0L&M=rm}XO@v8Cf(`aHN*KiEp4H~6}g@a z&#Cu57#w}0RdcrQ*ibj*Li3LDGiyKe zy|wj~W&XqCTm|&l`7Iqijq}n*HAMp{t@(+c{w3K;+q8w3EDe)>IH~Qc-FW?AnQvs9 zr;cCE7oDXNYEpKruFL$aXomklt@F(C4&>0d>VYS+GI9p)OKYoL{?mp!F01~d-o|4` zcABq}o6zLOT{JoS@ORfHBqrTtw$Gb;yO>{aV`^Rfh4d|(cGdg-7{LYWGB&gi9QJ2Y yL(%#~>9&3RLy5-Zz^w`?d0c9<+tU$UGq29t36l% literal 0 HcmV?d00001 diff --git a/assets/packs/Momentum/Icons/iButton/iButtonDolphinVerySuccess_92x55.png b/assets/packs/Momentum/Icons/iButton/iButtonDolphinVerySuccess_92x55.png new file mode 100644 index 0000000000000000000000000000000000000000..0ae1403da711f2f1b14367828910781505af0eac GIT binary patch literal 3372 zcmcIn4^R}>89$L`6l0>XL1Y5iEG7kUZ};~0ZrRlnfr9{YNP&1@{P%A6-QC8!+w1Of zpil)&EU7|cN}|>LQLIcSsah2sz!_~+Qq8CtG*i;riNsnp$!Ke*G1F!;?c4h&O+}cb zxw+eWyZ62C_kF+j`@Z+?#ftK$rzWK)84QN0CB^ni^t}vy*hvZK`p~idSJBtxU~#Q# zFie@De1wZMsZ?gDIn1CJc|p$4!9zb~Nj4K_UNmq%m$ zF^OYg6K%B(&(i_18b<|IAS)2F8Yx1cC^N=#Mw6AX(Cl1{rf7;Zp|^!Fu{@K*n`tci z!V#Jxy7@|bQ4|Z^*>JC>1$mOJudg@On~k#KAx#{|krYkRG=VG#bxlA64MafAh zRZyg$CdmOz_Xu2aoo2&Pq>&W-!MNLi8l?#lMmB&TX);oJN?}7$h}#A06kixz6iDcU zei+bHWNV7s2EDQ-t6q5&*YThK3jk5;aKth08H?W^hfuY`5K1FT$UUvq>NP<~Rzg*- zQv_HTLSbgavQf24c$d#{Oh@M7$w9tAfk2a$YFYNhR=FaUFGkZw6PD?e0-{{6X6b0r z3D6ESXv5KxGX!NOD5jb+@#xK1vh@KKF?GnIr2wUu!d>K;mpqi8iWX9hmRdgWL`ObpLCihJif)U@h=fI;-GoRnEWx>e z6`{FvXe(eT7fa74^}0fnu*fLlh`c`{0zf<$OL_!rqPSJ?NAU=-u)ujB=!I*;1zmJd z#G{X;d#um6n264=_d?|KK4OfZtFl|G2MWydptT%R?BuAy%YhJ7MuZ;(3PAl5Dyj{4 zE3zMp3lr013Xjy!03kuKz)iTU3{7x?)r^$K znuQ#b#X^Cc7{wzgiWDKun^~T+#)|NpP!y$K@PdE`qTPo47RfNQ*#&9H5-y8NAh>Wc z42^DCmqm1&7#f&eqbKvVj`AHRBaEqlF&16dMO-AKlO{q%8li!aI*W&Zg%$Vz!5dR? z<7=7`_WxIL-`OICJ65FKkYDdak$ZcD$s)Zv!{}#Pl&bG@%Aiz zzWi->0lja86CZITiakQ-EPWV1;GvuN5fKJZr7GwE_-p5uMFvB{#u9s8wfCc&+b?x& zpdPe;_3zws$DV0Bb!W5igYIY3=Tsk_R{n1N)qka(eB!Y4cv^1e(8Ne;RjK=j@vPtH+5GE`k7WEk=lFVK{$SaDOXrq^!c)UtPki`(VP#w5lKlhS52Y_X z$eh^M*fcz^?fvW1jR)J?rtf`xZhHOZyvvC%98LorSwFtH!#U@BXRZ_-_|UoT7nKRh zwk1UuS04%W-0J_sFAFldW>r^ZrcNovS|0zx@zLM*hT1nJRXQ# zTiNFF=0!ihwh5bc{YFK?lHGg!N4EMC;dV#riWy>y$K@F;Za%#&@B99`tv7a0AN=%> zw`bd0A3I;=!*}vMXFo4omN?XQ=W3Cfc^jPnU_k@3r*g+{SN2@H7|8s1?!=NZXVY_2 zGW)p3Yx$Sv{AJa;x(AX5VD1Y-N3!$1^(`0L7tPEJT}z(SQ(LjLC*$T%*DY>L`^n44 z9;qEvv$m#mc0QTs=#~yRmwxb@4K2&gEOb0|t~>dr_qlDpZ0DsNk1lK_Q(r5d9jctx zlCu1bId66iq&23j7+RsM+>`x9)}-xH6i^TCCC9bcw@x%1TdEf>zzU2QX;Z_ndO2cLT5 z^rDo`gFa*JA?M`YnxW782WEF=yw&&Fk;b<#G$o$BHx|K-9}m4c^Jlr! Yi+*+DAAMc=-ycIsVYz)@{;Jmh0DE literal 0 HcmV?d00001 diff --git a/documentation/JavaScript.md b/documentation/JavaScript.md index f6165e6975..3b88e90530 100644 --- a/documentation/JavaScript.md +++ b/documentation/JavaScript.md @@ -2,6 +2,7 @@ # JavaScript scripting API (WIP) ## Note: This documentation is still work in progress! Todo: +- Migrate to new format from OFW (see /documentation/js/ folder) - Add missing parameters & returns (Dialog and lower) ## Description diff --git a/documentation/doxygen/index.dox b/documentation/doxygen/index.dox index 6fb4d5a717..78055caad0 100644 --- a/documentation/doxygen/index.dox +++ b/documentation/doxygen/index.dox @@ -15,6 +15,7 @@ The documentation is divided into several sections, with all of them accessible - @ref dev_tools - Hardware and software tools for all kinds of programming - @ref expansion - Additional modules to expand Flipper's consciousness - @ref misc - Various useful pieces of information +- @ref js - JS-based scripting engine documentation Aside from the manually-written documentation files, there's also a few automatically-generated ones at the bottom of the sidebar: diff --git a/documentation/doxygen/js.dox b/documentation/doxygen/js.dox new file mode 100644 index 0000000000..1c81a08350 --- /dev/null +++ b/documentation/doxygen/js.dox @@ -0,0 +1,18 @@ +/** +@page js JavaScript + +This page contains some information on the Flipper Zero scripting engine, which is based on a modified mJS library + +- [Brief mJS description](https://github.com/cesanta/mjs/blob/master/README.md) +- @subpage js_data_types +- @subpage js_builtin + +JavaScript Modules +JS modules use the Flipper app plugin system. Each module is compiled into a .fal library file and is located on a microSD card. Here is a list of implemented modules: + +- @subpage js_badusb - BadUSB module +- @subpage js_serial - Serial module +- @subpage js_dialog - Dialog module +- @subpage js_notification - Notifications module + +*/ diff --git a/documentation/js/js_badusb.md b/documentation/js/js_badusb.md new file mode 100644 index 0000000000..28372e56a4 --- /dev/null +++ b/documentation/js/js_badusb.md @@ -0,0 +1,144 @@ +# js_badusb {#js_badusb} + +# BadUSB module +```js +let badusb = require("badusb"); +``` +# Methods +## setup +Start USB HID with optional parameters. Should be called before all other methods. + +### Parameters +Configuration object (optional): +- vid, pid (number): VID and PID values, both are mandatory +- mfr_name (string): Manufacturer name (32 ASCII characters max), optional +- prod_name (string): Product name (32 ASCII characters max), optional + +### Examples: +```js +// Start USB HID with default parameters +badusb.setup(); +// Start USB HID with custom vid:pid = AAAA:BBBB, manufacturer and product strings not defined +badusb.setup({ vid: 0xAAAA, pid: 0xBBBB }); +// Start USB HID with custom vid:pid = AAAA:BBBB, manufacturer string = "Flipper Devices", product string = "Flipper Zero" +badusb.setup({ vid: 0xAAAA, pid: 0xBBBB, mfr_name: "Flipper Devices", prod_name: "Flipper Zero" }); +``` + +## isConnected +Returns USB connection state. + +### Example: +```js +if (badusb.isConnected()) { + // Do something +} else { + // Show an error +} +``` + +## press +Press and release a key. + +### Parameters +Key or modifier name, key code. + +See a list of key names below. + +### Examples: +```js +badusb.press("a"); // Press "a" key +badusb.press("A"); // SHIFT + "a" +badusb.press("CTRL", "a"); // CTRL + "a" +badusb.press("CTRL", "SHIFT", "ESC"); // CTRL + SHIFT + ESC combo +badusb.press(98); // Press key with HID code (dec) 98 (Numpad 0 / Insert) +badusb.press(0x47); // Press key with HID code (hex) 0x47 (Scroll lock) +``` + +## hold +Hold a key. Up to 5 keys (excluding modifiers) can be held simultaneously. + +### Parameters +Same as `press` + +### Examples: +```js +badusb.hold("a"); // Press and hold "a" key +badusb.hold("CTRL", "v"); // Press and hold CTRL + "v" combo +``` + +## release +Release a previously hold key. + +### Parameters +Same as `press` + +Release all keys if called without parameters + +### Examples: +```js +badusb.release(); // Release all keys +badusb.release("a"); // Release "a" key +``` + +## print +Print a string. + +### Parameters +- A string to print +- (optional) delay between key presses + +### Examples: +```js +badusb.print("Hello, world!"); // print "Hello, world!" +badusb.print("Hello, world!", 100); // Add 100ms delay between key presses +``` + +## println +Same as `print` but ended with "ENTER" press. + +### Parameters +- A string to print +- (optional) delay between key presses + +### Examples: +```js +badusb.println("Hello, world!"); // print "Hello, world!" and press "ENTER" +``` + +# Key names list + +## Modifier keys + +| Name | +| ------------- | +| CTRL | +| SHIFT | +| ALT | +| GUI | + +## Special keys + +| Name | Notes | +| ------------------ | ---------------- | +| DOWN | Down arrow | +| LEFT | Left arrow | +| RIGHT | Right arrow | +| UP | Up arrow | +| ENTER | | +| DELETE | | +| BACKSPACE | | +| END | | +| HOME | | +| ESC | | +| INSERT | | +| PAGEUP | | +| PAGEDOWN | | +| CAPSLOCK | | +| NUMLOCK | | +| SCROLLLOCK | | +| PRINTSCREEN | | +| PAUSE | Pause/Break key | +| SPACE | | +| TAB | | +| MENU | Context menu key | +| Fx | F1-F24 keys | diff --git a/documentation/js/js_builtin.md b/documentation/js/js_builtin.md new file mode 100644 index 0000000000..3d113807b0 --- /dev/null +++ b/documentation/js/js_builtin.md @@ -0,0 +1,56 @@ +# Built-in methods {#js_builtin} + +## require +Load a module plugin. + +### Parameters +- Module name + +### Examples: +```js +let serial = require("serial"); // Load "serial" module +``` + +## delay +### Parameters +- Delay value in ms + +### Examples: +```js +delay(500); // Delay for 500ms +``` +## print +Print a message on a screen console. + +### Parameters +The following argument types are supported: +- String +- Number +- Bool +- undefined + +### Examples: +```js +print("string1", "string2", 123); +``` + +## console.log +## console.warn +## console.error +## console.debug +Same as `print`, but output to serial console only, with corresponding log level. + +## to_string +Convert a number to string. + +### Examples: +```js +to_string(123) +``` +## to_hex_string +Convert a number to string(hex format). + +### Examples: +```js +to_hex_string(0xFF) +``` diff --git a/documentation/js/js_data_types.md b/documentation/js/js_data_types.md new file mode 100644 index 0000000000..de1c896dce --- /dev/null +++ b/documentation/js/js_data_types.md @@ -0,0 +1,13 @@ +# Data types {#js_data_types} + +Here is a list of common data types used by mJS. +- string - sequence of single byte characters, no UTF8 support +- number +- boolean +- foreign - C function or data pointer +- undefined +- null +- object - a data structure with named fields +- array - special type of object, all items have indexes and equal types +- ArrayBuffer - raw data buffer +- DataView - provides interface for accessing ArrayBuffer contents diff --git a/documentation/js/js_dialog.md b/documentation/js/js_dialog.md new file mode 100644 index 0000000000..5804b075eb --- /dev/null +++ b/documentation/js/js_dialog.md @@ -0,0 +1,49 @@ +# js_dialog {#js_dialog} + +# Dialog module +```js +let dialog = require("dialog"); +``` +# Methods + +## message +Show a simple message dialog with header, text and "OK" button. + +### Parameters +- Dialog header text +- Dialog text + +### Retuns +true if central button was pressed, false if the dialog was closed by back key press + +### Examples: +```js +dialog.message("Dialog demo", "Press OK to start"); +``` + +## custom +More complex dialog with configurable buttons + +### Parameters +Configuration object with the following fileds: +- header: Dialog header text +- text: Dialog text +- button_left: (optional) left button name +- button_right: (optional) right button name +- button_center: (optional) central button name + +### Retuns +Name of pressed button or empty string if the dialog was closed by back key press + +### Examples: +```js +let dialog_params = ({ + header: "Dialog header", + text: "Dialog text", + button_left: "Left", + button_right: "Right", + button_center: "OK" +}); + +dialog.custom(dialog_params); +``` diff --git a/documentation/js/js_notification.md b/documentation/js/js_notification.md new file mode 100644 index 0000000000..100da44146 --- /dev/null +++ b/documentation/js/js_notification.md @@ -0,0 +1,36 @@ +# js_notification {#js_notification} + +# Notification module +```js +let notify = require("notification"); +``` +# Methods + +## success +"Success" flipper notification message + +### Examples: +```js +notify.success(); +``` + +## error +"Error" flipper notification message + +### Examples: +```js +notify.error(); +``` + +## blink +Blink notification LED + +### Parameters +- Blink color (blue/red/green/yellow/cyan/magenta) +- Blink type (short/long) + +### Examples: +```js +notify.blink("red", "short"); // Short blink of red LED +notify.blink("green", "short"); // Long blink of green LED +``` \ No newline at end of file diff --git a/documentation/js/js_serial.md b/documentation/js/js_serial.md new file mode 100644 index 0000000000..cd9993a181 --- /dev/null +++ b/documentation/js/js_serial.md @@ -0,0 +1,107 @@ +# js_serial {#js_serial} + +# Serial module +```js +let serial = require("serial"); +``` +# Methods + +## setup +Configure serial port. Should be called before all other methods. + +### Parameters +- Serial port name (usart, lpuart) +- Baudrate + +### Examples: +```js +// Configure LPUART port with baudrate = 115200 +serial.setup("lpuart", 115200); +``` + +## write +Write data to serial port + +### Parameters +One or more arguments of the following types: +- A string +- Single number, each number is interpreted as a byte +- Array of numbers, each number is interpreted as a byte +- ArrayBuffer or DataView + +### Examples: +```js +serial.write(0x0a); // Write a single byte 0x0A +serial.write("Hello, world!"); // Write a string +serial.write("Hello, world!", [0x0d, 0x0a]); // Write a string followed by two bytes +``` + +## read +Read a fixed number of characters from serial port. + +### Parameters +- Number of bytes to read +- (optional) Timeout value in ms + +### Returns +A sting of received characters or undefined if nothing was received before timeout. + +### Examples: +```js +serial.read(1); // Read a single byte, without timeout +serial.read(10, 5000); // Read 10 bytes, with 5s timeout +``` + +## readln +Read from serial port untill line break character + +### Parameters +(optional) Timeout value in ms + +### Returns +A sting of received characters or undefined if nothing was received before timeout. + +### Examples: +```js +serial.readln(); // Read without timeout +serial.readln(5000); // Read with 5s timeout +``` + +## readBytes +Read from serial port untill line break character + +### Parameters +- Number of bytes to read +- (optional) Timeout value in ms + +### Returns +ArrayBuffer with received data or undefined if nothing was received before timeout. + +### Examples: +```js +serial.readBytes(4); // Read 4 bytes, without timeout + +// Read one byte from receive buffer with zero timeout, returns UNDEFINED if Rx bufer is empty +serial.readBytes(1, 0); +``` + +## expect +Search for a string pattern in received data stream + +### Parameters +- Single argument or array of the following types: + - A string + - Array of numbers, each number is interpreted as a byte +- (optional) Timeout value in ms + +### Returns +Index of matched pattern in input patterns list, undefined if nothing was found. + +### Examples: +```js +// Wait for root shell prompt with 1s timeout, returns 0 if it was received before timeout, undefined if not +serial.expect("# ", 1000); + +// Infinitely wait for one of two strings, should return 0 if the first string got matched, 1 if the second one +serial.expect([": not found", "Usage: "]); +``` \ No newline at end of file diff --git a/furi/core/memmgr.c b/furi/core/memmgr.c index 768adc05df..768d448904 100644 --- a/furi/core/memmgr.c +++ b/furi/core/memmgr.c @@ -4,6 +4,8 @@ #include extern void* pvPortMalloc(size_t xSize); +extern void* pvPortAllocAligned(size_t xSize, size_t xAlignment); +extern void* pvPortRealloc(void* pv, size_t xSize); extern void vPortFree(void* pv); extern size_t xPortGetFreeHeapSize(void); extern size_t xPortGetTotalHeapSize(void); @@ -18,18 +20,7 @@ void free(void* ptr) { } void* realloc(void* ptr, size_t size) { - if(size == 0) { - vPortFree(ptr); - return NULL; - } - - void* p = pvPortMalloc(size); - if(ptr != NULL) { - memcpy(p, ptr, size); - vPortFree(ptr); - } - - return p; + return pvPortRealloc(ptr, size); } void* calloc(size_t count, size_t size) { @@ -47,6 +38,10 @@ char* strdup(const char* s) { return y; } +void* aligned_alloc(size_t alignment, size_t size) { + return pvPortAllocAligned(size, alignment); +} + size_t memmgr_get_free_heap(void) { return xPortGetFreeHeapSize(); } @@ -79,33 +74,17 @@ void* __wrap__realloc_r(struct _reent* r, void* ptr, size_t size) { return realloc(ptr, size); } -void* memmgr_alloc_from_pool(size_t size) { +void* memmgr_aux_pool_alloc(size_t size) { void* p = furi_hal_memory_alloc(size); if(p == NULL) p = malloc(size); return p; } -size_t memmgr_pool_get_free(void) { +size_t memmgr_aux_pool_get_free(void) { return furi_hal_memory_get_free(); } size_t memmgr_pool_get_max_block(void) { return furi_hal_memory_max_pool_block(); -} - -void* aligned_malloc(size_t size, size_t alignment) { - void* p1; // original block - void** p2; // aligned block - int offset = alignment - 1 + sizeof(void*); - if((p1 = (void*)malloc(size + offset)) == NULL) { - return NULL; - } - p2 = (void**)(((size_t)(p1) + offset) & ~(alignment - 1)); - p2[-1] = p1; - return p2; -} - -void aligned_free(void* p) { - free(((void**)p)[-1]); } \ No newline at end of file diff --git a/furi/core/memmgr.h b/furi/core/memmgr.h index bc0c35faa7..796a1f5378 100644 --- a/furi/core/memmgr.h +++ b/furi/core/memmgr.h @@ -36,37 +36,22 @@ size_t memmgr_get_total_heap(void); size_t memmgr_get_minimum_free_heap(void); /** - * An aligned version of malloc, used when you need to get the aligned space on the heap - * Freeing the received address is performed ONLY through the aligned_free function - * @param size - * @param alignment - * @return void* - */ -void* aligned_malloc(size_t size, size_t alignment); - -/** - * Freed space obtained through the aligned_malloc function - * @param p pointer to result of aligned_malloc - */ -void aligned_free(void* p); - -/** - * @brief Allocate memory from separate memory pool. That memory can't be freed. + * @brief Allocate memory from the auxiliary memory pool. That memory can't be freed. * * @param size * @return void* */ -void* memmgr_alloc_from_pool(size_t size); +void* memmgr_aux_pool_alloc(size_t size); /** - * @brief Get free memory pool size + * @brief Get the auxiliary pool free memory size * * @return size_t */ -size_t memmgr_pool_get_free(void); +size_t memmgr_aux_pool_get_free(void); /** - * @brief Get max free block size from memory pool + * @brief Get max free block size from the auxiliary memory pool * * @return size_t */ diff --git a/furi/core/memmgr_heap.c b/furi/core/memmgr_heap.c index 3f62b518c2..3dfc7f5acb 100644 --- a/furi/core/memmgr_heap.c +++ b/furi/core/memmgr_heap.c @@ -1,124 +1,18 @@ -/* - * FreeRTOS Kernel V10.2.1 - * Copyright (C) 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - * http://www.FreeRTOS.org - * http://aws.amazon.com/freertos - * - * 1 tab == 4 spaces! - */ - -/* - * A sample implementation of pvPortMalloc() and vPortFree() that combines - * (coalescences) adjacent memory blocks as they are freed, and in so doing - * limits memory fragmentation. - * - * See heap_1.c, heap_2.c and heap_3.c for alternative implementations, and the - * memory management pages of http://www.FreeRTOS.org for more information. - */ - -#include "memmgr_heap.h" -#include "check.h" -#include -#include -#include -#include -#include - -/* Defining MPU_WRAPPERS_INCLUDED_FROM_API_FILE prevents task.h from redefining -all the API functions to use the MPU wrappers. That should only be done when -task.h is included from an application file. */ -#define MPU_WRAPPERS_INCLUDED_FROM_API_FILE - +#include +#include +#include #include #include +#include -#undef MPU_WRAPPERS_INCLUDED_FROM_API_FILE - -#ifdef HEAP_PRINT_DEBUG -#error This feature is broken, logging transport must be replaced with RTT -#endif - -#if(configSUPPORT_DYNAMIC_ALLOCATION == 0) -#error This file must not be used if configSUPPORT_DYNAMIC_ALLOCATION is 0 -#endif - -/* Block sizes must not get too small. */ -#define heapMINIMUM_BLOCK_SIZE ((size_t)(xHeapStructSize << 1)) - -/* Assumes 8bit bytes! */ -#define heapBITS_PER_BYTE ((size_t)8) - -/* Heap start end symbols provided by linker */ extern const void __heap_start__; extern const void __heap_end__; -uint8_t* ucHeap = (uint8_t*)&__heap_start__; - -/* Define the linked list structure. This is used to link free blocks in order -of their memory address. */ -typedef struct A_BLOCK_LINK { - struct A_BLOCK_LINK* pxNextFreeBlock; /*<< The next free block in the list. */ - size_t xBlockSize; /*<< The size of the free block. */ -} BlockLink_t; - -/*-----------------------------------------------------------*/ - -/* - * Inserts a block of memory that is being freed into the correct position in - * the list of free memory blocks. The block being freed will be merged with - * the block in front it and/or the block behind it if the memory blocks are - * adjacent to each other. - */ -static void prvInsertBlockIntoFreeList(BlockLink_t* pxBlockToInsert); - -/* - * Called automatically to setup the required heap structures the first time - * pvPortMalloc() is called. - */ -static void prvHeapInit(void); - -/*-----------------------------------------------------------*/ - -/* The size of the structure placed at the beginning of each allocated memory -block must by correctly byte aligned. */ -static const size_t xHeapStructSize = (sizeof(BlockLink_t) + ((size_t)(portBYTE_ALIGNMENT - 1))) & - ~((size_t)portBYTE_ALIGNMENT_MASK); - -/* Create a couple of list links to mark the start and end of the list. */ -static BlockLink_t xStart, *pxEnd = NULL; - -/* Keeps track of the number of free bytes remaining, but says nothing about -fragmentation. */ -static size_t xFreeBytesRemaining = 0U; -static size_t xMinimumEverFreeBytesRemaining = 0U; - -/* Gets set to the top bit of an size_t type. When this bit in the xBlockSize -member of an BlockLink_t structure is set then the block belongs to the -application. When the bit is free the block is still part of the free heap -space. */ -static size_t xBlockAllocatedBit = 0; - -/* Furi heap extension */ -#include -/* Allocation tracking types */ +static tlsf_t tlsf = NULL; +static size_t heap_used = 0; +static size_t heap_max_used = 0; + +// Allocation tracking types DICT_DEF2(MemmgrHeapAllocDict, uint32_t, uint32_t) //-V1048 DICT_DEF2( //-V1048 @@ -128,17 +22,35 @@ DICT_DEF2( //-V1048 MemmgrHeapAllocDict_t, DICT_OPLIST(MemmgrHeapAllocDict)) -/* Thread allocation tracing storage */ +// Thread allocation tracing storage static MemmgrHeapThreadDict_t memmgr_heap_thread_dict = {0}; static volatile uint32_t memmgr_heap_thread_trace_depth = 0; -/* Initialize tracing storage on start */ -void memmgr_heap_init(void) { +static inline void memmgr_lock(void) { + vTaskSuspendAll(); +} + +static inline void memmgr_unlock(void) { + xTaskResumeAll(); +} + +static inline size_t memmgr_get_heap_size(void) { + return (size_t)&__heap_end__ - (size_t)&__heap_start__; +} + +// Initialize tracing storage +static void memmgr_heap_init(void) { MemmgrHeapThreadDict_init(memmgr_heap_thread_dict); } +__attribute__((constructor)) static void memmgr_init(void) { + size_t pool_size = (size_t)&__heap_end__ - (size_t)&__heap_start__; + tlsf = tlsf_create_with_pool((void*)&__heap_start__, pool_size, pool_size); + memmgr_heap_init(); +} + void memmgr_heap_enable_thread_trace(FuriThreadId thread_id) { - vTaskSuspendAll(); + memmgr_lock(); { memmgr_heap_thread_trace_depth++; furi_check(MemmgrHeapThreadDict_get(memmgr_heap_thread_dict, (uint32_t)thread_id) == NULL); @@ -148,517 +60,289 @@ void memmgr_heap_enable_thread_trace(FuriThreadId thread_id) { MemmgrHeapAllocDict_clear(alloc_dict); memmgr_heap_thread_trace_depth--; } - (void)xTaskResumeAll(); + memmgr_unlock(); } void memmgr_heap_disable_thread_trace(FuriThreadId thread_id) { - vTaskSuspendAll(); + memmgr_lock(); { memmgr_heap_thread_trace_depth++; furi_check(MemmgrHeapThreadDict_erase(memmgr_heap_thread_dict, (uint32_t)thread_id)); memmgr_heap_thread_trace_depth--; } - (void)xTaskResumeAll(); + memmgr_unlock(); } -size_t memmgr_heap_get_thread_memory(FuriThreadId thread_id) { - size_t leftovers = MEMMGR_HEAP_UNKNOWN; - vTaskSuspendAll(); - { +static inline void memmgr_heap_trace_malloc(void* pointer, size_t size) { + FuriThreadId thread_id = furi_thread_get_current_id(); + if(thread_id && memmgr_heap_thread_trace_depth == 0) { memmgr_heap_thread_trace_depth++; MemmgrHeapAllocDict_t* alloc_dict = MemmgrHeapThreadDict_get(memmgr_heap_thread_dict, (uint32_t)thread_id); if(alloc_dict) { - leftovers = 0; - MemmgrHeapAllocDict_it_t alloc_dict_it; - for(MemmgrHeapAllocDict_it(alloc_dict_it, *alloc_dict); - !MemmgrHeapAllocDict_end_p(alloc_dict_it); - MemmgrHeapAllocDict_next(alloc_dict_it)) { - MemmgrHeapAllocDict_itref_t* data = MemmgrHeapAllocDict_ref(alloc_dict_it); - if(data->key != 0) { - uint8_t* puc = (uint8_t*)data->key; - puc -= xHeapStructSize; - BlockLink_t* pxLink = (void*)puc; - - if((pxLink->xBlockSize & xBlockAllocatedBit) != 0 && - pxLink->pxNextFreeBlock == NULL) { - leftovers += data->value; - } - } - } + MemmgrHeapAllocDict_set_at(*alloc_dict, (uint32_t)pointer, (uint32_t)size); } memmgr_heap_thread_trace_depth--; } - (void)xTaskResumeAll(); - return leftovers; } -#undef traceMALLOC -static inline void traceMALLOC(void* pointer, size_t size) { +static inline void memmgr_heap_trace_free(void* pointer) { FuriThreadId thread_id = furi_thread_get_current_id(); if(thread_id && memmgr_heap_thread_trace_depth == 0) { memmgr_heap_thread_trace_depth++; MemmgrHeapAllocDict_t* alloc_dict = MemmgrHeapThreadDict_get(memmgr_heap_thread_dict, (uint32_t)thread_id); if(alloc_dict) { - MemmgrHeapAllocDict_set_at(*alloc_dict, (uint32_t)pointer, (uint32_t)size); + // In some cases thread may want to release memory that was not allocated by it + const bool res = MemmgrHeapAllocDict_erase(*alloc_dict, (uint32_t)pointer); + UNUSED(res); } memmgr_heap_thread_trace_depth--; } } -#undef traceFREE -static inline void traceFREE(void* pointer, size_t size) { - UNUSED(size); - FuriThreadId thread_id = furi_thread_get_current_id(); - if(thread_id && memmgr_heap_thread_trace_depth == 0) { +size_t memmgr_heap_get_thread_memory(FuriThreadId thread_id) { + size_t leftovers = MEMMGR_HEAP_UNKNOWN; + memmgr_lock(); + { memmgr_heap_thread_trace_depth++; MemmgrHeapAllocDict_t* alloc_dict = MemmgrHeapThreadDict_get(memmgr_heap_thread_dict, (uint32_t)thread_id); if(alloc_dict) { - // In some cases thread may want to release memory that was not allocated by it - const bool res = MemmgrHeapAllocDict_erase(*alloc_dict, (uint32_t)pointer); - UNUSED(res); + leftovers = 0; + MemmgrHeapAllocDict_it_t alloc_dict_it; + for(MemmgrHeapAllocDict_it(alloc_dict_it, *alloc_dict); + !MemmgrHeapAllocDict_end_p(alloc_dict_it); + MemmgrHeapAllocDict_next(alloc_dict_it)) { + MemmgrHeapAllocDict_itref_t* data = MemmgrHeapAllocDict_ref(alloc_dict_it); + if(data->key != 0) { + block_header_t* block = block_from_ptr((uint8_t*)data->key); + if(!block_is_free(block)) { + leftovers += data->value; + } + } + } } memmgr_heap_thread_trace_depth--; } + memmgr_unlock(); + return leftovers; } -size_t memmgr_heap_get_max_free_block(void) { - size_t max_free_size = 0; - BlockLink_t* pxBlock; - vTaskSuspendAll(); +static bool tlsf_walker_max_free(void* ptr, size_t size, int used, void* user) { + UNUSED(ptr); - pxBlock = xStart.pxNextFreeBlock; - while(pxBlock->pxNextFreeBlock != NULL) { - if(pxBlock->xBlockSize > max_free_size) { - max_free_size = pxBlock->xBlockSize; - } - pxBlock = pxBlock->pxNextFreeBlock; + bool free = !used; + size_t* max_free_block_size = (size_t*)user; + if(free && size > *max_free_block_size) { + *max_free_block_size = size; } - xTaskResumeAll(); - return max_free_size; + return true; } -void memmgr_heap_printf_free_blocks(void) { - BlockLink_t* pxBlock; - //TODO enable when we can do printf with a locked scheduler - //vTaskSuspendAll(); +size_t memmgr_heap_get_max_free_block(void) { + size_t max_free_block_size = 0; - pxBlock = xStart.pxNextFreeBlock; - while(pxBlock->pxNextFreeBlock != NULL) { - printf("A %p S %lu\r\n", (void*)pxBlock, (uint32_t)pxBlock->xBlockSize); - pxBlock = pxBlock->pxNextFreeBlock; - } + memmgr_lock(); - //xTaskResumeAll(); -} + pool_t pool = tlsf_get_pool(tlsf); + tlsf_walk_pool(pool, tlsf_walker_max_free, &max_free_block_size); -#ifdef HEAP_PRINT_DEBUG -char* ultoa(unsigned long num, char* str, int radix) { - char temp[33]; // at radix 2 the string is at most 32 + 1 null long. - int temp_loc = 0; - int digit; - int str_loc = 0; - - //construct a backward string of the number. - do { - digit = (unsigned long)num % ((unsigned long)radix); - if(digit < 10) - temp[temp_loc++] = digit + '0'; - else - temp[temp_loc++] = digit - 10 + 'A'; - num = ((unsigned long)num) / ((unsigned long)radix); - } while((unsigned long)num > 0); - - temp_loc--; - - //now reverse the string. - while(temp_loc >= 0) { // while there are still chars - str[str_loc++] = temp[temp_loc--]; - } - str[str_loc] = 0; // add null termination. + memmgr_unlock(); - return str; + return max_free_block_size; } -static void print_heap_init(void) { - char tmp_str[33]; - size_t heap_start = (size_t)&__heap_start__; - size_t heap_end = (size_t)&__heap_end__; - - // {PHStart|heap_start|heap_end} - FURI_CRITICAL_ENTER(); - furi_log_puts("{PHStart|"); - ultoa(heap_start, tmp_str, 16); - furi_log_puts(tmp_str); - furi_log_puts("|"); - ultoa(heap_end, tmp_str, 16); - furi_log_puts(tmp_str); - furi_log_puts("}\r\n"); - FURI_CRITICAL_EXIT(); -} +typedef struct { + BlockWalker walker; + void* context; +} BlockWalkerWrapper; -static void print_heap_malloc(void* ptr, size_t size) { - char tmp_str[33]; - const char* name = furi_thread_get_name(furi_thread_get_current_id()); - if(!name) { - name = ""; - } - - // {thread name|m|address|size} - FURI_CRITICAL_ENTER(); - furi_log_puts("{"); - furi_log_puts(name); - furi_log_puts("|m|0x"); - ultoa((unsigned long)ptr, tmp_str, 16); - furi_log_puts(tmp_str); - furi_log_puts("|"); - utoa(size, tmp_str, 10); - furi_log_puts(tmp_str); - furi_log_puts("}\r\n"); - FURI_CRITICAL_EXIT(); +static bool tlsf_walker_wrapper(void* ptr, size_t size, int used, void* user) { + BlockWalkerWrapper* wrapper = (BlockWalkerWrapper*)user; + return wrapper->walker(ptr, size, used, wrapper->context); } -static void print_heap_free(void* ptr) { - char tmp_str[33]; - const char* name = furi_thread_get_name(furi_thread_get_current_id()); - if(!name) { - name = ""; - } +void memmgr_heap_walk_blocks(BlockWalker walker, void* context) { + memmgr_lock(); - // {thread name|f|address} - FURI_CRITICAL_ENTER(); - furi_log_puts("{"); - furi_log_puts(name); - furi_log_puts("|f|0x"); - ultoa((unsigned long)ptr, tmp_str, 16); - furi_log_puts(tmp_str); - furi_log_puts("}\r\n"); - FURI_CRITICAL_EXIT(); -} -#endif -/*-----------------------------------------------------------*/ + BlockWalkerWrapper wrapper = {walker, context}; + pool_t pool = tlsf_get_pool(tlsf); + tlsf_walk_pool(pool, tlsf_walker_wrapper, &wrapper); -void* pvPortMalloc(size_t xWantedSize) { - BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink; - void* pvReturn = NULL; - size_t to_wipe = xWantedSize; + memmgr_unlock(); +} +void* pvPortMalloc(size_t xSize) { + // memory management in ISR is not allowed if(FURI_IS_IRQ_MODE()) { furi_crash("memmgt in ISR"); } -#ifdef HEAP_PRINT_DEBUG - BlockLink_t* print_heap_block = NULL; -#endif - - /* If this is the first call to malloc then the heap will require - initialisation to setup the list of free blocks. */ - if(pxEnd == NULL) { -#ifdef HEAP_PRINT_DEBUG - print_heap_init(); -#endif - - vTaskSuspendAll(); - { - prvHeapInit(); - memmgr_heap_init(); + memmgr_lock(); + + // allocate block + void* data = tlsf_malloc(tlsf, xSize); + if(data == NULL) { + if(xSize == 0) { + furi_crash("malloc(0)"); + } else { + furi_crash("out of memory"); } - (void)xTaskResumeAll(); - } else { - mtCOVERAGE_TEST_MARKER(); } - vTaskSuspendAll(); - { - /* Check the requested block size is not so large that the top bit is - set. The top bit of the block size member of the BlockLink_t structure - is used to determine who owns the block - the application or the - kernel, so it must be free. */ - if((xWantedSize & xBlockAllocatedBit) == 0) { - /* The wanted size is increased so it can contain a BlockLink_t - structure in addition to the requested amount of bytes. */ - if(xWantedSize > 0) { - xWantedSize += xHeapStructSize; - - /* Ensure that blocks are always aligned to the required number - of bytes. */ - if((xWantedSize & portBYTE_ALIGNMENT_MASK) != 0x00) { - /* Byte alignment required. */ - xWantedSize += (portBYTE_ALIGNMENT - (xWantedSize & portBYTE_ALIGNMENT_MASK)); - configASSERT((xWantedSize & portBYTE_ALIGNMENT_MASK) == 0); - } else { - mtCOVERAGE_TEST_MARKER(); - } - } else { - mtCOVERAGE_TEST_MARKER(); - } + // update heap usage + heap_used += tlsf_block_size(data); + heap_used += tlsf_alloc_overhead(); + if(heap_used > heap_max_used) { + heap_max_used = heap_used; + } - if((xWantedSize > 0) && (xWantedSize <= xFreeBytesRemaining)) { - /* Traverse the list from the start (lowest address) block until - one of adequate size is found. */ - pxPreviousBlock = &xStart; - pxBlock = xStart.pxNextFreeBlock; - while((pxBlock->xBlockSize < xWantedSize) && (pxBlock->pxNextFreeBlock != NULL)) { - pxPreviousBlock = pxBlock; - pxBlock = pxBlock->pxNextFreeBlock; - } + // trace allocation + memmgr_heap_trace_malloc(data, xSize); - /* If the end marker was reached then a block of adequate size - was not found. */ - if(pxBlock != pxEnd) { - /* Return the memory space pointed to - jumping over the - BlockLink_t structure at its start. */ - pvReturn = - (void*)(((uint8_t*)pxPreviousBlock->pxNextFreeBlock) + xHeapStructSize); - - /* This block is being returned for use so must be taken out - of the list of free blocks. */ - pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock; - - /* If the block is larger than required it can be split into - two. */ - if((pxBlock->xBlockSize - xWantedSize) > heapMINIMUM_BLOCK_SIZE) { - /* This block is to be split into two. Create a new - block following the number of bytes requested. The void - cast is used to prevent byte alignment warnings from the - compiler. */ - pxNewBlockLink = (void*)(((uint8_t*)pxBlock) + xWantedSize); - configASSERT((((size_t)pxNewBlockLink) & portBYTE_ALIGNMENT_MASK) == 0); - - /* Calculate the sizes of two blocks split from the - single block. */ - pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize; - pxBlock->xBlockSize = xWantedSize; - - /* Insert the new block into the list of free blocks. */ - prvInsertBlockIntoFreeList(pxNewBlockLink); - } else { - mtCOVERAGE_TEST_MARKER(); - } + memmgr_unlock(); - xFreeBytesRemaining -= pxBlock->xBlockSize; + // clear block content + memset(data, 0, xSize); - if(xFreeBytesRemaining < xMinimumEverFreeBytesRemaining) { - xMinimumEverFreeBytesRemaining = xFreeBytesRemaining; - } else { - mtCOVERAGE_TEST_MARKER(); - } + return data; +} - /* The block is being returned - it is allocated and owned - by the application and has no "next" block. */ - pxBlock->xBlockSize |= xBlockAllocatedBit; - pxBlock->pxNextFreeBlock = NULL; +void vPortFree(void* pv) { + // memory management in ISR is not allowed + if(FURI_IS_IRQ_MODE()) { + furi_crash("memmgt in ISR"); + } -#ifdef HEAP_PRINT_DEBUG - print_heap_block = pxBlock; -#endif - } else { - mtCOVERAGE_TEST_MARKER(); - } - } else { - mtCOVERAGE_TEST_MARKER(); - } - } else { - mtCOVERAGE_TEST_MARKER(); - } + // ignore NULL pointer + if(pv != NULL) { + memmgr_lock(); - traceMALLOC(pvReturn, xWantedSize); - } - (void)xTaskResumeAll(); + // get block size + size_t block_size = tlsf_block_size(pv); -#ifdef HEAP_PRINT_DEBUG - print_heap_malloc(print_heap_block, print_heap_block->xBlockSize & ~xBlockAllocatedBit); -#endif + // clear block content + memset(pv, 0, block_size); -#if(configUSE_MALLOC_FAILED_HOOK == 1) - { - if(pvReturn == NULL) { - extern void vApplicationMallocFailedHook(void); - vApplicationMallocFailedHook(); - } else { - mtCOVERAGE_TEST_MARKER(); - } - } -#endif + // update heap usage + heap_used -= block_size; + heap_used -= tlsf_alloc_overhead(); - configASSERT((((size_t)pvReturn) & (size_t)portBYTE_ALIGNMENT_MASK) == 0); + // free + tlsf_free(tlsf, pv); - furi_check(pvReturn, xWantedSize ? "out of memory" : "malloc(0)"); - pvReturn = memset(pvReturn, 0, to_wipe); - return pvReturn; -} -/*-----------------------------------------------------------*/ + // trace free + memmgr_heap_trace_free(pv); -void vPortFree(void* pv) { - uint8_t* puc = (uint8_t*)pv; - BlockLink_t* pxLink; + memmgr_unlock(); + } +} +extern void* pvPortAllocAligned(size_t xSize, size_t xAlignment) { + // memory management in ISR is not allowed if(FURI_IS_IRQ_MODE()) { furi_crash("memmgt in ISR"); } - if(pv != NULL) { - /* The memory being freed will have an BlockLink_t structure immediately - before it. */ - puc -= xHeapStructSize; - - /* This casting is to keep the compiler from issuing warnings. */ - pxLink = (void*)puc; - - /* Check the block is actually allocated. */ - configASSERT((pxLink->xBlockSize & xBlockAllocatedBit) != 0); - configASSERT(pxLink->pxNextFreeBlock == NULL); - - if((pxLink->xBlockSize & xBlockAllocatedBit) != 0) { - if(pxLink->pxNextFreeBlock == NULL) { - /* The block is being returned to the heap - it is no longer - allocated. */ - pxLink->xBlockSize &= ~xBlockAllocatedBit; - -#ifdef HEAP_PRINT_DEBUG - print_heap_free(pxLink); -#endif - - vTaskSuspendAll(); - { - furi_assert((size_t)pv >= SRAM_BASE); - furi_assert((size_t)pv < SRAM_BASE + 1024 * 256); - furi_assert(pxLink->xBlockSize >= xHeapStructSize); - furi_assert((pxLink->xBlockSize - xHeapStructSize) < 1024 * 256); - - /* Add this block to the list of free blocks. */ - xFreeBytesRemaining += pxLink->xBlockSize; - traceFREE(pv, pxLink->xBlockSize); - memset(pv, 0, pxLink->xBlockSize - xHeapStructSize); - prvInsertBlockIntoFreeList(((BlockLink_t*)pxLink)); - } - (void)xTaskResumeAll(); - } else { - mtCOVERAGE_TEST_MARKER(); - } + // alignment must be power of 2 + if((xAlignment & (xAlignment - 1)) != 0) { + furi_crash("invalid alignment"); + } + + memmgr_lock(); + + // allocate block + void* data = tlsf_memalign(tlsf, xAlignment, xSize); + if(data == NULL) { + if(xSize == 0) { + furi_crash("malloc_aligned(0)"); } else { - mtCOVERAGE_TEST_MARKER(); + furi_crash("out of memory"); } - } else { -#ifdef HEAP_PRINT_DEBUG - print_heap_free(pv); -#endif } -} -/*-----------------------------------------------------------*/ -size_t xPortGetTotalHeapSize(void) { - return (size_t)&__heap_end__ - (size_t)&__heap_start__; -} -/*-----------------------------------------------------------*/ + // update heap usage + heap_used += tlsf_block_size(data); + heap_used += tlsf_alloc_overhead(); + if(heap_used > heap_max_used) { + heap_max_used = heap_used; + } -size_t xPortGetFreeHeapSize(void) { - return xFreeBytesRemaining; -} -/*-----------------------------------------------------------*/ + // trace allocation + memmgr_heap_trace_malloc(data, xSize); -size_t xPortGetMinimumEverFreeHeapSize(void) { - return xMinimumEverFreeBytesRemaining; -} -/*-----------------------------------------------------------*/ + memmgr_unlock(); + + // clear block content + memset(data, 0, xSize); -void vPortInitialiseBlocks(void) { - /* This just exists to keep the linker quiet. */ + return data; } -/*-----------------------------------------------------------*/ -static void prvHeapInit(void) { - BlockLink_t* pxFirstFreeBlock; - uint8_t* pucAlignedHeap; - size_t uxAddress; - size_t xTotalHeapSize = (size_t)&__heap_end__ - (size_t)&__heap_start__; +extern void* pvPortRealloc(void* pv, size_t xSize) { + // realloc(ptr, 0) is equivalent to free(ptr) + if(xSize == 0) { + vPortFree(pv); + return NULL; + } + + // realloc(NULL, size) is equivalent to malloc(size) + if(pv == NULL) { + return pvPortMalloc(xSize); + } - /* Ensure the heap starts on a correctly aligned boundary. */ - uxAddress = (size_t)ucHeap; + /* realloc things */ - if((uxAddress & portBYTE_ALIGNMENT_MASK) != 0) { - uxAddress += (portBYTE_ALIGNMENT - 1); - uxAddress &= ~((size_t)portBYTE_ALIGNMENT_MASK); - xTotalHeapSize -= uxAddress - (size_t)ucHeap; + // memory management in ISR is not allowed + if(FURI_IS_IRQ_MODE()) { + furi_crash("memmgt in ISR"); } - pucAlignedHeap = (uint8_t*)uxAddress; - - /* xStart is used to hold a pointer to the first item in the list of free - blocks. The void cast is used to prevent compiler warnings. */ - xStart.pxNextFreeBlock = (void*)pucAlignedHeap; - xStart.xBlockSize = (size_t)0; - - /* pxEnd is used to mark the end of the list of free blocks and is inserted - at the end of the heap space. */ - uxAddress = ((size_t)pucAlignedHeap) + xTotalHeapSize; - uxAddress -= xHeapStructSize; - uxAddress &= ~((size_t)portBYTE_ALIGNMENT_MASK); - pxEnd = (void*)uxAddress; - pxEnd->xBlockSize = 0; - pxEnd->pxNextFreeBlock = NULL; - - /* To start with there is a single free block that is sized to take up the - entire heap space, minus the space taken by pxEnd. */ - pxFirstFreeBlock = (void*)pucAlignedHeap; - pxFirstFreeBlock->xBlockSize = uxAddress - (size_t)pxFirstFreeBlock; - pxFirstFreeBlock->pxNextFreeBlock = pxEnd; - - /* Only one block exists - and it covers the entire usable heap space. */ - xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize; - xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize; - - /* Work out the position of the top bit in a size_t variable. */ - xBlockAllocatedBit = ((size_t)1) << ((sizeof(size_t) * heapBITS_PER_BYTE) - 1); -} -/*-----------------------------------------------------------*/ + memmgr_lock(); -static void prvInsertBlockIntoFreeList(BlockLink_t* pxBlockToInsert) { - BlockLink_t* pxIterator; - uint8_t* puc; + // trace old block as free + size_t old_size = tlsf_block_size(pv); - /* Iterate through the list until a block is found that has a higher address - than the block being inserted. */ - for(pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; - pxIterator = pxIterator->pxNextFreeBlock) { - /* Nothing to do here, just iterate to the right position. */ - } + // trace free + memmgr_heap_trace_free(pv); - /* Do the block being inserted, and the block it is being inserted after - make a contiguous block of memory? */ - puc = (uint8_t*)pxIterator; - if((puc + pxIterator->xBlockSize) == (uint8_t*)pxBlockToInsert) { - pxIterator->xBlockSize += pxBlockToInsert->xBlockSize; - pxBlockToInsert = pxIterator; - } else { - mtCOVERAGE_TEST_MARKER(); + // reallocate block + void* data = tlsf_realloc(tlsf, pv, xSize); + if(data == NULL) { + furi_crash("out of memory"); } - /* Do the block being inserted, and the block it is being inserted before - make a contiguous block of memory? */ - puc = (uint8_t*)pxBlockToInsert; - if((puc + pxBlockToInsert->xBlockSize) == (uint8_t*)pxIterator->pxNextFreeBlock) { - if(pxIterator->pxNextFreeBlock != pxEnd) { - /* Form one big block from the two blocks. */ - pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize; - pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock; - } else { - pxBlockToInsert->pxNextFreeBlock = pxEnd; - } - } else { - pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock; + // update heap usage + heap_used -= old_size; + heap_used += tlsf_block_size(data); + if(heap_used > heap_max_used) { + heap_max_used = heap_used; } - /* If the block being inserted plugged a gab, so was merged with the block - before and the block after, then it's pxNextFreeBlock pointer will have - already been set, and should not be set here as that would make it point - to itself. */ - if(pxIterator != pxBlockToInsert) { - pxIterator->pxNextFreeBlock = pxBlockToInsert; - } else { - mtCOVERAGE_TEST_MARKER(); + // trace allocation + memmgr_heap_trace_malloc(data, xSize); + + memmgr_unlock(); + + // clear remain block content, if the new size is bigger + // can't guarantee that all data will be zeroed, cos tlsf_block_size is not always the same as xSize + if(xSize > old_size) { + memset((uint8_t*)data + old_size, 0, xSize - old_size); } + + return data; } + +size_t xPortGetFreeHeapSize(void) { + return memmgr_get_heap_size() - heap_used - tlsf_size(tlsf); +} + +size_t xPortGetTotalHeapSize(void) { + return memmgr_get_heap_size(); +} + +size_t xPortGetMinimumEverFreeHeapSize(void) { + return memmgr_get_heap_size() - heap_max_used - tlsf_size(tlsf); +} \ No newline at end of file diff --git a/furi/core/memmgr_heap.h b/furi/core/memmgr_heap.h index 7d889f1520..2f61deb642 100644 --- a/furi/core/memmgr_heap.h +++ b/furi/core/memmgr_heap.h @@ -40,9 +40,17 @@ size_t memmgr_heap_get_thread_memory(FuriThreadId thread_id); */ size_t memmgr_heap_get_max_free_block(void); -/** Print the address and size of all free blocks to stdout +typedef bool (*BlockWalker)(void* pointer, size_t size, bool used, void* context); + +/** + * @brief Walk through all heap blocks + * @warning This function will lock memory manager and may cause deadlocks if any malloc/free is called inside the callback. + * Also, printf and furi_log contains malloc calls, so do not use them. + * + * @param walker + * @param context */ -void memmgr_heap_printf_free_blocks(void); +void memmgr_heap_walk_blocks(BlockWalker walker, void* context); #ifdef __cplusplus } diff --git a/furi/core/thread.c b/furi/core/thread.c index ecb21636c7..c3c44ae6db 100644 --- a/furi/core/thread.c +++ b/furi/core/thread.c @@ -244,8 +244,8 @@ void furi_thread_start(FuriThread* thread) { stack, thread, priority, - memmgr_alloc_from_pool(sizeof(StackType_t) * stack), - memmgr_alloc_from_pool(sizeof(StaticTask_t))); + memmgr_aux_pool_alloc(sizeof(StackType_t) * stack), + memmgr_aux_pool_alloc(sizeof(StaticTask_t))); } else { BaseType_t ret = xTaskCreate( furi_thread_body, thread->name, stack, thread, priority, &thread->task_handle); diff --git a/furi/flipper.c b/furi/flipper.c index da4ebd2599..fb35c5f070 100644 --- a/furi/flipper.c +++ b/furi/flipper.c @@ -169,12 +169,17 @@ void flipper_init(void) { FURI_LOG_I(TAG, "Startup complete"); } +PLACE_IN_SECTION("MB_MEM2") static StaticTask_t idle_task_tcb; +PLACE_IN_SECTION("MB_MEM2") static StackType_t idle_task_stack[configIDLE_TASK_STACK_DEPTH]; +PLACE_IN_SECTION("MB_MEM2") static StaticTask_t timer_task_tcb; +PLACE_IN_SECTION("MB_MEM2") static StackType_t timer_task_stack[configTIMER_TASK_STACK_DEPTH]; + void vApplicationGetIdleTaskMemory( StaticTask_t** tcb_ptr, StackType_t** stack_ptr, uint32_t* stack_size) { - *tcb_ptr = memmgr_alloc_from_pool(sizeof(StaticTask_t)); - *stack_ptr = memmgr_alloc_from_pool(sizeof(StackType_t) * configIDLE_TASK_STACK_DEPTH); + *tcb_ptr = &idle_task_tcb; + *stack_ptr = idle_task_stack; *stack_size = configIDLE_TASK_STACK_DEPTH; } @@ -182,7 +187,7 @@ void vApplicationGetTimerTaskMemory( StaticTask_t** tcb_ptr, StackType_t** stack_ptr, uint32_t* stack_size) { - *tcb_ptr = memmgr_alloc_from_pool(sizeof(StaticTask_t)); - *stack_ptr = memmgr_alloc_from_pool(sizeof(StackType_t) * configTIMER_TASK_STACK_DEPTH); + *tcb_ptr = &timer_task_tcb; + *stack_ptr = timer_task_stack; *stack_size = configTIMER_TASK_STACK_DEPTH; } diff --git a/lib/SConscript b/lib/SConscript index 84247d53c6..61221d87a2 100644 --- a/lib/SConscript +++ b/lib/SConscript @@ -14,6 +14,7 @@ env.Append( libs = env.BuildModules( [ + "tlsf", "mlib", "stm32wb", "freertos", diff --git a/lib/ble_profile/extra_profiles/hid_profile.c b/lib/ble_profile/extra_profiles/hid_profile.c index 94bf896328..4e4f9e9912 100644 --- a/lib/ble_profile/extra_profiles/hid_profile.c +++ b/lib/ble_profile/extra_profiles/hid_profile.c @@ -380,8 +380,8 @@ static GapConfig template_config = { .pairing_method = GapPairingPinCodeVerifyYesNo, .conn_param = { - .conn_int_min = 0x18, // 30 ms - .conn_int_max = 0x24, // 45 ms + .conn_int_min = 0x18, // AN5289: 4.7, we need at least 25ms + advertisement, which is 30 ms + .conn_int_max = 0x18, // 30 ms .slave_latency = 0, .supervisor_timeout = 0, }, diff --git a/lib/flipper_application/elf/elf_file.c b/lib/flipper_application/elf/elf_file.c index 6543168662..e117e546a2 100644 --- a/lib/flipper_application/elf/elf_file.c +++ b/lib/flipper_application/elf/elf_file.c @@ -461,7 +461,7 @@ static bool elf_load_section_data(ELFFile* elf, ELFSection* section, Elf32_Shdr* return true; } - section->data = aligned_malloc(section_header->sh_size, section_header->sh_addralign); + section->data = aligned_alloc(section_header->sh_addralign, section_header->sh_size); section->size = section_header->sh_size; if(section_header->sh_type == SHT_NOBITS) { @@ -713,7 +713,7 @@ static bool elf_relocate_fast(ELFFile* elf, ELFSection* s) { } } - aligned_free(s->fast_rel->data); + free(s->fast_rel->data); free(s->fast_rel); s->fast_rel = NULL; @@ -780,10 +780,10 @@ void elf_file_free(ELFFile* elf) { ELFSectionDict_next(it)) { const ELFSectionDict_itref_t* itref = ELFSectionDict_cref(it); if(itref->value.data) { - aligned_free(itref->value.data); + free(itref->value.data); } if(itref->value.fast_rel) { - aligned_free(itref->value.fast_rel->data); + free(itref->value.fast_rel->data); free(itref->value.fast_rel); } free((void*)itref->key); diff --git a/lib/ibutton/ibutton_protocols.c b/lib/ibutton/ibutton_protocols.c index ce4392475b..84fcccd71c 100644 --- a/lib/ibutton/ibutton_protocols.c +++ b/lib/ibutton/ibutton_protocols.c @@ -287,6 +287,17 @@ bool ibutton_protocols_load(iButtonProtocols* protocols, iButtonKey* key, const return success; } +void ibutton_protocols_render_uid( + iButtonProtocols* protocols, + const iButtonKey* key, + FuriString* result) { + const iButtonProtocolId id = ibutton_key_get_protocol_id(key); + const iButtonProtocolData* data = ibutton_key_get_protocol_data(key); + + GET_PROTOCOL_GROUP(id); + GROUP_BASE->render_uid(GROUP_DATA, data, PROTOCOL_ID, result); +} + void ibutton_protocols_render_data( iButtonProtocols* protocols, const iButtonKey* key, diff --git a/lib/ibutton/ibutton_protocols.h b/lib/ibutton/ibutton_protocols.h index ec4a9fc723..dd2afbd6ee 100644 --- a/lib/ibutton/ibutton_protocols.h +++ b/lib/ibutton/ibutton_protocols.h @@ -133,6 +133,17 @@ bool ibutton_protocols_save( */ bool ibutton_protocols_load(iButtonProtocols* protocols, iButtonKey* key, const char* file_name); +/** + * Format a string containing defice UID + * @param [in] protocols pointer to an iButtonProtocols object + * @param [in] key pointer to the key to be rendered + * @param [out] result pointer to the FuriString instance (must be initialized) + */ +void ibutton_protocols_render_uid( + iButtonProtocols* protocols, + const iButtonKey* key, + FuriString* result); + /** * Format a string containing device full data * @param [in] protocols pointer to an iButtonProtocols object diff --git a/lib/ibutton/protocols/dallas/dallas_common.c b/lib/ibutton/protocols/dallas/dallas_common.c index 6e99a3be23..7ce12f7a06 100644 --- a/lib/ibutton/protocols/dallas/dallas_common.c +++ b/lib/ibutton/protocols/dallas/dallas_common.c @@ -208,15 +208,26 @@ bool dallas_common_is_valid_crc(const DallasCommonRomData* rom_data) { return crc_calculated == crc_received; } +void dallas_common_render_uid(FuriString* result, const DallasCommonRomData* rom_data) { + furi_string_cat_printf(result, "ID: "); + for(size_t i = 0; i < sizeof(DallasCommonRomData); ++i) { + furi_string_cat_printf(result, "%02X ", rom_data->bytes[i]); + } +} + void dallas_common_render_brief_data( FuriString* result, const DallasCommonRomData* rom_data, const uint8_t* mem_data, size_t mem_size, const char* mem_name) { + UNUSED(mem_data); + + furi_string_cat_printf(result, "ID: "); for(size_t i = 0; i < sizeof(rom_data->bytes); ++i) { furi_string_cat_printf(result, "%02X ", rom_data->bytes[i]); } + furi_string_cat_printf(result, "\nFamily Code: %02X\n", rom_data->bytes[0]); const char* size_prefix = ""; size_t mem_size_bits = mem_size * BITS_IN_BYTE; @@ -229,28 +240,23 @@ void dallas_common_render_brief_data( mem_size_bits /= BITS_IN_KBIT; } - furi_string_cat_printf( - result, "\nInternal %s: %zu %sbit\n", mem_name, mem_size_bits, size_prefix); - - for(size_t i = 0; i < DALLAS_COMMON_BRIEF_HEAD_COUNT; ++i) { - furi_string_cat_printf(result, "%02X ", mem_data[i]); - } - - furi_string_cat_printf(result, "[ . . . ]"); - - for(size_t i = mem_size - DALLAS_COMMON_BRIEF_TAIL_COUNT; i < mem_size; ++i) { - furi_string_cat_printf(result, " %02X", mem_data[i]); - } + furi_string_cat_printf(result, "%s: %zu %sbit\n", mem_name, mem_size_bits, size_prefix); } void dallas_common_render_crc_error(FuriString* result, const DallasCommonRomData* rom_data) { - furi_string_set(result, "CRC Error\n"); + furi_string_set(result, "\e#CRC Error\e#\n"); const size_t data_size = sizeof(DallasCommonRomData); for(size_t i = 0; i < data_size; ++i) { - furi_string_cat_printf(result, (i < data_size - 1) ? "%02X " : "%02X", rom_data->bytes[i]); + furi_string_cat_printf( + result, (i < data_size - 1) ? "%02X " : "\e!%02X\e!", rom_data->bytes[i]); } + + furi_string_cat_printf( + result, + "\nExpected CRC: \e!%02X\e!", + maxim_crc8(rom_data->bytes, sizeof(DallasCommonRomData) - 1, MAXIM_CRC8_INIT)); } void dallas_common_apply_edits(DallasCommonRomData* rom_data, uint8_t family_code) { diff --git a/lib/ibutton/protocols/dallas/dallas_common.h b/lib/ibutton/protocols/dallas/dallas_common.h index 6f5ff7cc01..90fec3e28a 100644 --- a/lib/ibutton/protocols/dallas/dallas_common.h +++ b/lib/ibutton/protocols/dallas/dallas_common.h @@ -96,6 +96,8 @@ bool dallas_common_load_rom_data( /* Miscellaneous */ bool dallas_common_is_valid_crc(const DallasCommonRomData* rom_data); +void dallas_common_render_uid(FuriString* result, const DallasCommonRomData* rom_data); + void dallas_common_render_brief_data( FuriString* result, const DallasCommonRomData* rom_data, diff --git a/lib/ibutton/protocols/dallas/protocol_dallas_base.h b/lib/ibutton/protocols/dallas/protocol_dallas_base.h index 55e1099360..05620329f8 100644 --- a/lib/ibutton/protocols/dallas/protocol_dallas_base.h +++ b/lib/ibutton/protocols/dallas/protocol_dallas_base.h @@ -30,6 +30,7 @@ typedef struct { iButtonProtocolDallasEmulateFunc emulate; iButtonProtocolDallasSaveFunc save; iButtonProtocolDallasLoadFunc load; + iButtonProtocolDallasRenderDataFunc render_uid; iButtonProtocolDallasRenderDataFunc render_data; iButtonProtocolDallasRenderDataFunc render_brief_data; iButtonProtocolDallasRenderDataFunc render_error; diff --git a/lib/ibutton/protocols/dallas/protocol_ds1971.c b/lib/ibutton/protocols/dallas/protocol_ds1971.c index b65e645846..d60803fc6d 100644 --- a/lib/ibutton/protocols/dallas/protocol_ds1971.c +++ b/lib/ibutton/protocols/dallas/protocol_ds1971.c @@ -35,6 +35,7 @@ static bool dallas_ds1971_write_copy(OneWireHost*, iButtonProtocolData*); static void dallas_ds1971_emulate(OneWireSlave*, iButtonProtocolData*); static bool dallas_ds1971_load(FlipperFormat*, uint32_t, iButtonProtocolData*); static bool dallas_ds1971_save(FlipperFormat*, const iButtonProtocolData*); +static void dallas_ds1971_render_uid(FuriString*, const iButtonProtocolData*); static void dallas_ds1971_render_data(FuriString*, const iButtonProtocolData*); static void dallas_ds1971_render_brief_data(FuriString*, const iButtonProtocolData*); static void dallas_ds1971_render_error(FuriString*, const iButtonProtocolData*); @@ -58,6 +59,7 @@ const iButtonProtocolDallasBase ibutton_protocol_ds1971 = { .emulate = dallas_ds1971_emulate, .save = dallas_ds1971_save, .load = dallas_ds1971_load, + .render_uid = dallas_ds1971_render_uid, .render_data = dallas_ds1971_render_data, .render_brief_data = dallas_ds1971_render_brief_data, .render_error = dallas_ds1971_render_error, @@ -209,14 +211,26 @@ bool dallas_ds1971_save(FlipperFormat* ff, const iButtonProtocolData* protocol_d return success; } +void dallas_ds1971_render_uid(FuriString* result, const iButtonProtocolData* protocol_data) { + const DS1971ProtocolData* data = protocol_data; + dallas_common_render_uid(result, &data->rom_data); +} + void dallas_ds1971_render_data(FuriString* result, const iButtonProtocolData* protocol_data) { const DS1971ProtocolData* data = protocol_data; + FuriString* data_string = furi_string_alloc(); + pretty_format_bytes_hex_canonical( - result, + data_string, DS1971_DATA_BYTE_COUNT, PRETTY_FORMAT_FONT_MONOSPACE, data->eeprom_data, DS1971_EEPROM_DATA_SIZE); + + furi_string_cat_printf(result, "\e#Memory Data\n--------------------\n"); + furi_string_cat_printf(result, "%s", furi_string_get_cstr(data_string)); + + furi_string_free(data_string); } void dallas_ds1971_render_brief_data(FuriString* result, const iButtonProtocolData* protocol_data) { diff --git a/lib/ibutton/protocols/dallas/protocol_ds1990.c b/lib/ibutton/protocols/dallas/protocol_ds1990.c index 86d39f1bd8..67e7545f45 100644 --- a/lib/ibutton/protocols/dallas/protocol_ds1990.c +++ b/lib/ibutton/protocols/dallas/protocol_ds1990.c @@ -27,6 +27,7 @@ static bool dallas_ds1990_write_blank(OneWireHost*, iButtonProtocolData*); static void dallas_ds1990_emulate(OneWireSlave*, iButtonProtocolData*); static bool dallas_ds1990_load(FlipperFormat*, uint32_t, iButtonProtocolData*); static bool dallas_ds1990_save(FlipperFormat*, const iButtonProtocolData*); +static void dallas_ds1990_render_uid(FuriString*, const iButtonProtocolData*); static void dallas_ds1990_render_brief_data(FuriString*, const iButtonProtocolData*); static void dallas_ds1990_render_error(FuriString*, const iButtonProtocolData*); static bool dallas_ds1990_is_data_valid(const iButtonProtocolData*); @@ -46,6 +47,7 @@ const iButtonProtocolDallasBase ibutton_protocol_ds1990 = { .emulate = dallas_ds1990_emulate, .save = dallas_ds1990_save, .load = dallas_ds1990_load, + .render_uid = dallas_ds1990_render_uid, .render_data = NULL, /* No data to render */ .render_brief_data = dallas_ds1990_render_brief_data, .render_error = dallas_ds1990_render_error, @@ -117,12 +119,20 @@ bool dallas_ds1990_load( return dallas_common_load_rom_data(ff, format_version, &data->rom_data); } +void dallas_ds1990_render_uid(FuriString* result, const iButtonProtocolData* protocol_data) { + const DS1990ProtocolData* data = protocol_data; + + dallas_common_render_uid(result, &data->rom_data); +} + void dallas_ds1990_render_brief_data(FuriString* result, const iButtonProtocolData* protocol_data) { const DS1990ProtocolData* data = protocol_data; + furi_string_cat_printf(result, "ID: "); for(size_t i = 0; i < sizeof(DallasCommonRomData); ++i) { furi_string_cat_printf(result, "%02X ", data->rom_data.bytes[i]); } + furi_string_cat_printf(result, "\nFamily Code: %02X\n", data->rom_data.bytes[0]); } void dallas_ds1990_render_error(FuriString* result, const iButtonProtocolData* protocol_data) { diff --git a/lib/ibutton/protocols/dallas/protocol_ds1992.c b/lib/ibutton/protocols/dallas/protocol_ds1992.c index 7440882ea0..5ddd8ef2ce 100644 --- a/lib/ibutton/protocols/dallas/protocol_ds1992.c +++ b/lib/ibutton/protocols/dallas/protocol_ds1992.c @@ -36,6 +36,7 @@ static bool dallas_ds1992_write_copy(OneWireHost*, iButtonProtocolData*); static void dallas_ds1992_emulate(OneWireSlave*, iButtonProtocolData*); static bool dallas_ds1992_load(FlipperFormat*, uint32_t, iButtonProtocolData*); static bool dallas_ds1992_save(FlipperFormat*, const iButtonProtocolData*); +static void dallas_ds1992_render_uid(FuriString*, const iButtonProtocolData*); static void dallas_ds1992_render_data(FuriString*, const iButtonProtocolData*); static void dallas_ds1992_render_brief_data(FuriString*, const iButtonProtocolData*); static void dallas_ds1992_render_error(FuriString*, const iButtonProtocolData*); @@ -57,6 +58,7 @@ const iButtonProtocolDallasBase ibutton_protocol_ds1992 = { .emulate = dallas_ds1992_emulate, .save = dallas_ds1992_save, .load = dallas_ds1992_load, + .render_uid = dallas_ds1992_render_uid, .render_data = dallas_ds1992_render_data, .render_brief_data = dallas_ds1992_render_brief_data, .render_error = dallas_ds1992_render_error, @@ -182,14 +184,26 @@ bool dallas_ds1992_save(FlipperFormat* ff, const iButtonProtocolData* protocol_d return success; } +void dallas_ds1992_render_uid(FuriString* result, const iButtonProtocolData* protocol_data) { + const DS1992ProtocolData* data = protocol_data; + dallas_common_render_uid(result, &data->rom_data); +} + void dallas_ds1992_render_data(FuriString* result, const iButtonProtocolData* protocol_data) { const DS1992ProtocolData* data = protocol_data; + FuriString* data_string = furi_string_alloc(); + pretty_format_bytes_hex_canonical( - result, + data_string, DS1992_DATA_BYTE_COUNT, PRETTY_FORMAT_FONT_MONOSPACE, data->sram_data, DS1992_SRAM_DATA_SIZE); + + furi_string_cat_printf(result, "\e#Memory Data\n--------------------\n"); + furi_string_cat_printf(result, "%s", furi_string_get_cstr(data_string)); + + furi_string_free(data_string); } void dallas_ds1992_render_brief_data(FuriString* result, const iButtonProtocolData* protocol_data) { diff --git a/lib/ibutton/protocols/dallas/protocol_ds1996.c b/lib/ibutton/protocols/dallas/protocol_ds1996.c index 5970a67bbf..6af61f3552 100644 --- a/lib/ibutton/protocols/dallas/protocol_ds1996.c +++ b/lib/ibutton/protocols/dallas/protocol_ds1996.c @@ -33,6 +33,7 @@ static bool dallas_ds1996_write_copy(OneWireHost*, iButtonProtocolData*); static void dallas_ds1996_emulate(OneWireSlave*, iButtonProtocolData*); static bool dallas_ds1996_load(FlipperFormat*, uint32_t, iButtonProtocolData*); static bool dallas_ds1996_save(FlipperFormat*, const iButtonProtocolData*); +static void dallas_ds1996_render_uid(FuriString*, const iButtonProtocolData*); static void dallas_ds1996_render_data(FuriString*, const iButtonProtocolData*); static void dallas_ds1996_render_brief_data(FuriString*, const iButtonProtocolData*); static void dallas_ds1996_render_error(FuriString*, const iButtonProtocolData*); @@ -53,6 +54,7 @@ const iButtonProtocolDallasBase ibutton_protocol_ds1996 = { .emulate = dallas_ds1996_emulate, .save = dallas_ds1996_save, .load = dallas_ds1996_load, + .render_uid = dallas_ds1996_render_uid, .render_data = dallas_ds1996_render_data, .render_brief_data = dallas_ds1996_render_brief_data, .render_error = dallas_ds1996_render_error, @@ -207,15 +209,27 @@ bool dallas_ds1996_save(FlipperFormat* ff, const iButtonProtocolData* protocol_d return success; } +void dallas_ds1996_render_uid(FuriString* result, const iButtonProtocolData* protocol_data) { + const DS1996ProtocolData* data = protocol_data; + dallas_common_render_uid(result, &data->rom_data); +} + void dallas_ds1996_render_data(FuriString* result, const iButtonProtocolData* protocol_data) { const DS1996ProtocolData* data = protocol_data; + FuriString* data_string = furi_string_alloc(); + pretty_format_bytes_hex_canonical( - result, + data_string, DS1996_DATA_BYTE_COUNT, PRETTY_FORMAT_FONT_MONOSPACE, data->sram_data, DS1996_SRAM_DATA_SIZE); + + furi_string_cat_printf(result, "\e#Memory Data\n--------------------\n"); + furi_string_cat_printf(result, "%s", furi_string_get_cstr(data_string)); + + furi_string_free(data_string); } void dallas_ds1996_render_brief_data(FuriString* result, const iButtonProtocolData* protocol_data) { diff --git a/lib/ibutton/protocols/dallas/protocol_ds_generic.c b/lib/ibutton/protocols/dallas/protocol_ds_generic.c index 6c698bb892..101db1dbe2 100644 --- a/lib/ibutton/protocols/dallas/protocol_ds_generic.c +++ b/lib/ibutton/protocols/dallas/protocol_ds_generic.c @@ -8,7 +8,7 @@ #include "../blanks/tm2004.h" #define DALLAS_GENERIC_FAMILY_CODE 0x00U -#define DALLAS_GENERIC_FAMILY_NAME "DSGeneric" +#define DALLAS_GENERIC_FAMILY_NAME "(non-specific)" typedef struct { OneWireSlave* bus; @@ -24,6 +24,7 @@ static bool ds_generic_write_blank(OneWireHost*, iButtonProtocolData*); static void ds_generic_emulate(OneWireSlave*, iButtonProtocolData*); static bool ds_generic_load(FlipperFormat*, uint32_t, iButtonProtocolData*); static bool ds_generic_save(FlipperFormat*, const iButtonProtocolData*); +static void ds_generic_render_uid(FuriString*, const iButtonProtocolData*); static void ds_generic_render_brief_data(FuriString*, const iButtonProtocolData*); static void ds_generic_render_error(FuriString*, const iButtonProtocolData*); static bool ds_generic_is_data_valid(const iButtonProtocolData*); @@ -44,6 +45,7 @@ const iButtonProtocolDallasBase ibutton_protocol_ds_generic = { .save = ds_generic_save, .load = ds_generic_load, .render_data = NULL, /* No data to render */ + .render_uid = ds_generic_render_uid, .render_brief_data = ds_generic_render_brief_data, .render_error = ds_generic_render_error, .is_valid = ds_generic_is_data_valid, @@ -111,9 +113,15 @@ bool ds_generic_load( return dallas_common_load_rom_data(ff, format_version, &data->rom_data); } +void ds_generic_render_uid(FuriString* result, const iButtonProtocolData* protocol_data) { + const DallasGenericProtocolData* data = protocol_data; + dallas_common_render_uid(result, &data->rom_data); +} + void ds_generic_render_brief_data(FuriString* result, const iButtonProtocolData* protocol_data) { const DallasGenericProtocolData* data = protocol_data; + furi_string_cat_printf(result, "ID: "); for(size_t i = 0; i < sizeof(DallasCommonRomData); ++i) { furi_string_cat_printf(result, "%02X ", data->rom_data.bytes[i]); } diff --git a/lib/ibutton/protocols/dallas/protocol_group_dallas.c b/lib/ibutton/protocols/dallas/protocol_group_dallas.c index 39cabbeb45..7dad756690 100644 --- a/lib/ibutton/protocols/dallas/protocol_group_dallas.c +++ b/lib/ibutton/protocols/dallas/protocol_group_dallas.c @@ -51,6 +51,12 @@ static bool ibutton_protocol_group_dallas_get_id_by_name( return true; } + // Handle files that refer to Dallas "Raw Data" as DSGeneric + if(strcmp(name, "DSGeneric") == 0) { + *id = iButtonProtocolDSGeneric; + return true; + } + for(iButtonProtocolLocalId i = 0; i < iButtonProtocolDSMax; ++i) { if(strcmp(ibutton_protocols_dallas[i]->name, name) == 0) { *id = i; @@ -212,6 +218,18 @@ static bool ibutton_protocol_group_dallas_load( return ibutton_protocols_dallas[id]->load(ff, version, data); } +static void ibutton_protocol_group_dallas_render_uid( + iButtonProtocolGroupDallas* group, + const iButtonProtocolData* data, + iButtonProtocolLocalId id, + FuriString* result) { + UNUSED(group); + furi_assert(id < iButtonProtocolDSMax); + const iButtonProtocolDallasBase* protocol = ibutton_protocols_dallas[id]; + furi_assert(protocol->render_uid); + protocol->render_uid(result, data); +} + static void ibutton_protocol_group_dallas_render_data( iButtonProtocolGroupDallas* group, const iButtonProtocolData* data, @@ -298,6 +316,7 @@ const iButtonProtocolGroupBase ibutton_protocol_group_dallas = { .save = (iButtonProtocolGroupSaveFunc)ibutton_protocol_group_dallas_save, .load = (iButtonProtocolGroupLoadFunc)ibutton_protocol_group_dallas_load, + .render_uid = (iButtonProtocolGroupRenderFunc)ibutton_protocol_group_dallas_render_uid, .render_data = (iButtonProtocolGroupRenderFunc)ibutton_protocol_group_dallas_render_data, .render_brief_data = (iButtonProtocolGroupRenderFunc)ibutton_protocol_group_dallas_render_brief_data, diff --git a/lib/ibutton/protocols/misc/protocol_cyfral.c b/lib/ibutton/protocols/misc/protocol_cyfral.c index 0cdb8766b0..d38865bae2 100644 --- a/lib/ibutton/protocols/misc/protocol_cyfral.c +++ b/lib/ibutton/protocols/misc/protocol_cyfral.c @@ -325,7 +325,15 @@ static LevelDuration protocol_cyfral_encoder_yield(ProtocolCyfral* proto) { return result; } +static void protocol_cyfral_render_uid(FuriString* result, ProtocolCyfral* proto) { + furi_string_cat_printf(result, "ID: "); + for(size_t i = 0; i < CYFRAL_DATA_SIZE; ++i) { + furi_string_cat_printf(result, "%02X ", ((uint8_t*)&proto->data)[i]); + } +} + static void protocol_cyfral_render_brief_data(ProtocolCyfral* proto, FuriString* result) { + furi_string_cat_printf(result, "ID: "); for(size_t i = 0; i < CYFRAL_DATA_SIZE; ++i) { furi_string_cat_printf(result, "%02X ", ((uint8_t*)&proto->data)[i]); } @@ -348,5 +356,6 @@ const ProtocolBase ibutton_protocol_misc_cyfral = { .start = (ProtocolEncoderStart)protocol_cyfral_encoder_start, .yield = (ProtocolEncoderYield)protocol_cyfral_encoder_yield, }, + .render_uid = (ProtocolRenderData)protocol_cyfral_render_uid, .render_brief_data = (ProtocolRenderData)protocol_cyfral_render_brief_data, }; diff --git a/lib/ibutton/protocols/misc/protocol_metakom.c b/lib/ibutton/protocols/misc/protocol_metakom.c index a2bd2cf7ca..6d5e0339d6 100644 --- a/lib/ibutton/protocols/misc/protocol_metakom.c +++ b/lib/ibutton/protocols/misc/protocol_metakom.c @@ -301,12 +301,17 @@ static LevelDuration protocol_metakom_encoder_yield(ProtocolMetakom* proto) { return result; } -static void protocol_metakom_render_brief_data(ProtocolMetakom* proto, FuriString* result) { +static void protocol_metakom_render_uid(ProtocolMetakom* proto, FuriString* result) { + furi_string_cat_printf(result, "ID: "); for(size_t i = 0; i < METAKOM_DATA_SIZE; ++i) { furi_string_cat_printf(result, "%02X ", ((uint8_t*)&proto->data)[i]); } } +static void protocol_metakom_render_brief_data(ProtocolMetakom* proto, FuriString* result) { + protocol_metakom_render_uid(proto, result); +} + const ProtocolBase ibutton_protocol_misc_metakom = { .name = "Metakom", .manufacturer = "Metakom", @@ -324,5 +329,6 @@ const ProtocolBase ibutton_protocol_misc_metakom = { .start = (ProtocolEncoderStart)protocol_metakom_encoder_start, .yield = (ProtocolEncoderYield)protocol_metakom_encoder_yield, }, + .render_uid = (ProtocolRenderData)protocol_metakom_render_uid, .render_brief_data = (ProtocolRenderData)protocol_metakom_render_brief_data, }; diff --git a/lib/ibutton/protocols/protocol_group_base.h b/lib/ibutton/protocols/protocol_group_base.h index c8fec70fe7..ef57fe0bc0 100644 --- a/lib/ibutton/protocols/protocol_group_base.h +++ b/lib/ibutton/protocols/protocol_group_base.h @@ -93,6 +93,7 @@ typedef struct { iButtonProtocolGroupSaveFunc save; iButtonProtocolGroupLoadFunc load; + iButtonProtocolGroupRenderFunc render_uid; iButtonProtocolGroupRenderFunc render_data; iButtonProtocolGroupRenderFunc render_brief_data; iButtonProtocolGroupRenderFunc render_error; diff --git a/lib/momentum/momentum.h b/lib/momentum/momentum.h index b5eeec02f2..ea2c86cc46 100644 --- a/lib/momentum/momentum.h +++ b/lib/momentum/momentum.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -89,6 +90,7 @@ typedef struct { VgmColorMode vgm_color_mode; Rgb565Color vgm_color_fg; Rgb565Color vgm_color_bg; + FuriHalVersionColor spoof_color; } MomentumSettings; typedef struct { diff --git a/lib/momentum/settings.c b/lib/momentum/settings.c index 367fd51c90..0294a796f2 100644 --- a/lib/momentum/settings.c +++ b/lib/momentum/settings.c @@ -42,6 +42,7 @@ MomentumSettings momentum_settings = { .vgm_color_mode = VgmColorModeDefault, // Default .vgm_color_fg.value = 0x0000, // Default Black .vgm_color_bg.value = 0xFC00, // Default Orange + .spoof_color = FuriHalVersionColorUnknown, // Real }; typedef enum { @@ -112,6 +113,7 @@ static const struct { {setting_enum(vgm_color_mode, VgmColorModeCount)}, {setting_uint(vgm_color_fg, 0x0000, 0xFFFF)}, {setting_uint(vgm_color_bg, 0x0000, 0xFFFF)}, + {setting_enum(spoof_color, FuriHalVersionColorCount)}, }; void momentum_settings_load(void) { diff --git a/lib/nfc/SConscript b/lib/nfc/SConscript index 3ad62f322a..ff41a9bc93 100644 --- a/lib/nfc/SConscript +++ b/lib/nfc/SConscript @@ -25,6 +25,7 @@ env.Append( File("protocols/emv/emv.h"), File("protocols/slix/slix.h"), File("protocols/st25tb/st25tb.h"), + File("protocols/felica/felica.h"), # Pollers File("protocols/iso14443_3a/iso14443_3a_poller.h"), File("protocols/iso14443_3b/iso14443_3b_poller.h"), @@ -35,6 +36,7 @@ env.Append( File("protocols/mf_desfire/mf_desfire_poller.h"), File("protocols/emv/emv_poller.h"), File("protocols/st25tb/st25tb_poller.h"), + File("protocols/felica/felica_poller.h"), # Listeners File("protocols/iso14443_3a/iso14443_3a_listener.h"), File("protocols/iso14443_4a/iso14443_4a_listener.h"), @@ -50,6 +52,7 @@ env.Append( File("helpers/iso14443_crc.h"), File("helpers/iso13239_crc.h"), File("helpers/nfc_data_generator.h"), + File("helpers/crypto1.h"), ], ) diff --git a/lib/nfc/protocols/mf_classic/crypto1.c b/lib/nfc/helpers/crypto1.c similarity index 100% rename from lib/nfc/protocols/mf_classic/crypto1.c rename to lib/nfc/helpers/crypto1.c diff --git a/lib/nfc/protocols/mf_classic/crypto1.h b/lib/nfc/helpers/crypto1.h similarity index 100% rename from lib/nfc/protocols/mf_classic/crypto1.h rename to lib/nfc/helpers/crypto1.h diff --git a/lib/nfc/protocols/felica/felica.c b/lib/nfc/protocols/felica/felica.c index 7de1310bf9..bc47e06422 100644 --- a/lib/nfc/protocols/felica/felica.c +++ b/lib/nfc/protocols/felica/felica.c @@ -13,6 +13,14 @@ static const uint32_t felica_data_format_version = 1; +/** @brief This is used in felica_prepare_first_block to define which + * type of block needs to be prepared. +*/ +typedef enum { + FelicaMACTypeRead, + FelicaMACTypeWrite, +} FelicaMACType; + const NfcDeviceBase nfc_device_felica = { .protocol_name = FELICA_PROTOCOL_NAME, .alloc = (NfcDeviceAlloc)felica_alloc, @@ -35,18 +43,18 @@ FelicaData* felica_alloc(void) { } void felica_free(FelicaData* data) { - furi_assert(data); - + furi_check(data); free(data); } void felica_reset(FelicaData* data) { + furi_check(data); memset(data, 0, sizeof(FelicaData)); } void felica_copy(FelicaData* data, const FelicaData* other) { - furi_assert(data); - furi_assert(other); + furi_check(data); + furi_check(other); *data = *other; } @@ -59,7 +67,7 @@ bool felica_verify(FelicaData* data, const FuriString* device_type) { } bool felica_load(FelicaData* data, FlipperFormat* ff, uint32_t version) { - furi_assert(data); + furi_check(data); bool parsed = false; @@ -77,13 +85,32 @@ bool felica_load(FelicaData* data, FlipperFormat* ff, uint32_t version) { break; parsed = true; + uint32_t blocks_total = 0; + uint32_t blocks_read = 0; + if(!flipper_format_read_uint32(ff, "Blocks total", &blocks_total, 1)) break; + if(!flipper_format_read_uint32(ff, "Blocks read", &blocks_read, 1)) break; + data->blocks_total = (uint8_t)blocks_total; + data->blocks_read = (uint8_t)blocks_read; + + FuriString* temp_str = furi_string_alloc(); + for(uint8_t i = 0; i < data->blocks_total; i++) { + furi_string_printf(temp_str, "Block %d", i); + if(!flipper_format_read_hex( + ff, + furi_string_get_cstr(temp_str), + (&data->data.dump[i * sizeof(FelicaBlock)]), + sizeof(FelicaBlock))) { + parsed = false; + break; + } + } } while(false); return parsed; } bool felica_save(const FelicaData* data, FlipperFormat* ff) { - furi_assert(data); + furi_check(data); bool saved = false; @@ -98,15 +125,33 @@ bool felica_save(const FelicaData* data, FlipperFormat* ff) { ff, FELICA_MANUFACTURE_PARAMETER, data->pmm.data, FELICA_PMM_SIZE)) break; + uint32_t blocks_total = data->blocks_total; + uint32_t blocks_read = data->blocks_read; + if(!flipper_format_write_uint32(ff, "Blocks total", &blocks_total, 1)) break; + if(!flipper_format_write_uint32(ff, "Blocks read", &blocks_read, 1)) break; + saved = true; + FuriString* temp_str = furi_string_alloc(); + for(uint8_t i = 0; i < blocks_total; i++) { + furi_string_printf(temp_str, "Block %d", i); + if(!flipper_format_write_hex( + ff, + furi_string_get_cstr(temp_str), + (&data->data.dump[i * sizeof(FelicaBlock)]), + sizeof(FelicaBlock))) { + saved = false; + break; + } + } + furi_string_free(temp_str); } while(false); return saved; } bool felica_is_equal(const FelicaData* data, const FelicaData* other) { - furi_assert(data); - furi_assert(other); + furi_check(data); + furi_check(other); return memcmp(data, other, sizeof(FelicaData)) == 0; } @@ -119,7 +164,7 @@ const char* felica_get_device_name(const FelicaData* data, NfcDeviceNameType nam } const uint8_t* felica_get_uid(const FelicaData* data, size_t* uid_len) { - furi_assert(data); + furi_check(data); // Consider Manufacturer ID as UID if(uid_len) { @@ -130,7 +175,7 @@ const uint8_t* felica_get_uid(const FelicaData* data, size_t* uid_len) { } bool felica_set_uid(FelicaData* data, const uint8_t* uid, size_t uid_len) { - furi_assert(data); + furi_check(data); // Consider Manufacturer ID as UID const bool uid_valid = uid_len == FELICA_IDM_SIZE; @@ -145,3 +190,149 @@ FelicaData* felica_get_base_data(const FelicaData* data) { UNUSED(data); furi_crash("No base data"); } + +static void felica_reverse_copy_block(const uint8_t* array, uint8_t* reverse_array) { + furi_assert(array); + furi_assert(reverse_array); + + for(int i = 0; i < 8; i++) { + reverse_array[i] = array[7 - i]; + } +} + +void felica_calculate_session_key( + mbedtls_des3_context* ctx, + const uint8_t* ck, + const uint8_t* rc, + uint8_t* out) { + furi_check(ctx); + furi_check(ck); + furi_check(rc); + furi_check(out); + + uint8_t iv[8]; + memset(iv, 0, 8); + + uint8_t ck_reversed[16]; + felica_reverse_copy_block(ck, ck_reversed); + felica_reverse_copy_block(ck + 8, ck_reversed + 8); + + uint8_t rc_reversed[16]; + felica_reverse_copy_block(rc, rc_reversed); + felica_reverse_copy_block(rc + 8, rc_reversed + 8); + + mbedtls_des3_set2key_enc(ctx, ck_reversed); + mbedtls_des3_crypt_cbc(ctx, MBEDTLS_DES_ENCRYPT, FELICA_DATA_BLOCK_SIZE, iv, rc_reversed, out); +} + +static bool felica_calculate_mac( + mbedtls_des3_context* ctx, + const uint8_t* session_key, + const uint8_t* rc, + const uint8_t* first_block, + const uint8_t* data, + const size_t length, + uint8_t* mac) { + furi_check((length % 8) == 0); + + uint8_t reverse_data[8]; + uint8_t iv[8]; + uint8_t out[8]; + mbedtls_des3_set2key_enc(ctx, session_key); + + felica_reverse_copy_block(rc, iv); + felica_reverse_copy_block(first_block, reverse_data); + uint8_t i = 0; + bool error = false; + do { + if(mbedtls_des3_crypt_cbc(ctx, MBEDTLS_DES_ENCRYPT, 8, iv, reverse_data, out) == 0) { + memcpy(iv, out, sizeof(iv)); + felica_reverse_copy_block(data + i, reverse_data); + i += 8; + } else { + error = true; + break; + } + } while(i <= length); + + if(!error) { + felica_reverse_copy_block(out, mac); + } + return !error; +} + +static void felica_prepare_first_block( + FelicaMACType operation_type, + const uint8_t* blocks, + const uint8_t block_count, + uint8_t* out) { + furi_check(blocks); + furi_check(out); + if(operation_type == FelicaMACTypeRead) { + memset(out, 0xFF, 8); + for(uint8_t i = 0, j = 0; i < block_count; i++, j += 2) { + out[j] = blocks[i]; + out[j + 1] = 0; + } + } else { + furi_check(block_count == 4); + memset(out, 0, 8); + out[0] = blocks[0]; + out[1] = blocks[1]; + out[2] = blocks[2]; + out[4] = blocks[3]; + out[6] = FELICA_BLOCK_INDEX_MAC_A; + } +} + +bool felica_check_mac( + mbedtls_des3_context* ctx, + const uint8_t* session_key, + const uint8_t* rc, + const uint8_t* blocks, + const uint8_t block_count, + uint8_t* data) { + furi_check(ctx); + furi_check(session_key); + furi_check(rc); + furi_check(blocks); + furi_check(data); + + uint8_t first_block[8]; + uint8_t mac[8]; + felica_prepare_first_block(FelicaMACTypeRead, blocks, block_count, first_block); + + uint8_t data_size_without_mac = FELICA_DATA_BLOCK_SIZE * (block_count - 1); + felica_calculate_mac(ctx, session_key, rc, first_block, data, data_size_without_mac, mac); + + uint8_t* mac_ptr = data + data_size_without_mac; + return !memcmp(mac, mac_ptr, 8); +} + +void felica_calculate_mac_write( + mbedtls_des3_context* ctx, + const uint8_t* session_key, + const uint8_t* rc, + const uint8_t* wcnt, + const uint8_t* data, + uint8_t* mac) { + furi_check(ctx); + furi_check(session_key); + furi_check(rc); + furi_check(wcnt); + furi_check(data); + furi_check(mac); + + const uint8_t WCNT_length = 3; + uint8_t block_data[WCNT_length + 1]; + uint8_t first_block[8]; + + memcpy(block_data, wcnt, WCNT_length); + block_data[3] = FELICA_BLOCK_INDEX_STATE; + felica_prepare_first_block(FelicaMACTypeWrite, block_data, WCNT_length + 1, first_block); + + uint8_t session_swapped[FELICA_DATA_BLOCK_SIZE]; + memcpy(session_swapped, session_key + 8, 8); + memcpy(session_swapped + 8, session_key, 8); + felica_calculate_mac(ctx, session_swapped, rc, first_block, data, FELICA_DATA_BLOCK_SIZE, mac); +} \ No newline at end of file diff --git a/lib/nfc/protocols/felica/felica.h b/lib/nfc/protocols/felica/felica.h index e1820d4dc5..d032943d35 100644 --- a/lib/nfc/protocols/felica/felica.h +++ b/lib/nfc/protocols/felica/felica.h @@ -2,6 +2,7 @@ #include #include +#include #ifdef __cplusplus extern "C" { @@ -9,6 +10,23 @@ extern "C" { #define FELICA_IDM_SIZE (8U) #define FELICA_PMM_SIZE (8U) +#define FELICA_DATA_BLOCK_SIZE (16U) + +#define FELICA_BLOCKS_TOTAL_COUNT (27U) +#define FELICA_BLOCK_INDEX_REG (0x0EU) +#define FELICA_BLOCK_INDEX_RC (0x80U) +#define FELICA_BLOCK_INDEX_MAC (0x81U) +#define FELICA_BLOCK_INDEX_ID (0x82U) +#define FELICA_BLOCK_INDEX_D_ID (0x83U) +#define FELICA_BLOCK_INDEX_SER_C (0x84U) +#define FELICA_BLOCK_INDEX_SYS_C (0x85U) +#define FELICA_BLOCK_INDEX_CKV (0x86U) +#define FELICA_BLOCK_INDEX_CK (0x87U) +#define FELICA_BLOCK_INDEX_MC (0x88U) +#define FELICA_BLOCK_INDEX_WCNT (0x90U) +#define FELICA_BLOCK_INDEX_MAC_A (0x91U) +#define FELICA_BLOCK_INDEX_STATE (0x92U) +#define FELICA_BLOCK_INDEX_CRC_CHECK (0xA0U) #define FELICA_GUARD_TIME_US (20000U) #define FELICA_FDT_POLL_FC (10000U) @@ -23,6 +41,7 @@ extern "C" { #define FELICA_TIME_SLOT_8 (0x07U) #define FELICA_TIME_SLOT_16 (0x0FU) +/** @brief Type of possible Felica errors */ typedef enum { FelicaErrorNone, FelicaErrorNotPresent, @@ -35,17 +54,78 @@ typedef enum { FelicaErrorTimeout, } FelicaError; +/** @brief Separate type for card key block. Used in authentication process */ +typedef struct { + uint8_t data[FELICA_DATA_BLOCK_SIZE]; +} FelicaCardKey; + +/** @brief In Felica there two types of auth. Internal is the first one, after + * which external became possible. Here are two flags representing which one + * was passed */ +typedef struct { + bool internal : 1; + bool external : 1; +} FelicaAuthenticationStatus; + +/** @brief Struct which controls the process of authentication and can be passed as + * a parameter to the application level. In order to force user to fill card key block data. */ +typedef struct { + bool skip_auth; /**< By default it is true, so auth is skipped. By setting this to false several auth steps will be performed in order to pass auth*/ + FelicaCardKey + card_key; /**< User must fill this field with known card key in order to pass auth*/ + FelicaAuthenticationStatus auth_status; /**< Authentication status*/ +} FelicaAuthenticationContext; + +/** @brief Felica ID block */ typedef struct { uint8_t data[FELICA_IDM_SIZE]; } FelicaIDm; +/** @brief Felica PMm block */ typedef struct { uint8_t data[FELICA_PMM_SIZE]; } FelicaPMm; +/** @brief Felica block with status flags indicating last operation with it. + * See Felica manual for more details on status codes. */ +typedef struct { + uint8_t SF1; /**< Status flag 1, equals to 0 when success*/ + uint8_t SF2; /**< Status flag 2, equals to 0 when success*/ + uint8_t data[FELICA_DATA_BLOCK_SIZE]; /**< Block data */ +} FelicaBlock; + +/** @brief Felica filesystem structure */ +typedef struct { + FelicaBlock spad[14]; + FelicaBlock reg; + FelicaBlock rc; + FelicaBlock mac; + FelicaBlock id; + FelicaBlock d_id; + FelicaBlock ser_c; + FelicaBlock sys_c; + FelicaBlock ckv; + FelicaBlock ck; + FelicaBlock mc; + FelicaBlock wcnt; + FelicaBlock mac_a; + FelicaBlock state; + FelicaBlock crc_check; +} FelicaFileSystem; + +/** @brief Union which represents filesystem in junction with plain data dump */ +typedef union { + FelicaFileSystem fs; + uint8_t dump[sizeof(FelicaFileSystem)]; +} FelicaFSUnion; + +/** @brief Structure used to store Felica data and additional values about reading */ typedef struct { FelicaIDm idm; FelicaPMm pmm; + uint8_t blocks_total; + uint8_t blocks_read; + FelicaFSUnion data; } FelicaData; extern const NfcDeviceBase nfc_device_felica; @@ -74,6 +154,27 @@ bool felica_set_uid(FelicaData* data, const uint8_t* uid, size_t uid_len); FelicaData* felica_get_base_data(const FelicaData* data); +void felica_calculate_session_key( + mbedtls_des3_context* ctx, + const uint8_t* ck, + const uint8_t* rc, + uint8_t* out); + +bool felica_check_mac( + mbedtls_des3_context* ctx, + const uint8_t* session_key, + const uint8_t* rc, + const uint8_t* blocks, + const uint8_t block_count, + uint8_t* data); + +void felica_calculate_mac_write( + mbedtls_des3_context* ctx, + const uint8_t* session_key, + const uint8_t* rc, + const uint8_t* wcnt, + const uint8_t* data, + uint8_t* mac); #ifdef __cplusplus } #endif diff --git a/lib/nfc/protocols/felica/felica_poller.c b/lib/nfc/protocols/felica/felica_poller.c index 23b1604e19..720daf2384 100644 --- a/lib/nfc/protocols/felica/felica_poller.c +++ b/lib/nfc/protocols/felica/felica_poller.c @@ -3,6 +3,11 @@ #include #include +#include + +#define TAG "FelicaPoller" + +typedef NfcCommand (*FelicaPollerReadHandler)(FelicaPoller* instance); const FelicaData* felica_poller_get_data(FelicaPoller* instance) { furi_assert(instance); @@ -23,6 +28,8 @@ static FelicaPoller* felica_poller_alloc(Nfc* nfc) { nfc_set_guard_time_us(instance->nfc, FELICA_GUARD_TIME_US); nfc_set_fdt_poll_fc(instance->nfc, FELICA_FDT_POLL_FC); nfc_set_fdt_poll_poll_us(instance->nfc, FELICA_POLL_POLL_MIN_US); + + mbedtls_des3_init(&instance->auth.des_context); instance->data = felica_alloc(); instance->felica_event.data = &instance->felica_event_data; @@ -40,6 +47,7 @@ static void felica_poller_free(FelicaPoller* instance) { furi_assert(instance->rx_buffer); furi_assert(instance->data); + mbedtls_des3_free(&instance->auth.des_context); bit_buffer_free(instance->tx_buffer); bit_buffer_free(instance->rx_buffer); felica_free(instance->data); @@ -55,6 +63,212 @@ static void instance->context = context; } +NfcCommand felica_poller_state_handler_idle(FelicaPoller* instance) { + FURI_LOG_D(TAG, "Idle"); + felica_reset(instance->data); + instance->state = FelicaPollerStateActivated; + + return NfcCommandContinue; +} + +NfcCommand felica_poller_state_handler_activate(FelicaPoller* instance) { + FURI_LOG_D(TAG, "Activate"); + + NfcCommand command = NfcCommandContinue; + + FelicaError error = felica_poller_activate(instance, instance->data); + if(error == FelicaErrorNone) { + furi_hal_random_fill_buf(instance->data->data.fs.rc.data, FELICA_DATA_BLOCK_SIZE); + + instance->felica_event.type = FelicaPollerEventTypeRequestAuthContext; + instance->felica_event_data.auth_context = &instance->auth.context; + + instance->callback(instance->general_event, instance->context); + + bool skip_auth = instance->auth.context.skip_auth; + instance->state = skip_auth ? FelicaPollerStateReadBlocks : + FelicaPollerStateAuthenticateInternal; + } else if(error != FelicaErrorTimeout) { + instance->felica_event.type = FelicaPollerEventTypeError; + instance->felica_event_data.error = error; + instance->state = FelicaPollerStateReadFailed; + } + return command; +} + +NfcCommand felica_poller_state_handler_auth_internal(FelicaPoller* instance) { + FURI_LOG_D(TAG, "Auth Internal"); + + felica_calculate_session_key( + &instance->auth.des_context, + instance->auth.context.card_key.data, + instance->data->data.fs.rc.data, + instance->auth.session_key.data); + + instance->state = FelicaPollerStateReadBlocks; + + uint8_t blocks[3] = {FELICA_BLOCK_INDEX_RC, 0, 0}; + FelicaPollerWriteCommandResponse* tx_resp; + do { + FelicaError error = felica_poller_write_blocks( + instance, 1, blocks, instance->data->data.fs.rc.data, &tx_resp); + if((error != FelicaErrorNone) || (tx_resp->SF1 != 0) || (tx_resp->SF2 != 0)) break; + + blocks[0] = FELICA_BLOCK_INDEX_ID; + blocks[1] = FELICA_BLOCK_INDEX_WCNT; + blocks[2] = FELICA_BLOCK_INDEX_MAC_A; + FelicaPollerReadCommandResponse* rx_resp; + error = felica_poller_read_blocks(instance, sizeof(blocks), blocks, &rx_resp); + if(error != FelicaErrorNone || rx_resp->SF1 != 0 || rx_resp->SF2 != 0) break; + + if(felica_check_mac( + &instance->auth.des_context, + instance->auth.session_key.data, + instance->data->data.fs.rc.data, + blocks, + rx_resp->block_count, + rx_resp->data)) { + instance->auth.context.auth_status.internal = true; + instance->data->data.fs.wcnt.SF1 = 0; + instance->data->data.fs.wcnt.SF2 = 0; + memcpy( + instance->data->data.fs.wcnt.data, + rx_resp->data + FELICA_DATA_BLOCK_SIZE, + FELICA_DATA_BLOCK_SIZE); + instance->state = FelicaPollerStateAuthenticateExternal; + } + } while(false); + + return NfcCommandContinue; +} + +NfcCommand felica_poller_state_handler_auth_external(FelicaPoller* instance) { + FURI_LOG_D(TAG, "Auth External"); + instance->state = FelicaPollerStateReadBlocks; + uint8_t blocks[2]; + + instance->data->data.fs.state.data[0] = 1; + FelicaAuthentication* auth = &instance->auth; + felica_calculate_mac_write( + &auth->des_context, + auth->session_key.data, + instance->data->data.fs.rc.data, + instance->data->data.fs.wcnt.data, + instance->data->data.fs.state.data, + instance->data->data.fs.mac_a.data); + + memcpy(instance->data->data.fs.mac_a.data + 8, instance->data->data.fs.wcnt.data, 3); //-V1086 + + uint8_t tx_data[FELICA_DATA_BLOCK_SIZE * 2]; + memcpy(tx_data, instance->data->data.fs.state.data, FELICA_DATA_BLOCK_SIZE); + memcpy( + tx_data + FELICA_DATA_BLOCK_SIZE, + instance->data->data.fs.mac_a.data, + FELICA_DATA_BLOCK_SIZE); + + do { + blocks[0] = FELICA_BLOCK_INDEX_STATE; + blocks[1] = FELICA_BLOCK_INDEX_MAC_A; + FelicaPollerWriteCommandResponse* tx_resp; + FelicaError error = felica_poller_write_blocks(instance, 2, blocks, tx_data, &tx_resp); + if(error != FelicaErrorNone || tx_resp->SF1 != 0 || tx_resp->SF2 != 0) break; + + FelicaPollerReadCommandResponse* rx_resp; + error = felica_poller_read_blocks(instance, 1, blocks, &rx_resp); + if(error != FelicaErrorNone || rx_resp->SF1 != 0 || rx_resp->SF2 != 0) break; + + instance->data->data.fs.state.SF1 = 0; + instance->data->data.fs.state.SF2 = 0; + memcpy(instance->data->data.fs.state.data, rx_resp->data, FELICA_DATA_BLOCK_SIZE); + instance->auth.context.auth_status.external = instance->data->data.fs.state.data[0]; + } while(false); + instance->state = FelicaPollerStateReadBlocks; + return NfcCommandContinue; +} + +NfcCommand felica_poller_state_handler_read_blocks(FelicaPoller* instance) { + FURI_LOG_D(TAG, "Read Blocks"); + + uint8_t block_count = 1; + uint8_t block_list[4] = {0, 0, 0, 0}; + block_list[0] = instance->block_index; + + instance->block_index++; + if(instance->block_index == FELICA_BLOCK_INDEX_REG + 1) { + instance->block_index = FELICA_BLOCK_INDEX_RC; + } else if(instance->block_index == FELICA_BLOCK_INDEX_MC + 1) { + instance->block_index = FELICA_BLOCK_INDEX_WCNT; + } else if(instance->block_index == FELICA_BLOCK_INDEX_STATE + 1) { + instance->block_index = FELICA_BLOCK_INDEX_CRC_CHECK; + } + + FelicaPollerReadCommandResponse* response; + FelicaError error = felica_poller_read_blocks(instance, block_count, block_list, &response); + if(error == FelicaErrorNone) { + block_count = (response->SF1 == 0) ? response->block_count : block_count; + uint8_t* data_ptr = + instance->data->data.dump + instance->data->blocks_total * sizeof(FelicaBlock); + + *data_ptr++ = response->SF1; + *data_ptr++ = response->SF2; + + if(response->SF1 == 0) { + uint8_t* response_data_ptr = response->data; + instance->data->blocks_read++; + memcpy(data_ptr, response_data_ptr, FELICA_DATA_BLOCK_SIZE); + } else { + memset(data_ptr, 0, FELICA_DATA_BLOCK_SIZE); + } + instance->data->blocks_total++; + + if(instance->data->blocks_total == FELICA_BLOCKS_TOTAL_COUNT) { + instance->state = FelicaPollerStateReadSuccess; + } + } else { + instance->felica_event.type = FelicaPollerEventTypeError; + instance->felica_event_data.error = error; + instance->state = FelicaPollerStateReadFailed; + } + + return NfcCommandContinue; +} + +NfcCommand felica_poller_state_handler_read_success(FelicaPoller* instance) { + FURI_LOG_D(TAG, "Read Success"); + + if(!instance->auth.context.auth_status.internal || + !instance->auth.context.auth_status.external) { + instance->data->blocks_read--; + instance->felica_event.type = FelicaPollerEventTypeIncomplete; + } else { + memcpy( + instance->data->data.fs.ck.data, + instance->auth.context.card_key.data, + FELICA_DATA_BLOCK_SIZE); + instance->felica_event.type = FelicaPollerEventTypeReady; + } + + instance->felica_event_data.error = FelicaErrorNone; + return instance->callback(instance->general_event, instance->context); +} + +NfcCommand felica_poller_state_handler_read_failed(FelicaPoller* instance) { + FURI_LOG_D(TAG, "Read Fail"); + instance->callback(instance->general_event, instance->context); + + return NfcCommandStop; +} + +static const FelicaPollerReadHandler felica_poller_handler[FelicaPollerStateNum] = { + [FelicaPollerStateIdle] = felica_poller_state_handler_idle, + [FelicaPollerStateActivated] = felica_poller_state_handler_activate, + [FelicaPollerStateAuthenticateInternal] = felica_poller_state_handler_auth_internal, + [FelicaPollerStateAuthenticateExternal] = felica_poller_state_handler_auth_external, + [FelicaPollerStateReadBlocks] = felica_poller_state_handler_read_blocks, + [FelicaPollerStateReadSuccess] = felica_poller_state_handler_read_success, + [FelicaPollerStateReadFailed] = felica_poller_state_handler_read_failed, +}; + static NfcCommand felica_poller_run(NfcGenericEvent event, void* context) { furi_assert(context); furi_assert(event.protocol == NfcProtocolInvalid); @@ -65,24 +279,7 @@ static NfcCommand felica_poller_run(NfcGenericEvent event, void* context) { NfcCommand command = NfcCommandContinue; if(nfc_event->type == NfcEventTypePollerReady) { - if(instance->state != FelicaPollerStateActivated) { - FelicaError error = felica_poller_activate(instance, instance->data); - if(error == FelicaErrorNone) { - instance->felica_event.type = FelicaPollerEventTypeReady; - instance->felica_event_data.error = error; - command = instance->callback(instance->general_event, instance->context); - } else { - instance->felica_event.type = FelicaPollerEventTypeError; - instance->felica_event_data.error = error; - command = instance->callback(instance->general_event, instance->context); - // Add delay to switch context - furi_delay_ms(100); - } - } else { - instance->felica_event.type = FelicaPollerEventTypeReady; - instance->felica_event_data.error = FelicaErrorNone; - command = instance->callback(instance->general_event, instance->context); - } + command = felica_poller_handler[instance->state](instance); } return command; diff --git a/lib/nfc/protocols/felica/felica_poller.h b/lib/nfc/protocols/felica/felica_poller.h index b0e6778a0f..d4366e7672 100644 --- a/lib/nfc/protocols/felica/felica_poller.h +++ b/lib/nfc/protocols/felica/felica_poller.h @@ -19,14 +19,33 @@ typedef struct FelicaPoller FelicaPoller; */ typedef enum { FelicaPollerEventTypeError, /**< An error occured during activation procedure. */ - FelicaPollerEventTypeReady, /**< The card was activated by the poller. */ + FelicaPollerEventTypeReady, /**< The card was activated and fully read by the poller. */ + FelicaPollerEventTypeIncomplete, /**< The card was activated and partly read by the poller. */ + FelicaPollerEventTypeRequestAuthContext, /**< Authentication context was requested by poller. */ } FelicaPollerEventType; +/** + * @brief Stucture for holding Felica session key which is calculated from rc and ck. +*/ +typedef struct { + uint8_t data[FELICA_DATA_BLOCK_SIZE]; +} FelicaSessionKey; + +/** + * @brief Structure used to hold authentication related fields. +*/ +typedef struct { + mbedtls_des3_context des_context; /**< Context for mbedtls des functions. */ + FelicaSessionKey session_key; /**< Calculated session key. */ + FelicaAuthenticationContext context; /**< Public auth context provided to upper levels. */ +} FelicaAuthentication; + /** * @brief Felica poller event data. */ typedef union { FelicaError error; /**< Error code indicating card activation fail reason. */ + FelicaAuthenticationContext* auth_context; /**< Authentication context to be filled by user. */ } FelicaPollerEventData; /** diff --git a/lib/nfc/protocols/felica/felica_poller_i.c b/lib/nfc/protocols/felica/felica_poller_i.c index bfbf150ef9..f7726be32c 100644 --- a/lib/nfc/protocols/felica/felica_poller_i.c +++ b/lib/nfc/protocols/felica/felica_poller_i.c @@ -3,6 +3,11 @@ #include #define TAG "FelicaPoller" +#define FELICA_CMD_READ_WITHOUT_ENCRYPTION (0x06U) +#define FELICA_CMD_WRITE_WITHOUT_ENCRYPTION (0x08U) + +#define FELICA_SERVICE_RW_ACCESS (0x0009U) +#define FELICA_SERVICE_RO_ACCESS (0x000BU) static FelicaError felica_poller_process_error(NfcError error) { switch(error) { @@ -15,8 +20,8 @@ static FelicaError felica_poller_process_error(NfcError error) { } } -static FelicaError felica_poller_frame_exchange( - FelicaPoller* instance, +FelicaError felica_poller_frame_exchange( + const FelicaPoller* instance, const BitBuffer* tx_buffer, BitBuffer* rx_buffer, uint32_t fwt) { @@ -93,10 +98,104 @@ FelicaError felica_poller_polling( return error; } -FelicaError felica_poller_activate(FelicaPoller* instance, FelicaData* data) { +static void felica_poller_prepare_tx_buffer( + const FelicaPoller* instance, + const uint8_t command, + const uint16_t service_code, + const uint8_t block_count, + const uint8_t* const blocks, + const uint8_t data_block_count, + const uint8_t* data) { + FelicaCommandHeader cmd = { + .code = command, + .idm = instance->data->idm, + .service_num = 1, + .service_code = service_code, + .block_count = block_count, + }; + + FelicaBlockListElement block_list[4] = {{0}, {0}, {0}, {0}}; + for(uint8_t i = 0; i < block_count; i++) { + block_list[i].length = 1; + block_list[i].block_number = blocks[i]; + } + + uint8_t block_list_count = block_count; + uint8_t block_list_size = block_list_count * sizeof(FelicaBlockListElement); + uint8_t total_size = sizeof(FelicaCommandHeader) + 1 + block_list_size + + data_block_count * FELICA_DATA_BLOCK_SIZE; + bit_buffer_reset(instance->tx_buffer); + bit_buffer_append_byte(instance->tx_buffer, total_size); + bit_buffer_append_bytes(instance->tx_buffer, (uint8_t*)&cmd, sizeof(FelicaCommandHeader)); + bit_buffer_append_bytes(instance->tx_buffer, (uint8_t*)&block_list, block_list_size); + + if(data_block_count != 0) { + bit_buffer_append_bytes( + instance->tx_buffer, data, data_block_count * FELICA_DATA_BLOCK_SIZE); + } +} + +FelicaError felica_poller_read_blocks( + FelicaPoller* instance, + const uint8_t block_count, + const uint8_t* const block_numbers, + FelicaPollerReadCommandResponse** const response_ptr) { furi_assert(instance); + furi_assert(block_count <= 4); + furi_assert(block_numbers); + furi_assert(response_ptr); + + felica_poller_prepare_tx_buffer( + instance, + FELICA_CMD_READ_WITHOUT_ENCRYPTION, + FELICA_SERVICE_RO_ACCESS, + block_count, + block_numbers, + 0, + NULL); + bit_buffer_reset(instance->rx_buffer); + + FelicaError error = felica_poller_frame_exchange( + instance, instance->tx_buffer, instance->rx_buffer, FELICA_POLLER_POLLING_FWT); + if(error == FelicaErrorNone) { + *response_ptr = (FelicaPollerReadCommandResponse*)bit_buffer_get_data(instance->rx_buffer); + } + return error; +} + +FelicaError felica_poller_write_blocks( + const FelicaPoller* instance, + const uint8_t block_count, + const uint8_t* const block_numbers, + const uint8_t* data, + FelicaPollerWriteCommandResponse** const response_ptr) { + furi_assert(instance); + furi_assert(block_count <= 2); + furi_assert(block_numbers); + furi_assert(data); + furi_assert(response_ptr); + + felica_poller_prepare_tx_buffer( + instance, + FELICA_CMD_WRITE_WITHOUT_ENCRYPTION, + FELICA_SERVICE_RW_ACCESS, + block_count, + block_numbers, + block_count, + data); + bit_buffer_reset(instance->rx_buffer); + + FelicaError error = felica_poller_frame_exchange( + instance, instance->tx_buffer, instance->rx_buffer, FELICA_POLLER_POLLING_FWT); + if(error == FelicaErrorNone) { + *response_ptr = + (FelicaPollerWriteCommandResponse*)bit_buffer_get_data(instance->rx_buffer); + } + return error; +} - felica_reset(data); +FelicaError felica_poller_activate(FelicaPoller* instance, FelicaData* data) { + furi_assert(instance); FelicaError ret; diff --git a/lib/nfc/protocols/felica/felica_poller_i.h b/lib/nfc/protocols/felica/felica_poller_i.h index 3bd4d91f9f..f7df4c8450 100644 --- a/lib/nfc/protocols/felica/felica_poller_i.h +++ b/lib/nfc/protocols/felica/felica_poller_i.h @@ -1,7 +1,6 @@ #pragma once #include "felica_poller.h" - #include #ifdef __cplusplus @@ -18,11 +17,20 @@ extern "C" { typedef enum { FelicaPollerStateIdle, FelicaPollerStateActivated, + FelicaPollerStateAuthenticateInternal, + FelicaPollerStateAuthenticateExternal, + FelicaPollerStateReadBlocks, + FelicaPollerStateReadSuccess, + FelicaPollerStateReadFailed, + + FelicaPollerStateNum } FelicaPollerState; struct FelicaPoller { Nfc* nfc; FelicaPollerState state; + FelicaAuthentication auth; + FelicaData* data; BitBuffer* tx_buffer; BitBuffer* rx_buffer; @@ -31,6 +39,7 @@ struct FelicaPoller { FelicaPollerEvent felica_event; FelicaPollerEventData felica_event_data; NfcGenericCallback callback; + uint8_t block_index; void* context; }; @@ -46,13 +55,106 @@ typedef struct { uint8_t request_data[2]; } FelicaPollerPollingResponse; +typedef struct { + uint8_t service_code : 4; + uint8_t access_mode : 3; + uint8_t length : 1; + uint8_t block_number; +} FelicaBlockListElement; + +#pragma pack(push, 1) +typedef struct { + uint8_t code; + FelicaIDm idm; + uint8_t service_num; + uint16_t service_code; + uint8_t block_count; +} FelicaCommandHeader; +#pragma pack(pop) + +typedef struct { + uint8_t length; + uint8_t response_code; + FelicaIDm idm; + uint8_t SF1; + uint8_t SF2; + uint8_t block_count; + uint8_t data[]; +} FelicaPollerReadCommandResponse; + +typedef struct { + uint8_t length; + uint8_t response_code; + FelicaIDm idm; + uint8_t SF1; + uint8_t SF2; +} FelicaPollerWriteCommandResponse; + const FelicaData* felica_poller_get_data(FelicaPoller* instance); +/** + * @brief Performs felica polling operation as part of the activation process + * + * @param[in, out] instance pointer to the instance to be used in the transaction. + * @param[in] cmd Pointer to polling command structure + * @param[out] resp Pointer to the response structure + * @return FelicaErrorNone on success, an error code on failure +*/ FelicaError felica_poller_polling( FelicaPoller* instance, const FelicaPollerPollingCommand* cmd, FelicaPollerPollingResponse* resp); +/** + * @brief Performs felica read operation for blocks provided as parameters + * + * @param[in, out] instance pointer to the instance to be used in the transaction. + * @param[in] block_count Amount of blocks involved in reading procedure + * @param[in] block_numbers Array with block indexes according to felica docs + * @param[out] response_ptr Pointer to the response structure + * @return FelicaErrorNone on success, an error code on failure. +*/ +FelicaError felica_poller_read_blocks( + FelicaPoller* instance, + const uint8_t block_count, + const uint8_t* const block_numbers, + FelicaPollerReadCommandResponse** const response_ptr); + +/** + * @brief Performs felica write operation with data provided as parameters + * + * @param[in, out] instance pointer to the instance to be used in the transaction. + * @param[in] block_count Amount of blocks involved in writing procedure + * @param[in] block_numbers Array with block indexes according to felica docs + * @param[in] data Data of blocks provided in block_numbers + * @param[out] response_ptr Pointer to the response structure + * @return FelicaErrorNone on success, an error code on failure. +*/ +FelicaError felica_poller_write_blocks( + const FelicaPoller* instance, + const uint8_t block_count, + const uint8_t* const block_numbers, + const uint8_t* data, + FelicaPollerWriteCommandResponse** const response_ptr); + +/** + * @brief Perform frame exchange procedure. + * + * Prepares data for sending by adding crc, after that performs + * low level calls to send package data to the card + * + * @param[in, out] instance pointer to the instance to be used in the transaction. + * @param[in] tx_buffer pointer to the buffer with data to be transmitted + * @param[out] rx_buffer pointer to the buffer with received data from card + * @param[in] fwt timeout window + * @return FelicaErrorNone on success, an error code on failure. + */ +FelicaError felica_poller_frame_exchange( + const FelicaPoller* instance, + const BitBuffer* tx_buffer, + BitBuffer* rx_buffer, + uint32_t fwt); + #ifdef __cplusplus } #endif diff --git a/lib/nfc/protocols/mf_classic/mf_classic_listener_i.h b/lib/nfc/protocols/mf_classic/mf_classic_listener_i.h index 5269743b5c..af22b5234f 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_listener_i.h +++ b/lib/nfc/protocols/mf_classic/mf_classic_listener_i.h @@ -3,7 +3,7 @@ #include "mf_classic_listener.h" #include #include -#include "crypto1.h" +#include #ifdef __cplusplus extern "C" { diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller.h b/lib/nfc/protocols/mf_classic/mf_classic_poller.h index 19e5257017..518d029d07 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller.h +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller.h @@ -315,6 +315,89 @@ MfClassicError mf_classic_poller_value_cmd( */ MfClassicError mf_classic_poller_value_transfer(MfClassicPoller* instance, uint8_t block_num); +/** + * @brief Transmit and receive Iso14443_3a standard frames in poller mode. + * + * Must ONLY be used inside the callback function. + * + * The rx_buffer will be filled with any data received as a response to data + * sent from tx_buffer, with a timeout defined by the fwt parameter. + * + * @param[in, out] instance pointer to the instance to be used in the transaction. + * @param[in] tx_buffer pointer to the buffer containing the data to be transmitted. + * @param[out] rx_buffer pointer to the buffer to be filled with received data. + * @param[in] fwt frame wait time (response timeout), in carrier cycles. + * @return MfClassicErrorNone on success, an error code on failure. + */ +MfClassicError mf_classic_poller_send_standard_frame( + MfClassicPoller* instance, + const BitBuffer* tx_buffer, + BitBuffer* rx_buffer, + uint32_t fwt_fc); + +/** + * @brief Transmit and receive Iso14443_3a frames in poller mode. + * + * Must ONLY be used inside the callback function. + * + * The rx_buffer will be filled with any data received as a response to data + * sent from tx_buffer, with a timeout defined by the fwt parameter. + * + * @param[in, out] instance pointer to the instance to be used in the transaction. + * @param[in] tx_buffer pointer to the buffer containing the data to be transmitted. + * @param[out] rx_buffer pointer to the buffer to be filled with received data. + * @param[in] fwt frame wait time (response timeout), in carrier cycles. + * @return MfClassicErrorNone on success, an error code on failure. + */ +MfClassicError mf_classic_poller_send_frame( + MfClassicPoller* instance, + const BitBuffer* tx_buffer, + BitBuffer* rx_buffer, + uint32_t fwt_fc); + +/** + * @brief Transmit and receive Iso14443_3a frames with custom parity bits in poller mode. + * + * Must ONLY be used inside the callback function. + * + * The rx_buffer will be filled with any data received as a response to data + * sent from tx_buffer, with a timeout defined by the fwt parameter. + * + * Custom parity bits must be set in the tx_buffer. The rx_buffer will contain + * the received data with the parity bits. + * + * @param[in, out] instance pointer to the instance to be used in the transaction. + * @param[in] tx_buffer pointer to the buffer containing the data to be transmitted. + * @param[out] rx_buffer pointer to the buffer to be filled with received data. + * @param[in] fwt frame wait time (response timeout), in carrier cycles. + * @return MfClassicErrorNone on success, an error code on failure. + */ +MfClassicError mf_classic_poller_send_custom_parity_frame( + MfClassicPoller* instance, + const BitBuffer* tx_buffer, + BitBuffer* rx_buffer, + uint32_t fwt_fc); + +/** + * @brief Transmit and receive Mifare Classic encrypted frames with custom parity bits in poller mode. + * + * Must ONLY be used inside the callback function. + * + * The rx_buffer will be filled with any data received as a response to data + * sent from tx_buffer, with a timeout defined by the fwt parameter. + * + * @param[in, out] instance pointer to the instance to be used in the transaction. + * @param[in] tx_buffer pointer to the buffer containing the plain data to be transmitted. + * @param[out] rx_buffer pointer to the buffer to be filled with decyphered received data. + * @param[in] fwt frame wait time (response timeout), in carrier cycles. + * @return MfClassicErrorNone on success, an error code on failure. + */ +MfClassicError mf_classic_poller_send_encrypted_frame( + MfClassicPoller* instance, + const BitBuffer* tx_buffer, + BitBuffer* rx_buffer, + uint32_t fwt_fc); + #ifdef __cplusplus } #endif diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller_i.c b/lib/nfc/protocols/mf_classic/mf_classic_poller_i.c index 2b01e74eed..949ef8e66e 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller_i.c +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller_i.c @@ -465,3 +465,77 @@ MfClassicError mf_classic_poller_value_transfer(MfClassicPoller* instance, uint8 return ret; } + +MfClassicError mf_classic_poller_send_standard_frame( + MfClassicPoller* instance, + const BitBuffer* tx_buffer, + BitBuffer* rx_buffer, + uint32_t fwt_fc) { + furi_check(instance); + furi_check(tx_buffer); + furi_check(rx_buffer); + + Iso14443_3aError error = iso14443_3a_poller_send_standard_frame( + instance->iso14443_3a_poller, tx_buffer, rx_buffer, fwt_fc); + + return mf_classic_process_error(error); +} + +MfClassicError mf_classic_poller_send_frame( + MfClassicPoller* instance, + const BitBuffer* tx_buffer, + BitBuffer* rx_buffer, + uint32_t fwt_fc) { + furi_check(instance); + furi_check(tx_buffer); + furi_check(rx_buffer); + + Iso14443_3aError error = + iso14443_3a_poller_txrx(instance->iso14443_3a_poller, tx_buffer, rx_buffer, fwt_fc); + + return mf_classic_process_error(error); +} + +MfClassicError mf_classic_poller_send_custom_parity_frame( + MfClassicPoller* instance, + const BitBuffer* tx_buffer, + BitBuffer* rx_buffer, + uint32_t fwt_fc) { + furi_check(instance); + furi_check(tx_buffer); + furi_check(rx_buffer); + + Iso14443_3aError error = iso14443_3a_poller_txrx_custom_parity( + instance->iso14443_3a_poller, tx_buffer, rx_buffer, fwt_fc); + + return mf_classic_process_error(error); +} + +MfClassicError mf_classic_poller_send_encrypted_frame( + MfClassicPoller* instance, + const BitBuffer* tx_buffer, + BitBuffer* rx_buffer, + uint32_t fwt_fc) { + furi_check(instance); + furi_check(tx_buffer); + furi_check(rx_buffer); + + MfClassicError ret = MfClassicErrorNone; + do { + crypto1_encrypt(instance->crypto, NULL, tx_buffer, instance->tx_encrypted_buffer); + + Iso14443_3aError error = iso14443_3a_poller_txrx_custom_parity( + instance->iso14443_3a_poller, + instance->tx_encrypted_buffer, + instance->rx_encrypted_buffer, + fwt_fc); + if(error != Iso14443_3aErrorNone) { + ret = mf_classic_process_error(error); + break; + } + + crypto1_decrypt(instance->crypto, instance->rx_encrypted_buffer, rx_buffer); + } while(false); + + return ret; +} diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller_i.h b/lib/nfc/protocols/mf_classic/mf_classic_poller_i.h index a5af315307..14a7c61fd4 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller_i.h +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller_i.h @@ -3,7 +3,7 @@ #include "mf_classic_poller.h" #include #include -#include "crypto1.h" +#include #ifdef __cplusplus extern "C" { diff --git a/lib/nfc/protocols/mf_desfire/mf_desfire.h b/lib/nfc/protocols/mf_desfire/mf_desfire.h index 4c30753860..2818e9a24e 100644 --- a/lib/nfc/protocols/mf_desfire/mf_desfire.h +++ b/lib/nfc/protocols/mf_desfire/mf_desfire.h @@ -21,8 +21,6 @@ extern "C" { #define MF_DESFIRE_CMD_GET_VALUE (0x6C) #define MF_DESFIRE_CMD_READ_RECORDS (0xBB) -#define MF_DESFIRE_FLAG_HAS_NEXT (0xAF) - #define MF_DESFIRE_MAX_KEYS (14) #define MF_DESFIRE_MAX_FILES (32) @@ -71,11 +69,6 @@ typedef struct { typedef uint8_t MfDesfireKeyVersion; -typedef struct { - MfDesfireKeySettings key_settings; - SimpleArray* key_versions; -} MfDesfireKeyConfiguration; - typedef enum { MfDesfireFileTypeStandard = 0, MfDesfireFileTypeBackup = 1, @@ -96,7 +89,8 @@ typedef uint16_t MfDesfireFileAccessRights; typedef struct { MfDesfireFileType type; MfDesfireFileCommunicationSettings comm; - MfDesfireFileAccessRights access_rights; + MfDesfireFileAccessRights access_rights[MF_DESFIRE_MAX_KEYS]; + uint8_t access_rights_len; union { struct { uint32_t size; @@ -136,6 +130,7 @@ typedef enum { MfDesfireErrorNotPresent, MfDesfireErrorProtocol, MfDesfireErrorTimeout, + MfDesfireErrorAuthentication, } MfDesfireError; typedef struct { diff --git a/lib/nfc/protocols/mf_desfire/mf_desfire_i.c b/lib/nfc/protocols/mf_desfire/mf_desfire_i.c index 646803e75b..bfbbadadff 100644 --- a/lib/nfc/protocols/mf_desfire/mf_desfire_i.c +++ b/lib/nfc/protocols/mf_desfire/mf_desfire_i.c @@ -1,5 +1,7 @@ #include "mf_desfire_i.h" +#define TAG "MfDesfire" + #define BITS_IN_BYTE (8U) #define MF_DESFIRE_FFF_VERSION_KEY \ @@ -175,59 +177,88 @@ bool mf_desfire_file_settings_parse(MfDesfireFileSettings* data, const BitBuffer }; } MfDesfireFileSettingsLayout; + MfDesfireFileSettings file_settings_temp = {}; do { const size_t data_size = bit_buffer_get_size_bytes(buf); + const uint8_t* data_ptr = bit_buffer_get_data(buf); const size_t min_data_size = sizeof(MfDesfireFileSettingsHeader) + sizeof(MfDesfireFileSettingsData); - const size_t max_data_size = - sizeof(MfDesfireFileSettingsHeader) + sizeof(MfDesfireFileSettingsValue); - - if(data_size < min_data_size) break; - if(data_size <= max_data_size) { - MfDesfireFileSettingsLayout layout; - bit_buffer_write_bytes(buf, &layout, sizeof(MfDesfireFileSettingsLayout)); - - data->type = layout.header.type; - data->comm = layout.header.comm; - data->access_rights = layout.header.access_rights; - - if(data->type == MfDesfireFileTypeStandard || data->type == MfDesfireFileTypeBackup) { - if(data_size != min_data_size) break; - - data->data.size = layout.data.size; - } else if(data->type == MfDesfireFileTypeValue) { - if(data_size != - sizeof(MfDesfireFileSettingsHeader) + sizeof(MfDesfireFileSettingsValue)) - break; - data->value.lo_limit = layout.value.lo_limit; - data->value.hi_limit = layout.value.hi_limit; - data->value.limited_credit_value = layout.value.limited_credit_value; - data->value.limited_credit_enabled = layout.value.limited_credit_enabled; - - } else if( - data->type == MfDesfireFileTypeLinearRecord || - data->type == MfDesfireFileTypeCyclicRecord) { - if(data_size != - sizeof(MfDesfireFileSettingsHeader) + sizeof(MfDesfireFileSettingsRecord)) - break; + if(data_size < min_data_size) { + FURI_LOG_E( + TAG, "File settings size %zu less than minimum %zu", data_size, min_data_size); + break; + } - data->record.size = layout.record.size; - data->record.max = layout.record.max; - data->record.cur = layout.record.cur; + size_t bytes_processed = sizeof(MfDesfireFileSettingsHeader); + MfDesfireFileSettingsLayout layout = {}; + memcpy(&layout.header, data_ptr, sizeof(MfDesfireFileSettingsHeader)); + bool has_additional_access_rights = (layout.header.comm & 0x80) != 0; + + file_settings_temp.type = layout.header.type; + file_settings_temp.comm = layout.header.comm & 0x03; + file_settings_temp.access_rights_len = 1; + file_settings_temp.access_rights[0] = layout.header.access_rights; + + if(file_settings_temp.type == MfDesfireFileTypeStandard || + file_settings_temp.type == MfDesfireFileTypeBackup) { + memcpy( + &layout.data, + &data_ptr[sizeof(MfDesfireFileSettingsHeader)], + sizeof(MfDesfireFileSettingsData)); + file_settings_temp.data.size = layout.data.size; + bytes_processed += sizeof(MfDesfireFileSettingsData); + } else if(file_settings_temp.type == MfDesfireFileTypeValue) { + memcpy( + &layout.value, + &data_ptr[sizeof(MfDesfireFileSettingsHeader)], + sizeof(MfDesfireFileSettingsValue)); + file_settings_temp.value.lo_limit = layout.value.lo_limit; + file_settings_temp.value.hi_limit = layout.value.hi_limit; + file_settings_temp.value.limited_credit_value = layout.value.limited_credit_value; + file_settings_temp.value.limited_credit_enabled = layout.value.limited_credit_enabled; + bytes_processed += sizeof(MfDesfireFileSettingsValue); + } else if( + file_settings_temp.type == MfDesfireFileTypeLinearRecord || + file_settings_temp.type == MfDesfireFileTypeCyclicRecord) { + memcpy( + &layout.record, + &data_ptr[sizeof(MfDesfireFileSettingsHeader)], + sizeof(MfDesfireFileSettingsRecord)); + file_settings_temp.record.size = layout.record.size; + file_settings_temp.record.max = layout.record.max; + file_settings_temp.record.cur = layout.record.cur; + bytes_processed += sizeof(MfDesfireFileSettingsRecord); + } else { + FURI_LOG_W(TAG, "Unknown file type: %02x", file_settings_temp.type); + break; + } - } else { + if(has_additional_access_rights) { + uint8_t additional_access_rights_len = bit_buffer_get_byte(buf, bytes_processed); + FURI_LOG_D(TAG, "Has additional rights: %d", additional_access_rights_len); + if(data_size != bytes_processed + + additional_access_rights_len * sizeof(MfDesfireFileAccessRights) + + 1) { + FURI_LOG_W(TAG, "Unexpected command length: %zu", data_size); + for(size_t i = 0; i < bit_buffer_get_size_bytes(buf); i++) { + printf("%02X ", bit_buffer_get_byte(buf, i)); + } + printf("\r\n"); break; } - } else { - // TODO FL-3750: process HID Desfire command response here - // Set default fields for now - data->type = 0; - data->comm = 0; - data->access_rights = 0; - data->data.size = 0; + if(additional_access_rights_len > + MF_DESFIRE_MAX_KEYS * sizeof(MfDesfireFileAccessRights)) + break; + + memcpy( + &file_settings_temp.access_rights[1], + &data_ptr[bytes_processed], + additional_access_rights_len * sizeof(MfDesfireFileAccessRights)); + file_settings_temp.access_rights_len += additional_access_rights_len; } + *data = file_settings_temp; parsed = true; } while(false); @@ -392,18 +423,19 @@ bool mf_desfire_file_settings_load( break; furi_string_printf(key, "%s %s", prefix, MF_DESFIRE_FFF_FILE_ACCESS_RIGHTS_KEY); + uint32_t access_rights_len = 0; + if(!flipper_format_get_value_count(ff, furi_string_get_cstr(key), &access_rights_len)) + break; + if((access_rights_len == 0) || ((access_rights_len % 2) != 0)) break; if(!flipper_format_read_hex( - ff, - furi_string_get_cstr(key), - (uint8_t*)&data->access_rights, - sizeof(MfDesfireFileAccessRights))) + ff, furi_string_get_cstr(key), (uint8_t*)&data->access_rights, access_rights_len)) break; + data->access_rights_len = access_rights_len / sizeof(MfDesfireFileAccessRights); if(data->type == MfDesfireFileTypeStandard || data->type == MfDesfireFileTypeBackup) { furi_string_printf(key, "%s %s", prefix, MF_DESFIRE_FFF_FILE_SIZE_KEY); if(!flipper_format_read_uint32(ff, furi_string_get_cstr(key), &data->data.size, 1)) break; - } else if(data->type == MfDesfireFileTypeValue) { furi_string_printf(key, "%s %s", prefix, MF_DESFIRE_FFF_FILE_HI_LIMIT_KEY); if(!flipper_format_read_uint32(ff, furi_string_get_cstr(key), &data->value.hi_limit, 1)) @@ -641,8 +673,8 @@ bool mf_desfire_file_settings_save( if(!flipper_format_write_hex( ff, furi_string_get_cstr(key), - (const uint8_t*)&data->access_rights, - sizeof(MfDesfireFileAccessRights))) + (const uint8_t*)data->access_rights, + data->access_rights_len * sizeof(MfDesfireFileAccessRights))) break; if(data->type == MfDesfireFileTypeStandard || data->type == MfDesfireFileTypeBackup) { @@ -737,8 +769,11 @@ bool mf_desfire_application_save( if(i != key_version_count) break; const uint32_t file_count = simple_array_get_count(data->file_ids); - if(!mf_desfire_file_ids_save(simple_array_get_data(data->file_ids), file_count, prefix, ff)) - break; + if(file_count > 0) { + if(!mf_desfire_file_ids_save( + simple_array_get_data(data->file_ids), file_count, prefix, ff)) + break; + } for(i = 0; i < file_count; ++i) { const MfDesfireFileId* file_id = simple_array_cget(data->file_ids, i); diff --git a/lib/nfc/protocols/mf_desfire/mf_desfire_i.h b/lib/nfc/protocols/mf_desfire/mf_desfire_i.h index 05381096d1..21250baace 100644 --- a/lib/nfc/protocols/mf_desfire/mf_desfire_i.h +++ b/lib/nfc/protocols/mf_desfire/mf_desfire_i.h @@ -5,6 +5,52 @@ #define MF_DESFIRE_FFF_PICC_PREFIX "PICC" #define MF_DESFIRE_FFF_APP_PREFIX "Application" +// Successful operation +#define MF_DESFIRE_STATUS_OPERATION_OK (0x00) +// No changes done to backup files, CommitTransaction / AbortTransaction not necessary +#define MF_DESFIRE_STATUS_NO_CHANGES (0x0C) +// Insufficient NV-Memory to complete command +#define MF_DESFIRE_STATUS_OUT_OF_EEPROM_ERROR (0x0E) +// Command code not supported +#define MF_DESFIRE_STATUS_ILLEGAL_COMMAND_CODE (0x1C) +// CRC or MAC does not match data Padding bytes not valid +#define MF_DESFIRE_STATUS_INTEGRITY_ERROR (0x1E) +// Invalid key number specified +#define MF_DESFIRE_STATUS_NO_SUCH_KEY (0x40) +// Length of command string invalid +#define MF_DESFIRE_STATUS_LENGTH_ERROR (0x7E) +// Current configuration / status does not allow the requested command +#define MF_DESFIRE_STATUS_PERMISSION_DENIED (0x9D) +// Value of the parameter(s) invalid +#define MF_DESFIRE_STATUS_PARAMETER_ERROR (0x9E) +// Requested AID not present on PICC +#define MF_DESFIRE_STATUS_APPLICATION_NOT_FOUND (0xA0) +// Unrecoverable error within application, application will be disabled +#define MF_DESFIRE_STATUS_APPL_INTEGRITY_ERROR (0xA1) +// Current authentication status does not allow the requested command +#define MF_DESFIRE_STATUS_AUTHENTICATION_ERROR (0xAE) +// Additional data frame is expected to be sent +#define MF_DESFIRE_STATUS_ADDITIONAL_FRAME (0xAF) +// Attempt to read/write data from/to beyond the file's/record's limits +// Attempt to exceed the limits of a value file. +#define MF_DESFIRE_STATUS_BOUNDARY_ERROR (0xBE) +// Unrecoverable error within PICC, PICC will be disabled +#define MF_DESFIRE_STATUS_PICC_INTEGRITY_ERROR (0xC1) +// Previous Command was not fully completed. Not all Frames were requested or provided by the PCD +#define MF_DESFIRE_STATUS_COMMAND_ABORTED (0xCA) +// PICC was disabled by an unrecoverable error +#define MF_DESFIRE_STATUS_PICC_DISABLED_ERROR (0xCD) +// Number of Applications limited to 28, no additional CreateApplication possible +#define MF_DESFIRE_STATUS_COUNT_ERROR (0xCE) +// Creation of file/application failed because file/application with same number already exists +#define MF_DESFIRE_STATUS_DUBLICATE_ERROR (0xDE) +// Could not complete NV-write operation due to loss of power, internal backup/rollback mechanism activated +#define MF_DESFIRE_STATUS_EEPROM_ERROR (0xEE) +// Specified file number does not exist +#define MF_DESFIRE_STATUS_FILE_NOT_FOUND (0xF0) +// Unrecoverable error within file, file will be disabled +#define MF_DESFIRE_STATUS_FILE_INTEGRITY_ERROR (0xF1) + // SimpleArray configurations extern const SimpleArrayConfig mf_desfire_key_version_array_config; diff --git a/lib/nfc/protocols/mf_desfire/mf_desfire_poller.c b/lib/nfc/protocols/mf_desfire/mf_desfire_poller.c index c9d8bbab60..fa8a7ae9b9 100644 --- a/lib/nfc/protocols/mf_desfire/mf_desfire_poller.c +++ b/lib/nfc/protocols/mf_desfire/mf_desfire_poller.c @@ -75,17 +75,23 @@ static NfcCommand mf_desfire_poller_handler_read_version(MfDesfirePoller* instan } static NfcCommand mf_desfire_poller_handler_read_free_memory(MfDesfirePoller* instance) { + NfcCommand command = NfcCommandContinue; + instance->error = mf_desfire_poller_read_free_memory(instance, &instance->data->free_memory); if(instance->error == MfDesfireErrorNone) { FURI_LOG_D(TAG, "Read free memory success"); instance->state = MfDesfirePollerStateReadMasterKeySettings; + } else if(instance->error == MfDesfireErrorNotPresent) { + FURI_LOG_D(TAG, "Read free memoty is unsupported"); + instance->state = MfDesfirePollerStateReadMasterKeySettings; + command = NfcCommandReset; } else { FURI_LOG_E(TAG, "Failed to read free memory"); iso14443_4a_poller_halt(instance->iso14443_4a_poller); instance->state = MfDesfirePollerStateReadFailed; } - return NfcCommandContinue; + return command; } static NfcCommand mf_desfire_poller_handler_read_master_key_settings(MfDesfirePoller* instance) { @@ -94,6 +100,11 @@ static NfcCommand mf_desfire_poller_handler_read_master_key_settings(MfDesfirePo if(instance->error == MfDesfireErrorNone) { FURI_LOG_D(TAG, "Read master key settings success"); instance->state = MfDesfirePollerStateReadMasterKeyVersion; + } else if(instance->error == MfDesfireErrorAuthentication) { + FURI_LOG_D(TAG, "Auth is required to read master key settings and app ids"); + instance->data->master_key_settings.is_free_directory_list = false; + instance->data->master_key_settings.max_keys = 1; + instance->state = MfDesfirePollerStateReadMasterKeyVersion; } else { FURI_LOG_E(TAG, "Failed to read master key settings"); iso14443_4a_poller_halt(instance->iso14443_4a_poller); @@ -110,7 +121,11 @@ static NfcCommand mf_desfire_poller_handler_read_master_key_version(MfDesfirePol instance->data->master_key_settings.max_keys); if(instance->error == MfDesfireErrorNone) { FURI_LOG_D(TAG, "Read master key version success"); - instance->state = MfDesfirePollerStateReadApplicationIds; + if(instance->data->master_key_settings.is_free_directory_list) { + instance->state = MfDesfirePollerStateReadApplicationIds; + } else { + instance->state = MfDesfirePollerStateReadSuccess; + } } else { FURI_LOG_E(TAG, "Failed to read master key version"); iso14443_4a_poller_halt(instance->iso14443_4a_poller); @@ -126,6 +141,9 @@ static NfcCommand mf_desfire_poller_handler_read_application_ids(MfDesfirePoller if(instance->error == MfDesfireErrorNone) { FURI_LOG_D(TAG, "Read application ids success"); instance->state = MfDesfirePollerStateReadApplications; + } else if(instance->error == MfDesfireErrorAuthentication) { + FURI_LOG_D(TAG, "Read application ids impossible without authentication"); + instance->state = MfDesfirePollerStateReadSuccess; } else { FURI_LOG_E(TAG, "Failed to read application ids"); iso14443_4a_poller_halt(instance->iso14443_4a_poller); diff --git a/lib/nfc/protocols/mf_desfire/mf_desfire_poller_i.c b/lib/nfc/protocols/mf_desfire/mf_desfire_poller_i.c index 790f117157..2b86318491 100644 --- a/lib/nfc/protocols/mf_desfire/mf_desfire_poller_i.c +++ b/lib/nfc/protocols/mf_desfire/mf_desfire_poller_i.c @@ -19,6 +19,17 @@ MfDesfireError mf_desfire_process_error(Iso14443_4aError error) { } } +MfDesfireError mf_desfire_process_status_code(uint8_t status_code) { + switch(status_code) { + case MF_DESFIRE_STATUS_OPERATION_OK: + return MfDesfireErrorNone; + case MF_DESFIRE_STATUS_AUTHENTICATION_ERROR: + return MfDesfireErrorAuthentication; + default: + return MfDesfireErrorProtocol; + } +} + MfDesfireError mf_desfire_send_chunks( MfDesfirePoller* instance, const BitBuffer* tx_buffer, @@ -42,7 +53,7 @@ MfDesfireError mf_desfire_send_chunks( } bit_buffer_reset(instance->tx_buffer); - bit_buffer_append_byte(instance->tx_buffer, MF_DESFIRE_FLAG_HAS_NEXT); + bit_buffer_append_byte(instance->tx_buffer, MF_DESFIRE_STATUS_ADDITIONAL_FRAME); if(bit_buffer_get_size_bytes(instance->rx_buffer) > sizeof(uint8_t)) { bit_buffer_copy_right(rx_buffer, instance->rx_buffer, sizeof(uint8_t)); @@ -50,7 +61,8 @@ MfDesfireError mf_desfire_send_chunks( bit_buffer_reset(rx_buffer); } - while(bit_buffer_starts_with_byte(instance->rx_buffer, MF_DESFIRE_FLAG_HAS_NEXT)) { + while( + bit_buffer_starts_with_byte(instance->rx_buffer, MF_DESFIRE_STATUS_ADDITIONAL_FRAME)) { Iso14443_4aError iso14443_4a_error = iso14443_4a_poller_send_block( instance->iso14443_4a_poller, instance->tx_buffer, instance->rx_buffer); @@ -71,6 +83,11 @@ MfDesfireError mf_desfire_send_chunks( } } while(false); + if(error == MfDesfireErrorNone) { + uint8_t err_code = bit_buffer_get_byte(instance->rx_buffer, 0); + error = mf_desfire_process_status_code(err_code); + } + return error; } @@ -110,7 +127,7 @@ MfDesfireError if(error != MfDesfireErrorNone) break; if(!mf_desfire_free_memory_parse(data, instance->result_buffer)) { - error = MfDesfireErrorProtocol; + error = MfDesfireErrorNotPresent; } } while(false); @@ -414,13 +431,25 @@ MfDesfireError mf_desfire_poller_read_file_data_multi( simple_array_init(data, file_id_count); } - for(uint32_t i = 0; i < file_id_count; ++i) { + for(size_t i = 0; i < file_id_count; ++i) { const MfDesfireFileId file_id = *(const MfDesfireFileId*)simple_array_cget(file_ids, i); const MfDesfireFileSettings* file_settings_cur = simple_array_cget(file_settings, i); const MfDesfireFileType file_type = file_settings_cur->type; MfDesfireFileData* file_data = simple_array_get(data, i); + bool can_read_data = false; + for(size_t j = 0; j < file_settings_cur->access_rights_len; j++) { + uint8_t read_access = (file_settings_cur->access_rights[j] >> 12) & 0x0f; + uint8_t read_write_access = (file_settings_cur->access_rights[j] >> 4) & 0x0f; + can_read_data = (read_access == 0x0e) || (read_write_access == 0x0e); + if(can_read_data) break; + } + if(!can_read_data) { + FURI_LOG_D(TAG, "Can't read file %zu data without authentication", i); + continue; + } + if(file_type == MfDesfireFileTypeStandard || file_type == MfDesfireFileTypeBackup) { error = mf_desfire_poller_read_file_data( instance, file_id, 0, file_settings_cur->data.size, file_data); @@ -432,8 +461,6 @@ MfDesfireError mf_desfire_poller_read_file_data_multi( error = mf_desfire_poller_read_file_records( instance, file_id, 0, file_settings_cur->data.size, file_data); } - - if(error != MfDesfireErrorNone) break; } return error; @@ -448,22 +475,36 @@ MfDesfireError do { error = mf_desfire_poller_read_key_settings(instance, &data->key_settings); + if(error == MfDesfireErrorAuthentication) { + FURI_LOG_D(TAG, "Auth is required to read master key settings and app ids"); + data->key_settings.is_free_directory_list = false; + error = MfDesfireErrorNone; + break; + } if(error != MfDesfireErrorNone) break; error = mf_desfire_poller_read_key_versions( instance, data->key_versions, data->key_settings.max_keys); - if(error != MfDesfireErrorNone) break; + if(error != MfDesfireErrorNone) { + FURI_LOG_E(TAG, "Failed to read key version: %d", error); + break; + } error = mf_desfire_poller_read_file_ids(instance, data->file_ids); - if(error != MfDesfireErrorNone) break; + if(error != MfDesfireErrorNone) { + FURI_LOG_E(TAG, "Failed to read file ids: %d", error); + break; + } error = mf_desfire_poller_read_file_settings_multi( instance, data->file_ids, data->file_settings); - if(error != MfDesfireErrorNone) break; + if(error != MfDesfireErrorNone) { + FURI_LOG_E(TAG, "Failed to read file settings: %d", error); + break; + } error = mf_desfire_poller_read_file_data_multi( instance, data->file_ids, data->file_settings, data->file_data); - if(error != MfDesfireErrorNone) break; } while(false); @@ -484,11 +525,13 @@ MfDesfireError mf_desfire_poller_read_applications( simple_array_init(data, app_id_count); } - for(uint32_t i = 0; i < app_id_count; ++i) { + for(size_t i = 0; i < app_id_count; ++i) { do { + FURI_LOG_D(TAG, "Selecting app %zu", i); error = mf_desfire_poller_select_application(instance, simple_array_cget(app_ids, i)); if(error != MfDesfireErrorNone) break; + FURI_LOG_D(TAG, "Reading app %zu", i); MfDesfireApplication* current_app = simple_array_get(data, i); error = mf_desfire_poller_read_application(instance, current_app); diff --git a/lib/nfc/protocols/mf_desfire/mf_desfire_poller_i.h b/lib/nfc/protocols/mf_desfire/mf_desfire_poller_i.h index 1c80af36fb..19e38bebbc 100644 --- a/lib/nfc/protocols/mf_desfire/mf_desfire_poller_i.h +++ b/lib/nfc/protocols/mf_desfire/mf_desfire_poller_i.h @@ -47,6 +47,8 @@ struct MfDesfirePoller { MfDesfireError mf_desfire_process_error(Iso14443_4aError error); +MfDesfireError mf_desfire_process_status_code(uint8_t status_code); + const MfDesfireData* mf_desfire_poller_get_data(MfDesfirePoller* instance); #ifdef __cplusplus diff --git a/lib/subghz/devices/devices.c b/lib/subghz/devices/devices.c index 729ccca807..25e5223439 100644 --- a/lib/subghz/devices/devices.c +++ b/lib/subghz/devices/devices.c @@ -35,7 +35,7 @@ bool subghz_devices_begin(const SubGhzDevice* device) { SubGhzDeviceConf conf = { .ver = 1, .extended_range = false, // TODO - .power_amp = furi_hal_subghz_get_ext_power_amp(), + .power_amp = true, }; ret = device->interconnect->begin(&conf); diff --git a/lib/tlsf b/lib/tlsf new file mode 160000 index 0000000000..8fc595fe22 --- /dev/null +++ b/lib/tlsf @@ -0,0 +1 @@ +Subproject commit 8fc595fe223cd0b3b5d7b29eb86825e4bd38e6e8 diff --git a/lib/tlsf.scons b/lib/tlsf.scons new file mode 100644 index 0000000000..0a8419dbdc --- /dev/null +++ b/lib/tlsf.scons @@ -0,0 +1,21 @@ +Import("env") + +env.Append( + CPPPATH=[ + "#/lib/tlsf", + ], +) + + +libenv = env.Clone(FW_LIB_NAME="tlsf") +libenv.ApplyLibFlags() + +libenv.Append( + CPPDEFINES=[], +) + +sources = [File("tlsf/tlsf.c")] + +lib = libenv.StaticLibrary("${FW_LIB_NAME}", sources) +libenv.Install("${LIB_DIST_DIR}", lib) +Return("lib") diff --git a/lib/toolbox/name_generator.c b/lib/toolbox/name_generator.c index ad7c08668c..ea6213d986 100644 --- a/lib/toolbox/name_generator.c +++ b/lib/toolbox/name_generator.c @@ -74,22 +74,29 @@ void name_generator_make_random_prefixed( uint8_t name_generator_left_i = rand() % COUNT_OF(name_generator_left); uint8_t name_generator_right_i = rand() % COUNT_OF(name_generator_right); - if(prefix_after) { - snprintf( - name, - max_name_size, - "%s-%s%s%s", - name_generator_left[name_generator_left_i], - name_generator_right[name_generator_right_i], - prefix ? "_" : "", - prefix ? prefix : ""); + if(prefix) { + if(prefix_after) { + snprintf( + name, + max_name_size, + "%s-%s_%s", + name_generator_left[name_generator_left_i], + name_generator_right[name_generator_right_i], + prefix); + } else { + snprintf( + name, + max_name_size, + "%s_%s-%s", + prefix, + name_generator_left[name_generator_left_i], + name_generator_right[name_generator_right_i]); + } } else { snprintf( name, max_name_size, - "%s%s%s-%s", - prefix ? prefix : "", - prefix ? "_" : "", + "%s-%s", name_generator_left[name_generator_left_i], name_generator_right[name_generator_right_i]); } @@ -111,7 +118,6 @@ void name_generator_make_detailed_datetime( bool prefix_after) { furi_check(name); furi_check(max_name_size); - furi_check(prefix); DateTime dateTime; if(custom_time) { @@ -120,24 +126,37 @@ void name_generator_make_detailed_datetime( furi_hal_rtc_get_datetime(&dateTime); } - if(prefix_after) { - snprintf( - name, - max_name_size, - "%.4d-%.2d-%.2d_%.2d,%.2d,%.2d_%s", - dateTime.year, - dateTime.month, - dateTime.day, - dateTime.hour, - dateTime.minute, - dateTime.second, - prefix); + if(prefix) { + if(prefix_after) { + snprintf( + name, + max_name_size, + "%.4d-%.2d-%.2d_%.2d,%.2d,%.2d_%s", + dateTime.year, + dateTime.month, + dateTime.day, + dateTime.hour, + dateTime.minute, + dateTime.second, + prefix); + } else { + snprintf( + name, + max_name_size, + "%s_%.4d-%.2d-%.2d_%.2d,%.2d,%.2d", + prefix, + dateTime.year, + dateTime.month, + dateTime.day, + dateTime.hour, + dateTime.minute, + dateTime.second); + } } else { snprintf( name, max_name_size, - "%s_%.4d-%.2d-%.2d_%.2d,%.2d,%.2d", - prefix, + "%.4d-%.2d-%.2d_%.2d,%.2d,%.2d", dateTime.year, dateTime.month, dateTime.day, diff --git a/lib/toolbox/protocols/protocol.h b/lib/toolbox/protocols/protocol.h index 5a8015b1e1..0ee165d13e 100644 --- a/lib/toolbox/protocols/protocol.h +++ b/lib/toolbox/protocols/protocol.h @@ -40,6 +40,7 @@ typedef struct { ProtocolGetData get_data; ProtocolDecoder decoder; ProtocolEncoder encoder; + ProtocolRenderData render_uid; ProtocolRenderData render_data; ProtocolRenderData render_brief_data; ProtocolWriteData write_data; diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index f6199445d1..b5eaa47028 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,60.4,, +Version,+,61.1,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/cli/cli.h,, Header,+,applications/services/cli/cli_vcp.h,, @@ -199,6 +199,7 @@ Header,+,targets/f7/platform_specific/cxx_virtual_stub.h,, Header,+,targets/f7/platform_specific/intrinsic_export.h,, Header,+,targets/f7/platform_specific/math_wrapper.h,, Header,+,targets/furi_hal_include/furi_hal.h,, +Header,+,targets/furi_hal_include/furi_hal_adc.h,, Header,+,targets/furi_hal_include/furi_hal_bt.h,, Header,+,targets/furi_hal_include/furi_hal_cortex.h,, Header,+,targets/furi_hal_include/furi_hal_crypto.h,, @@ -514,9 +515,7 @@ Function,-,acosh,double,double Function,-,acoshf,float,float Function,-,acoshl,long double,long double Function,-,acosl,long double,long double -Function,-,aligned_alloc,void*,"size_t, size_t" -Function,+,aligned_free,void,void* -Function,+,aligned_malloc,void*,"size_t, size_t" +Function,+,aligned_alloc,void*,"size_t, size_t" Function,-,arc4random,__uint32_t, Function,-,arc4random_buf,void,"void*, size_t" Function,-,arc4random_uniform,__uint32_t,__uint32_t @@ -1086,6 +1085,16 @@ Function,+,furi_event_flag_get,uint32_t,FuriEventFlag* Function,+,furi_event_flag_set,uint32_t,"FuriEventFlag*, uint32_t" Function,+,furi_event_flag_wait,uint32_t,"FuriEventFlag*, uint32_t, uint32_t, uint32_t" Function,+,furi_get_tick,uint32_t, +Function,+,furi_hal_adc_acquire,FuriHalAdcHandle*, +Function,+,furi_hal_adc_configure,void,FuriHalAdcHandle* +Function,+,furi_hal_adc_configure_ex,void,"FuriHalAdcHandle*, FuriHalAdcScale, FuriHalAdcClock, FuriHalAdcOversample, FuriHalAdcSamplingTime" +Function,+,furi_hal_adc_convert_temp,float,"FuriHalAdcHandle*, uint16_t" +Function,+,furi_hal_adc_convert_to_voltage,float,"FuriHalAdcHandle*, uint16_t" +Function,+,furi_hal_adc_convert_vbat,float,"FuriHalAdcHandle*, uint16_t" +Function,+,furi_hal_adc_convert_vref,float,"FuriHalAdcHandle*, uint16_t" +Function,+,furi_hal_adc_init,void, +Function,+,furi_hal_adc_read,uint16_t,"FuriHalAdcHandle*, FuriHalAdcChannel" +Function,+,furi_hal_adc_release,void,FuriHalAdcHandle* Function,+,furi_hal_bt_change_app,FuriHalBleProfileBase*,"const FuriHalBleProfileTemplate*, FuriHalBleProfileParams, GapEventCallback, void*" Function,+,furi_hal_bt_check_profile_type,_Bool,"FuriHalBleProfileBase*, const FuriHalBleProfileTemplate*" Function,+,furi_hal_bt_clear_white_list,_Bool, @@ -1748,6 +1757,7 @@ Function,+,loader_is_locked,_Bool,Loader* Function,+,loader_lock,_Bool,Loader* Function,+,loader_show_menu,void,Loader* Function,+,loader_start,LoaderStatus,"Loader*, const char*, const char*, FuriString*" +Function,+,loader_start_detached_with_gui_error,void,"Loader*, const char*, const char*" Function,+,loader_start_with_gui_error,LoaderStatus,"Loader*, const char*, const char*" Function,+,loader_unlock,void,Loader* Function,+,loading_alloc,Loading*, @@ -1972,7 +1982,8 @@ Function,+,memchr,void*,"const void*, int, size_t" Function,+,memcmp,int,"const void*, const void*, size_t" Function,+,memcpy,void*,"void*, const void*, size_t" Function,-,memmem,void*,"const void*, size_t, const void*, size_t" -Function,-,memmgr_alloc_from_pool,void*,size_t +Function,+,memmgr_aux_pool_alloc,void*,size_t +Function,+,memmgr_aux_pool_get_free,size_t, Function,+,memmgr_get_free_heap,size_t, Function,+,memmgr_get_minimum_free_heap,size_t, Function,+,memmgr_get_total_heap,size_t, @@ -1980,8 +1991,7 @@ Function,+,memmgr_heap_disable_thread_trace,void,FuriThreadId Function,+,memmgr_heap_enable_thread_trace,void,FuriThreadId Function,+,memmgr_heap_get_max_free_block,size_t, Function,+,memmgr_heap_get_thread_memory,size_t,FuriThreadId -Function,+,memmgr_heap_printf_free_blocks,void, -Function,-,memmgr_pool_get_free,size_t, +Function,+,memmgr_heap_walk_blocks,void,"BlockWalker, void*" Function,-,memmgr_pool_get_max_block,size_t, Function,+,memmove,void*,"void*, const void*, size_t" Function,-,mempcpy,void*,"void*, const void*, size_t" diff --git a/targets/f18/furi_hal/furi_hal.c b/targets/f18/furi_hal/furi_hal.c index 6e0a254c13..6cfc939b8c 100644 --- a/targets/f18/furi_hal/furi_hal.c +++ b/targets/f18/furi_hal/furi_hal.c @@ -31,6 +31,7 @@ void furi_hal_deinit_early(void) { void furi_hal_init(void) { furi_hal_mpu_init(); + furi_hal_adc_init(); furi_hal_clock_init(); furi_hal_random_init(); furi_hal_serial_control_init(); diff --git a/targets/f18/furi_hal/furi_hal_resources.c b/targets/f18/furi_hal/furi_hal_resources.c index 9935d4124c..45ca3e6c49 100644 --- a/targets/f18/furi_hal/furi_hal_resources.c +++ b/targets/f18/furi_hal/furi_hal_resources.c @@ -68,49 +68,161 @@ const GpioPin gpio_usb_dp = {.port = GPIOA, .pin = LL_GPIO_PIN_12}; const GpioPinRecord gpio_pins[] = { // 5V: 1 - {.pin = &gpio_ext_pa7, .name = "PA7", .number = 2, .debug = false}, - {.pin = &gpio_ext_pa6, .name = "PA6", .number = 3, .debug = false}, - {.pin = &gpio_ext_pa4, .name = "PA4", .number = 4, .debug = false}, - {.pin = &gpio_ext_pb3, .name = "PB3", .number = 5, .debug = false}, - {.pin = &gpio_ext_pb2, .name = "PB2", .number = 6, .debug = false}, - {.pin = &gpio_ext_pc3, .name = "PC3", .number = 7, .debug = false}, + {.pin = &gpio_ext_pa7, + .name = "PA7", + .channel = FuriHalAdcChannel12, + .number = 2, + .debug = false}, + {.pin = &gpio_ext_pa6, + .name = "PA6", + .channel = FuriHalAdcChannel11, + .number = 3, + .debug = false}, + {.pin = &gpio_ext_pa4, + .name = "PA4", + .channel = FuriHalAdcChannel9, + .number = 4, + .debug = false}, + {.pin = &gpio_ext_pb3, + .name = "PB3", + .channel = FuriHalAdcChannelNone, + .number = 5, + .debug = false}, + {.pin = &gpio_ext_pb2, + .name = "PB2", + .channel = FuriHalAdcChannelNone, + .number = 6, + .debug = false}, + {.pin = &gpio_ext_pc3, + .name = "PC3", + .channel = FuriHalAdcChannel4, + .number = 7, + .debug = false}, // GND: 8 // Space // 3v3: 9 - {.pin = &gpio_swclk, .name = "PA14", .number = 10, .debug = true}, + {.pin = &gpio_swclk, + .name = "PA14", + .channel = FuriHalAdcChannelNone, + .number = 10, + .debug = true}, // GND: 11 - {.pin = &gpio_swdio, .name = "PA13", .number = 12, .debug = true}, - {.pin = &gpio_usart_tx, .name = "PB6", .number = 13, .debug = true}, - {.pin = &gpio_usart_rx, .name = "PB7", .number = 14, .debug = true}, - {.pin = &gpio_ext_pc1, .name = "PC1", .number = 15, .debug = false}, - {.pin = &gpio_ext_pc0, .name = "PC0", .number = 16, .debug = false}, - {.pin = &gpio_ibutton, .name = "PB14", .number = 17, .debug = true}, + {.pin = &gpio_swdio, + .name = "PA13", + .channel = FuriHalAdcChannelNone, + .number = 12, + .debug = true}, + {.pin = &gpio_usart_tx, + .name = "PB6", + .channel = FuriHalAdcChannelNone, + .number = 13, + .debug = true}, + {.pin = &gpio_usart_rx, + .name = "PB7", + .channel = FuriHalAdcChannelNone, + .number = 14, + .debug = true}, + {.pin = &gpio_ext_pc1, + .name = "PC1", + .channel = FuriHalAdcChannel2, + .number = 15, + .debug = false}, + {.pin = &gpio_ext_pc0, + .name = "PC0", + .channel = FuriHalAdcChannel1, + .number = 16, + .debug = false}, + {.pin = &gpio_ibutton, + .name = "PB14", + .channel = FuriHalAdcChannelNone, + .number = 17, + .debug = true}, // GND: 18 // 2nd column // 5V: 19 - {.pin = &gpio_ext_pc5, .name = "PC5", .number = 20, .debug = false}, - {.pin = &gpio_ext_pc4, .name = "PC4", .number = 21, .debug = false}, - {.pin = &gpio_ext_pa5, .name = "PA5", .number = 22, .debug = false}, - {.pin = &gpio_ext_pb9, .name = "PB9", .number = 23, .debug = false}, - {.pin = &gpio_ext_pa0, .name = "PA0", .number = 24, .debug = false}, - {.pin = &gpio_ext_pa1, .name = "PA1", .number = 25, .debug = false}, + {.pin = &gpio_ext_pc5, + .name = "PC5", + .channel = FuriHalAdcChannel14, + .number = 20, + .debug = false}, + {.pin = &gpio_ext_pc4, + .name = "PC4", + .channel = FuriHalAdcChannel13, + .number = 21, + .debug = false}, + {.pin = &gpio_ext_pa5, + .name = "PA5", + .channel = FuriHalAdcChannel10, + .number = 22, + .debug = false}, + {.pin = &gpio_ext_pb9, + .name = "PB9", + .channel = FuriHalAdcChannelNone, + .number = 23, + .debug = false}, + {.pin = &gpio_ext_pa0, + .name = "PA0", + .channel = FuriHalAdcChannel5, + .number = 24, + .debug = false}, + {.pin = &gpio_ext_pa1, + .name = "PA1", + .channel = FuriHalAdcChannel6, + .number = 25, + .debug = false}, // KEY: 26 // Space // 3v3: 27 - {.pin = &gpio_ext_pa15, .name = "PA15", .number = 28, .debug = false}, + {.pin = &gpio_ext_pa15, + .name = "PA15", + .channel = FuriHalAdcChannelNone, + .number = 28, + .debug = false}, // GND: 29 - {.pin = &gpio_ext_pe4, .name = "PE4", .number = 30, .debug = false}, - {.pin = &gpio_ext_pa2, .name = "PA2", .number = 31, .debug = false}, - {.pin = &gpio_ext_pb4, .name = "PB4", .number = 32, .debug = false}, - {.pin = &gpio_ext_pb5, .name = "PB5", .number = 33, .debug = false}, - {.pin = &gpio_ext_pd0, .name = "PD0", .number = 34, .debug = false}, - {.pin = &gpio_ext_pb13, .name = "PB13", .number = 35, .debug = false}, + {.pin = &gpio_ext_pe4, + .name = "PE4", + .channel = FuriHalAdcChannelNone, + .number = 30, + .debug = false}, + {.pin = &gpio_ext_pa2, + .name = "PA2", + .channel = FuriHalAdcChannel7, + .number = 31, + .debug = false}, + {.pin = &gpio_ext_pb4, + .name = "PB4", + .channel = FuriHalAdcChannelNone, + .number = 32, + .debug = false}, + {.pin = &gpio_ext_pb5, + .name = "PB5", + .channel = FuriHalAdcChannelNone, + .number = 33, + .debug = false}, + {.pin = &gpio_ext_pd0, + .name = "PD0", + .channel = FuriHalAdcChannelNone, + .number = 34, + .debug = false}, + {.pin = &gpio_ext_pb13, + .name = "PB13", + .channel = FuriHalAdcChannelNone, + .number = 35, + .debug = false}, // GND: 36 /* Dangerous pins, may damage hardware */ - {.pin = &gpio_usart_rx, .name = "PB7", .number = 0, .debug = true}, - {.pin = &gpio_speaker, .name = "PB8", .number = 0, .debug = true}, + {.pin = &gpio_usart_rx, + .name = "PB7", + .channel = FuriHalAdcChannelNone, + .number = 0, + .debug = true}, + {.pin = &gpio_speaker, + .name = "PB8", + .channel = FuriHalAdcChannelNone, + .number = 0, + .debug = true}, }; const size_t gpio_pins_count = COUNT_OF(gpio_pins); diff --git a/targets/f18/furi_hal/furi_hal_resources.h b/targets/f18/furi_hal/furi_hal_resources.h index 3d45ad885e..8f6173eb9d 100644 --- a/targets/f18/furi_hal/furi_hal_resources.h +++ b/targets/f18/furi_hal/furi_hal_resources.h @@ -1,9 +1,7 @@ #pragma once #include - -#include -#include +#include #ifdef __cplusplus extern "C" { @@ -41,6 +39,7 @@ typedef struct { typedef struct { const GpioPin* pin; const char* name; + const FuriHalAdcChannel channel; const uint8_t number; const bool debug; } GpioPinRecord; diff --git a/targets/f18/target.json b/targets/f18/target.json index 43e9254cd4..a61c1373e1 100644 --- a/targets/f18/target.json +++ b/targets/f18/target.json @@ -13,6 +13,7 @@ "print", "flipper18", "furi", + "tlsf", "freertos", "stm32wb", "hwdrivers", @@ -68,4 +69,4 @@ "ibutton", "infrared" ] -} +} \ No newline at end of file diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index be6c7fc92c..1fb2632b2d 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,60.4,, +Version,+,61.1,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/main/archive/helpers/archive_helpers_ext.h,, Header,+,applications/main/subghz/subghz_fap.h,, @@ -136,6 +136,7 @@ Header,+,lib/music_worker/music_worker.h,, Header,+,lib/nanopb/pb.h,, Header,+,lib/nanopb/pb_decode.h,, Header,+,lib/nanopb/pb_encode.h,, +Header,+,lib/nfc/helpers/crypto1.h,, Header,+,lib/nfc/helpers/iso13239_crc.h,, Header,+,lib/nfc/helpers/iso14443_crc.h,, Header,+,lib/nfc/helpers/nfc_data_generator.h,, @@ -147,6 +148,8 @@ Header,+,lib/nfc/nfc_poller.h,, Header,+,lib/nfc/nfc_scanner.h,, Header,+,lib/nfc/protocols/emv/emv.h,, Header,+,lib/nfc/protocols/emv/emv_poller.h,, +Header,+,lib/nfc/protocols/felica/felica.h,, +Header,+,lib/nfc/protocols/felica/felica_poller.h,, Header,+,lib/nfc/protocols/iso14443_3a/iso14443_3a.h,, Header,+,lib/nfc/protocols/iso14443_3a/iso14443_3a_listener.h,, Header,+,lib/nfc/protocols/iso14443_3a/iso14443_3a_poller.h,, @@ -282,6 +285,7 @@ Header,+,targets/f7/platform_specific/cxx_virtual_stub.h,, Header,+,targets/f7/platform_specific/intrinsic_export.h,, Header,+,targets/f7/platform_specific/math_wrapper.h,, Header,+,targets/furi_hal_include/furi_hal.h,, +Header,+,targets/furi_hal_include/furi_hal_adc.h,, Header,+,targets/furi_hal_include/furi_hal_bt.h,, Header,+,targets/furi_hal_include/furi_hal_cortex.h,, Header,+,targets/furi_hal_include/furi_hal_crypto.h,, @@ -609,9 +613,7 @@ Function,-,acosh,double,double Function,-,acoshf,float,float Function,-,acoshl,long double,long double Function,-,acosl,long double,long double -Function,-,aligned_alloc,void*,"size_t, size_t" -Function,+,aligned_free,void,void* -Function,+,aligned_malloc,void*,"size_t, size_t" +Function,+,aligned_alloc,void*,"size_t, size_t" Function,-,arc4random,__uint32_t, Function,-,arc4random_buf,void,"void*, size_t" Function,-,arc4random_uniform,__uint32_t,__uint32_t @@ -888,6 +890,16 @@ Function,-,coshl,long double,long double Function,-,cosl,long double,long double Function,+,crc32_calc_buffer,uint32_t,"uint32_t, const void*, size_t" Function,+,crc32_calc_file,uint32_t,"File*, const FileCrcProgressCb, void*" +Function,+,crypto1_alloc,Crypto1*, +Function,+,crypto1_bit,uint8_t,"Crypto1*, uint8_t, int" +Function,+,crypto1_byte,uint8_t,"Crypto1*, uint8_t, int" +Function,+,crypto1_decrypt,void,"Crypto1*, const BitBuffer*, BitBuffer*" +Function,+,crypto1_encrypt,void,"Crypto1*, uint8_t*, const BitBuffer*, BitBuffer*" +Function,+,crypto1_encrypt_reader_nonce,void,"Crypto1*, uint64_t, uint32_t, uint8_t*, uint8_t*, BitBuffer*, _Bool" +Function,+,crypto1_free,void,Crypto1* +Function,+,crypto1_init,void,"Crypto1*, uint64_t" +Function,+,crypto1_reset,void,Crypto1* +Function,+,crypto1_word,uint32_t,"Crypto1*, uint32_t, int" Function,-,ctermid,char*,char* Function,-,cuserid,char*,char* Function,+,datetime_datetime_to_timestamp,uint32_t,DateTime* @@ -1055,6 +1067,22 @@ Function,-,fdim,double,"double, double" Function,-,fdimf,float,"float, float" Function,-,fdiml,long double,"long double, long double" Function,-,fdopen,FILE*,"int, const char*" +Function,+,felica_alloc,FelicaData*, +Function,+,felica_calculate_mac_write,void,"mbedtls_des3_context*, const uint8_t*, const uint8_t*, const uint8_t*, const uint8_t*, uint8_t*" +Function,+,felica_calculate_session_key,void,"mbedtls_des3_context*, const uint8_t*, const uint8_t*, uint8_t*" +Function,+,felica_check_mac,_Bool,"mbedtls_des3_context*, const uint8_t*, const uint8_t*, const uint8_t*, const uint8_t, uint8_t*" +Function,+,felica_copy,void,"FelicaData*, const FelicaData*" +Function,+,felica_free,void,FelicaData* +Function,+,felica_get_base_data,FelicaData*,const FelicaData* +Function,+,felica_get_device_name,const char*,"const FelicaData*, NfcDeviceNameType" +Function,+,felica_get_uid,const uint8_t*,"const FelicaData*, size_t*" +Function,+,felica_is_equal,_Bool,"const FelicaData*, const FelicaData*" +Function,+,felica_load,_Bool,"FelicaData*, FlipperFormat*, uint32_t" +Function,+,felica_poller_activate,FelicaError,"FelicaPoller*, FelicaData*" +Function,+,felica_reset,void,FelicaData* +Function,+,felica_save,_Bool,"const FelicaData*, FlipperFormat*" +Function,+,felica_set_uid,_Bool,"FelicaData*, const uint8_t*, size_t" +Function,+,felica_verify,_Bool,"FelicaData*, const FuriString*" Function,-,feof,int,FILE* Function,-,feof_unlocked,int,FILE* Function,-,ferror,int,FILE* @@ -1236,6 +1264,16 @@ Function,+,furi_event_flag_get,uint32_t,FuriEventFlag* Function,+,furi_event_flag_set,uint32_t,"FuriEventFlag*, uint32_t" Function,+,furi_event_flag_wait,uint32_t,"FuriEventFlag*, uint32_t, uint32_t, uint32_t" Function,+,furi_get_tick,uint32_t, +Function,+,furi_hal_adc_acquire,FuriHalAdcHandle*, +Function,+,furi_hal_adc_configure,void,FuriHalAdcHandle* +Function,+,furi_hal_adc_configure_ex,void,"FuriHalAdcHandle*, FuriHalAdcScale, FuriHalAdcClock, FuriHalAdcOversample, FuriHalAdcSamplingTime" +Function,+,furi_hal_adc_convert_temp,float,"FuriHalAdcHandle*, uint16_t" +Function,+,furi_hal_adc_convert_to_voltage,float,"FuriHalAdcHandle*, uint16_t" +Function,+,furi_hal_adc_convert_vbat,float,"FuriHalAdcHandle*, uint16_t" +Function,+,furi_hal_adc_convert_vref,float,"FuriHalAdcHandle*, uint16_t" +Function,+,furi_hal_adc_init,void, +Function,+,furi_hal_adc_read,uint16_t,"FuriHalAdcHandle*, FuriHalAdcChannel" +Function,+,furi_hal_adc_release,void,FuriHalAdcHandle* Function,+,furi_hal_bt_change_app,FuriHalBleProfileBase*,"const FuriHalBleProfileTemplate*, FuriHalBleProfileParams, GapEventCallback, void*" Function,+,furi_hal_bt_check_profile_type,_Bool,"FuriHalBleProfileBase*, const FuriHalBleProfileTemplate*" Function,+,furi_hal_bt_clear_white_list,_Bool, @@ -1643,7 +1681,6 @@ Function,-,furi_hal_subghz_dump_state,void, Function,+,furi_hal_subghz_flush_rx,void, Function,+,furi_hal_subghz_flush_tx,void, Function,+,furi_hal_subghz_get_data_gpio,const GpioPin*, -Function,+,furi_hal_subghz_get_ext_power_amp,_Bool, Function,+,furi_hal_subghz_get_lqi,uint8_t, Function,+,furi_hal_subghz_get_rolling_counter_mult,int8_t, Function,+,furi_hal_subghz_get_rssi,float, @@ -1661,7 +1698,6 @@ Function,+,furi_hal_subghz_reset,void, Function,+,furi_hal_subghz_rx,void, Function,+,furi_hal_subghz_rx_pipe_not_empty,_Bool, Function,+,furi_hal_subghz_set_async_mirror_pin,void,const GpioPin* -Function,+,furi_hal_subghz_set_ext_power_amp,void,_Bool Function,+,furi_hal_subghz_set_frequency,uint32_t,uint32_t Function,+,furi_hal_subghz_set_frequency_and_path,uint32_t,uint32_t Function,+,furi_hal_subghz_set_path,void,FuriHalSubGhzPath @@ -1956,6 +1992,7 @@ Function,+,ibutton_protocols_read,_Bool,"iButtonProtocols*, iButtonKey*" Function,+,ibutton_protocols_render_brief_data,void,"iButtonProtocols*, const iButtonKey*, FuriString*" Function,+,ibutton_protocols_render_data,void,"iButtonProtocols*, const iButtonKey*, FuriString*" Function,+,ibutton_protocols_render_error,void,"iButtonProtocols*, const iButtonKey*, FuriString*" +Function,+,ibutton_protocols_render_uid,void,"iButtonProtocols*, const iButtonKey*, FuriString*" Function,+,ibutton_protocols_save,_Bool,"iButtonProtocols*, const iButtonKey*, const char*" Function,+,ibutton_protocols_write_blank,_Bool,"iButtonProtocols*, iButtonKey*" Function,+,ibutton_protocols_write_copy,_Bool,"iButtonProtocols*, iButtonKey*" @@ -2458,7 +2495,8 @@ Function,+,memchr,void*,"const void*, int, size_t" Function,+,memcmp,int,"const void*, const void*, size_t" Function,+,memcpy,void*,"void*, const void*, size_t" Function,-,memmem,void*,"const void*, size_t, const void*, size_t" -Function,-,memmgr_alloc_from_pool,void*,size_t +Function,+,memmgr_aux_pool_alloc,void*,size_t +Function,+,memmgr_aux_pool_get_free,size_t, Function,+,memmgr_get_free_heap,size_t, Function,+,memmgr_get_minimum_free_heap,size_t, Function,+,memmgr_get_total_heap,size_t, @@ -2466,8 +2504,7 @@ Function,+,memmgr_heap_disable_thread_trace,void,FuriThreadId Function,+,memmgr_heap_enable_thread_trace,void,FuriThreadId Function,+,memmgr_heap_get_max_free_block,size_t, Function,+,memmgr_heap_get_thread_memory,size_t,FuriThreadId -Function,+,memmgr_heap_printf_free_blocks,void, -Function,-,memmgr_pool_get_free,size_t, +Function,+,memmgr_heap_walk_blocks,void,"BlockWalker, void*" Function,-,memmgr_pool_get_max_block,size_t, Function,+,memmove,void*,"void*, const void*, size_t" Function,-,mempcpy,void*,"void*, const void*, size_t" @@ -2511,6 +2548,10 @@ Function,+,mf_classic_poller_get_nt,MfClassicError,"MfClassicPoller*, uint8_t, M Function,+,mf_classic_poller_get_nt_nested,MfClassicError,"MfClassicPoller*, uint8_t, MfClassicKeyType, MfClassicNt*" Function,+,mf_classic_poller_halt,MfClassicError,MfClassicPoller* Function,+,mf_classic_poller_read_block,MfClassicError,"MfClassicPoller*, uint8_t, MfClassicBlock*" +Function,+,mf_classic_poller_send_custom_parity_frame,MfClassicError,"MfClassicPoller*, const BitBuffer*, BitBuffer*, uint32_t" +Function,+,mf_classic_poller_send_encrypted_frame,MfClassicError,"MfClassicPoller*, const BitBuffer*, BitBuffer*, uint32_t" +Function,+,mf_classic_poller_send_frame,MfClassicError,"MfClassicPoller*, const BitBuffer*, BitBuffer*, uint32_t" +Function,+,mf_classic_poller_send_standard_frame,MfClassicError,"MfClassicPoller*, const BitBuffer*, BitBuffer*, uint32_t" Function,+,mf_classic_poller_sync_auth,MfClassicError,"Nfc*, uint8_t, MfClassicKey*, MfClassicKeyType, MfClassicAuthContext*" Function,+,mf_classic_poller_sync_change_value,MfClassicError,"Nfc*, uint8_t, MfClassicKey*, MfClassicKeyType, int32_t, int32_t*" Function,+,mf_classic_poller_sync_collect_nt,MfClassicError,"Nfc*, uint8_t, MfClassicKeyType, MfClassicNt*" @@ -2900,6 +2941,7 @@ Function,+,powf,float,"float, float" Function,-,powl,long double,"long double, long double" Function,+,pretty_format_bytes_hex_canonical,void,"FuriString*, size_t, const char*, const uint8_t*, size_t" Function,-,printf,int,"const char*, ..." +Function,+,prng_successor,uint32_t,"uint32_t, uint32_t" Function,+,process_favorite_launch,_Bool,char** Function,+,property_value_out,void,"PropertyValueContext*, const char*, unsigned int, ..." Function,+,protocol_dict_alloc,ProtocolDict*,"const ProtocolBase**, size_t" @@ -3766,6 +3808,7 @@ Variable,+,I_DolphinMafia_119x62,Icon, Variable,+,I_DolphinReadingSuccess_59x63,Icon, Variable,+,I_DolphinSaved_92x58,Icon, Variable,+,I_DolphinSuccess_91x55,Icon, +Variable,+,I_DolphinWait_59x54,Icon, Variable,+,I_DolphinWait_61x59,Icon, Variable,+,I_Drive_112x35,Icon, Variable,+,I_Dynamic_9x7,Icon, @@ -4187,6 +4230,7 @@ Variable,+,message_vibro_off,const NotificationMessage, Variable,+,message_vibro_on,const NotificationMessage, Variable,+,momentum_settings,MomentumSettings, Variable,-,nfc_device_emv,const NfcDeviceBase, +Variable,-,nfc_device_felica,const NfcDeviceBase, Variable,-,nfc_device_mf_classic,const NfcDeviceBase, Variable,-,nfc_device_mf_desfire,const NfcDeviceBase, Variable,-,nfc_device_mf_ultralight,const NfcDeviceBase, diff --git a/targets/f7/ble_glue/app_conf.h b/targets/f7/ble_glue/app_conf.h index fbf6d0291d..d0e089eb1e 100644 --- a/targets/f7/ble_glue/app_conf.h +++ b/targets/f7/ble_glue/app_conf.h @@ -9,7 +9,6 @@ /** * Define IO Authentication */ -#define CFG_USED_FIXED_PIN USE_FIXED_PIN_FOR_PAIRING_FORBIDDEN #define CFG_ENCRYPTION_KEY_SIZE_MAX (16) #define CFG_ENCRYPTION_KEY_SIZE_MIN (8) @@ -18,11 +17,6 @@ */ #define CFG_IO_CAPABILITY IO_CAP_DISPLAY_YES_NO -/** - * Define MITM modes - */ -#define CFG_MITM_PROTECTION MITM_PROTECTION_REQUIRED - /** * Define Secure Connections Support */ diff --git a/targets/f7/ble_glue/extra_beacon.c b/targets/f7/ble_glue/extra_beacon.c index 2ef3e056fb..60338cb76b 100644 --- a/targets/f7/ble_glue/extra_beacon.c +++ b/targets/f7/ble_glue/extra_beacon.c @@ -9,7 +9,8 @@ #define GAP_MS_TO_SCAN_INTERVAL(x) ((uint16_t)((x) / 0.625)) // Also used as an indicator of whether the beacon had ever been configured -#define GAP_MIN_ADV_INTERVAL_MS (20) +// AN5289: 4.7, we need at least 25ms + advertisement, which is 30 ms +#define GAP_MIN_ADV_INTERVAL_MS (30u) typedef struct { GapExtraBeaconConfig last_config; diff --git a/targets/f7/ble_glue/gap.c b/targets/f7/ble_glue/gap.c index 1d535664e3..5a95cff263 100644 --- a/targets/f7/ble_glue/gap.c +++ b/targets/f7/ble_glue/gap.c @@ -372,9 +372,8 @@ static void gap_init_svc(Gap* gap) { // Set default PHY hci_le_set_default_phy(ALL_PHYS_PREFERENCE, TX_2M_PREFERRED, RX_2M_PREFERRED); // Set I/O capability - bool bonding_mode = gap->config->bonding_mode; - uint8_t cfg_mitm_protection = CFG_MITM_PROTECTION; - uint8_t cfg_used_fixed_pin = CFG_USED_FIXED_PIN; + uint8_t auth_req_mitm_mode = MITM_PROTECTION_REQUIRED; + uint8_t auth_req_use_fixed_pin = USE_FIXED_PIN_FOR_PAIRING_FORBIDDEN; bool keypress_supported = false; if(gap->config->pairing_method == GapPairingPinCodeShow) { aci_gap_set_io_capability(IO_CAP_DISPLAY_ONLY); @@ -383,22 +382,21 @@ static void gap_init_svc(Gap* gap) { keypress_supported = true; } else if(gap->config->pairing_method == GapPairingNone) { // "Just works" pairing method (iOS accepts it, it seems Android and Linux don't) - bonding_mode = false; - cfg_mitm_protection = MITM_PROTECTION_NOT_REQUIRED; - cfg_used_fixed_pin = USE_FIXED_PIN_FOR_PAIRING_ALLOWED; + auth_req_mitm_mode = MITM_PROTECTION_NOT_REQUIRED; + auth_req_use_fixed_pin = USE_FIXED_PIN_FOR_PAIRING_ALLOWED; // If "just works" isn't supported, we want the numeric comparaison method aci_gap_set_io_capability(IO_CAP_DISPLAY_YES_NO); keypress_supported = true; } // Setup authentication aci_gap_set_authentication_requirement( - bonding_mode, - cfg_mitm_protection, + gap->config->bonding_mode, + auth_req_mitm_mode, CFG_SC_SUPPORT, keypress_supported, CFG_ENCRYPTION_KEY_SIZE_MIN, CFG_ENCRYPTION_KEY_SIZE_MAX, - cfg_used_fixed_pin, + auth_req_use_fixed_pin, 0, CFG_IDENTITY_ADDRESS); // Configure whitelist diff --git a/targets/f7/ble_glue/gap.h b/targets/f7/ble_glue/gap.h index 8ee4b3d91c..4376570ad7 100644 --- a/targets/f7/ble_glue/gap.h +++ b/targets/f7/ble_glue/gap.h @@ -52,6 +52,7 @@ typedef enum { GapPairingNone, GapPairingPinCodeShow, GapPairingPinCodeVerifyYesNo, + GapPairingCount, } GapPairing; typedef struct { diff --git a/targets/f7/ble_glue/profiles/serial_profile.c b/targets/f7/ble_glue/profiles/serial_profile.c index a3949abfc8..165b813307 100644 --- a/targets/f7/ble_glue/profiles/serial_profile.c +++ b/targets/f7/ble_glue/profiles/serial_profile.c @@ -46,8 +46,8 @@ static GapConfig serial_template_config = { .bonding_mode = true, .pairing_method = GapPairingPinCodeShow, .conn_param = { - .conn_int_min = 0x18, // 30 ms - .conn_int_max = 0x24, // 45 ms + .conn_int_min = 0x18, // AN5289: 4.7, we need at least 25ms + advertisement, which is 30 ms + .conn_int_max = 0x18, // 30 ms .slave_latency = 0, .supervisor_timeout = 0, }}; diff --git a/targets/f7/fatfs/sector_cache.c b/targets/f7/fatfs/sector_cache.c index 319dd21732..df86cb7f15 100644 --- a/targets/f7/fatfs/sector_cache.c +++ b/targets/f7/fatfs/sector_cache.c @@ -19,7 +19,7 @@ static SectorCache* cache = NULL; void sector_cache_init(void) { if(cache == NULL) { - cache = memmgr_alloc_from_pool(sizeof(SectorCache)); + cache = memmgr_aux_pool_alloc(sizeof(SectorCache)); } if(cache != NULL) { diff --git a/targets/f7/furi_hal/furi_hal.c b/targets/f7/furi_hal/furi_hal.c index 425865e82d..d4b7a9e319 100644 --- a/targets/f7/furi_hal/furi_hal.c +++ b/targets/f7/furi_hal/furi_hal.c @@ -41,6 +41,7 @@ void furi_hal_deinit_early(void) { void furi_hal_init(void) { furi_hal_mpu_init(); + furi_hal_adc_init(); furi_hal_clock_init(); furi_hal_random_init(); furi_hal_serial_control_init(); diff --git a/targets/f7/furi_hal/furi_hal_adc.c b/targets/f7/furi_hal/furi_hal_adc.c new file mode 100644 index 0000000000..7a15f2e54b --- /dev/null +++ b/targets/f7/furi_hal/furi_hal_adc.c @@ -0,0 +1,281 @@ +#include +#include +#include +#include + +#include + +#include +#include + +struct FuriHalAdcHandle { + ADC_TypeDef* adc; + FuriMutex* mutex; + uint32_t full_scale; +}; + +static const uint32_t furi_hal_adc_clock[] = { + [FuriHalAdcClockSync16] = LL_ADC_CLOCK_SYNC_PCLK_DIV4, + [FuriHalAdcClockSync32] = LL_ADC_CLOCK_SYNC_PCLK_DIV2, + [FuriHalAdcClockSync64] = LL_ADC_CLOCK_SYNC_PCLK_DIV1, +}; + +static const uint8_t furi_hal_adc_clock_div[] = { + [FuriHalAdcClockSync16] = 4, + [FuriHalAdcClockSync32] = 2, + [FuriHalAdcClockSync64] = 1, +}; + +static const uint32_t furi_hal_adc_oversample_ratio[] = { + [FuriHalAdcOversample2] = LL_ADC_OVS_RATIO_2, + [FuriHalAdcOversample4] = LL_ADC_OVS_RATIO_4, + [FuriHalAdcOversample8] = LL_ADC_OVS_RATIO_8, + [FuriHalAdcOversample16] = LL_ADC_OVS_RATIO_16, + [FuriHalAdcOversample32] = LL_ADC_OVS_RATIO_32, + [FuriHalAdcOversample64] = LL_ADC_OVS_RATIO_64, + [FuriHalAdcOversample128] = LL_ADC_OVS_RATIO_128, + [FuriHalAdcOversample256] = LL_ADC_OVS_RATIO_256, +}; + +static const uint32_t furi_hal_adc_oversample_shift[] = { + [FuriHalAdcOversample2] = LL_ADC_OVS_SHIFT_RIGHT_1, + [FuriHalAdcOversample4] = LL_ADC_OVS_SHIFT_RIGHT_2, + [FuriHalAdcOversample8] = LL_ADC_OVS_SHIFT_RIGHT_3, + [FuriHalAdcOversample16] = LL_ADC_OVS_SHIFT_RIGHT_4, + [FuriHalAdcOversample32] = LL_ADC_OVS_SHIFT_RIGHT_5, + [FuriHalAdcOversample64] = LL_ADC_OVS_SHIFT_RIGHT_6, + [FuriHalAdcOversample128] = LL_ADC_OVS_SHIFT_RIGHT_7, + [FuriHalAdcOversample256] = LL_ADC_OVS_SHIFT_RIGHT_8, +}; + +static const uint32_t furi_hal_adc_sampling_time[] = { + [FuriHalAdcSamplingtime2_5] = LL_ADC_SAMPLINGTIME_2CYCLES_5, + [FuriHalAdcSamplingtime6_5] = LL_ADC_SAMPLINGTIME_6CYCLES_5, + [FuriHalAdcSamplingtime12_5] = LL_ADC_SAMPLINGTIME_12CYCLES_5, + [FuriHalAdcSamplingtime24_5] = LL_ADC_SAMPLINGTIME_24CYCLES_5, + [FuriHalAdcSamplingtime47_5] = LL_ADC_SAMPLINGTIME_47CYCLES_5, + [FuriHalAdcSamplingtime92_5] = LL_ADC_SAMPLINGTIME_92CYCLES_5, + [FuriHalAdcSamplingtime247_5] = LL_ADC_SAMPLINGTIME_247CYCLES_5, + [FuriHalAdcSamplingtime640_5] = LL_ADC_SAMPLINGTIME_640CYCLES_5, +}; + +static const uint32_t furi_hal_adc_channel_map[] = { + [FuriHalAdcChannel0] = LL_ADC_CHANNEL_0, + [FuriHalAdcChannel1] = LL_ADC_CHANNEL_1, + [FuriHalAdcChannel2] = LL_ADC_CHANNEL_2, + [FuriHalAdcChannel3] = LL_ADC_CHANNEL_3, + [FuriHalAdcChannel4] = LL_ADC_CHANNEL_4, + [FuriHalAdcChannel5] = LL_ADC_CHANNEL_5, + [FuriHalAdcChannel6] = LL_ADC_CHANNEL_6, + [FuriHalAdcChannel7] = LL_ADC_CHANNEL_7, + [FuriHalAdcChannel8] = LL_ADC_CHANNEL_8, + [FuriHalAdcChannel9] = LL_ADC_CHANNEL_9, + [FuriHalAdcChannel10] = LL_ADC_CHANNEL_10, + [FuriHalAdcChannel11] = LL_ADC_CHANNEL_11, + [FuriHalAdcChannel12] = LL_ADC_CHANNEL_12, + [FuriHalAdcChannel13] = LL_ADC_CHANNEL_13, + [FuriHalAdcChannel14] = LL_ADC_CHANNEL_14, + [FuriHalAdcChannel15] = LL_ADC_CHANNEL_15, + [FuriHalAdcChannel16] = LL_ADC_CHANNEL_16, + [FuriHalAdcChannel17] = LL_ADC_CHANNEL_17, + [FuriHalAdcChannel18] = LL_ADC_CHANNEL_18, + [FuriHalAdcChannelVREFINT] = LL_ADC_CHANNEL_VREFINT, + [FuriHalAdcChannelTEMPSENSOR] = LL_ADC_CHANNEL_TEMPSENSOR, + [FuriHalAdcChannelVBAT] = LL_ADC_CHANNEL_VBAT, +}; + +static FuriHalAdcHandle* furi_hal_adc_handle = NULL; + +void furi_hal_adc_init(void) { + furi_hal_adc_handle = malloc(sizeof(FuriHalAdcHandle)); + furi_hal_adc_handle->adc = ADC1; + furi_hal_adc_handle->mutex = furi_mutex_alloc(FuriMutexTypeNormal); +} + +FuriHalAdcHandle* furi_hal_adc_acquire(void) { + furi_check(furi_mutex_acquire(furi_hal_adc_handle->mutex, FuriWaitForever) == FuriStatusOk); + + furi_hal_power_insomnia_enter(); + + furi_hal_bus_enable(FuriHalBusADC); + + return furi_hal_adc_handle; +} + +void furi_hal_adc_release(FuriHalAdcHandle* handle) { + furi_check(handle); + + if(furi_hal_bus_is_enabled(FuriHalBusADC)) furi_hal_bus_disable(FuriHalBusADC); + + LL_VREFBUF_Disable(); + LL_VREFBUF_EnableHIZ(); + + furi_hal_power_insomnia_exit(); + + furi_check(furi_mutex_release(furi_hal_adc_handle->mutex) == FuriStatusOk); +} + +void furi_hal_adc_configure(FuriHalAdcHandle* handle) { + furi_hal_adc_configure_ex( + handle, + FuriHalAdcScale2048, + FuriHalAdcClockSync64, + FuriHalAdcOversample64, + FuriHalAdcSamplingtime247_5); +} + +void furi_hal_adc_configure_ex( + FuriHalAdcHandle* handle, + FuriHalAdcScale scale, + FuriHalAdcClock clock, + FuriHalAdcOversample oversample, + FuriHalAdcSamplingTime sampling_time) { + furi_check(handle); + furi_check(scale == FuriHalAdcScale2048 || scale == FuriHalAdcScale2500); + furi_check(clock <= FuriHalAdcClockSync64); + furi_check(oversample <= FuriHalAdcOversampleNone); + furi_check(sampling_time <= FuriHalAdcSamplingtime640_5); + + FuriHalCortexTimer timer; + + if(furi_hal_bus_is_enabled(FuriHalBusADC)) furi_hal_bus_disable(FuriHalBusADC); + + uint32_t trim_value = 0; + switch(scale) { + case FuriHalAdcScale2048: + LL_VREFBUF_SetVoltageScaling(LL_VREFBUF_VOLTAGE_SCALE0); + trim_value = LL_VREFBUF_SC0_GetCalibration() & 0x3FU; + handle->full_scale = 2048; + break; + case FuriHalAdcScale2500: + LL_VREFBUF_SetVoltageScaling(LL_VREFBUF_VOLTAGE_SCALE1); + trim_value = LL_VREFBUF_SC1_GetCalibration() & 0x3FU; + handle->full_scale = 2500; + break; + default: + furi_crash(); + } + LL_VREFBUF_SetTrimming(trim_value); + LL_VREFBUF_Enable(); + LL_VREFBUF_DisableHIZ(); + + timer = furi_hal_cortex_timer_get(500000); // 500ms to stabilize VREF + while(!LL_VREFBUF_IsVREFReady()) { + furi_check(!furi_hal_cortex_timer_is_expired(timer), "VREF fail"); + }; + + furi_hal_bus_enable(FuriHalBusADC); + + // ADC Common config + LL_ADC_CommonInitTypeDef ADC_CommonInitStruct = {0}; + ADC_CommonInitStruct.CommonClock = furi_hal_adc_clock[clock]; + furi_check( + LL_ADC_CommonInit(__LL_ADC_COMMON_INSTANCE(handle->adc), &ADC_CommonInitStruct) == + SUCCESS); + LL_ADC_SetCommonPathInternalCh( + __LL_ADC_COMMON_INSTANCE(handle->adc), + LL_ADC_PATH_INTERNAL_VREFINT | LL_ADC_PATH_INTERNAL_TEMPSENSOR | + LL_ADC_PATH_INTERNAL_VBAT); + + // ADC config part 1 + LL_ADC_InitTypeDef ADC_InitStruct = {0}; + ADC_InitStruct.Resolution = LL_ADC_RESOLUTION_12B; //-V1048 + ADC_InitStruct.DataAlignment = LL_ADC_DATA_ALIGN_RIGHT; + ADC_InitStruct.LowPowerMode = LL_ADC_LP_MODE_NONE; + furi_check(LL_ADC_Init(handle->adc, &ADC_InitStruct) == SUCCESS); + + // ADC config part 2: groups parameters + LL_ADC_REG_InitTypeDef ADC_REG_InitStruct = {0}; + ADC_REG_InitStruct.TriggerSource = LL_ADC_REG_TRIG_SOFTWARE; //-V1048 + ADC_REG_InitStruct.SequencerLength = LL_ADC_REG_SEQ_SCAN_DISABLE; + ADC_REG_InitStruct.SequencerDiscont = LL_ADC_REG_SEQ_DISCONT_DISABLE; + ADC_REG_InitStruct.ContinuousMode = LL_ADC_REG_CONV_SINGLE; + ADC_REG_InitStruct.DMATransfer = LL_ADC_REG_DMA_TRANSFER_NONE; + ADC_REG_InitStruct.Overrun = LL_ADC_REG_OVR_DATA_OVERWRITTEN; + furi_check(LL_ADC_REG_Init(handle->adc, &ADC_REG_InitStruct) == SUCCESS); + + // ADC config part 3: sequencer and channels + if(oversample == FuriHalAdcOversampleNone) { + LL_ADC_SetOverSamplingScope(handle->adc, LL_ADC_OVS_DISABLE); + } else { + LL_ADC_SetOverSamplingScope(handle->adc, LL_ADC_OVS_GRP_REGULAR_CONTINUED); + LL_ADC_ConfigOverSamplingRatioShift( + handle->adc, + furi_hal_adc_oversample_ratio[oversample], + furi_hal_adc_oversample_shift[oversample]); + } + + for(FuriHalAdcChannel channel = FuriHalAdcChannel0; channel < FuriHalAdcChannelNone; + channel++) { + // 47.5 cycles on 64MHz is first meaningful value for internal sources sampling + LL_ADC_SetChannelSamplingTime( + handle->adc, + furi_hal_adc_channel_map[channel], + furi_hal_adc_sampling_time[sampling_time]); + LL_ADC_SetChannelSingleDiff( + handle->adc, furi_hal_adc_channel_map[channel], LL_ADC_SINGLE_ENDED); + } + + // Disable ADC deep power down (enabled by default after reset state) + LL_ADC_DisableDeepPowerDown(handle->adc); + + // Enable ADC internal voltage regulator + LL_ADC_EnableInternalRegulator(handle->adc); + // Delay for ADC internal voltage regulator stabilization. + timer = furi_hal_cortex_timer_get(LL_ADC_DELAY_INTERNAL_REGUL_STAB_US); + while(!furi_hal_cortex_timer_is_expired(timer)) + ; + + // Run ADC self calibration + LL_ADC_StartCalibration(handle->adc, LL_ADC_SINGLE_ENDED); + // Poll for ADC effectively calibrated + while(LL_ADC_IsCalibrationOnGoing(handle->adc) != 0) + ; + // Delay between ADC end of calibration and ADC enable + size_t end = + DWT->CYCCNT + (LL_ADC_DELAY_CALIB_ENABLE_ADC_CYCLES * furi_hal_adc_clock_div[clock]); + while(DWT->CYCCNT < end) + ; + + // Enable ADC + LL_ADC_ClearFlag_ADRDY(handle->adc); + LL_ADC_Enable(handle->adc); + while(LL_ADC_IsActiveFlag_ADRDY(handle->adc) == 0) + ; +} + +uint16_t furi_hal_adc_read(FuriHalAdcHandle* handle, FuriHalAdcChannel channel) { + furi_check(handle); + furi_check(channel <= FuriHalAdcChannelVBAT); + furi_check(LL_ADC_IsEnabled(handle->adc) == 1); + furi_check(LL_ADC_IsDisableOngoing(handle->adc) == 0); + furi_check(LL_ADC_REG_IsConversionOngoing(handle->adc) == 0); + + LL_ADC_REG_SetSequencerRanks( + handle->adc, LL_ADC_REG_RANK_1, furi_hal_adc_channel_map[channel]); + + LL_ADC_REG_StartConversion(handle->adc); + + while(LL_ADC_IsActiveFlag_EOC(handle->adc) == 0) + ; + uint16_t value = LL_ADC_REG_ReadConversionData12(handle->adc); + + return value; +} + +float furi_hal_adc_convert_to_voltage(FuriHalAdcHandle* handle, uint16_t value) { + return (float)__LL_ADC_CALC_DATA_TO_VOLTAGE(handle->full_scale, value, LL_ADC_RESOLUTION_12B); +} + +float furi_hal_adc_convert_vref(FuriHalAdcHandle* handle, uint16_t value) { + UNUSED(handle); + return (float)__LL_ADC_CALC_VREFANALOG_VOLTAGE(value, LL_ADC_RESOLUTION_12B); +} + +float furi_hal_adc_convert_temp(FuriHalAdcHandle* handle, uint16_t value) { + return (float)__LL_ADC_CALC_TEMPERATURE(handle->full_scale, value, LL_ADC_RESOLUTION_12B); +} + +float furi_hal_adc_convert_vbat(FuriHalAdcHandle* handle, uint16_t value) { + return furi_hal_adc_convert_to_voltage(handle, value) * 3; +} diff --git a/targets/f7/furi_hal/furi_hal_clock.c b/targets/f7/furi_hal/furi_hal_clock.c index ad21031fe6..9184fa7157 100644 --- a/targets/f7/furi_hal/furi_hal_clock.c +++ b/targets/f7/furi_hal/furi_hal_clock.c @@ -121,6 +121,7 @@ void furi_hal_clock_init(void) { LL_RCC_SetSMPSClockSource(LL_RCC_SMPS_CLKSOURCE_HSI); LL_RCC_SetSMPSPrescaler(LL_RCC_SMPS_DIV_1); LL_RCC_SetRFWKPClockSource(LL_RCC_RFWKP_CLKSOURCE_LSE); + LL_RCC_SetADCClockSource(LL_RCC_ADC_CLKSOURCE_SYSCLK); FURI_LOG_I(TAG, "Init OK"); } diff --git a/targets/f7/furi_hal/furi_hal_infrared.c b/targets/f7/furi_hal/furi_hal_infrared.c index 6f2210cc1d..a0b166fad4 100644 --- a/targets/f7/furi_hal/furi_hal_infrared.c +++ b/targets/f7/furi_hal/furi_hal_infrared.c @@ -357,7 +357,7 @@ static void furi_hal_infrared_configure_tim_pwm_tx(uint32_t freq, float duty_cyc if(infrared_tx_output == FuriHalInfraredTxPinInternal) { LL_TIM_OC_SetCompareCH3( INFRARED_DMA_TIMER, - ((LL_TIM_GetAutoReload(INFRARED_DMA_TIMER) + 1) * (1 - duty_cycle))); + ((LL_TIM_GetAutoReload(INFRARED_DMA_TIMER) + 1) * (1.0f - duty_cycle))); LL_TIM_OC_EnablePreload(INFRARED_DMA_TIMER, LL_TIM_CHANNEL_CH3); /* LL_TIM_OCMODE_PWM2 set by DMA */ LL_TIM_OC_SetMode(INFRARED_DMA_TIMER, LL_TIM_CHANNEL_CH3, LL_TIM_OCMODE_FORCED_INACTIVE); @@ -368,7 +368,7 @@ static void furi_hal_infrared_configure_tim_pwm_tx(uint32_t freq, float duty_cyc } else if(infrared_tx_output == FuriHalInfraredTxPinExtPA7) { LL_TIM_OC_SetCompareCH1( INFRARED_DMA_TIMER, - ((LL_TIM_GetAutoReload(INFRARED_DMA_TIMER) + 1) * (1 - duty_cycle))); + ((LL_TIM_GetAutoReload(INFRARED_DMA_TIMER) + 1) * (1.0f - duty_cycle))); LL_TIM_OC_EnablePreload(INFRARED_DMA_TIMER, LL_TIM_CHANNEL_CH1); /* LL_TIM_OCMODE_PWM2 set by DMA */ LL_TIM_OC_SetMode(INFRARED_DMA_TIMER, LL_TIM_CHANNEL_CH1, LL_TIM_OCMODE_FORCED_INACTIVE); @@ -609,7 +609,7 @@ static void furi_hal_infrared_async_tx_free_resources(void) { } void furi_hal_infrared_async_tx_start(uint32_t freq, float duty_cycle) { - if((duty_cycle > 1) || (duty_cycle <= 0) || (freq > INFRARED_MAX_FREQUENCY) || + if((duty_cycle > 1.0f) || (duty_cycle <= 0.0f) || (freq > INFRARED_MAX_FREQUENCY) || (freq < INFRARED_MIN_FREQUENCY) || (infrared_tim_tx.data_callback == NULL)) { furi_crash(); } diff --git a/targets/f7/furi_hal/furi_hal_resources.c b/targets/f7/furi_hal/furi_hal_resources.c index b98d93769d..486c24230e 100644 --- a/targets/f7/furi_hal/furi_hal_resources.c +++ b/targets/f7/furi_hal/furi_hal_resources.c @@ -70,28 +70,88 @@ const GpioPin gpio_usb_dp = {.port = GPIOA, .pin = LL_GPIO_PIN_12}; const GpioPinRecord gpio_pins[] = { // 5V: 1 - {.pin = &gpio_ext_pa7, .name = "PA7", .number = 2, .debug = false}, - {.pin = &gpio_ext_pa6, .name = "PA6", .number = 3, .debug = false}, - {.pin = &gpio_ext_pa4, .name = "PA4", .number = 4, .debug = false}, - {.pin = &gpio_ext_pb3, .name = "PB3", .number = 5, .debug = false}, - {.pin = &gpio_ext_pb2, .name = "PB2", .number = 6, .debug = false}, - {.pin = &gpio_ext_pc3, .name = "PC3", .number = 7, .debug = false}, + {.pin = &gpio_ext_pa7, + .name = "PA7", + .channel = FuriHalAdcChannel12, + .number = 2, + .debug = false}, + {.pin = &gpio_ext_pa6, + .name = "PA6", + .channel = FuriHalAdcChannel11, + .number = 3, + .debug = false}, + {.pin = &gpio_ext_pa4, + .name = "PA4", + .channel = FuriHalAdcChannel9, + .number = 4, + .debug = false}, + {.pin = &gpio_ext_pb3, + .name = "PB3", + .channel = FuriHalAdcChannelNone, + .number = 5, + .debug = false}, + {.pin = &gpio_ext_pb2, + .name = "PB2", + .channel = FuriHalAdcChannelNone, + .number = 6, + .debug = false}, + {.pin = &gpio_ext_pc3, + .name = "PC3", + .channel = FuriHalAdcChannel4, + .number = 7, + .debug = false}, // GND: 8 // Space // 3v3: 9 - {.pin = &gpio_swclk, .name = "PA14", .number = 10, .debug = true}, + {.pin = &gpio_swclk, + .name = "PA14", + .channel = FuriHalAdcChannelNone, + .number = 10, + .debug = true}, // GND: 11 - {.pin = &gpio_swdio, .name = "PA13", .number = 12, .debug = true}, - {.pin = &gpio_usart_tx, .name = "PB6", .number = 13, .debug = true}, - {.pin = &gpio_usart_rx, .name = "PB7", .number = 14, .debug = true}, - {.pin = &gpio_ext_pc1, .name = "PC1", .number = 15, .debug = false}, - {.pin = &gpio_ext_pc0, .name = "PC0", .number = 16, .debug = false}, - {.pin = &gpio_ibutton, .name = "PB14", .number = 17, .debug = true}, + {.pin = &gpio_swdio, + .name = "PA13", + .channel = FuriHalAdcChannelNone, + .number = 12, + .debug = true}, + {.pin = &gpio_usart_tx, + .name = "PB6", + .channel = FuriHalAdcChannelNone, + .number = 13, + .debug = true}, + {.pin = &gpio_usart_rx, + .name = "PB7", + .channel = FuriHalAdcChannelNone, + .number = 14, + .debug = true}, + {.pin = &gpio_ext_pc1, + .name = "PC1", + .channel = FuriHalAdcChannel2, + .number = 15, + .debug = false}, + {.pin = &gpio_ext_pc0, + .name = "PC0", + .channel = FuriHalAdcChannel1, + .number = 16, + .debug = false}, + {.pin = &gpio_ibutton, + .name = "PB14", + .channel = FuriHalAdcChannelNone, + .number = 17, + .debug = true}, // GND: 18 /* Dangerous pins, may damage hardware */ - {.pin = &gpio_speaker, .name = "PB8", .debug = true}, - {.pin = &gpio_infrared_tx, .name = "PB9", .debug = true}, + {.pin = &gpio_speaker, + .name = "PB8", + .channel = FuriHalAdcChannelNone, + .number = 0, + .debug = true}, + {.pin = &gpio_infrared_tx, + .name = "PB9", + .channel = FuriHalAdcChannelNone, + .number = 0, + .debug = true}, }; const size_t gpio_pins_count = COUNT_OF(gpio_pins); diff --git a/targets/f7/furi_hal/furi_hal_resources.h b/targets/f7/furi_hal/furi_hal_resources.h index bb01b9bcdd..99768654d9 100644 --- a/targets/f7/furi_hal/furi_hal_resources.h +++ b/targets/f7/furi_hal/furi_hal_resources.h @@ -1,9 +1,7 @@ #pragma once #include - -#include -#include +#include #ifdef __cplusplus extern "C" { @@ -41,6 +39,7 @@ typedef struct { typedef struct { const GpioPin* pin; const char* name; + const FuriHalAdcChannel channel; const uint8_t number; const bool debug; } GpioPinRecord; @@ -220,10 +219,11 @@ void furi_hal_resources_deinit_early(void); void furi_hal_resources_init(void); -/** - * Get a corresponding external connector pin number for a gpio - * @param gpio GpioPin - * @return pin number or -1 if gpio is not on the external connector +/** Get a corresponding external connector pin number for a gpio + * + * @param gpio GpioPin + * + * @return pin number or -1 if gpio is not on the external connector */ int32_t furi_hal_resources_get_ext_pin_number(const GpioPin* gpio); diff --git a/targets/f7/furi_hal/furi_hal_subghz.c b/targets/f7/furi_hal/furi_hal_subghz.c index 15f2ad01ac..1350bcd584 100644 --- a/targets/f7/furi_hal/furi_hal_subghz.c +++ b/targets/f7/furi_hal/furi_hal_subghz.c @@ -52,7 +52,6 @@ typedef struct { const GpioPin* async_mirror_pin; int8_t rolling_counter_mult; - bool ext_power_amp : 1; bool extended_frequency_i : 1; bool bypass_region : 1; } FuriHalSubGhz; @@ -62,7 +61,6 @@ volatile FuriHalSubGhz furi_hal_subghz = { .regulation = SubGhzRegulationTxRx, .async_mirror_pin = NULL, .rolling_counter_mult = 1, - .ext_power_amp = false, .extended_frequency_i = false, .bypass_region = false, }; @@ -83,14 +81,6 @@ void furi_hal_subghz_set_bypass_region(bool enabled) { furi_hal_subghz.bypass_region = enabled; } -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(void) { - return furi_hal_subghz.ext_power_amp; -} - void furi_hal_subghz_set_async_mirror_pin(const GpioPin* pin) { furi_hal_subghz.async_mirror_pin = pin; } diff --git a/targets/f7/furi_hal/furi_hal_subghz.h b/targets/f7/furi_hal/furi_hal_subghz.h index 68c040928a..10d89947a9 100644 --- a/targets/f7/furi_hal/furi_hal_subghz.h +++ b/targets/f7/furi_hal/furi_hal_subghz.h @@ -238,54 +238,7 @@ bool furi_hal_subghz_is_async_tx_complete(void); */ void furi_hal_subghz_stop_async_tx(void); -// /** Initialize and switch to power save mode Used by internal API-HAL -// * initialization routine Can be used to reinitialize device to safe state and -// * send it to sleep -// * @return true if initialisation is successfully -// */ -// bool furi_hal_subghz_init_check(void); - -// /** Switching between internal and external radio -// * @param state SubGhzRadioInternal or SubGhzRadioExternal -// * @return true if switching is successful -// */ -// bool furi_hal_subghz_init_radio_type(SubGhzRadioType state); - -// /** Get current radio -// * @return SubGhzRadioInternal or SubGhzRadioExternal -// */ -// SubGhzRadioType furi_hal_subghz_get_radio_type(void); - -// /** Check for a radio module -// * @return true if check is successful -// */ -// bool furi_hal_subghz_check_radio(void); - -// /** Turn on the power of the external radio module -// * @return true if power-up is successful -// */ -// bool furi_hal_subghz_enable_ext_power(void); - -// /** Turn off the power of the external radio module -// */ -// void furi_hal_subghz_disable_ext_power(void); - -// /** If true - disable 5v power of the external radio module -// */ -// void furi_hal_subghz_set_external_power_disable(bool state); - -// /** Get the current state of the external power disable flag -// */ -// bool furi_hal_subghz_get_external_power_disable(void); - -// /** Set what radio module we will be using -// */ -// void furi_hal_subghz_select_radio_type(SubGhzRadioType state); - -// External CC1101 Ebytes power amplifier control -void furi_hal_subghz_set_ext_power_amp(bool enabled); - -bool furi_hal_subghz_get_ext_power_amp(void); +// External CC1101 Ebytes power amplifier control is now enabled by default #ifdef __cplusplus } diff --git a/targets/f7/furi_hal/furi_hal_version.c b/targets/f7/furi_hal/furi_hal_version.c index 8c650bf0cf..04479a7b85 100644 --- a/targets/f7/furi_hal/furi_hal_version.c +++ b/targets/f7/furi_hal/furi_hal_version.c @@ -8,6 +8,8 @@ #include #include +#include + #define TAG "FuriHalVersion" #define FURI_HAL_VERSION_OTP_HEADER_MAGIC 0xBABE @@ -241,7 +243,10 @@ uint8_t furi_hal_version_get_hw_body(void) { } FuriHalVersionColor furi_hal_version_get_hw_color(void) { - return furi_hal_version.board_color; + if(momentum_settings.spoof_color == FuriHalVersionColorUnknown) { + return furi_hal_version.board_color; + } + return momentum_settings.spoof_color; } uint8_t furi_hal_version_get_hw_connect(void) { diff --git a/targets/f7/target.json b/targets/f7/target.json index fc1a575839..4c79b2e666 100644 --- a/targets/f7/target.json +++ b/targets/f7/target.json @@ -22,6 +22,7 @@ "print", "flipper7", "furi", + "tlsf", "freertos", "stm32wb", "hwdrivers", diff --git a/targets/furi_hal_include/furi_hal.h b/targets/furi_hal_include/furi_hal.h index 193398a522..458ee501f6 100644 --- a/targets/furi_hal_include/furi_hal.h +++ b/targets/furi_hal_include/furi_hal.h @@ -12,6 +12,7 @@ struct STOP_EXTERNING_ME {}; #include #include +#include #include #include #include diff --git a/targets/furi_hal_include/furi_hal_adc.h b/targets/furi_hal_include/furi_hal_adc.h new file mode 100644 index 0000000000..ecbdad2ca3 --- /dev/null +++ b/targets/furi_hal_include/furi_hal_adc.h @@ -0,0 +1,229 @@ +/** + * @file furi_hal_adc.h + * @brief ADC HAL API + * + * For the sake of simplicity this API implements only small subset + * of what ADC is actually capable of. Feel free to visit Reference + * Manual for STM32WB series and implement any other modes by your + * self. + * + * Couple things to keep in mind: + * + * - ADC resolution is 12 bits, but effective number of bits is ~10 at the best + * and further depends on how you use and configure it. + * - Analog domain is fed from SMPS which is quite noisy. + * - Because of that we use internal on-chip voltage reference for ADC. + * - It's capable of producing 2 voltages: 2.5V and 2.048V. This is the scale + * for your signal. + * - Only single ended mode is available. But you can implement differential one + * by using low level controls directly. + * - No DMA or interrupt API available at this point. But can be implemented + * with low level controls. + * + * + * How to use: + * + * - furi_hal_gpio_init - Configure your pins in `GpioModeAnalog` + * - furi_hal_adc_acquire - acquire ADC handle to work with + * - furi_hal_adc_configure - configure ADC block + * - furi_hal_adc_read - read value + * - furi_hal_adc_release - release ADC handle + */ + +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct FuriHalAdcHandle FuriHalAdcHandle; + +typedef enum { + FuriHalAdcScale2048, /**< 2.048V scale */ + FuriHalAdcScale2500, /**< 2.5V scale */ +} FuriHalAdcScale; + +typedef enum { + FuriHalAdcClockSync16, /**< 16MHZ, synchronous */ + FuriHalAdcClockSync32, /**< 32MHZ, synchronous */ + FuriHalAdcClockSync64, /**< 64MHz, synchronous */ +} FuriHalAdcClock; + +typedef enum { + FuriHalAdcOversample2, /**< ADC will take 2 samples per each value */ + FuriHalAdcOversample4, /**< ADC will take 4 samples per each value */ + FuriHalAdcOversample8, /**< ADC will take 8 samples per each value */ + FuriHalAdcOversample16, /**< ADC will take 16 samples per each value */ + FuriHalAdcOversample32, /**< ADC will take 32 samples per each value */ + FuriHalAdcOversample64, /**< ADC will take 64 samples per each value */ + FuriHalAdcOversample128, /**< ADC will take 128 samples per each value */ + FuriHalAdcOversample256, /**< ADC will take 256 samples per each value */ + FuriHalAdcOversampleNone, /**< disable oversampling */ +} FuriHalAdcOversample; + +typedef enum { + FuriHalAdcSamplingtime2_5, /**< Sampling time 2.5 ADC clock */ + FuriHalAdcSamplingtime6_5, /**< Sampling time 6.5 ADC clock */ + FuriHalAdcSamplingtime12_5, /**< Sampling time 12.5 ADC clock */ + FuriHalAdcSamplingtime24_5, /**< Sampling time 24.5 ADC clock */ + FuriHalAdcSamplingtime47_5, /**< Sampling time 47.5 ADC clock */ + FuriHalAdcSamplingtime92_5, /**< Sampling time 92.5 ADC clock */ + FuriHalAdcSamplingtime247_5, /**< Sampling time 247.5 ADC clock */ + FuriHalAdcSamplingtime640_5, /**< Sampling time 640.5 ADC clock */ +} FuriHalAdcSamplingTime; + +typedef enum { + /* Channels 0 - 5 are fast channels */ + FuriHalAdcChannel0, /**< Internal channel, see `FuriHalAdcChannelVREFINT`. */ + FuriHalAdcChannel1, /**< Channel 1p */ + FuriHalAdcChannel2, /**< Channel 2p or 1n */ + FuriHalAdcChannel3, /**< Channel 3p or 2n */ + FuriHalAdcChannel4, /**< Channel 4p or 3n */ + FuriHalAdcChannel5, /**< Channel 5p or 4n */ + /* Channels 6 - 18 are slow channels */ + FuriHalAdcChannel6, /**< Channel 6p or 5n */ + FuriHalAdcChannel7, /**< Channel 7p or 6n */ + FuriHalAdcChannel8, /**< Channel 8p or 7n */ + FuriHalAdcChannel9, /**< Channel 9p or 8n */ + FuriHalAdcChannel10, /**< Channel 10p or 9n */ + FuriHalAdcChannel11, /**< Channel 11p or 10n */ + FuriHalAdcChannel12, /**< Channel 12p or 11n */ + FuriHalAdcChannel13, /**< Channel 13p or 12n */ + FuriHalAdcChannel14, /**< Channel 14p or 13n */ + FuriHalAdcChannel15, /**< Channel 15p or 14n */ + FuriHalAdcChannel16, /**< Channel 16p or 15n */ + FuriHalAdcChannel17, /**< Internal channel, see `FuriHalAdcChannelTEMPSENSOR`. */ + FuriHalAdcChannel18, /**< Internal channel, see `FuriHalAdcChannelVBAT`. */ + /* Special Channels: combines one of the 0-18 channel and additional internal peripherals */ + FuriHalAdcChannelVREFINT, /**< Special channel for VREFINT, used for calibration and self test */ + FuriHalAdcChannelTEMPSENSOR, /**< Special channel for on-die temperature sensor, requires at least 5us of sampling time */ + FuriHalAdcChannelVBAT, /**< Special channel for VBAT/3 voltage, requires at least 12us of sampling time */ + /* Special value to indicate that pin is not connected to ADC */ + FuriHalAdcChannelNone, /**< No channel */ +} FuriHalAdcChannel; + +/** Initialize ADC subsystem */ +void furi_hal_adc_init(void); + +/** Acquire ADC handle + * + * Enables appropriate power and clocking domains + * + * @return FuriHalAdcHandle pointer + */ +FuriHalAdcHandle* furi_hal_adc_acquire(void); + +/** Release ADC handle + * + * @param handle The ADC handle + */ +void furi_hal_adc_release(FuriHalAdcHandle* handle); + +/** Configure with default parameters and enable ADC + * + * Parameters used: + * - FuriHalAdcScale2048 - 2.048V VREF Scale. Your signal should be in 0 - + * 2.048V range. + * - FuriHalAdcClockSync64 - Clocked from sysclk bus at 64MHz in synchronous + * mode. Fast, no delay on data bus access. + * - FuriHalAdcOversample64 - Going to acquire and average 64 samples. For + * circuits with slowly or not changing signal. Total time per one read: + * (1/64)*(12.5+247.5)*64 = 260us. The best results you'll get if your signal + * will stay on the same level all this time. + * - FuriHalAdcSamplingtime247_5 - Sampling(transfer from source to internal + * sampling capacitor) time is 247.5 ADC clocks: (1/64)*247.5 = 3.8671875us. + * For relatively high impedance circuits. + * + * Also keep your measurement circuit impedance under 10KOhm or oversampling + * results will be compromised. Verify your signal with oscilloscope(you may + * need fast oscilloscope: 200MHz bandwidth, 125MS/s), ensure that signal is not + * distorted by sampling. + * + * Those parameters were optimized for 0 - 2.048 voltage measurement with ~0.1% + * precision. You can get more, but it will require some magic. + * + * @param handle The ADC handle + */ +void furi_hal_adc_configure(FuriHalAdcHandle* handle); + +/** Configure with extended parameters and enable ADC + * + * General advice is to start with default parameters, figure out what exactly + * is not working for you and then tune it. Also in some cases changing + * circuit(adding caps, lowering impedance, adding opamp) may be better than changing + * parameters. + * + * @warning In general ADC is a world of magic: make sure that you understand + * how your circuit and ADC works. Then carefully read STM32WB + * series reference manual. Setting incorrect parameters leads to + * very poor results. Also internal channels require special + * settings. + * + * @param handle The ADC handle + * @param[in] scale The ADC voltage scale + * @param[in] clock The ADC clock + * @param[in] oversample The ADC oversample mode + * @param[in] sampling_time The ADC sampling time + */ +void furi_hal_adc_configure_ex( + FuriHalAdcHandle* handle, + FuriHalAdcScale scale, + FuriHalAdcClock clock, + FuriHalAdcOversample oversample, + FuriHalAdcSamplingTime sampling_time); + +/** Read single ADC value + * + * @param handle The ADC handle + * @param[in] channel The channel to sample + * + * @return value, 12 bits + */ +uint16_t furi_hal_adc_read(FuriHalAdcHandle* handle, FuriHalAdcChannel channel); + +/** Convert sampled value to voltage + * + * @param handle The ADC handle + * @param[in] value The value acquired with `furi_hal_adc_read` + * + * @return Voltage in mV + */ +float furi_hal_adc_convert_to_voltage(FuriHalAdcHandle* handle, uint16_t value); + +/** Convert sampled VREFINT value to voltage + * + * @param handle The ADC handle + * @param[in] value The value acquired with `furi_hal_adc_read` for + * `FuriHalAdcChannelVREFINT` channel + * + * @return Voltage in mV + */ +float furi_hal_adc_convert_vref(FuriHalAdcHandle* handle, uint16_t value); + +/** Convert sampled TEMPSENSOR value to temperature + * + * @param handle The ADC handle + * @param[in] value The value acquired with `furi_hal_adc_read` for + * `FuriHalAdcChannelTEMPSENSOR` channel + * + * @return temperature in degree C + */ +float furi_hal_adc_convert_temp(FuriHalAdcHandle* handle, uint16_t value); + +/** Convert sampled VBAT value to voltage + * + * @param handle The ADC handle + * @param[in] value The value acquired with `furi_hal_adc_read` for + * `FuriHalAdcChannelVBAT` channel + * + * @return Voltage in mV + */ +float furi_hal_adc_convert_vbat(FuriHalAdcHandle* handle, uint16_t value); + +#ifdef __cplusplus +} +#endif diff --git a/targets/furi_hal_include/furi_hal_random.h b/targets/furi_hal_include/furi_hal_random.h index 20c6c3357d..fab62083f1 100644 --- a/targets/furi_hal_include/furi_hal_random.h +++ b/targets/furi_hal_include/furi_hal_random.h @@ -6,7 +6,7 @@ extern "C" { #endif -#define FURI_HAL_RANDOM_MAX 0xFFFFFFFF +#define FURI_HAL_RANDOM_MAX 0xFFFFFFFFU /** Initialize random subsystem */ void furi_hal_random_init(void); diff --git a/targets/furi_hal_include/furi_hal_version.h b/targets/furi_hal_include/furi_hal_version.h index 0cdea50733..bbcb77c0d5 100644 --- a/targets/furi_hal_include/furi_hal_version.h +++ b/targets/furi_hal_include/furi_hal_version.h @@ -35,6 +35,7 @@ typedef enum { FuriHalVersionColorBlack = 0x01, FuriHalVersionColorWhite = 0x02, FuriHalVersionColorTransparent = 0x03, + FuriHalVersionColorCount, } FuriHalVersionColor; /** Device Regions */