Przeglądaj źródła

Add magspoof from https://github.com/xMasterX/all-the-plugins

git-subtree-dir: magspoof
git-subtree-mainline: c23b1238dc3a2ca1773923a5f21d22395276c63c
git-subtree-split: 301abddf087ed3a658aaebc4c3f4bdd54638fc04
Willy-JL 2 lat temu
rodzic
commit
d732f65ff1
46 zmienionych plików z 3319 dodań i 0 usunięć
  1. 2 0
      magspoof/.gitattributes
  2. 13 0
      magspoof/.github/FUNDING.yml
  3. 1 0
      magspoof/.gitsubtree
  4. 21 0
      magspoof/LICENSE
  5. 80 0
      magspoof/README.md
  6. 23 0
      magspoof/application.fam
  7. 7 0
      magspoof/assets/SamyExampleImage.mag
  8. 7 0
      magspoof/assets/SamyExpiredCard.mag
  9. 6 0
      magspoof/assets/TestMagstripe.mag
  10. BIN
      magspoof/assets/wiring_diagram.png
  11. 479 0
      magspoof/helpers/mag_helpers.c
  12. 25 0
      magspoof/helpers/mag_helpers.h
  13. 583 0
      magspoof/helpers/mag_text_input.c
  14. 82 0
      magspoof/helpers/mag_text_input.h
  15. 45 0
      magspoof/helpers/mag_types.h
  16. BIN
      magspoof/icons/DolphinMafia_115x62.png
  17. BIN
      magspoof/icons/DolphinNice_96x59.png
  18. BIN
      magspoof/icons/KeyBackspaceSelected_16x9.png
  19. BIN
      magspoof/icons/KeyBackspace_16x9.png
  20. BIN
      magspoof/icons/KeySaveSelected_24x11.png
  21. BIN
      magspoof/icons/KeySave_24x11.png
  22. BIN
      magspoof/icons/mag_10px.png
  23. BIN
      magspoof/icons/mag_file_10px.png
  24. 256 0
      magspoof/mag.c
  25. 312 0
      magspoof/mag_device.c
  26. 58 0
      magspoof/mag_device.h
  27. 106 0
      magspoof/mag_i.h
  28. 30 0
      magspoof/scenes/mag_scene.c
  29. 29 0
      magspoof/scenes/mag_scene.h
  30. 40 0
      magspoof/scenes/mag_scene_about.c
  31. 15 0
      magspoof/scenes/mag_scene_config.h
  32. 49 0
      magspoof/scenes/mag_scene_delete_confirm.c
  33. 39 0
      magspoof/scenes/mag_scene_delete_success.c
  34. 93 0
      magspoof/scenes/mag_scene_emulate.c
  35. 264 0
      magspoof/scenes/mag_scene_emulate_config.c
  36. 20 0
      magspoof/scenes/mag_scene_exit_confirm.c
  37. 24 0
      magspoof/scenes/mag_scene_file_select.c
  38. 82 0
      magspoof/scenes/mag_scene_input_name.c
  39. 37 0
      magspoof/scenes/mag_scene_input_value.c
  40. 185 0
      magspoof/scenes/mag_scene_read.c
  41. 21 0
      magspoof/scenes/mag_scene_read.h
  42. 43 0
      magspoof/scenes/mag_scene_save_success.c
  43. 50 0
      magspoof/scenes/mag_scene_saved_info.c
  44. 81 0
      magspoof/scenes/mag_scene_saved_menu.c
  45. 71 0
      magspoof/scenes/mag_scene_start.c
  46. 40 0
      magspoof/scenes/mag_scene_under_construction.c

+ 2 - 0
magspoof/.gitattributes

@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto

+ 13 - 0
magspoof/.github/FUNDING.yml

@@ -0,0 +1,13 @@
+# These are supported funding model platforms
+
+github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
+patreon: # Replace with a single Patreon username
+open_collective: # Replace with a single Open Collective username
+ko_fi: # Replace with a single Ko-fi username
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
+custom: www.buymeacoffee.com/zweiss

+ 1 - 0
magspoof/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/xMasterX/all-the-plugins dev non_catalog_apps/magspoof_flipper

+ 21 - 0
magspoof/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Zachary Weiss
+
+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.

+ 80 - 0
magspoof/README.md

