From 700b2578978d432205553b57dbe7baa306ba8443 Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Fri, 4 Aug 2023 19:25:43 +0200 Subject: [PATCH] Add magspoof --- applications/external/magspoof/LICENSE | 21 + .../external/magspoof/application.fam | 21 + .../external/magspoof/helpers/mag_helpers.c | 480 ++++++++++++++ .../external/magspoof/helpers/mag_helpers.h | 25 + .../magspoof/helpers/mag_text_input.c | 583 ++++++++++++++++++ .../magspoof/helpers/mag_text_input.h | 82 +++ .../external/magspoof/helpers/mag_types.h | 44 ++ .../magspoof/icons/DolphinMafia_115x62.png | Bin 0 -> 2504 bytes .../magspoof/icons/DolphinNice_96x59.png | Bin 0 -> 2459 bytes .../icons/KeyBackspaceSelected_16x9.png | Bin 0 -> 1812 bytes .../magspoof/icons/KeyBackspace_16x9.png | Bin 0 -> 1829 bytes .../magspoof/icons/KeySaveSelected_24x11.png | Bin 0 -> 1853 bytes .../external/magspoof/icons/KeySave_24x11.png | Bin 0 -> 1863 bytes .../external/magspoof/icons/mag_10px.png | Bin 0 -> 4847 bytes .../external/magspoof/icons/mag_file_10px.png | Bin 0 -> 2401 bytes applications/external/magspoof/mag.c | 244 ++++++++ applications/external/magspoof/mag_device.c | 311 ++++++++++ applications/external/magspoof/mag_device.h | 58 ++ applications/external/magspoof/mag_i.h | 105 ++++ .../external/magspoof/scenes/mag_scene.c | 30 + .../external/magspoof/scenes/mag_scene.h | 29 + .../magspoof/scenes/mag_scene_about.c | 40 ++ .../magspoof/scenes/mag_scene_config.h | 15 + .../scenes/mag_scene_delete_confirm.c | 49 ++ .../scenes/mag_scene_delete_success.c | 39 ++ .../magspoof/scenes/mag_scene_emulate.c | 93 +++ .../scenes/mag_scene_emulate_config.c | 264 ++++++++ .../magspoof/scenes/mag_scene_exit_confirm.c | 20 + .../magspoof/scenes/mag_scene_file_select.c | 24 + .../magspoof/scenes/mag_scene_input_name.c | 82 +++ .../magspoof/scenes/mag_scene_input_value.c | 37 ++ .../external/magspoof/scenes/mag_scene_read.c | 185 ++++++ .../external/magspoof/scenes/mag_scene_read.h | 20 + .../magspoof/scenes/mag_scene_save_success.c | 43 ++ .../magspoof/scenes/mag_scene_saved_info.c | 50 ++ .../magspoof/scenes/mag_scene_saved_menu.c | 83 +++ .../magspoof/scenes/mag_scene_start.c | 71 +++ .../scenes/mag_scene_under_construction.c | 40 ++ 38 files changed, 3188 insertions(+) create mode 100644 applications/external/magspoof/LICENSE create mode 100644 applications/external/magspoof/application.fam create mode 100644 applications/external/magspoof/helpers/mag_helpers.c create mode 100644 applications/external/magspoof/helpers/mag_helpers.h create mode 100644 applications/external/magspoof/helpers/mag_text_input.c create mode 100644 applications/external/magspoof/helpers/mag_text_input.h create mode 100644 applications/external/magspoof/helpers/mag_types.h create mode 100644 applications/external/magspoof/icons/DolphinMafia_115x62.png create mode 100644 applications/external/magspoof/icons/DolphinNice_96x59.png create mode 100644 applications/external/magspoof/icons/KeyBackspaceSelected_16x9.png create mode 100644 applications/external/magspoof/icons/KeyBackspace_16x9.png create mode 100644 applications/external/magspoof/icons/KeySaveSelected_24x11.png create mode 100644 applications/external/magspoof/icons/KeySave_24x11.png create mode 100644 applications/external/magspoof/icons/mag_10px.png create mode 100644 applications/external/magspoof/icons/mag_file_10px.png create mode 100644 applications/external/magspoof/mag.c create mode 100644 applications/external/magspoof/mag_device.c create mode 100644 applications/external/magspoof/mag_device.h create mode 100644 applications/external/magspoof/mag_i.h create mode 100644 applications/external/magspoof/scenes/mag_scene.c create mode 100644 applications/external/magspoof/scenes/mag_scene.h create mode 100644 applications/external/magspoof/scenes/mag_scene_about.c create mode 100644 applications/external/magspoof/scenes/mag_scene_config.h create mode 100644 applications/external/magspoof/scenes/mag_scene_delete_confirm.c create mode 100644 applications/external/magspoof/scenes/mag_scene_delete_success.c create mode 100644 applications/external/magspoof/scenes/mag_scene_emulate.c create mode 100644 applications/external/magspoof/scenes/mag_scene_emulate_config.c create mode 100644 applications/external/magspoof/scenes/mag_scene_exit_confirm.c create mode 100644 applications/external/magspoof/scenes/mag_scene_file_select.c create mode 100644 applications/external/magspoof/scenes/mag_scene_input_name.c create mode 100644 applications/external/magspoof/scenes/mag_scene_input_value.c create mode 100644 applications/external/magspoof/scenes/mag_scene_read.c create mode 100644 applications/external/magspoof/scenes/mag_scene_read.h create mode 100644 applications/external/magspoof/scenes/mag_scene_save_success.c create mode 100644 applications/external/magspoof/scenes/mag_scene_saved_info.c create mode 100644 applications/external/magspoof/scenes/mag_scene_saved_menu.c create mode 100644 applications/external/magspoof/scenes/mag_scene_start.c create mode 100644 applications/external/magspoof/scenes/mag_scene_under_construction.c diff --git a/applications/external/magspoof/LICENSE b/applications/external/magspoof/LICENSE new file mode 100644 index 000000000..ac4d2d21a --- /dev/null +++ b/applications/external/magspoof/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Zachary Weiss + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/applications/external/magspoof/application.fam b/applications/external/magspoof/application.fam new file mode 100644 index 000000000..78359f831 --- /dev/null +++ b/applications/external/magspoof/application.fam @@ -0,0 +1,21 @@ +App( + appid="magspoof", + name="MagSpoof", + apptype=FlipperAppType.EXTERNAL, + entry_point="mag_app", + requires=[ + "gui", + "storage", + "notification", + "dialogs", + ], + stack_size=6 * 1024, + order=64, # keep it at the bottom of the list while still WIP + fap_icon="icons/mag_10px.png", + fap_category="GPIO", + fap_icon_assets="icons", + fap_version=(0, 5), # major, minor + fap_description="WIP MagSpoof port using the RFID subsystem", + fap_author="Zachary Weiss", + fap_weburl="https://github.com/hummusec/magspoof_flipper", # Original by zacharyweiss +) diff --git a/applications/external/magspoof/helpers/mag_helpers.c b/applications/external/magspoof/helpers/mag_helpers.c new file mode 100644 index 000000000..997cf3862 --- /dev/null +++ b/applications/external/magspoof/helpers/mag_helpers.c @@ -0,0 +1,480 @@ +#include "mag_helpers.h" + +#define TAG "MagHelpers" + +// Haviv Board - pins gpio_ext_pa7 & gpio_ext_pa6 was swapped. +#define GPIO_PIN_A &gpio_ext_pa7 +#define GPIO_PIN_B &gpio_ext_pa6 +#define GPIO_PIN_ENABLE &gpio_ext_pa4 +#define RFID_PIN_OUT &gpio_rfid_carrier_out + +#define ZERO_PREFIX 25 // n zeros prefix +#define ZERO_BETWEEN 53 // n zeros between tracks +#define ZERO_SUFFIX 25 // n zeros suffix + +// bits per char on a given track +const uint8_t bitlen[] = {7, 5, 5}; +// char offset by track +const int sublen[] = {32, 48, 48}; + +uint8_t last_value = 2; + +void play_halfbit(bool value, MagSetting* setting) { + switch(setting->tx) { + case MagTxStateRFID: + furi_hal_gpio_write(RFID_PIN_OUT, value); + /*furi_hal_gpio_write(RFID_PIN_OUT, !value); + furi_hal_gpio_write(RFID_PIN_OUT, value); + furi_hal_gpio_write(RFID_PIN_OUT, !value); + furi_hal_gpio_write(RFID_PIN_OUT, value);*/ + break; + case MagTxStateGPIO: + furi_hal_gpio_write(GPIO_PIN_A, value); + furi_hal_gpio_write(GPIO_PIN_B, !value); + break; + case MagTxStatePiezo: + furi_hal_gpio_write(&gpio_speaker, value); + /*furi_hal_gpio_write(&gpio_speaker, !value); + furi_hal_gpio_write(&gpio_speaker, value); + furi_hal_gpio_write(&gpio_speaker, !value); + furi_hal_gpio_write(&gpio_speaker, value);*/ + + break; + case MagTxStateLF_P: + furi_hal_gpio_write(RFID_PIN_OUT, value); + furi_hal_gpio_write(&gpio_speaker, value); + + /* // Weaker but cleaner signal + if(value) { + furi_hal_gpio_write(RFID_PIN_OUT, value); + furi_hal_gpio_write(&gpio_speaker, value); + furi_delay_us(10); + furi_hal_gpio_write(RFID_PIN_OUT, !value); + furi_hal_gpio_write(&gpio_speaker, !value); + } else { + furi_delay_us(10); + }*/ + + /*furi_hal_gpio_write(RFID_PIN_OUT, value); + furi_hal_gpio_write(&gpio_speaker, value); + furi_hal_gpio_write(RFID_PIN_OUT, !value); + furi_hal_gpio_write(&gpio_speaker, !value); + furi_hal_gpio_write(RFID_PIN_OUT, value); + furi_hal_gpio_write(&gpio_speaker, value);*/ + break; + case MagTxStateNFC: + // turn on for duration of half-bit? or "blip" the field on / off? + // getting nothing from the mag reader either way + //(value) ? furi_hal_nfc_ll_txrx_on() : furi_hal_nfc_ll_txrx_off(); + + if(last_value == 2 || value != (bool)last_value) { + furi_hal_nfc_ll_txrx_on(); + //furi_delay_us(64); + furi_hal_nfc_ll_txrx_off(); + } + break; + case MagTxCC1101_434: + case MagTxCC1101_868: + if(last_value == 2 || value != (bool)last_value) { + furi_hal_gpio_write(&gpio_cc1101_g0, true); + furi_delay_us(64); + furi_hal_gpio_write(&gpio_cc1101_g0, false); + } + break; + default: + break; + } + + last_value = value; +} + +void play_track(uint8_t* bits_manchester, uint16_t n_bits, MagSetting* setting, bool reverse) { + for(uint16_t i = 0; i < n_bits; i++) { + uint16_t j = (reverse) ? (n_bits - i - 1) : i; + uint8_t byte = j / 8; + uint8_t bitmask = 1 << (7 - (j % 8)); + /* Bits are stored in their arrays like on a card (LSB first). This is not how usually bits are stored in a + * byte, with the MSB first. the var bitmask creates the pattern to iterate through each bit, LSB first, like so + * 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01, 0x80... masking bits one by one from the current byte + * + * I've chosen this LSB approach since bits and bytes are hard enough to visualize with the 5/8 and 7/8 encoding + * MSR uses. It's a biiit more complicated to process, but visualizing it with printf or a debugger is + * infinitely easier + * + * Encoding the following pairs of 5 bits as 5/8: A1234 B1234 C1234 D1234 + * using this LSB format looks like: A1234B12 34C1234D 12340000 + * using the MSB format, looks like: 21B4321A D4321C43 00004321 + * this means reading each byte backwards when printing/debugging, and the jumping 16 bits ahead, reading 8 more + * bits backward, jumping 16 more bits ahead. + * + * I find this much more convenient for debugging, with the tiny incovenience of reading the bits in reverse + * order. Thus, the reason for the bitmask above + */ + + bool bit = !!(bits_manchester[byte] & bitmask); + + // TODO: reimplement timing delays. Replace fixed furi_hal_cortex_delay_us to wait instead to a specific value + // for DWT->CYCCNT. Note timer is aliased to 64us as per + // #define FURI_HAL_CORTEX_INSTRUCTIONS_PER_MICROSECOND (SystemCoreClock / 1000000) | furi_hal_cortex.c + + play_halfbit(bit, setting); + furi_delay_us(setting->us_clock); + // if (i % 2 == 1) furi_delay_us(setting->us_interpacket); + } +} + +void tx_init_rfid() { + // initialize RFID system for TX + + // OTG needed for RFID? Or just legacy from GPIO? + // furi_hal_power_enable_otg(); + furi_hal_ibutton_pin_configure(); + + // furi_hal_ibutton_start_drive(); + furi_hal_ibutton_pin_write(false); + + // Initializing at GpioSpeedLow seems sufficient for our needs; no improvements seen by increasing speed setting + + // this doesn't seem to make a difference, leaving it in + furi_hal_gpio_init(&gpio_rfid_data_in, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow); + furi_hal_gpio_write(&gpio_rfid_data_in, false); + + // false->ground RFID antenna; true->don't ground + // skotopes (RFID dev) say normally you'd want RFID_PULL in high for signal forming, while modulating RFID_OUT + // dunaevai135 had it low in their old code. Leaving low, as it doesn't seem to make a difference on my janky antenna + furi_hal_gpio_init(&gpio_nfc_irq_rfid_pull, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow); + furi_hal_gpio_write(&gpio_nfc_irq_rfid_pull, false); + + furi_hal_gpio_init(RFID_PIN_OUT, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow); + + furi_delay_ms(300); +} + +void tx_deinit_rfid() { + // reset RFID system + furi_hal_gpio_write(RFID_PIN_OUT, 0); + + furi_hal_rfid_pins_reset(); + furi_hal_power_disable_otg(); +} + +void tx_init_rf(int hz) { + // presets and frequency will need some experimenting + furi_hal_subghz_reset(); + // furi_hal_subghz_load_preset(FuriHalSubGhzPresetOok650Async); + // furi_hal_subghz_load_preset(FuriHalSubGhzPresetGFSK9_99KbAsync); + // furi_hal_subghz_load_preset(FuriHalSubGhzPresetMSK99_97KbAsync); + // furi_hal_subghz_load_preset(FuriHalSubGhzPreset2FSKDev238Async); + // furi_hal_subghz_load_preset(FuriHalSubGhzPreset2FSKDev476Async); + furi_hal_gpio_init(&gpio_cc1101_g0, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow); + furi_hal_subghz_set_frequency_and_path(hz); + furi_hal_subghz_tx(); + furi_hal_gpio_write(&gpio_cc1101_g0, false); +} + +void tx_init_piezo() { + // TODO: some special mutex acquire procedure? c.f. furi_hal_speaker.c + furi_hal_gpio_init(&gpio_speaker, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow); +} + +void tx_deinit_piezo() { + // TODO: some special mutex release procedure? + furi_hal_gpio_init(&gpio_speaker, GpioModeAnalog, GpioPullNo, GpioSpeedLow); +} + +bool tx_init(MagSetting* setting) { + // Initialize configured TX method + switch(setting->tx) { + case MagTxStateRFID: + tx_init_rfid(); + break; + case MagTxStateGPIO: + furi_hal_power_enable_otg(); + // gpio_item_configure_all_pins(GpioModeOutputPushPull); + furi_hal_gpio_init(GPIO_PIN_A, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow); + furi_hal_gpio_init(GPIO_PIN_B, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow); + furi_hal_gpio_init(GPIO_PIN_ENABLE, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow); + + furi_hal_gpio_write(GPIO_PIN_ENABLE, 1); + + // had some issues with ~300; bumped higher temporarily + furi_delay_ms(500); + break; + case MagTxStatePiezo: + tx_init_piezo(); + break; + case MagTxStateLF_P: + tx_init_piezo(); + tx_init_rfid(); + break; + case MagTxStateNFC: + furi_hal_nfc_exit_sleep(); + break; + case MagTxCC1101_434: + tx_init_rf(434000000); + break; + case MagTxCC1101_868: + tx_init_rf(868000000); + break; + default: + return false; + } + + return true; +} + +bool tx_deinit(MagSetting* setting) { + // Reset configured TX method + switch(setting->tx) { + case MagTxStateRFID: + tx_deinit_rfid(); + break; + case MagTxStateGPIO: + furi_hal_gpio_write(GPIO_PIN_A, 0); + furi_hal_gpio_write(GPIO_PIN_B, 0); + furi_hal_gpio_write(GPIO_PIN_ENABLE, 0); + + // set back to analog output mode? + //gpio_item_configure_all_pins(GpioModeAnalog); + furi_hal_power_disable_otg(); + break; + case MagTxStatePiezo: + tx_deinit_piezo(); + break; + case MagTxStateLF_P: + tx_deinit_piezo(); + tx_deinit_rfid(); + break; + case MagTxStateNFC: + furi_hal_nfc_ll_txrx_off(); + furi_hal_nfc_start_sleep(); + break; + case MagTxCC1101_434: + case MagTxCC1101_868: + furi_hal_gpio_write(&gpio_cc1101_g0, false); + furi_hal_subghz_reset(); + furi_hal_subghz_idle(); + break; + default: + return false; + } + + return true; +} + +void mag_spoof(Mag* mag) { + MagSetting* setting = mag->setting; + + // TODO: cleanup this section. Possibly move precompute + tx_init to emulate_on_enter? + FuriString* ft1 = mag->mag_dev->dev_data.track[0].str; + FuriString* ft2 = mag->mag_dev->dev_data.track[1].str; + FuriString* ft3 = mag->mag_dev->dev_data.track[2].str; + + char *data1, *data2, *data3; + data1 = malloc(furi_string_size(ft1) + 1); + data2 = malloc(furi_string_size(ft2) + 1); + data3 = malloc(furi_string_size(ft3) + 1); + strncpy(data1, furi_string_get_cstr(ft1), furi_string_size(ft1)); + strncpy(data2, furi_string_get_cstr(ft2), furi_string_size(ft2)); + strncpy(data3, furi_string_get_cstr(ft3), furi_string_size(ft3)); + + if(furi_log_get_level() >= FuriLogLevelDebug) { + debug_mag_string(data1, bitlen[0], sublen[0]); + debug_mag_string(data2, bitlen[1], sublen[1]); + debug_mag_string(data3, bitlen[2], sublen[2]); + } + + uint8_t bits_t1_raw[64] = {0x00}; // 68 chars max track 1 + 1 char crc * 7 approx =~ 483 bits + uint8_t bits_t1_manchester[128] = {0x00}; // twice the above + uint16_t bits_t1_count = mag_encode( + data1, (uint8_t*)bits_t1_manchester, (uint8_t*)bits_t1_raw, bitlen[0], sublen[0]); + uint8_t bits_t2_raw[64] = {0x00}; // 68 chars max track 1 + 1 char crc * 7 approx =~ 483 bits + uint8_t bits_t2_manchester[128] = {0x00}; // twice the above + uint16_t bits_t2_count = mag_encode( + data2, (uint8_t*)bits_t2_manchester, (uint8_t*)bits_t2_raw, bitlen[1], sublen[1]); + uint8_t bits_t3_raw[64] = {0x00}; + uint8_t bits_t3_manchester[128] = {0x00}; + uint16_t bits_t3_count = mag_encode( + data3, (uint8_t*)bits_t3_manchester, (uint8_t*)bits_t3_raw, bitlen[2], sublen[2]); + + if(furi_log_get_level() >= FuriLogLevelDebug) { + printf( + "Manchester bitcount: T1: %d, T2: %d, T3: %d\r\n", + bits_t1_count, + bits_t2_count, + bits_t3_count); + printf("T1 raw: "); + for(int i = 0; i < bits_t1_count / 16; i++) printf("%02x ", bits_t1_raw[i]); + printf("\r\nT1 manchester: "); + for(int i = 0; i < bits_t1_count / 8; i++) printf("%02x ", bits_t1_manchester[i]); + printf("\r\nT2 raw: "); + for(int i = 0; i < bits_t2_count / 16; i++) printf("%02x ", bits_t2_raw[i]); + printf("\r\nT2 manchester: "); + for(int i = 0; i < bits_t2_count / 8; i++) printf("%02x ", bits_t2_manchester[i]); + printf("\r\nT3 raw: "); + for(int i = 0; i < bits_t3_count / 16; i++) printf("%02x ", bits_t3_raw[i]); + printf("\r\nT3 manchester: "); + for(int i = 0; i < bits_t3_count / 8; i++) printf("%02x ", bits_t3_manchester[i]); + printf("\r\nBitwise emulation done\r\n\r\n"); + } + + last_value = 2; + bool bit = false; + + if(!tx_init(setting)) return; + + FURI_CRITICAL_ENTER(); + for(uint16_t i = 0; i < (ZERO_PREFIX * 2); i++) { + // is this right? + if(!!(i % 2)) bit ^= 1; + play_halfbit(bit, setting); + furi_delay_us(setting->us_clock); + } + + if((setting->track == MagTrackStateOneAndTwo) || (setting->track == MagTrackStateOne)) + play_track((uint8_t*)bits_t1_manchester, bits_t1_count, setting, false); + + if((setting->track == MagTrackStateOneAndTwo)) + for(uint16_t i = 0; i < (ZERO_BETWEEN * 2); i++) { + if(!!(i % 2)) bit ^= 1; + play_halfbit(bit, setting); + furi_delay_us(setting->us_clock); + } + + if((setting->track == MagTrackStateOneAndTwo) || (setting->track == MagTrackStateTwo)) + play_track( + (uint8_t*)bits_t2_manchester, + bits_t2_count, + setting, + (setting->reverse == MagReverseStateOn)); + + if((setting->track == MagTrackStateThree)) + play_track((uint8_t*)bits_t3_manchester, bits_t3_count, setting, false); + + for(uint16_t i = 0; i < (ZERO_SUFFIX * 2); i++) { + if(!!(i % 2)) bit ^= 1; + play_halfbit(bit, setting); + furi_delay_us(setting->us_clock); + } + FURI_CRITICAL_EXIT(); + + free(data1); + free(data2); + free(data3); + tx_deinit(setting); +} + +uint16_t add_bit(bool value, uint8_t* out, uint16_t count) { + uint8_t bit = count % 8; + uint8_t byte = count / 8; + if(value) { + out[byte] |= 0x01; + } + if(bit < 7) out[byte] <<= 1; + return count + 1; +} + +uint16_t add_bit_manchester(bool value, uint8_t* out, uint16_t count) { + static bool toggle = 0; + toggle ^= 0x01; + count = add_bit(toggle, out, count); + if(value) toggle ^= 0x01; + count = add_bit(toggle, out, count); + return count; +} + +uint16_t mag_encode( + char* data, + uint8_t* out_manchester, + uint8_t* out_raw, + uint8_t track_bits, + uint8_t track_ascii_offset) { + /* + * track_bits - the number of raw (data) bits on the track. on ISO cards, that's 7 for track 1, or 5 for 2/3 - this is samy's bitlen + * - this count includes the parity bit + * track_ascii_offset - how much the ascii values are offset. track 1 makes space (ascii 32) become data 0x00, + * - tracks 2/3 make ascii "0" become data 0x00 - this is samy's sublen + * + */ + + uint16_t raw_bits_count = 0; + uint16_t output_count = 0; + int tmp, crc, lrc = 0; + + /* // why are we adding zeros to the encoded string if we're also doing it while playing? + for(int i = 0; i < ZERO_PREFIX; i++) { + output_count = add_bit_manchester(0, out_manchester, output_count); + raw_bits_count = add_bit(0, out_raw, raw_bits_count); + }*/ + + for(int i = 0; *(data + i) != 0; i++) { + crc = 1; + tmp = *(data + i) - track_ascii_offset; + + for(int j = 0; j < track_bits - 1; j++) { + crc ^= tmp & 1; + lrc ^= (tmp & 1) << j; + raw_bits_count = add_bit(tmp & 0x01, out_raw, raw_bits_count); + output_count = add_bit_manchester(tmp & 0x01, out_manchester, output_count); + tmp >>= 1; + } + raw_bits_count = add_bit(crc, out_raw, raw_bits_count); + output_count = add_bit_manchester(crc, out_manchester, output_count); + } + + // LRC byte + tmp = lrc; + crc = 1; + for(int j = 0; j < track_bits - 1; j++) { + crc ^= tmp & 0x01; + raw_bits_count = add_bit(tmp & 0x01, out_raw, raw_bits_count); + output_count = add_bit_manchester(tmp & 0x01, out_manchester, output_count); + tmp >>= 1; + } + raw_bits_count = add_bit(crc, out_raw, raw_bits_count); + output_count = add_bit_manchester(crc, out_manchester, output_count); + + return output_count; +} + +void debug_mag_string(char* data, uint8_t track_bits, uint8_t track_ascii_offset) { + uint8_t bits_raw[64] = {0}; // 68 chars max track 1 + 1 char crc * 7 approx =~ 483 bits + uint8_t bits_manchester[128] = {0}; // twice the above + int numbits = 0; + + printf("Encoding [%s] with %d bits\r\n", data, track_bits); + numbits = mag_encode( + data, (uint8_t*)bits_manchester, (uint8_t*)bits_raw, track_bits, track_ascii_offset); + printf("Got %d bits\r\n", numbits); + printf("Raw byte stream: "); + for(int i = 0; i < numbits / 8 / 2; i++) { + printf("%02x", bits_raw[i]); + if(i % 4 == 3) printf(" "); + } + + printf("\r\n"); + + printf("Bits "); + int space_counter = 0; + for(int i = 0; i < numbits / 2; i++) { + /*if(i < ZERO_PREFIX) { + printf("X"); + continue; + } else if(i == ZERO_PREFIX) { + printf(" "); + space_counter = 0; + }*/ + printf("%01x", (bits_raw[i / 8] & (1 << (7 - (i % 8)))) != 0); + if((space_counter) % track_bits == track_bits - 1) printf(" "); + space_counter++; + } + + printf("\r\n"); + + printf("Manchester encoded, byte stream: "); + for(int i = 0; i < numbits / 8; i++) { + printf("%02x", bits_manchester[i]); + if(i % 4 == 3) printf(" "); + } + printf("\r\n\r\n"); +} diff --git a/applications/external/magspoof/helpers/mag_helpers.h b/applications/external/magspoof/helpers/mag_helpers.h new file mode 100644 index 000000000..a61f143b8 --- /dev/null +++ b/applications/external/magspoof/helpers/mag_helpers.h @@ -0,0 +1,25 @@ +#include "../mag_i.h" +#include +#include + +void play_halfbit(bool value, MagSetting* setting); +void play_track(uint8_t* bits_manchester, uint16_t n_bits, MagSetting* setting, bool reverse); + +void tx_init_rf(int hz); +void tx_init_rfid(); +void tx_init_piezo(); +bool tx_init(MagSetting* setting); +void tx_deinit_piezo(); +void tx_deinit_rfid(); +bool tx_deinit(MagSetting* setting); + +uint16_t add_bit(bool value, uint8_t* out, uint16_t count); +uint16_t add_bit_manchester(bool value, uint8_t* out, uint16_t count); +uint16_t mag_encode( + char* data, + uint8_t* out_manchester, + uint8_t* out_raw, + uint8_t track_bits, + uint8_t track_ascii_offset); +void debug_mag_string(char* data, uint8_t track_bits, uint8_t track_ascii_offset); +void mag_spoof(Mag* mag); diff --git a/applications/external/magspoof/helpers/mag_text_input.c b/applications/external/magspoof/helpers/mag_text_input.c new file mode 100644 index 000000000..e5daa74a0 --- /dev/null +++ b/applications/external/magspoof/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/applications/external/magspoof/helpers/mag_text_input.h b/applications/external/magspoof/helpers/mag_text_input.h new file mode 100644 index 000000000..1b3d1689a --- /dev/null +++ b/applications/external/magspoof/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/applications/external/magspoof/helpers/mag_types.h b/applications/external/magspoof/helpers/mag_types.h new file mode 100644 index 000000000..96b8ee6e6 --- /dev/null +++ b/applications/external/magspoof/helpers/mag_types.h @@ -0,0 +1,44 @@ +#pragma once + +#define MAG_VERSION_APP "0.05" +#define MAG_DEVELOPER "Zachary Weiss" +#define MAG_GITHUB "github.com/zacharyweiss/magspoof_flipper" + +typedef enum { + MagViewSubmenu, + MagViewDialogEx, + MagViewPopup, + MagViewLoading, + MagViewWidget, + MagViewVariableItemList, + MagViewTextInput, + MagViewMagTextInput, +} MagView; + +typedef enum { + MagReverseStateOff, + MagReverseStateOn, +} MagReverseState; + +typedef enum { + MagTrackStateOneAndTwo, + MagTrackStateOne, + MagTrackStateTwo, + MagTrackStateThree, +} MagTrackState; + +typedef enum { + MagTxStateRFID, + MagTxStateGPIO, + MagTxStatePiezo, + MagTxStateLF_P, // combo of RFID and Piezo + MagTxStateNFC, + MagTxCC1101_434, + MagTxCC1101_868, +} MagTxState; + +typedef enum { + UART_TerminalEventRefreshConsoleOutput = 0, + UART_TerminalEventStartConsole, + UART_TerminalEventStartKeyboard, +} UART_TerminalCustomEvent; diff --git a/applications/external/magspoof/icons/DolphinMafia_115x62.png b/applications/external/magspoof/icons/DolphinMafia_115x62.png new file mode 100644 index 0000000000000000000000000000000000000000..66fdb40ff2651916faed4a2ae1d564cafdbf7bcb GIT binary patch literal 2504 zcmbVO3se(V8ji5Kg5ZmV3I#ewZHbsZB8b0=g#+k z_y7La$vcsnwa$(njyxXES*27&ad(Ehf@a%szx(??vFC0MMrAy=Img9%&ES885R5X2P@K{dB9p<$p?SQ()g~i~r4cNkC3JdH&i}sg6d%yza(--p8dMv@ zh!njtmnNcfH8EIj8V2M1)j>d@3E>C~1d9SDLpsSICOLnC7va{{Z80C1fUs$Deu(uz zAWj_#gi$mBNJXF!13?Io!6J#&-(L!@03Z+o#bAI~0tqEj1oTHFGGOY%=T4*XWF$)Q z`>C_ICpkZbWsQhfoSmI5%Jvgcv`#F6VOR`8Vh9p)2qBY0vZzT&GE1fz6a<6OdLyf+ zNWjX7YNwhuAF&nut zlTM%T7{|m!I$L;81?0JCCML&7h@%LG%A_%3 zO%`|J5~~^`5=Ij!OVKeDl|G%Q$Z2^1BoRS?PpqEAscc5@GXp|_vV@$^WlbUkA?_Ok zK?n#V5acTX5fGe&s<}GAQ5OB*z!a`e&iO^CEx1S+l}^!W3g`Ur;{!N`BvZ5jW@%VH?>OF2Tk@O zPGOv@&rGEfX|lv0Cxk2gKu)ie6Af#Vr9x}>!CI+Aiv@szVry$~6u{(al2-hTBEgTzn_D^}jklllIvu1V{Q`ig6OgP|0jI zN)sVEE|=@hm?j7H6PqgYzU5==|fB0<6@J90B?N8); z?B48M`Q6&q<>QYftD|a*tJ$!0YduA;TS}(23t@i9jJ}9E&d>+O-{j}lDtd6mP7wiU?pLh0* zla-TQ!!6f>9b(>jct-Z*@vzVmEjaUp9adYyRH)W#u&{1)0G7#K8z}OOe9Z4J`?k~5 z;u#n4^?R%GdBZDjly!H8xtVMF9ud_Q|CsUp%X4BI?jMd19&&9{QqgG_a)Rz9J*BH| z$zM9cbZYA6R(n(=QYD(cO(#Aoy6CQh;hG<}_gRz&>ZIovmNuT&Z9VwM8m5pu&$kG$ zvTJ!+pA|E6E-UBtJJrv;*XaRo7|Z#x4L(qON`UQa?6`jZqnkg3XliTEuJKo%PCa~M z@WlnE3u1ZRT?c;b@m&$07PGImr1km-TQZ8*DS|rZudw{x4R!5F9=$VOt{XWj(Y>BT zd-yG`a(KJ-o0Dfs8h&U=J*C(_ z=8hNq6aC?^r7wqGy5!v`zvX@KNEDDEpXqBVXiB`Z=eNZRgGG2tG`F;x~xDn9)G1Y@4Fl28Px*E!|ivy@~-8Lx%@`DyQ}?V z4f!BGF*jl}N~1D%!=YeZY6W)9lyDw_Uq#NDJx^=CJZDD2|CF# zA7Ixt{Z7BT8@4fZgFkI{D9fJxang<$JS``+d(*81cbB@prG*c!rZ)8U4y-<__Pt)Z zZ3lJfK;Y5eZHd?A3O-!mWX3$UChhmy)r@4iKkvyz(mdTtF7?TWn4`7t4=} zZ`OLe!fHzEo3eUH7jwVD-n?Xnx$AC<-H6`;RB2iYH9UO}ROfZkPOl32mRZ%`xW#FL zD@GqK${E&#=gzidc(qkxLZ^tk7u}u0Uu|;00}}A@rq4$9xE75>Hwj!4$Nk!`)YmDg{{4HeKCy?7Z85xPzg%Peucca}QJ6#D*z!+`G0ZOj literal 0 HcmV?d00001 diff --git a/applications/external/magspoof/icons/DolphinNice_96x59.png b/applications/external/magspoof/icons/DolphinNice_96x59.png new file mode 100644 index 0000000000000000000000000000000000000000..a299d3630239b4486e249cc501872bed5996df3b GIT binary patch literal 2459 zcmbVO3s4i+8V(M(gEFORwSrA`4O0uPn|M|5y* zB*aMDxC&7(gP9JN;POOi-9khrC>Z9YJs2U!LnVcQEEC0fDtKo&ILlzb30%M}3J^;~ zv7RzcsilOs4Mq@tD*&R;!LMSk2A~{(`HK9|hQBqEX)3sQr9Je6SZU*F-^fD-p+~Hs; zHLkO%v?>ZoxEv+F#whudr%615FkA0DYR0tMEo}3OOY#xecLWe>xV?u5KtSmC^ z7)Fmj6gjfKstiEV-*Cxbbb+&rRWuI_rBJ)ybs_f1Rn&f2>q3pYwI^|J(hdn{j{0EZIm_F zpIyIWLsRUgOItR-dUbVd|6Zo=_BU_Tj4|{{jxO#=JH4o8er(5{!nZD_j4}MH&zh~9 zVLC~y(0-D6GO0ghZD8BYzP?o{>22~lT6^d@X{SwQ8vrNY-PPIMajIwC)`s14Ep72@ zeq7YOzM`?U{+W)ocXBr`eSOcpk?Rxc=ou5&)fWW|pD};-Z0mvk9}=&`Rb&y<77W~a z(>6YM;6Y5aIU~JKZ}mQZynKHiSTQ#Bczn@&jTiN^?vPJ(jhm7cXLx0oum5P$`TceG zU+wR;OO^)8CVlnM)5p$CO&e94KJt>HccCaHGusmW_b`T6m| z-R6V6Db1pErTot?^d22ojm+2>_)FbD`_+WbDGMx9f@hO27maS2`csiV(D&Fs`PS2& zvrq18du_&zXID(!KIxsU$)iuTYuZ?zmYiP&n&i@Be{IdbS-jA2c0QAlu5NXQv_0K< z3Hvs4eeu6B7yD&CNT~gIkMV&UkRU=V!iQ(+_(O&u^ah$+s{_yn(yBYeD40HeU{xGsIT6W Zfq!wOp!Q()N{W z-JN@vFI~T+Y1-v}FWiIo6?k5J;aT|qw)dv2CwcE-scA1=t)FMK&%d~)Y0rO}4EC%2 z=aK4R$F?2(hE6fX7H(UFBH{$t4v4EaKLer_Vi@d&Z#A)C)-lFal?RqJo6XEw%T&e4 zBEIiim|Bz~ut4QeR$2KDgeVQ)Gl9#&Q7)}LS*mHl<@TY>s++4|`B+t|9IGdATYvr+ zL&4Vp^Jy_zlt*w&PGkz$CD@V$zdYy`l2xi0C^cC%YIhY;r^KZCtp`aa)U15HX4E*y zkX5o{K-UPu6k#$T&@w-;?b`$g7%xpD(1BnTyO^;O$?)hRrco61v$A3tm;JC~04Xy` zM9{K5&YYn{cI-;z+O~({*abcDHnS;pNXu(4c!7VY__VG>?Z1?*P#iGU)eM+zGa?B_ zvgI?>CU%T`*JH?wZ6SP@IW~7zXzvsW>>M^Zjasu3fJoi8ARJ`vf+uoa+eOUrBobcB zw>cJ!4w<1pj@wleRYXcabz7&```zwtp@zu>K9qa+w)FmX*CD>+AZijr7d#lMB4r@7 zBxNIM<=Lo~JFR)Z8qvz(k!=8Gk?gq@8g zfS#k0rCF(l)r=K#a|A8Qr4h!%R?w1c= z{pq*skHW8}pZxgsP>68$^3NZ9>0PQ& Cw>kg- literal 0 HcmV?d00001 diff --git a/applications/external/magspoof/icons/KeyBackspace_16x9.png b/applications/external/magspoof/icons/KeyBackspace_16x9.png new file mode 100644 index 0000000000000000000000000000000000000000..9946232d953ef1cbfbf0e6754be6645e5ea2747b GIT binary patch literal 1829 zcmcIl&u`pB6gEEvMG+BPN`-{wNT>+Lo*8@XwOcpZ?1t{5I;81J4Y!WR<6SFjkFlNX zCgRjnK|!3jpo$Y0E}Xb=;LeTzpnm{T)f-4i;dy_hpfu#tmAsxAzxTcGz4y(`m)l!6 zS1w(-q$tWtuiM#y_bNQEzxE>h|J=PM>Pg=HtW=aY-mae)lh*~S0I8^$I!Q-a=}mlXitE9+UN$s!YEtd_TB{DI?graxTNXmKb&NR1RCQdP z*p_AEk5q~&HgLlr6cO9QmPZ_Q{?i~@5yjq4=i_-SnEBeUs&daT#^bR*Hg#DH4C1=3 zfvG_$0t-|gW)+*DtXx|lbVSLEB(D;gsWl=C<$mRBz;u>EnlE9qa$Y7Vm@#3wL3CWF zv@i^U^G(xqX_e|ijf0zqnN0f5E;9~PYWYyXtSU!}MEQj(L+?JpJ#W3Q_ zfcbtgnwBTxh8T$yuuHHdQ+~PEE(EJ&(U)?xXw>#1qDqNQ)vI@tERy5$gPPIYL3CIp zd=0ur5T*!|K7p3G9x*>8*u!{c8h{QWRntB?yEl08lWCYa({L}SbyS-h=I2pl*a_8oT+S_c~#IXiq#zs}!z^4spCcOR_0+*o_q`N6;X{rjK1`QsBs`Gcx|z4v#k QTVG_o&8^N)8~5)21Ktij=l}o! literal 0 HcmV?d00001 diff --git a/applications/external/magspoof/icons/KeySaveSelected_24x11.png b/applications/external/magspoof/icons/KeySaveSelected_24x11.png new file mode 100644 index 0000000000000000000000000000000000000000..eeb3569d3accc5a5c56829b12c85079172b56729 GIT binary patch literal 1853 zcmcIlPiW*+7#}^zw%YZuf+B)3d$6*;fxiXAXK-X$#&ks`?Z1FR>QX z26aVbT~%`&N5w=X1OWo&yGcQZD9KMx7+O3JvM4Pgkw_&Y^~HA4kU?pcLYz)%lYCqz zD405=sj4ZsOlbo2yrZFUJVocl(hfu!>phe>@9d^rUFW&j&H}!)!;|9lBv{%Lg~)s2 z4%()#|Dlit(}3xA)*qFJ1uF0J7`Su5Y9oEA+srsEMAi|aKWWt3B%(w#g-G)oQNqL^ zf1*@0Ucg(l;0+nNrXfra);gN*63r#X84bG_S5Oapz-U2_2No;}caH=0Jhz?X1x*6p zZZ%{Or9=^P?a(oNj2+}NPLO5lb>xS(hK#yzQ(Fto(z;~|u)ZaN?XnW(`pULU1i&$^ zrW-ON3~kFtm|7MJL)}ES1tUU3N-T@l>$)*vdoGLM%c1>)tfeXjj8tQ`U&jXGx~+pC zJw&zve}0HDBg6^Jz?Y@lahswqGEXq5ZvEhVyV+dJL>TqqMZUhgD7BZGrskL?B8nzU zEO0}S#T1Md#k9-SH0hSMuhLzKa_I5y_(QtDUmB14ku-9rOM~*GXvjh71`cJarlUj3 ze7uCJ^@AP<(j#0_!EzB61Df%LF0|x7U8vqkd`@?cmVP{k{EyPdWes{X>2la%Rk=(? zE%&0TDeAxbb=w#db1i`F%Wmf5GAz>Wv>@jW_p)ho-#0CeC9cGbyZ*s9Cn^o)Rq=_$h#NIZix%+wt GFa8a7)lmol literal 0 HcmV?d00001 diff --git a/applications/external/magspoof/icons/KeySave_24x11.png b/applications/external/magspoof/icons/KeySave_24x11.png new file mode 100644 index 0000000000000000000000000000000000000000..e7dba987a04dad7dd96001913c55566dbde96c8e GIT binary patch literal 1863 zcmcIlO^6&t6dpxnjjSF75f9pQJZy|LUDdzSZ6$n-*6-2D5s-9_fx~uK( zotfQBVDX?Q(UXLzXF6aX)%U*l z-d9y`w^q+Do_PF3p-@5qOL5h2N9RU z^i-~BIziNECdw*wjUcQeOxncsbnKa>(*%1MPoPck0jC)~9$50g-#!ks+4LGwn$d`f zMy;%ZsA3Rsk2!`#ELuW>2#g3fF>;CFBHMCo-El0(@X1&g%&$qdl~*F4Kd~*B3^?Z1 z^bEmDf}0)Wn??sYC6l9$X;6esLO7#_ZCmDy?ZqU3l|%anS#wn!7%f39-Qp(l9fyJ- z(?=x}n~2%2PcX9#VmYdECvH{tWzv)!s%sn^Z&a(TMEXG=KBQ~smzBm!)h4cOBfSV| zapw6l2`LyY2x(Vnan#Li4>BO#dXPeox2Fr~f_P*4)DM)gJ3Y$sMNw8+?gqit>2PpJ znU9yygm%~yKzf8rCa_fc*^nlp(uJ1%rwg^aiBIX^Xz9mu$p0vPT2|JhQCGkYtEqW1 zTD})enxg%?Uw4c#Ggk#{pLa8zmSLH8=LI=?xR>pc=yYsHAgcQUxz^arx`9fR>e$sw zj@}Uy75!kQXF{tT9e=F+z^*!*3|n>nI6oucWq!(t2og`=40-O!tAE1z^ID@;X)nEd zVK7xqj`wg%Ed2Op{k=a*YZpKHX787$ zzhKuN9(?M5ojmn%xcU1}OP>$^`ZD?cW@lIaTn=x3xBtP#z16o~{qVMZ>hecsD?jQQ ME3387mS5lf8`39Il>h($ literal 0 HcmV?d00001 diff --git a/applications/external/magspoof/icons/mag_10px.png b/applications/external/magspoof/icons/mag_10px.png new file mode 100644 index 0000000000000000000000000000000000000000..5e4c152444bbc543f94cc7911ab298209fd8865e GIT binary patch literal 4847 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 Tdlgk|00000NkvXXu0mjfBn6wm literal 0 HcmV?d00001 diff --git a/applications/external/magspoof/mag.c b/applications/external/magspoof/mag.c new file mode 100644 index 000000000..ce4a46be8 --- /dev/null +++ b/applications/external/magspoof/mag.c @@ -0,0 +1,244 @@ +#include "mag_i.h" + +#define TAG "Mag" + +#define SETTING_DEFAULT_REVERSE MagReverseStateOff +#define SETTING_DEFAULT_TRACK MagTrackStateOneAndTwo +#define SETTING_DEFAULT_TX_RFID MagTxStateGPIO +#define SETTING_DEFAULT_US_CLOCK 240 +#define SETTING_DEFAULT_US_INTERPACKET 10 + +static bool mag_debug_custom_event_callback(void* context, uint32_t event) { + furi_assert(context); + Mag* mag = context; + return scene_manager_handle_custom_event(mag->scene_manager, event); +} + +static bool mag_debug_back_event_callback(void* context) { + furi_assert(context); + Mag* mag = context; + return scene_manager_handle_back_event(mag->scene_manager); +} + +static MagSetting* mag_setting_alloc() { + // temp hardcoded defaults + MagSetting* setting = malloc(sizeof(MagSetting)); + setting->reverse = SETTING_DEFAULT_REVERSE; + setting->track = SETTING_DEFAULT_TRACK; + setting->tx = SETTING_DEFAULT_TX_RFID; + setting->us_clock = SETTING_DEFAULT_US_CLOCK; + setting->us_interpacket = SETTING_DEFAULT_US_INTERPACKET; + + return setting; +} + +static Mag* mag_alloc() { + Mag* mag = malloc(sizeof(Mag)); + + mag->storage = furi_record_open(RECORD_STORAGE); + mag->dialogs = furi_record_open(RECORD_DIALOGS); + + mag->file_name = furi_string_alloc(); + mag->file_path = furi_string_alloc_set(MAG_APP_FOLDER); + + mag->view_dispatcher = view_dispatcher_alloc(); + mag->scene_manager = scene_manager_alloc(&mag_scene_handlers, mag); + view_dispatcher_enable_queue(mag->view_dispatcher); + view_dispatcher_set_event_callback_context(mag->view_dispatcher, mag); + view_dispatcher_set_custom_event_callback( + mag->view_dispatcher, mag_debug_custom_event_callback); + view_dispatcher_set_navigation_event_callback( + mag->view_dispatcher, mag_debug_back_event_callback); + + mag->mag_dev = mag_device_alloc(); + mag->setting = mag_setting_alloc(); + + // Open GUI record + mag->gui = furi_record_open(RECORD_GUI); + + // Open Notification record + mag->notifications = furi_record_open(RECORD_NOTIFICATION); + + // Submenu + mag->submenu = submenu_alloc(); + view_dispatcher_add_view(mag->view_dispatcher, MagViewSubmenu, submenu_get_view(mag->submenu)); + + // Dialog + mag->dialog_ex = dialog_ex_alloc(); + view_dispatcher_add_view( + mag->view_dispatcher, MagViewDialogEx, dialog_ex_get_view(mag->dialog_ex)); + + // Popup + mag->popup = popup_alloc(); + view_dispatcher_add_view(mag->view_dispatcher, MagViewPopup, popup_get_view(mag->popup)); + + // Loading + mag->loading = loading_alloc(); + view_dispatcher_add_view(mag->view_dispatcher, MagViewLoading, loading_get_view(mag->loading)); + + // Widget + mag->widget = widget_alloc(); + view_dispatcher_add_view(mag->view_dispatcher, MagViewWidget, widget_get_view(mag->widget)); + + // Variable Item List + mag->variable_item_list = variable_item_list_alloc(); + view_dispatcher_add_view( + mag->view_dispatcher, + MagViewVariableItemList, + variable_item_list_get_view(mag->variable_item_list)); + + // Text Input + mag->text_input = text_input_alloc(); + view_dispatcher_add_view( + mag->view_dispatcher, MagViewTextInput, text_input_get_view(mag->text_input)); + + // Custom Mag Text Input + mag->mag_text_input = mag_text_input_alloc(); + view_dispatcher_add_view( + mag->view_dispatcher, MagViewMagTextInput, mag_text_input_get_view(mag->mag_text_input)); + + return mag; +} + +static void mag_setting_free(MagSetting* setting) { + furi_assert(setting); + + free(setting); +} + +static void mag_free(Mag* mag) { + furi_assert(mag); + + furi_string_free(mag->file_name); + furi_string_free(mag->file_path); + + // Mag device + mag_device_free(mag->mag_dev); + mag->mag_dev = NULL; + + // Mag setting + mag_setting_free(mag->setting); + mag->setting = NULL; + + // Submenu + view_dispatcher_remove_view(mag->view_dispatcher, MagViewSubmenu); + submenu_free(mag->submenu); + + // DialogEx + view_dispatcher_remove_view(mag->view_dispatcher, MagViewDialogEx); + dialog_ex_free(mag->dialog_ex); + + // Popup + view_dispatcher_remove_view(mag->view_dispatcher, MagViewPopup); + popup_free(mag->popup); + + // Loading + view_dispatcher_remove_view(mag->view_dispatcher, MagViewLoading); + loading_free(mag->loading); + + // Widget + view_dispatcher_remove_view(mag->view_dispatcher, MagViewWidget); + widget_free(mag->widget); + + // Variable Item List + view_dispatcher_remove_view(mag->view_dispatcher, MagViewVariableItemList); + variable_item_list_free(mag->variable_item_list); + + // TextInput + view_dispatcher_remove_view(mag->view_dispatcher, MagViewTextInput); + text_input_free(mag->text_input); + + // Custom Mag TextInput + view_dispatcher_remove_view(mag->view_dispatcher, MagViewMagTextInput); + mag_text_input_free(mag->mag_text_input); + + // View Dispatcher + view_dispatcher_free(mag->view_dispatcher); + + // Scene Manager + scene_manager_free(mag->scene_manager); + + // GUI + furi_record_close(RECORD_GUI); + mag->gui = NULL; + + // Notifications + furi_record_close(RECORD_NOTIFICATION); + mag->notifications = NULL; + + furi_record_close(RECORD_STORAGE); + furi_record_close(RECORD_DIALOGS); + + free(mag); +} + +// entry point for app +int32_t mag_app(void* p) { + Mag* mag = mag_alloc(); + UNUSED(p); + + mag_make_app_folder(mag); + + view_dispatcher_attach_to_gui(mag->view_dispatcher, mag->gui, ViewDispatcherTypeFullscreen); + scene_manager_next_scene(mag->scene_manager, MagSceneStart); + + view_dispatcher_run(mag->view_dispatcher); + + mag_free(mag); + + return 0; +} + +void mag_make_app_folder(Mag* mag) { + furi_assert(mag); + + if(!storage_simply_mkdir(mag->storage, MAG_APP_FOLDER)) { + dialog_message_show_storage_error(mag->dialogs, "Cannot create\napp folder"); + } +} + +void mag_text_store_set(Mag* mag, const char* text, ...) { + furi_assert(mag); + va_list args; + va_start(args, text); + + vsnprintf(mag->text_store, MAG_TEXT_STORE_SIZE, text, args); + + va_end(args); +} + +void mag_text_store_clear(Mag* mag) { + furi_assert(mag); + memset(mag->text_store, 0, sizeof(mag->text_store)); +} + +void mag_popup_timeout_callback(void* context) { + Mag* mag = context; + view_dispatcher_send_custom_event(mag->view_dispatcher, MagEventPopupClosed); +} + +void mag_widget_callback(GuiButtonType result, InputType type, void* context) { + Mag* mag = context; + if(type == InputTypeShort) { + view_dispatcher_send_custom_event(mag->view_dispatcher, result); + } +} + +void mag_text_input_callback(void* context) { + Mag* mag = context; + view_dispatcher_send_custom_event(mag->view_dispatcher, MagEventNext); +} + +void mag_show_loading_popup(void* context, bool show) { + Mag* mag = context; + TaskHandle_t timer_task = xTaskGetHandle(configTIMER_SERVICE_TASK_NAME); + + if(show) { + // Raise timer priority so that animations can play + vTaskPrioritySet(timer_task, configMAX_PRIORITIES - 1); + view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewLoading); + } else { + // Restore default timer priority + vTaskPrioritySet(timer_task, configTIMER_TASK_PRIORITY); + } +} diff --git a/applications/external/magspoof/mag_device.c b/applications/external/magspoof/mag_device.c new file mode 100644 index 000000000..f3be3a5ff --- /dev/null +++ b/applications/external/magspoof/mag_device.c @@ -0,0 +1,311 @@ +#include "mag_device.h" + +#include +#include + +#define TAG "MagDevice" + +static const char* mag_file_header = "Flipper Mag device"; +static const uint32_t mag_file_version = 1; + +MagDevice* mag_device_alloc() { + MagDevice* mag_dev = malloc(sizeof(MagDevice)); + mag_dev->dev_data.track[0].str = furi_string_alloc(); + mag_dev->dev_data.track[1].str = furi_string_alloc(); + mag_dev->dev_data.track[2].str = furi_string_alloc(); + mag_dev->storage = furi_record_open(RECORD_STORAGE); + mag_dev->dialogs = furi_record_open(RECORD_DIALOGS); + mag_dev->load_path = furi_string_alloc(); + return mag_dev; +} + +void mag_device_data_clear(MagDeviceData* dev_data) { + furi_string_reset(dev_data->track[0].str); + furi_string_reset(dev_data->track[1].str); + furi_string_reset(dev_data->track[2].str); +} + +void mag_device_clear(MagDevice* mag_dev) { + furi_assert(mag_dev); + + mag_device_data_clear(&mag_dev->dev_data); + memset(&mag_dev->dev_data, 0, sizeof(mag_dev->dev_data)); + furi_string_reset(mag_dev->load_path); +} + +void mag_device_free(MagDevice* mag_dev) { + furi_assert(mag_dev); + + mag_device_clear(mag_dev); + furi_record_close(RECORD_STORAGE); + furi_record_close(RECORD_DIALOGS); + furi_string_free(mag_dev->load_path); + + //furi_string_free(mag_dev->dev_data.track[0].str); + //furi_string_free(mag_dev->dev_data.track[1].str); + //furi_string_free(mag_dev->dev_data.track[2].str); + + free(mag_dev); +} + +void mag_device_set_name(MagDevice* mag_dev, const char* name) { + furi_assert(mag_dev); + + strlcpy(mag_dev->dev_name, name, MAG_DEV_NAME_MAX_LEN); +} + +static bool mag_device_save_file( + MagDevice* mag_dev, + const char* dev_name, + const char* folder, + const char* extension, + bool use_load_path) { + furi_assert(mag_dev); + + bool saved = false; + FlipperFormat* file = flipper_format_file_alloc(mag_dev->storage); + FuriString* temp_str; + temp_str = furi_string_alloc(); + + do { + if(use_load_path && !furi_string_empty(mag_dev->load_path)) { + // Get dir name + path_extract_dirname(furi_string_get_cstr(mag_dev->load_path), temp_str); + // Create mag directory if necessary + if(!storage_simply_mkdir((mag_dev->storage), furi_string_get_cstr(temp_str))) break; + // Make path to file to be saved + furi_string_cat_printf(temp_str, "/%s%s", dev_name, extension); + } else { + // Create mag directory if necessary + if(!storage_simply_mkdir((mag_dev->storage), MAG_APP_FOLDER)) break; + // First remove mag device file if it was saved + furi_string_printf(temp_str, "%s/%s%s", folder, dev_name, extension); + } + // Open file + if(!flipper_format_file_open_always(file, furi_string_get_cstr(temp_str))) break; + + // Write header + if(!flipper_format_write_header_cstr(file, mag_file_header, mag_file_version)) break; + + // Write comment + if(!flipper_format_write_comment_cstr(file, "Mag device track data")) break; + + // Write data + for(uint8_t i = 0; i < MAG_DEV_TRACKS; i++) { + furi_string_printf(temp_str, "Track %d", i + 1); + if(!flipper_format_write_string_cstr( + file, + furi_string_get_cstr(temp_str), + furi_string_get_cstr(mag_dev->dev_data.track[i].str))) + break; + } + + saved = true; + } while(0); + + if(!saved) { + dialog_message_show_storage_error(mag_dev->dialogs, "Cannot save\nfile"); + } + + furi_string_free(temp_str); + flipper_format_free(file); + + return saved; +} + +bool mag_device_save(MagDevice* mag_dev, const char* dev_name) { + // wrapping function in the event we have multiple formats + return mag_device_save_file(mag_dev, dev_name, MAG_APP_FOLDER, MAG_APP_EXTENSION, true); +} + +static bool mag_device_load_data(MagDevice* mag_dev, FuriString* path, bool show_dialog) { + bool parsed = false; + + FlipperFormat* file = flipper_format_file_alloc(mag_dev->storage); + FuriString* temp_str; + temp_str = furi_string_alloc(); + bool deprecated_version = false; + bool data_read = true; + + if(mag_dev->loading_cb) { + mag_dev->loading_cb(mag_dev->loading_cb_ctx, true); + } + + do { + if(!flipper_format_file_open_existing(file, furi_string_get_cstr(path))) break; + + // Read and verify header, check file version + uint32_t version; + if(!flipper_format_read_header(file, temp_str, &version)) break; + if(furi_string_cmp_str(temp_str, mag_file_header) || (version != mag_file_version)) { + deprecated_version = true; + break; + } + + // Parse data + for(uint8_t i = 0; i < MAG_DEV_TRACKS; i++) { + furi_string_printf(temp_str, "Track %d", i + 1); + if(!flipper_format_read_string( + file, furi_string_get_cstr(temp_str), mag_dev->dev_data.track[i].str)) { + FURI_LOG_D(TAG, "Could not read track %d data", i + 1); + + // TODO: smarter load handling now that it is acceptible for some tracks to be empty + data_read = false; + } + } + + parsed = true; + } while(false); + + if((!parsed) && (show_dialog)) { + if(deprecated_version) { + dialog_message_show_storage_error(mag_dev->dialogs, "File format\ndeprecated"); + } else if(!data_read) { + dialog_message_show_storage_error(mag_dev->dialogs, "Cannot read\ndata"); + } else { + dialog_message_show_storage_error(mag_dev->dialogs, "Cannot parse\nfile"); + } + } + + furi_string_free(temp_str); + flipper_format_free(file); + + return parsed; +} + +bool mag_file_select(MagDevice* mag_dev) { + furi_assert(mag_dev); + + // Input events and views are managed by file_browser + FuriString* mag_app_folder; + mag_app_folder = furi_string_alloc_set(MAG_APP_FOLDER); + + DialogsFileBrowserOptions browser_options; + dialog_file_browser_set_basic_options(&browser_options, MAG_APP_EXTENSION, &I_mag_file_10px); + browser_options.base_path = MAG_APP_FOLDER; + + bool res = dialog_file_browser_show( + mag_dev->dialogs, mag_dev->load_path, mag_app_folder, &browser_options); + + furi_string_free(mag_app_folder); + if(res) { + FuriString* filename; + filename = furi_string_alloc(); + path_extract_filename(mag_dev->load_path, filename, true); + strncpy(mag_dev->dev_name, furi_string_get_cstr(filename), MAG_DEV_NAME_MAX_LEN); + res = mag_device_load_data(mag_dev, mag_dev->load_path, true); + if(res) { + mag_device_set_name(mag_dev, mag_dev->dev_name); + } + furi_string_free(filename); + } + + return res; +} + +bool mag_device_delete(MagDevice* mag_dev, bool use_load_path) { + furi_assert(mag_dev); + + bool deleted = false; + FuriString* file_path; + file_path = furi_string_alloc(); + + do { + // Delete original file + if(use_load_path && !furi_string_empty(mag_dev->load_path)) { + furi_string_set(file_path, mag_dev->load_path); + } else { + furi_string_printf( + file_path, "%s/%s%s", MAG_APP_FOLDER, mag_dev->dev_name, MAG_APP_EXTENSION); + } + if(!storage_simply_remove(mag_dev->storage, furi_string_get_cstr(file_path))) break; + deleted = true; + } while(false); + + if(!deleted) { + dialog_message_show_storage_error(mag_dev->dialogs, "Cannot remove\nfile"); + } + + furi_string_free(file_path); + return deleted; +} + +bool mag_device_parse_card_string(MagDevice* mag_dev, FuriString* f_card_str) { + furi_assert(mag_dev); + FURI_LOG_D(TAG, "mag_device_parse_card_string"); + + const char* card_str = furi_string_get_cstr(f_card_str); + + FURI_LOG_D(TAG, "Parsing card string: %s", card_str); + + // Track 1 + const char* track1_start = strchr(card_str, '%'); + if(!track1_start) { + FURI_LOG_D(TAG, "Could not find track 1 start"); + return false; + } + track1_start++; + const char* track1_end = strchr(track1_start, '?'); + if(!track1_end) { + FURI_LOG_D(TAG, "Could not find track 1 end"); + return false; + } + size_t track1_len = track1_end - track1_start; + + FURI_LOG_D(TAG, "Track 1: %.*s", track1_len, track1_start); + + mag_dev->dev_data.track[0].len = track1_len; + furi_string_printf(mag_dev->dev_data.track[0].str, "%%%.*s?", track1_len, track1_start); + + // Track 2 + const char* track2_start = strchr(track1_end, ';'); + if(!track2_start) { + FURI_LOG_D(TAG, "Could not find track 2 start"); + return true; + } + + track2_start++; + const char* track2_end = strchr(track2_start, '?'); + if(!track2_end) { + FURI_LOG_D(TAG, "Could not find track 2 end"); + return true; + } + size_t track2_len = track2_end - track2_start; + + FURI_LOG_D(TAG, "Track 2: %.*s", track2_len, track2_start); + + mag_dev->dev_data.track[1].len = track2_len; + furi_string_printf(mag_dev->dev_data.track[1].str, "%%%.*s?", track2_len, track2_start); + + // Track 3 + const char* track3_start = strchr(track2_end, ';'); + if(!track3_start) { + FURI_LOG_D(TAG, "Could not find track 3 start"); + return true; + } + + track3_start++; + const char* track3_end = strchr(track3_start, '?'); + if(!track3_end) { + FURI_LOG_D(TAG, "Could not find track 3 end"); + return true; + } + size_t track3_len = track3_end - track3_start; + + FURI_LOG_D(TAG, "Track 3: %.*s", track3_len, track3_start); + + mag_dev->dev_data.track[2].len = track3_len; + furi_string_printf(mag_dev->dev_data.track[2].str, "%%%.*s?", track3_len, track3_start); + + return true; +} + +void mag_device_set_loading_callback( + MagDevice* mag_dev, + MagLoadingCallback callback, + void* context) { + furi_assert(mag_dev); + + mag_dev->loading_cb = callback; + mag_dev->loading_cb_ctx = context; +} diff --git a/applications/external/magspoof/mag_device.h b/applications/external/magspoof/mag_device.h new file mode 100644 index 000000000..ca6715d99 --- /dev/null +++ b/applications/external/magspoof/mag_device.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include +#include + +#include "magspoof_icons.h" + +#define MAG_DEV_NAME_MAX_LEN 22 +#define MAG_DEV_TRACKS 3 + +#define MAG_APP_FOLDER ANY_PATH("mag") +#define MAG_APP_EXTENSION ".mag" + +typedef void (*MagLoadingCallback)(void* context, bool state); + +typedef struct { + FuriString* str; + size_t len; +} MagTrack; + +typedef struct { + MagTrack track[MAG_DEV_TRACKS]; +} MagDeviceData; + +typedef struct { + Storage* storage; + DialogsApp* dialogs; + MagDeviceData dev_data; + char dev_name[MAG_DEV_NAME_MAX_LEN + 1]; + FuriString* load_path; + MagLoadingCallback loading_cb; + void* loading_cb_ctx; +} MagDevice; + +MagDevice* mag_device_alloc(); + +void mag_device_free(MagDevice* mag_dev); + +void mag_device_set_name(MagDevice* mag_dev, const char* name); + +bool mag_device_save(MagDevice* mag_dev, const char* dev_name); + +bool mag_file_select(MagDevice* mag_dev); + +void mag_device_data_clear(MagDeviceData* dev_data); + +void mag_device_clear(MagDevice* mag_dev); + +bool mag_device_delete(MagDevice* mag_dev, bool use_load_path); + +bool mag_device_parse_card_string(MagDevice* mag_dev, FuriString* card_str); + +void mag_device_set_loading_callback( + MagDevice* mag_dev, + MagLoadingCallback callback, + void* context); diff --git a/applications/external/magspoof/mag_i.h b/applications/external/magspoof/mag_i.h new file mode 100644 index 000000000..fcf9ccd57 --- /dev/null +++ b/applications/external/magspoof/mag_i.h @@ -0,0 +1,105 @@ +#pragma once + +#include "mag_device.h" +//#include "helpers/mag_helpers.h" +#include "helpers/mag_text_input.h" +#include "helpers/mag_types.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include "scenes/mag_scene.h" +#include "scenes/mag_scene_read.h" + +#define MAG_TEXT_STORE_SIZE 150 + +enum MagCustomEvent { + MagEventNext = 100, + MagEventExit, + MagEventPopupClosed, +}; + +typedef struct { + MagTxState tx; + MagTrackState track; + MagReverseState reverse; + uint32_t us_clock; + uint32_t us_interpacket; +} MagSetting; + +typedef struct { + ViewDispatcher* view_dispatcher; + Gui* gui; + NotificationApp* notifications; + SceneManager* scene_manager; + Storage* storage; + DialogsApp* dialogs; + MagDevice* mag_dev; + + char text_store[MAG_TEXT_STORE_SIZE + 1]; + FuriString* file_path; + FuriString* file_name; + + MagSetting* setting; + + // Common views + Submenu* submenu; + DialogEx* dialog_ex; + Popup* popup; + Loading* loading; + TextInput* text_input; + Widget* widget; + VariableItemList* variable_item_list; + + // Custom views + Mag_TextInput* mag_text_input; + + // UART + FuriThread* uart_rx_thread; + FuriStreamBuffer* uart_rx_stream; + uint8_t uart_rx_buf[UART_RX_BUF_SIZE + 1]; + void (*handle_rx_data_cb)(uint8_t* buf, size_t len, void* context); + + char uart_text_input_store[UART_TERMINAL_TEXT_INPUT_STORE_SIZE + 1]; + FuriString* uart_text_box_store; + size_t uart_text_box_store_strlen; + // UART_TextInput* text_input; +} Mag; + +void mag_text_store_set(Mag* mag, const char* text, ...); + +void mag_text_store_clear(Mag* mag); + +void mag_show_loading_popup(void* context, bool show); + +void mag_make_app_folder(Mag* mag); + +void mag_popup_timeout_callback(void* context); + +void mag_widget_callback(GuiButtonType result, InputType type, void* context); + +void mag_text_input_callback(void* context); diff --git a/applications/external/magspoof/scenes/mag_scene.c b/applications/external/magspoof/scenes/mag_scene.c new file mode 100644 index 000000000..61d847add --- /dev/null +++ b/applications/external/magspoof/scenes/mag_scene.c @@ -0,0 +1,30 @@ +#include "mag_scene.h" + +// Generate scene on_enter handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter, +void (*const mag_on_enter_handlers[])(void*) = { +#include "mag_scene_config.h" +}; +#undef ADD_SCENE + +// Generate scene on_event handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event, +bool (*const mag_on_event_handlers[])(void* context, SceneManagerEvent event) = { +#include "mag_scene_config.h" +}; +#undef ADD_SCENE + +// Generate scene on_exit handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit, +void (*const mag_on_exit_handlers[])(void* context) = { +#include "mag_scene_config.h" +}; +#undef ADD_SCENE + +// Initialize scene handlers configuration structure +const SceneManagerHandlers mag_scene_handlers = { + .on_enter_handlers = mag_on_enter_handlers, + .on_event_handlers = mag_on_event_handlers, + .on_exit_handlers = mag_on_exit_handlers, + .scene_num = MagSceneNum, +}; \ No newline at end of file diff --git a/applications/external/magspoof/scenes/mag_scene.h b/applications/external/magspoof/scenes/mag_scene.h new file mode 100644 index 000000000..0f61112b9 --- /dev/null +++ b/applications/external/magspoof/scenes/mag_scene.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +// Generate scene id and total number +#define ADD_SCENE(prefix, name, id) MagScene##id, +typedef enum { +#include "mag_scene_config.h" + MagSceneNum, +} MagScene; +#undef ADD_SCENE + +extern const SceneManagerHandlers mag_scene_handlers; + +// Generate scene on_enter handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*); +#include "mag_scene_config.h" +#undef ADD_SCENE + +// Generate scene on_event handlers declaration +#define ADD_SCENE(prefix, name, id) \ + bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event); +#include "mag_scene_config.h" +#undef ADD_SCENE + +// Generate scene on_exit handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context); +#include "mag_scene_config.h" +#undef ADD_SCENE diff --git a/applications/external/magspoof/scenes/mag_scene_about.c b/applications/external/magspoof/scenes/mag_scene_about.c new file mode 100644 index 000000000..247c83199 --- /dev/null +++ b/applications/external/magspoof/scenes/mag_scene_about.c @@ -0,0 +1,40 @@ +#include "../mag_i.h" + +void mag_scene_about_on_enter(void* context) { + Mag* mag = context; + Widget* widget = mag->widget; + + FuriString* tmp_str; + tmp_str = furi_string_alloc(); + + furi_string_cat_printf(tmp_str, "Version: %s\n", MAG_VERSION_APP); + furi_string_cat_printf(tmp_str, "Developer: %s\n", MAG_DEVELOPER); + furi_string_cat_printf(tmp_str, "GitHub: %s\n\n", MAG_GITHUB); + + furi_string_cat_printf( + tmp_str, + "Unfinished port of Samy Kamkar's MagSpoof. Confer GitHub for updates; in the interim, use responsibly and at your own risk."); + + // TODO: Add credits + + widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(tmp_str)); + furi_string_free(tmp_str); + + view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewWidget); +} + +bool mag_scene_about_on_event(void* context, SceneManagerEvent event) { + Mag* mag = context; + SceneManager* scene_manager = mag->scene_manager; + bool consumed = false; + + UNUSED(event); + UNUSED(scene_manager); + + return consumed; +} + +void mag_scene_about_on_exit(void* context) { + Mag* mag = context; + widget_reset(mag->widget); +} \ No newline at end of file diff --git a/applications/external/magspoof/scenes/mag_scene_config.h b/applications/external/magspoof/scenes/mag_scene_config.h new file mode 100644 index 000000000..7ab276e53 --- /dev/null +++ b/applications/external/magspoof/scenes/mag_scene_config.h @@ -0,0 +1,15 @@ +ADD_SCENE(mag, start, Start) +ADD_SCENE(mag, about, About) +ADD_SCENE(mag, emulate, Emulate) +ADD_SCENE(mag, emulate_config, EmulateConfig) +ADD_SCENE(mag, file_select, FileSelect) +ADD_SCENE(mag, saved_menu, SavedMenu) +ADD_SCENE(mag, saved_info, SavedInfo) +ADD_SCENE(mag, input_name, InputName) +ADD_SCENE(mag, input_value, InputValue) +ADD_SCENE(mag, save_success, SaveSuccess) +ADD_SCENE(mag, delete_success, DeleteSuccess) +ADD_SCENE(mag, delete_confirm, DeleteConfirm) +ADD_SCENE(mag, exit_confirm, ExitConfirm) +ADD_SCENE(mag, under_construction, UnderConstruction) +ADD_SCENE(mag, read, Read) \ No newline at end of file diff --git a/applications/external/magspoof/scenes/mag_scene_delete_confirm.c b/applications/external/magspoof/scenes/mag_scene_delete_confirm.c new file mode 100644 index 000000000..b7349ba70 --- /dev/null +++ b/applications/external/magspoof/scenes/mag_scene_delete_confirm.c @@ -0,0 +1,49 @@ +#include "../mag_i.h" +#include "../mag_device.h" + +void mag_scene_delete_confirm_on_enter(void* context) { + Mag* mag = context; + Widget* widget = mag->widget; + MagDevice* mag_dev = mag->mag_dev; + + FuriString* tmp_str; + tmp_str = furi_string_alloc(); + + furi_string_printf(tmp_str, "\e#Delete %s?\e#", mag_dev->dev_name); + + //TODO: print concise summary of data on card? Would need to vary by card/track type + + widget_add_text_box_element( + widget, 0, 0, 128, 27, AlignCenter, AlignCenter, furi_string_get_cstr(tmp_str), true); + widget_add_button_element(widget, GuiButtonTypeLeft, "Cancel", mag_widget_callback, mag); + widget_add_button_element(widget, GuiButtonTypeRight, "Delete", mag_widget_callback, mag); + + view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewWidget); + + furi_string_free(tmp_str); +} + +bool mag_scene_delete_confirm_on_event(void* context, SceneManagerEvent event) { + Mag* mag = context; + SceneManager* scene_manager = mag->scene_manager; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == GuiButtonTypeRight) { + consumed = true; + if(mag_device_delete(mag->mag_dev, true)) { + scene_manager_next_scene(scene_manager, MagSceneDeleteSuccess); + } + } else if(event.event == GuiButtonTypeLeft) { + consumed = true; + scene_manager_previous_scene(scene_manager); + } + } + + return consumed; +} + +void mag_scene_delete_confirm_on_exit(void* context) { + Mag* mag = context; + widget_reset(mag->widget); +} diff --git a/applications/external/magspoof/scenes/mag_scene_delete_success.c b/applications/external/magspoof/scenes/mag_scene_delete_success.c new file mode 100644 index 000000000..ca7dbbbf7 --- /dev/null +++ b/applications/external/magspoof/scenes/mag_scene_delete_success.c @@ -0,0 +1,39 @@ +#include "../mag_i.h" + +void mag_scene_delete_success_on_enter(void* context) { + Mag* mag = context; + Popup* popup = mag->popup; + + popup_set_icon(popup, 0, 2, &I_DolphinMafia_115x62); + popup_set_header(popup, "Deleted", 83, 19, AlignLeft, AlignBottom); + + popup_set_callback(popup, mag_popup_timeout_callback); + popup_set_context(popup, mag); + popup_set_timeout(popup, 1500); + popup_enable_timeout(popup); + + view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewPopup); +} + +bool mag_scene_delete_success_on_event(void* context, SceneManagerEvent event) { + Mag* mag = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == MagEventPopupClosed) { + consumed = true; + + scene_manager_search_and_switch_to_previous_scene( + mag->scene_manager, MagSceneFileSelect); + } + } + + return consumed; +} + +void mag_scene_delete_success_on_exit(void* context) { + Mag* mag = context; + Popup* popup = mag->popup; + + popup_reset(popup); +} \ No newline at end of file diff --git a/applications/external/magspoof/scenes/mag_scene_emulate.c b/applications/external/magspoof/scenes/mag_scene_emulate.c new file mode 100644 index 000000000..e7e8737eb --- /dev/null +++ b/applications/external/magspoof/scenes/mag_scene_emulate.c @@ -0,0 +1,93 @@ +#include "../mag_i.h" +#include "../helpers/mag_helpers.h" + +#define TAG "MagSceneEmulate" + +void cat_trackstr(FuriString* str, uint8_t calls, uint8_t i, FuriString* trackstr) { + furi_string_cat_printf( + str, + "%sTrack %d:%s%s\n", + (calls == 0) ? "" : "\n", // if first line, don't prepend a "\n" + (i + 1), + furi_string_empty(trackstr) ? " " : "\n", + furi_string_empty(trackstr) ? "< empty >" : furi_string_get_cstr(trackstr)); +} + +void mag_scene_emulate_on_enter(void* context) { + Mag* mag = context; + Widget* widget = mag->widget; + + FuriString* tmp_str; + tmp_str = furi_string_alloc(); + + // Use strlcpy instead perhaps, to truncate to screen width, then add ellipses if needed? + furi_string_printf(tmp_str, "%s\r\n", mag->mag_dev->dev_name); + + // TODO: Display other relevant config settings (namely RFID vs GPIO)? + + widget_add_icon_element(widget, 1, 1, &I_mag_file_10px); + widget_add_string_element( + widget, 13, 2, AlignLeft, AlignTop, FontPrimary, furi_string_get_cstr(tmp_str)); + furi_string_reset(tmp_str); + + FURI_LOG_D(TAG, "%d", mag->setting->reverse); + + // print relevant data + uint8_t cat_count = 0; + for(uint8_t i = 0; i < MAG_DEV_TRACKS; i++) { + FuriString* trackstr = mag->mag_dev->dev_data.track[i].str; + + // still messy / dumb way to do this, but slightly cleaner than before. + // will clean up more later + switch(mag->setting->track) { + case MagTrackStateOne: + if(i == 0) cat_trackstr(tmp_str, cat_count++, i, trackstr); + break; + case MagTrackStateTwo: + if(i == 1) cat_trackstr(tmp_str, cat_count++, i, trackstr); + break; + case MagTrackStateThree: + if(i == 2) cat_trackstr(tmp_str, cat_count++, i, trackstr); + break; + case MagTrackStateOneAndTwo: + if((i == 0) | (i == 1)) cat_trackstr(tmp_str, cat_count++, i, trackstr); + break; + } + } + + widget_add_text_scroll_element(widget, 0, 15, 128, 49, furi_string_get_cstr(tmp_str)); + + widget_add_button_element(widget, GuiButtonTypeLeft, "Config", mag_widget_callback, mag); + widget_add_button_element(widget, GuiButtonTypeRight, "Send", mag_widget_callback, mag); + view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewWidget); + furi_string_free(tmp_str); +} + +bool mag_scene_emulate_on_event(void* context, SceneManagerEvent event) { + Mag* mag = context; + SceneManager* scene_manager = mag->scene_manager; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + switch(event.event) { + case GuiButtonTypeLeft: + consumed = true; + scene_manager_next_scene(scene_manager, MagSceneEmulateConfig); + break; + case GuiButtonTypeRight: + consumed = true; + notification_message(mag->notifications, &sequence_blink_start_cyan); + mag_spoof(mag); + notification_message(mag->notifications, &sequence_blink_stop); + break; + } + } + + return consumed; +} + +void mag_scene_emulate_on_exit(void* context) { + Mag* mag = context; + notification_message(mag->notifications, &sequence_blink_stop); + widget_reset(mag->widget); +} \ No newline at end of file diff --git a/applications/external/magspoof/scenes/mag_scene_emulate_config.c b/applications/external/magspoof/scenes/mag_scene_emulate_config.c new file mode 100644 index 000000000..437f536a7 --- /dev/null +++ b/applications/external/magspoof/scenes/mag_scene_emulate_config.c @@ -0,0 +1,264 @@ +#include "../mag_i.h" + +#define TAG "MagSceneEmulateConfig" + +enum MagSettingIndex { + MagSettingIndexTx, + MagSettingIndexTrack, + MagSettingIndexReverse, + MagSettingIndexClock, + MagSettingIndexInterpacket, +}; + +#define TX_COUNT 7 +const char* const tx_text[TX_COUNT] = { + "RFID", + "GPIO", + "Piezo", + "LF + P", + "NFC", + "434MHz", + "868MHz", +}; +const uint32_t tx_value[TX_COUNT] = { + MagTxStateRFID, + MagTxStateGPIO, + MagTxStatePiezo, + MagTxStateLF_P, + MagTxStateNFC, + MagTxCC1101_434, + MagTxCC1101_868, +}; + +#define TRACK_COUNT 4 +const char* const track_text[TRACK_COUNT] = { + "1 + 2", + "1", + "2", + "3", +}; +const uint32_t track_value[TRACK_COUNT] = { + MagTrackStateOneAndTwo, + MagTrackStateOne, + MagTrackStateTwo, + MagTrackStateThree, +}; + +#define REVERSE_COUNT 2 +const char* const reverse_text[REVERSE_COUNT] = { + "OFF", + "ON", +}; +const uint32_t reverse_value[REVERSE_COUNT] = { + MagReverseStateOff, + MagReverseStateOn, +}; + +#define CLOCK_COUNT 15 +const char* const clock_text[CLOCK_COUNT] = { + "200us", + "220us", + "240us", + "250us", + "260us", + "280us", + "300us", + "325us", + "350us", + "375us", + "400us", + "450us", + "500us", + "600us", + "700us", +}; +const uint32_t clock_value[CLOCK_COUNT] = { + 200, + 220, + 240, + 250, + 260, + 280, + 300, + 325, + 350, + 375, + 400, + 450, + 500, + 600, + 700, +}; + +#define INTERPACKET_COUNT 13 +const char* const interpacket_text[INTERPACKET_COUNT] = { + "0us", + "2us", + "4us", + "6us", + "8us", + "10us", + "12us", + "14us", + "16us", + "18us", + "20us", + "25us", + "30us", +}; +const uint32_t interpacket_value[INTERPACKET_COUNT] = { + 0, + 2, + 4, + 6, + 8, + 10, + 12, + 14, + 16, + 18, + 20, + 25, + 30, +}; + +static void mag_scene_emulate_config_set_tx(VariableItem* item) { + Mag* mag = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + variable_item_set_current_value_text(item, tx_text[index]); + + mag->setting->tx = tx_value[index]; +}; + +static void mag_scene_emulate_config_set_track(VariableItem* item) { + Mag* mag = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + if(mag->setting->reverse == MagReverseStateOff) { + variable_item_set_current_value_text(item, track_text[index]); + mag->setting->track = track_value[index]; + } else if(mag->setting->reverse == MagReverseStateOn) { + variable_item_set_current_value_index( + item, value_index_uint32(MagTrackStateOneAndTwo, track_value, TRACK_COUNT)); + } + + // TODO: Check there is data in selected track? + // Only display track options with data? +}; + +static void mag_scene_emulate_config_set_reverse(VariableItem* item) { + Mag* mag = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + if(mag->setting->track == MagTrackStateOneAndTwo) { + // only allow reverse track to be set when playing both 1 and 2 + variable_item_set_current_value_text(item, reverse_text[index]); + mag->setting->reverse = reverse_value[index]; + //FURI_LOG_D(TAG, "%s", reverse_text[index]); + //FURI_LOG_D(TAG, "%d", mag->setting->reverse); + } else { + variable_item_set_current_value_index( + item, value_index_uint32(MagReverseStateOff, reverse_value, REVERSE_COUNT)); + } +}; + +static void mag_scene_emulate_config_set_clock(VariableItem* item) { + Mag* mag = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + variable_item_set_current_value_text(item, clock_text[index]); + + mag->setting->us_clock = clock_value[index]; +}; + +static void mag_scene_emulate_config_set_interpacket(VariableItem* item) { + Mag* mag = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + variable_item_set_current_value_text(item, interpacket_text[index]); + + mag->setting->us_interpacket = interpacket_value[index]; +}; + +void mag_scene_emulate_config_on_enter(void* context) { + // TODO: retrieve current values from struct, rather than setting to default on setup + + Mag* mag = context; + VariableItem* item; + uint8_t value_index; + + // TX + item = variable_item_list_add( + mag->variable_item_list, "TX via:", TX_COUNT, mag_scene_emulate_config_set_tx, mag); + value_index = value_index_uint32(mag->setting->tx, tx_value, TX_COUNT); + scene_manager_set_scene_state(mag->scene_manager, MagSceneEmulateConfig, (uint32_t)item); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, tx_text[value_index]); + + // Track + item = variable_item_list_add( + mag->variable_item_list, "Track:", TRACK_COUNT, mag_scene_emulate_config_set_track, mag); + value_index = value_index_uint32(mag->setting->track, track_value, TRACK_COUNT); + scene_manager_set_scene_state(mag->scene_manager, MagSceneEmulateConfig, (uint32_t)item); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, track_text[value_index]); + + // Reverse + //FURI_LOG_D(TAG, "%d", mag->setting->reverse); + item = variable_item_list_add( + mag->variable_item_list, + "Reverse:", + REVERSE_COUNT, + mag_scene_emulate_config_set_reverse, + mag); + value_index = value_index_uint32(mag->setting->reverse, reverse_value, REVERSE_COUNT); + scene_manager_set_scene_state(mag->scene_manager, MagSceneEmulateConfig, (uint32_t)item); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, reverse_text[value_index]); + + // Clock + item = variable_item_list_add( + mag->variable_item_list, "Clock:", CLOCK_COUNT, mag_scene_emulate_config_set_clock, mag); + value_index = value_index_uint32(mag->setting->us_clock, clock_value, CLOCK_COUNT); + scene_manager_set_scene_state(mag->scene_manager, MagSceneEmulateConfig, (uint32_t)item); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, clock_text[value_index]); + + // Interpacket + /* + item = variable_item_list_add( + mag->variable_item_list, + "Interpacket:", + INTERPACKET_COUNT, + mag_scene_emulate_config_set_interpacket, + mag); + value_index = + value_index_uint32(mag->setting->us_interpacket, interpacket_value, INTERPACKET_COUNT); + scene_manager_set_scene_state(mag->scene_manager, MagSceneEmulateConfig, (uint32_t)item); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, interpacket_text[value_index]);*/ + UNUSED(mag_scene_emulate_config_set_interpacket); + + view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewVariableItemList); +} + +bool mag_scene_emulate_config_on_event(void* context, SceneManagerEvent event) { + Mag* mag = context; + SceneManager* scene_manager = mag->scene_manager; + bool consumed = false; + + UNUSED(mag); + UNUSED(scene_manager); + UNUSED(event); + + return consumed; +} + +void mag_scene_emulate_config_on_exit(void* context) { + Mag* mag = context; + variable_item_list_set_selected_item(mag->variable_item_list, 0); + variable_item_list_reset(mag->variable_item_list); + // mag_last_settings_save? + // scene_manager_set_scene_state? Using subghz_scene_reciever_config as framework/inspo +} \ No newline at end of file diff --git a/applications/external/magspoof/scenes/mag_scene_exit_confirm.c b/applications/external/magspoof/scenes/mag_scene_exit_confirm.c new file mode 100644 index 000000000..e26234fb8 --- /dev/null +++ b/applications/external/magspoof/scenes/mag_scene_exit_confirm.c @@ -0,0 +1,20 @@ +#include "../mag_i.h" + +void mag_scene_exit_confirm_on_enter(void* context) { + Mag* mag = context; + UNUSED(mag); +} + +bool mag_scene_exit_confirm_on_event(void* context, SceneManagerEvent event) { + Mag* mag = context; + UNUSED(mag); + UNUSED(event); + bool consumed = false; + + return consumed; +} + +void mag_scene_exit_confirm_on_exit(void* context) { + Mag* mag = context; + UNUSED(mag); +} diff --git a/applications/external/magspoof/scenes/mag_scene_file_select.c b/applications/external/magspoof/scenes/mag_scene_file_select.c new file mode 100644 index 000000000..b759c4d18 --- /dev/null +++ b/applications/external/magspoof/scenes/mag_scene_file_select.c @@ -0,0 +1,24 @@ +#include "../mag_i.h" +#include "../mag_device.h" + +void mag_scene_file_select_on_enter(void* context) { + Mag* mag = context; + //UNUSED(mag); + mag_device_set_loading_callback(mag->mag_dev, mag_show_loading_popup, mag); + if(mag_file_select(mag->mag_dev)) { + scene_manager_next_scene(mag->scene_manager, MagSceneSavedMenu); + } else { + scene_manager_search_and_switch_to_previous_scene(mag->scene_manager, MagSceneStart); + } + mag_device_set_loading_callback(mag->mag_dev, NULL, mag); +} + +bool mag_scene_file_select_on_event(void* context, SceneManagerEvent event) { + UNUSED(context); + UNUSED(event); + return false; +} + +void mag_scene_file_select_on_exit(void* context) { + UNUSED(context); +} \ No newline at end of file diff --git a/applications/external/magspoof/scenes/mag_scene_input_name.c b/applications/external/magspoof/scenes/mag_scene_input_name.c new file mode 100644 index 000000000..7368b4598 --- /dev/null +++ b/applications/external/magspoof/scenes/mag_scene_input_name.c @@ -0,0 +1,82 @@ +#include +#include "../mag_i.h" + +void mag_scene_input_name_on_enter(void* context) { + Mag* mag = context; + TextInput* text_input = mag->text_input; + FuriString* folder_path; + folder_path = furi_string_alloc(); + + //TODO: compatible types / etc + //bool name_is_empty = furi_string_empty(mag->mag_dev->dev_name); + bool name_is_empty = true; + + if(name_is_empty) { + furi_string_set(mag->file_path, MAG_APP_FOLDER); + set_random_name(mag->text_store, MAG_TEXT_STORE_SIZE); + furi_string_set(folder_path, MAG_APP_FOLDER); + } else { + // TODO: compatible types etc + //mag_text_store_set(mag, "%s", furi_string_get_cstr(mag->mag_dev->dev_name)); + path_extract_dirname(furi_string_get_cstr(mag->file_path), folder_path); + } + + text_input_set_header_text(text_input, "Name the card"); + text_input_set_result_callback( + text_input, + mag_text_input_callback, + mag, + mag->text_store, + MAG_DEV_NAME_MAX_LEN, + name_is_empty); + + FURI_LOG_I("", "%s %s", furi_string_get_cstr(folder_path), mag->text_store); + + ValidatorIsFile* validator_is_file = validator_is_file_alloc_init( + furi_string_get_cstr(folder_path), + MAG_APP_EXTENSION, + furi_string_get_cstr(mag->file_name)); + text_input_set_validator(text_input, validator_is_file_callback, validator_is_file); + + furi_string_free(folder_path); + + view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewTextInput); +} + +bool mag_scene_input_name_on_event(void* context, SceneManagerEvent event) { + Mag* mag = context; + SceneManager* scene_manager = mag->scene_manager; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == MagEventNext) { + consumed = true; + //if(!furi_string_empty(mag->file_name)) { + // mag_delete_key(mag); + //} + + furi_string_set(mag->file_name, mag->text_store); + + if(mag_device_save(mag->mag_dev, furi_string_get_cstr(mag->file_name))) { + scene_manager_next_scene(scene_manager, MagSceneSaveSuccess); + } else { + //scene_manager_search_and_switch_to_previous_scene( + // scene_manager, MagSceneReadKeyMenu); + // TODO: Replace with appropriate scene! No read scene prior if adding manually... + } + } + } + + return consumed; +} + +void mag_scene_input_name_on_exit(void* context) { + Mag* mag = context; + TextInput* text_input = mag->text_input; + + void* validator_context = text_input_get_validator_callback_context(text_input); + text_input_set_validator(text_input, NULL, NULL); + validator_is_file_free((ValidatorIsFile*)validator_context); + + text_input_reset(text_input); +} diff --git a/applications/external/magspoof/scenes/mag_scene_input_value.c b/applications/external/magspoof/scenes/mag_scene_input_value.c new file mode 100644 index 000000000..1a8ac0904 --- /dev/null +++ b/applications/external/magspoof/scenes/mag_scene_input_value.c @@ -0,0 +1,37 @@ +#include "../mag_i.h" + +void mag_scene_input_value_on_enter(void* context) { + Mag* mag = context; + Mag_TextInput* mag_text_input = mag->mag_text_input; + + // TODO: retrieve stored/existing data if editing rather than adding anew? + mag_text_store_set(mag, furi_string_get_cstr(mag->mag_dev->dev_data.track[1].str)); + + mag_text_input_set_header_text(mag_text_input, "Enter track data (WIP)"); + mag_text_input_set_result_callback( + mag_text_input, mag_text_input_callback, mag, mag->text_store, MAG_TEXT_STORE_SIZE, true); + + view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewMagTextInput); +} + +bool mag_scene_input_value_on_event(void* context, SceneManagerEvent event) { + Mag* mag = context; + SceneManager* scene_manager = mag->scene_manager; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == MagEventNext) { + consumed = true; + + furi_string_set(mag->mag_dev->dev_data.track[1].str, mag->text_store); + scene_manager_next_scene(scene_manager, MagSceneInputName); + } + } + + return consumed; +} + +void mag_scene_input_value_on_exit(void* context) { + Mag* mag = context; + UNUSED(mag); +} \ No newline at end of file diff --git a/applications/external/magspoof/scenes/mag_scene_read.c b/applications/external/magspoof/scenes/mag_scene_read.c new file mode 100644 index 000000000..48a223c0f --- /dev/null +++ b/applications/external/magspoof/scenes/mag_scene_read.c @@ -0,0 +1,185 @@ +// Creator: Hummus@FlipperGang + +#include "../mag_i.h" +#include "../helpers/mag_helpers.h" + +#include "mag_scene_read.h" + +#define TAG "MagSceneRead" + +void uart_callback(UartIrqEvent event, uint8_t data, void* context) { + Mag* mag = context; + if(event == UartIrqEventRXNE) { + furi_stream_buffer_send(mag->uart_rx_stream, &data, 1, 0); + furi_thread_flags_set(furi_thread_get_id(mag->uart_rx_thread), WorkerEvtRxDone); + } +} + +static int32_t uart_worker(void* context) { + Mag* mag = context; + mag->uart_rx_stream = furi_stream_buffer_alloc(UART_RX_BUF_SIZE, 1); + mag->uart_text_box_store_strlen = 0; + + while(1) { + uint32_t events = + furi_thread_flags_wait(WORKER_ALL_RX_EVENTS, FuriFlagWaitAny, FuriWaitForever); + // furi_check((events & FuriFlagError) == 0); + + if(events & WorkerEvtStop) break; + if(events & WorkerEvtRxDone) { + FURI_LOG_D(TAG, "WorkerEvtRxDone"); + // notification_message(mag->notifications, &sequence_success); + size_t len = furi_stream_buffer_receive( + mag->uart_rx_stream, mag->uart_rx_buf, UART_RX_BUF_SIZE, 200); + FURI_LOG_D(TAG, "UART RX len: %d", len); + + if(len > 0) { + // If text box store gets too big, then truncate it + mag->uart_text_box_store_strlen += len; + + if(mag->uart_text_box_store_strlen >= UART_TERMINAL_TEXT_BOX_STORE_SIZE - 1) { + furi_string_right( + mag->uart_text_box_store, mag->uart_text_box_store_strlen / 2); + mag->uart_text_box_store_strlen = + furi_string_size(mag->uart_text_box_store) + len; + } + + // Add '\0' to the end of the string, and then add the new data + mag->uart_rx_buf[len] = '\0'; + furi_string_cat_printf(mag->uart_text_box_store, "%s", mag->uart_rx_buf); + + FURI_LOG_D(TAG, "UART RX buf: %*.s", len, mag->uart_rx_buf); + FURI_LOG_D( + TAG, "UART RX store: %s", furi_string_get_cstr(mag->uart_text_box_store)); + } + + FURI_LOG_D(TAG, "UARTEventRxData"); + + view_dispatcher_send_custom_event(mag->view_dispatcher, UARTEventRxData); + } + } + + furi_stream_buffer_free(mag->uart_rx_stream); + + return 0; +} + +void update_widgets(Mag* mag) { + // Clear widget from all elements + widget_reset(mag->widget); + + // Titlebar + widget_add_icon_element(mag->widget, 38, -1, &I_mag_file_10px); + widget_add_string_element(mag->widget, 64, 0, AlignCenter, AlignTop, FontPrimary, "READ"); + widget_add_icon_element(mag->widget, 81, -1, &I_mag_file_10px); + + // Text box + widget_add_text_scroll_element( + mag->widget, 0, 10, 128, 40, furi_string_get_cstr(mag->uart_text_box_store)); + + // Buttons + widget_add_button_element(mag->widget, GuiButtonTypeLeft, "Clear", mag_widget_callback, mag); + widget_add_button_element(mag->widget, GuiButtonTypeRight, "Parse", mag_widget_callback, mag); +} + +void mag_scene_read_on_enter(void* context) { + Mag* mag = context; + FuriString* message = furi_string_alloc(); + furi_string_printf(message, "Please swipe a card!\n"); + mag->uart_text_box_store = message; + + view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewWidget); + + update_widgets(mag); + + // Initialize UART + // furi_hal_console_disable(); + furi_hal_uart_deinit(FuriHalUartIdUSART1); + furi_hal_uart_init(FuriHalUartIdUSART1, 9600); + furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, uart_callback, mag); + FURI_LOG_D(TAG, "UART initialized"); + + mag->uart_rx_thread = furi_thread_alloc(); + furi_thread_set_name(mag->uart_rx_thread, "UartRx"); + furi_thread_set_stack_size(mag->uart_rx_thread, 1024); + furi_thread_set_context(mag->uart_rx_thread, mag); + furi_thread_set_callback(mag->uart_rx_thread, uart_worker); + + furi_thread_start(mag->uart_rx_thread); + FURI_LOG_D(TAG, "UART worker started"); +} + +bool mag_scene_read_on_event(void* context, SceneManagerEvent event) { + Mag* mag = context; + + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + FURI_LOG_D(TAG, "Custom event: %ld", event.event); + + switch(event.event) { + case GuiButtonTypeLeft: // Clear + consumed = true; + // Clear text box store + furi_string_reset(mag->uart_text_box_store); + mag->uart_text_box_store_strlen = 0; + break; + + case GuiButtonTypeRight: // Parse + consumed = true; + FURI_LOG_D(TAG, "Trying to parse"); + MagDevice* mag_dev = mag->mag_dev; + + bool res = mag_device_parse_card_string(mag_dev, mag->uart_text_box_store); + furi_string_reset(mag->uart_text_box_store); + if(res) { + notification_message(mag->notifications, &sequence_success); + + furi_string_printf( + mag->uart_text_box_store, + "Track 1: %.*s\nTrack 2: %.*s\nTrack 3: %.*s", + mag_dev->dev_data.track[0].len, + furi_string_get_cstr(mag_dev->dev_data.track[0].str), + mag_dev->dev_data.track[1].len, + furi_string_get_cstr(mag_dev->dev_data.track[1].str), + mag_dev->dev_data.track[2].len, + furi_string_get_cstr(mag_dev->dev_data.track[2].str)); + + // Switch to saved menu scene + scene_manager_next_scene(mag->scene_manager, MagSceneSavedMenu); + + } else { + furi_string_printf(mag->uart_text_box_store, "Failed to parse! Try again\n"); + notification_message(mag->notifications, &sequence_error); + } + + break; + } + + update_widgets(mag); + } + + return consumed; +} + +void mag_scene_read_on_exit(void* context) { + Mag* mag = context; + // notification_message(mag->notifications, &sequence_blink_stop); + widget_reset(mag->widget); + // view_dispatcher_remove_view(mag->view_dispatcher, MagViewWidget); + + // Stop UART worker + FURI_LOG_D(TAG, "Stopping UART worker"); + furi_thread_flags_set(furi_thread_get_id(mag->uart_rx_thread), WorkerEvtStop); + furi_thread_join(mag->uart_rx_thread); + furi_thread_free(mag->uart_rx_thread); + FURI_LOG_D(TAG, "UART worker stopped"); + + furi_string_free(mag->uart_text_box_store); + + furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, NULL, NULL); + furi_hal_uart_deinit(FuriHalUartIdUSART1); + // furi_hal_console_enable(); + + notification_message(mag->notifications, &sequence_blink_stop); +} \ No newline at end of file diff --git a/applications/external/magspoof/scenes/mag_scene_read.h b/applications/external/magspoof/scenes/mag_scene_read.h new file mode 100644 index 000000000..df8327877 --- /dev/null +++ b/applications/external/magspoof/scenes/mag_scene_read.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +#define UART_RX_BUF_SIZE (320) +#define UART_TERMINAL_TEXT_BOX_STORE_SIZE (4096) +#define UART_TERMINAL_TEXT_INPUT_STORE_SIZE (512) +#define UART_CH (FuriHalUartIdUSART1) +#define UART_BAUDRATE (9600) + +typedef enum { + WorkerEvtStop = (1 << 0), + WorkerEvtRxDone = (1 << 1), +} WorkerEvtFlags; + +typedef enum { + UARTEventRxData = 100, +} UARTEvents; + +#define WORKER_ALL_RX_EVENTS (WorkerEvtStop | WorkerEvtRxDone) diff --git a/applications/external/magspoof/scenes/mag_scene_save_success.c b/applications/external/magspoof/scenes/mag_scene_save_success.c new file mode 100644 index 000000000..fe9b6b4af --- /dev/null +++ b/applications/external/magspoof/scenes/mag_scene_save_success.c @@ -0,0 +1,43 @@ +#include "../mag_i.h" + +void mag_scene_save_success_on_enter(void* context) { + Mag* mag = context; + Popup* popup = mag->popup; + + // Clear state of data enter scene + //scene_manager_set_scene_state(mag->scene_manager, LfRfidSceneSaveData, 0); + mag_text_store_clear(mag); + + popup_set_icon(popup, 32, 5, &I_DolphinNice_96x59); + popup_set_header(popup, "Saved!", 5, 7, AlignLeft, AlignTop); + popup_set_context(popup, mag); + popup_set_callback(popup, mag_popup_timeout_callback); + popup_set_timeout(popup, 1500); + popup_enable_timeout(popup); + + view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewPopup); +} + +bool mag_scene_save_success_on_event(void* context, SceneManagerEvent event) { + Mag* mag = context; + bool consumed = false; + + if((event.type == SceneManagerEventTypeBack) || + ((event.type == SceneManagerEventTypeCustom) && (event.event == MagEventPopupClosed))) { + bool result = + scene_manager_search_and_switch_to_previous_scene(mag->scene_manager, MagSceneStart); + if(!result) { + scene_manager_search_and_switch_to_another_scene( + mag->scene_manager, MagSceneFileSelect); + } + consumed = true; + } + + return consumed; +} + +void mag_scene_save_success_on_exit(void* context) { + Mag* mag = context; + + popup_reset(mag->popup); +} \ No newline at end of file diff --git a/applications/external/magspoof/scenes/mag_scene_saved_info.c b/applications/external/magspoof/scenes/mag_scene_saved_info.c new file mode 100644 index 000000000..fd4e808b5 --- /dev/null +++ b/applications/external/magspoof/scenes/mag_scene_saved_info.c @@ -0,0 +1,50 @@ +#include "../mag_i.h" + +void mag_scene_saved_info_on_enter(void* context) { + Mag* mag = context; + Widget* widget = mag->widget; + + FuriString* tmp_str; + tmp_str = furi_string_alloc(); + + // Use strlcpy instead perhaps, to truncate to screen width, then add ellipses if needed? + furi_string_printf(tmp_str, "%s\r\n", mag->mag_dev->dev_name); + + widget_add_icon_element(widget, 1, 1, &I_mag_file_10px); + widget_add_string_element( + widget, 13, 2, AlignLeft, AlignTop, FontPrimary, furi_string_get_cstr(tmp_str)); + furi_string_reset(tmp_str); + + for(uint8_t i = 0; i < MAG_DEV_TRACKS; i++) { + FuriString* trackstr = mag->mag_dev->dev_data.track[i].str; + + furi_string_cat_printf( + tmp_str, + "Track %d:%s%s%s", + (i + 1), + furi_string_empty(trackstr) ? " " : "\n", + furi_string_empty(trackstr) ? "< empty >" : furi_string_get_cstr(trackstr), + (i + 1 == MAG_DEV_TRACKS) ? "" : "\n\n"); + } + + widget_add_text_scroll_element(widget, 0, 15, 128, 49, furi_string_get_cstr(tmp_str)); + + view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewWidget); + furi_string_free(tmp_str); +} + +bool mag_scene_saved_info_on_event(void* context, SceneManagerEvent event) { + Mag* mag = context; + SceneManager* scene_manager = mag->scene_manager; + bool consumed = false; + + UNUSED(event); + UNUSED(scene_manager); + + return consumed; +} + +void mag_scene_saved_info_on_exit(void* context) { + Mag* mag = context; + widget_reset(mag->widget); +} diff --git a/applications/external/magspoof/scenes/mag_scene_saved_menu.c b/applications/external/magspoof/scenes/mag_scene_saved_menu.c new file mode 100644 index 000000000..f9235cd54 --- /dev/null +++ b/applications/external/magspoof/scenes/mag_scene_saved_menu.c @@ -0,0 +1,83 @@ +#include "../mag_i.h" + +enum SubmenuIndex { + SubmenuIndexEmulate, + //SubmenuIndexEdit, + SubmenuIndexDelete, + SubmenuIndexInfo, +}; + +void mag_scene_saved_menu_submenu_callback(void* context, uint32_t index) { + Mag* mag = context; + + view_dispatcher_send_custom_event(mag->view_dispatcher, index); +} + +void mag_scene_saved_menu_on_enter(void* context) { + Mag* mag = context; + Submenu* submenu = mag->submenu; + + notification_message(mag->notifications, &sequence_blink_cyan_10); + + // messy code to quickly check which tracks are available for emulation/display + // there's likely a better spot to do this, but the MagDevice functions don't have access to the full mag struct... + bool is_empty_t1 = furi_string_empty(mag->mag_dev->dev_data.track[0].str); + bool is_empty_t2 = furi_string_empty(mag->mag_dev->dev_data.track[1].str); + bool is_empty_t3 = furi_string_empty(mag->mag_dev->dev_data.track[2].str); + + if(!is_empty_t1 && !is_empty_t2) { + mag->setting->track = MagTrackStateOneAndTwo; + } else if(!is_empty_t1) { + mag->setting->track = MagTrackStateOne; + } else if(!is_empty_t2) { + mag->setting->track = MagTrackStateTwo; + } else if(!is_empty_t3) { + mag->setting->track = MagTrackStateThree; + } // TODO: what happens if no track data present? + + submenu_add_item( + submenu, "Emulate (WIP)", SubmenuIndexEmulate, mag_scene_saved_menu_submenu_callback, mag); + //submenu_add_item( + // submenu, "Edit (WIP)", SubmenuIndexEdit, mag_scene_saved_menu_submenu_callback, mag); + submenu_add_item( + submenu, "Delete", SubmenuIndexDelete, mag_scene_saved_menu_submenu_callback, mag); + submenu_add_item( + submenu, "Info", SubmenuIndexInfo, mag_scene_saved_menu_submenu_callback, mag); + + submenu_set_selected_item( + mag->submenu, scene_manager_get_scene_state(mag->scene_manager, MagSceneSavedMenu)); + + view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewSubmenu); +} + +bool mag_scene_saved_menu_on_event(void* context, SceneManagerEvent event) { + Mag* mag = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + scene_manager_set_scene_state(mag->scene_manager, MagSceneSavedMenu, event.event); + + // TODO: replace with actual next scenes once built + if(event.event == SubmenuIndexEmulate) { + scene_manager_next_scene(mag->scene_manager, MagSceneEmulate); + consumed = true; + //} else if(event.event == SubmenuIndexEdit) { + // scene_manager_next_scene(mag->scene_manager, MagSceneUnderConstruction); + // consumed = true; + } else if(event.event == SubmenuIndexDelete) { + scene_manager_next_scene(mag->scene_manager, MagSceneDeleteConfirm); + consumed = true; + } else if(event.event == SubmenuIndexInfo) { + scene_manager_next_scene(mag->scene_manager, MagSceneSavedInfo); + consumed = true; + } + } + + return consumed; +} + +void mag_scene_saved_menu_on_exit(void* context) { + Mag* mag = context; + + submenu_reset(mag->submenu); +} diff --git a/applications/external/magspoof/scenes/mag_scene_start.c b/applications/external/magspoof/scenes/mag_scene_start.c new file mode 100644 index 000000000..b048daaba --- /dev/null +++ b/applications/external/magspoof/scenes/mag_scene_start.c @@ -0,0 +1,71 @@ +#include "../mag_i.h" + +typedef enum { + SubmenuIndexSaved, + SubmenuIndexRead, + //SubmenuIndexAddManually, + SubmenuIndexAbout, +} SubmenuIndex; + +static void mag_scene_start_submenu_callback(void* context, uint32_t index) { + Mag* mag = context; + + view_dispatcher_send_custom_event(mag->view_dispatcher, index); +} + +void mag_scene_start_on_enter(void* context) { + Mag* mag = context; + Submenu* submenu = mag->submenu; + + submenu_add_item(submenu, "Saved", SubmenuIndexSaved, mag_scene_start_submenu_callback, mag); + submenu_add_item(submenu, "Read", SubmenuIndexRead, mag_scene_start_submenu_callback, mag); + //submenu_add_item( + // submenu, "Add Manually", SubmenuIndexAddManually, mag_scene_start_submenu_callback, mag); + submenu_add_item(submenu, "About", SubmenuIndexAbout, mag_scene_start_submenu_callback, mag); + + submenu_set_selected_item( + submenu, scene_manager_get_scene_state(mag->scene_manager, MagSceneStart)); + + // clear key + furi_string_reset(mag->file_name); + + view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewSubmenu); +} + +bool mag_scene_start_on_event(void* context, SceneManagerEvent event) { + Mag* mag = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + switch(event.event) { + case SubmenuIndexSaved: + furi_string_set(mag->file_path, MAG_APP_FOLDER); + scene_manager_next_scene(mag->scene_manager, MagSceneFileSelect); + consumed = true; + break; + + case SubmenuIndexRead: + scene_manager_next_scene(mag->scene_manager, MagSceneRead); + consumed = true; + break; + //case SubmenuIndexAddManually: + // scene_manager_next_scene(mag->scene_manager, MagSceneInputValue); + // consumed = true; + // break; + case SubmenuIndexAbout: + scene_manager_next_scene(mag->scene_manager, MagSceneAbout); + consumed = true; + break; + } + + scene_manager_set_scene_state(mag->scene_manager, MagSceneStart, event.event); + } + + return consumed; +} + +void mag_scene_start_on_exit(void* context) { + Mag* mag = context; + + submenu_reset(mag->submenu); +} diff --git a/applications/external/magspoof/scenes/mag_scene_under_construction.c b/applications/external/magspoof/scenes/mag_scene_under_construction.c new file mode 100644 index 000000000..520f0a792 --- /dev/null +++ b/applications/external/magspoof/scenes/mag_scene_under_construction.c @@ -0,0 +1,40 @@ +#include "../mag_i.h" + +void mag_scene_under_construction_on_enter(void* context) { + Mag* mag = context; + Widget* widget = mag->widget; + + FuriString* tmp_str; + tmp_str = furi_string_alloc(); + + widget_add_button_element(widget, GuiButtonTypeLeft, "Back", mag_widget_callback, mag); + + furi_string_printf(tmp_str, "Under construction!"); + widget_add_string_element( + widget, 64, 4, AlignCenter, AlignTop, FontPrimary, furi_string_get_cstr(tmp_str)); + furi_string_reset(tmp_str); + + view_dispatcher_switch_to_view(mag->view_dispatcher, MagViewWidget); + furi_string_free(tmp_str); +} + +bool mag_scene_under_construction_on_event(void* context, SceneManagerEvent event) { + Mag* mag = context; + SceneManager* scene_manager = mag->scene_manager; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == GuiButtonTypeLeft) { + consumed = true; + + scene_manager_previous_scene(scene_manager); + } + } + + return consumed; +} + +void mag_scene_under_construction_on_exit(void* context) { + Mag* mag = context; + widget_reset(mag->widget); +} \ No newline at end of file