Luu 1 год назад
Родитель
Сommit
3f75f44cf6
3 измененных файлов с 186 добавлено и 0 удалено
  1. 1 0
      scenes/metroflip_scene_config.h
  2. 182 0
      scenes/metroflip_scene_myki.c
  3. 3 0
      scenes/metroflip_scene_start.c

+ 1 - 0
scenes/metroflip_scene_config.h

@@ -6,6 +6,7 @@ ADD_SCENE(metroflip, clipper, Clipper)
 ADD_SCENE(metroflip, metromoney, Metromoney)
 ADD_SCENE(metroflip, metromoney, Metromoney)
 ADD_SCENE(metroflip, read_success, ReadSuccess)
 ADD_SCENE(metroflip, read_success, ReadSuccess)
 ADD_SCENE(metroflip, bip, Bip)
 ADD_SCENE(metroflip, bip, Bip)
+ADD_SCENE(metroflip, myki, Myki)
 ADD_SCENE(metroflip, troika, Troika)
 ADD_SCENE(metroflip, troika, Troika)
 ADD_SCENE(metroflip, about, About)
 ADD_SCENE(metroflip, about, About)
 ADD_SCENE(metroflip, credits, Credits)
 ADD_SCENE(metroflip, credits, Credits)

+ 182 - 0
scenes/metroflip_scene_myki.c