@@ -0,0 +1,80 @@
+# magspoof_flipper
+WIP of MagSpoof for the Flipper Zero. Basic TX of saved files confirmed working against an MSR90 with an external H-bridge module mirroring Samy Kamkar's design. RFID coil output weaker; able to be picked up/detected by more compact mag readers such as Square, but yet to have success with it being decoded/parsed properly. Additional investigation into alternate internal TX options (CC1101, ST25R3916, piezo) underway; tentatively, RFID coil + speaker (`LF + P` config setting) results in the strongest internal TX tested to date but still weaker than a dedicated external module or an actual card swipe (and sounds like a dial-up modem from hell). Sample files with test data are included in `assets` for anyone wishing to experiment.
+
+Disclaimer: use responsibly, and at your own risk. While in my testing, I've seen no reason to believe this could damage the RFID (or other) hardware, this is inherently driving the coil in ways it was not designed or intended for; I take no responsibility for fried/bricked Flippers. Similarly, please only use this with magstripe cards and mag readers you own — this is solely meant as a proof of concept for educational purposes. I neither condone nor am sympathetic to malicious uses of my code.
+
+## Optional GPIO TX Module
+For those desiring better TX than the internal RFID coil can offer, one can build the module below, consisting of an H-bridge, a capacitor, and a coil.
+
+<img src="https://user-images.githubusercontent.com/20050953/215654078-1f4b370e-21b3-4324-b63c-3bbbc643120e.png" alt="Wiring diagram" title="Wiring diagram" style="height:320px">
+
+
+## TODO
+Known bugs:
+- [X] File format issues when Track 2 data exists but Track 1 is left empty; doesn't seem to be setting the Track 2 field with anything (doesn't overwrite existing data). However, `flipper_format_read_string()` doesn't seem to return `false`. Is the bug in my code, or with `flipper_format`?
+  - [X] Review how it's done in [unirfremix (Sub-GHz Remote)](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/applications/main/unirfremix/unirfremix_app.c), as IIRC that can handle empty keys, despite using the `flipper_format` lib for parsing.
+- [X] Attempting to play a track that doesn't have data results in a crash (as one might expect). Need to lock out users from selecting empty tracks in the config menu or do better error handling (*Doesn't crash now, but still should probably prevent users from being able to select*)
+- [ ] Custom text input scene with expanded characterset (Add Manually) has odd behavior when navigating the keys near the numpad
+
+Emulation:
+- [X] Validate arha's bitmap changes, transition over to it fully
+- [X] Test piezo TX (prelim tests promising)
+- [ ] General code cleanup
+- [X] Reverse track precompute & replay
+- [ ] Parameter tuning, find best defaults, troubleshoot improperly parsed TX
+- [ ] Pursue skunkworks TX improvement ideas listed below
+- [ ] Remove or reimplement interpacket 
+  - [ ] Verify `furi_delay_us` aliasing to `64us`
+
+Scenes:
+- [X] Finish emulation config scene (reverse track functionality; possibly expand settings list to include prefix/between/suffix options)
+- [ ] "Edit" scene (generalize `input_value`)
+- [ ] "Rename" scene (generalize `input_name`)
+
+File management:
+- [ ] Update Add Manually flow to reflect new file format (currently only sets Track 2)
+- [ ] Validation of card track data?
+- [ ] Parsing loaded files into human-readable fields? (would we need to specify card type to decode correctly?)
+
+## Skunkworks ideas
+Internal TX improvements:
+- [ ] Attempt downstream modulation techniques in addition to upstream, like the LF RFID worker does when writing.
+- [ ] Implement using the timer system, rather than direct-writing to pins
+- [X] Use the NFC (HF RFID) coil instead of or in addition to the LF coil (likely unfruitful from initial tests; we can enable/disable the oscillating field, but even with transparent mode to the ST25R3916, it seems we don't get low-enough-level control to pull it high/low correctly) 
+- [ ] Add "subcarriers" to each half-bit transmitted (wiggle the pin high and low rapidly)
+  - [ ] Piezo subcarrier tests
+  - [ ] LF subcarrier tests
+  - [X] Retry NFC oscillating field? 
+
+External RX options:
+1. [TTL / PS/2 mag reader connected to UART](https://www.alibaba.com/product-detail/Mini-portable-12-3-tracks-usb_60679900708.html) (bulky, harder to source, but likely easiest to read over GPIO, and means one can read all tracks)
+2. Square audio jack mag reader (this may be DOA; seems like newer versions of the Square modules have some form of preprocessing that also modifies the signal, perhaps in an effort to discourage folks using their hardware independent of their software. Thanks arha for your work investigating this)
+3. Some [read-head](https://www.alibaba.com/product-detail/POS-1-2-3-triple-track_60677205741.html) directly connected to GPIO, ADC'd, and parsed all on the Flipper. Likely the most compact and cheapest module option, but also would require some signal-processing cleverness.
+4. USB HID input over pre-existing USB C port infeasible; seems the FZ cannot act as an HID host (MCU is the STM32WB55RGV6TR).
+5. Custom USB HID host hat based on the [MAX3421E](https://www.analog.com/en/products/max3421e.html) (USB Host Controller w/ SPI), like the [Arduino USB Host Shield](https://docs.arduino.cc/retired/shields/arduino-usb-host-shield). Would be a large but worthwhile project in its own right, and would let one connect any USB HID reader they desire (or other HID devices for other projects). Suggestion credit to arha.
+6. Implement a software/firmware USB host solution over GPIO like [esp32_usb_soft_host (for ESP32)](https://github.com/sdima1357/esp32_usb_soft_host) or [V-USB (for AVR)](https://www.obdev.at/products/vusb/index.html). Suggestion credit to arha. Also a massive undertaking, but valuable in and of itself.
+
+## arha todo & notes
+Attempting to exploit flipper hardware to some extent
+
+- [X] Preprocess all MSR data into bitwise arrays, including manchester encoding. 
+- [ ] Feed bits from timers
+- [ ] Sync to the lfrfid timer and experiment representing a field flip with a few cycles of a high frequency carrier, like the 125khz lfrfid one. Perhaps mag readers' frontends will lowpass such signals, and keep only the low frequency component, in an attempt to drown out nearby noise?
+- [X] Can the CC1101 radio be used in any way? Driving it from GD0 can achieve 50us, or about 10khz. Probably more with sync/packet mode. **Currently under testing**. The signal is extra noisy with a very wide bandwidth, but, in theory, it can work
+- [ ] Can the 5V pin act as a coil driver? I've read reports it can drive 0.4A, other reports it can drive 2A. It boils down to bq25896 being fast enough. Ref: bq25896_enable_otg, which will probably need bypassing kernel libs and calling furi_hal_i2c_tx/furi_hal_i2c_tx whatever calls from Cube libs.
+- [ ] Investigate transparent mode on 3916
+- [ ] Can the piezo be used at its resonant frequency? I've seen LF signals being emulated with [nothing but headphones](https://github.com/smre/DCF77/blob/master/DCF77.py#L124) running a subharmonic; the wheel brake on some carts seems to react to audiofreq signals (or the RF emission from driving a speaker)
+
+----
+## Credits
+This project interpolates work from [Samy Kamkar](https://github.com/samyk/)'s [original MagSpoof project](https://github.com/samyk/magspoof), [Alexey D. (dunaevai135)](https://github.com/dunaevai135/) & [Alexandr Yaroshevich](https://github.com/AYaro)'s [Flipper hackathon project](https://github.com/dunaevai135/flipperzero-firmware/tree/dev/applications/magspoof), and the [Flipper team](https://github.com/flipperdevices)'s [LF RFID](https://github.com/flipperdevices/flipperzero-firmware/tree/dev/applications/main/lfrfid) and [SubGhz](https://github.com/flipperdevices/flipperzero-firmware/tree/dev/applications/main/subghz) apps.  
+
+Many thanks to everyone who has helped in addition to those above, most notably: 
+- [arha](https://github.com/arha) for bitmapping work, skunkworks testing, and innumerable suggestions/ideas/feedback (now a collaborator!)
+- [Zalán Kórósi (Z4urce)](https://github.com/Z4urce) for an earlier app icon
+- [Salvatore Sanfilippo (antirez)](https://github.com/antirez) for bitmapping suggestions and general C wisdom
+- [skotopes](https://github.com/skotopes) for RFID consultation
+- [Tiernan (NVX)](https://github.com/nvx) + dlz for NFC consultation
+- davethepirate for EE insight and acting as a sounding board
+- [cool4uma](https://github.com/cool4uma) for their work on custom text_input scenes 
+- Everyone else I've had the pleasure of chatting with!

+ 23 - 0
magspoof/application.fam

@@ -0,0 +1,23 @@
+App(
+    appid="mag",
+    name="MagSpoof",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="mag_app",
+    cdefines=["APP_MAG"],
+    requires=[
+        "gui",
+        "storage",
+        "notification",
+        "dialogs",
+    ],
+    provides=[],
+    stack_size=6 * 1024,
+    order=64,  # keep it at the bottom of the list while still WIP
+    fap_icon="icons/mag_10px.png",
+    fap_category="GPIO",
+    fap_icon_assets="icons",
+    fap_version=(0, 5),  # major, minor
+    fap_description="WIP MagSpoof port using the RFID subsystem",
+    fap_author="Zachary Weiss",
+    fap_weburl="https://github.com/zacharyweiss/magspoof_flipper",
+)

+ 7 - 0
magspoof/assets/SamyExampleImage.mag

@@ -0,0 +1,7 @@
+Filetype: Flipper Mag device
+Version: 1
+# Mag device track data
+# Data matching image example in Samy's repo, for ease of comparison
+Track 1: %B426684131234567^LASTNAME/FIRST^YYMMSSSDDDDDDDDDDDDDDDDDDDDDDDDD?
+Track 2: ;426684131234567=230188855555555555555?
+Track 3: 

+ 7 - 0
magspoof/assets/SamyExpiredCard.mag

@@ -0,0 +1,7 @@
+Filetype: Flipper Mag device
+Version: 1
+# Mag device track data
+# Found in samyk's MagSpoof branch f150bb783237051fba7e4e6ed96a722e542a9663; using as test data, card is long expired
+Track 1: %B493173000682759^URISTA HDZ-IVAN JAVIER    ^150220100234000000?
+Track 2: ;493173000682759=15022100000234?
+Track 3: 

+ 6 - 0
magspoof/assets/TestMagstripe.mag

@@ -0,0 +1,6 @@
+Filetype: Flipper Mag device
+Version: 1
+# Mag device track data
+Track 1: %B123456781234567^LASTNAME/FIRST^YYMMSSSDDDDDDDDDDDDDDDDDDDDDDDDD?
+Track 2: ;123456781234567=YYMMSSSDDDDDDDDDDDDDD?
+Track 3: 

BIN
magspoof/assets/wiring_diagram.png


+ 479 - 0
magspoof/helpers/mag_helpers.c

@@ -0,0 +1,479 @@
+#include "mag_helpers.h"
+
+#define TAG "MagHelpers"
+
+// Haviv Board - pins gpio_ext_pa7 & gpio_ext_pa6 was swapped.
+#define GPIO_PIN_A &gpio_ext_pa7
+#define GPIO_PIN_B &gpio_ext_pa6
+#define GPIO_PIN_ENABLE &gpio_ext_pa4
+#define RFID_PIN_OUT &gpio_rfid_carrier_out
+
+#define ZERO_PREFIX 25 // n zeros prefix
+#define ZERO_BETWEEN 53 // n zeros between tracks
+#define ZERO_SUFFIX 25 // n zeros suffix
+
+// bits per char on a given track
+const uint8_t bitlen[] = {7, 5, 5};
+// char offset by track
+const int sublen[] = {32, 48, 48};
+
+uint8_t last_value = 2;
+
+void play_halfbit(bool value, MagSetting* setting) {
+    switch(setting->tx) {
+    case MagTxStateRFID:
+        furi_hal_gpio_write(RFID_PIN_OUT, value);
+        /*furi_hal_gpio_write(RFID_PIN_OUT, !value);
+        furi_hal_gpio_write(RFID_PIN_OUT, value);
+        furi_hal_gpio_write(RFID_PIN_OUT, !value);
+        furi_hal_gpio_write(RFID_PIN_OUT, value);*/
+        break;
+    case MagTxStateGPIO:
+        furi_hal_gpio_write(GPIO_PIN_A, value);
+        furi_hal_gpio_write(GPIO_PIN_B, !value);
+        break;
+    case MagTxStatePiezo:
+        furi_hal_gpio_write(&gpio_speaker, value);
+        /*furi_hal_gpio_write(&gpio_speaker, !value);
+        furi_hal_gpio_write(&gpio_speaker, value);
+        furi_hal_gpio_write(&gpio_speaker, !value);
+        furi_hal_gpio_write(&gpio_speaker, value);*/
+
+        break;
+    case MagTxStateLF_P:
+        furi_hal_gpio_write(RFID_PIN_OUT, value);
+        furi_hal_gpio_write(&gpio_speaker, value);
+
+        /* // Weaker but cleaner signal
+        if(value) {
+            furi_hal_gpio_write(RFID_PIN_OUT, value);
+            furi_hal_gpio_write(&gpio_speaker, value);
+            furi_delay_us(10);
+            furi_hal_gpio_write(RFID_PIN_OUT, !value);
+            furi_hal_gpio_write(&gpio_speaker, !value);
+        } else {
+            furi_delay_us(10);
+        }*/
+
+        /*furi_hal_gpio_write(RFID_PIN_OUT, value);
+        furi_hal_gpio_write(&gpio_speaker, value);
+        furi_hal_gpio_write(RFID_PIN_OUT, !value);
+        furi_hal_gpio_write(&gpio_speaker, !value);
+        furi_hal_gpio_write(RFID_PIN_OUT, value);
+        furi_hal_gpio_write(&gpio_speaker, value);*/
+        break;
+    case MagTxStateNFC:
+        // turn on for duration of half-bit? or "blip" the field on / off?
+        // getting nothing from the mag reader either way
+        //(value) ? furi_hal_nfc_ll_txrx_on() : furi_hal_nfc_ll_txrx_off();
+
+        if(last_value == 2 || value != (bool)last_value) {
+            //furi_hal_nfc_ll_txrx_on();
+            //furi_delay_us(64);
+            //furi_hal_nfc_ll_txrx_off();
+        }
+        break;
+    case MagTxCC1101_434:
+    case MagTxCC1101_868:
+        if(last_value == 2 || value != (bool)last_value) {
+            furi_hal_gpio_write(&gpio_cc1101_g0, true);
+            furi_delay_us(64);
+            furi_hal_gpio_write(&gpio_cc1101_g0, false);
+        }
+        break;
+    default:
+        break;
+    }
+
+    last_value = value;
+}
+
+void play_track(uint8_t* bits_manchester, uint16_t n_bits, MagSetting* setting, bool reverse) {
+    for(uint16_t i = 0; i < n_bits; i++) {
+        uint16_t j = (reverse) ? (n_bits - i - 1) : i;
+        uint8_t byte = j / 8;
+        uint8_t bitmask = 1 << (7 - (j % 8));
+        /* Bits are stored in their arrays like on a card (LSB first). This is not how usually bits are stored in a
+         * byte, with the MSB first. the var bitmask creates the pattern to iterate through each bit, LSB first, like so
+         * 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01, 0x80... masking bits one by one from the current byte
+         *
+         * I've chosen this LSB approach since bits and bytes are hard enough to visualize with the 5/8 and 7/8 encoding
+         * MSR uses. It's a biiit more complicated to process, but visualizing it with printf or a debugger is
+         * infinitely easier
+         *
+         * Encoding the following pairs of 5 bits as 5/8: A1234 B1234 C1234 D1234
+         * using this LSB format looks like: A1234B12 34C1234D 12340000
+         * using the MSB format, looks like: 21B4321A D4321C43 00004321
+         * this means reading each byte backwards when printing/debugging, and the jumping 16 bits ahead, reading 8 more
+         * bits backward, jumping 16 more bits ahead.
+         *
+         * I find this much more convenient for debugging, with the tiny incovenience of reading the bits in reverse
+         * order. Thus, the reason for the bitmask above
+         */
+
+        bool bit = !!(bits_manchester[byte] & bitmask);
+
+        // TODO: reimplement timing delays. Replace fixed furi_hal_cortex_delay_us to wait instead to a specific value
+        // for DWT->CYCCNT. Note timer is aliased to 64us as per
+        // #define FURI_HAL_CORTEX_INSTRUCTIONS_PER_MICROSECOND (SystemCoreClock / 1000000) | furi_hal_cortex.c
+
+        play_halfbit(bit, setting);
+        furi_delay_us(setting->us_clock);
+        // if (i % 2 == 1) furi_delay_us(setting->us_interpacket);
+    }
+}
+
+void tx_init_rfid() {
+    // initialize RFID system for TX
+
+    furi_hal_ibutton_pin_configure();
+
+    // furi_hal_ibutton_start_drive();
+    furi_hal_ibutton_pin_write(false);
+
+    // Initializing at GpioSpeedLow seems sufficient for our needs; no improvements seen by increasing speed setting
+
+    // this doesn't seem to make a difference, leaving it in
+    furi_hal_gpio_init(&gpio_rfid_data_in, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow);
+    furi_hal_gpio_write(&gpio_rfid_data_in, false);
+
+    // false->ground RFID antenna; true->don't ground
+    // skotopes (RFID dev) say normally you'd want RFID_PULL in high for signal forming, while modulating RFID_OUT
+    // dunaevai135 had it low in their old code. Leaving low, as it doesn't seem to make a difference on my janky antenna
+    furi_hal_gpio_init(&gpio_nfc_irq_rfid_pull, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow);
+    furi_hal_gpio_write(&gpio_nfc_irq_rfid_pull, false);
+
+    furi_hal_gpio_init(RFID_PIN_OUT, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow);
+
+    furi_delay_ms(300);
+}
+
+void tx_deinit_rfid() {
+    // reset RFID system
+    furi_hal_gpio_write(RFID_PIN_OUT, 0);
+
+    furi_hal_rfid_pins_reset();
+}
+
+void tx_init_rf(int hz) {
+    // presets and frequency will need some experimenting
+    furi_hal_subghz_reset();
+    // furi_hal_subghz_load_preset(FuriHalSubGhzPresetOok650Async);
+    // furi_hal_subghz_load_preset(FuriHalSubGhzPresetGFSK9_99KbAsync);
+    // furi_hal_subghz_load_preset(FuriHalSubGhzPresetMSK99_97KbAsync);
+    // furi_hal_subghz_load_preset(FuriHalSubGhzPreset2FSKDev238Async);
+    // furi_hal_subghz_load_preset(FuriHalSubGhzPreset2FSKDev476Async);
+    furi_hal_gpio_init(&gpio_cc1101_g0, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow);
+    furi_hal_subghz_set_frequency_and_path(hz);
+    furi_hal_subghz_tx();
+    furi_hal_gpio_write(&gpio_cc1101_g0, false);
+}
+
+void tx_init_piezo() {
+    // TODO: some special mutex acquire procedure? c.f. furi_hal_speaker.c
+    furi_hal_gpio_init(&gpio_speaker, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow);
+}
+
+void tx_deinit_piezo() {
+    // TODO: some special mutex release procedure?
+    furi_hal_gpio_init(&gpio_speaker, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
+}
+
+bool tx_init(MagSetting* setting) {
+    // Initialize configured TX method
+    switch(setting->tx) {
+    case MagTxStateRFID:
+        tx_init_rfid();
+        break;
+    case MagTxStateGPIO:
+        // gpio_item_configure_all_pins(GpioModeOutputPushPull);
+        furi_hal_gpio_init(GPIO_PIN_A, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow);
+        furi_hal_gpio_init(GPIO_PIN_B, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow);
+        furi_hal_gpio_init(GPIO_PIN_ENABLE, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow);
+
+        furi_hal_gpio_write(GPIO_PIN_ENABLE, 1);
+
+        // had some issues with ~300; bumped higher temporarily
+        furi_delay_ms(500);
+        break;
+    case MagTxStatePiezo:
+        tx_init_piezo();
+        break;
+    case MagTxStateLF_P:
+        tx_init_piezo();
+        tx_init_rfid();
+        break;
+    case MagTxStateNFC:
+        //furi_hal_nfc_exit_sleep();
+        break;
+    case MagTxCC1101_434:
+        tx_init_rf(434000000);
+        break;
+    case MagTxCC1101_868:
+        tx_init_rf(868000000);
+        break;
+    default:
+        return false;
+    }
+
+    return true;
+}
+
+bool tx_deinit(MagSetting* setting) {
+    // Reset configured TX method
+    switch(setting->tx) {
+    case MagTxStateRFID:
+        tx_deinit_rfid();
+        break;
+    case MagTxStateGPIO:
+        furi_hal_gpio_write(GPIO_PIN_A, 0);
+        furi_hal_gpio_write(GPIO_PIN_B, 0);
+        furi_hal_gpio_write(GPIO_PIN_ENABLE, 0);
+
+        // set back to analog output mode? - YES
+        furi_hal_gpio_init(GPIO_PIN_A, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
+        furi_hal_gpio_init(GPIO_PIN_B, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
+        furi_hal_gpio_init(GPIO_PIN_ENABLE, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
+
+        //gpio_item_configure_all_pins(GpioModeAnalog);
+        break;
+    case MagTxStatePiezo:
+        tx_deinit_piezo();
+        break;
+    case MagTxStateLF_P:
+        tx_deinit_piezo();
+        tx_deinit_rfid();
+        break;
+    case MagTxStateNFC:
+        //furi_hal_nfc_ll_txrx_off();
+        //furi_hal_nfc_start_sleep();
+        break;
+    case MagTxCC1101_434:
+    case MagTxCC1101_868:
+        furi_hal_gpio_write(&gpio_cc1101_g0, false);
+        furi_hal_subghz_reset();
+        furi_hal_subghz_idle();
+        break;
+    default:
+        return false;
+    }
+
+    return true;
+}
+
+void mag_spoof(Mag* mag) {
+    MagSetting* setting = mag->setting;
+
+    // TODO: cleanup this section. Possibly move precompute + tx_init to emulate_on_enter?
+    FuriString* ft1 = mag->mag_dev->dev_data.track[0].str;
+    FuriString* ft2 = mag->mag_dev->dev_data.track[1].str;
+    FuriString* ft3 = mag->mag_dev->dev_data.track[2].str;
+
+    char *data1, *data2, *data3;
+    data1 = malloc(furi_string_size(ft1) + 1);
+    data2 = malloc(furi_string_size(ft2) + 1);
+    data3 = malloc(furi_string_size(ft3) + 1);
+    strncpy(data1, furi_string_get_cstr(ft1), furi_string_size(ft1));
+    strncpy(data2, furi_string_get_cstr(ft2), furi_string_size(ft2));
+    strncpy(data3, furi_string_get_cstr(ft3), furi_string_size(ft3));
+
+    if(furi_log_get_level() >= FuriLogLevelDebug) {
+        debug_mag_string(data1, bitlen[0], sublen[0]);
+        debug_mag_string(data2, bitlen[1], sublen[1]);
+        debug_mag_string(data3, bitlen[2], sublen[2]);
+    }
+
+    uint8_t bits_t1_raw[64] = {0x00}; // 68 chars max track 1 + 1 char crc * 7 approx =~ 483 bits
+    uint8_t bits_t1_manchester[128] = {0x00}; // twice the above
+    uint16_t bits_t1_count = mag_encode(
+        data1, (uint8_t*)bits_t1_manchester, (uint8_t*)bits_t1_raw, bitlen[0], sublen[0]);
+    uint8_t bits_t2_raw[64] = {0x00}; // 68 chars max track 1 + 1 char crc * 7 approx =~ 483 bits
+    uint8_t bits_t2_manchester[128] = {0x00}; // twice the above
+    uint16_t bits_t2_count = mag_encode(
+        data2, (uint8_t*)bits_t2_manchester, (uint8_t*)bits_t2_raw, bitlen[1], sublen[1]);
+    uint8_t bits_t3_raw[64] = {0x00};
+    uint8_t bits_t3_manchester[128] = {0x00};
+    uint16_t bits_t3_count = mag_encode(
+        data3, (uint8_t*)bits_t3_manchester, (uint8_t*)bits_t3_raw, bitlen[2], sublen[2]);
+
+    if(furi_log_get_level() >= FuriLogLevelDebug) {
+        printf(
+            "Manchester bitcount: T1: %d, T2: %d, T3: %d\r\n",
+            bits_t1_count,
+            bits_t2_count,
+            bits_t3_count);
+        printf("T1 raw: ");
+        for(int i = 0; i < bits_t1_count / 16; i++) printf("%02x ", bits_t1_raw[i]);
+        printf("\r\nT1 manchester: ");
+        for(int i = 0; i < bits_t1_count / 8; i++) printf("%02x ", bits_t1_manchester[i]);
+        printf("\r\nT2 raw: ");
+        for(int i = 0; i < bits_t2_count / 16; i++) printf("%02x ", bits_t2_raw[i]);
+        printf("\r\nT2 manchester: ");
+        for(int i = 0; i < bits_t2_count / 8; i++) printf("%02x ", bits_t2_manchester[i]);
+        printf("\r\nT3 raw: ");
+        for(int i = 0; i < bits_t3_count / 16; i++) printf("%02x ", bits_t3_raw[i]);
+        printf("\r\nT3 manchester: ");
+        for(int i = 0; i < bits_t3_count / 8; i++) printf("%02x ", bits_t3_manchester[i]);
+        printf("\r\nBitwise emulation done\r\n\r\n");
+    }
+
+    last_value = 2;
+    bool bit = false;
+
+    if(!tx_init(setting)) return;
+
+    FURI_CRITICAL_ENTER();
+    for(uint16_t i = 0; i < (ZERO_PREFIX * 2); i++) {
+        // is this right?
+        if(!!(i % 2)) bit ^= 1;
+        play_halfbit(bit, setting);
+        furi_delay_us(setting->us_clock);
+    }
+
+    if((setting->track == MagTrackStateOneAndTwo) || (setting->track == MagTrackStateOne))
+        play_track((uint8_t*)bits_t1_manchester, bits_t1_count, setting, false);
+
+    if((setting->track == MagTrackStateOneAndTwo))
+        for(uint16_t i = 0; i < (ZERO_BETWEEN * 2); i++) {
+            if(!!(i % 2)) bit ^= 1;
+            play_halfbit(bit, setting);
+            furi_delay_us(setting->us_clock);
+        }
+
+    if((setting->track == MagTrackStateOneAndTwo) || (setting->track == MagTrackStateTwo))
+        play_track(
+            (uint8_t*)bits_t2_manchester,
+            bits_t2_count,
+            setting,
+            (setting->reverse == MagReverseStateOn));
+
+    if((setting->track == MagTrackStateThree))
+        play_track((uint8_t*)bits_t3_manchester, bits_t3_count, setting, false);
+
+    for(uint16_t i = 0; i < (ZERO_SUFFIX * 2); i++) {
+        if(!!(i % 2)) bit ^= 1;
+        play_halfbit(bit, setting);
+        furi_delay_us(setting->us_clock);
+    }
+    FURI_CRITICAL_EXIT();
+
+    free(data1);
+    free(data2);
+    free(data3);
+    tx_deinit(setting);
+}
+
+uint16_t add_bit(bool value, uint8_t* out, uint16_t count) {
+    uint8_t bit = count % 8;
+    uint8_t byte = count / 8;
+    if(value) {
+        out[byte] |= 0x01;
+    }
+    if(bit < 7) out[byte] <<= 1;
+    return count + 1;
+}
+
+uint16_t add_bit_manchester(bool value, uint8_t* out, uint16_t count) {
+    static bool toggle = 0;
+    toggle ^= 0x01;
+    count = add_bit(toggle, out, count);
+    if(value) toggle ^= 0x01;
+    count = add_bit(toggle, out, count);
+    return count;
+}
+
+uint16_t mag_encode(
+    char* data,
+    uint8_t* out_manchester,
+    uint8_t* out_raw,
+    uint8_t track_bits,
+    uint8_t track_ascii_offset) {
+    /*
+     * track_bits - the number of raw (data) bits on the track. on ISO cards, that's 7 for track 1, or 5 for 2/3 - this is samy's bitlen
+     *            - this count includes the parity bit
+     * track_ascii_offset - how much the ascii values are offset. track 1 makes space (ascii 32) become data 0x00,
+     *                    - tracks 2/3 make ascii "0" become data 0x00 - this is samy's sublen
+     *
+     */
+
+    uint16_t raw_bits_count = 0;
+    uint16_t output_count = 0;
+    int tmp, crc, lrc = 0;
+
+    /* // why are we adding zeros to the encoded string if we're also doing it while playing?
+    for(int i = 0; i < ZERO_PREFIX; i++) {
+        output_count = add_bit_manchester(0, out_manchester, output_count);
+        raw_bits_count = add_bit(0, out_raw, raw_bits_count);
+    }*/
+
+    for(int i = 0; *(data + i) != 0; i++) {
+        crc = 1;
+        tmp = *(data + i) - track_ascii_offset;
+
+        for(int j = 0; j < track_bits - 1; j++) {
+            crc ^= tmp & 1;
+            lrc ^= (tmp & 1) << j;
+            raw_bits_count = add_bit(tmp & 0x01, out_raw, raw_bits_count);
+            output_count = add_bit_manchester(tmp & 0x01, out_manchester, output_count);
+            tmp >>= 1;
+        }
+        raw_bits_count = add_bit(crc, out_raw, raw_bits_count);
+        output_count = add_bit_manchester(crc, out_manchester, output_count);
+    }
+
+    // LRC byte
+    tmp = lrc;
+    crc = 1;
+    for(int j = 0; j < track_bits - 1; j++) {
+        crc ^= tmp & 0x01;
+        raw_bits_count = add_bit(tmp & 0x01, out_raw, raw_bits_count);
+        output_count = add_bit_manchester(tmp & 0x01, out_manchester, output_count);
+        tmp >>= 1;
+    }
+    raw_bits_count = add_bit(crc, out_raw, raw_bits_count);
+    output_count = add_bit_manchester(crc, out_manchester, output_count);
+
+    return output_count;
+}
+
+void debug_mag_string(char* data, uint8_t track_bits, uint8_t track_ascii_offset) {
+    uint8_t bits_raw[64] = {0}; // 68 chars max track 1 + 1 char crc * 7 approx =~ 483 bits
+    uint8_t bits_manchester[128] = {0}; // twice the above
+    int numbits = 0;
+
+    printf("Encoding [%s] with %d bits\r\n", data, track_bits);
+    numbits = mag_encode(
+        data, (uint8_t*)bits_manchester, (uint8_t*)bits_raw, track_bits, track_ascii_offset);
+    printf("Got %d bits\r\n", numbits);
+    printf("Raw byte stream:     ");
+    for(int i = 0; i < numbits / 8 / 2; i++) {
+        printf("%02x", bits_raw[i]);
+        if(i % 4 == 3) printf(" ");
+    }
+
+    printf("\r\n");
+
+    printf("Bits                 ");
+    int space_counter = 0;
+    for(int i = 0; i < numbits / 2; i++) {
+        /*if(i < ZERO_PREFIX) {
+            printf("X");
+            continue;
+        } else if(i == ZERO_PREFIX) {
+            printf(" ");
+            space_counter = 0;
+        }*/
+        printf("%01x", (bits_raw[i / 8] & (1 << (7 - (i % 8)))) != 0);
+        if((space_counter) % track_bits == track_bits - 1) printf(" ");
+        space_counter++;
+    }
+
+    printf("\r\n");
+
+    printf("Manchester encoded, byte stream: ");
+    for(int i = 0; i < numbits / 8; i++) {
+        printf("%02x", bits_manchester[i]);
+        if(i % 4 == 3) printf(" ");
+    }
+    printf("\r\n\r\n");
+}

+ 25 - 0
magspoof/helpers/mag_helpers.h

@@ -0,0 +1,25 @@
+#include "../mag_i.h"
+#include <stdio.h>
+#include <string.h>
+
+void play_halfbit(bool value, MagSetting* setting);
+void play_track(uint8_t* bits_manchester, uint16_t n_bits, MagSetting* setting, bool reverse);
+
+void tx_init_rf(int hz);
+void tx_init_rfid();
+void tx_init_piezo();
+bool tx_init(MagSetting* setting);
+void tx_deinit_piezo();
+void tx_deinit_rfid();
+bool tx_deinit(MagSetting* setting);
+
+uint16_t add_bit(bool value, uint8_t* out, uint16_t count);
+uint16_t add_bit_manchester(bool value, uint8_t* out, uint16_t count);
+uint16_t mag_encode(
+    char* data,
+    uint8_t* out_manchester,
+    uint8_t* out_raw,
+    uint8_t track_bits,
+    uint8_t track_ascii_offset);
+void debug_mag_string(char* data, uint8_t track_bits, uint8_t track_ascii_offset);
+void mag_spoof(Mag* mag);

+ 583 - 0
magspoof/helpers/mag_text_input.c

@@ -0,0 +1,583 @@
+#include "mag_text_input.h"
+#include <gui/elements.h>
+#include <assets_icons.h>
+#include <furi.h>
+
+struct Mag_TextInput {
+    View* view;
+    FuriTimer* timer;
+};
+
+typedef struct {
+    const char text;
+    const uint8_t x;
+    const uint8_t y;
+} Mag_TextInputKey;
+
+typedef struct {
+    const char* header;
+    char* text_buffer;
+    size_t text_buffer_size;
+    bool clear_default_text;
+
+    Mag_TextInputCallback callback;
+    void* callback_context;
+
+    uint8_t selected_row;
+    uint8_t selected_column;
+
+    // Mag_TextInputValidatorCallback validator_callback;
+    // void* validator_callback_context;
+    // FuriString* validator_text;
+    // bool validator_message_visible;
+} Mag_TextInputModel;
+
+static const uint8_t keyboard_origin_x = 1;
+static const uint8_t keyboard_origin_y = 29;
+static const uint8_t keyboard_row_count = 3;
+
+#define ENTER_KEY '\r'
+#define BACKSPACE_KEY '\b'
+
+static const Mag_TextInputKey keyboard_keys_row_1[] = {
+    {'q', 1, 8},
+    {'w', 9, 8},
+    {'e', 17, 8},
+    {'r', 25, 8},
+    {'t', 33, 8},
+    {'y', 41, 8},
+    {'u', 49, 8},
+    {'i', 57, 8},
+    {'o', 65, 8},
+    {'p', 73, 8},
+    {'0', 81, 8},
+    {'1', 89, 8},
+    {'2', 97, 8},
+    {'3', 105, 8},
+    {'%', 113, 8},
+    {'^', 120, 8},
+};
+
+static const Mag_TextInputKey keyboard_keys_row_2[] = {
+    {'a', 1, 20},
+    {'s', 9, 20},
+    {'d', 18, 20},
+    {'f', 25, 20},
+    {'g', 33, 20},
+    {'h', 41, 20},
+    {'j', 49, 20},
+    {'k', 57, 20},
+    {'l', 65, 20},
+    {BACKSPACE_KEY, 72, 12},
+    {'4', 89, 20},
+    {'5', 97, 20},
+    {'6', 105, 20},
+    {'/', 113, 20},
+    {'?', 120, 20},
+
+};
+
+static const Mag_TextInputKey keyboard_keys_row_3[] = {
+    {'z', 1, 32},
+    {'x', 9, 32},
+    {'c', 18, 32},
+    {'v', 25, 32},
+    {'b', 33, 32},
+    {'n', 41, 32},
+    {'m', 49, 32},
+    {'_', 57, 32},
+    {ENTER_KEY, 64, 23},
+    {'7', 89, 32},
+    {'8', 97, 32},
+    {'9', 105, 32},
+    {';', 113, 32},
+    {'=', 120, 32},
+};
+
+static uint8_t get_row_size(uint8_t row_index) {
+    uint8_t row_size = 0;
+
+    switch(row_index + 1) {
+    case 1:
+        row_size = sizeof(keyboard_keys_row_1) / sizeof(Mag_TextInputKey);
+        break;
+    case 2:
+        row_size = sizeof(keyboard_keys_row_2) / sizeof(Mag_TextInputKey);
+        break;
+    case 3:
+        row_size = sizeof(keyboard_keys_row_3) / sizeof(Mag_TextInputKey);
+        break;
+    }
+
+    return row_size;
+}
+
+static const Mag_TextInputKey* get_row(uint8_t row_index) {
+    const Mag_TextInputKey* row = NULL;
+
+    switch(row_index + 1) {
+    case 1:
+        row = keyboard_keys_row_1;
+        break;
+    case 2:
+        row = keyboard_keys_row_2;
+        break;
+    case 3:
+        row = keyboard_keys_row_3;
+        break;
+    }
+
+    return row;
+}
+
+static char get_selected_char(Mag_TextInputModel* model) {
+    return get_row(model->selected_row)[model->selected_column].text;
+}
+
+static bool char_is_lowercase(char letter) {
+    return (letter >= 0x61 && letter <= 0x7A);
+}
+
+static char char_to_uppercase(const char letter) {
+    if(letter == '_') {
+        return 0x20;
+    } else if(isalpha(letter)) {
+        return (letter - 0x20);
+    } else {
+        return letter;
+    }
+}
+
+static void mag_text_input_backspace_cb(Mag_TextInputModel* model) {
+    uint8_t text_length = model->clear_default_text ? 1 : strlen(model->text_buffer);
+    if(text_length > 0) {
+        model->text_buffer[text_length - 1] = 0;
+    }
+}
+
+static void mag_text_input_view_draw_callback(Canvas* canvas, void* _model) {
+    Mag_TextInputModel* model = _model;
+    // uint8_t text_length = model->text_buffer ? strlen(model->text_buffer) : 0;
+    uint8_t needed_string_width = canvas_width(canvas) - 8;
+    uint8_t start_pos = 4;
+
+    const char* text = model->text_buffer;
+
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+
+    canvas_draw_str(canvas, 2, 8, model->header);
+    elements_slightly_rounded_frame(canvas, 1, 12, 126, 15);
+
+    if(canvas_string_width(canvas, text) > needed_string_width) {
+        canvas_draw_str(canvas, start_pos, 22, "...");
+        start_pos += 6;
+        needed_string_width -= 8;
+    }
+
+    while(text != 0 && canvas_string_width(canvas, text) > needed_string_width) {
+        text++;
+    }
+
+    if(model->clear_default_text) {
+        elements_slightly_rounded_box(
+            canvas, start_pos - 1, 14, canvas_string_width(canvas, text) + 2, 10);
+        canvas_set_color(canvas, ColorWhite);
+    } else {
+        canvas_draw_str(canvas, start_pos + canvas_string_width(canvas, text) + 1, 22, "|");
+        canvas_draw_str(canvas, start_pos + canvas_string_width(canvas, text) + 2, 22, "|");
+    }
+    canvas_draw_str(canvas, start_pos, 22, text);
+
+    canvas_set_font(canvas, FontKeyboard);
+
+    for(uint8_t row = 0; row <= keyboard_row_count; row++) {
+        const uint8_t column_count = get_row_size(row);
+        const Mag_TextInputKey* keys = get_row(row);
+
+        for(size_t column = 0; column < column_count; column++) {
+            if(keys[column].text == ENTER_KEY) {
+                canvas_set_color(canvas, ColorBlack);
+                if(model->selected_row == row && model->selected_column == column) {
+                    canvas_draw_icon(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        &I_KeySaveSelected_24x11);
+                } else {
+                    canvas_draw_icon(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        &I_KeySave_24x11);
+                }
+            } else if(keys[column].text == BACKSPACE_KEY) {
+                canvas_set_color(canvas, ColorBlack);
+                if(model->selected_row == row && model->selected_column == column) {
+                    canvas_draw_icon(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        &I_KeyBackspaceSelected_16x9);
+                } else {
+                    canvas_draw_icon(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        &I_KeyBackspace_16x9);
+                }
+            } else {
+                if(model->selected_row == row && model->selected_column == column) {
+                    canvas_set_color(canvas, ColorBlack);
+                    canvas_draw_box(
+                        canvas,
+                        keyboard_origin_x + keys[column].x - 1,
+                        keyboard_origin_y + keys[column].y - 8,
+                        7,
+                        10);
+                    canvas_set_color(canvas, ColorWhite);
+                } else {
+                    canvas_set_color(canvas, ColorBlack);
+                }
+
+                if(model->clear_default_text || (char_is_lowercase(keys[column].text))) {
+                    canvas_draw_glyph(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        char_to_uppercase(keys[column].text));
+                    //keys[column].text);
+                } else {
+                    canvas_draw_glyph(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        keys[column].text);
+                }
+            }
+        }
+    }
+    /*if(model->validator_message_visible) {
+        canvas_set_font(canvas, FontSecondary);
+        canvas_set_color(canvas, ColorWhite);
+        canvas_draw_box(canvas, 8, 10, 110, 48);
+        canvas_set_color(canvas, ColorBlack);
+        canvas_draw_icon(canvas, 10, 14, &I_WarningDolphin_45x42);
+        canvas_draw_rframe(canvas, 8, 8, 112, 50, 3);
+        canvas_draw_rframe(canvas, 9, 9, 110, 48, 2);
+        elements_multiline_text(canvas, 62, 20, furi_string_get_cstr(model->validator_text));
+        canvas_set_font(canvas, FontKeyboard);
+    }*/
+}
+
+static void mag_text_input_handle_up(Mag_TextInput* mag_text_input, Mag_TextInputModel* model) {
+    UNUSED(mag_text_input);
+    if(model->selected_row > 0) {
+        model->selected_row--;
+        if(model->selected_column > get_row_size(model->selected_row) - 6) {
+            model->selected_column = model->selected_column + 1;
+        }
+    }
+}
+
+static void mag_text_input_handle_down(Mag_TextInput* mag_text_input, Mag_TextInputModel* model) {
+    UNUSED(mag_text_input);
+    if(model->selected_row < keyboard_row_count - 1) {
+        model->selected_row++;
+        if(model->selected_column > get_row_size(model->selected_row) - 4) {
+            model->selected_column = model->selected_column - 1;
+        }
+    }
+}
+
+static void mag_text_input_handle_left(Mag_TextInput* mag_text_input, Mag_TextInputModel* model) {
+    UNUSED(mag_text_input);
+    if(model->selected_column > 0) {
+        model->selected_column--;
+    } else {
+        model->selected_column = get_row_size(model->selected_row) - 1;
+    }
+}
+
+static void mag_text_input_handle_right(Mag_TextInput* mag_text_input, Mag_TextInputModel* model) {
+    UNUSED(mag_text_input);
+    if(model->selected_column < get_row_size(model->selected_row) - 1) {
+        model->selected_column++;
+    } else {
+        model->selected_column = 0;
+    }
+}
+
+static void
+    mag_text_input_handle_ok(Mag_TextInput* mag_text_input, Mag_TextInputModel* model, bool shift) {
+    UNUSED(mag_text_input);
+
+    char selected = get_selected_char(model);
+    uint8_t text_length = strlen(model->text_buffer);
+
+    if(shift) {
+        selected = char_to_uppercase(selected);
+    }
+
+    if(selected == ENTER_KEY) {
+        /*if(model->validator_callback &&
+           (!model->validator_callback(
+               model->text_buffer, model->validator_text, model->validator_callback_context))) {
+            model->validator_message_visible = true;
+            furi_timer_start(mag_text_input->timer, furi_kernel_get_tick_frequency() * 4);
+        } else*/
+        if(model->callback != 0 && text_length > 0) {
+            model->callback(model->callback_context);
+        }
+    } else if(selected == BACKSPACE_KEY) {
+        mag_text_input_backspace_cb(model);
+    } else {
+        if(model->clear_default_text) {
+            text_length = 0;
+        }
+        if(text_length < (model->text_buffer_size - 1)) {
+            if(char_is_lowercase(selected)) {
+                selected = char_to_uppercase(selected);
+            }
+            model->text_buffer[text_length] = selected;
+            model->text_buffer[text_length + 1] = 0;
+        }
+    }
+    model->clear_default_text = false;
+}
+
+static bool mag_text_input_view_input_callback(InputEvent* event, void* context) {
+    Mag_TextInput* mag_text_input = context;
+    furi_assert(mag_text_input);
+
+    bool consumed = false;
+
+    // Acquire model
+    Mag_TextInputModel* model = view_get_model(mag_text_input->view);
+
+    /* if((!(event->type == InputTypePress) && !(event->type == InputTypeRelease)) &&
+       model->validator_message_visible) {
+        model->validator_message_visible = false;
+        consumed = true;
+    } else*/
+    if(event->type == InputTypeShort) {
+        consumed = true;
+        switch(event->key) {
+        case InputKeyUp:
+            mag_text_input_handle_up(mag_text_input, model);
+            break;
+        case InputKeyDown:
+            mag_text_input_handle_down(mag_text_input, model);
+            break;
+        case InputKeyLeft:
+            mag_text_input_handle_left(mag_text_input, model);
+            break;
+        case InputKeyRight:
+            mag_text_input_handle_right(mag_text_input, model);
+            break;
+        case InputKeyOk:
+            mag_text_input_handle_ok(mag_text_input, model, false);
+            break;
+        default:
+            consumed = false;
+            break;
+        }
+    } else if(event->type == InputTypeLong) {
+        consumed = true;
+        switch(event->key) {
+        case InputKeyUp:
+            mag_text_input_handle_up(mag_text_input, model);
+            break;
+        case InputKeyDown:
+            mag_text_input_handle_down(mag_text_input, model);
+            break;
+        case InputKeyLeft:
+            mag_text_input_handle_left(mag_text_input, model);
+            break;
+        case InputKeyRight:
+            mag_text_input_handle_right(mag_text_input, model);
+            break;
+        case InputKeyOk:
+            mag_text_input_handle_ok(mag_text_input, model, true);
+            break;
+        case InputKeyBack:
+            mag_text_input_backspace_cb(model);
+            break;
+        default:
+            consumed = false;
+            break;
+        }
+    } else if(event->type == InputTypeRepeat) {
+        consumed = true;
+        switch(event->key) {
+        case InputKeyUp:
+            mag_text_input_handle_up(mag_text_input, model);
+            break;
+        case InputKeyDown:
+            mag_text_input_handle_down(mag_text_input, model);
+            break;
+        case InputKeyLeft:
+            mag_text_input_handle_left(mag_text_input, model);
+            break;
+        case InputKeyRight:
+            mag_text_input_handle_right(mag_text_input, model);
+            break;
+        case InputKeyBack:
+            mag_text_input_backspace_cb(model);
+            break;
+        default:
+            consumed = false;
+            break;
+        }
+    }
+
+    // Commit model
+    view_commit_model(mag_text_input->view, consumed);
+
+    return consumed;
+}
+
+void mag_text_input_timer_callback(void* context) {
+    furi_assert(context);
+    Mag_TextInput* mag_text_input = context;
+    UNUSED(mag_text_input);
+
+    /*with_view_model(
+        mag_text_input->view,
+        Mag_TextInputModel * model,
+        { model->validator_message_visible = false; },
+        true);*/
+}
+
+Mag_TextInput* mag_text_input_alloc() {
+    Mag_TextInput* mag_text_input = malloc(sizeof(Mag_TextInput));
+    mag_text_input->view = view_alloc();
+    view_set_context(mag_text_input->view, mag_text_input);
+    view_allocate_model(mag_text_input->view, ViewModelTypeLocking, sizeof(Mag_TextInputModel));
+    view_set_draw_callback(mag_text_input->view, mag_text_input_view_draw_callback);
+    view_set_input_callback(mag_text_input->view, mag_text_input_view_input_callback);
+
+    mag_text_input->timer =
+        furi_timer_alloc(mag_text_input_timer_callback, FuriTimerTypeOnce, mag_text_input);
+
+    /*with_view_model(
+        mag_text_input->view,
+        Mag_TextInputModel * model,
+        { model->validator_text = furi_string_alloc(); },
+        false);*/
+
+    mag_text_input_reset(mag_text_input);
+
+    return mag_text_input;
+}
+
+void mag_text_input_free(Mag_TextInput* mag_text_input) {
+    furi_assert(mag_text_input);
+    /*with_view_model(
+        mag_text_input->view,
+        Mag_TextInputModel * model,
+        { furi_string_free(model->validator_text); },
+        false);*/
+
+    // Send stop command
+    furi_timer_stop(mag_text_input->timer);
+    // Release allocated memory
+    furi_timer_free(mag_text_input->timer);
+
+    view_free(mag_text_input->view);
+
+    free(mag_text_input);
+}
+
+void mag_text_input_reset(Mag_TextInput* mag_text_input) {
+    furi_assert(mag_text_input);
+    with_view_model(
+        mag_text_input->view,
+        Mag_TextInputModel * model,
+        {
+            model->text_buffer_size = 0;
+            model->header = "";
+            model->selected_row = 0;
+            model->selected_column = 0;
+            model->clear_default_text = false;
+            model->text_buffer = NULL;
+            model->text_buffer_size = 0;
+            model->callback = NULL;
+            model->callback_context = NULL;
+            /*model->validator_callback = NULL;
+            model->validator_callback_context = NULL;
+            furi_string_reset(model->validator_text);
+            model->validator_message_visible = false;*/
+        },
+        true);
+}
+
+View* mag_text_input_get_view(Mag_TextInput* mag_text_input) {
+    furi_assert(mag_text_input);
+    return mag_text_input->view;
+}
+
+void mag_text_input_set_result_callback(
+    Mag_TextInput* mag_text_input,
+    Mag_TextInputCallback callback,
+    void* callback_context,
+    char* text_buffer,
+    size_t text_buffer_size,
+    bool clear_default_text) {
+    with_view_model(
+        mag_text_input->view,
+        Mag_TextInputModel * model,
+        {
+            model->callback = callback;
+            model->callback_context = callback_context;
+            model->text_buffer = text_buffer;
+            model->text_buffer_size = text_buffer_size;
+            model->clear_default_text = clear_default_text;
+            if(text_buffer && text_buffer[0] != '\0') {
+                // Set focus on Save
+                model->selected_row = 2;
+                model->selected_column = 8;
+            }
+        },
+        true);
+}
+
+/* void mag_text_input_set_validator(
+    Mag_TextInput* mag_text_input,
+    Mag_TextInputValidatorCallback callback,
+    void* callback_context) {
+    with_view_model(
+        mag_text_input->view,
+        Mag_TextInputModel * model,
+        {
+            model->validator_callback = callback;
+            model->validator_callback_context = callback_context;
+        },
+        true);
+}
+
+Mag_TextInputValidatorCallback
+    mag_text_input_get_validator_callback(Mag_TextInput* mag_text_input) {
+    Mag_TextInputValidatorCallback validator_callback = NULL;
+    with_view_model(
+        mag_text_input->view,
+        Mag_TextInputModel * model,
+        { validator_callback = model->validator_callback; },
+        false);
+    return validator_callback;
+}
+
+void* mag_text_input_get_validator_callback_context(Mag_TextInput* mag_text_input) {
+    void* validator_callback_context = NULL;
+    with_view_model(
+        mag_text_input->view,
+        Mag_TextInputModel * model,
+        { validator_callback_context = model->validator_callback_context; },
+        false);
+    return validator_callback_context;
+}*/
+
+void mag_text_input_set_header_text(Mag_TextInput* mag_text_input, const char* text) {
+    with_view_model(
+        mag_text_input->view, Mag_TextInputModel * model, { model->header = text; }, true);
+}

+ 82 - 0
magspoof/helpers/mag_text_input.h

@@ -0,0 +1,82 @@
+#pragma once
+
+#include <gui/view.h>
+// #include "mag_validators.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Text input anonymous structure */
+typedef struct Mag_TextInput Mag_TextInput;
+typedef void (*Mag_TextInputCallback)(void* context);
+// typedef bool (*Mag_TextInputValidatorCallback)(const char* text, FuriString* error, void* context);
+
+/** Allocate and initialize text input 
+ * 
+ * This text input is used to enter string
+ *
+ * @return     Mag_TextInput instance
+ */
+Mag_TextInput* mag_text_input_alloc();
+
+/** Deinitialize and free text input
+ *
+ * @param      mag_text_input  Mag_TextInput instance
+ */
+void mag_text_input_free(Mag_TextInput* mag_text_input);
+
+/** Clean text input view Note: this function does not free memory
+ *
+ * @param      mag_text_input  Text input instance
+ */
+void mag_text_input_reset(Mag_TextInput* mag_text_input);
+
+/** Get text input view
+ *
+ * @param      mag_text_input  Mag_TextInput instance
+ *
+ * @return     View instance that can be used for embedding
+ */
+View* mag_text_input_get_view(Mag_TextInput* mag_text_input);
+
+/** Set text input result callback
+ *
+ * @param      mag_text_input          Mag_TextInput instance
+ * @param      callback            callback fn
+ * @param      callback_context    callback context
+ * @param      text_buffer         pointer to YOUR text buffer, that we going
+ *                                 to modify
+ * @param      text_buffer_size    YOUR text buffer size in bytes. Max string
+ *                                 length will be text_buffer_size-1.
+ * @param      clear_default_text  clear text from text_buffer on first OK
+ *                                 event
+ */
+void mag_text_input_set_result_callback(
+    Mag_TextInput* mag_text_input,
+    Mag_TextInputCallback callback,
+    void* callback_context,
+    char* text_buffer,
+    size_t text_buffer_size,
+    bool clear_default_text);
+
+/* void mag_text_input_set_validator(
+    Mag_TextInput* mag_text_input,
+    Mag_TextInputValidatorCallback callback,
+    void* callback_context);
+
+Mag_TextInputValidatorCallback
+    mag_text_input_get_validator_callback(Mag_TextInput* mag_text_input);
+
+void* mag_text_input_get_validator_callback_context(Mag_TextInput* mag_text_input); */
+
+/** Set text input header text
+ *
+ * @param      mag_text_input  Mag_TextInput instance
+ * @param      text        text to be shown
+ */
+void mag_text_input_set_header_text(Mag_TextInput* mag_text_input, const char* text);
+
+#ifdef __cplusplus
+}
+#endif

+ 45 - 0
magspoof/helpers/mag_types.h

@@ -0,0 +1,45 @@
+#pragma once
+
+#define MAG_VERSION_APP "0.05"
+#define MAG_DEVELOPER "Zachary Weiss"
+#define MAG_GITHUB "github.com/zacharyweiss/magspoof_flipper"
+
+typedef enum {
+    MagViewSubmenu,
+    MagViewDialogEx,
+    MagViewPopup,
+    MagViewLoading,
+    MagViewWidget,
+    MagViewVariableItemList,
+    MagViewTextInput,
+    MagViewMagTextInput,
+} MagView;
+
+typedef enum {
+    MagReverseStateOff,
+    MagReverseStateOn,
+} MagReverseState;
+
+typedef enum {
+    MagTrackStateOneAndTwo,
+    MagTrackStateOne,
+    MagTrackStateTwo,
+    MagTrackStateThree,
+} MagTrackState;
+
+typedef enum {
+    MagTxStateRFID,
+    MagTxStateGPIO,
+    MagTxStatePiezo,
+    MagTxStateLF_P, // combo of RFID and Piezo
+    MagTxStateNFC,
+    MagTxCC1101_434,
+    MagTxCC1101_868,
+} MagTxState;
+
+
+typedef enum {
+    UART_TerminalEventRefreshConsoleOutput = 0,
+    UART_TerminalEventStartConsole,
+    UART_TerminalEventStartKeyboard,
+} UART_TerminalCustomEvent;

BIN
magspoof/icons/DolphinMafia_115x62.png


BIN
magspoof/icons/DolphinNice_96x59.png


BIN
magspoof/icons/KeyBackspaceSelected_16x9.png


BIN
magspoof/icons/KeyBackspace_16x9.png


BIN
magspoof/icons/KeySaveSelected_24x11.png


BIN
magspoof/icons/KeySave_24x11.png


BIN
magspoof/icons/mag_10px.png


BIN
magspoof/icons/mag_file_10px.png


+ 256 - 0
magspoof/mag.c

@@ -0,0 +1,256 @@
+#include "mag_i.h"
+
+#define TAG "Mag"
+
+#define SETTING_DEFAULT_REVERSE MagReverseStateOff
+#define SETTING_DEFAULT_TRACK MagTrackStateOneAndTwo
+#define SETTING_DEFAULT_TX_RFID MagTxStateGPIO
+#define SETTING_DEFAULT_US_CLOCK 240
+#define SETTING_DEFAULT_US_INTERPACKET 10
+
+static bool mag_debug_custom_event_callback(void* context, uint32_t event) {
+    furi_assert(context);
+    Mag* mag = context;
+    return scene_manager_handle_custom_event(mag->scene_manager, event);
+}
+
+static bool mag_debug_back_event_callback(void* context) {
+    furi_assert(context);
+    Mag* mag = context;
+    return scene_manager_handle_back_event(mag->scene_manager);
+}
+
+static MagSetting* mag_setting_alloc() {
+    // temp hardcoded defaults
+    MagSetting* setting = malloc(sizeof(MagSetting));
+    setting->reverse = SETTING_DEFAULT_REVERSE;
+    setting->track = SETTING_DEFAULT_TRACK;
+    setting->tx = SETTING_DEFAULT_TX_RFID;
+    setting->us_clock = SETTING_DEFAULT_US_CLOCK;
+    setting->us_interpacket = SETTING_DEFAULT_US_INTERPACKET;
+
+    return setting;
+}
+
+static Mag* mag_alloc() {
+    Mag* mag = malloc(sizeof(Mag));
+
+    mag->storage = furi_record_open(RECORD_STORAGE);
+    mag->dialogs = furi_record_open(RECORD_DIALOGS);
+
+    mag->file_name = furi_string_alloc();
+    mag->file_path = furi_string_alloc_set(MAG_APP_FOLDER);
+
+    mag->view_dispatcher = view_dispatcher_alloc();
+    mag->scene_manager = scene_manager_alloc(&mag_scene_handlers, mag);
+    view_dispatcher_enable_queue(mag->view_dispatcher);
+    view_dispatcher_set_event_callback_context(mag->view_dispatcher, mag);
+    view_dispatcher_set_custom_event_callback(
+        mag->view_dispatcher, mag_debug_custom_event_callback);
+    view_dispatcher_set_navigation_event_callback(
+        mag->view_dispatcher, mag_debug_back_event_callback);
+
+    mag->mag_dev = mag_device_alloc();
+    mag->setting = mag_setting_alloc();
+
+    // Open GUI record
+    mag->gui = furi_record_open(RECORD_GUI);
+
+    // Open Notification record
+    mag->notifications = furi_record_open(RECORD_NOTIFICATION);
+
+    // Submenu
+    mag->submenu = submenu_alloc();
+    view_dispatcher_add_view(mag->view_dispatcher, MagViewSubmenu, submenu_get_view(mag->submenu));
+
+    // Dialog
+    mag->dialog_ex = dialog_ex_alloc();
+    view_dispatcher_add_view(
+        mag->view_dispatcher, MagViewDialogEx, dialog_ex_get_view(mag->dialog_ex));
+
+    // Popup
+    mag->popup = popup_alloc();
+    view_dispatcher_add_view(mag->view_dispatcher, MagViewPopup, popup_get_view(mag->popup));
+
+    // Loading
+    mag->loading = loading_alloc();
+    view_dispatcher_add_view(mag->view_dispatcher, MagViewLoading, loading_get_view(mag->loading));
+
+    // Widget
+    mag->widget = widget_alloc();
+    view_dispatcher_add_view(mag->view_dispatcher, MagViewWidget, widget_get_view(mag->widget));
+
+    // Variable Item List
+    mag->variable_item_list = variable_item_list_alloc();
+    view_dispatcher_add_view(
+        mag->view_dispatcher,
+        MagViewVariableItemList,
+        variable_item_list_get_view(mag->variable_item_list));
+
+    // Text Input
+    mag->text_input = text_input_alloc();
+    view_dispatcher_add_view(
+        mag->view_dispatcher, MagViewTextInput, text_input_get_view(mag->text_input));
+
+    // Custom Mag Text Input
+    mag->mag_text_input = mag_text_input_alloc();
+    view_dispatcher_add_view(
+        mag->view_dispatcher, MagViewMagTextInput, mag_text_input_get_view(mag->mag_text_input));
+
+    return mag;
+}
+
+static void mag_setting_free(MagSetting* setting) {
+    furi_assert(setting);
+
+    free(setting);
+}
+
+static void mag_free(Mag* mag) {
+    furi_assert(mag);
+
+    furi_string_free(mag->file_name);
+    furi_string_free(mag->file_path);
+
+    // Mag device
+    mag_device_free(mag->mag_dev);
+    mag->mag_dev = NULL;
+
+    // Mag setting
+    mag_setting_free(mag->setting);
+    mag->setting = NULL;
+
+    // Submenu
+    view_dispatcher_remove_view(mag->view_dispatcher, MagViewSubmenu);
+    submenu_free(mag->submenu);
+
+    // DialogEx
+    view_dispatcher_remove_view(mag->view_dispatcher, MagViewDialogEx);
+    dialog_ex_free(mag->dialog_ex);
+
+    // Popup
+    view_dispatcher_remove_view(mag->view_dispatcher, MagViewPopup);
+    popup_free(mag->popup);
+
+    // Loading
+    view_dispatcher_remove_view(mag->view_dispatcher, MagViewLoading);
+    loading_free(mag->loading);
+
+    // Widget
+    view_dispatcher_remove_view(mag->view_dispatcher, MagViewWidget);
+    widget_free(mag->widget);
+
+    // Variable Item List
+    view_dispatcher_remove_view(mag->view_dispatcher, MagViewVariableItemList);
+    variable_item_list_free(mag->variable_item_list);
+
+    // TextInput
+    view_dispatcher_remove_view(mag->view_dispatcher, MagViewTextInput);
+    text_input_free(mag->text_input);
+
+    // Custom Mag TextInput
+    view_dispatcher_remove_view(mag->view_dispatcher, MagViewMagTextInput);
+    mag_text_input_free(mag->mag_text_input);
+
+    // View Dispatcher
+    view_dispatcher_free(mag->view_dispatcher);
+
+    // Scene Manager
+    scene_manager_free(mag->scene_manager);
+
+    // GUI
+    furi_record_close(RECORD_GUI);
+    mag->gui = NULL;
+
+    // Notifications
+    furi_record_close(RECORD_NOTIFICATION);
+    mag->notifications = NULL;
+
+    furi_record_close(RECORD_STORAGE);
+    furi_record_close(RECORD_DIALOGS);
+
+    free(mag);
+}
+
+// entry point for app
+int32_t mag_app(void* p) {
+    Mag* mag = mag_alloc();
+    UNUSED(p);
+
+    mag_make_app_folder(mag);
+
+    // Enable 5v power, multiple attempts to avoid issues with power chip protection false triggering
+    uint8_t attempts = 0;
+    bool otg_was_enabled = furi_hal_power_is_otg_enabled();
+    while(!furi_hal_power_is_otg_enabled() && attempts++ < 5) {
+        furi_hal_power_enable_otg();
+        furi_delay_ms(10);
+    }
+
+    view_dispatcher_attach_to_gui(mag->view_dispatcher, mag->gui, ViewDispatcherTypeFullscreen);
+    scene_manager_next_scene(mag->scene_manager, MagSceneStart);
+
+    view_dispatcher_run(mag->view_dispatcher);
+
+    // Disable 5v power
+    if(furi_hal_power_is_otg_enabled() && !otg_was_enabled) {
+        furi_hal_power_disable_otg();
+    }
+
+    mag_free(mag);
+
+    return 0;
+}
+
+void mag_make_app_folder(Mag* mag) {
+    furi_assert(mag);
+
+    if(!storage_simply_mkdir(mag->storage, MAG_APP_FOLDER)) {
+        dialog_message_show_storage_error(mag->dialogs, "Cannot create\napp folder");
+    }
+}
+
+void mag_text_store_set(Mag* mag, const char* text, ...) {
+    furi_assert(mag);
+    va_list args;
+    va_start(args, text);
+
+    vsnprintf(mag->text_store, MAG_TEXT_STORE_SIZE, text, args);
+
+    va_end(args);
+}
+
+void mag_text_store_clear(Mag* mag) {
+    furi_assert(mag);
+    memset(mag->text_store, 0, sizeof(mag->text_store));
+}
+
+void mag_popup_timeout_callback(void* context) {
+    Mag* mag = context;
+    view_dispatcher_send_custom_event(mag->view_dispatcher, MagEventPopupClosed);
+}
+
+void mag_widget_callback(GuiButtonType result, InputType type, void* context) {
+    Mag* mag = context;
+    if(type == InputTypeShort) {
+        view_dispatcher_send_custom_event(mag->view_dispatcher, result);
+    }
+}
+
+void mag_text_input_callback(void* context) {
+    Mag* mag = context;
+    view_dispatcher_send_custom_event(mag->view_dispatcher, MagEventNext);
+}
+
+void mag_show_loading_popup(void* context, bool show) {
+    Mag* mag = context;
+
+    if(show) {
+        // Raise timer priority so that animations can play
+        furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated);
+        view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewLoading);
+    } else {
+        // Restore default timer priority
+        furi_timer_set_thread_priority(FuriTimerThreadPriorityNormal);
+    }
+}

+ 312 - 0
magspoof/mag_device.c

@@ -0,0 +1,312 @@
+#include "mag_device.h"
+
+#include <toolbox/path.h>
+#include <flipper_format/flipper_format.h>
+
+#define TAG "MagDevice"
+
+static const char* mag_file_header = "Flipper Mag device";
+static const uint32_t mag_file_version = 1;
+
+MagDevice* mag_device_alloc() {
+    MagDevice* mag_dev = malloc(sizeof(MagDevice));
+    mag_dev->dev_data.track[0].str = furi_string_alloc();
+    mag_dev->dev_data.track[1].str = furi_string_alloc();
+    mag_dev->dev_data.track[2].str = furi_string_alloc();
+    mag_dev->storage = furi_record_open(RECORD_STORAGE);
+    mag_dev->dialogs = furi_record_open(RECORD_DIALOGS);
+    mag_dev->load_path = furi_string_alloc();
+    return mag_dev;
+}
+
+void mag_device_data_clear(MagDeviceData* dev_data) {
+    furi_string_reset(dev_data->track[0].str);
+    furi_string_reset(dev_data->track[1].str);
+    furi_string_reset(dev_data->track[2].str);
+}
+
+void mag_device_clear(MagDevice* mag_dev) {
+    furi_assert(mag_dev);
+
+    mag_device_data_clear(&mag_dev->dev_data);
+    memset(&mag_dev->dev_data, 0, sizeof(mag_dev->dev_data));
+    furi_string_reset(mag_dev->load_path);
+}
+
+void mag_device_free(MagDevice* mag_dev) {
+    furi_assert(mag_dev);
+
+    mag_device_clear(mag_dev);
+    furi_record_close(RECORD_STORAGE);
+    furi_record_close(RECORD_DIALOGS);
+    furi_string_free(mag_dev->load_path);
+
+    //furi_string_free(mag_dev->dev_data.track[0].str);
+    //furi_string_free(mag_dev->dev_data.track[1].str);
+    //furi_string_free(mag_dev->dev_data.track[2].str);
+
+    free(mag_dev);
+}
+
+void mag_device_set_name(MagDevice* mag_dev, const char* name) {
+    furi_assert(mag_dev);
+
+    strlcpy(mag_dev->dev_name, name, MAG_DEV_NAME_MAX_LEN);
+}
+
+static bool mag_device_save_file(
+    MagDevice* mag_dev,
+    const char* dev_name,
+    const char* folder,
+    const char* extension,
+    bool use_load_path) {
+    furi_assert(mag_dev);
+
+    bool saved = false;
+    FlipperFormat* file = flipper_format_file_alloc(mag_dev->storage);
+    FuriString* temp_str;
+    temp_str = furi_string_alloc();
+
+    do {
+        if(use_load_path && !furi_string_empty(mag_dev->load_path)) {
+            // Get dir name
+            path_extract_dirname(furi_string_get_cstr(mag_dev->load_path), temp_str);
+            // Create mag directory if necessary
+            if(!storage_simply_mkdir((mag_dev->storage), furi_string_get_cstr(temp_str))) break;
+            // Make path to file to be saved
+            furi_string_cat_printf(temp_str, "/%s%s", dev_name, extension);
+        } else {
+            // Create mag directory if necessary
+            if(!storage_simply_mkdir((mag_dev->storage), MAG_APP_FOLDER)) break;
+            // First remove mag device file if it was saved
+            furi_string_printf(temp_str, "%s/%s%s", folder, dev_name, extension);
+        }
+        // Open file
+        if(!flipper_format_file_open_always(file, furi_string_get_cstr(temp_str))) break;
+
+        // Write header
+        if(!flipper_format_write_header_cstr(file, mag_file_header, mag_file_version)) break;
+
+        // Write comment
+        if(!flipper_format_write_comment_cstr(file, "Mag device track data")) break;
+
+        // Write data
+        for(uint8_t i = 0; i < MAG_DEV_TRACKS; i++) {
+            furi_string_printf(temp_str, "Track %d", i + 1);
+            if(!flipper_format_write_string_cstr(
+                   file,
+                   furi_string_get_cstr(temp_str),
+                   furi_string_get_cstr(mag_dev->dev_data.track[i].str)))
+                break;
+        }
+
+        saved = true;
+    } while(0);
+
+    if(!saved) {
+        dialog_message_show_storage_error(mag_dev->dialogs, "Cannot save\nfile");
+    }
+
+    furi_string_free(temp_str);
+    flipper_format_free(file);
+
+    return saved;
+}
+
+bool mag_device_save(MagDevice* mag_dev, const char* dev_name) {
+    // wrapping function in the event we have multiple formats
+    return mag_device_save_file(mag_dev, dev_name, MAG_APP_FOLDER, MAG_APP_EXTENSION, true);
+}
+
+static bool mag_device_load_data(MagDevice* mag_dev, FuriString* path, bool show_dialog) {
+    bool parsed = false;
+
+    FlipperFormat* file = flipper_format_file_alloc(mag_dev->storage);
+    FuriString* temp_str;
+    temp_str = furi_string_alloc();
+    bool deprecated_version = false;
+    bool data_read = true;
+
+    if(mag_dev->loading_cb) {
+        mag_dev->loading_cb(mag_dev->loading_cb_ctx, true);
+    }
+
+    do {
+        if(!flipper_format_file_open_existing(file, furi_string_get_cstr(path))) break;
+
+        // Read and verify header, check file version
+        uint32_t version;
+        if(!flipper_format_read_header(file, temp_str, &version)) break;
+        if(furi_string_cmp_str(temp_str, mag_file_header) || (version != mag_file_version)) {
+            deprecated_version = true;
+            break;
+        }
+
+        // Parse data
+        for(uint8_t i = 0; i < MAG_DEV_TRACKS; i++) {
+            furi_string_printf(temp_str, "Track %d", i + 1);
+            if(!flipper_format_read_string(
+                   file, furi_string_get_cstr(temp_str), mag_dev->dev_data.track[i].str)) {
+                FURI_LOG_D(TAG, "Could not read track %d data", i + 1);
+
+                // TODO: smarter load handling now that it is acceptible for some tracks to be empty
+                data_read = false;
+            }
+        }
+
+        parsed = true;
+    } while(false);
+
+    if((!parsed) && (show_dialog)) {
+        if(deprecated_version) {
+            dialog_message_show_storage_error(mag_dev->dialogs, "File format\ndeprecated");
+        } else if(!data_read) {
+            dialog_message_show_storage_error(mag_dev->dialogs, "Cannot read\ndata");
+        } else {
+            dialog_message_show_storage_error(mag_dev->dialogs, "Cannot parse\nfile");
+        }
+    }
+
+    furi_string_free(temp_str);
+    flipper_format_free(file);
+
+    return parsed;
+}
+
+bool mag_file_select(MagDevice* mag_dev) {
+    furi_assert(mag_dev);
+
+    // Input events and views are managed by file_browser
+    FuriString* mag_app_folder;
+    mag_app_folder = furi_string_alloc_set(MAG_APP_FOLDER);
+
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(&browser_options, MAG_APP_EXTENSION, &I_mag_file_10px);
+    browser_options.base_path = MAG_APP_FOLDER;
+
+    bool res = dialog_file_browser_show(
+        mag_dev->dialogs, mag_dev->load_path, mag_app_folder, &browser_options);
+
+    furi_string_free(mag_app_folder);
+    if(res) {
+        FuriString* filename;
+        filename = furi_string_alloc();
+        path_extract_filename(mag_dev->load_path, filename, true);
+        strncpy(mag_dev->dev_name, furi_string_get_cstr(filename), MAG_DEV_NAME_MAX_LEN);
+        res = mag_device_load_data(mag_dev, mag_dev->load_path, true);
+        if(res) {
+            mag_device_set_name(mag_dev, mag_dev->dev_name);
+        }
+        furi_string_free(filename);
+    }
+
+    return res;
+}
+
+bool mag_device_delete(MagDevice* mag_dev, bool use_load_path) {
+    furi_assert(mag_dev);
+
+    bool deleted = false;
+    FuriString* file_path;
+    file_path = furi_string_alloc();
+
+    do {
+        // Delete original file
+        if(use_load_path && !furi_string_empty(mag_dev->load_path)) {
+            furi_string_set(file_path, mag_dev->load_path);
+        } else {
+            furi_string_printf(
+                file_path, "%s/%s%s", MAG_APP_FOLDER, mag_dev->dev_name, MAG_APP_EXTENSION);
+        }
+        if(!storage_simply_remove(mag_dev->storage, furi_string_get_cstr(file_path))) break;
+        deleted = true;
+    } while(false);
+
+    if(!deleted) {
+        dialog_message_show_storage_error(mag_dev->dialogs, "Cannot remove\nfile");
+    }
+
+    furi_string_free(file_path);
+    return deleted;
+}
+
+bool mag_device_parse_card_string(MagDevice* mag_dev, FuriString* f_card_str) {
+    furi_assert(mag_dev);
+    FURI_LOG_D(TAG, "mag_device_parse_card_string");
+
+    const char* card_str = furi_string_get_cstr(f_card_str);
+
+    FURI_LOG_D(TAG, "Parsing card string: %s", card_str);
+
+    // Track 1
+    const char* track1_start = strchr(card_str, '%');
+    if(!track1_start) {
+        FURI_LOG_D(TAG, "Could not find track 1 start");
+        return false;
+    }
+    track1_start++;
+    const char* track1_end = strchr(track1_start, '?');
+    if(!track1_end) {
+        FURI_LOG_D(TAG, "Could not find track 1 end");
+        return false;
+    }
+    size_t track1_len = track1_end - track1_start;
+    
+    FURI_LOG_D(TAG, "Track 1: %.*s", track1_len, track1_start);
+
+    mag_dev->dev_data.track[0].len = track1_len;
+    furi_string_printf(mag_dev->dev_data.track[0].str, "%%%.*s?", track1_len, track1_start);
+
+    // Track 2
+    const char* track2_start = strchr(track1_end, ';');
+    if (!track2_start) {
+        FURI_LOG_D(TAG, "Could not find track 2 start");
+        return true;
+    }
+
+    track2_start++;
+    const char* track2_end = strchr(track2_start, '?');
+    if(!track2_end) {
+        FURI_LOG_D(TAG, "Could not find track 2 end");
+        return true;
+    }
+    size_t track2_len = track2_end - track2_start;
+
+    FURI_LOG_D(TAG, "Track 2: %.*s", track2_len, track2_start);
+
+    mag_dev->dev_data.track[1].len = track2_len;
+    furi_string_printf(mag_dev->dev_data.track[1].str, "%%%.*s?", track2_len, track2_start);
+
+    // Track 3
+    const char* track3_start = strchr(track2_end, ';');
+    if (!track3_start) {
+        FURI_LOG_D(TAG, "Could not find track 3 start");
+        return true;
+    }
+
+    track3_start++;
+    const char* track3_end = strchr(track3_start, '?');
+    if(!track3_end) {
+        FURI_LOG_D(TAG, "Could not find track 3 end");
+        return true;
+    }
+    size_t track3_len = track3_end - track3_start;
+
+    FURI_LOG_D(TAG, "Track 3: %.*s", track3_len, track3_start);
+
+    mag_dev->dev_data.track[2].len = track3_len;
+    furi_string_printf(mag_dev->dev_data.track[2].str, "%%%.*s?", track3_len, track3_start);
+    
+    return true;
+}
+
+
+void mag_device_set_loading_callback(
+    MagDevice* mag_dev,
+    MagLoadingCallback callback,
+    void* context) {
+    furi_assert(mag_dev);
+
+    mag_dev->loading_cb = callback;
+    mag_dev->loading_cb_ctx = context;
+}

+ 58 - 0
magspoof/mag_device.h

@@ -0,0 +1,58 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <storage/storage.h>
+#include <dialogs/dialogs.h>
+
+#include "mag_icons.h"
+
+#define MAG_DEV_NAME_MAX_LEN 22
+#define MAG_DEV_TRACKS 3
+
+#define MAG_APP_FOLDER ANY_PATH("mag")
+#define MAG_APP_EXTENSION ".mag"
+
+typedef void (*MagLoadingCallback)(void* context, bool state);
+
+typedef struct {
+    FuriString* str;
+    size_t len;
+} MagTrack;
+
+typedef struct {
+    MagTrack track[MAG_DEV_TRACKS];
+} MagDeviceData;
+
+typedef struct {
+    Storage* storage;
+    DialogsApp* dialogs;
+    MagDeviceData dev_data;
+    char dev_name[MAG_DEV_NAME_MAX_LEN + 1];
+    FuriString* load_path;
+    MagLoadingCallback loading_cb;
+    void* loading_cb_ctx;
+} MagDevice;
+
+MagDevice* mag_device_alloc();
+
+void mag_device_free(MagDevice* mag_dev);
+
+void mag_device_set_name(MagDevice* mag_dev, const char* name);
+
+bool mag_device_save(MagDevice* mag_dev, const char* dev_name);
+
+bool mag_file_select(MagDevice* mag_dev);
+
+void mag_device_data_clear(MagDeviceData* dev_data);
+
+void mag_device_clear(MagDevice* mag_dev);
+
+bool mag_device_delete(MagDevice* mag_dev, bool use_load_path);
+
+bool mag_device_parse_card_string(MagDevice* mag_dev, FuriString* card_str);
+
+void mag_device_set_loading_callback(
+    MagDevice* mag_dev,
+    MagLoadingCallback callback,
+    void* context);

+ 106 - 0
magspoof/mag_i.h

@@ -0,0 +1,106 @@
+#pragma once
+
+#include "mag_device.h"
+//#include "helpers/mag_helpers.h"
+#include "helpers/mag_text_input.h"
+#include "helpers/mag_types.h"
+
+#include <furi.h>
+#include <furi_hal.h>
+#include <furi/core/log.h>
+#include <furi_hal_gpio.h>
+#include <furi_hal_resources.h>
+
+#include <gui/gui.h>
+#include <gui/view.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <notification/notification_messages.h>
+
+#include <gui/modules/submenu.h>
+#include <gui/modules/dialog_ex.h>
+#include <gui/modules/popup.h>
+#include <gui/modules/loading.h>
+#include <gui/modules/text_input.h>
+#include <gui/modules/widget.h>
+#include <gui/modules/variable_item_list.h>
+
+#include <dialogs/dialogs.h>
+#include <storage/storage.h>
+#include <flipper_format/flipper_format.h>
+
+#include <toolbox/path.h>
+#include <toolbox/value_index.h>
+
+#include "scenes/mag_scene.h"
+#include "scenes/mag_scene_read.h"
+
+#define MAG_TEXT_STORE_SIZE 150
+
+enum MagCustomEvent {
+    MagEventNext = 100,
+    MagEventExit,
+    MagEventPopupClosed,
+};
+
+typedef struct {
+    MagTxState tx;
+    MagTrackState track;
+    MagReverseState reverse;
+    uint32_t us_clock;
+    uint32_t us_interpacket;
+} MagSetting;
+
+
+typedef struct {
+    ViewDispatcher* view_dispatcher;
+    Gui* gui;
+    NotificationApp* notifications;
+    SceneManager* scene_manager;
+    Storage* storage;
+    DialogsApp* dialogs;
+    MagDevice* mag_dev;
+
+    char text_store[MAG_TEXT_STORE_SIZE + 1];
+    FuriString* file_path;
+    FuriString* file_name;
+
+    MagSetting* setting;
+
+    // Common views
+    Submenu* submenu;
+    DialogEx* dialog_ex;
+    Popup* popup;
+    Loading* loading;
+    TextInput* text_input;
+    Widget* widget;
+    VariableItemList* variable_item_list;
+
+    // Custom views
+    Mag_TextInput* mag_text_input;
+
+    // UART
+    FuriThread* uart_rx_thread;
+    FuriStreamBuffer* uart_rx_stream;
+    uint8_t uart_rx_buf[UART_RX_BUF_SIZE + 1];
+    void (*handle_rx_data_cb)(uint8_t* buf, size_t len, void* context);
+    
+    char uart_text_input_store[UART_TERMINAL_TEXT_INPUT_STORE_SIZE + 1];
+    FuriString* uart_text_box_store;
+    size_t uart_text_box_store_strlen;
+    // UART_TextInput* text_input;
+} Mag;
+
+void mag_text_store_set(Mag* mag, const char* text, ...);
+
+void mag_text_store_clear(Mag* mag);
+
+void mag_show_loading_popup(void* context, bool show);
+
+void mag_make_app_folder(Mag* mag);
+
+void mag_popup_timeout_callback(void* context);
+
+void mag_widget_callback(GuiButtonType result, InputType type, void* context);
+
+void mag_text_input_callback(void* context);

+ 30 - 0
magspoof/scenes/mag_scene.c

@@ -0,0 +1,30 @@
+#include "mag_scene.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const mag_on_enter_handlers[])(void*) = {
+#include "mag_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_event handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
+bool (*const mag_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "mag_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
+void (*const mag_on_exit_handlers[])(void* context) = {
+#include "mag_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers mag_scene_handlers = {
+    .on_enter_handlers = mag_on_enter_handlers,
+    .on_event_handlers = mag_on_event_handlers,
+    .on_exit_handlers = mag_on_exit_handlers,
+    .scene_num = MagSceneNum,
+};

+ 29 - 0
magspoof/scenes/mag_scene.h

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

+ 40 - 0
magspoof/scenes/mag_scene_about.c

@@ -0,0 +1,40 @@
+#include "../mag_i.h"
+
+void mag_scene_about_on_enter(void* context) {
+    Mag* mag = context;
+    Widget* widget = mag->widget;
+
+    FuriString* tmp_str;
+    tmp_str = furi_string_alloc();
+
+    furi_string_cat_printf(tmp_str, "Version: %s\n", MAG_VERSION_APP);
+    furi_string_cat_printf(tmp_str, "Developer: %s\n", MAG_DEVELOPER);
+    furi_string_cat_printf(tmp_str, "GitHub: %s\n\n", MAG_GITHUB);
+
+    furi_string_cat_printf(
+        tmp_str,
+        "Unfinished port of Samy Kamkar's MagSpoof. Confer GitHub for updates; in the interim, use responsibly and at your own risk.");
+
+    // TODO: Add credits
+
+    widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(tmp_str));
+    furi_string_free(tmp_str);
+
+    view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewWidget);
+}
+
+bool mag_scene_about_on_event(void* context, SceneManagerEvent event) {
+    Mag* mag = context;
+    SceneManager* scene_manager = mag->scene_manager;
+    bool consumed = false;
+
+    UNUSED(event);
+    UNUSED(scene_manager);
+
+    return consumed;
+}
+
+void mag_scene_about_on_exit(void* context) {
+    Mag* mag = context;
+    widget_reset(mag->widget);
+}

+ 15 - 0
magspoof/scenes/mag_scene_config.h

@@ -0,0 +1,15 @@
+ADD_SCENE(mag, start, Start)
+ADD_SCENE(mag, about, About)
+ADD_SCENE(mag, emulate, Emulate)
+ADD_SCENE(mag, emulate_config, EmulateConfig)
+ADD_SCENE(mag, file_select, FileSelect)
+ADD_SCENE(mag, saved_menu, SavedMenu)
+ADD_SCENE(mag, saved_info, SavedInfo)
+ADD_SCENE(mag, input_name, InputName)
+ADD_SCENE(mag, input_value, InputValue)
+ADD_SCENE(mag, save_success, SaveSuccess)
+ADD_SCENE(mag, delete_success, DeleteSuccess)
+ADD_SCENE(mag, delete_confirm, DeleteConfirm)
+ADD_SCENE(mag, exit_confirm, ExitConfirm)
+ADD_SCENE(mag, under_construction, UnderConstruction)
+ADD_SCENE(mag, read, Read)

+ 49 - 0
magspoof/scenes/mag_scene_delete_confirm.c

@@ -0,0 +1,49 @@
+#include "../mag_i.h"
+#include "../mag_device.h"
+
+void mag_scene_delete_confirm_on_enter(void* context) {
+    Mag* mag = context;
+    Widget* widget = mag->widget;
+    MagDevice* mag_dev = mag->mag_dev;
+
+    FuriString* tmp_str;
+    tmp_str = furi_string_alloc();
+
+    furi_string_printf(tmp_str, "\e#Delete %s?\e#", mag_dev->dev_name);
+
+    //TODO: print concise summary of data on card? Would need to vary by card/track type
+
+    widget_add_text_box_element(
+        widget, 0, 0, 128, 27, AlignCenter, AlignCenter, furi_string_get_cstr(tmp_str), true);
+    widget_add_button_element(widget, GuiButtonTypeLeft, "Cancel", mag_widget_callback, mag);
+    widget_add_button_element(widget, GuiButtonTypeRight, "Delete", mag_widget_callback, mag);
+
+    view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewWidget);
+
+    furi_string_free(tmp_str);
+}
+
+bool mag_scene_delete_confirm_on_event(void* context, SceneManagerEvent event) {
+    Mag* mag = context;
+    SceneManager* scene_manager = mag->scene_manager;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == GuiButtonTypeRight) {
+            consumed = true;
+            if(mag_device_delete(mag->mag_dev, true)) {
+                scene_manager_next_scene(scene_manager, MagSceneDeleteSuccess);
+            }
+        } else if(event.event == GuiButtonTypeLeft) {
+            consumed = true;
+            scene_manager_previous_scene(scene_manager);
+        }
+    }
+
+    return consumed;
+}
+
+void mag_scene_delete_confirm_on_exit(void* context) {
+    Mag* mag = context;
+    widget_reset(mag->widget);
+}

+ 39 - 0
magspoof/scenes/mag_scene_delete_success.c

@@ -0,0 +1,39 @@
+#include "../mag_i.h"
+
+void mag_scene_delete_success_on_enter(void* context) {
+    Mag* mag = context;
+    Popup* popup = mag->popup;
+
+    popup_set_icon(popup, 0, 2, &I_DolphinMafia_115x62);
+    popup_set_header(popup, "Deleted", 83, 19, AlignLeft, AlignBottom);
+
+    popup_set_callback(popup, mag_popup_timeout_callback);
+    popup_set_context(popup, mag);
+    popup_set_timeout(popup, 1500);
+    popup_enable_timeout(popup);
+
+    view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewPopup);
+}
+
+bool mag_scene_delete_success_on_event(void* context, SceneManagerEvent event) {
+    Mag* mag = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == MagEventPopupClosed) {
+            consumed = true;
+
+            scene_manager_search_and_switch_to_previous_scene(
+                mag->scene_manager, MagSceneFileSelect);
+        }
+    }
+
+    return consumed;
+}
+
+void mag_scene_delete_success_on_exit(void* context) {
+    Mag* mag = context;
+    Popup* popup = mag->popup;
+
+    popup_reset(popup);
+}

+ 93 - 0
magspoof/scenes/mag_scene_emulate.c

@@ -0,0 +1,93 @@
+#include "../mag_i.h"
+#include "../helpers/mag_helpers.h"
+
+#define TAG "MagSceneEmulate"
+
+void cat_trackstr(FuriString* str, uint8_t calls, uint8_t i, FuriString* trackstr) {
+    furi_string_cat_printf(
+        str,
+        "%sTrack %d:%s%s\n",
+        (calls == 0) ? "" : "\n", // if first line, don't prepend a "\n"
+        (i + 1),
+        furi_string_empty(trackstr) ? "  " : "\n",
+        furi_string_empty(trackstr) ? "< empty >" : furi_string_get_cstr(trackstr));
+}
+
+void mag_scene_emulate_on_enter(void* context) {
+    Mag* mag = context;
+    Widget* widget = mag->widget;
+
+    FuriString* tmp_str;
+    tmp_str = furi_string_alloc();
+
+    // Use strlcpy instead perhaps, to truncate to screen width, then add ellipses if needed?
+    furi_string_printf(tmp_str, "%s\r\n", mag->mag_dev->dev_name);
+
+    // TODO: Display other relevant config settings (namely RFID vs GPIO)?
+
+    widget_add_icon_element(widget, 1, 1, &I_mag_file_10px);
+    widget_add_string_element(
+        widget, 13, 2, AlignLeft, AlignTop, FontPrimary, furi_string_get_cstr(tmp_str));
+    furi_string_reset(tmp_str);
+
+    FURI_LOG_D(TAG, "%d", mag->setting->reverse);
+
+    // print relevant data
+    uint8_t cat_count = 0;
+    for(uint8_t i = 0; i < MAG_DEV_TRACKS; i++) {
+        FuriString* trackstr = mag->mag_dev->dev_data.track[i].str;
+
+        // still messy / dumb way to do this, but slightly cleaner than before.
+        // will clean up more later
+        switch(mag->setting->track) {
+        case MagTrackStateOne:
+            if(i == 0) cat_trackstr(tmp_str, cat_count++, i, trackstr);
+            break;
+        case MagTrackStateTwo:
+            if(i == 1) cat_trackstr(tmp_str, cat_count++, i, trackstr);
+            break;
+        case MagTrackStateThree:
+            if(i == 2) cat_trackstr(tmp_str, cat_count++, i, trackstr);
+            break;
+        case MagTrackStateOneAndTwo:
+            if((i == 0) | (i == 1)) cat_trackstr(tmp_str, cat_count++, i, trackstr);
+            break;
+        }
+    }
+
+    widget_add_text_scroll_element(widget, 0, 15, 128, 49, furi_string_get_cstr(tmp_str));
+
+    widget_add_button_element(widget, GuiButtonTypeLeft, "Config", mag_widget_callback, mag);
+    widget_add_button_element(widget, GuiButtonTypeRight, "Send", mag_widget_callback, mag);
+    view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewWidget);
+    furi_string_free(tmp_str);
+}
+
+bool mag_scene_emulate_on_event(void* context, SceneManagerEvent event) {
+    Mag* mag = context;
+    SceneManager* scene_manager = mag->scene_manager;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case GuiButtonTypeLeft:
+            consumed = true;
+            scene_manager_next_scene(scene_manager, MagSceneEmulateConfig);
+            break;
+        case GuiButtonTypeRight:
+            consumed = true;
+            notification_message(mag->notifications, &sequence_blink_start_cyan);
+            mag_spoof(mag);
+            notification_message(mag->notifications, &sequence_blink_stop);
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+void mag_scene_emulate_on_exit(void* context) {
+    Mag* mag = context;
+    notification_message(mag->notifications, &sequence_blink_stop);
+    widget_reset(mag->widget);
+}

+ 264 - 0
magspoof/scenes/mag_scene_emulate_config.c

@@ -0,0 +1,264 @@
+#include "../mag_i.h"
+
+#define TAG "MagSceneEmulateConfig"
+
+enum MagSettingIndex {
+    MagSettingIndexTx,
+    MagSettingIndexTrack,
+    MagSettingIndexReverse,
+    MagSettingIndexClock,
+    MagSettingIndexInterpacket,
+};
+
+#define TX_COUNT 7
+const char* const tx_text[TX_COUNT] = {
+    "RFID",
+    "GPIO",
+    "Piezo",
+    "LF + P",
+    "NFC",
+    "434MHz",
+    "868MHz",
+};
+const uint32_t tx_value[TX_COUNT] = {
+    MagTxStateRFID,
+    MagTxStateGPIO,
+    MagTxStatePiezo,
+    MagTxStateLF_P,
+    MagTxStateNFC,
+    MagTxCC1101_434,
+    MagTxCC1101_868,
+};
+
+#define TRACK_COUNT 4
+const char* const track_text[TRACK_COUNT] = {
+    "1 + 2",
+    "1",
+    "2",
+    "3",
+};
+const uint32_t track_value[TRACK_COUNT] = {
+    MagTrackStateOneAndTwo,
+    MagTrackStateOne,
+    MagTrackStateTwo,
+    MagTrackStateThree,
+};
+
+#define REVERSE_COUNT 2
+const char* const reverse_text[REVERSE_COUNT] = {
+    "OFF",
+    "ON",
+};
+const uint32_t reverse_value[REVERSE_COUNT] = {
+    MagReverseStateOff,
+    MagReverseStateOn,
+};
+
+#define CLOCK_COUNT 15
+const char* const clock_text[CLOCK_COUNT] = {
+    "200us",
+    "220us",
+    "240us",
+    "250us",
+    "260us",
+    "280us",
+    "300us",
+    "325us",
+    "350us",
+    "375us",
+    "400us",
+    "450us",
+    "500us",
+    "600us",
+    "700us",
+};
+const uint32_t clock_value[CLOCK_COUNT] = {
+    200,
+    220,
+    240,
+    250,
+    260,
+    280,
+    300,
+    325,
+    350,
+    375,
+    400,
+    450,
+    500,
+    600,
+    700,
+};
+
+#define INTERPACKET_COUNT 13
+const char* const interpacket_text[INTERPACKET_COUNT] = {
+    "0us",
+    "2us",
+    "4us",
+    "6us",
+    "8us",
+    "10us",
+    "12us",
+    "14us",
+    "16us",
+    "18us",
+    "20us",
+    "25us",
+    "30us",
+};
+const uint32_t interpacket_value[INTERPACKET_COUNT] = {
+    0,
+    2,
+    4,
+    6,
+    8,
+    10,
+    12,
+    14,
+    16,
+    18,
+    20,
+    25,
+    30,
+};
+
+static void mag_scene_emulate_config_set_tx(VariableItem* item) {
+    Mag* mag = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+
+    variable_item_set_current_value_text(item, tx_text[index]);
+
+    mag->setting->tx = tx_value[index];
+};
+
+static void mag_scene_emulate_config_set_track(VariableItem* item) {
+    Mag* mag = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+
+    if(mag->setting->reverse == MagReverseStateOff) {
+        variable_item_set_current_value_text(item, track_text[index]);
+        mag->setting->track = track_value[index];
+    } else if(mag->setting->reverse == MagReverseStateOn) {
+        variable_item_set_current_value_index(
+            item, value_index_uint32(MagTrackStateOneAndTwo, track_value, TRACK_COUNT));
+    }
+
+    // TODO: Check there is data in selected track?
+    //       Only display track options with data?
+};
+
+static void mag_scene_emulate_config_set_reverse(VariableItem* item) {
+    Mag* mag = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+
+    if(mag->setting->track == MagTrackStateOneAndTwo) {
+        // only allow reverse track to be set when playing both 1 and 2
+        variable_item_set_current_value_text(item, reverse_text[index]);
+        mag->setting->reverse = reverse_value[index];
+        //FURI_LOG_D(TAG, "%s", reverse_text[index]);
+        //FURI_LOG_D(TAG, "%d", mag->setting->reverse);
+    } else {
+        variable_item_set_current_value_index(
+            item, value_index_uint32(MagReverseStateOff, reverse_value, REVERSE_COUNT));
+    }
+};
+
+static void mag_scene_emulate_config_set_clock(VariableItem* item) {
+    Mag* mag = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+
+    variable_item_set_current_value_text(item, clock_text[index]);
+
+    mag->setting->us_clock = clock_value[index];
+};
+
+static void mag_scene_emulate_config_set_interpacket(VariableItem* item) {
+    Mag* mag = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+
+    variable_item_set_current_value_text(item, interpacket_text[index]);
+
+    mag->setting->us_interpacket = interpacket_value[index];
+};
+
+void mag_scene_emulate_config_on_enter(void* context) {
+    // TODO: retrieve current values from struct, rather than setting to default on setup
+
+    Mag* mag = context;
+    VariableItem* item;
+    uint8_t value_index;
+
+    // TX
+    item = variable_item_list_add(
+        mag->variable_item_list, "TX via:", TX_COUNT, mag_scene_emulate_config_set_tx, mag);
+    value_index = value_index_uint32(mag->setting->tx, tx_value, TX_COUNT);
+    scene_manager_set_scene_state(mag->scene_manager, MagSceneEmulateConfig, (uint32_t)item);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, tx_text[value_index]);
+
+    // Track
+    item = variable_item_list_add(
+        mag->variable_item_list, "Track:", TRACK_COUNT, mag_scene_emulate_config_set_track, mag);
+    value_index = value_index_uint32(mag->setting->track, track_value, TRACK_COUNT);
+    scene_manager_set_scene_state(mag->scene_manager, MagSceneEmulateConfig, (uint32_t)item);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, track_text[value_index]);
+
+    // Reverse
+    //FURI_LOG_D(TAG, "%d", mag->setting->reverse);
+    item = variable_item_list_add(
+        mag->variable_item_list,
+        "Reverse:",
+        REVERSE_COUNT,
+        mag_scene_emulate_config_set_reverse,
+        mag);
+    value_index = value_index_uint32(mag->setting->reverse, reverse_value, REVERSE_COUNT);
+    scene_manager_set_scene_state(mag->scene_manager, MagSceneEmulateConfig, (uint32_t)item);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, reverse_text[value_index]);
+
+    // Clock
+    item = variable_item_list_add(
+        mag->variable_item_list, "Clock:", CLOCK_COUNT, mag_scene_emulate_config_set_clock, mag);
+    value_index = value_index_uint32(mag->setting->us_clock, clock_value, CLOCK_COUNT);
+    scene_manager_set_scene_state(mag->scene_manager, MagSceneEmulateConfig, (uint32_t)item);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, clock_text[value_index]);
+
+    // Interpacket
+    /*
+    item = variable_item_list_add(
+        mag->variable_item_list,
+        "Interpacket:",
+        INTERPACKET_COUNT,
+        mag_scene_emulate_config_set_interpacket,
+        mag);
+    value_index =
+        value_index_uint32(mag->setting->us_interpacket, interpacket_value, INTERPACKET_COUNT);
+    scene_manager_set_scene_state(mag->scene_manager, MagSceneEmulateConfig, (uint32_t)item);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, interpacket_text[value_index]);*/
+    UNUSED(mag_scene_emulate_config_set_interpacket);
+
+    view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewVariableItemList);
+}
+
+bool mag_scene_emulate_config_on_event(void* context, SceneManagerEvent event) {
+    Mag* mag = context;
+    SceneManager* scene_manager = mag->scene_manager;
+    bool consumed = false;
+
+    UNUSED(mag);
+    UNUSED(scene_manager);
+    UNUSED(event);
+
+    return consumed;
+}
+
+void mag_scene_emulate_config_on_exit(void* context) {
+    Mag* mag = context;
+    variable_item_list_set_selected_item(mag->variable_item_list, 0);
+    variable_item_list_reset(mag->variable_item_list);
+    // mag_last_settings_save?
+    // scene_manager_set_scene_state? Using subghz_scene_reciever_config as framework/inspo
+}

+ 20 - 0
magspoof/scenes/mag_scene_exit_confirm.c

@@ -0,0 +1,20 @@
+#include "../mag_i.h"
+
+void mag_scene_exit_confirm_on_enter(void* context) {
+    Mag* mag = context;
+    UNUSED(mag);
+}
+
+bool mag_scene_exit_confirm_on_event(void* context, SceneManagerEvent event) {
+    Mag* mag = context;
+    UNUSED(mag);
+    UNUSED(event);
+    bool consumed = false;
+
+    return consumed;
+}
+
+void mag_scene_exit_confirm_on_exit(void* context) {
+    Mag* mag = context;
+    UNUSED(mag);
+}

+ 24 - 0
magspoof/scenes/mag_scene_file_select.c

@@ -0,0 +1,24 @@
+#include "../mag_i.h"
+#include "../mag_device.h"
+
+void mag_scene_file_select_on_enter(void* context) {
+    Mag* mag = context;
+    //UNUSED(mag);
+    mag_device_set_loading_callback(mag->mag_dev, mag_show_loading_popup, mag);
+    if(mag_file_select(mag->mag_dev)) {
+        scene_manager_next_scene(mag->scene_manager, MagSceneSavedMenu);
+    } else {
+        scene_manager_search_and_switch_to_previous_scene(mag->scene_manager, MagSceneStart);
+    }
+    mag_device_set_loading_callback(mag->mag_dev, NULL, mag);
+}
+
+bool mag_scene_file_select_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+
+void mag_scene_file_select_on_exit(void* context) {
+    UNUSED(context);
+}

+ 82 - 0
magspoof/scenes/mag_scene_input_name.c

@@ -0,0 +1,82 @@
+#include <toolbox/name_generator.h>
+#include "../mag_i.h"
+
+void mag_scene_input_name_on_enter(void* context) {
+    Mag* mag = context;
+    TextInput* text_input = mag->text_input;
+    FuriString* folder_path;
+    folder_path = furi_string_alloc();
+
+    //TODO: compatible types / etc
+    //bool name_is_empty = furi_string_empty(mag->mag_dev->dev_name);
+    bool name_is_empty = true;
+
+    if(name_is_empty) {
+        furi_string_set(mag->file_path, MAG_APP_FOLDER);
+        name_generator_make_auto(mag->text_store, MAG_TEXT_STORE_SIZE, "Mag");
+        furi_string_set(folder_path, MAG_APP_FOLDER);
+    } else {
+        // TODO: compatible types etc
+        //mag_text_store_set(mag, "%s", furi_string_get_cstr(mag->mag_dev->dev_name));
+        path_extract_dirname(furi_string_get_cstr(mag->file_path), folder_path);
+    }
+
+    text_input_set_header_text(text_input, "Name the card");
+    text_input_set_result_callback(
+        text_input,
+        mag_text_input_callback,
+        mag,
+        mag->text_store,
+        MAG_DEV_NAME_MAX_LEN,
+        name_is_empty);
+
+    FURI_LOG_I("", "%s %s", furi_string_get_cstr(folder_path), mag->text_store);
+
+    ValidatorIsFile* validator_is_file = validator_is_file_alloc_init(
+        furi_string_get_cstr(folder_path),
+        MAG_APP_EXTENSION,
+        furi_string_get_cstr(mag->file_name));
+    text_input_set_validator(text_input, validator_is_file_callback, validator_is_file);
+
+    furi_string_free(folder_path);
+
+    view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewTextInput);
+}
+
+bool mag_scene_input_name_on_event(void* context, SceneManagerEvent event) {
+    Mag* mag = context;
+    SceneManager* scene_manager = mag->scene_manager;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == MagEventNext) {
+            consumed = true;
+            //if(!furi_string_empty(mag->file_name)) {
+            //    mag_delete_key(mag);
+            //}
+
+            furi_string_set(mag->file_name, mag->text_store);
+
+            if(mag_device_save(mag->mag_dev, furi_string_get_cstr(mag->file_name))) {
+                scene_manager_next_scene(scene_manager, MagSceneSaveSuccess);
+            } else {
+                //scene_manager_search_and_switch_to_previous_scene(
+                //    scene_manager, MagSceneReadKeyMenu);
+                // TODO: Replace with appropriate scene! No read scene prior if adding manually...
+            }
+        }
+    }
+
+    return consumed;
+}
+
+void mag_scene_input_name_on_exit(void* context) {
+    Mag* mag = context;
+    TextInput* text_input = mag->text_input;
+
+    void* validator_context = text_input_get_validator_callback_context(text_input);
+    text_input_set_validator(text_input, NULL, NULL);
+    validator_is_file_free((ValidatorIsFile*)validator_context);
+
+    text_input_reset(text_input);
+}

