Просмотр исходного кода

update magspoof

keep custom text input
MX 1 год назад
Родитель
Сommit
60756801f1

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+dist/*
+.vscode
+.clang-format
+.editorconfig
+.env
+.ufbt

+ 65 - 0
NOTES.md

@@ -0,0 +1,65 @@
+## 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)
+
+## Hummus's Fork
+I made this fork initially to add reading capability using UART magnetic card readers.
+
+Things that changed in this fork:
+- Added a basic card reading ability
+- Added a function to parse a new MagDevice from a Card String (%Track1?;Track2;Track3?;)
+- Swapped the pins between A6 to A7 on the card that I'm using, might add this to configuration scene later on
+- Adapted some of the APIs to the most recent firmware changes.
+

+ 8 - 68
README.md

@@ -1,79 +1,17 @@
-# magspoof_flipper by Zachary Weiss
-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.
+# 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. Sample files are included in `resources`.
 
-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.
+ 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 was made into alternate internal TX options (CC1101, ST25R3916, piezo); 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). For information on the state of internal TX &/or misc TODOs, known bugs, etc, confer `NOTES.md`.
 
-## Hummus's Fork
-I made this fork initially to add reading capability using UART magnetic card readers.
+**Disclaimer**: use responsibly, and at your own risk. ***I neither condone nor am sympathetic to malicious uses of my code.***  Please only use this with magstripe cards and mag readers you own — this is solely meant as a proof of concept for educational purposes. Similarly, if using internal TX: 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.
 
-Things that changed in this fork:
-- Added a basic card reading ability
-- Added a function to parse a new MagDevice from a Card String (%Track1?;Track2;Track3?;)
-- Swapped the pins between A6 to A7 on the card that I'm using, might add this to configuration scene later on
-- Adapted some of the APIs to the most recent firmware changes.
 
-## 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.
+## 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. Custom GPIO pin selection is a planned future feature.
 
 <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.  
@@ -86,4 +24,6 @@ Many thanks to everyone who has helped in addition to those above, most notably:
 - [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 
+- [hummusec](https://github.com/hummusec) for testing of UART RX
+- [xMasterX](https://github.com/xMasterX) and [WillyJL](https://github.com/Willy-JL) for keeping the app updated across API changes while I was away!
 - Everyone else I've had the pleasure of chatting with!

+ 4 - 3
application.fam

@@ -1,5 +1,5 @@
 App(
-    appid="mag",
+    appid="magspoof",
     name="MagSpoof",
     apptype=FlipperAppType.EXTERNAL,
     entry_point="mag_app",
@@ -16,8 +16,9 @@ App(
     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_icon_assets_symbol="mag",
+    fap_version=(0, 7),  # major, minor
+    fap_description="Enables wireless transmission of magstripe data",
     fap_author="Zachary Weiss",
     fap_weburl="https://github.com/zacharyweiss/magspoof_flipper",
 )

+ 61 - 52
helpers/mag_helpers.c

@@ -2,12 +2,6 @@
 
 #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
@@ -19,18 +13,18 @@ const int sublen[] = {32, 48, 48};
 
 uint8_t last_value = 2;
 
-void play_halfbit(bool value, MagSetting* setting) {
-    switch(setting->tx) {
+void play_halfbit(bool value, MagState* state) {
+    switch(state->tx) {
     case MagTxStateRFID:
-        furi_hal_gpio_write(RFID_PIN_OUT, value);
+        furi_hal_gpio_write(&gpio_rfid_carrier_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);
+        furi_hal_gpio_write(mag_state_enum_to_pin(state->pin_input), value);
+        furi_hal_gpio_write(mag_state_enum_to_pin(state->pin_output), !value);
         break;
     case MagTxStatePiezo:
         furi_hal_gpio_write(&gpio_speaker, value);
@@ -41,7 +35,7 @@ void play_halfbit(bool value, MagSetting* setting) {
 
         break;
     case MagTxStateLF_P:
-        furi_hal_gpio_write(RFID_PIN_OUT, value);
+        furi_hal_gpio_write(&gpio_rfid_carrier_out, value);
         furi_hal_gpio_write(&gpio_speaker, value);
 
         /* // Weaker but cleaner signal
@@ -69,7 +63,7 @@ void play_halfbit(bool value, MagSetting* setting) {
 
         if(last_value == 2 || value != (bool)last_value) {
             //furi_hal_nfc_ll_txrx_on();
-            //furi_delay_us(64);
+            furi_delay_us(64);
             //furi_hal_nfc_ll_txrx_off();
         }
         break;
@@ -88,7 +82,7 @@ void play_halfbit(bool value, MagSetting* setting) {
     last_value = value;
 }
 
-void play_track(uint8_t* bits_manchester, uint16_t n_bits, MagSetting* setting, bool reverse) {
+void play_track(uint8_t* bits_manchester, uint16_t n_bits, MagState* state, 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;
@@ -117,9 +111,9 @@ void play_track(uint8_t* bits_manchester, uint16_t n_bits, MagSetting* setting,
         // 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);
+        play_halfbit(bit, state);
+        furi_delay_us(state->us_clock);
+        // if (i % 2 == 1) furi_delay_us(state->us_interpacket);
     }
 }
 
@@ -131,7 +125,7 @@ void tx_init_rfid() {
     // 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
+    // Initializing at GpioSpeedLow seems sufficient for our needs; no improvements seen by increasing speed state
 
     // this doesn't seem to make a difference, leaving it in
     furi_hal_gpio_init(&gpio_rfid_data_in, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow);
@@ -143,14 +137,14 @@ void tx_init_rfid() {
     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_hal_gpio_init(&gpio_rfid_carrier_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_gpio_write(&gpio_rfid_carrier_out, 0);
 
     furi_hal_rfid_pins_reset();
 }
@@ -179,19 +173,31 @@ void tx_deinit_piezo() {
     furi_hal_gpio_init(&gpio_speaker, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
 }
 
-bool tx_init(MagSetting* setting) {
+bool tx_init(MagState* state) {
     // Initialize configured TX method
-    switch(setting->tx) {
+    switch(state->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);
+        furi_hal_gpio_init(
+            mag_state_enum_to_pin(state->pin_input),
+            GpioModeOutputPushPull,
+            GpioPullNo,
+            GpioSpeedLow);
+        furi_hal_gpio_init(
+            mag_state_enum_to_pin(state->pin_output),
+            GpioModeOutputPushPull,
+            GpioPullNo,
+            GpioSpeedLow);
+        furi_hal_gpio_init(
+            mag_state_enum_to_pin(state->pin_enable),
+            GpioModeOutputPushPull,
+            GpioPullNo,
+            GpioSpeedLow);
+
+        furi_hal_gpio_write(mag_state_enum_to_pin(state->pin_enable), 1);
 
         // had some issues with ~300; bumped higher temporarily
         furi_delay_ms(500);
@@ -219,21 +225,24 @@ bool tx_init(MagSetting* setting) {
     return true;
 }
 
-bool tx_deinit(MagSetting* setting) {
+bool tx_deinit(MagState* state) {
     // Reset configured TX method
-    switch(setting->tx) {
+    switch(state->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);
+        furi_hal_gpio_write(mag_state_enum_to_pin(state->pin_input), 0);
+        furi_hal_gpio_write(mag_state_enum_to_pin(state->pin_output), 0);
+        furi_hal_gpio_write(mag_state_enum_to_pin(state->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);
+        furi_hal_gpio_init(
+            mag_state_enum_to_pin(state->pin_input), GpioModeAnalog, GpioPullNo, GpioSpeedLow);
+        furi_hal_gpio_init(
+            mag_state_enum_to_pin(state->pin_output), GpioModeAnalog, GpioPullNo, GpioSpeedLow);
+        furi_hal_gpio_init(
+            mag_state_enum_to_pin(state->pin_enable), GpioModeAnalog, GpioPullNo, GpioSpeedLow);
 
         //gpio_item_configure_all_pins(GpioModeAnalog);
         break;
@@ -262,7 +271,7 @@ bool tx_deinit(MagSetting* setting) {
 }
 
 void mag_spoof(Mag* mag) {
-    MagSetting* setting = mag->setting;
+    MagState* state = &mag->state;
 
     // TODO: cleanup this section. Possibly move precompute + tx_init to emulate_on_enter?
     FuriString* ft1 = mag->mag_dev->dev_data.track[0].str;
@@ -320,47 +329,47 @@ void mag_spoof(Mag* mag) {
     last_value = 2;
     bool bit = false;
 
-    if(!tx_init(setting)) return;
+    if(!tx_init(state)) 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);
+        play_halfbit(bit, state);
+        furi_delay_us(state->us_clock);
     }
 
-    if((setting->track == MagTrackStateOneAndTwo) || (setting->track == MagTrackStateOne))
-        play_track((uint8_t*)bits_t1_manchester, bits_t1_count, setting, false);
+    if((state->track == MagTrackStateOneAndTwo) || (state->track == MagTrackStateOne))
+        play_track((uint8_t*)bits_t1_manchester, bits_t1_count, state, false);
 
-    if((setting->track == MagTrackStateOneAndTwo))
+    if((state->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);
+            play_halfbit(bit, state);
+            furi_delay_us(state->us_clock);
         }
 
-    if((setting->track == MagTrackStateOneAndTwo) || (setting->track == MagTrackStateTwo))
+    if((state->track == MagTrackStateOneAndTwo) || (state->track == MagTrackStateTwo))
         play_track(
             (uint8_t*)bits_t2_manchester,
             bits_t2_count,
-            setting,
-            (setting->reverse == MagReverseStateOn));
+            state,
+            (state->reverse == MagReverseStateOn));
 
-    if((setting->track == MagTrackStateThree))
-        play_track((uint8_t*)bits_t3_manchester, bits_t3_count, setting, false);
+    if((state->track == MagTrackStateThree))
+        play_track((uint8_t*)bits_t3_manchester, bits_t3_count, state, 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);
+        play_halfbit(bit, state);
+        furi_delay_us(state->us_clock);
     }
     FURI_CRITICAL_EXIT();
 
     free(data1);
     free(data2);
     free(data3);
-    tx_deinit(setting);
+    tx_deinit(state);
 }
 
 uint16_t add_bit(bool value, uint8_t* out, uint16_t count) {

+ 4 - 4
helpers/mag_helpers.h

@@ -2,16 +2,16 @@
 #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 play_halfbit(bool value, MagState* state);
+void play_track(uint8_t* bits_manchester, uint16_t n_bits, MagState* state, bool reverse);
 
 void tx_init_rf(int hz);
 void tx_init_rfid();
 void tx_init_piezo();
-bool tx_init(MagSetting* setting);
+bool tx_init(MagState* state);
 void tx_deinit_piezo();
 void tx_deinit_rfid();
-bool tx_deinit(MagSetting* setting);
+bool tx_deinit(MagState* state);
 
 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);

+ 32 - 12
helpers/mag_types.h

@@ -1,20 +1,9 @@
 #pragma once
 
-#define MAG_VERSION_APP "0.05"
+#define MAG_VERSION_APP FAP_VERSION
 #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,
@@ -37,6 +26,37 @@ typedef enum {
     MagTxCC1101_868,
 } MagTxState;
 
+typedef enum {
+    MagPinA7,
+    MagPinA6,
+    MagPinA4,
+    MagPinB3,
+    MagPinB2,
+    MagPinC3,
+    MagPinC1,
+    MagPinC0,
+} MagPin;
+
+#define MAG_STATE_DEFAULT_REVERSE MagReverseStateOff
+#define MAG_STATE_DEFAULT_TRACK MagTrackStateOneAndTwo
+#define MAG_STATE_DEFAULT_TX MagTxStateGPIO
+#define MAG_STATE_DEFAULT_US_CLOCK 240
+#define MAG_STATE_DEFAULT_US_INTERPACKET 10
+#define MAG_STATE_DEFAULT_PIN_INPUT MagPinA7
+#define MAG_STATE_DEFAULT_PIN_OUTPUT MagPinA6
+#define MAG_STATE_DEFAULT_PIN_ENABLE MagPinA4
+#define MAG_STATE_DEFAULT_ALLOW_UART false
+
+typedef enum {
+    MagViewSubmenu,
+    MagViewDialogEx,
+    MagViewPopup,
+    MagViewLoading,
+    MagViewWidget,
+    MagViewVariableItemList,
+    MagViewTextInput,
+    MagViewTextMagInput,
+} MagView;
 
 typedef enum {
     UART_TerminalEventRefreshConsoleOutput = 0,

+ 30 - 51
mag.c

@@ -1,14 +1,7 @@
 #include "mag_i.h"
-#include <expansion/expansion.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;
@@ -21,18 +14,6 @@ static bool mag_debug_back_event_callback(void* 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));
 
@@ -41,6 +22,7 @@ static Mag* mag_alloc() {
 
     mag->file_name = furi_string_alloc();
     mag->file_path = furi_string_alloc_set(MAG_APP_FOLDER);
+    mag->args = furi_string_alloc();
 
     mag->view_dispatcher = view_dispatcher_alloc();
     mag->scene_manager = scene_manager_alloc(&mag_scene_handlers, mag);
@@ -52,7 +34,7 @@ static Mag* mag_alloc() {
         mag->view_dispatcher, mag_debug_back_event_callback);
 
     mag->mag_dev = mag_device_alloc();
-    mag->setting = mag_setting_alloc();
+    mag_state_load(&mag->state);
 
     // Open GUI record
     mag->gui = furi_record_open(RECORD_GUI);
@@ -64,11 +46,6 @@ static Mag* mag_alloc() {
     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));
@@ -96,15 +73,15 @@ static Mag* mag_alloc() {
     // 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));
+        mag->view_dispatcher, MagViewTextMagInput, mag_text_input_get_view(mag->mag_text_input));
 
-    return mag;
-}
+    // Disable expansion protocol to avoid interference with UART Handle
+    mag->expansion = furi_record_open(RECORD_EXPANSION);
+    expansion_disable(mag->expansion);
 
-static void mag_setting_free(MagSetting* setting) {
-    furi_assert(setting);
+    // Move UART here? conditional upon setting?
 
-    free(setting);
+    return mag;
 }
 
 static void mag_free(Mag* mag) {
@@ -112,23 +89,16 @@ static void mag_free(Mag* mag) {
 
     furi_string_free(mag->file_name);
     furi_string_free(mag->file_path);
+    furi_string_free(mag->args);
 
     // 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);
@@ -149,8 +119,7 @@ static void mag_free(Mag* mag) {
     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);
+    view_dispatcher_remove_view(mag->view_dispatcher, MagViewTextMagInput);
     mag_text_input_free(mag->mag_text_input);
 
     // View Dispatcher
@@ -167,6 +136,10 @@ static void mag_free(Mag* mag) {
     furi_record_close(RECORD_NOTIFICATION);
     mag->notifications = NULL;
 
+    // Return previous state of expansion
+    expansion_enable(mag->expansion);
+    furi_record_close(RECORD_EXPANSION);
+
     furi_record_close(RECORD_STORAGE);
     furi_record_close(RECORD_DIALOGS);
 
@@ -175,14 +148,14 @@ static void mag_free(Mag* mag) {
 
 // entry point for app
 int32_t mag_app(void* p) {
-    UNUSED(p);
-    
-    // Disable expansion protocol to avoid interference with UART Handle
-    Expansion* expansion = furi_record_open(RECORD_EXPANSION);
-    expansion_disable(expansion);
+    const char* args = p;
 
     Mag* mag = mag_alloc();
 
+    if(args && strlen(args)) {
+        furi_string_set(mag->args, args);
+    }
+
     mag_make_app_folder(mag);
 
     // Enable 5v power, multiple attempts to avoid issues with power chip protection false triggering
@@ -194,7 +167,17 @@ int32_t mag_app(void* p) {
     }
 
     view_dispatcher_attach_to_gui(mag->view_dispatcher, mag->gui, ViewDispatcherTypeFullscreen);
-    scene_manager_next_scene(mag->scene_manager, MagSceneStart);
+
+    if(furi_string_empty(mag->args)) {
+        scene_manager_next_scene(mag->scene_manager, MagSceneStart);
+    } else {
+        mag_device_load_data(mag->mag_dev, mag->args, true);
+        MagTrackState auto_track = mag_device_autoselect_track_state(mag->mag_dev);
+        if(auto_track) {
+            mag->state.track = auto_track;
+        }
+        scene_manager_next_scene(mag->scene_manager, MagSceneEmulate);
+    }
 
     view_dispatcher_run(mag->view_dispatcher);
 
@@ -205,10 +188,6 @@ int32_t mag_app(void* p) {
 
     mag_free(mag);
 
-    // Return previous state of expansion
-    expansion_enable(expansion);
-    furi_record_close(RECORD_EXPANSION);
-
     return 0;
 }
 

+ 31 - 10
mag_device.c

@@ -118,9 +118,14 @@ bool mag_device_save(MagDevice* mag_dev, const char* dev_name) {
     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 mag_device_load_data(MagDevice* mag_dev, FuriString* path, bool show_dialog) {
     bool parsed = false;
 
+    FuriString* filename;
+    filename = furi_string_alloc();
+    path_extract_filename(path, filename, true);
+    strncpy(mag_dev->dev_name, furi_string_get_cstr(filename), MAG_DEV_NAME_MAX_LEN);
+
     FlipperFormat* file = flipper_format_file_alloc(mag_dev->storage);
     FuriString* temp_str;
     temp_str = furi_string_alloc();
@@ -168,6 +173,7 @@ static bool mag_device_load_data(MagDevice* mag_dev, FuriString* path, bool show
     }
 
     furi_string_free(temp_str);
+    furi_string_free(filename);
     flipper_format_free(file);
 
     return parsed;
@@ -189,15 +195,10 @@ bool mag_file_select(MagDevice* mag_dev) {
 
     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;
@@ -251,7 +252,7 @@ bool mag_device_parse_card_string(MagDevice* mag_dev, FuriString* f_card_str) {
         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;
@@ -259,7 +260,7 @@ bool mag_device_parse_card_string(MagDevice* mag_dev, FuriString* f_card_str) {
 
     // Track 2
     const char* track2_start = strchr(track1_end, ';');
-    if (!track2_start) {
+    if(!track2_start) {
         FURI_LOG_D(TAG, "Could not find track 2 start");
         return true;
     }
@@ -279,7 +280,7 @@ bool mag_device_parse_card_string(MagDevice* mag_dev, FuriString* f_card_str) {
 
     // Track 3
     const char* track3_start = strchr(track2_end, ';');
-    if (!track3_start) {
+    if(!track3_start) {
         FURI_LOG_D(TAG, "Could not find track 3 start");
         return true;
     }
@@ -296,10 +297,30 @@ bool mag_device_parse_card_string(MagDevice* mag_dev, FuriString* f_card_str) {
 
     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;
 }
 
+MagTrackState mag_device_autoselect_track_state(MagDevice* mag_dev) {
+    // messy code to quickly check which tracks are available for emulation/display
+    bool is_empty_t1 = furi_string_empty(mag_dev->dev_data.track[0].str);
+    bool is_empty_t2 = furi_string_empty(mag_dev->dev_data.track[1].str);
+    bool is_empty_t3 = furi_string_empty(mag_dev->dev_data.track[2].str);
+
+    if(!is_empty_t1 && !is_empty_t2) {
+        return MagTrackStateOneAndTwo;
+    } else if(!is_empty_t1) {
+        return MagTrackStateOne;
+    } else if(!is_empty_t2) {
+        return MagTrackStateTwo;
+    } else if(!is_empty_t3) {
+        return MagTrackStateThree;
+    }
+
+    // if all empty (or something wrong with the above code)
+    // return default value
+    return MAG_STATE_DEFAULT_TRACK;
+}
 
 void mag_device_set_loading_callback(
     MagDevice* mag_dev,

+ 7 - 1
mag_device.h

@@ -6,11 +6,13 @@
 #include <dialogs/dialogs.h>
 
 #include "mag_icons.h"
+#include "helpers/mag_types.h"
+
 
 #define MAG_DEV_NAME_MAX_LEN 22
 #define MAG_DEV_TRACKS 3
 
-#define MAG_APP_FOLDER ANY_PATH("mag")
+#define MAG_APP_FOLDER STORAGE_APP_DATA_PATH_PREFIX
 #define MAG_APP_EXTENSION ".mag"
 
 typedef void (*MagLoadingCallback)(void* context, bool state);
@@ -42,6 +44,8 @@ void mag_device_set_name(MagDevice* mag_dev, const char* name);
 
 bool mag_device_save(MagDevice* mag_dev, const char* dev_name);
 
+bool mag_device_load_data(MagDevice* mag_dev, FuriString* path, bool show_dialog);
+
 bool mag_file_select(MagDevice* mag_dev);
 
 void mag_device_data_clear(MagDeviceData* dev_data);
@@ -52,6 +56,8 @@ bool mag_device_delete(MagDevice* mag_dev, bool use_load_path);
 
 bool mag_device_parse_card_string(MagDevice* mag_dev, FuriString* card_str);
 
+MagTrackState mag_device_autoselect_track_state(MagDevice* mag_dev);
+
 void mag_device_set_loading_callback(
     MagDevice* mag_dev,
     MagLoadingCallback callback,

+ 16 - 14
mag_i.h

@@ -1,6 +1,7 @@
 #pragma once
 
 #include "mag_device.h"
+#include "mag_state.h"
 //#include "helpers/mag_helpers.h"
 #include "helpers/mag_text_input.h"
 #include "helpers/mag_types.h"
@@ -10,6 +11,7 @@
 #include <furi/core/log.h>
 #include <furi_hal_gpio.h>
 #include <furi_hal_resources.h>
+#include <expansion/expansion.h>
 
 #include <gui/gui.h>
 #include <gui/view.h>
@@ -37,21 +39,22 @@
 
 #define MAG_TEXT_STORE_SIZE 150
 
+// CFWs have `submenue_add_lockable_item`; OFW doesn't,
+// replace with conditional submenu item
+#ifdef FW_ORIGIN_Official
+#define submenu_add_lockable_item(                                             \
+    submenu, label, index, callback, callback_context, locked, locked_message) \
+    if(!locked) {                                                              \
+        submenu_add_item(submenu, label, index, callback, callback_context)    \
+    }
+#endif
+
 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;
@@ -64,32 +67,31 @@ typedef struct {
     char text_store[MAG_TEXT_STORE_SIZE + 1];
     FuriString* file_path;
     FuriString* file_name;
+    FuriString* args;
 
-    MagSetting* setting;
+    MagState state;
 
     // 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
+    Expansion* expansion;
     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);
     FuriHalSerialHandle* serial_handle;
-    
+
     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, ...);

+ 116 - 0
mag_state.c

@@ -0,0 +1,116 @@
+#include "mag_state.h"
+
+#define TAG "MagState"
+
+const GpioPin* mag_state_enum_to_pin(MagPin pin) {
+    switch(pin) {
+    case MagPinA7:
+        return &gpio_ext_pa7;
+    case MagPinA6:
+        return &gpio_ext_pa6;
+    case MagPinA4:
+        return &gpio_ext_pa4;
+    case MagPinB3:
+        return &gpio_ext_pb3;
+    case MagPinB2:
+        return &gpio_ext_pb2;
+    case MagPinC3:
+        return &gpio_ext_pc3;
+    case MagPinC1:
+        return &gpio_ext_pc1;
+    case MagPinC0:
+        return &gpio_ext_pc0;
+    default:
+        return NULL;
+    }
+}
+
+bool mag_state_gpio_is_valid(MagState* state) {
+    return (state->pin_input != state->pin_output) && (state->pin_input != state->pin_enable) &&
+           (state->pin_enable != state->pin_output);
+}
+
+void mag_state_gpio_reset(MagState* state) {
+    state->pin_input = MAG_STATE_DEFAULT_PIN_INPUT;
+    state->pin_output = MAG_STATE_DEFAULT_PIN_OUTPUT;
+    state->pin_enable = MAG_STATE_DEFAULT_PIN_ENABLE;
+}
+
+bool mag_state_load(MagState* out_state) {
+    MagState state;
+
+    // Try to load from file
+    bool loaded_from_file = false;
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    if(storage_file_exists(storage, MAG_STATE_PATH)) {
+        FlipperFormat* file = flipper_format_file_alloc(storage);
+        do {
+            uint32_t tmp;
+            FuriString* str = furi_string_alloc();
+            if(!flipper_format_file_open_existing(file, MAG_STATE_PATH)) break;
+            if(!flipper_format_read_header(file, str, &tmp)) break;
+            if(furi_string_cmp_str(str, MAG_STATE_HEADER)) break;
+            if(tmp != MAG_STATE_VER) break;
+
+            if(!flipper_format_read_uint32(file, "pin_input", &tmp, 1)) break;
+            state.pin_input = tmp;
+            if(!flipper_format_read_uint32(file, "pin_output", &tmp, 1)) break;
+            state.pin_output = tmp;
+            if(!flipper_format_read_uint32(file, "pin_enable", &tmp, 1)) break;
+            state.pin_enable = tmp;
+            if(!flipper_format_read_bool(file, "allow_uart", &state.allow_uart, 1)) break;
+
+            loaded_from_file = true;
+        } while(0);
+        flipper_format_free(file);
+    }
+    furi_record_close(RECORD_STORAGE);
+
+    // If could not be read from file
+    // Or file GPIO config is invalid (pins overlap)
+    // Set defaults
+    // Additionally raise message to user?
+    if(!loaded_from_file || !mag_state_gpio_is_valid(&state)) {
+        mag_state_gpio_reset(&state);
+    }
+
+    if(!loaded_from_file) {
+        state.allow_uart = MAG_STATE_DEFAULT_ALLOW_UART;
+    }
+
+    // set defaults we don't save
+    state.tx = MAG_STATE_DEFAULT_TX;
+    state.track = MAG_STATE_DEFAULT_TRACK;
+    state.reverse = MAG_STATE_DEFAULT_REVERSE;
+    state.us_clock = MAG_STATE_DEFAULT_US_CLOCK;
+    state.us_interpacket = MAG_STATE_DEFAULT_US_INTERPACKET;
+    state.is_debug = furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug);
+
+    // Copy to caller state before popping stack
+    memcpy(out_state, &state, sizeof(state));
+
+    return loaded_from_file;
+}
+
+void mag_state_save(MagState* state) {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    storage_simply_mkdir(storage, MAG_STATE_DIR);
+    FlipperFormat* file = flipper_format_file_alloc(storage);
+
+    do {
+        uint32_t tmp;
+        if(!flipper_format_file_open_always(file, MAG_STATE_PATH)) break;
+        if(!flipper_format_write_header_cstr(file, MAG_STATE_HEADER, MAG_STATE_VER)) break;
+
+        tmp = state->pin_input;
+        if(!flipper_format_write_uint32(file, "pin_input", &tmp, 1)) break;
+        tmp = state->pin_output;
+        if(!flipper_format_write_uint32(file, "pin_output", &tmp, 1)) break;
+        tmp = state->pin_enable;
+        if(!flipper_format_write_uint32(file, "pin_enable", &tmp, 1)) break;
+        if(!flipper_format_write_bool(file, "allow_uart", &state->allow_uart, 1)) break;
+
+    } while(0);
+    flipper_format_free(file);
+    furi_record_close(RECORD_STORAGE);
+}

+ 42 - 0
mag_state.h

@@ -0,0 +1,42 @@
+#pragma once
+
+#include <string.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+#include <furi_hal_resources.h>
+#include <furi_hal_rtc.h>
+
+#include <storage/storage.h>
+#include <flipper_format/flipper_format.h>
+
+#include "helpers/mag_types.h"
+
+#define MAG_STATE_HEADER "Mag State"
+#define MAG_STATE_VER 1
+#define MAG_STATE_DIR STORAGE_APP_DATA_PATH_PREFIX
+#define MAG_STATE_PATH MAG_STATE_DIR "/mag_state.txt"
+
+typedef struct {
+    MagTxState tx;
+    MagTrackState track;
+    MagReverseState reverse;
+    uint32_t us_clock;
+    uint32_t us_interpacket;
+    MagPin pin_input;
+    MagPin pin_output;
+    MagPin pin_enable;
+    bool allow_uart;
+    bool is_debug;
+} MagState;
+
+const GpioPin* mag_state_enum_to_pin(MagPin pin);
+
+bool mag_state_gpio_is_valid(MagState* state);
+
+void mag_state_gpio_reset(MagState* state);
+
+bool mag_state_load(MagState* out_state);
+
+void mag_state_save(MagState* state);

+ 3 - 2
scenes/mag_scene_config.h

@@ -1,5 +1,7 @@
 ADD_SCENE(mag, start, Start)
 ADD_SCENE(mag, about, About)
+ADD_SCENE(mag, read, Read)
+ADD_SCENE(mag, settings, Settings)
 ADD_SCENE(mag, emulate, Emulate)
 ADD_SCENE(mag, emulate_config, EmulateConfig)
 ADD_SCENE(mag, file_select, FileSelect)
@@ -11,5 +13,4 @@ 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)
+ADD_SCENE(mag, under_construction, UnderConstruction)

+ 2 - 2
scenes/mag_scene_emulate.c

@@ -30,7 +30,7 @@ void mag_scene_emulate_on_enter(void* context) {
         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);
+    FURI_LOG_D(TAG, "%d", mag->state.reverse);
 
     // print relevant data
     uint8_t cat_count = 0;
@@ -39,7 +39,7 @@ void mag_scene_emulate_on_enter(void* context) {
 
         // still messy / dumb way to do this, but slightly cleaner than before.
         // will clean up more later
-        switch(mag->setting->track) {
+        switch(mag->state.track) {
         case MagTrackStateOne:
             if(i == 0) cat_trackstr(tmp_str, cat_count++, i, trackstr);
             break;

+ 35 - 28
scenes/mag_scene_emulate_config.c

@@ -2,12 +2,12 @@
 
 #define TAG "MagSceneEmulateConfig"
 
-enum MagSettingIndex {
-    MagSettingIndexTx,
-    MagSettingIndexTrack,
-    MagSettingIndexReverse,
-    MagSettingIndexClock,
-    MagSettingIndexInterpacket,
+enum MagEmulateConfigIndex {
+    MagEmulateConfigIndexTx,
+    MagEmulateConfigIndexTrack,
+    MagEmulateConfigIndexReverse,
+    MagEmulateConfigIndexClock,
+    MagEmulateConfigIndexInterpacket,
 };
 
 #define TX_COUNT 7
@@ -128,17 +128,17 @@ static void mag_scene_emulate_config_set_tx(VariableItem* item) {
 
     variable_item_set_current_value_text(item, tx_text[index]);
 
-    mag->setting->tx = tx_value[index];
+    mag->state.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) {
+    if(mag->state.reverse == MagReverseStateOff) {
         variable_item_set_current_value_text(item, track_text[index]);
-        mag->setting->track = track_value[index];
-    } else if(mag->setting->reverse == MagReverseStateOn) {
+        mag->state.track = track_value[index];
+    } else if(mag->state.reverse == MagReverseStateOn) {
         variable_item_set_current_value_index(
             item, value_index_uint32(MagTrackStateOneAndTwo, track_value, TRACK_COUNT));
     }
@@ -151,10 +151,10 @@ 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) {
+    if(mag->state.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];
+        mag->state.reverse = reverse_value[index];
         //FURI_LOG_D(TAG, "%s", reverse_text[index]);
         //FURI_LOG_D(TAG, "%d", mag->setting->reverse);
     } else {
@@ -169,7 +169,7 @@ static void mag_scene_emulate_config_set_clock(VariableItem* item) {
 
     variable_item_set_current_value_text(item, clock_text[index]);
 
-    mag->setting->us_clock = clock_value[index];
+    mag->state.us_clock = clock_value[index];
 };
 
 static void mag_scene_emulate_config_set_interpacket(VariableItem* item) {
@@ -178,7 +178,7 @@ static void mag_scene_emulate_config_set_interpacket(VariableItem* item) {
 
     variable_item_set_current_value_text(item, interpacket_text[index]);
 
-    mag->setting->us_interpacket = interpacket_value[index];
+    mag->state.us_interpacket = interpacket_value[index];
 };
 
 void mag_scene_emulate_config_on_enter(void* context) {
@@ -188,18 +188,18 @@ void mag_scene_emulate_config_on_enter(void* context) {
     VariableItem* item;
     uint8_t value_index;
 
-    // TX
+    // Clock
     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);
+        mag->variable_item_list, "Clock:", CLOCK_COUNT, mag_scene_emulate_config_set_clock, mag);
+    value_index = value_index_uint32(mag->state.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, tx_text[value_index]);
+    variable_item_set_current_value_text(item, clock_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);
+    value_index = value_index_uint32(mag->state.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]);
@@ -212,19 +212,26 @@ void mag_scene_emulate_config_on_enter(void* context) {
         REVERSE_COUNT,
         mag_scene_emulate_config_set_reverse,
         mag);
-    value_index = value_index_uint32(mag->setting->reverse, reverse_value, REVERSE_COUNT);
+    value_index = value_index_uint32(mag->state.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]);
-
+    // TX
+#ifdef FW_ORIGIN_Official
+    if(mag->state.is_debug) {
+#endif
+        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->state.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]);
+#ifdef FW_ORIGIN_Official
+    }
+#else
+    variable_item_set_locked(item, !mag->state.is_debug, "Enable Debug!");
+#endif
     // Interpacket
     /*
     item = variable_item_list_add(

+ 4 - 1
scenes/mag_scene_file_select.c

@@ -3,9 +3,12 @@
 
 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)) {
+        MagTrackState auto_track = mag_device_autoselect_track_state(mag->mag_dev);
+        if(auto_track) {
+            mag->state.track = auto_track;
+        }
         scene_manager_next_scene(mag->scene_manager, MagSceneSavedMenu);
     } else {
         scene_manager_search_and_switch_to_previous_scene(mag->scene_manager, MagSceneStart);

+ 1 - 1
scenes/mag_scene_input_value.c

@@ -11,7 +11,7 @@ void mag_scene_input_value_on_enter(void* context) {
     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);
+    view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewTextMagInput);
 }
 
 bool mag_scene_input_value_on_event(void* context, SceneManagerEvent event) {

+ 5 - 5
scenes/mag_scene_saved_menu.c

@@ -24,17 +24,17 @@ void mag_scene_saved_menu_on_enter(void* context) {
     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;
+        mag->state.track = MagTrackStateOneAndTwo;
     } else if(!is_empty_t1) {
-        mag->setting->track = MagTrackStateOne;
+        mag->state.track = MagTrackStateOne;
     } else if(!is_empty_t2) {
-        mag->setting->track = MagTrackStateTwo;
+        mag->state.track = MagTrackStateTwo;
     } else if(!is_empty_t3) {
-        mag->setting->track = MagTrackStateThree;
+        mag->state.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, "Emulate", 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(

+ 177 - 0
scenes/mag_scene_settings.c

@@ -0,0 +1,177 @@
+#include "../mag_i.h"
+#include "../mag_state.h"
+#include "../helpers/mag_helpers.h"
+
+#define TAG "MagSceneEmulateConfig"
+
+enum VarItemListIndex {
+    VarItemListIndexPinInput,
+    VarItemListIndexPinOutput,
+    VarItemListIndexPinEnable,
+    VarItemListIndexAllowUART,
+};
+
+static const char* gpio[] = {
+    [MagPinA7] = "2 (A7)",
+    [MagPinA6] = "3 (A6)",
+    [MagPinA4] = "4 (A4)",
+    [MagPinB3] = "5 (B3)",
+    [MagPinB2] = "6 (B2)",
+    [MagPinC3] = "7 (C3)",
+    [MagPinC1] = "15 (C1)",
+    [MagPinC0] = "16 (C0)",
+};
+const uint8_t GPIO_COUNT = COUNT_OF(gpio);
+// static const char* uart_pins[] = {[DapUartTypeUSART1] = "13,14", [DapUartTypeLPUART1] = "15,16"};
+// static const char* uart_swap[] = {[DapUartTXRXNormal] = "No", [DapUartTXRXSwap] = "Yes"};
+
+void mag_scene_settings_var_item_list_callback(void* context, uint32_t index) {
+    Mag* mag = context;
+    view_dispatcher_send_custom_event(mag->view_dispatcher, index);
+}
+
+static void mag_scene_settings_set_gpio(VariableItem* item, MagPin* pin_out) {
+    MagPin pin = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, gpio[pin]);
+    *pin_out = pin;
+}
+
+static void mag_scene_settings_set_gpio_input(VariableItem* item) {
+    Mag* mag = variable_item_get_context(item);
+    mag_scene_settings_set_gpio(item, &mag->state.pin_input);
+};
+
+static void mag_scene_settings_set_gpio_output(VariableItem* item) {
+    Mag* mag = variable_item_get_context(item);
+    mag_scene_settings_set_gpio(item, &mag->state.pin_output);
+};
+
+static void mag_scene_settings_set_gpio_enable(VariableItem* item) {
+    Mag* mag = variable_item_get_context(item);
+    mag_scene_settings_set_gpio(item, &mag->state.pin_enable);
+};
+
+static void mag_pin_variable_item_list_add(
+    Mag* mag,
+    const char* label,
+    MagPin pin,
+    VariableItemChangeCallback change_callback) {
+    VariableItem* item =
+        variable_item_list_add(mag->variable_item_list, label, GPIO_COUNT, change_callback, mag);
+    variable_item_set_current_value_index(item, pin);
+    variable_item_set_current_value_text(item, gpio[pin]);
+}
+
+void mag_scene_settings_on_enter(void* context) {
+    Mag* mag = context;
+    VariableItem* item;
+    VariableItemList* var_item_list = mag->variable_item_list;
+
+    mag_pin_variable_item_list_add(
+        mag, "Input pin:", mag->state.pin_input, mag_scene_settings_set_gpio_input);
+    mag_pin_variable_item_list_add(
+        mag, "Output pin:", mag->state.pin_output, mag_scene_settings_set_gpio_output);
+    mag_pin_variable_item_list_add(
+        mag, "Enable pin:", mag->state.pin_enable, mag_scene_settings_set_gpio_enable);
+
+    item = variable_item_list_add(var_item_list, "UART MSR: ", 1, NULL, mag);
+    variable_item_set_current_value_text(item, mag->state.allow_uart ? "ON" : "OFF");
+
+    variable_item_list_set_enter_callback(
+        var_item_list, mag_scene_settings_var_item_list_callback, mag);
+
+    variable_item_list_set_selected_item(
+        var_item_list, scene_manager_get_scene_state(mag->scene_manager, MagSceneSettings));
+
+    view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewVariableItemList);
+}
+
+void mag_scene_settings_dialog_invalid_pins(Mag* mag) {
+    SceneManager* scene_manager = mag->scene_manager;
+
+    DialogMessage* message = dialog_message_alloc();
+
+    dialog_message_set_header(message, "Invalid Pin Config!", 64, 0, AlignCenter, AlignTop);
+    dialog_message_set_buttons(message, "Modify", NULL, "Reset");
+    dialog_message_set_text(
+        message,
+        "Pins cannot overlap.\nChange, or reset to defaults.",
+        64,
+        32,
+        AlignCenter,
+        AlignCenter);
+    DialogMessageButton res = dialog_message_show(furi_record_open(RECORD_DIALOGS), message);
+    dialog_message_free(message);
+    furi_record_close(RECORD_DIALOGS);
+    if(res == DialogMessageButtonRight) {
+        mag_state_gpio_reset(&mag->state);
+        scene_manager_previous_scene(scene_manager);
+    }
+}
+
+void mag_scene_settings_dialog_allow_uart(Mag* mag) {
+    bool change = mag->state.allow_uart;
+    if(!change) {
+        DialogMessage* msg = dialog_message_alloc();
+        dialog_message_set_header(msg, "UART MSR", 64, 0, AlignCenter, AlignTop);
+        dialog_message_set_buttons(msg, "No", NULL, "Yes");
+        dialog_message_set_text(
+            msg,
+            "This option requires a\nUART-compatible mag reader.\nIs it installed?\n",
+            64,
+            32,
+            AlignCenter,
+            AlignCenter);
+        DialogMessageButton res = dialog_message_show(furi_record_open(RECORD_DIALOGS), msg);
+        if(res == DialogMessageButtonRight) {
+            change = true;
+        }
+        dialog_message_free(msg);
+        furi_record_close(RECORD_DIALOGS);
+    }
+    if(change) {
+        mag->state.allow_uart = !mag->state.allow_uart;
+        variable_item_set_current_value_text(
+            variable_item_list_get(mag->variable_item_list, VarItemListIndexAllowUART),
+            mag->state.allow_uart ? "ON" : "OFF");
+    }
+}
+
+bool mag_scene_settings_on_event(void* context, SceneManagerEvent event) {
+    Mag* mag = context;
+    SceneManager* scene_manager = mag->scene_manager;
+    bool consumed = false;
+
+    switch(event.type) {
+    case SceneManagerEventTypeBack:
+        // when attempting to exit, validate pin configuration
+        // if invalid, prompt
+        consumed = true;
+
+        if(!mag_state_gpio_is_valid(&mag->state)) {
+            mag_scene_settings_dialog_invalid_pins(mag);
+        } else {
+            scene_manager_previous_scene(scene_manager);
+        }
+        break;
+    case SceneManagerEventTypeCustom:
+        scene_manager_set_scene_state(mag->scene_manager, MagSceneSettings, event.event);
+        consumed = true;
+        if(event.event == VarItemListIndexAllowUART) {
+            mag_scene_settings_dialog_allow_uart(mag);
+        }
+        break;
+    default:
+        break;
+    }
+
+    return consumed;
+}
+
+void mag_scene_settings_on_exit(void* context) {
+    Mag* mag = context;
+
+    variable_item_list_reset(mag->variable_item_list);
+
+    mag_state_save(&mag->state);
+}

+ 16 - 1
scenes/mag_scene_start.c

@@ -4,6 +4,7 @@ typedef enum {
     SubmenuIndexSaved,
     SubmenuIndexRead,
     //SubmenuIndexAddManually,
+    SubmenuIndexSettings,
     SubmenuIndexAbout,
 } SubmenuIndex;
 
@@ -18,9 +19,19 @@ void mag_scene_start_on_enter(void* 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_lockable_item(
+        submenu,
+        "Read",
+        SubmenuIndexRead,
+        mag_scene_start_submenu_callback,
+        mag,
+        (!mag->state.is_debug && !mag->state.allow_uart),
+        "Enable Debug!");
     //submenu_add_item(
     //    submenu, "Add Manually", SubmenuIndexAddManually, mag_scene_start_submenu_callback, mag);
+    submenu_add_item(
+        submenu, "Settings", SubmenuIndexSettings, mag_scene_start_submenu_callback, mag);
+
     submenu_add_item(submenu, "About", SubmenuIndexAbout, mag_scene_start_submenu_callback, mag);
 
     submenu_set_selected_item(
@@ -52,6 +63,10 @@ bool mag_scene_start_on_event(void* context, SceneManagerEvent event) {
         //    scene_manager_next_scene(mag->scene_manager, MagSceneInputValue);
         //    consumed = true;
         //    break;
+        case SubmenuIndexSettings:
+            scene_manager_next_scene(mag->scene_manager, MagSceneSettings);
+            consumed = true;
+            break;
         case SubmenuIndexAbout:
             scene_manager_next_scene(mag->scene_manager, MagSceneAbout);
             consumed = true;