@@ -0,0 +1,182 @@
+#include <flipper_application.h>
+
+#include <lib/nfc/protocols/mf_desfire/mf_desfire.h>
+#include <stdio.h>
+
+#include "../metroflip_i.h"
+#include <nfc/protocols/mf_desfire/mf_desfire_poller.h>
+
+static const MfDesfireApplicationId myki_app_id = {.data = {0x00, 0x11, 0xf2}};
+static const MfDesfireFileId myki_file_id = 0x0f;
+
+static uint8_t myki_calculate_luhn(uint64_t number) {
+    // https://en.wikipedia.org/wiki/Luhn_algorithm
+    // Drop existing check digit to form payload
+    uint64_t payload = number / 10;
+    int sum = 0;
+    int position = 0;
+
+    while(payload > 0) {
+        int digit = payload % 10;
+        if(position % 2 == 0) {
+            digit *= 2;
+        }
+        if(digit > 9) {
+            digit = (digit / 10) + (digit % 10);
+        }
+        sum += digit;
+        payload /= 10;
+        position++;
+    }
+
+    return (10 - (sum % 10)) % 10;
+}
+
+static bool myki_parse(const NfcDevice* device, FuriString* parsed_data) {
+    furi_assert(device);
+    furi_assert(parsed_data);
+
+    bool parsed = false;
+
+    do {
+        const MfDesfireData* data = nfc_device_get_data(device, NfcProtocolMfDesfire);
+
+        const MfDesfireApplication* app = mf_desfire_get_application(data, &myki_app_id);
+        if(app == NULL) break;
+
+        typedef struct {
+            uint32_t top;
+            uint32_t bottom;
+        } MykiFile;
+
+        const MfDesfireFileSettings* file_settings =
+            mf_desfire_get_file_settings(app, &myki_file_id);
+
+        if(file_settings == NULL || file_settings->type != MfDesfireFileTypeStandard ||
+           file_settings->data.size < sizeof(MykiFile))
+            break;
+
+        const MfDesfireFileData* file_data = mf_desfire_get_file_data(app, &myki_file_id);
+        if(file_data == NULL) break;
+
+        const MykiFile* myki_file = simple_array_cget_data(file_data->data);
+
+        // All myki card numbers are prefixed with "308425"
+        if(myki_file->top != 308425UL) break;
+        // Card numbers are always 15 digits in length
+        if(myki_file->bottom < 10000000UL || myki_file->bottom >= 100000000UL) break;
+
+        uint64_t card_number = myki_file->top * 1000000000ULL + myki_file->bottom * 10UL;
+        // Stored card number doesn't include check digit
+        card_number += myki_calculate_luhn(card_number);
+
+        furi_string_set(parsed_data, "\e#myki\nNo.: ");
+
+        // Stylise card number according to the physical card
+        char card_string[20];
+        snprintf(card_string, sizeof(card_string), "%llu", card_number);
+
+        // Digit count in each space-separated group
+        static const uint8_t digit_count[] = {1, 5, 4, 4, 1};
+
+        for(uint32_t i = 0, k = 0; i < COUNT_OF(digit_count); k += digit_count[i++]) {
+            for(uint32_t j = 0; j < digit_count[i]; ++j) {
+                furi_string_push_back(parsed_data, card_string[j + k]);
+            }
+            furi_string_push_back(parsed_data, ' ');
+        }
+
+        parsed = true;
+    } while(false);
+
+    return parsed;
+}
+
+static NfcCommand metroflip_scene_myki_poller_callback(NfcGenericEvent event, void* context) {
+    furi_assert(event.protocol == NfcProtocolMfDesfire);
+
+    Metroflip* app = context;
+    NfcCommand command = NfcCommandContinue;
+
+    FuriString* parsed_data = furi_string_alloc();
+    Widget* widget = app->widget;
+    furi_string_reset(app->text_box_store);
+    const MfDesfirePollerEvent* mf_desfire_event = event.event_data;
+    if(mf_desfire_event->type == MfDesfirePollerEventTypeReadSuccess) {
+        nfc_device_set_data(
+            app->nfc_device, NfcProtocolMfDesfire, nfc_poller_get_data(app->poller));
+        myki_parse(app->nfc_device, parsed_data);
+        widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
+
+        widget_add_button_element(
+            widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
+
+        furi_string_free(parsed_data);
+        view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
+        metroflip_app_blink_stop(app);
+        command = NfcCommandStop;
+    } else if(mf_desfire_event->type == MfDesfirePollerEventTypeReadFailed) {
+        view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventPollerSuccess);
+        command = NfcCommandReset;
+    }
+
+    return command;
+}
+
+void metroflip_scene_myki_on_enter(void* context) {
+    Metroflip* app = context;
+    dolphin_deed(DolphinDeedNfcRead);
+
+    // Setup view
+    Popup* popup = app->popup;
+    popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
+    popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
+
+    // Start worker
+    view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
+    nfc_scanner_alloc(app->nfc);
+    app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfDesfire);
+    nfc_poller_start(app->poller, metroflip_scene_myki_poller_callback, app);
+
+    metroflip_app_blink_start(app);
+}
+
+bool metroflip_scene_myki_on_event(void* context, SceneManagerEvent event) {
+    Metroflip* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == MetroflipCustomEventCardDetected) {
+            Popup* popup = app->popup;
+            popup_set_header(popup, "DON'T\nMOVE", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == MetroflipCustomEventCardLost) {
+            Popup* popup = app->popup;
+            popup_set_header(popup, "Card \n lost", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == MetroflipCustomEventWrongCard) {
+            Popup* popup = app->popup;
+            popup_set_header(popup, "WRONG \n CARD", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == MetroflipCustomEventPollerFail) {
+            Popup* popup = app->popup;
+            popup_set_header(popup, "Failed", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+void metroflip_scene_myki_on_exit(void* context) {
+    Metroflip* app = context;
+    widget_reset(app->widget);
+
+    if(app->poller) {
+        nfc_poller_stop(app->poller);
+        nfc_poller_free(app->poller);
+    }
+}

+ 3 - 0
scenes/metroflip_scene_start.c

@@ -27,6 +27,9 @@ void metroflip_scene_start_on_enter(void* context) {
     submenu_add_item(
     submenu_add_item(
         submenu, "Clipper", MetroflipSceneClipper, metroflip_scene_start_submenu_callback, app);
         submenu, "Clipper", MetroflipSceneClipper, metroflip_scene_start_submenu_callback, app);
 
 
+    submenu_add_item(
+        submenu, "Myki", MetroflipSceneMyki, metroflip_scene_start_submenu_callback, app);
+
     submenu_add_item(
     submenu_add_item(
         submenu, "Troika", MetroflipSceneTroika, metroflip_scene_start_submenu_callback, app);
         submenu, "Troika", MetroflipSceneTroika, metroflip_scene_start_submenu_callback, app);