+ 37 - 0
magspoof/scenes/mag_scene_input_value.c

@@ -0,0 +1,37 @@
+#include "../mag_i.h"
+
+void mag_scene_input_value_on_enter(void* context) {
+    Mag* mag = context;
+    Mag_TextInput* mag_text_input = mag->mag_text_input;
+
+    // TODO: retrieve stored/existing data if editing rather than adding anew?
+    mag_text_store_set(mag, furi_string_get_cstr(mag->mag_dev->dev_data.track[1].str));
+
+    mag_text_input_set_header_text(mag_text_input, "Enter track data (WIP)");
+    mag_text_input_set_result_callback(
+        mag_text_input, mag_text_input_callback, mag, mag->text_store, MAG_TEXT_STORE_SIZE, true);
+
+    view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewMagTextInput);
+}
+
+bool mag_scene_input_value_on_event(void* context, SceneManagerEvent event) {
+    Mag* mag = context;
+    SceneManager* scene_manager = mag->scene_manager;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == MagEventNext) {
+            consumed = true;
+
+            furi_string_set(mag->mag_dev->dev_data.track[1].str, mag->text_store);
+            scene_manager_next_scene(scene_manager, MagSceneInputName);
+        }
+    }
+
+    return consumed;
+}
+
+void mag_scene_input_value_on_exit(void* context) {
+    Mag* mag = context;
+    UNUSED(mag);
+}

