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

Merge pull request #13 from DocSystem/main

Luu 1 год назад
Родитель
Сommit
f16eb01fc8
5 измененных файлов с 1048 добавлено и 184 удалено
  1. 5 1
      CHANGELOG.md
  2. 1 1
      README.md
  3. 1 1
      manifest.yml
  4. 650 168
      scenes/metroflip_scene_navigo.c
  5. 391 13
      scenes/navigo.h

+ 5 - 1
CHANGELOG.md

@@ -14,4 +14,8 @@
 - Added Troika parser (Moscow, Russia)
 - Added Troika parser (Moscow, Russia)
 - Added Myki parser (Melbourne (and surrounds), VIC, Australia)
 - Added Myki parser (Melbourne (and surrounds), VIC, Australia)
 - Added Opal parser (Sydney (and surrounds), NSW, Australia)
 - Added Opal parser (Sydney (and surrounds), NSW, Australia)
-- Added ITSO parser (United Kingdom)
+- Added ITSO parser (United Kingdom)
+
+## v0.4
+
+- Updated Navigo parser (Paris, France)

+ 1 - 1
README.md

@@ -36,7 +36,7 @@ This is a list of metro cards and transit systems that need support or have part
 - **App Author**: [@luu176](https://github.com/luu176)
 - **App Author**: [@luu176](https://github.com/luu176)
 - **Charliecard Parser**: [@zacharyweiss](https://github.com/zacharyweiss)
 - **Charliecard Parser**: [@zacharyweiss](https://github.com/zacharyweiss)
 - **Rav-Kav Parser**: [@luu176](https://github.com/luu176)
 - **Rav-Kav Parser**: [@luu176](https://github.com/luu176)
-- **Navigo Parser**: [@luu176](https://github.com/luu176)
+- **Navigo Parser**: [@luu176](https://github.com/luu176) and [@DocSystem](https://github.com/DocSystem)
 - **Metromoney Parser**: [@Leptopt1los](https://github.com/Leptopt1los)
 - **Metromoney Parser**: [@Leptopt1los](https://github.com/Leptopt1los)
 - **Bip! Parser**: [@rbasoalto](https://github.com/rbasoalto) [@gornekich](https://github.com/gornekich)
 - **Bip! Parser**: [@rbasoalto](https://github.com/rbasoalto) [@gornekich](https://github.com/gornekich)
 - **Clipper Parser**: [@ke6jjj](https://github.com/ke6jjj)
 - **Clipper Parser**: [@ke6jjj](https://github.com/ke6jjj)

+ 1 - 1
manifest.yml

@@ -17,4 +17,4 @@ sourcecode:
     origin: https://github.com/luu176/Metroflip
     origin: https://github.com/luu176/Metroflip
     subdir:
     subdir:
   type: git
   type: git
-version: 0.3.1
+version: 0.4.0

+ 650 - 168
scenes/metroflip_scene_navigo.c

@@ -47,6 +47,444 @@ int check_events(int* array, int size, int number) {
     return total + 53;
     return total + 53;
 }
 }
 
 
+int select_new_app(
+    int new_app,
+    BitBuffer* tx_buffer,
+    BitBuffer* rx_buffer,
+    Iso14443_4bPoller* iso14443_4b_poller,
+    Metroflip* app,
+    MetroflipPollerEventType* stage) {
+    select_app[6] = new_app;
+
+    bit_buffer_reset(tx_buffer);
+    bit_buffer_append_bytes(tx_buffer, select_app, sizeof(select_app));
+    int error = iso14443_4b_poller_send_block(iso14443_4b_poller, tx_buffer, rx_buffer);
+    if(error != Iso14443_4bErrorNone) {
+        FURI_LOG_I(TAG, "Select File: iso14443_4b_poller_send_block error %d", error);
+        *stage = MetroflipPollerEventTypeFail;
+        view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventPollerFail);
+        return error;
+    }
+    return 0;
+}
+
+int read_new_file(
+    int new_file,
+    BitBuffer* tx_buffer,
+    BitBuffer* rx_buffer,
+    Iso14443_4bPoller* iso14443_4b_poller,
+    Metroflip* app,
+    MetroflipPollerEventType* stage) {
+    read_file[2] = new_file;
+    bit_buffer_reset(tx_buffer);
+    bit_buffer_append_bytes(tx_buffer, read_file, sizeof(read_file));
+    Iso14443_4bError error =
+        iso14443_4b_poller_send_block(iso14443_4b_poller, tx_buffer, rx_buffer);
+    if(error != Iso14443_4bErrorNone) {
+        FURI_LOG_I(TAG, "Read File: iso14443_4b_poller_send_block error %d", error);
+        *stage = MetroflipPollerEventTypeFail;
+        view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventPollerFail);
+        return error;
+    }
+    return 0;
+}
+
+int check_response(
+    BitBuffer* rx_buffer,
+    Metroflip* app,
+    MetroflipPollerEventType* stage,
+    size_t* response_length) {
+    *response_length = bit_buffer_get_size_bytes(rx_buffer);
+    if(bit_buffer_get_byte(rx_buffer, *response_length - 2) != apdu_success[0] ||
+       bit_buffer_get_byte(rx_buffer, *response_length - 1) != apdu_success[1]) {
+        FURI_LOG_I(
+            TAG,
+            "Select profile app/file failed: %02x%02x",
+            bit_buffer_get_byte(rx_buffer, *response_length - 2),
+            bit_buffer_get_byte(rx_buffer, *response_length - 1));
+        *stage = MetroflipPollerEventTypeFail;
+        view_dispatcher_send_custom_event(
+            app->view_dispatcher, MetroflipCustomEventPollerFileNotFound);
+        return 1;
+    }
+    return 0;
+}
+
+const char* get_country(int country_num) {
+    switch(country_num) {
+    case 250:
+        return "France";
+    default:
+        return "Unknown";
+    }
+}
+
+const char* get_network(int network_num) {
+    switch(network_num) {
+    case 901:
+        return "Ile-de-France Mobilites";
+    default:
+        return "Unknown";
+    }
+}
+
+const char* get_transport_type(int type) {
+    switch(type) {
+    case BUS_URBAIN:
+        return "Bus Urbain";
+    case BUS_INTERURBAIN:
+        return "Bus Interurbain";
+    case METRO:
+        return "Metro";
+    case TRAM:
+        return "Tram";
+    case TRAIN:
+        return "Train";
+    case PARKING:
+        return "Parking";
+    default:
+        return "Unknown";
+    }
+}
+
+const char* get_service_provider(int provider) {
+    switch(provider) {
+    case 2:
+        return "SNCF";
+    case 3:
+        return "RATP";
+    case 115:
+        return "CSO (VEOLIA)";
+    case 116:
+        return "R'Bus (VEOLIA)";
+    case 156:
+        return "Phebus";
+    case 175:
+        return "RATP (Veolia Transport Nanterre)";
+    default:
+        return "Unknown";
+    }
+}
+
+const char* get_transition_type(int transition) {
+    switch(transition) {
+    case 1:
+        return "Validation - entree";
+    case 2:
+        return "Validation - sortie";
+    case 4:
+        return "Controle volant (a bord)";
+    case 5:
+        return "Validation de test";
+    case 6:
+        return "Validation en correspondance - entree";
+    case 7:
+        return "Validation en correspondance - sortie";
+    case 9:
+        return "Annulation de validation";
+    case 10:
+        return "Validation - entree";
+    case 13:
+        return "Distribution";
+    case 15:
+        return "Invalidation";
+    default: {
+        char* transition_str = malloc(13);
+        snprintf(transition_str, 13, "Unknown (%d)", transition);
+        return transition_str;
+    }
+    }
+}
+
+const char* get_metro_station(int station_group_id, int station_id) {
+    // Use NAVIGO_H constants
+    if(station_group_id < 32 && station_id < 16) {
+        return METRO_STATION_LIST[station_group_id][station_id];
+    }
+    // cast station_group_id-station_id to a string
+    char* station = malloc(12 * sizeof(char));
+    if(!station) {
+        return "Unknown";
+    }
+    snprintf(station, 10, "%d-%d", station_group_id, station_id);
+    return station;
+}
+
+const char* get_train_line(int station_group_id) {
+    if(station_group_id < 77) {
+        return TRAIN_LINES_LIST[station_group_id];
+    }
+    return "Unknown";
+}
+
+const char* get_train_station(int station_group_id, int station_id) {
+    if(station_group_id < 77 && station_id < 19) {
+        return TRAIN_STATION_LIST[station_group_id][station_id];
+    }
+    // cast station_group_id-station_id to a string
+    char* station = malloc(12 * sizeof(char));
+    if(!station) {
+        return "Unknown";
+    }
+    snprintf(station, 10, "%d-%d", station_group_id, station_id);
+    return station;
+}
+
+void show_event_info(NavigoCardEvent* event, FuriString* parsed_data) {
+    if(event->transport_type == BUS_URBAIN || event->transport_type == BUS_INTERURBAIN ||
+       event->transport_type == METRO || event->transport_type == TRAM) {
+        if(event->route_number_available) {
+            if(event->transport_type == METRO && event->route_number == 103) {
+                furi_string_cat_printf(
+                    parsed_data,
+                    "%s 3 bis\n%s\n",
+                    get_transport_type(event->transport_type),
+                    get_transition_type(event->transition));
+            } else {
+                furi_string_cat_printf(
+                    parsed_data,
+                    "%s %d\n%s\n",
+                    get_transport_type(event->transport_type),
+                    event->route_number,
+                    get_transition_type(event->transition));
+            }
+        } else {
+            furi_string_cat_printf(
+                parsed_data,
+                "%s\n%s\n",
+                get_transport_type(event->transport_type),
+                get_transition_type(event->transition));
+        }
+        furi_string_cat_printf(
+            parsed_data, "Transporteur : %s\n", get_service_provider(event->service_provider));
+        if(event->transport_type == METRO) {
+            furi_string_cat_printf(
+                parsed_data,
+                "Station : %s\nSecteur : %s\n",
+                get_metro_station(event->station_group_id, event->station_id),
+                get_metro_station(event->station_group_id, 0));
+        } else {
+            furi_string_cat_printf(
+                parsed_data, "ID Station : %d-%d\n", event->station_group_id, event->station_id);
+        }
+        if(event->location_gate_available) {
+            furi_string_cat_printf(parsed_data, "Passage : %d\n", event->location_gate);
+        }
+        if(event->device_available) {
+            if(event->transport_type == BUS_URBAIN || event->transport_type == BUS_INTERURBAIN) {
+                const char* side = event->side == 0 ? "droit" : "gauche";
+                furi_string_cat_printf(parsed_data, "Porte : %d\nCote %s\n", event->door, side);
+            } else {
+                furi_string_cat_printf(parsed_data, "Equipement : %d\n", event->device);
+            }
+        }
+        if(event->mission_available) {
+            furi_string_cat_printf(parsed_data, "Mission : %d\n", event->mission);
+        }
+        if(event->vehicle_id_available) {
+            furi_string_cat_printf(parsed_data, "Vehicule : %d\n", event->vehicle_id);
+        }
+        if(event->used_contract_available) {
+            furi_string_cat_printf(parsed_data, "Contrat : %d\n", event->used_contract);
+        }
+        locale_format_datetime_cat(parsed_data, &event->date, true);
+        furi_string_cat_printf(parsed_data, "\n");
+    } else if(event->transport_type == TRAIN) {
+        if(event->route_number_available) {
+            furi_string_cat_printf(
+                parsed_data,
+                "RER %c\nStation : %s\n",
+                (65 + event->route_number - 17),
+                get_train_station(event->station_group_id, event->station_id));
+        } else {
+            furi_string_cat_printf(
+                parsed_data,
+                "%s %s\nStation : %s\n",
+                get_transport_type(event->transport_type),
+                get_train_line(event->station_group_id),
+                get_train_station(event->station_group_id, event->station_id));
+        }
+        if(event->route_number_available) {
+            furi_string_cat_printf(parsed_data, "Route : %d\n", event->route_number);
+        }
+        if(event->location_gate_available) {
+            furi_string_cat_printf(parsed_data, "Passage : %d\n", event->location_gate);
+        }
+        if(event->device_available) {
+            furi_string_cat_printf(parsed_data, "Equipement : %d\n", event->device);
+        }
+        if(event->mission_available) {
+            furi_string_cat_printf(parsed_data, "Mission : %d\n", event->mission);
+        }
+        if(event->vehicle_id_available) {
+            furi_string_cat_printf(parsed_data, "Vehicule : %d\n", event->vehicle_id);
+        }
+        if(event->used_contract_available) {
+            furi_string_cat_printf(parsed_data, "Contrat : %d\n", event->used_contract);
+        }
+        locale_format_datetime_cat(parsed_data, &event->date, true);
+        furi_string_cat_printf(parsed_data, "\n");
+    } else {
+        furi_string_cat_printf(
+            parsed_data,
+            "%s - %s\n",
+            get_transport_type(event->transport_type),
+            get_transition_type(event->transition));
+        furi_string_cat_printf(
+            parsed_data, "Transporteur : %s\n", get_service_provider(event->service_provider));
+        furi_string_cat_printf(
+            parsed_data, "ID Station : %d-%d\n", event->station_group_id, event->station_id);
+        if(event->location_gate_available) {
+            furi_string_cat_printf(parsed_data, "Passage : %d\n", event->location_gate);
+        }
+        if(event->device_available) {
+            furi_string_cat_printf(parsed_data, "Equipement : %d\n", event->device);
+        }
+        if(event->mission_available) {
+            furi_string_cat_printf(parsed_data, "Mission : %d\n", event->mission);
+        }
+        if(event->vehicle_id_available) {
+            furi_string_cat_printf(parsed_data, "Vehicule : %d\n", event->vehicle_id);
+        }
+        if(event->used_contract_available) {
+            furi_string_cat_printf(parsed_data, "Contrat : %d\n", event->used_contract);
+        }
+        locale_format_datetime_cat(parsed_data, &event->date, true);
+        furi_string_cat_printf(parsed_data, "\n");
+    }
+}
+
+void show_contract_info(NavigoCardContract* contract, FuriString* parsed_data) {
+    furi_string_cat_printf(parsed_data, "Balance : %.2f EUR\n", (double)contract->balance);
+    furi_string_cat_printf(parsed_data, "Debut de validite:\n");
+    locale_format_datetime_cat(parsed_data, &contract->start_dt, false);
+    furi_string_cat_printf(parsed_data, "\n");
+}
+
+void show_environment_info(NavigoCardEnv* environment, FuriString* parsed_data) {
+    furi_string_cat_printf(
+        parsed_data, "Version de l'application : %d\n", environment->app_version);
+    if(environment->country_num == 250) {
+        furi_string_cat_printf(parsed_data, "Pays : France\n");
+    } else {
+        furi_string_cat_printf(parsed_data, "Pays : %d\n", environment->country_num);
+    }
+    if(environment->network_num == 901) {
+        furi_string_cat_printf(parsed_data, "Reseau : Ile-de-France Mobilites\n");
+    } else {
+        furi_string_cat_printf(parsed_data, "Reseau : %d\n", environment->network_num);
+    }
+    furi_string_cat_printf(parsed_data, "Fin de validite:\n");
+    locale_format_datetime_cat(parsed_data, &environment->end_dt, false);
+    furi_string_cat_printf(parsed_data, "\n");
+}
+
+void update_page_info(NavigoContext* ctx, FuriString* parsed_data) {
+    if(ctx->page_id == 0) {
+        furi_string_cat_printf(parsed_data, "\e#Navigo :\n");
+        furi_string_cat_printf(parsed_data, "\e#Contrat 1:\n");
+        show_contract_info(&ctx->card->contracts[0], parsed_data);
+    } else if(ctx->page_id == 1) {
+        furi_string_cat_printf(parsed_data, "\e#Environnement :\n");
+        show_environment_info(&ctx->card->environment, parsed_data);
+    } else if(ctx->page_id == 2) {
+        furi_string_cat_printf(parsed_data, "\e#Event 1 :\n");
+        show_event_info(&ctx->card->events[0], parsed_data);
+    } else if(ctx->page_id == 3) {
+        furi_string_cat_printf(parsed_data, "\e#Event 2 :\n");
+        show_event_info(&ctx->card->events[1], parsed_data);
+    } else if(ctx->page_id == 4) {
+        furi_string_cat_printf(parsed_data, "\e#Event 3 :\n");
+        show_event_info(&ctx->card->events[2], parsed_data);
+    }
+}
+
+void update_widget_elements(Widget* widget, NavigoContext* ctx, void* context) {
+    if(ctx->page_id < 4) {
+        widget_add_button_element(
+            widget, GuiButtonTypeRight, "Next", metroflip_next_button_widget_callback, context);
+    } else {
+        widget_add_button_element(
+            widget, GuiButtonTypeRight, "Exit", metroflip_next_button_widget_callback, context);
+    }
+    if(ctx->page_id > 0) {
+        widget_add_button_element(
+            widget, GuiButtonTypeLeft, "Back", metroflip_back_button_widget_callback, context);
+    }
+}
+
+void metroflip_back_button_widget_callback(GuiButtonType result, InputType type, void* context) {
+    NavigoContext* ctx = context;
+    Metroflip* app = ctx->app;
+    UNUSED(result);
+
+    Widget* widget = app->widget;
+
+    if(type == InputTypePress) {
+        widget_reset(widget);
+
+        FuriString* parsed_data = furi_string_alloc();
+
+        FURI_LOG_I(TAG, "Page ID: %d -> %d", ctx->page_id, ctx->page_id - 1);
+
+        if(ctx->page_id > 0) {
+            ctx->page_id -= 1;
+        }
+
+        // Ensure no nested mutexes
+        furi_mutex_acquire(ctx->mutex, FuriWaitForever);
+        update_page_info(ctx, parsed_data);
+        furi_mutex_release(ctx->mutex);
+
+        widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
+
+        // Ensure no nested mutexes
+        furi_mutex_acquire(ctx->mutex, FuriWaitForever);
+        update_widget_elements(widget, ctx, context);
+        furi_mutex_release(ctx->mutex);
+
+        furi_string_free(parsed_data);
+    }
+}
+
+void metroflip_next_button_widget_callback(GuiButtonType result, InputType type, void* context) {
+    NavigoContext* ctx = context;
+    Metroflip* app = ctx->app;
+    UNUSED(result);
+
+    Widget* widget = app->widget;
+
+    if(type == InputTypePress) {
+        widget_reset(widget);
+
+        FuriString* parsed_data = furi_string_alloc();
+
+        FURI_LOG_I(TAG, "Page ID: %d -> %d", ctx->page_id, ctx->page_id + 1);
+
+        if(ctx->page_id < 4) {
+            ctx->page_id += 1;
+        } else {
+            ctx->page_id = 0;
+            scene_manager_search_and_switch_to_previous_scene(
+                app->scene_manager, MetroflipSceneStart);
+        }
+
+        // Ensure no nested mutexes
+        furi_mutex_acquire(ctx->mutex, FuriWaitForever);
+        update_page_info(ctx, parsed_data);
+        furi_mutex_release(ctx->mutex);
+
+        widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
+
+        // Ensure no nested mutexes
+        furi_mutex_acquire(ctx->mutex, FuriWaitForever);
+        update_widget_elements(widget, ctx, context);
+        furi_mutex_release(ctx->mutex);
+
+        furi_string_free(parsed_data);
+    }
+}
+
 static NfcCommand metroflip_scene_navigo_poller_callback(NfcGenericEvent event, void* context) {
 static NfcCommand metroflip_scene_navigo_poller_callback(NfcGenericEvent event, void* context) {
     furi_assert(event.protocol == NfcProtocolIso14443_4b);
     furi_assert(event.protocol == NfcProtocolIso14443_4b);
     NfcCommand next_command = NfcCommandContinue;
     NfcCommand next_command = NfcCommandContinue;
@@ -73,61 +511,35 @@ static NfcCommand metroflip_scene_navigo_poller_callback(NfcGenericEvent event,
             size_t response_length = 0;
             size_t response_length = 0;
 
 
             do {
             do {
-                // Select app for contracts
-                select_app[6] = 32;
-                bit_buffer_reset(tx_buffer);
-                bit_buffer_append_bytes(tx_buffer, select_app, sizeof(select_app));
-                error = iso14443_4b_poller_send_block(iso14443_4b_poller, tx_buffer, rx_buffer);
-                if(error != Iso14443_4bErrorNone) {
-                    FURI_LOG_I(TAG, "Select File: iso14443_4b_poller_send_block error %d", error);
-                    stage = MetroflipPollerEventTypeFail;
-                    view_dispatcher_send_custom_event(
-                        app->view_dispatcher, MetroflipCustomEventPollerFail);
+                NavigoCardData* card = malloc(sizeof(NavigoCardData));
+
+                // Initialize the card
+                card->contracts = malloc(sizeof(NavigoCardContract));
+                card->events = malloc(3 * sizeof(NavigoCardEvent));
+
+                // Select app for contract 1
+                error =
+                    select_new_app(0x20, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                if(error != 0) {
                     break;
                     break;
                 }
                 }
 
 
                 // Check the response after selecting app
                 // Check the response after selecting app
-                response_length = bit_buffer_get_size_bytes(rx_buffer);
-                if(bit_buffer_get_byte(rx_buffer, response_length - 2) != apdu_success[0] ||
-                   bit_buffer_get_byte(rx_buffer, response_length - 1) != apdu_success[1]) {
-                    FURI_LOG_I(
-                        TAG,
-                        "Select profile app failed: %02x%02x",
-                        bit_buffer_get_byte(rx_buffer, response_length - 2),
-                        bit_buffer_get_byte(rx_buffer, response_length - 1));
-                    stage = MetroflipPollerEventTypeFail;
-                    view_dispatcher_send_custom_event(
-                        app->view_dispatcher, MetroflipCustomEventPollerFileNotFound);
+                if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
                     break;
                     break;
                 }
                 }
 
 
                 // read file 1
                 // read file 1
-                read_file[2] = 1;
-                bit_buffer_reset(tx_buffer);
-                bit_buffer_append_bytes(tx_buffer, read_file, sizeof(read_file));
-                error = iso14443_4b_poller_send_block(iso14443_4b_poller, tx_buffer, rx_buffer);
-                if(error != Iso14443_4bErrorNone) {
-                    FURI_LOG_I(TAG, "Read File: iso14443_4b_poller_send_block error %d", error);
-                    stage = MetroflipPollerEventTypeFail;
-                    view_dispatcher_send_custom_event(
-                        app->view_dispatcher, MetroflipCustomEventPollerFail);
+                error = read_new_file(1, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                if(error != 0) {
                     break;
                     break;
                 }
                 }
 
 
                 // Check the response after reading the file
                 // Check the response after reading the file
-                response_length = bit_buffer_get_size_bytes(rx_buffer);
-                if(bit_buffer_get_byte(rx_buffer, response_length - 2) != apdu_success[0] ||
-                   bit_buffer_get_byte(rx_buffer, response_length - 1) != apdu_success[1]) {
-                    FURI_LOG_I(
-                        TAG,
-                        "Read file failed: %02x%02x",
-                        bit_buffer_get_byte(rx_buffer, response_length - 2),
-                        bit_buffer_get_byte(rx_buffer, response_length - 1));
-                    stage = MetroflipPollerEventTypeFail;
-                    view_dispatcher_send_custom_event(
-                        app->view_dispatcher, MetroflipCustomEventPollerFileNotFound);
+                if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
                     break;
                     break;
                 }
                 }
+
                 char bit_representation[response_length * 8 + 1];
                 char bit_representation[response_length * 8 + 1];
                 bit_representation[0] = '\0';
                 bit_representation[0] = '\0';
                 for(size_t i = 0; i < response_length; i++) {
                 for(size_t i = 0; i < response_length; i++) {
@@ -139,74 +551,34 @@ static NfcCommand metroflip_scene_navigo_poller_callback(NfcGenericEvent event,
                 bit_representation[response_length * 8] = '\0';
                 bit_representation[response_length * 8] = '\0';
                 int start = 55, end = 70;
                 int start = 55, end = 70;
                 float decimal_value = bit_slice_to_dec(bit_representation, start, end);
                 float decimal_value = bit_slice_to_dec(bit_representation, start, end);
-                float balance = decimal_value / 100;
-                furi_string_printf(parsed_data, "\e#Navigo:\n\n");
-                furi_string_cat_printf(parsed_data, "\e#Contract 1:\n");
-                furi_string_cat_printf(parsed_data, "Balance: %.2f EUR\n", (double)balance);
+                card->contracts[0].balance = decimal_value / 100;
                 start = 80, end = 93;
                 start = 80, end = 93;
                 decimal_value = bit_slice_to_dec(bit_representation, start, end);
                 decimal_value = bit_slice_to_dec(bit_representation, start, end);
                 uint64_t start_date_timestamp = (decimal_value * 24 * 3600) + (float)epoch + 3600;
                 uint64_t start_date_timestamp = (decimal_value * 24 * 3600) + (float)epoch + 3600;
-                DateTime start_dt = {0};
-                datetime_timestamp_to_datetime(start_date_timestamp, &start_dt);
-                furi_string_cat_printf(parsed_data, "\nStart Date:\n");
-                locale_format_datetime_cat(parsed_data, &start_dt, false);
-                furi_string_cat_printf(parsed_data, "\n");
+                datetime_timestamp_to_datetime(start_date_timestamp, &card->contracts[0].start_dt);
 
 
                 // Select app for environment
                 // Select app for environment
-                select_app[6] = 1;
-                bit_buffer_reset(tx_buffer);
-                bit_buffer_append_bytes(tx_buffer, select_app, sizeof(select_app));
-                error = iso14443_4b_poller_send_block(iso14443_4b_poller, tx_buffer, rx_buffer);
-                if(error != Iso14443_4bErrorNone) {
-                    FURI_LOG_I(TAG, "Select File: iso14443_4b_poller_send_block error %d", error);
-                    stage = MetroflipPollerEventTypeFail;
-                    view_dispatcher_send_custom_event(
-                        app->view_dispatcher, MetroflipCustomEventPollerFail);
+                error = select_new_app(0x1, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                if(error != 0) {
                     break;
                     break;
                 }
                 }
 
 
                 // Check the response after selecting app
                 // Check the response after selecting app
-                response_length = bit_buffer_get_size_bytes(rx_buffer);
-                if(bit_buffer_get_byte(rx_buffer, response_length - 2) != apdu_success[0] ||
-                   bit_buffer_get_byte(rx_buffer, response_length - 1) != apdu_success[1]) {
-                    FURI_LOG_I(
-                        TAG,
-                        "Select profile app failed: %02x%02x",
-                        bit_buffer_get_byte(rx_buffer, response_length - 2),
-                        bit_buffer_get_byte(rx_buffer, response_length - 1));
-                    stage = MetroflipPollerEventTypeFail;
-                    view_dispatcher_send_custom_event(
-                        app->view_dispatcher, MetroflipCustomEventPollerFileNotFound);
+                if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
                     break;
                     break;
                 }
                 }
 
 
                 // read file 1
                 // read file 1
-                read_file[2] = 1;
-                bit_buffer_reset(tx_buffer);
-                bit_buffer_append_bytes(tx_buffer, read_file, sizeof(read_file));
-                error = iso14443_4b_poller_send_block(iso14443_4b_poller, tx_buffer, rx_buffer);
-                if(error != Iso14443_4bErrorNone) {
-                    FURI_LOG_I(TAG, "Read File: iso14443_4b_poller_send_block error %d", error);
-                    stage = MetroflipPollerEventTypeFail;
-                    view_dispatcher_send_custom_event(
-                        app->view_dispatcher, MetroflipCustomEventPollerFail);
+                error = read_new_file(1, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                if(error != 0) {
                     break;
                     break;
                 }
                 }
 
 
                 // Check the response after reading the file
                 // Check the response after reading the file
-                response_length = bit_buffer_get_size_bytes(rx_buffer);
-                if(bit_buffer_get_byte(rx_buffer, response_length - 2) != apdu_success[0] ||
-                   bit_buffer_get_byte(rx_buffer, response_length - 1) != apdu_success[1]) {
-                    FURI_LOG_I(
-                        TAG,
-                        "Read file failed: %02x%02x",
-                        bit_buffer_get_byte(rx_buffer, response_length - 2),
-                        bit_buffer_get_byte(rx_buffer, response_length - 1));
-                    stage = MetroflipPollerEventTypeFail;
-                    view_dispatcher_send_custom_event(
-                        app->view_dispatcher, MetroflipCustomEventPollerFileNotFound);
+                if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
                     break;
                     break;
                 }
                 }
+
                 char environment_bit_representation[response_length * 8 + 1];
                 char environment_bit_representation[response_length * 8 + 1];
                 environment_bit_representation[0] = '\0';
                 environment_bit_representation[0] = '\0';
                 for(size_t i = 0; i < response_length; i++) {
                 for(size_t i = 0; i < response_length; i++) {
@@ -218,74 +590,56 @@ static NfcCommand metroflip_scene_navigo_poller_callback(NfcGenericEvent event,
                         bits,
                         bits,
                         sizeof(environment_bit_representation));
                         sizeof(environment_bit_representation));
                 }
                 }
+                start = 0;
+                end = 5;
+                card->environment.app_version =
+                    bit_slice_to_dec(environment_bit_representation, start, end);
+                start = 13;
+                end = 36;
+                decimal_value = bit_slice_to_dec(environment_bit_representation, start, end);
+                FURI_LOG_I(TAG, "Network ID: %d", (int)decimal_value);
+                start = 13;
+                end = 16;
+                card->environment.country_num =
+                    bit_slice_to_dec(environment_bit_representation, start, end) * 100 +
+                    bit_slice_to_dec(environment_bit_representation, start + 4, end + 4) * 10 +
+                    bit_slice_to_dec(environment_bit_representation, start + 8, end + 8);
+                start = 25;
+                end = 28;
+                card->environment.network_num =
+                    bit_slice_to_dec(environment_bit_representation, start, end) * 100 +
+                    bit_slice_to_dec(environment_bit_representation, start + 4, end + 4) * 10 +
+                    bit_slice_to_dec(environment_bit_representation, start + 8, end + 8);
                 start = 45;
                 start = 45;
                 end = 58;
                 end = 58;
                 decimal_value = bit_slice_to_dec(environment_bit_representation, start, end);
                 decimal_value = bit_slice_to_dec(environment_bit_representation, start, end);
                 uint64_t end_validity_timestamp =
                 uint64_t end_validity_timestamp =
                     (decimal_value * 24 * 3600) + (float)epoch + 3600;
                     (decimal_value * 24 * 3600) + (float)epoch + 3600;
-                DateTime end_dt = {0};
-                datetime_timestamp_to_datetime(end_validity_timestamp, &end_dt);
-                furi_string_cat_printf(parsed_data, "\nEnd Validity:\n");
-                locale_format_datetime_cat(parsed_data, &end_dt, false);
-                furi_string_cat_printf(parsed_data, "\n\n");
+                datetime_timestamp_to_datetime(end_validity_timestamp, &card->environment.end_dt);
 
 
                 // Select app for events
                 // Select app for events
-                select_app[6] = 16;
-                bit_buffer_reset(tx_buffer);
-                bit_buffer_append_bytes(tx_buffer, select_app, sizeof(select_app));
-                error = iso14443_4b_poller_send_block(iso14443_4b_poller, tx_buffer, rx_buffer);
-                if(error != Iso14443_4bErrorNone) {
-                    FURI_LOG_I(TAG, "Select File: iso14443_4b_poller_send_block error %d", error);
-                    stage = MetroflipPollerEventTypeFail;
-                    view_dispatcher_send_custom_event(
-                        app->view_dispatcher, MetroflipCustomEventPollerFail);
+                error =
+                    select_new_app(0x10, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                if(error != 0) {
                     break;
                     break;
                 }
                 }
 
 
                 // Check the response after selecting app
                 // Check the response after selecting app
-                response_length = bit_buffer_get_size_bytes(rx_buffer);
-                if(bit_buffer_get_byte(rx_buffer, response_length - 2) != apdu_success[0] ||
-                   bit_buffer_get_byte(rx_buffer, response_length - 1) != apdu_success[1]) {
-                    FURI_LOG_I(
-                        TAG,
-                        "Select events app failed: %02x%02x",
-                        bit_buffer_get_byte(rx_buffer, response_length - 2),
-                        bit_buffer_get_byte(rx_buffer, response_length - 1));
-                    stage = MetroflipPollerEventTypeFail;
-                    view_dispatcher_send_custom_event(
-                        app->view_dispatcher, MetroflipCustomEventPollerFileNotFound);
+                if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
                     break;
                     break;
                 }
                 }
 
 
-                furi_string_cat_printf(parsed_data, "\e#Events:\n");
+                // furi_string_cat_printf(parsed_data, "\e#Events :\n");
                 // Now send the read command
                 // Now send the read command
                 for(size_t i = 1; i < 4; i++) {
                 for(size_t i = 1; i < 4; i++) {
-                    read_file[2] = i;
-                    bit_buffer_reset(tx_buffer);
-                    bit_buffer_append_bytes(tx_buffer, read_file, sizeof(read_file));
                     error =
                     error =
-                        iso14443_4b_poller_send_block(iso14443_4b_poller, tx_buffer, rx_buffer);
-                    if(error != Iso14443_4bErrorNone) {
-                        FURI_LOG_I(
-                            TAG, "Read File: iso14443_4b_poller_send_block error %d", error);
-                        stage = MetroflipPollerEventTypeFail;
-                        view_dispatcher_send_custom_event(
-                            app->view_dispatcher, MetroflipCustomEventPollerFail);
+                        read_new_file(i, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                    if(error != 0) {
                         break;
                         break;
                     }
                     }
 
 
                     // Check the response after reading the file
                     // Check the response after reading the file
-                    response_length = bit_buffer_get_size_bytes(rx_buffer);
-                    if(bit_buffer_get_byte(rx_buffer, response_length - 2) != apdu_success[0] ||
-                       bit_buffer_get_byte(rx_buffer, response_length - 1) != apdu_success[1]) {
-                        FURI_LOG_I(
-                            TAG,
-                            "Read file failed: %02x%02x",
-                            bit_buffer_get_byte(rx_buffer, response_length - 2),
-                            bit_buffer_get_byte(rx_buffer, response_length - 1));
-                        stage = MetroflipPollerEventTypeFail;
-                        view_dispatcher_send_custom_event(
-                            app->view_dispatcher, MetroflipCustomEventPollerFileNotFound);
+                    if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
                         break;
                         break;
                     }
                     }
 
 
@@ -297,71 +651,183 @@ static NfcCommand metroflip_scene_navigo_poller_callback(NfcGenericEvent event,
                         byte_to_binary(byte, bits);
                         byte_to_binary(byte, bits);
                         strlcat(event_bit_representation, bits, sizeof(event_bit_representation));
                         strlcat(event_bit_representation, bits, sizeof(event_bit_representation));
                     }
                     }
+                    FURI_LOG_I(
+                        TAG, "Event %d bit_representation: %s", i, event_bit_representation);
 
 
-                    furi_string_cat_printf(parsed_data, "\nEvent 0%d:\n", i);
+                    // furi_string_cat_printf(parsed_data, "Event 0%d :\n", i);
                     int count = 0;
                     int count = 0;
                     int start = 25, end = 53;
                     int start = 25, end = 53;
                     char bit_slice[end - start + 2];
                     char bit_slice[end - start + 2];
                     strncpy(bit_slice, event_bit_representation + start, end - start + 1);
                     strncpy(bit_slice, event_bit_representation + start, end - start + 1);
                     bit_slice[end - start + 1] = '\0';
                     bit_slice[end - start + 1] = '\0';
                     int* positions = get_bit_positions(bit_slice, &count);
                     int* positions = get_bit_positions(bit_slice, &count);
+                    /*FURI_LOG_I(TAG, "Positions: ");
                     for(int i = 0; i < count; i++) {
                     for(int i = 0; i < count; i++) {
                         FURI_LOG_I(TAG, "%d ", positions[i]);
                         FURI_LOG_I(TAG, "%d ", positions[i]);
-                    }
+                    }*/
 
 
+                    // 2. EventCode
+                    // 8 bits
                     int event_number = 2;
                     int event_number = 2;
                     if(is_event_present(positions, count, event_number)) {
                     if(is_event_present(positions, count, event_number)) {
                         int positionOffset = check_events(positions, count, event_number);
                         int positionOffset = check_events(positions, count, event_number);
                         int start = positionOffset, end = positionOffset + 7;
                         int start = positionOffset, end = positionOffset + 7;
                         int decimal_value = bit_slice_to_dec(event_bit_representation, start, end);
                         int decimal_value = bit_slice_to_dec(event_bit_representation, start, end);
-                        int transport_type = decimal_value >> 4;
-                        int transition = decimal_value & 15;
-                        furi_string_cat_printf(
-                            parsed_data,
-                            "%s - %s\n",
-                            TRANSPORT_LIST[transport_type],
-                            TRANSITION_LIST[transition]);
+                        card->events[i - 1].transport_type = decimal_value >> 4;
+                        card->events[i - 1].transition = decimal_value & 15;
+                        FURI_LOG_I(
+                            TAG,
+                            "%s - %s",
+                            TRANSPORT_LIST[card->events[i - 1].transport_type],
+                            TRANSITION_LIST[card->events[i - 1].transition]);
                     }
                     }
 
 
+                    // 4. EventServiceProvider
+                    // 8 bits
                     event_number = 4;
                     event_number = 4;
                     if(is_event_present(positions, count, event_number)) {
                     if(is_event_present(positions, count, event_number)) {
                         int positionOffset = check_events(positions, count, event_number);
                         int positionOffset = check_events(positions, count, event_number);
                         start = positionOffset, end = positionOffset + 7;
                         start = positionOffset, end = positionOffset + 7;
-                        int decimal_value = bit_slice_to_dec(event_bit_representation, start, end);
-                        furi_string_cat_printf(
-                            parsed_data, "Provider: %s\n", SERVICE_PROVIDERS[decimal_value]);
+                        card->events[i - 1].service_provider =
+                            bit_slice_to_dec(event_bit_representation, start, end);
+                        FURI_LOG_I(
+                            TAG,
+                            "Transporteur : %s",
+                            SERVICE_PROVIDERS[card->events[i - 1].service_provider]);
                     }
                     }
 
 
+                    // 8. EventLocationId
+                    // 16 bits
                     event_number = 8;
                     event_number = 8;
                     if(is_event_present(positions, count, event_number)) {
                     if(is_event_present(positions, count, event_number)) {
                         int positionOffset = check_events(positions, count, event_number);
                         int positionOffset = check_events(positions, count, event_number);
                         start = positionOffset, end = positionOffset + 15;
                         start = positionOffset, end = positionOffset + 15;
                         int decimal_value = bit_slice_to_dec(event_bit_representation, start, end);
                         int decimal_value = bit_slice_to_dec(event_bit_representation, start, end);
-                        int line_id = decimal_value >> 9;
-                        int station_id = (decimal_value >> 4) & 31;
-                        furi_string_cat_printf(
-                            parsed_data,
-                            "Line: %s\nStation: %s\n",
-                            STATION_LIST[line_id][0],
-                            STATION_LIST[line_id][station_id]);
+                        card->events[i - 1].station_group_id = decimal_value >> 9;
+                        card->events[i - 1].station_id = (decimal_value >> 4) & 31;
+                        if(card->events[i - 1].transport_type == METRO) {
+                            FURI_LOG_I(
+                                TAG,
+                                "Secteur %s - Station %s",
+                                METRO_STATION_LIST[card->events[i - 1].station_group_id][0],
+                                METRO_STATION_LIST[card->events[i - 1].station_group_id]
+                                                  [card->events[i - 1].station_id]);
+                        } else if(card->events[i - 1].transport_type == TRAIN) {
+                            FURI_LOG_I(
+                                TAG,
+                                "Ligne %s - Station %s",
+                                TRAIN_LINES_LIST[card->events[i - 1].station_group_id],
+                                TRAIN_STATION_LIST[card->events[i - 1].station_group_id]
+                                                  [card->events[i - 1].station_id]);
+                        } else {
+                            FURI_LOG_I(
+                                TAG,
+                                "Groupe ID %d - Station ID %d",
+                                card->events[i - 1].station_group_id,
+                                card->events[i - 1].station_id);
+                        }
+                    }
+
+                    // 9. EventLocationGate
+                    // 8 bits
+                    event_number = 9;
+                    if(is_event_present(positions, count, event_number)) {
+                        int positionOffset = check_events(positions, count, event_number);
+                        start = positionOffset, end = positionOffset + 7;
+                        card->events[i - 1].location_gate =
+                            bit_slice_to_dec(event_bit_representation, start, end);
+                        card->events[i - 1].location_gate_available = true;
+                        FURI_LOG_I(TAG, "Passage : %d", card->events[i - 1].location_gate);
+                    }
+
+                    // 10. EventDevice
+                    // 16 bits
+                    event_number = 10;
+                    if(is_event_present(positions, count, event_number)) {
+                        int positionOffset = check_events(positions, count, event_number);
+                        start = positionOffset, end = positionOffset + 15;
+                        int decimal_value = bit_slice_to_dec(event_bit_representation, start, end);
+                        card->events[i - 1].device = decimal_value >> 8;
+                        card->events[i - 1].door = card->events[i - 1].device / 2 + 1;
+                        card->events[i - 1].side = card->events[i - 1].device % 2;
+                        card->events[i - 1].device_available = true;
+                        const char* side = card->events[i - 1].side == 0 ? "droit" : "gauche";
+                        FURI_LOG_I(TAG, "Equipement : %d", card->events[i - 1].device);
+                        FURI_LOG_I(TAG, "Porte : %d - Côté %s", card->events[i - 1].door, side);
+                    }
+
+                    // 11. EventRouteNumber
+                    // 16 bits
+                    event_number = 11;
+                    if(is_event_present(positions, count, event_number)) {
+                        int positionOffset = check_events(positions, count, event_number);
+                        start = positionOffset, end = positionOffset + 15;
+                        card->events[i - 1].route_number =
+                            bit_slice_to_dec(event_bit_representation, start, end);
+                        card->events[i - 1].route_number_available = true;
+                        FURI_LOG_I(TAG, "Route : %d", card->events[i - 1].route_number);
+                    }
+
+                    // 13. EventJourneyRun
+                    // 16 bits
+                    event_number = 13;
+                    if(is_event_present(positions, count, event_number)) {
+                        int positionOffset = check_events(positions, count, event_number);
+                        start = positionOffset, end = positionOffset + 15;
+                        card->events[i - 1].mission =
+                            bit_slice_to_dec(event_bit_representation, start, end);
+                        card->events[i - 1].mission_available = true;
+                        FURI_LOG_I(TAG, "Mission : %d", card->events[i - 1].mission);
                     }
                     }
+
+                    // 14. EventVehicleId
+                    // 16 bits
+                    event_number = 14;
+                    if(is_event_present(positions, count, event_number)) {
+                        int positionOffset = check_events(positions, count, event_number);
+                        start = positionOffset, end = positionOffset + 15;
+                        card->events[i - 1].vehicle_id =
+                            bit_slice_to_dec(event_bit_representation, start, end);
+                        card->events[i - 1].vehicle_id_available = true;
+                        FURI_LOG_I(TAG, "Vehicule : %d", card->events[i - 1].vehicle_id);
+                    }
+
+                    // 25. EventContractPointer
+                    // 5 bits
+                    event_number = 25;
+                    if(is_event_present(positions, count, event_number)) {
+                        int positionOffset = check_events(positions, count, event_number);
+                        start = positionOffset, end = positionOffset + 4;
+                        card->events[i - 1].used_contract =
+                            bit_slice_to_dec(event_bit_representation, start, end);
+                        card->events[i - 1].used_contract_available = true;
+                        FURI_LOG_I(TAG, "Contrat : %d", card->events[i - 1].used_contract);
+                    }
+
                     free(positions);
                     free(positions);
 
 
+                    // EventDate
+                    // 14 bits
                     start = 0, end = 13;
                     start = 0, end = 13;
                     int decimal_value = bit_slice_to_dec(event_bit_representation, start, end);
                     int decimal_value = bit_slice_to_dec(event_bit_representation, start, end);
                     uint64_t date_timestamp = (decimal_value * 24 * 3600) + epoch + 3600;
                     uint64_t date_timestamp = (decimal_value * 24 * 3600) + epoch + 3600;
-                    DateTime dt = {0};
-                    datetime_timestamp_to_datetime(date_timestamp, &dt);
-                    furi_string_cat_printf(parsed_data, "Time: ");
-                    locale_format_datetime_cat(parsed_data, &dt, false);
+                    datetime_timestamp_to_datetime(date_timestamp, &card->events[i - 1].date);
+
+                    // EventTime
+                    // 11 bits
                     start = 14, end = 24;
                     start = 14, end = 24;
                     decimal_value = bit_slice_to_dec(event_bit_representation, start, end);
                     decimal_value = bit_slice_to_dec(event_bit_representation, start, end);
-                    furi_string_cat_printf(
-                        parsed_data,
-                        " %02d:%02d:%02d\n\n",
-                        ((decimal_value * 60) / 3600),
-                        (((decimal_value * 60) % 3600) / 60),
-                        (((decimal_value * 60) % 3600) % 60));
+                    card->events[i - 1].date.hour = (decimal_value * 60) / 3600;
+                    card->events[i - 1].date.minute = ((decimal_value * 60) % 3600) / 60;
+                    card->events[i - 1].date.second = ((decimal_value * 60) % 3600) % 60;
+                    FURI_LOG_I(
+                        TAG,
+                        "Date : %02d/%02d/%04d %02dh%02d",
+                        card->events[i - 1].date.day,
+                        card->events[i - 1].date.month,
+                        card->events[i - 1].date.year,
+                        card->events[i - 1].date.hour,
+                        card->events[i - 1].date.minute);
                 }
                 }
                 UNUSED(TRANSITION_LIST);
                 UNUSED(TRANSITION_LIST);
                 UNUSED(TRANSPORT_LIST);
                 UNUSED(TRANSPORT_LIST);
@@ -370,8 +836,24 @@ static NfcCommand metroflip_scene_navigo_poller_callback(NfcGenericEvent event,
                 widget_add_text_scroll_element(
                 widget_add_text_scroll_element(
                     widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
                     widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
 
 
-                widget_add_button_element(
-                    widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
+                NavigoContext* context = malloc(sizeof(NavigoContext));
+                context->app = app;
+                context->card = card;
+                context->page_id = 0;
+                context->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
+
+                // Ensure no nested mutexes
+                furi_mutex_acquire(context->mutex, FuriWaitForever);
+                update_page_info(context, parsed_data);
+                furi_mutex_release(context->mutex);
+
+                widget_add_text_scroll_element(
+                    widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
+
+                // Ensure no nested mutexes
+                furi_mutex_acquire(context->mutex, FuriWaitForever);
+                update_widget_elements(widget, context, context);
+                furi_mutex_release(context->mutex);
 
 
                 furi_string_free(parsed_data);
                 furi_string_free(parsed_data);
                 view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
                 view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);

+ 391 - 13
scenes/navigo.h

@@ -1,32 +1,115 @@
+#include "../metroflip_i.h"
+#include <stdbool.h>
+#include <datetime.h>
+
 #ifndef METRO_LIST_H
 #ifndef METRO_LIST_H
 #define METRO_LIST_H
 #define METRO_LIST_H
 
 
 #ifndef NAVIGO_H
 #ifndef NAVIGO_H
 #define NAVIGO_H
 #define NAVIGO_H
 
 
+void metroflip_back_button_widget_callback(GuiButtonType result, InputType type, void* context);
+void metroflip_next_button_widget_callback(GuiButtonType result, InputType type, void* context);
+
+typedef struct {
+    int transport_type;
+    int transition;
+    int service_provider;
+    int station_group_id;
+    int station_id;
+    int location_gate;
+    bool location_gate_available;
+    int device;
+    int door;
+    int side;
+    bool device_available;
+    int route_number;
+    bool route_number_available;
+    int mission;
+    bool mission_available;
+    int vehicle_id;
+    bool vehicle_id_available;
+    int used_contract;
+    bool used_contract_available;
+    DateTime date;
+} NavigoCardEvent;
+
+typedef struct {
+    int app_version;
+    int country_num;
+    int network_num;
+    DateTime end_dt;
+} NavigoCardEnv;
+
+typedef struct {
+    float balance;
+    DateTime start_dt;
+} NavigoCardContract;
+
+typedef struct {
+    NavigoCardEnv environment;
+    NavigoCardContract* contracts;
+    NavigoCardEvent* events;
+} NavigoCardData;
+
+typedef struct {
+    Metroflip* app;
+    NavigoCardData* card;
+    int page_id;
+    // mutex
+    FuriMutex* mutex;
+} NavigoContext;
+
+/* // Navigo Card Subscriptions Types
+static const char* SUBSCRIPTIONS_LIST[] = {
+    [1] = "Navigo decouverte",
+    [2] = "Navigo standard",
+    [6] = " Navigo integral",
+    [14] = "Imagine R (etudiant)"}; */
+
 // Service Providers
 // Service Providers
-static const char* SERVICE_PROVIDERS[] = {[2] = "SNCF", [3] = "RATP"};
+static const char* SERVICE_PROVIDERS[] = {
+    [2] = "SNCF",
+    [3] = "RATP",
+    [115] = "CSO (VEOLIA)",
+    [116] = "R'Bus (VEOLIA)",
+    [156] = "Phebus",
+    [175] = "RATP (Veolia Transport Nanterre)"};
 
 
 // Transport Types
 // Transport Types
 static const char* TRANSPORT_LIST[] = {
 static const char* TRANSPORT_LIST[] = {
-    [1] = "Urban Bus",
-    [2] = "Interurban Bus",
+    [1] = "Bus Urbain",
+    [2] = "Bus Interurbain",
     [3] = "Metro",
     [3] = "Metro",
     [4] = "Tram",
     [4] = "Tram",
     [5] = "Train",
     [5] = "Train",
     [8] = "Parking"};
     [8] = "Parking"};
 
 
+typedef enum {
+    BUS_URBAIN = 1,
+    BUS_INTERURBAIN = 2,
+    METRO = 3,
+    TRAM = 4,
+    TRAIN = 5,
+    PARKING = 8
+} TRANSPORT_TYPE;
+
 // Transition Types
 // Transition Types
 static const char* TRANSITION_LIST[] = {
 static const char* TRANSITION_LIST[] = {
-    [1] = "Entry",
-    [2] = "Exit",
-    [4] = "Inspection",
-    [6] = "Interchange (entry)",
-    [7] = "Interchange (exit)"};
+    [1] = "Validation en entree",
+    [2] = "Validation en sortie",
+    [4] = "Controle volant (a bord)",
+    [5] = "Validation de test",
+    [6] = "Validation en correspondance (entree)",
+    [7] = "Validation en correspondance (sortie)",
+    [9] = "Annulation de validation",
+    [10] = "Validation en entree",
+    [13] = "Distribution",
+    [15] = "Invalidation"};
 
 
 #endif // NAVIGO_H
 #endif // NAVIGO_H
 
 
-static const char* STATION_LIST[32][16] =
+static const char* METRO_STATION_LIST[32][16] =
     {[1] =
     {[1] =
          {[0] = "Cite",
          {[0] = "Cite",
           [1] = "Saint-Michel",
           [1] = "Saint-Michel",
@@ -40,7 +123,7 @@ static const char* STATION_LIST[32][16] =
           [12] = "Louvre - Rivoli",
           [12] = "Louvre - Rivoli",
           [13] = "Pont Neuf",
           [13] = "Pont Neuf",
           [14] = "Cite",
           [14] = "Cite",
-          [15] = "Hôtel de Ville"},
+          [15] = "Hotel de Ville"},
      [2] =
      [2] =
          {[0] = "Rennes",
          {[0] = "Rennes",
           [2] = "Cambronne",
           [2] = "Cambronne",
@@ -62,7 +145,7 @@ static const char* STATION_LIST[32][16] =
           [6] = "Fort d'Aubervilliers",
           [6] = "Fort d'Aubervilliers",
           [7] = "La Courneuve - 8 Mai 1945",
           [7] = "La Courneuve - 8 Mai 1945",
           [9] = "Hoche",
           [9] = "Hoche",
-          [10] = "eglise de Pantin",
+          [10] = "Eglise de Pantin",
           [11] = "Bobigny - Pantin - Raymond Queneau",
           [11] = "Bobigny - Pantin - Raymond Queneau",
           [12] = "Bobigny - Pablo Picasso"},
           [12] = "Bobigny - Pablo Picasso"},
      [4] =
      [4] =
@@ -238,7 +321,7 @@ static const char* STATION_LIST[32][16] =
           [14] = "Boissiere",
           [14] = "Boissiere",
           [15] = "Trocadero"},
           [15] = "Trocadero"},
      [18] =
      [18] =
-         {[0] = "etoile",
+         {[0] = "Etoile",
           [1] = "Iena",
           [1] = "Iena",
           [3] = "Alma - Marceau",
           [3] = "Alma - Marceau",
           [4] = "Miromesnil",
           [4] = "Miromesnil",
@@ -380,7 +463,7 @@ static const char* STATION_LIST[32][16] =
           [5] = "Pereire",
           [5] = "Pereire",
           [8] = "Brochant",
           [8] = "Brochant",
           [9] = "Porte de Clichy",
           [9] = "Porte de Clichy",
-          [12] = "Guy Môquet",
+          [12] = "Guy Moquet",
           [13] = "Porte de Saint-Ouen"},
           [13] = "Porte de Saint-Ouen"},
      [31] = {
      [31] = {
          [0] = "Pigalle",
          [0] = "Pigalle",
@@ -396,4 +479,299 @@ static const char* STATION_LIST[32][16] =
          [12] = "Rome",
          [12] = "Rome",
          [13] = "Place de Clichy",
          [13] = "Place de Clichy",
          [14] = "La Fourche"}};
          [14] = "La Fourche"}};
+
+static const char* TRAIN_LINES_LIST[77] = {
+    [1] = "RER B",         [3] = "RER B",         [6] = "RER A",         [14] = "RER B",
+    [15] = "RER B",        [16] = "RER A",        [17] = "RER A",        [18] = "RER B",
+    [20] = "Transilien P", [21] = "Transilien P", [22] = "T4",           [23] = "Transilien P",
+    [26] = "RER A",        [28] = "RER B",        [30] = "Transilien L", [31] = "Transilien L",
+    [32] = "Transilien J", [33] = "RER A",        [35] = "Transilien J", [40] = "RER D",
+    [41] = "RER C",        [42] = "RER C",        [43] = "Transilien R", [44] = "Transilien R",
+    [45] = "RER D",        [50] = "Transilien H", [51] = "Transilien K", [52] = "RER D",
+    [53] = "Transilien H", [54] = "Transilien J", [55] = "RER C",        [56] = "Transilien H",
+    [57] = "Transilien H", [60] = "Transilien N", [61] = "Transilien N", [63] = "RER C",
+    [64] = "RER C",        [65] = "Transilien V", [70] = "RER B",        [72] = "Transilien J",
+    [73] = "Transilien J", [75] = "RER C",        [76] = "RER C"};
+
+static const char* TRAIN_STATION_LIST[77][19] = {
+    [1] = {[0] = "Châtelet-Les Halles", [1] = "Châtelet-Les Halles", [7] = "Luxembourg"},
+    [3] = {[0] = "Saint-Michel Notre-Dame"},
+    [6] = {[0] = "Auber", [6] = "Auber"},
+    [14] = {[4] = "Cite Universitaire"},
+    [15] = {[12] = "Port Royal"},
+    [16] =
+        {[1] = "Nation",
+         [2] = "Fontenay-sous-Bois | Vincennes",
+         [3] = "Joinville-le-Pont | Nogent-sur-Marne",
+         [4] = "Saint-Maur Creteil",
+         [5] = "Le Parc de Saint-Maur",
+         [6] = "Champigny",
+         [7] = "La Varenne-Chennevieres",
+         [8] = "Boissy-Saint-Leger | Sucy Bonneuil"},
+    [17] =
+        {[1] = "Charles de Gaulle-Etoile",
+         [4] = "La Defense (Grande Arche)",
+         [5] = "Nanterre-Ville",
+         [6] = "Rueil-Malmaison",
+         [8] = "Chatou-Croissy",
+         [9] = "Le Vesinet-Centre | Le Vesinet-Le Pecq | Saint-Germain-en-Laye"},
+    [18] =
+        {[0] = "Denfert-Rochereau",
+         [1] = "Gentilly",
+         [2] = "Arcueil-Cachan | Laplace",
+         [3] = "Bagneux | Bourg-la-Reine",
+         [4] = "La Croix-de-Berny | Parc de Sceaux",
+         [5] = "Antony | Fontaine-Michalon | Les Baconnets",
+         [6] = "Massy-Palaiseau | Massy-Verrieres",
+         [7] = "Palaiseau Villebon | Palaiseau",
+         [8] = "Lozere",
+         [9] = "Le Guichet | Orsay-Ville",
+         [10] =
+             "Bures-sur-Yvette | Courcelle-sur-Yvette | Gif-sur-Yvette | La Hacquiniere | Saint-Remy-les-Chevreuse"},
+    [20] =
+        {[1] = "Gare de l'Est",
+         [4] = "Pantin",
+         [5] = "Noisy-le-Sec",
+         [6] = "Bondy",
+         [7] = "Gagny | Le Raincy Villemomble Montfermeil",
+         [9] = "Chelles Gournay | Le Chenay Gagny",
+         [10] = "Vaires Torcy",
+         [11] = "Lagny-Thorigny",
+         [13] = "Esbly",
+         [14] = "Meaux",
+         [15] = "Changis-Saint-Jean | Isles-Armentieres Congis | Lizy-sur-Ourcq | Trilport",
+         [16] = "Crouy-sur-Ourcq | La Ferte-sous-Jouarre | Nanteuil Saacy"},
+    [21] =
+        {[5] = "Rosny-Bois-Perrier | Rosny-sous-Bois | Val de Fontenay",
+         [6] = "Nogent Le-Perreux",
+         [7] = "Les Boullereaux Champigny",
+         [8] = "Villiers-sur-Marne Plessis-Trevise",
+         [9] = "Les Yvris Noisy-le-Grand",
+         [10] = "Emerainville Pontault-Combault | Roissy-en-Brie",
+         [11] = "Ozoir-la-Ferriere",
+         [12] = "Gretz-Armainvilliers | Tournan",
+         [15] =
+             "Courquetaine | Faremoutiers Pommeuse | Guerard La-Celle-sur-Morin | Liverdy en Brie | Marles-en-Brie | Mormant | Mortcerf | Mouroux | Ozouer le voulgis | Verneuil-l'Etang | Villepatour - Presles | Yebles - Guignes | Yebles",
+         [16] =
+             "Chailly Boissy-le-Châtel | Chauffry | Coulommiers | Jouy-sur-Morin Le-Marais | Nangis | Saint-Remy-la-Vanne | Saint-Simeon",
+         [17] =
+             "Champbenoist-Poigny | La Ferte-Gaucher | Longueville | Provins | Sainte-Colombe-Septveilles",
+         [18] = "Flamboin | Meilleray | Villiers St Georges"},
+    [22] =
+        {[7] =
+             "Allee de la Tour-Rendez-Vous | La Remise-a-Jorelle | Les Coquetiers | Les Pavillons-sous-Bois",
+         [8] = "Gargan",
+         [9] = "Freinville Sevran | L'Abbaye"},
+    [23] =
+        {[13] = "Couilly Saint-Germain Quincy | Les Champs-Forts | Montry Conde",
+         [14] = "Crecy-en-Brie La Chapelle | Villiers-Montbarbin"},
+    [26] =
+        {[5] = "Val de Fontenay",
+         [6] = "Bry-sur-Marne | Neuilly-Plaisance",
+         [7] = "Noisy-le-Grand (Mont d'Est)",
+         [8] = "Noisy-Champs",
+         [10] = "Lognes | Noisiel | Torcy",
+         [11] = "Bussy-Saint-Georges",
+         [12] = "Val d'europe",
+         [13] = "Marne-la-Vallee Chessy"},
+    [28] = {[4] = "Fontenay-aux-Roses | Robinson | Sceaux"},
+    [30] =
+        {[1] = "Gare Saint-Lazare",
+         [3] = "Pont Cardinet",
+         [4] =
+             "Asnieres | Becon-les-Bruyeres | Clichy Levallois | Courbevoie | La Defense (Grande Arche)",
+         [5] = "Puteaux | Suresnes Mont-Valerien",
+         [7] = "Garches Marne-la-Coquette | Le Val d'Or | Saint-Cloud",
+         [8] = "Vaucresson",
+         [9] = "Bougival | La Celle-Saint-Cloud | Louveciennes | Marly-le-Roi",
+         [10] = "L'Etang-la-Ville | Saint-Nom-la-Breteche Foret de Marly"},
+    [31] =
+        {[7] = "Chaville-Rive Droite | Sevres Ville-d'Avray | Viroflay-Rive Droite",
+         [8] = "Montreuil | Versailles-Rive Droite"},
+    [32] =
+        {[5] = "La Garenne-Colombes | Les Vallees | Nanterre-Universite",
+         [7] = "Houilles Carrieres-sur-Seine | Sartrouville",
+         [9] = "Maisons-Laffitte",
+         [10] = "Poissy",
+         [11] = "Villennes-sur-Seine",
+         [12] = "Les Clairieres de Verneuil | Vernouillet Verneuil",
+         [13] = "Aubergenville-Elisabethville | Les Mureaux",
+         [14] = "Epone Mezieres",
+         [16] = "Bonnieres | Mantes-Station | Mantes-la-Jolie | Port-Villez | Rosny-sur-Seine"},
+    [33] =
+        {[10] = "Acheres-Grand-Cormier | Acheres-Ville",
+         [11] = "Cergy-Prefecture | Neuville-Universite",
+         [12] = "Cergy-Saint-Christophe | Cergy-le-Haut"},
+    [35] =
+        {[4] = "Bois-Colombes",
+         [5] = "Colombes | Le Stade",
+         [6] = "Argenteuil | Argenteuil",
+         [8] = "Cormeilles-en-Parisis | Val d'Argenteuil | Val d'Argenteuil",
+         [9] = "Herblay | La Frette Montigny",
+         [10] = "Conflans-Fin d'Oise | Conflans-Sainte-Honorine",
+         [11] = "Andresy | Chanteloup-les-Vignes | Maurecourt",
+         [12] = "Triel-sur-Seine | Vaux-sur-Seine",
+         [13] = "Meulan Hadricourt | Thun-le-Paradis",
+         [14] = "Gargenville | Juziers",
+         [15] = "Issou Porcheville | Limay",
+         [16] = "Breval | Menerville"},
+    [40] =
+        {[1] = "Gare de Lyon",
+         [5] = "Le Vert de Maisons | Maisons-Alfort Alfortville",
+         [6] = "Villeneuve-Prairie",
+         [7] = "Villeneuve-Triage",
+         [8] = "Villeneuve-Saint-Georges",
+         [9] = "Juvisy | Vigneux-sur-Seine",
+         [10] = "Ris-Orangis | Viry-Châtillon",
+         [11] = "Evry Val de Seine | Grand-Bourg",
+         [12] = "Corbeil-Essonnes | Mennecy | Moulin-Galant",
+         [13] = "Ballancourt | Fontenay le Vicomte",
+         [14] = "La Ferte-Alais",
+         [16] = "Boutigny | Maisse",
+         [17] = "Boigneville | Buno-Gironville"},
+    [41] =
+        {[0] = "Musee d'Orsay | Saint-Michel Notre-Dame",
+         [1] = "Gare d'Austerlitz",
+         [2] = "Bibliotheque-Francois Mitterrand",
+         [4] = "Ivry-sur-Seine | Vitry-sur-Seine",
+         [5] = "Choisy-le-Roi | Les Ardoines",
+         [7] = "Villeneuve-le-Roi",
+         [8] = "Ablon",
+         [9] = "Athis-Mons"},
+    [42] =
+        {[9] = "Epinay-sur-Orge | Savigny-sur-Orge",
+         [10] = "Sainte-Genevieve-des-Bois",
+         [11] = "Saint-Michel-sur-Orge",
+         [12] = "Bretigny-sur-Orge | Marolles-en-Hurepoix",
+         [13] = "Bouray | Lardy",
+         [14] = "Chamarande | Etampes | Etrechy",
+         [16] = "Saint-Martin d'Etampes",
+         [17] = "Guillerval"},
+    [43] =
+        {[9] = "Montgeron Crosne | Yerres",
+         [10] = "Brunoy",
+         [11] = "Boussy-Saint-Antoine | Combs-la-Ville Quincy",
+         [12] = "Lieusaint Moissy",
+         [13] = "Cesson | Savigny-le-Temple Nandy",
+         [15] = "Le Mee | Melun",
+         [16] = "Chartrettes | Fontaine-le-Port | Livry-sur-Seine",
+         [17] =
+             "Champagne-sur-Seine | Hericy | La Grande Paroisse | Vernou-sur-Seine | Vulaines-sur-Seine Samoreau"},
+    [44] =
+        {[12] = "Essonnes-Robinson | Villabe",
+         [13] = "Coudray-Montceaux | Le Plessis-Chenet-IBM | Saint-Fargeau",
+         [14] = "Boissise-le-Roi | Ponthierry Pringy",
+         [15] = "Vosves",
+         [16] = "Bois-le-Roi",
+         [17] =
+             "Bagneaux-sur-Loing | Bourron-Marlotte Grez | Fontainebleau-Avon | Montereau | Montigny-sur-Loing | Moret Veneux-les-Sablons | Nemours Saint-Pierre | Saint-Mammes | Souppes | Thomery"},
+    [45] =
+        {[10] = "Grigny-Centre",
+         [11] = "Evry Courcouronnes | Orangis Bois de l'Epine",
+         [12] = "Le Bras-de-Fer - Evry Genopole"},
+    [50] =
+        {[0] = "Haussmann-Saint-Lazare",
+         [1] = "Gare du Nord | Magenta | Paris-Nord",
+         [5] = "Epinay-Villetaneuse | Saint-Denis | Sevres-Rive Gauche",
+         [6] = "La Barre-Ormesson",
+         [7] = "Champ de Courses d'Enghien | Enghien-les-Bains",
+         [8] = "Ermont-Eaubonne | Ermont-Halte | Gros-Noyer Saint-Prix",
+         [9] = "Saint-Leu-La-Foret | Taverny | Vaucelles",
+         [10] = "Bessancourt | Frepillon | Mery",
+         [11] = "Meriel | Valmondois",
+         [12] = "Bruyeres-sur-Oise | Champagne-sur-Oise | L'Isle-Adam Parmain | Persan Beaumont"},
+    [51] =
+        {[4] = "La Courneuve-Aubervilliers | La Plaine-Stade de France",
+         [5] = "Le Bourget",
+         [7] = "Blanc-Mesnil | Drancy",
+         [8] = "Aulnay-sous-Bois",
+         [9] = "Sevran Livry | Vert-Galant",
+         [10] = "Villeparisis",
+         [11] = "Compans | Mitry-Claye",
+         [12] = "Dammartin Juilly Saint-Mard | Thieux Nantouillet"},
+    [52] =
+        {[5] = "Stade de France-Saint-Denis",
+         [6] = "Pierrefitte Stains",
+         [7] = "Garges-Sarcelles",
+         [8] = "Villiers-le-Bel (Gonesse - Arnouville)",
+         [10] = "Goussainville | Les Noues | Louvres",
+         [11] = "La Borne-Blanche | Survilliers-Fosses"},
+    [53] =
+        {[6] = "Deuil Montmagny",
+         [7] = "Groslay",
+         [8] = "Sarcelles Saint-Brice",
+         [9] = "Domont | Ecouen Ezanville",
+         [10] = "Bouffemont Moisselles | Montsoult Maffliers",
+         [11] = "Belloy-Saint-Martin | Luzarches | Seugy | Viarmes | Villaines"},
+    [54] =
+        {[8] = "Cernay",
+         [9] = "Franconville Plessis-Bouchard | Montigny-Beauchamp",
+         [10] = "Pierrelaye",
+         [11] = "Pontoise | Saint-Ouen-l'Aumone-Liesse",
+         [12] = "Boissy-l'Aillerie | Osny",
+         [15] = "Chars | Montgeroult Courcelles | Santeuil Le Perchay | Us"},
+    [55] =
+        {[0] =
+             "Avenue Foch | Avenue Henri-Martin | Boulainvilliers | Kennedy Radio-France | Neuilly-Porte Maillot (Palais des congres)",
+         [1] = "Pereire-Levallois",
+         [2] = "Porte de Clichy",
+         [3] = "Saint-Ouen",
+         [4] = "Les Gresillons",
+         [5] = "Gennevilliers",
+         [6] = "Epinay-sur-Seine",
+         [7] = "Saint-Gratien"},
+    [56] = {[11] = "Auvers-sur-Oise | Chaponval | Epluches | Pont Petit"},
+    [57] = {[11] = "Presles Courcelles", [12] = "Nointel Mours"},
+    [60] =
+        {[1] = "Gare Montparnasse",
+         [4] = "Clamart | Vanves Malakoff",
+         [5] = "Bellevue | Bievres | Meudon",
+         [6] = "Chaville-Rive Gauche | Chaville-Velizy | Viroflay-Rive Gauche",
+         [7] = "Versailles-Chantiers",
+         [10] = "Saint-Cyr",
+         [11] = "Saint-Quentin-en-Yvelines - Montigny le Bretonneux | Trappes",
+         [12] = "Coignieres | La Verriere",
+         [13] = "Les Essarts-le-Roi",
+         [14] = "Le Perray | Rambouillet",
+         [15] = "Gazeran"},
+    [61] =
+        {[10] = "Fontenay-le-Fleury",
+         [11] = "Villepreux Les-Clayes",
+         [12] = "Plaisir Grignon | Plaisir Les-Clayes",
+         [13] = "Beynes | Mareil-sur-Mauldre | Maule | Nezel Aulnay",
+         [15] =
+             "Garancieres La-Queue | Montfort-l'Amaury Mere | Orgerus Behoust | Tacoigneres Richebourg | Villiers Neauphle Pontchartrain",
+         [16] = "Houdan"},
+    [63] = {[7] = "Porchefontaine | Versailles-Rive Gauche"},
+    [64] =
+        {[0] = "Invalides | Pont de l'alma",
+         [1] = "Champ de Mars-Tour Eiffel",
+         [2] = "Javel",
+         [3] = "Boulevard Victor - Pont du Garigliano | Issy-Val de Seine | Issy",
+         [5] = "Meudon-Val-Fleury"},
+    [65] =
+        {[8] = "Jouy-en-Josas | Petit-Jouy-les-Loges",
+         [9] = "Vauboyen",
+         [10] = "Igny",
+         [11] = "Massy-Palaiseau",
+         [12] = "Longjumeau",
+         [13] = "Chilly-Mazarin",
+         [14] = "Gravigny-Balizy | Petit-Vaux"},
+    [70] =
+        {[9] = "Parc des Expositions | Sevran-Beaudottes | Villepinte",
+         [10] = "Aeroport Charles de Gaulle"},
+    [72] = {[7] = "Sannois"},
+    [73] = {[11] = "Eragny Neuville | Saint-Ouen-l'Aumone (Eglise)"},
+    [75] =
+        {[7] = "Les Saules | Orly-Ville",
+         [9] = "Pont de Rungis Aeroport d'Orly | Rungis-La Fraternelle",
+         [10] = "Chemin d'Antony",
+         [12] = "Massy-Verrieres | Arpajon"},
+    [76] =
+        {[12] = "Egly | La Norville Saint-Germain-les-Arpajon",
+         [13] = "Breuillet Bruyeres-le-Châtel | Breuillet-Village | Saint-Cheron",
+         [14] = "Sermaise",
+         [15] = "Dourdan | Dourdan-la-Foret"},
+};
+
 #endif // METRO_LIST_H
 #endif // METRO_LIST_H