diff --git a/README.md b/README.md index 40ddb0992fa..67a12d27a59 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,44 @@ # magspoof_flipper -Very early WIP of MagSpoof for the Flipper Zero. Currently rewriting from the ground up. +WIP of MagSpoof for the Flipper Zero. Currently rewriting from the ground up. Interpolates work from Samy Kamkar's original MagSpoof project, dunaevai135's Flipper hackathon project, and the Flipper team's LF RFID app. -Interpolates work from Samy Kamkar's original MagSpoof project, dunaevai135's Flipper hackathon project, and the Flipper team's LF RFID app. +Many thanks to everyone who has helped in addition to those above, most notably: antirez for bitmapping suggestions, skotopes for RFID consultation, NVX + dlz for NFC consulation, davethepirate for EE insight and being a sounding board, and cool4uma for their work on custom text_input scenes — as well as everyone else I've had the pleasure of chatting with. -Courses of action to try in the event the LF coil signal is too weak: -- Attempt downstream modulation techniques, in addition to upstream, like the LF RFID worker does when writing -- Introduce a subcarrier at ~125kHz, and OOK modulate it at the desired freq of bits (~4kHz) +Using this README as coarse notes of what remains to be done; anyone is welcome to contribute! + +## TODO +Emulation: +- Finish refactor from hardcoded test scene to mag_helpers (most notable change: precomputing bit output akin to devBioS's "RedSpoof" implementation of MagSpoof) +- Multi-track emulation, reverse track emulation +- Experimentation on timing and other parameters (zero prefix/between/suffix, interpacket delay, reverse vs non-reverse track, etc) +- Implement/integrate better bitmap than hacky first pass? antirez's better approach (from ProtoView) included at bottom of mag_helpers +- External TX option(s) — interface with original H-bridge design, also perhaps singular coil. Does GPIO have sufficient output for this? Need a capacitor to discharge from? +- Pursue skunkworks TX improvement ideas listed below + +Scenes: +- Non-hardcoded emulation scene (using mag_helpers functions) that play loaded card data +- Emulation config scene. Be able to select between RFID / GPIO H-bridge / GPIO plain coil(?), modify timing (clock and interpacket), select track(s) to be emulated, toggle reverse track (?) +- Improved saved info display (better text wrapping options? remove and just include that info on the emulate scene? decode data to fields?) +- Edit saved card scene + +File management: +- What is best way to save track data, and designate which tracks are in a file? Just use end sentinels to determine when loaded, or split it out into different fields? +- Parsing loaded files into relevent fields (would we need to specify card type as well, to decode correctly?) +- Modify manual add scene to allow editing and renaming of existing files +- Validation of card track data? +- Better cleanup / management of data during add manually + +Known bugs: +- Currently there's a few functions that are unused, while the refactor is in progress. To avoid compilation errors relating to the unused functions, one must comment out `-Werror` in `site_scons/cc.scons` (or comment out the unused functions, the former is just easier/faster). +- Custom text input scene with expanded characterset (Add Manually) has odd behavior when navigating the keys near the numpad +- Track 1 data typically starts with a `%` sign. Unless escaped, it won't be displayed when printed, as C considers it a special character. To confirm: how does this impact the emulation when iterating through the chars? Does it get played correctly? + +## Skunkworks ideas +Internal TX improvements: +- Attempt downstream modulation techniques, in addition to upstream, like the LF RFID worker does when writing, for stronger signal - Implement using the timer system, rather than direct-writing to pins -- Use the NFC (HF RFID) coil instead of or in addition to the LF coil (this is promising in my mind; Samsung Wallet's discontinued magstripe emulation would've been over their NFC coil, most likely) -- Scrap all this and stick to using an external module for TX (could likely simplify to just a resistor and some coiled wire, rather than the full H-bridge build) - -Other misc things to investigate / build: -- File format, manual add, saving / loading -- Ideal timing / speed -- Precomputing bit output, and then sending ("RedSpoof" by devBioS does this, as they say they had timing issues when computing the bits live) -- Reverse-track emulate? -- Tuning of parameters like pre-signal zeros? -- "Interpacket delay" like the RedSpoof implementation? -- (Less important) Any way to easily wrap text on screen, without having to manually calculate the number of chars that fit and splicing the string accordingly into lines? - - -HF coil notes: -~~NFC reader field can be turned on / off with `furi_hal_nfc_field_on();` and `furi_hal_nfc_field_off();` respectively, as seen in nfc_scene_field.c (used for debug purposes). Initial tests with `furi_hal_nfc_field_on();` are promising signal-wise, but the delay introduced by the wake/sleep initialization renders it impossible to toggle rapidly. At a lower level, that consists of `furi_hal_nfc_exit_sleep();` and `st25r3916TxRxOn();` to turn on, and `st25r3916TxRxOff();` and `furi_hal_nfc_start_sleep();` to turn off. May be worth trying directly (wake from sleep at setup, toggle on and off corresponding with bit direction, send to sleep on exit). Initial tests have been difficult to get work as some of the st25r3916 symbols are unresolved; need to figure out how to import/call it properly, or how to get another layer lower of control.~~ -Testing with `furi_hal_nfc_ll_txrx_on();` and `furi_hal_nfc_ll_txrx_off();` does indeed create a nice strong signal on my 'scope (thanks @dlz#7721 for finding the wrapped functions), but no response from a mag reader; makes sense, was a long shot -- next step NFC testing would be lower-level control that lets us pull the coil high/low, rather than just producing the standard 13.56MHz signal and OOK modulating it. +- 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) +External RX options (What is simplest read module?): +- Some UART mag reader (bulky, but likely easiest to read over GPIO, and means one can read all tracks) +- Square audio jack mag reader (compact, but will be harder to decode from GPIO. Also only will read track 2 without modification) +- USB HID input feasible? Flipper seemingly can't act as an HID host, is there any way to circumvent this or is it due to a hardware incompatibility? This would be the easiest / best option all-around if feasible. diff --git a/helpers/mag_helpers.c b/helpers/mag_helpers.c new file mode 100644 index 00000000000..5f417756070 --- /dev/null +++ b/helpers/mag_helpers.c @@ -0,0 +1,241 @@ +#include "mag_helpers.h" + +#define GPIO_PIN_A &gpio_ext_pa6 +#define GPIO_PIN_B &gpio_ext_pa7 +#define RFID_PIN &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 +#define US_CLOCK 240 +#define US_INTERPACKET 10 + +// 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 bit_dir = 0; + +void play_bit_rfid(uint8_t send_bit) { + // internal TX over RFID coil + bit_dir ^= 1; + furi_hal_gpio_write(RFID_PIN, bit_dir); + furi_delay_us(US_CLOCK); + + if(send_bit) { + bit_dir ^= 1; + furi_hal_gpio_write(RFID_PIN, bit_dir); + } + furi_delay_us(US_CLOCK); + + furi_delay_us(US_INTERPACKET); +} + +/*static void play_bit_gpio(uint8_t send_bit) { + // external TX over motor driver wired to PIN_A and PIN_B + bit_dir ^= 1; + furi_hal_gpio_write(GPIO_PIN_A, bit_dir); + furi_hal_gpio_write(GPIO_PIN_B, !bit_dir); + furi_delay_us(US_CLOCK); + + if(send_bit) { + bit_dir ^= 1; + furi_hal_gpio_write(GPIO_PIN_A, bit_dir); + furi_hal_gpio_write(GPIO_PIN_B, !bit_dir); + } + furi_delay_us(US_CLOCK); + + furi_delay_us(US_INTERPACKET); +}*/ + +void rfid_tx_init() { + // initialize RFID system for TX + furi_hal_power_enable_otg(); + + furi_hal_ibutton_start_drive(); + furi_hal_ibutton_pin_low(); + + // 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, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow); + + // confirm this delay is needed / sufficient? legacy from hackathon... + furi_delay_ms(300); +} + +void rfid_tx_reset() { + // reset RFID system + furi_hal_gpio_write(RFID_PIN, 0); + + furi_hal_rfid_pins_reset(); + furi_hal_power_disable_otg(); +} + +/* +static void gpio_tx_init() { + furi_hal_power_enable_otg(); + gpio_item_configure_all_pins(GpioModeOutputPushPull); +} + +static void gpio_tx_reset() { + gpio_item_set_pin(PIN_A, 0); + gpio_item_set_pin(PIN_B, 0); + gpio_item_set_pin(ENABLE_PIN, 0); + + gpio_item_configure_all_pins(GpioModeAnalog); + furi_hal_power_disable_otg(); +} +*/ + +void track_to_bits(uint8_t* bit_array, const char* track_data, uint8_t track_index) { + // convert individual track to bits + + int tmp, crc, lrc = 0; + int i = 0; + + // convert track data to bits + for(uint8_t j = 0; track_data[i] != '\0'; j++) { + crc = 1; + tmp = track_data[j] - sublen[track_index]; + + for(uint8_t k = 0; k < bitlen[track_index] - 1; k++) { + crc ^= tmp & 1; + lrc ^= (tmp & 1) << k; + bit_array[i] = tmp & 1; + i++; + tmp >>= 1; + } + bit_array[i] = crc; + i++; + } + + // finish calculating final "byte" (LRC) + tmp = lrc; + crc = 1; + for(uint8_t j = 0; j < bitlen[track_index] - 1; j++) { + crc ^= tmp & 1; + bit_array[i] = tmp & 1; + i++; + tmp >>= 1; + } + bit_array[i] = crc; + i++; + + // My makeshift end sentinel. All other values 0/1 + bit_array[i] = 2; + i++; + + //bool is_correct_length = (i == (strlen(track_data) * bitlen[track_index])); + //furi_assert(is_correct_length); +} + +void mag_spoof_single_track_rfid(FuriString* track_str, uint8_t track_index) { + // Quick testing... + + rfid_tx_init(); + + size_t from; + size_t to; + + // TODO ';' in first track case + if(track_index == 0) { + from = furi_string_search_char(track_str, '%'); + to = furi_string_search_char(track_str, '?', from); + } else if(track_index == 1) { + from = furi_string_search_char(track_str, ';'); + to = furi_string_search_char(track_str, '?', from); + } else { + from = 0; + to = furi_string_size(track_str); + } + if(from >= to) { + return; + } + furi_string_mid(track_str, from, to - from + 1); + + const char* data = furi_string_get_cstr(track_str); + uint8_t bit_array[(strlen(data) * bitlen[track_index]) + 1]; + track_to_bits(bit_array, data, track_index); + + FURI_CRITICAL_ENTER(); + for(uint8_t i = 0; i < ZERO_PREFIX; i++) play_bit_rfid(0); + for(uint8_t i = 0; bit_array[i] != 2; i++) play_bit_rfid(bit_array[i] & 1); + for(uint8_t i = 0; i < ZERO_SUFFIX; i++) play_bit_rfid(0); + FURI_CRITICAL_EXIT(); + + rfid_tx_reset(); +} + +void mag_spoof_two_track_rfid(FuriString* track1, FuriString* track2) { + // Quick testing... + + rfid_tx_init(); + + const char* data1 = furi_string_get_cstr(track1); + uint8_t bit_array1[(strlen(data1) * bitlen[0]) + 1]; + const char* data2 = furi_string_get_cstr(track2); + uint8_t bit_array2[(strlen(data2) * bitlen[1]) + 1]; + + track_to_bits(bit_array1, data1, 0); + track_to_bits(bit_array2, data2, 1); + + FURI_CRITICAL_ENTER(); + for(uint8_t i = 0; i < ZERO_PREFIX; i++) play_bit_rfid(0); + for(uint8_t i = 0; bit_array1[i] != 2; i++) play_bit_rfid(bit_array1[i] & 1); + for(uint8_t i = 0; i < ZERO_BETWEEN; i++) play_bit_rfid(0); + for(uint8_t i = 0; bit_array2[i] != 2; i++) play_bit_rfid(bit_array2[i] & 1); + for(uint8_t i = 0; i < ZERO_SUFFIX; i++) play_bit_rfid(0); + FURI_CRITICAL_EXIT(); + + rfid_tx_reset(); +} + +//// @antirez's code from protoview for bitmapping. May want to refactor to use this... + +/* Set the 'bitpos' bit to value 'val', in the specified bitmap + * 'b' of len 'blen'. + * Out of range bits will silently be discarded. */ +void set_bit(uint8_t* b, uint32_t blen, uint32_t bitpos, bool val) { + uint32_t byte = bitpos / 8; + uint32_t bit = bitpos & 7; + if(byte >= blen) return; + if(val) + b[byte] |= 1 << bit; + else + b[byte] &= ~(1 << bit); +} + +/* Get the bit 'bitpos' of the bitmap 'b' of 'blen' bytes. + * Out of range bits return false (not bit set). */ +bool get_bit(uint8_t* b, uint32_t blen, uint32_t bitpos) { + uint32_t byte = bitpos / 8; + uint32_t bit = bitpos & 7; + if(byte >= blen) return 0; + return (b[byte] & (1 << bit)) != 0; +} + +/*uint32_t convert_signal_to_bits(uint8_t *b, uint32_t blen, RawSamplesBuffer *s, uint32_t idx, uint32_t count, uint32_t rate) { + if (rate == 0) return 0; // We can't perform the conversion. + uint32_t bitpos = 0; + for (uint32_t j = 0; j < count; j++) { + uint32_t dur; + bool level; + raw_samples_get(s, j+idx, &level, &dur); + + uint32_t numbits = dur / rate; // full bits that surely fit. + uint32_t rest = dur % rate; // How much we are left with. + if (rest > rate/2) numbits++; // There is another one. + while(numbits--) set_bit(b,blen,bitpos++,s[j].level); + } + return bitpos; +}*/ diff --git a/helpers/mag_helpers.h b/helpers/mag_helpers.h new file mode 100644 index 00000000000..e2b668d66ec --- /dev/null +++ b/helpers/mag_helpers.h @@ -0,0 +1,13 @@ +#include "../mag_i.h" +#include +#include + +void play_bit_rfid(uint8_t send_bit); +// void play_bit_gpio(uint8_t send_bit); +void rfid_tx_init(); +void rfid_tx_reset(); +void track_to_bits(uint8_t* bit_array, const char* track_data, uint8_t track_index); +void mag_spoof_single_track_rfid(FuriString* track_str, uint8_t track_index); +void mag_spoof_two_track_rfid(FuriString* track1, FuriString* track2); +void set_bit(uint8_t* b, uint32_t blen, uint32_t bitpos, bool val); +bool get_bit(uint8_t* b, uint32_t blen, uint32_t bitpos); diff --git a/helpers/mag_text_input.c b/helpers/mag_text_input.c new file mode 100644 index 00000000000..e5daa74a027 --- /dev/null +++ b/helpers/mag_text_input.c @@ -0,0 +1,583 @@ +#include "mag_text_input.h" +#include +#include +#include + +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); +} diff --git a/helpers/mag_text_input.h b/helpers/mag_text_input.h new file mode 100644 index 00000000000..1b3d1689a70 --- /dev/null +++ b/helpers/mag_text_input.h @@ -0,0 +1,82 @@ +#pragma once + +#include +// #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 diff --git a/icons/DolphinNice_96x59.png b/icons/DolphinNice_96x59.png new file mode 100644 index 00000000000..a299d363023 Binary files /dev/null and b/icons/DolphinNice_96x59.png differ diff --git a/icons/KeyBackspaceSelected_16x9.png b/icons/KeyBackspaceSelected_16x9.png new file mode 100644 index 00000000000..7cc0759a8ca Binary files /dev/null and b/icons/KeyBackspaceSelected_16x9.png differ diff --git a/icons/KeyBackspace_16x9.png b/icons/KeyBackspace_16x9.png new file mode 100644 index 00000000000..9946232d953 Binary files /dev/null and b/icons/KeyBackspace_16x9.png differ diff --git a/icons/KeySaveSelected_24x11.png b/icons/KeySaveSelected_24x11.png new file mode 100644 index 00000000000..eeb3569d3ac Binary files /dev/null and b/icons/KeySaveSelected_24x11.png differ diff --git a/icons/KeySave_24x11.png b/icons/KeySave_24x11.png new file mode 100644 index 00000000000..e7dba987a04 Binary files /dev/null and b/icons/KeySave_24x11.png differ diff --git a/mag.c b/mag.c index 3099d6d7b60..93e920504ea 100644 --- a/mag.c +++ b/mag.c @@ -66,6 +66,11 @@ static Mag* mag_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; } @@ -103,6 +108,10 @@ 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); + mag_text_input_free(mag->mag_text_input); + // View Dispatcher view_dispatcher_free(mag->view_dispatcher); @@ -140,73 +149,6 @@ int32_t mag_app(void* p) { return 0; } -bool mag_save_key(Mag* mag) { - furi_assert(mag); - - bool result = false; - - mag_make_app_folder(mag); - - if(furi_string_end_with(mag->file_path, MAG_APP_EXTENSION)) { - size_t filename_start = furi_string_search_rchar(mag->file_path, '/'); - furi_string_left(mag->file_path, filename_start); - } - - furi_string_cat_printf( - mag->file_path, "/%s%s", furi_string_get_cstr(mag->file_name), MAG_APP_EXTENSION); - - result = mag_save_key_data(mag, mag->file_path); - return result; -} - -bool mag_load_key_from_file_select(Mag* mag) { - furi_assert(mag); - - DialogsFileBrowserOptions browser_options; - dialog_file_browser_set_basic_options(&browser_options, MAG_APP_EXTENSION, &I_mag_10px); - browser_options.base_path = MAG_APP_FOLDER; - - // Input events and views are managed by file_browser - bool result = - dialog_file_browser_show(mag->dialogs, mag->file_path, mag->file_path, &browser_options); - - if(result) { - result = mag_load_key_data(mag, mag->file_path, true); - } - - return result; -} - -bool mag_delete_key(Mag* mag) { - furi_assert(mag); - - return storage_simply_remove(mag->storage, furi_string_get_cstr(mag->file_path)); -} - -bool mag_load_key_data(Mag* mag, FuriString* path, bool show_dialog) { - bool result = false; - UNUSED(mag); - UNUSED(path); - UNUSED(show_dialog); - - // TODO: Needs reworking from LFRFID version, as that goes through some custom protocol by key type. - // Alternatively, co-opt the "protocol" typing as our way of encoding track # (Track 1, 2, 3, or some combination thereof) - - return result; -} - -bool mag_save_key_data(Mag* mag, FuriString* path) { - bool result = false; - UNUSED(path); - //bool result = lfrfid_dict_file_save(app->dict, app->protocol_id, furi_string_get_cstr(path)); - // TODO: needs reworking from LFRFID version - if(!result) { - dialog_message_show_storage_error(mag->dialogs, "Cannot save\nkey file"); - } - - return result; -} - void mag_make_app_folder(Mag* mag) { furi_assert(mag); diff --git a/mag_i.h b/mag_i.h index f5543c2bdd3..6ced10e057a 100644 --- a/mag_i.h +++ b/mag_i.h @@ -1,6 +1,8 @@ #pragma once #include "mag_device.h" +#include "helpers/mag_helpers.h" +#include "helpers/mag_text_input.h" #include #include @@ -27,7 +29,7 @@ #include "scenes/mag_scene.h" -#define MAG_TEXT_STORE_SIZE 128 +#define MAG_TEXT_STORE_SIZE 150 enum MagCustomEvent { MagEventNext = 100, @@ -58,7 +60,8 @@ struct Mag { TextInput* text_input; Widget* widget; - // Custom views? + // Custom views + Mag_TextInput* mag_text_input; }; typedef enum { @@ -68,6 +71,7 @@ typedef enum { MagViewLoading, MagViewWidget, MagViewTextInput, + MagViewMagTextInput, } MagView; void mag_text_store_set(Mag* mag, const char* text, ...); @@ -76,18 +80,6 @@ void mag_text_store_clear(Mag* mag); void mag_show_loading_popup(void* context, bool show); -// all below this comment are destined for deprecation (now handled by mag_device) - -bool mag_save_key(Mag* mag); - -bool mag_load_key_from_file_select(Mag* mag); - -bool mag_delete_key(Mag* mag); - -bool mag_load_key_data(Mag* mag, FuriString* path, bool show_dialog); - -bool mag_save_key_data(Mag* mag, FuriString* path); - void mag_make_app_folder(Mag* mag); void mag_popup_timeout_callback(void* context); diff --git a/scenes/mag_scene_emulate_test.c b/scenes/mag_scene_emulate_test.c index 431d5c4a64c..21082904c5c 100644 --- a/scenes/mag_scene_emulate_test.c +++ b/scenes/mag_scene_emulate_test.c @@ -2,8 +2,8 @@ #define PIN_A 0 #define PIN_B 1 // currently unused -#define CLOCK_US 240 // typically set between 200-500us -#define TEST_STR "%%B123456781234567^LASTNAME/FIRST^YYMMSSSDDDDDDDDDDDDDDDDDDDDDDDDD?" +#define CLOCK_US 500 // typically set between 200-500us +#define TEST_STR "%%B123456781234567^LASTNAME/FIRST^YYMMSSSDDDDDDDDDDDDDDDDDDDDDDDDD?\0" #define TEST_TRACK 0 // TODO: better way of setting temp test str, // text wrapping on screen? (Will be relevant for any loaded data too) @@ -159,6 +159,8 @@ void mag_scene_emulate_test_on_enter(void* context) { widget_add_button_element(widget, GuiButtonTypeLeft, "Back", mag_widget_callback, mag); widget_add_button_element(widget, GuiButtonTypeRight, "Emulate", mag_widget_callback, mag); + //widget_add_button_element(widget, GuiButtonTypeRight, "Re", mag_widget_callback, mag); + //widget_add_button_element(widget, GuiButtonTypeCenter, "Two", mag_widget_callback, mag); //furi_string_printf(tmp_string, test_str); //widget_add_string_element( @@ -185,6 +187,7 @@ bool mag_scene_emulate_test_on_event(void* context, SceneManagerEvent event) { // blink led while spoofing notification_message(mag->notifications, &sequence_blink_start_cyan); mag_spoof(v, TEST_TRACK); + // mag_spoof_single_track_rfid(v, TEST_TRACK); notification_message(mag->notifications, &sequence_blink_stop); furi_string_free(v); @@ -200,5 +203,7 @@ bool mag_scene_emulate_test_on_event(void* context, SceneManagerEvent event) { void mag_scene_emulate_test_on_exit(void* context) { Mag* mag = context; + + notification_message(mag->notifications, &sequence_blink_stop); widget_reset(mag->widget); } diff --git a/scenes/mag_scene_input_name.c b/scenes/mag_scene_input_name.c index 037b599e967..7368b4598e2 100644 --- a/scenes/mag_scene_input_name.c +++ b/scenes/mag_scene_input_name.c @@ -9,7 +9,7 @@ void mag_scene_input_name_on_enter(void* context) { //TODO: compatible types / etc //bool name_is_empty = furi_string_empty(mag->mag_dev->dev_name); - bool name_is_empty = false; + bool name_is_empty = true; if(name_is_empty) { furi_string_set(mag->file_path, MAG_APP_FOLDER); @@ -51,23 +51,14 @@ bool mag_scene_input_name_on_event(void* context, SceneManagerEvent event) { if(event.type == SceneManagerEventTypeCustom) { if(event.event == MagEventNext) { consumed = true; - if(!furi_string_empty(mag->file_name)) { - mag_delete_key(mag); - } + //if(!furi_string_empty(mag->file_name)) { + // mag_delete_key(mag); + //} furi_string_set(mag->file_name, mag->text_store); - if(mag_save_key(mag)) { + if(mag_device_save(mag->mag_dev, furi_string_get_cstr(mag->file_name))) { scene_manager_next_scene(scene_manager, MagSceneSaveSuccess); - if(scene_manager_has_previous_scene(scene_manager, MagSceneSavedMenu)) { - // Nothing, do not count editing as saving - //} else if(scene_manager_has_previous_scene(scene_manager, MagSceneSaveType)) { - //DOLPHIN_DEED(DolphinDeedRfidAdd); - // TODO: replace dolphin deed! - } else { - //DOLPHIN_DEED(DolphinDeedRfidSave); - // TODO: replace dolphin deed! - } } else { //scene_manager_search_and_switch_to_previous_scene( // scene_manager, MagSceneReadKeyMenu); diff --git a/scenes/mag_scene_input_value.c b/scenes/mag_scene_input_value.c index e2134083430..ef602f46f99 100644 --- a/scenes/mag_scene_input_value.c +++ b/scenes/mag_scene_input_value.c @@ -2,23 +2,32 @@ void mag_scene_input_value_on_enter(void* context) { Mag* mag = context; - TextInput* text_input = mag->text_input; + Mag_TextInput* mag_text_input = mag->mag_text_input; - FuriString* tmp_str; - tmp_str = furi_string_alloc(); - UNUSED(tmp_str); + // 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)); - text_input_set_header_text(text_input, "Enter track data (WIP)"); - //text_input_set_result_callback( - // text_input, mag_text_input_callback, mag, mag->mag_dev->dev_data, ) + 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; - UNUSED(mag); - UNUSED(event); + 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, mag->text_store); + scene_manager_next_scene(scene_manager, MagSceneInputName); + } + } + return consumed; } diff --git a/scenes/mag_scene_save_success.c b/scenes/mag_scene_save_success.c index 185698d7f77..fe9b6b4af7e 100644 --- a/scenes/mag_scene_save_success.c +++ b/scenes/mag_scene_save_success.c @@ -2,19 +2,42 @@ void mag_scene_save_success_on_enter(void* context) { Mag* mag = context; - UNUSED(mag); + 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; - UNUSED(mag); - UNUSED(event); 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; - UNUSED(mag); + + popup_reset(mag->popup); } \ No newline at end of file diff --git a/scenes/mag_scene_start.c b/scenes/mag_scene_start.c index 998e4805d0b..776f837c813 100644 --- a/scenes/mag_scene_start.c +++ b/scenes/mag_scene_start.c @@ -48,7 +48,7 @@ bool mag_scene_start_on_event(void* context, SceneManagerEvent event) { scene_manager_next_scene(mag->scene_manager, MagSceneFileSelect); consumed = true; } else if(event.event == SubmenuIndexAddManually) { - scene_manager_next_scene(mag->scene_manager, MagSceneUnderConstruction); + scene_manager_next_scene(mag->scene_manager, MagSceneInputValue); consumed = true; } scene_manager_set_scene_state(mag->scene_manager, MagSceneStart, event.event);