+ 185 - 0
magspoof/scenes/mag_scene_read.c

@@ -0,0 +1,185 @@
+// Creator: Hummus@FlipperGang
+
+#include "../mag_i.h"
+#include "../helpers/mag_helpers.h"
+
+#include "mag_scene_read.h"
+
+#define TAG "MagSceneRead"
+
+void uart_callback(UartIrqEvent event, uint8_t data, void* context) {
+    Mag* mag = context;
+    if(event == UartIrqEventRXNE) {
+        furi_stream_buffer_send(mag->uart_rx_stream, &data, 1, 0);
+        furi_thread_flags_set(furi_thread_get_id(mag->uart_rx_thread), WorkerEvtRxDone);
+    }
+}
+
+static int32_t uart_worker(void* context) {
+    Mag* mag = context;
+    mag->uart_rx_stream = furi_stream_buffer_alloc(UART_RX_BUF_SIZE, 1);
+    mag->uart_text_box_store_strlen = 0;
+
+    while(1) {
+        uint32_t events =
+            furi_thread_flags_wait(WORKER_ALL_RX_EVENTS, FuriFlagWaitAny, FuriWaitForever);
+        // furi_check((events & FuriFlagError) == 0);
+
+        if(events & WorkerEvtStop) break;
+        if(events & WorkerEvtRxDone) {
+            FURI_LOG_D(TAG, "WorkerEvtRxDone");
+            // notification_message(mag->notifications, &sequence_success);
+            size_t len = furi_stream_buffer_receive(
+                mag->uart_rx_stream, mag->uart_rx_buf, UART_RX_BUF_SIZE, 200);
+            FURI_LOG_D(TAG, "UART RX len: %d", len);
+
+            if(len > 0) {
+                // If text box store gets too big, then truncate it
+                mag->uart_text_box_store_strlen += len;
+
+                if(mag->uart_text_box_store_strlen >= UART_TERMINAL_TEXT_BOX_STORE_SIZE - 1) {
+                    furi_string_right(
+                        mag->uart_text_box_store, mag->uart_text_box_store_strlen / 2);
+                    mag->uart_text_box_store_strlen =
+                        furi_string_size(mag->uart_text_box_store) + len;
+                }
+
+                // Add '\0' to the end of the string, and then add the new data
+                mag->uart_rx_buf[len] = '\0';
+                furi_string_cat_printf(mag->uart_text_box_store, "%s", mag->uart_rx_buf);
+
+                FURI_LOG_D(TAG, "UART RX buf: %*.s", len, mag->uart_rx_buf);
+                FURI_LOG_D(
+                    TAG, "UART RX store: %s", furi_string_get_cstr(mag->uart_text_box_store));
+            }
+
+            FURI_LOG_D(TAG, "UARTEventRxData");
+
+            view_dispatcher_send_custom_event(mag->view_dispatcher, UARTEventRxData);
+        }
+    }
+
+    furi_stream_buffer_free(mag->uart_rx_stream);
+
+    return 0;
+}
+
+void update_widgets(Mag* mag) {
+    // Clear widget from all elements
+    widget_reset(mag->widget);
+
+    // Titlebar
+    widget_add_icon_element(mag->widget, 38, -1, &I_mag_file_10px);
+    widget_add_string_element(mag->widget, 64, 0, AlignCenter, AlignTop, FontPrimary, "READ");
+    widget_add_icon_element(mag->widget, 81, -1, &I_mag_file_10px);
+
+    // Text box
+    widget_add_text_scroll_element(
+        mag->widget, 0, 10, 128, 40, furi_string_get_cstr(mag->uart_text_box_store));
+
+    // Buttons
+    widget_add_button_element(mag->widget, GuiButtonTypeLeft, "Clear", mag_widget_callback, mag);
+    widget_add_button_element(mag->widget, GuiButtonTypeRight, "Parse", mag_widget_callback, mag);
+}
+
+void mag_scene_read_on_enter(void* context) {
+    Mag* mag = context;
+    FuriString* message = furi_string_alloc();
+    furi_string_printf(message, "Please swipe a card!\n");
+    mag->uart_text_box_store = message;
+
+    view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewWidget);
+
+    update_widgets(mag);
+
+    // Initialize UART
+    // furi_hal_console_disable();
+    furi_hal_uart_deinit(FuriHalUartIdUSART1);
+    furi_hal_uart_init(FuriHalUartIdUSART1, 9600);
+    furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, uart_callback, mag);
+    FURI_LOG_D(TAG, "UART initialized");
+
+    mag->uart_rx_thread = furi_thread_alloc();
+    furi_thread_set_name(mag->uart_rx_thread, "UartRx");
+    furi_thread_set_stack_size(mag->uart_rx_thread, 1024);
+    furi_thread_set_context(mag->uart_rx_thread, mag);
+    furi_thread_set_callback(mag->uart_rx_thread, uart_worker);
+
+    furi_thread_start(mag->uart_rx_thread);
+    FURI_LOG_D(TAG, "UART worker started");
+}
+
+bool mag_scene_read_on_event(void* context, SceneManagerEvent event) {
+    Mag* mag = context;
+
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        FURI_LOG_D(TAG, "Custom event: %ld", event.event);
+
+        switch(event.event) {
+        case GuiButtonTypeLeft: // Clear
+            consumed = true;
+            // Clear text box store
+            furi_string_reset(mag->uart_text_box_store);
+            mag->uart_text_box_store_strlen = 0;
+            break;
+
+        case GuiButtonTypeRight: // Parse
+            consumed = true;
+            FURI_LOG_D(TAG, "Trying to parse");
+            MagDevice* mag_dev = mag->mag_dev;
+
+            bool res = mag_device_parse_card_string(mag_dev, mag->uart_text_box_store);
+            furi_string_reset(mag->uart_text_box_store);
+            if(res) {
+                notification_message(mag->notifications, &sequence_success);
+
+                furi_string_printf(
+                    mag->uart_text_box_store,
+                    "Track 1: %.*s\nTrack 2: %.*s\nTrack 3: %.*s",
+                    mag_dev->dev_data.track[0].len,
+                    furi_string_get_cstr(mag_dev->dev_data.track[0].str),
+                    mag_dev->dev_data.track[1].len,
+                    furi_string_get_cstr(mag_dev->dev_data.track[1].str),
+                    mag_dev->dev_data.track[2].len,
+                    furi_string_get_cstr(mag_dev->dev_data.track[2].str));
+
+                // Switch to saved menu scene
+                scene_manager_next_scene(mag->scene_manager, MagSceneSavedMenu);
+
+            } else {
+                furi_string_printf(mag->uart_text_box_store, "Failed to parse! Try again\n");
+                notification_message(mag->notifications, &sequence_error);
+            }
+
+            break;
+        }
+
+        update_widgets(mag);
+    }
+
+    return consumed;
+}
+
+void mag_scene_read_on_exit(void* context) {
+    Mag* mag = context;
+    // notification_message(mag->notifications, &sequence_blink_stop);
+    widget_reset(mag->widget);
+    // view_dispatcher_remove_view(mag->view_dispatcher, MagViewWidget);
+
+    // Stop UART worker
+    FURI_LOG_D(TAG, "Stopping UART worker");
+    furi_thread_flags_set(furi_thread_get_id(mag->uart_rx_thread), WorkerEvtStop);
+    furi_thread_join(mag->uart_rx_thread);
+    furi_thread_free(mag->uart_rx_thread);
+    FURI_LOG_D(TAG, "UART worker stopped");
+
+    furi_string_free(mag->uart_text_box_store);
+
+    furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, NULL, NULL);
+    furi_hal_uart_deinit(FuriHalUartIdUSART1);
+    // furi_hal_console_enable();
+
+    notification_message(mag->notifications, &sequence_blink_stop);
+}

+ 21 - 0
magspoof/scenes/mag_scene_read.h

@@ -0,0 +1,21 @@
+#pragma once
+
+#include <gui/modules/text_box.h>
+
+#define UART_RX_BUF_SIZE (320)
+#define UART_TERMINAL_TEXT_BOX_STORE_SIZE (4096)
+#define UART_TERMINAL_TEXT_INPUT_STORE_SIZE (512)
+#define UART_CH (FuriHalUartIdUSART1)
+#define UART_BAUDRATE (9600)
+
+typedef enum {
+    WorkerEvtStop = (1 << 0),
+    WorkerEvtRxDone = (1 << 1),
+} WorkerEvtFlags;
+
+typedef enum {
+    UARTEventRxData = 100,
+} UARTEvents;
+
+
+#define WORKER_ALL_RX_EVENTS (WorkerEvtStop | WorkerEvtRxDone)

+ 43 - 0
magspoof/scenes/mag_scene_save_success.c

@@ -0,0 +1,43 @@
+#include "../mag_i.h"
+
+void mag_scene_save_success_on_enter(void* context) {
+    Mag* mag = context;
+    Popup* popup = mag->popup;
+
+    // Clear state of data enter scene
+    //scene_manager_set_scene_state(mag->scene_manager, LfRfidSceneSaveData, 0);
+    mag_text_store_clear(mag);
+
+    popup_set_icon(popup, 32, 5, &I_DolphinNice_96x59);
+    popup_set_header(popup, "Saved!", 5, 7, AlignLeft, AlignTop);
+    popup_set_context(popup, mag);
+    popup_set_callback(popup, mag_popup_timeout_callback);
+    popup_set_timeout(popup, 1500);
+    popup_enable_timeout(popup);
+
+    view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewPopup);
+}
+
+bool mag_scene_save_success_on_event(void* context, SceneManagerEvent event) {
+    Mag* mag = context;
+    bool consumed = false;
+
+    if((event.type == SceneManagerEventTypeBack) ||
+       ((event.type == SceneManagerEventTypeCustom) && (event.event == MagEventPopupClosed))) {
+        bool result =
+            scene_manager_search_and_switch_to_previous_scene(mag->scene_manager, MagSceneStart);
+        if(!result) {
+            scene_manager_search_and_switch_to_another_scene(
+                mag->scene_manager, MagSceneFileSelect);
+        }
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+void mag_scene_save_success_on_exit(void* context) {
+    Mag* mag = context;
+
+    popup_reset(mag->popup);
+}

+ 50 - 0
magspoof/scenes/mag_scene_saved_info.c

@@ -0,0 +1,50 @@
+#include "../mag_i.h"
+
+void mag_scene_saved_info_on_enter(void* context) {
+    Mag* mag = context;
+    Widget* widget = mag->widget;
+
+    FuriString* tmp_str;
+    tmp_str = furi_string_alloc();
+
+    // Use strlcpy instead perhaps, to truncate to screen width, then add ellipses if needed?
+    furi_string_printf(tmp_str, "%s\r\n", mag->mag_dev->dev_name);
+
+    widget_add_icon_element(widget, 1, 1, &I_mag_file_10px);
+    widget_add_string_element(
+        widget, 13, 2, AlignLeft, AlignTop, FontPrimary, furi_string_get_cstr(tmp_str));
+    furi_string_reset(tmp_str);
+
+    for(uint8_t i = 0; i < MAG_DEV_TRACKS; i++) {
+        FuriString* trackstr = mag->mag_dev->dev_data.track[i].str;
+
+        furi_string_cat_printf(
+            tmp_str,
+            "Track %d:%s%s%s",
+            (i + 1),
+            furi_string_empty(trackstr) ? "  " : "\n",
+            furi_string_empty(trackstr) ? "< empty >" : furi_string_get_cstr(trackstr),
+            (i + 1 == MAG_DEV_TRACKS) ? "" : "\n\n");
+    }
+
+    widget_add_text_scroll_element(widget, 0, 15, 128, 49, furi_string_get_cstr(tmp_str));
+
+    view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewWidget);
+    furi_string_free(tmp_str);
+}
+
+bool mag_scene_saved_info_on_event(void* context, SceneManagerEvent event) {
+    Mag* mag = context;
+    SceneManager* scene_manager = mag->scene_manager;
+    bool consumed = false;
+
+    UNUSED(event);
+    UNUSED(scene_manager);
+
+    return consumed;
+}
+
+void mag_scene_saved_info_on_exit(void* context) {
+    Mag* mag = context;
+    widget_reset(mag->widget);
+}

+ 81 - 0
magspoof/scenes/mag_scene_saved_menu.c

@@ -0,0 +1,81 @@
+#include "../mag_i.h"
+
+enum SubmenuIndex {
+    SubmenuIndexEmulate,
+    //SubmenuIndexEdit,
+    SubmenuIndexDelete,
+    SubmenuIndexInfo,
+};
+
+void mag_scene_saved_menu_submenu_callback(void* context, uint32_t index) {
+    Mag* mag = context;
+
+    view_dispatcher_send_custom_event(mag->view_dispatcher, index);
+}
+
+void mag_scene_saved_menu_on_enter(void* context) {
+    Mag* mag = context;
+    Submenu* submenu = mag->submenu;
+
+    // messy code to quickly check which tracks are available for emulation/display
+    // there's likely a better spot to do this, but the MagDevice functions don't have access to the full mag struct...
+    bool is_empty_t1 = furi_string_empty(mag->mag_dev->dev_data.track[0].str);
+    bool is_empty_t2 = furi_string_empty(mag->mag_dev->dev_data.track[1].str);
+    bool is_empty_t3 = furi_string_empty(mag->mag_dev->dev_data.track[2].str);
+
+    if(!is_empty_t1 && !is_empty_t2) {
+        mag->setting->track = MagTrackStateOneAndTwo;
+    } else if(!is_empty_t1) {
+        mag->setting->track = MagTrackStateOne;
+    } else if(!is_empty_t2) {
+        mag->setting->track = MagTrackStateTwo;
+    } else if(!is_empty_t3) {
+        mag->setting->track = MagTrackStateThree;
+    } // TODO: what happens if no track data present?
+
+    submenu_add_item(
+        submenu, "Emulate (WIP)", SubmenuIndexEmulate, mag_scene_saved_menu_submenu_callback, mag);
+    //submenu_add_item(
+    //    submenu, "Edit (WIP)", SubmenuIndexEdit, mag_scene_saved_menu_submenu_callback, mag);
+    submenu_add_item(
+        submenu, "Delete", SubmenuIndexDelete, mag_scene_saved_menu_submenu_callback, mag);
+    submenu_add_item(
+        submenu, "Info", SubmenuIndexInfo, mag_scene_saved_menu_submenu_callback, mag);
+
+    submenu_set_selected_item(
+        mag->submenu, scene_manager_get_scene_state(mag->scene_manager, MagSceneSavedMenu));
+
+    view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewSubmenu);
+}
+
+bool mag_scene_saved_menu_on_event(void* context, SceneManagerEvent event) {
+    Mag* mag = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        scene_manager_set_scene_state(mag->scene_manager, MagSceneSavedMenu, event.event);
+
+        // TODO: replace with actual next scenes once built
+        if(event.event == SubmenuIndexEmulate) {
+            scene_manager_next_scene(mag->scene_manager, MagSceneEmulate);
+            consumed = true;
+            //} else if(event.event == SubmenuIndexEdit) {
+            //    scene_manager_next_scene(mag->scene_manager, MagSceneUnderConstruction);
+            //    consumed = true;
+        } else if(event.event == SubmenuIndexDelete) {
+            scene_manager_next_scene(mag->scene_manager, MagSceneDeleteConfirm);
+            consumed = true;
+        } else if(event.event == SubmenuIndexInfo) {
+            scene_manager_next_scene(mag->scene_manager, MagSceneSavedInfo);
+            consumed = true;
+        }
+    }
+
+    return consumed;
+}
+
+void mag_scene_saved_menu_on_exit(void* context) {
+    Mag* mag = context;
+
+    submenu_reset(mag->submenu);
+}

+ 71 - 0
magspoof/scenes/mag_scene_start.c

@@ -0,0 +1,71 @@
+#include "../mag_i.h"
+
+typedef enum {
+    SubmenuIndexSaved,
+    SubmenuIndexRead,
+    //SubmenuIndexAddManually,
+    SubmenuIndexAbout,
+} SubmenuIndex;
+
+static void mag_scene_start_submenu_callback(void* context, uint32_t index) {
+    Mag* mag = context;
+
+    view_dispatcher_send_custom_event(mag->view_dispatcher, index);
+}
+
+void mag_scene_start_on_enter(void* context) {
+    Mag* mag = context;
+    Submenu* submenu = mag->submenu;
+
+    submenu_add_item(submenu, "Saved", SubmenuIndexSaved, mag_scene_start_submenu_callback, mag);
+    submenu_add_item(submenu, "Read", SubmenuIndexRead, mag_scene_start_submenu_callback, mag);
+    //submenu_add_item(
+    //    submenu, "Add Manually", SubmenuIndexAddManually, mag_scene_start_submenu_callback, mag);
+    submenu_add_item(submenu, "About", SubmenuIndexAbout, mag_scene_start_submenu_callback, mag);
+
+    submenu_set_selected_item(
+        submenu, scene_manager_get_scene_state(mag->scene_manager, MagSceneStart));
+
+    // clear key
+    furi_string_reset(mag->file_name);
+
+    view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewSubmenu);
+}
+
+bool mag_scene_start_on_event(void* context, SceneManagerEvent event) {
+    Mag* mag = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case SubmenuIndexSaved:
+            furi_string_set(mag->file_path, MAG_APP_FOLDER);
+            scene_manager_next_scene(mag->scene_manager, MagSceneFileSelect);
+            consumed = true;
+            break;
+
+        case SubmenuIndexRead:
+            scene_manager_next_scene(mag->scene_manager, MagSceneRead);
+            consumed = true;
+            break;
+        //case SubmenuIndexAddManually:
+        //    scene_manager_next_scene(mag->scene_manager, MagSceneInputValue);
+        //    consumed = true;
+        //    break;
+        case SubmenuIndexAbout:
+            scene_manager_next_scene(mag->scene_manager, MagSceneAbout);
+            consumed = true;
+            break;
+        }
+
+        scene_manager_set_scene_state(mag->scene_manager, MagSceneStart, event.event);
+    }
+
+    return consumed;
+}
+
+void mag_scene_start_on_exit(void* context) {
+    Mag* mag = context;
+
+    submenu_reset(mag->submenu);
+}

+ 40 - 0
magspoof/scenes/mag_scene_under_construction.c

@@ -0,0 +1,40 @@
+#include "../mag_i.h"
+
+void mag_scene_under_construction_on_enter(void* context) {
+    Mag* mag = context;
+    Widget* widget = mag->widget;
+
+    FuriString* tmp_str;
+    tmp_str = furi_string_alloc();
+
+    widget_add_button_element(widget, GuiButtonTypeLeft, "Back", mag_widget_callback, mag);
+
+    furi_string_printf(tmp_str, "Under construction!");
+    widget_add_string_element(
+        widget, 64, 4, AlignCenter, AlignTop, FontPrimary, furi_string_get_cstr(tmp_str));
+    furi_string_reset(tmp_str);
+
+    view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewWidget);
+    furi_string_free(tmp_str);
+}
+
+bool mag_scene_under_construction_on_event(void* context, SceneManagerEvent event) {
+    Mag* mag = context;
+    SceneManager* scene_manager = mag->scene_manager;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == GuiButtonTypeLeft) {
+            consumed = true;
+
+            scene_manager_previous_scene(scene_manager);
+        }
+    }
+
+    return consumed;
+}
+
+void mag_scene_under_construction_on_exit(void* context) {
+    Mag* mag = context;
+    widget_reset(mag->widget);
+}