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

ravkav, and an auto detect mode

Luu 11 месяцев назад
Родитель
Сommit
7832c68deb

+ 3 - 0
api/calypso/calypso_i.h

@@ -33,6 +33,9 @@ typedef struct {
     int contracts_count;
     int events_count;
     int special_events_count;
+
+    int country_num;
+    int network_num;
 } CalypsoCardData;
 
 typedef struct {

+ 72 - 0
api/calypso/cards/intercode.c

@@ -581,6 +581,8 @@ const char* get_intercode_string_transition_type(int transition) {
         return "Entry (First validation)";
     case 0x2:
         return "Exit";
+    case 0x3:
+        return "Validation";
     case 0x4:
         return "Inspection";
     case 0x5:
@@ -637,3 +639,73 @@ const char* get_intercode_string_event_result(int result) {
     }
     }
 }
+
+const char* get_intercode_string_version(int version) {
+    // version is a 6 bits int
+    // if the first 3 bits are 000, it's a 1.x version
+    // if the first 3 bits are 001, it's a 2.x version
+    // else, it's unknown
+    int major = (version >> 3) & 0x07;
+    if(major == 0) {
+        return "Intercode I";
+    } else if(major == 1) {
+        return "Intercode II";
+    }
+    return "Unknown";
+}
+
+int get_intercode_string_subversion(int version) {
+    // subversion is a 3 bits int
+    return version & 0x07;
+}
+
+const char* get_intercode_string_holder_type(int card_status) {
+    // b3 -> RFU
+    // b2 -> linked to an organization
+    // b1..b0 -> personalization status (0: anonymous, 1: identified, 2: personalized, 3: networkSpecific)
+    int status = card_status & 0x03;
+    switch(status) {
+    case 0:
+        return "Anonymous";
+    case 1:
+        return "Identified";
+    case 2:
+        return "Personalized";
+    case 3:
+        return "Network Specific";
+    default:
+        return "Unknown";
+    }
+}
+
+bool is_intercode_string_holder_linked(int card_status) {
+    // b3 -> RFU
+    // b2 -> linked to an organization
+    // b1..b0 -> personalization status (0: anonymous, 1: identified, 2: personalized, 3: networkSpecific)
+    return card_status & 0x04;
+}
+
+const char* get_intercode_string_contract_status(int status) {
+    switch(status) {
+    case 0x0:
+        return "Valid (never used)";
+    case 0x1:
+        return "Valid (used)";
+    case 0x3:
+        return "Renewal required";
+    case 0xD:
+        return "Not validable";
+    case 0x13:
+        return "Blocked";
+    case 0x3F:
+        return "Suspended";
+    case 0x58:
+        return "Invalid";
+    case 0x7F:
+        return "Refunded";
+    case 0xFF:
+        return "Erasable";
+    default:
+        return "Unknown";
+    }
+}

+ 10 - 0
api/calypso/cards/intercode.h

@@ -15,6 +15,16 @@ const char* get_intercode_string_transition_type(int transition);
 
 const char* get_intercode_string_event_result(int result);
 
+const char* get_intercode_string_version(int version);
+
+int get_intercode_string_subversion(int version);
+
+const char* get_intercode_string_holder_type(int card_status);
+
+bool is_intercode_string_holder_linked(int card_status);
+
+const char* get_intercode_string_contract_status(int status);
+
 typedef enum {
     URBAN_BUS = 1,
     INTERURBAN_BUS = 2,

+ 232 - 0
api/calypso/cards/ravkav.c

@@ -0,0 +1,232 @@
+#include <stdlib.h>
+#include "ravkav.h"
+
+CalypsoApp* get_ravkav_contract_structure() {
+    /*
+    En1545FixedInteger("Version", 3),
+    En1545FixedInteger.date(CONTRACT_START),
+    En1545FixedInteger(CONTRACT_PROVIDER, 8),
+    En1545FixedInteger(CONTRACT_TARIFF, 11),
+    En1545FixedInteger.date(CONTRACT_SALE),
+    En1545FixedInteger(CONTRACT_SALE_DEVICE, 12),
+    En1545FixedInteger("ContractSaleNumber", 10),
+    En1545FixedInteger(CONTRACT_INTERCHANGE, 1),
+    En1545Bitmap(
+            En1545FixedInteger(CONTRACT_UNKNOWN_A, 5),
+            En1545FixedInteger(CONTRACT_RESTRICT_CODE, 5),
+            En1545FixedInteger("ContractRestrictDuration", 6),
+            En1545FixedInteger.date(CONTRACT_END),
+            En1545FixedInteger(CONTRACT_DURATION, 8),
+            En1545FixedInteger(CONTRACT_UNKNOWN_B, 32),
+            En1545FixedInteger(CONTRACT_UNKNOWN_C, 6),
+            En1545FixedInteger(CONTRACT_UNKNOWN_D, 32),
+            En1545FixedInteger(CONTRACT_UNKNOWN_E, 32)
+    )
+    */
+    CalypsoApp* RavKavContractStructure = malloc(sizeof(CalypsoApp));
+
+    if(!RavKavContractStructure) {
+        return NULL;
+    }
+
+    int app_elements_count = 9;
+
+    RavKavContractStructure->type = CALYPSO_APP_CONTRACT;
+    RavKavContractStructure->container = malloc(sizeof(CalypsoContainerElement));
+    RavKavContractStructure->container->elements =
+        malloc(app_elements_count * sizeof(CalypsoElement));
+    RavKavContractStructure->container->size = app_elements_count;
+
+    RavKavContractStructure->container->elements[0] =
+        make_calypso_final_element("ContractVersion", 3, "Version", CALYPSO_FINAL_TYPE_NUMBER);
+    RavKavContractStructure->container->elements[1] =
+        make_calypso_final_element("ContractStartDate", 14, "Start Date", CALYPSO_FINAL_TYPE_DATE);
+    RavKavContractStructure->container->elements[2] =
+        make_calypso_final_element("ContractProvider", 8, "Provider", CALYPSO_FINAL_TYPE_UNKNOWN);
+    RavKavContractStructure->container->elements[3] =
+        make_calypso_final_element("ContractTariff", 11, "Tariff", CALYPSO_FINAL_TYPE_TARIFF);
+    RavKavContractStructure->container->elements[4] =
+        make_calypso_final_element("ContractSaleDate", 14, "Sale Date", CALYPSO_FINAL_TYPE_DATE);
+    RavKavContractStructure->container->elements[5] = make_calypso_final_element(
+        "ContractSaleDevice", 12, "Sale Device", CALYPSO_FINAL_TYPE_UNKNOWN);
+    RavKavContractStructure->container->elements[6] = make_calypso_final_element(
+        "ContractSaleNumber", 10, "Sale Number", CALYPSO_FINAL_TYPE_UNKNOWN);
+    RavKavContractStructure->container->elements[7] = make_calypso_final_element(
+        "ContractInterchange", 1, "Contract Interchange", CALYPSO_FINAL_TYPE_UNKNOWN);
+    RavKavContractStructure->container->elements[8] = make_calypso_bitmap_element(
+        "Contract",
+        9,
+        (CalypsoElement[]){
+            make_calypso_final_element(
+                "ContractUnknownA", 5, "Unknown A", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "ContractRestrictCode", 5, "Restrict Code", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "ContractRestrictDuration", 6, "Restrict Duration", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element("ContractEndDate", 14, "End Date", CALYPSO_FINAL_TYPE_DATE),
+            make_calypso_final_element(
+                "ContractDuration", 8, "Duration", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "ContractUnknownB", 32, "Unknown B", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "ContractUnknownC", 6, "Unknown C", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "ContractUnknownD", 32, "Unknown D", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "ContractUnknownE", 32, "Unknown E", CALYPSO_FINAL_TYPE_UNKNOWN),
+
+        });
+
+    return RavKavContractStructure;
+}
+
+CalypsoApp* get_ravkav_env_holder_structure() {
+    /*
+    En1545FixedInteger(ENV_VERSION_NUMBER, 3),
+    En1545FixedInteger(ENV_NETWORK_ID, 20),
+    En1545FixedInteger(ENV_UNKNOWN_A, 26),
+    En1545FixedInteger.date(ENV_APPLICATION_ISSUE),
+    En1545FixedInteger.date(ENV_APPLICATION_VALIDITY_END),
+    En1545FixedInteger("PayMethod", 3),
+    En1545FixedInteger.dateBCD(HOLDER_BIRTH_DATE),
+    En1545FixedHex(ENV_UNKNOWN_B, 44),
+    En1545FixedInteger(HOLDER_ID_NUMBER, 30)
+    */
+
+    CalypsoApp* RavKavEnvHolderStructure = malloc(sizeof(CalypsoApp));
+    if(!RavKavEnvHolderStructure) {
+        return NULL;
+    }
+
+    int app_elements_count = 8;
+
+    RavKavEnvHolderStructure->type = CALYPSO_APP_ENV_HOLDER;
+    RavKavEnvHolderStructure->container = malloc(sizeof(CalypsoContainerElement));
+    RavKavEnvHolderStructure->container->elements =
+        malloc(app_elements_count * sizeof(CalypsoElement));
+    RavKavEnvHolderStructure->container->size = app_elements_count;
+
+    RavKavEnvHolderStructure->container->elements[0] = make_calypso_final_element(
+        "EnvApplicationVersionNumber",
+        3,
+        "Numéro de version de l’application Billettique",
+        CALYPSO_FINAL_TYPE_NUMBER);
+    RavKavEnvHolderStructure->container->elements[1] = make_calypso_final_element(
+        "EnvCountry", 12, "Identification du réseau", CALYPSO_FINAL_TYPE_NUMBER);
+    RavKavEnvHolderStructure->container->elements[2] =
+        make_calypso_final_element("EnvIssuer", 8, "Issuer", CALYPSO_FINAL_TYPE_NUMBER);
+    RavKavEnvHolderStructure->container->elements[3] = make_calypso_final_element(
+        "EnvApplicationNumber", 26, "Application Number", CALYPSO_FINAL_TYPE_NUMBER);
+    RavKavEnvHolderStructure->container->elements[4] =
+        make_calypso_final_element("EnvDateOfIssue", 14, "Date of issue", CALYPSO_FINAL_TYPE_DATE);
+    RavKavEnvHolderStructure->container->elements[5] =
+        make_calypso_final_element("EnvEndValidity", 14, "End Validity", CALYPSO_FINAL_TYPE_DATE);
+    RavKavEnvHolderStructure->container->elements[6] =
+        make_calypso_final_element("   ", 3, "Payment method", CALYPSO_FINAL_TYPE_PAY_METHOD),
+    RavKavEnvHolderStructure->container->elements[7] = make_calypso_final_element(
+        "EnvBCDDate", 32, "Birth Date", CALYPSO_FINAL_TYPE_DATE); // might do this one later
+
+    return RavKavEnvHolderStructure;
+}
+
+CalypsoApp* get_ravkav_event_structure() {
+    /*
+    En1545FixedInteger("EventVersion", 3),
+    En1545FixedInteger(EVENT_SERVICE_PROVIDER, 8),
+    En1545FixedInteger(EVENT_CONTRACT_POINTER, 4),
+    En1545FixedInteger(EVENT_CODE, 8),
+    En1545FixedInteger.dateTime(EVENT),
+    En1545FixedInteger("EventTransferFlag", 1),
+    En1545FixedInteger.dateTime(EVENT_FIRST_STAMP),
+    En1545FixedInteger("EventContractPrefs", 32),
+    En1545Bitmap(
+            En1545FixedInteger(EVENT_LOCATION_ID, 16),
+            En1545FixedInteger(EVENT_ROUTE_NUMBER, 16),
+            En1545FixedInteger("StopEnRoute", 8),
+            En1545FixedInteger(EVENT_UNKNOWN_A, 12),
+            En1545FixedInteger(EVENT_VEHICLE_ID, 14),
+            En1545FixedInteger(EVENT_UNKNOWN_B, 4),
+            En1545FixedInteger(EVENT_UNKNOWN_C, 8)
+    ),
+    En1545Bitmap(
+            En1545Container(
+                    En1545FixedInteger("RouteSystem", 10),
+                    En1545FixedInteger("FareCode", 8),
+                    En1545FixedInteger(EVENT_PRICE_AMOUNT, 16)
+            ),
+            En1545FixedInteger(EVENT_UNKNOWN_D, 32),
+            En1545FixedInteger(EVENT_UNKNOWN_E, 32)
+    )
+    */
+    //  EventTime EventVehicle
+    CalypsoApp* RavKavEventStructure = malloc(sizeof(CalypsoApp));
+
+    if(!RavKavEventStructure) {
+        return NULL;
+    }
+
+    int app_elements_count = 11;
+
+    RavKavEventStructure->type = CALYPSO_APP_EVENT;
+    RavKavEventStructure->container = malloc(sizeof(CalypsoContainerElement));
+    RavKavEventStructure->container->elements =
+        malloc(app_elements_count * sizeof(CalypsoElement));
+    RavKavEventStructure->container->size = app_elements_count;
+
+    RavKavEventStructure->container->elements[0] =
+        make_calypso_final_element("EventVersion", 3, "Version Number", CALYPSO_FINAL_TYPE_NUMBER);
+    RavKavEventStructure->container->elements[1] = make_calypso_final_element(
+        "EventServiceProvider", 8, "Service provider", CALYPSO_FINAL_TYPE_UNKNOWN);
+    RavKavEventStructure->container->elements[2] = make_calypso_final_element(
+        "EventContractID", 4, "Contract ID", CALYPSO_FINAL_TYPE_UNKNOWN);
+    RavKavEventStructure->container->elements[3] =
+        make_calypso_final_element("EventAreaID", 4, "Area ID", CALYPSO_FINAL_TYPE_UNKNOWN);
+    RavKavEventStructure->container->elements[4] =
+        make_calypso_final_element("EventType", 4, "Event Type", CALYPSO_FINAL_TYPE_UNKNOWN);
+    RavKavEventStructure->container->elements[5] =
+        make_calypso_final_element("EventTime", 30, "Event Time", CALYPSO_FINAL_TYPE_DATE);
+    RavKavEventStructure->container->elements[6] = make_calypso_final_element(
+        "EventInterchangeFlag", 1, "Event Interchange flag", CALYPSO_FINAL_TYPE_UNKNOWN);
+    RavKavEventStructure->container->elements[7] = make_calypso_final_element(
+        "EventFirstTime", 30, "Event First Time", CALYPSO_FINAL_TYPE_DATE);
+    RavKavEventStructure->container->elements[8] =
+        make_calypso_final_element("EventUnknownA", 32, "Unknown A", CALYPSO_FINAL_TYPE_UNKNOWN);
+    RavKavEventStructure->container->elements[9] = make_calypso_bitmap_element(
+        "Location",
+        7,
+        (CalypsoElement[]){
+            make_calypso_final_element(
+                "EventLocationID", 16, "Location ID", CALYPSO_FINAL_TYPE_NUMBER),
+            make_calypso_final_element(
+                "EVentRouteNumber", 16, "Route Number", CALYPSO_FINAL_TYPE_NUMBER),
+            make_calypso_final_element(
+                "EventStopEnRoute", 8, "Stop En Route", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventUnknownB", 12, "Unknown B", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventVehicleID", 14, "Vehicle ID", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element("EventUnknownC", 4, "Unknown C", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventUnknownD", 8, "Unknown D", CALYPSO_FINAL_TYPE_UNKNOWN)});
+    RavKavEventStructure->container->elements[10] = make_calypso_bitmap_element(
+        "EventExtension",
+        3,
+        (CalypsoElement[]){
+            make_calypso_container_element(
+                "EventData",
+                3,
+                (CalypsoElement[]){
+                    make_calypso_final_element(
+                        "EventRouteSystem", 10, "Route System", CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "EventfareCode", 8, "fare Code", CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "EventDebitAmount", 16, "Debit Amount", CALYPSO_FINAL_TYPE_UNKNOWN),
+                }),
+            make_calypso_final_element(
+                "EventUnknownE", 32, "Unknown C", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventUnknownF", 32, "Unknown D", CALYPSO_FINAL_TYPE_UNKNOWN),
+        });
+    return RavKavEventStructure;
+}

+ 12 - 0
api/calypso/cards/ravkav.h

@@ -0,0 +1,12 @@
+#include "../calypso_util.h"
+
+#ifndef RAVKAV_STRUCTURES_H
+#define RAVKAV_STRUCTURES_H
+
+CalypsoApp* get_ravkav_contract_structure();
+
+CalypsoApp* get_ravkav_event_structure();
+
+CalypsoApp* get_ravkav_env_holder_structure();
+
+#endif // RAVKAV_STRUCTURES_H

+ 28 - 36
api/calypso/transit/navigo.c

@@ -192,25 +192,6 @@ const char* get_zones(int* zones) {
     }
 }
 
-const char* get_intercode_version(int version) {
-    // version is a 6 bits int
-    // if the first 3 bits are 000, it's a 1.x version
-    // if the first 3 bits are 001, it's a 2.x version
-    // else, it's unknown
-    int major = (version >> 3) & 0x07;
-    if(major == 0) {
-        return "Intercode I";
-    } else if(major == 1) {
-        return "Intercode II";
-    }
-    return "Unknown";
-}
-
-int get_intercode_subversion(int version) {
-    // subversion is a 3 bits int
-    return version & 0x07;
-}
-
 char* get_token(char* psrc, const char* delimit, void* psave) {
     static char sret[512];
     register char* ptr = psave;
@@ -592,8 +573,9 @@ void show_navigo_special_event_info(NavigoCardSpecialEvent* event, FuriString* p
 void show_navigo_contract_info(NavigoCardContract* contract, FuriString* parsed_data) {
     // Core type and ticket info
     furi_string_cat_printf(parsed_data, "Type: %s\n", get_navigo_tariff(contract->tariff));
-    if(is_ticket_count_available(contract->tariff)) {
+    if(contract->counter_present) {
         furi_string_cat_printf(parsed_data, "Remaining Tickets: %d\n", contract->counter.count);
+        furi_string_cat_printf(parsed_data, "Last load: %d\n", contract->counter.last_load);
     }
 
     // Validity period
@@ -610,17 +592,24 @@ void show_navigo_contract_info(NavigoCardContract* contract, FuriString* parsed_
     if(contract->serial_number_available) {
         furi_string_cat_printf(parsed_data, "TCN Number: %d\n", contract->serial_number);
     }
-
-    // Payment and pricing details
+    if(contract->price_amount_available) {
+        furi_string_cat_printf(parsed_data, "Amount: %.2f EUR\n", contract->price_amount);
+    }
     if(contract->pay_method_available) {
         furi_string_cat_printf(
             parsed_data, "Payment Method: %s\n", get_pay_method(contract->pay_method));
     }
-    if(contract->price_amount_available) {
-        furi_string_cat_printf(parsed_data, "Amount: %.2f EUR\n", contract->price_amount);
+    if(contract->end_date_available) {
+        furi_string_cat_printf(parsed_data, "Valid\nfrom: ");
+        locale_format_datetime_cat(parsed_data, &contract->start_date, false);
+        furi_string_cat_printf(parsed_data, "\nto: ");
+        locale_format_datetime_cat(parsed_data, &contract->end_date, false);
+        furi_string_cat_printf(parsed_data, "\n");
+    } else {
+        furi_string_cat_printf(parsed_data, "Valid from\n");
+        locale_format_datetime_cat(parsed_data, &contract->start_date, false);
+        furi_string_cat_printf(parsed_data, "\n");
     }
-
-    // Zone and sales details
     if(contract->zones_available) {
         furi_string_cat_printf(parsed_data, "%s\n", get_zones(contract->zones));
     }
@@ -630,22 +619,25 @@ void show_navigo_contract_info(NavigoCardContract* contract, FuriString* parsed_
     furi_string_cat_printf(
         parsed_data, "Sales Agent: %s\n", get_navigo_service_provider(contract->sale_agent));
     furi_string_cat_printf(parsed_data, "Sales Terminal: %d\n", contract->sale_device);
-
-    // Status and authenticity
-    if(contract->status == 1) {
-        furi_string_cat_printf(parsed_data, "Status: OK\n");
-    } else {
-        furi_string_cat_printf(parsed_data, "Status: Unknown (%d)\n", contract->status);
-    }
+    furi_string_cat_printf(
+        parsed_data, "Status: %s\n", get_intercode_string_contract_status(contract->status));
     furi_string_cat_printf(parsed_data, "Authenticity Code: %d\n", contract->authenticator);
 }
 
-void show_navigo_environment_info(NavigoCardEnv* environment, FuriString* parsed_data) {
+void show_navigo_environment_info(
+    NavigoCardEnv* environment,
+    NavigoCardHolder* holder,
+    FuriString* parsed_data) {
+    furi_string_cat_printf(
+        parsed_data, "Card status: %s\n", get_intercode_string_holder_type(holder->card_status));
+    if(is_intercode_string_holder_linked(holder->card_status)) {
+        furi_string_cat_printf(parsed_data, "Linked to an organization\n");
+    }
     furi_string_cat_printf(
         parsed_data,
         "App Version: %s - v%d\n",
-        get_intercode_version(environment->app_version),
-        get_intercode_subversion(environment->app_version));
+        get_intercode_string_version(environment->app_version),
+        get_intercode_string_subversion(environment->app_version));
     furi_string_cat_printf(
         parsed_data, "Country: %s\n", get_country_string(environment->country_num));
     furi_string_cat_printf(

+ 4 - 1
api/calypso/transit/navigo.h

@@ -31,7 +31,10 @@ void show_navigo_special_event_info(NavigoCardSpecialEvent* event, FuriString* p
 
 void show_navigo_contract_info(NavigoCardContract* contract, FuriString* parsed_data);
 
-void show_navigo_environment_info(NavigoCardEnv* environment, FuriString* parsed_data);
+void show_navigo_environment_info(
+    NavigoCardEnv* environment,
+    NavigoCardHolder* holder,
+    FuriString* parsed_data);
 
 typedef enum {
     NAVIGO_EASY = 0,

+ 2 - 0
api/calypso/transit/navigo_i.h

@@ -59,6 +59,7 @@ typedef struct {
     int count;
     int relative_first_stamp_15mn;
     int struct_number;
+    int last_load;
 } NavigoCardContractCounter;
 
 typedef struct {
@@ -80,6 +81,7 @@ typedef struct {
     int status;
     int authenticator;
     NavigoCardContractCounter counter;
+    bool counter_present;
     bool present;
 } NavigoCardContract;
 

+ 100 - 0
api/calypso/transit/ravkav.c

@@ -0,0 +1,100 @@
+#include "ravkav.h"
+#include "ravkav_lists.h"
+#include "../../../metroflip_i.h"
+
+const char* get_ravkav_issuer(int issuer) {
+    if(RAVKAV_ISSUERS_LIST[issuer]) {
+        return RAVKAV_ISSUERS_LIST[issuer];
+    } else {
+        // Return hex
+        char* issuer_str = malloc(9 * sizeof(char));
+        if(!issuer_str) {
+            return "Unknown";
+        }
+        snprintf(issuer_str, 9, "0x%02X", issuer);
+        return issuer_str;
+    }
+}
+
+void show_ravkav_contract_info(RavKavCardContract* contract, FuriString* parsed_data) {
+    // Core contract validity period
+    furi_string_cat_printf(parsed_data, "Valid from: ");
+    locale_format_datetime_cat(parsed_data, &contract->start_date, false);
+    if(contract->end_date_available) {
+        furi_string_cat_printf(parsed_data, "\nto: ");
+        locale_format_datetime_cat(parsed_data, &contract->end_date, false);
+    }
+    furi_string_cat_printf(parsed_data, "\n");
+
+    // Issuer information
+    furi_string_cat_printf(parsed_data, "Issuer: %s\n", get_ravkav_issuer(contract->provider));
+
+    // Sale details
+    furi_string_cat_printf(parsed_data, "Sold on: ");
+    locale_format_datetime_cat(parsed_data, &contract->sale_date, false);
+    furi_string_cat_printf(parsed_data, "\nSale device: %d\n", contract->sale_device);
+    furi_string_cat_printf(parsed_data, "Sale number: %d\n", contract->sale_number);
+
+    // Restriction details
+    if(contract->restrict_code_available) {
+        furi_string_cat_printf(parsed_data, "Restriction code: %d\n", contract->restrict_code);
+    }
+    if(contract->restrict_duration_available) {
+        furi_string_cat_printf(
+            parsed_data, "Restriction duration: \n%d minutes\n", contract->restrict_duration);
+    }
+
+    // Additional metadata
+    furi_string_cat_printf(parsed_data, "Contract version: %d\n", contract->version);
+    furi_string_cat_printf(parsed_data, "Journey interchanges flag: %d\n", contract->interchange);
+}
+
+void show_ravkav_environment_info(RavKavCardEnv* environment, FuriString* parsed_data) {
+    // Validity information
+    furi_string_cat_printf(parsed_data, "End of validity:\n");
+    locale_format_datetime_cat(parsed_data, &environment->end_dt, false);
+    furi_string_cat_printf(parsed_data, "\n");
+
+    // Issue date
+    furi_string_cat_printf(parsed_data, "Issue date:\n");
+    locale_format_datetime_cat(parsed_data, &environment->issue_dt, false);
+    furi_string_cat_printf(parsed_data, "\n");
+
+    // Application details
+    furi_string_cat_printf(parsed_data, "App version: %d\n", environment->app_version);
+    furi_string_cat_printf(parsed_data, "App number: %d\n", environment->app_num);
+
+    // Payment and network details
+    furi_string_cat_printf(parsed_data, "Pay method: %d\n", environment->pay_method);
+}
+
+void show_ravkav_event_info(RavKavCardEvent* event, FuriString* parsed_data) {
+    // Essential details first
+    if(event->location_id_available) {
+        furi_string_cat_printf(parsed_data, "Location:\n%d\n", event->location_id);
+    }
+    furi_string_cat_printf(parsed_data, "Time:\n");
+    locale_format_datetime_cat(parsed_data, &event->time, true);
+    furi_string_cat_printf(parsed_data, "\n");
+    furi_string_cat_printf(parsed_data, "Service provider: %d\n", event->service_provider);
+
+    // Fare and route-related information
+    if(event->fare_code_available) {
+        furi_string_cat_printf(parsed_data, "Fare Code: %d\n", event->fare_code);
+    }
+    if(event->route_number_available) {
+        furi_string_cat_printf(parsed_data, "Route Number: %d\n", event->route_number);
+    }
+    if(event->debit_amount_available) {
+        furi_string_cat_printf(parsed_data, "Debit Amount: %.2f\n", (double)event->debit_amount);
+    }
+    if(event->stop_en_route_available) {
+        furi_string_cat_printf(parsed_data, "Stop en Route: %d\n", event->stop_en_route);
+    }
+
+    // Remaining event metadata
+    furi_string_cat_printf(parsed_data, "Area ID: %d\n", event->area_id);
+    furi_string_cat_printf(parsed_data, "Contract ID: %d\n", event->contract_id);
+    furi_string_cat_printf(parsed_data, "Event Type: %d\n", event->type);
+    furi_string_cat_printf(parsed_data, "Event Interchange Flag: %d\n", event->interchange_flag);
+}

+ 14 - 0
api/calypso/transit/ravkav.h

@@ -0,0 +1,14 @@
+#include "../calypso_util.h"
+#include "../cards/ravkav.h"
+#include "ravkav_i.h"
+
+#ifndef RAVKAV_H
+#define RAVKAV_H
+
+void show_ravkav_event_info(RavKavCardEvent* event, FuriString* parsed_data);
+
+void show_ravkav_contract_info(RavKavCardContract* contract, FuriString* parsed_data);
+
+void show_ravkav_environment_info(RavKavCardEnv* environment, FuriString* parsed_data);
+
+#endif // RAVKAV_H

+ 67 - 0
api/calypso/transit/ravkav_i.h

@@ -0,0 +1,67 @@
+#include <datetime.h>
+#include <stdbool.h>
+
+#ifndef RAVKAV_I_H
+#define RAVKAV_I_H
+
+typedef struct {
+    int version;
+    int provider;
+    int tariff;
+    int sale_device;
+    int sale_number;
+    DateTime start_date;
+    DateTime sale_date;
+    int interchange;
+
+    //bitmap elements
+    int restrict_code;
+    int restrict_duration;
+    DateTime end_date;
+    bool restrict_code_available;
+    bool restrict_duration_available;
+    bool end_date_available;
+
+    bool present;
+} RavKavCardContract;
+
+typedef struct {
+    int event_version;
+    int service_provider;
+    int contract_id;
+    int area_id;
+    int type;
+    DateTime time;
+    int interchange_flag;
+
+    int location_id;
+    bool location_id_available;
+    int route_number;
+    bool route_number_available;
+    int stop_en_route;
+    bool stop_en_route_available;
+    int vehicle_id;
+    bool vehicle_id_available;
+
+    int fare_code;
+    bool fare_code_available;
+    float debit_amount;
+    bool debit_amount_available;
+} RavKavCardEvent;
+
+typedef struct {
+    int app_version;
+    int network_num;
+    int app_num;
+    int pay_method;
+    DateTime end_dt;
+    DateTime issue_dt;
+} RavKavCardEnv;
+
+typedef struct {
+    RavKavCardEnv environment;
+    RavKavCardContract contracts[4];
+    RavKavCardEvent events[3];
+} RavKavCardData;
+
+#endif // RAVKAV_I_H

+ 64 - 0
api/calypso/transit/ravkav_lists.h

@@ -0,0 +1,64 @@
+#ifndef RAVKAV_LISTS_H
+#define RAVKAV_LISTS_H
+
+static const char* RAVKAV_ISSUERS_LIST[300] = {
+    [0] = "undefined",
+    [1] = "Service Center (Postal Bank)",
+    [2] = "Israel Railways",
+    [3] = "Egged",
+    [4] = "Egged Transport",
+    [5] = "Dan",
+    [6] = "NTT (United Bus Services Nazareth)",
+    [7] = "NTT (Nazareth Travel & Tourism)",
+    [8] = "GB Tours",
+    [9] = "Omni Nazrin Express (transferred to Nativ Express)",
+    [10] = "Eilot Regional Council",
+    [11] = "Illit (merged with Kavim)",
+    [12] = "undefined 12",
+    [13] = "undefined 13",
+    [14] = "Nativ Express",
+    [15] = "Metropoline",
+    [16] = "Superbus",
+    [17] = "Connex Veolia (transferred to Afikim)",
+    [18] = "Kavim",
+    [19] = "Metrodan (defunct, transferred to Dan Be\'er Sheva)",
+    [20] = "Carmelit",
+    [21] = "CityPass",
+    [22] = "Tel Aviv Light Rail operator",
+    [23] = "Galim (Narkis Gal)",
+    [24] = "Golan Regional Council",
+    [25] = "Afikim",
+    [26] = "undefined 26",
+    [27] = "undefined 27",
+    [28] = "undefined 28",
+    [29] = "undefined 29",
+    [30] = "Dan North",
+    [31] = "Dan South",
+    [32] = "Dan Be\'er Sheva",
+    [33] = "undefined 33",
+    [34] = "undefined 34",
+    [35] = "undefined 35",
+    [36] = "undefined 36",
+    [37] = "undefined 37",
+    [38] = "undefined 38",
+    [39] = "undefined 39",
+    [40] = "undefined 40",
+    [41] = "East Jerusalem operators",
+    [42] = "Jerusalem - Ramallah united",
+    [43] = "(obsolete) Jerusalem - Mahane Shuafat",
+    [44] = "Jerusalem - Anata united",
+    [45] = "Jerusalem - Isawiya united",
+    [46] = "(obsolete) Jabal Al Muhbar Ltd",
+    [47] = "Jerusalem - Mount of Olives",
+    [48] = "(obsolete) Abu-Tor Siluan bus company",
+    [49] = "Jerusalem - Isawiya - Mahane Shuafat united",
+    [50] = "South Jerusalem united",
+    [51] = "Jerusalem - Sur Baher united",
+    [52] = "(obsolete) Ras-al Amud Izriya - Abu-Dis",
+    [53] = "(obsolete) Jerusalem - A. Swahra united",
+
+    [98] = "Tel Aviv Service Taxi Lines 4, 2 and 5",
+    [240] = "[Technical] Service Center - Contract restores",
+    [250] = "Israel Defence Forces"};
+
+#endif // RAVKAV_LISTS_H

+ 3 - 1
metroflip.c

@@ -26,6 +26,7 @@ Metroflip* metroflip_alloc() {
     //nfc device
     app->nfc = nfc_alloc();
     app->nfc_device = nfc_device_alloc();
+    app->detected_protocols = nfc_detected_protocols_alloc();
 
     // key cache
     app->mfc_key_cache = mf_classic_key_cache_alloc();
@@ -82,6 +83,7 @@ void metroflip_free(Metroflip* app) {
     //nfc device
     nfc_free(app->nfc);
     nfc_device_free(app->nfc_device);
+    nfc_detected_protocols_free(app->detected_protocols);
 
     // key cache
     mf_classic_key_cache_free(app->mfc_key_cache);
@@ -212,7 +214,7 @@ int bit_slice_to_dec(const char* bit_representation, int start, int end) {
 extern int32_t metroflip(void* p) {
     UNUSED(p);
     Metroflip* app = metroflip_alloc();
-    scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneRavKav);
+    scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneCalypso);
     scene_manager_next_scene(app->scene_manager, MetroflipSceneStart);
     view_dispatcher_run(app->view_dispatcher);
     metroflip_free(app);

+ 10 - 0
metroflip_i.h

@@ -31,6 +31,9 @@ extern const Icon I_RFIDDolphinReceive_97x61;
 #include <furi_hal_bt.h>
 #include <notification/notification_messages.h>
 
+#include "scenes/desfire.h"
+#include "scenes/nfc_detected_protocols.h"
+#include "scenes/keys.h"
 #include <lib/nfc/nfc.h>
 #include <nfc/nfc_poller.h>
 #include <nfc/nfc_scanner.h>
@@ -67,6 +70,8 @@ typedef struct {
     NfcScanner* scanner;
     NfcDevice* nfc_device;
     MfClassicKeyCache* mfc_key_cache;
+    NfcDetectedProtocols* detected_protocols;
+    DesfireCardType desfire_card_type;
 
     // card details:
     uint32_t balance_lari;
@@ -76,6 +81,9 @@ typedef struct {
     float value;
     char currency[4];
     char card_type[32];
+    bool auto_mode;
+    CardType mfc_card_type;
+    NfcProtocol protocol;
 
     // Calypso specific context
     CalypsoContext* calypso_context;
@@ -138,6 +146,8 @@ KeyfileManager manage_keyfiles(
 void metroflip_app_blink_start(Metroflip* metroflip);
 void metroflip_app_blink_stop(Metroflip* metroflip);
 
+CardType determine_card_type(Nfc* nfc);
+
 #ifdef FW_ORIGIN_Official
 #define submenu_add_lockable_item(                                             \
     submenu, label, index, callback, callback_context, locked, locked_message) \

+ 19 - 0
scenes/desfire.h

@@ -0,0 +1,19 @@
+#ifndef DESFIRE_H
+#define DESFIRE_H
+
+#include "../metroflip_i.h"
+
+typedef enum {
+    CARD_TYPE_ITSO,
+    CARD_TYPE_OPAL,
+    CARD_TYPE_CLIPPER,
+    CARD_TYPE_MYKI,
+    CARD_TYPE_DESFIRE_UNKNOWN
+} DesfireCardType;
+
+bool itso_parse(const NfcDevice* device, FuriString* parsed_data);
+bool opal_parse(const NfcDevice* device, FuriString* parsed_data);
+bool clipper_parse(const NfcDevice* device, FuriString* parsed_data);
+bool myki_parse(const NfcDevice* device, FuriString* parsed_data);
+
+#endif // DESFIRE_H

+ 326 - 0
scenes/keys.c

@@ -0,0 +1,326 @@
+#include "../metroflip_i.h"
+#include "keys.h"
+#include <bit_lib.h>
+#include <nfc/protocols/mf_classic/mf_classic.h>
+#include <nfc/protocols/mf_classic/mf_classic_poller.h>
+#include <nfc/nfc.h>
+#include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
+#include <string.h>
+
+#define TAG "keys_check"
+
+const MfClassicKeyPair troika_1k_keys[16] = {
+    {.a = 0xa0a1a2a3a4a5, .b = 0xfbf225dc5d58},
+    {.a = 0xa82607b01c0d, .b = 0x2910989b6880},
+    {.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99},
+    {.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99},
+    {.a = 0x73068f118c13, .b = 0x2b7f3253fac5},
+    {.a = 0xfbc2793d540b, .b = 0xd3a297dc2698},
+    {.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99},
+    {.a = 0xae3d65a3dad4, .b = 0x0f1c63013dba},
+    {.a = 0xa73f5dc1d333, .b = 0xe35173494a81},
+    {.a = 0x69a32f1c2f19, .b = 0x6b8bd9860763},
+    {.a = 0x9becdf3d9273, .b = 0xf8493407799d},
+    {.a = 0x08b386463229, .b = 0x5efbaecef46b},
+    {.a = 0xcd4c61c26e3d, .b = 0x31c7610de3b0},
+    {.a = 0xa82607b01c0d, .b = 0x2910989b6880},
+    {.a = 0x0e8f64340ba4, .b = 0x4acec1205d75},
+    {.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99},
+};
+
+const MfClassicKeyPair troika_4k_keys[40] = {
+    {.a = 0xEC29806D9738, .b = 0xFBF225DC5D58}, //1
+    {.a = 0xA0A1A2A3A4A5, .b = 0x7DE02A7F6025}, //2
+    {.a = 0x2AA05ED1856F, .b = 0xEAAC88E5DC99}, //3
+    {.a = 0x2AA05ED1856F, .b = 0xEAAC88E5DC99}, //4
+    {.a = 0x73068F118C13, .b = 0x2B7F3253FAC5}, //5
+    {.a = 0xFBC2793D540B, .b = 0xD3A297DC2698}, //6
+    {.a = 0x2AA05ED1856F, .b = 0xEAAC88E5DC99}, //7
+    {.a = 0xAE3D65A3DAD4, .b = 0x0F1C63013DBA}, //8
+    {.a = 0xA73F5DC1D333, .b = 0xE35173494A81}, //9
+    {.a = 0x69A32F1C2F19, .b = 0x6B8BD9860763}, //10
+    {.a = 0x9BECDF3D9273, .b = 0xF8493407799D}, //11
+    {.a = 0x08B386463229, .b = 0x5EFBAECEF46B}, //12
+    {.a = 0xCD4C61C26E3D, .b = 0x31C7610DE3B0}, //13
+    {.a = 0xA82607B01C0D, .b = 0x2910989B6880}, //14
+    {.a = 0x0E8F64340BA4, .b = 0x4ACEC1205D75}, //15
+    {.a = 0x2AA05ED1856F, .b = 0xEAAC88E5DC99}, //16
+    {.a = 0x6B02733BB6EC, .b = 0x7038CD25C408}, //17
+    {.a = 0x403D706BA880, .b = 0xB39D19A280DF}, //18
+    {.a = 0xC11F4597EFB5, .b = 0x70D901648CB9}, //19
+    {.a = 0x0DB520C78C1C, .b = 0x73E5B9D9D3A4}, //20
+    {.a = 0x3EBCE0925B2F, .b = 0x372CC880F216}, //21
+    {.a = 0x16A27AF45407, .b = 0x9868925175BA}, //22
+    {.a = 0xABA208516740, .b = 0xCE26ECB95252}, //23
+    {.a = 0xCD64E567ABCD, .b = 0x8F79C4FD8A01}, //24
+    {.a = 0x764CD061F1E6, .b = 0xA74332F74994}, //25
+    {.a = 0x1CC219E9FEC1, .b = 0xB90DE525CEB6}, //26
+    {.a = 0x2FE3CB83EA43, .b = 0xFBA88F109B32}, //27
+    {.a = 0x07894FFEC1D6, .b = 0xEFCB0E689DB3}, //28
+    {.a = 0x04C297B91308, .b = 0xC8454C154CB5}, //29
+    {.a = 0x7A38E3511A38, .b = 0xAB16584C972A}, //30
+    {.a = 0x7545DF809202, .b = 0xECF751084A80}, //31
+    {.a = 0x5125974CD391, .b = 0xD3EAFB5DF46D}, //32
+    {.a = 0x7A86AA203788, .b = 0xE41242278CA2}, //33
+    {.a = 0xAFCEF64C9913, .b = 0x9DB96DCA4324}, //34
+    {.a = 0x04EAA462F70B, .b = 0xAC17B93E2FAE}, //35
+    {.a = 0xE734C210F27E, .b = 0x29BA8C3E9FDA}, //36
+    {.a = 0xD5524F591EED, .b = 0x5DAF42861B4D}, //37
+    {.a = 0xE4821A377B75, .b = 0xE8709E486465}, //38
+    {.a = 0x518DC6EEA089, .b = 0x97C64AC98CA4}, //39
+    {.a = 0xBB52F8CCE07F, .b = 0x6B6119752C70}, //40
+};
+
+const uint8_t SMARTRIDER_STANDARD_KEYS[3][6] = {
+    {0x20, 0x31, 0xD1, 0xE5, 0x7A, 0x3B},
+    {0x4C, 0xA6, 0x02, 0x9F, 0x94, 0x73},
+    {0x19, 0x19, 0x53, 0x98, 0xE3, 0x2F}};
+
+const MfClassicKeyPair charliecard_1k_keys[16] = {
+    {.a = 0x3060206F5B0A, .b = 0xF1B9F5669CC8},
+    {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89},
+    {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89},
+    {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89},
+    {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89},
+    {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89},
+    {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89},
+    {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89},
+    {.a = 0x3A09594C8587, .b = 0x62387B8D250D},
+    {.a = 0xF238D78FF48F, .b = 0x9DC282D46217},
+    {.a = 0xAFD0BA94D624, .b = 0x92EE4DC87191},
+    {.a = 0xB35A0E4ACC09, .b = 0x756EF55E2507},
+    {.a = 0x447AB7FD5A6B, .b = 0x932B9CB730EF},
+    {.a = 0x1F1A0A111B5B, .b = 0xAD9E0A1CA2F7},
+    {.a = 0xD58023BA2BDC, .b = 0x62CED42A6D87},
+    {.a = 0x2548A443DF28, .b = 0x2ED3B15E7C0F},
+};
+
+const MfClassicKeyPair bip_1k_keys[16] = {
+    {.a = 0x3a42f33af429, .b = 0x1fc235ac1309},
+    {.a = 0x6338a371c0ed, .b = 0x243f160918d1},
+    {.a = 0xf124c2578ad0, .b = 0x9afc42372af1},
+    {.a = 0x32ac3b90ac13, .b = 0x682d401abb09},
+    {.a = 0x4ad1e273eaf1, .b = 0x067db45454a9},
+    {.a = 0xe2c42591368a, .b = 0x15fc4c7613fe},
+    {.a = 0x2a3c347a1200, .b = 0x68d30288910a},
+    {.a = 0x16f3d5ab1139, .b = 0xf59a36a2546d},
+    {.a = 0x937a4fff3011, .b = 0x64e3c10394c2},
+    {.a = 0x35c3d2caee88, .b = 0xb736412614af},
+    {.a = 0x693143f10368, .b = 0x324f5df65310},
+    {.a = 0xa3f97428dd01, .b = 0x643fb6de2217},
+    {.a = 0x63f17a449af0, .b = 0x82f435dedf01},
+    {.a = 0xc4652c54261c, .b = 0x0263de1278f3},
+    {.a = 0xd49e2826664f, .b = 0x51284c3686a6},
+    {.a = 0x3df14c8000a1, .b = 0x6a470d54127c},
+};
+
+const MfClassicKeyPair metromoney_1k_keys[16] = {
+    {.a = 0x2803BCB0C7E1, .b = 0x4FA9EB49F75E},
+    {.a = 0x9C616585E26D, .b = 0xD1C71E590D16},
+    {.a = 0x9C616585E26D, .b = 0xA160FCD5EC4C},
+    {.a = 0x9C616585E26D, .b = 0xA160FCD5EC4C},
+    {.a = 0x9C616585E26D, .b = 0xA160FCD5EC4C},
+    {.a = 0x9C616585E26D, .b = 0xA160FCD5EC4C},
+    {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF},
+    {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF},
+    {.a = 0x112233445566, .b = 0x361A62F35BC9},
+    {.a = 0x112233445566, .b = 0x361A62F35BC9},
+    {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF},
+    {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF},
+    {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF},
+    {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF},
+    {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF},
+    {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF},
+};
+
+static bool charliecard_verify(Nfc* nfc) {
+    bool verified = false;
+    FURI_LOG_I(TAG, "verifying charliecard..");
+
+    do {
+        const uint8_t verify_sector = 1;
+        const uint8_t verify_block = mf_classic_get_first_block_num_of_sector(verify_sector) + 1;
+        FURI_LOG_I(TAG, "Verifying sector %u", verify_sector);
+
+        MfClassicKey key = {0};
+        bit_lib_num_to_bytes_be(
+            charliecard_1k_keys[verify_sector].a, COUNT_OF(key.data), key.data);
+
+        MfClassicAuthContext auth_context;
+        MfClassicError error =
+            mf_classic_poller_sync_auth(nfc, verify_block, &key, MfClassicKeyTypeA, &auth_context);
+        if(error != MfClassicErrorNone) {
+            FURI_LOG_I(TAG, "Failed to read block %u: %d", verify_block, error);
+            break;
+        }
+
+        verified = true;
+    } while(false);
+
+    return verified;
+}
+
+bool bip_verify(Nfc* nfc) {
+    bool verified = false;
+
+    do {
+        const uint8_t verify_sector = 0;
+        uint8_t block_num = mf_classic_get_first_block_num_of_sector(verify_sector);
+        FURI_LOG_I(TAG, "Verifying sector %u", verify_sector);
+
+        MfClassicKey key = {};
+        bit_lib_num_to_bytes_be(bip_1k_keys[0].a, COUNT_OF(key.data), key.data);
+
+        MfClassicAuthContext auth_ctx = {};
+        MfClassicError error =
+            mf_classic_poller_sync_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_ctx);
+
+        if(error != MfClassicErrorNone) {
+            FURI_LOG_I(TAG, "Failed to read block %u: %d", block_num, error);
+            break;
+        }
+
+        verified = true;
+    } while(false);
+
+    return verified;
+}
+
+static bool metromoney_verify(Nfc* nfc) {
+    bool verified = false;
+
+    do {
+        const uint8_t ticket_sector_number = 1;
+        const uint8_t ticket_block_number =
+            mf_classic_get_first_block_num_of_sector(ticket_sector_number) + 1;
+        FURI_LOG_D(TAG, "Verifying sector %u", ticket_sector_number);
+
+        MfClassicKey key = {0};
+        bit_lib_num_to_bytes_be(
+            metromoney_1k_keys[ticket_sector_number].a, COUNT_OF(key.data), key.data);
+
+        MfClassicAuthContext auth_context;
+        MfClassicError error = mf_classic_poller_sync_auth(
+            nfc, ticket_block_number, &key, MfClassicKeyTypeA, &auth_context);
+        if(error != MfClassicErrorNone) {
+            FURI_LOG_D(TAG, "Failed to read block %u: %d", ticket_block_number, error);
+            break;
+        }
+
+        verified = true;
+    } while(false);
+
+    return verified;
+}
+
+static bool smartrider_authenticate_and_read(
+    Nfc* nfc,
+    uint8_t sector,
+    const uint8_t* key,
+    MfClassicKeyType key_type,
+    MfClassicBlock* block_data) {
+    MfClassicKey mf_key;
+    memcpy(mf_key.data, key, 6);
+    uint8_t block = mf_classic_get_first_block_num_of_sector(sector);
+
+    if(mf_classic_poller_sync_auth(nfc, block, &mf_key, key_type, NULL) != MfClassicErrorNone) {
+        FURI_LOG_D(TAG, "Authentication failed for sector %d key type %d", sector, key_type);
+        return false;
+    }
+
+    if(mf_classic_poller_sync_read_block(nfc, block, &mf_key, key_type, block_data) !=
+       MfClassicErrorNone) {
+        FURI_LOG_D(TAG, "Read failed for sector %d", sector);
+        return false;
+    }
+
+    return true;
+}
+
+static bool smartrider_verify(Nfc* nfc) {
+    furi_assert(nfc);
+    MfClassicBlock block_data;
+
+    for(int i = 0; i < 3; i++) {
+        if(!smartrider_authenticate_and_read(
+               nfc,
+               i * 6,
+               SMARTRIDER_STANDARD_KEYS[i],
+               i % 2 == 0 ? MfClassicKeyTypeA : MfClassicKeyTypeB,
+               &block_data) ||
+           memcmp(block_data.data, SMARTRIDER_STANDARD_KEYS[i], 6) != 0) {
+            FURI_LOG_D(TAG, "Authentication or key mismatch for key %d", i);
+            return false;
+        }
+    }
+
+    FURI_LOG_I(TAG, "SmartRider card verified");
+    return true;
+}
+
+static bool troika_get_card_config(TroikaCardConfig* config, MfClassicType type) {
+    bool success = true;
+
+    if(type == MfClassicType1k) {
+        config->data_sector = 11;
+        config->keys = troika_1k_keys;
+    } else if(type == MfClassicType4k) {
+        config->data_sector = 8; // Further testing needed
+        config->keys = troika_4k_keys;
+    } else {
+        success = false;
+    }
+
+    return success;
+}
+
+static bool troika_verify_type(Nfc* nfc, MfClassicType type) {
+    bool verified = false;
+
+    do {
+        TroikaCardConfig cfg = {};
+        if(!troika_get_card_config(&cfg, type)) break;
+
+        const uint8_t block_num = mf_classic_get_first_block_num_of_sector(cfg.data_sector);
+        FURI_LOG_D(TAG, "Verifying sector %lu", cfg.data_sector);
+
+        MfClassicKey key = {0};
+        bit_lib_num_to_bytes_be(cfg.keys[cfg.data_sector].a, COUNT_OF(key.data), key.data);
+
+        MfClassicAuthContext auth_context;
+        MfClassicError error =
+            mf_classic_poller_sync_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_context);
+        if(error != MfClassicErrorNone) {
+            FURI_LOG_D(TAG, "Failed to read block %u: %d", block_num, error);
+            break;
+        }
+        FURI_LOG_D(TAG, "Verify success!");
+        verified = true;
+    } while(false);
+
+    return verified;
+}
+
+static bool troika_verify(Nfc* nfc) {
+    return troika_verify_type(nfc, MfClassicType1k) || troika_verify_type(nfc, MfClassicType4k);
+}
+
+CardType determine_card_type(Nfc* nfc) {
+    FURI_LOG_I(TAG, "checking keys..");
+    UNUSED(bip_verify);
+
+    if(bip_verify(nfc)) {
+        return CARD_TYPE_METROMONEY;
+    } else if(metromoney_verify(nfc)) {
+        return CARD_TYPE_METROMONEY;
+    } else if(smartrider_verify(nfc)) {
+        return CARD_TYPE_SMARTRIDER;
+    } else if(troika_verify(nfc)) {
+        return CARD_TYPE_TROIKA;
+    } else if(charliecard_verify(nfc)) {
+        return CARD_TYPE_CHARLIECARD;
+    } else {
+        FURI_LOG_I(TAG, "its unknown");
+        return CARD_TYPE_UNKNOWN;
+    }
+}

+ 36 - 0
scenes/keys.h

@@ -0,0 +1,36 @@
+#ifndef KEYS_H
+#define KEYS_H
+
+#include "../metroflip_i.h"
+
+typedef enum {
+    CARD_TYPE_BIP,
+    CARD_TYPE_METROMONEY,
+    CARD_TYPE_CHARLIECARD,
+    CARD_TYPE_SMARTRIDER,
+    CARD_TYPE_TROIKA,
+    CARD_TYPE_UNKNOWN
+} CardType;
+
+typedef struct {
+    CardType type;
+} CardInfo;
+
+typedef struct {
+    uint64_t a;
+    uint64_t b;
+} MfClassicKeyPair;
+
+typedef struct {
+    const MfClassicKeyPair* keys;
+    uint32_t data_sector;
+} TroikaCardConfig;
+
+extern const MfClassicKeyPair troika_1k_keys[16];
+extern const MfClassicKeyPair troika_4k_keys[40];
+extern const uint8_t SMARTRIDER_STANDARD_KEYS[3][6];
+extern const MfClassicKeyPair charliecard_1k_keys[16];
+extern const MfClassicKeyPair bip_1k_keys[16];
+extern const MfClassicKeyPair metromoney_1k_keys[16];
+
+#endif // KEYS_H

+ 0 - 1
scenes/metroflip_scene_about.c

@@ -44,5 +44,4 @@ bool metroflip_scene_about_on_event(void* context, SceneManagerEvent event) {
 void metroflip_scene_about_on_exit(void* context) {
     Metroflip* app = context;
     widget_reset(app->widget);
-    UNUSED(context);
 }

+ 232 - 0
scenes/metroflip_scene_auto.c

@@ -0,0 +1,232 @@
+#include "../metroflip_i.h"
+#include <dolphin/dolphin.h>
+#include <furi.h>
+#include <bit_lib.h>
+#include <lib/nfc/protocols/nfc_protocol.h>
+#include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
+#include <nfc/protocols/mf_classic/mf_classic.h>
+#include <nfc/protocols/mf_classic/mf_classic_poller.h>
+#include "keys.h"
+#include "desfire.h"
+#include <nfc/protocols/mf_desfire/mf_desfire_poller.h>
+#include <lib/nfc/protocols/mf_desfire/mf_desfire.h>
+
+#define TAG "Metroflip:Scene:Auto"
+
+static NfcCommand
+    metroflip_scene_detect_desfire_poller_callback(NfcGenericEvent event, void* context) {
+    furi_assert(event.protocol == NfcProtocolMfDesfire);
+
+    Metroflip* app = context;
+    NfcCommand command = NfcCommandContinue;
+
+    FuriString* parsed_data = furi_string_alloc();
+    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));
+        if(clipper_parse(app->nfc_device, parsed_data)) {
+            furi_string_reset(app->text_box_store);
+            view_dispatcher_send_custom_event(
+                app->view_dispatcher, MetroflipCustomEventPollerSuccess);
+            app->desfire_card_type = CARD_TYPE_CLIPPER;
+        } else if(itso_parse(app->nfc_device, parsed_data)) {
+            furi_string_reset(app->text_box_store);
+            view_dispatcher_send_custom_event(
+                app->view_dispatcher, MetroflipCustomEventPollerSuccess);
+            app->desfire_card_type = CARD_TYPE_ITSO;
+        } else if(myki_parse(app->nfc_device, parsed_data)) {
+            furi_string_reset(app->text_box_store);
+            view_dispatcher_send_custom_event(
+                app->view_dispatcher, MetroflipCustomEventPollerSuccess);
+            app->desfire_card_type = CARD_TYPE_MYKI;
+        } else if(opal_parse(app->nfc_device, parsed_data)) {
+            furi_string_reset(app->text_box_store);
+            view_dispatcher_send_custom_event(
+                app->view_dispatcher, MetroflipCustomEventPollerSuccess);
+            app->desfire_card_type = CARD_TYPE_OPAL;
+        } else {
+            furi_string_reset(app->text_box_store);
+            view_dispatcher_send_custom_event(
+                app->view_dispatcher, MetroflipCustomEventPollerSuccess);
+            app->desfire_card_type = CARD_TYPE_DESFIRE_UNKNOWN;
+        }
+        furi_string_free(parsed_data);
+        command = NfcCommandStop;
+    } else if(mf_desfire_event->type == MfDesfirePollerEventTypeReadFailed) {
+        view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventPollerSuccess);
+        app->desfire_card_type = CARD_TYPE_DESFIRE_UNKNOWN;
+        command = NfcCommandContinue;
+    }
+
+    return command;
+}
+
+void metroflip_scene_detect_scan_callback(NfcScannerEvent event, void* context) {
+    furi_assert(context);
+    Metroflip* app = context;
+
+    if(event.type == NfcScannerEventTypeDetected) {
+        view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventCardDetected);
+        if(event.data.protocols && *event.data.protocols == NfcProtocolMfClassic) {
+            nfc_detected_protocols_set(
+                app->detected_protocols, event.data.protocols, event.data.protocol_num);
+            view_dispatcher_send_custom_event(
+                app->view_dispatcher, MetroflipCustomEventPollerDetect);
+        } else if(event.data.protocols && *event.data.protocols == NfcProtocolIso14443_4b) {
+            nfc_detected_protocols_set(
+                app->detected_protocols, event.data.protocols, event.data.protocol_num);
+            view_dispatcher_send_custom_event(
+                app->view_dispatcher, MetroflipCustomEventPollerDetect);
+        } else if(event.data.protocols && *event.data.protocols == NfcProtocolMfDesfire) {
+            nfc_detected_protocols_set(
+                app->detected_protocols, event.data.protocols, event.data.protocol_num);
+            view_dispatcher_send_custom_event(
+                app->view_dispatcher, MetroflipCustomEventPollerDetect);
+        } else {
+            const NfcProtocol* invalid_protocol = (const NfcProtocol*)NfcProtocolInvalid;
+            nfc_detected_protocols_set(
+                app->detected_protocols, invalid_protocol, event.data.protocol_num);
+        }
+    }
+}
+
+void metroflip_scene_auto_on_enter(void* context) {
+    Metroflip* app = context;
+    dolphin_deed(DolphinDeedNfcRead);
+
+    app->sec_num = 0;
+
+    // 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);
+    app->scanner = nfc_scanner_alloc(app->nfc);
+    nfc_scanner_start(app->scanner, metroflip_scene_detect_scan_callback, app);
+
+    metroflip_app_blink_start(app);
+}
+
+bool metroflip_scene_auto_on_event(void* context, SceneManagerEvent event) {
+    Metroflip* app = context;
+    UNUSED(app);
+    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 == MetroflipCustomEventPollerSuccess) {
+            nfc_poller_stop(app->poller);
+            nfc_poller_free(app->poller);
+            if(app->desfire_card_type == CARD_TYPE_CLIPPER) {
+                scene_manager_next_scene(app->scene_manager, MetroflipSceneClipper);
+            } else if(app->desfire_card_type == CARD_TYPE_OPAL) {
+                scene_manager_next_scene(app->scene_manager, MetroflipSceneOpal);
+            } else if(app->desfire_card_type == CARD_TYPE_MYKI) {
+                scene_manager_next_scene(app->scene_manager, MetroflipSceneClipper);
+            } else if(app->desfire_card_type == CARD_TYPE_ITSO) {
+                scene_manager_next_scene(app->scene_manager, MetroflipSceneItso);
+            } else if(app->desfire_card_type == CARD_TYPE_DESFIRE_UNKNOWN) {
+                Popup* popup = app->popup;
+                popup_set_header(popup, "Unsupported\n card", 58, 31, AlignLeft, AlignTop);
+            } else {
+                Popup* popup = app->popup;
+                popup_set_header(popup, "Unsupported\n card", 58, 31, 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.event == MetroflipCustomEventPollerDetect) {
+            nfc_scanner_stop(app->scanner);
+            nfc_scanner_free(app->scanner);
+            app->auto_mode = true;
+            if(nfc_detected_protocols_get_protocol(app->detected_protocols, 0) ==
+               NfcProtocolMfClassic) {
+                CardType card_type = determine_card_type(app->nfc);
+                app->mfc_card_type = card_type;
+                Popup* popup = app->popup;
+                UNUSED(popup);
+                switch(card_type) {
+                case CARD_TYPE_METROMONEY:
+                    FURI_LOG_I(TAG, "Detected: Metromoney\n");
+                    scene_manager_next_scene(app->scene_manager, MetroflipSceneMetromoney);
+                    break;
+                case CARD_TYPE_CHARLIECARD:
+                    FURI_LOG_I(TAG, "Detected: CharlieCard\n");
+                    scene_manager_next_scene(app->scene_manager, MetroflipSceneCharlieCard);
+                    break;
+                case CARD_TYPE_SMARTRIDER:
+                    FURI_LOG_I(TAG, "Detected: SmartRider\n");
+                    scene_manager_next_scene(app->scene_manager, MetroflipSceneSmartrider);
+                    break;
+                case CARD_TYPE_TROIKA:
+                    FURI_LOG_I(TAG, "Detected: Troika\n");
+                    scene_manager_next_scene(app->scene_manager, MetroflipSceneTroika);
+                    break;
+                case CARD_TYPE_UNKNOWN:
+                    popup_set_header(popup, "Unsupported\n card", 58, 31, AlignLeft, AlignTop);
+                    break;
+                default:
+                    FURI_LOG_I(TAG, "Detected: Unknown card type\n");
+                    popup_set_header(popup, "Unsupported\n card", 58, 31, AlignLeft, AlignTop);
+                    break;
+                }
+                consumed = true;
+            } else if(
+                nfc_detected_protocols_get_protocol(app->detected_protocols, 0) ==
+                NfcProtocolIso14443_4b) {
+                scene_manager_next_scene(app->scene_manager, MetroflipSceneCalypso);
+                consumed = true;
+            } else if(
+                nfc_detected_protocols_get_protocol(app->detected_protocols, 0) ==
+                NfcProtocolMfDesfire) {
+                app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfDesfire);
+                nfc_poller_start(app->poller, metroflip_scene_detect_desfire_poller_callback, app);
+                consumed = true;
+            } else if(
+                nfc_detected_protocols_get_protocol(app->detected_protocols, 0) ==
+                NfcProtocolInvalid) {
+                Popup* popup = app->popup;
+                popup_set_header(
+                    popup, "protocol\n currently\n unsupported", 58, 31, AlignLeft, AlignTop);
+                consumed = true;
+            } else {
+                Popup* popup = app->popup;
+                popup_set_header(popup, "error", 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_auto_on_exit(void* context) {
+    Metroflip* app = context;
+    if(!app->auto_mode) {
+        nfc_scanner_stop(app->scanner);
+        nfc_scanner_free(app->scanner);
+    }
+    app->auto_mode = false;
+    popup_reset(app->popup);
+
+    metroflip_app_blink_stop(app);
+}

+ 0 - 24
scenes/metroflip_scene_bip.c

@@ -44,30 +44,6 @@ typedef struct {
     uint16_t amount;
 } BipTransaction;
 
-typedef struct {
-    uint64_t a;
-    uint64_t b;
-} MfClassicKeyPair;
-
-static const MfClassicKeyPair bip_1k_keys[] = {
-    {.a = 0x3a42f33af429, .b = 0x1fc235ac1309},
-    {.a = 0x6338a371c0ed, .b = 0x243f160918d1},
-    {.a = 0xf124c2578ad0, .b = 0x9afc42372af1},
-    {.a = 0x32ac3b90ac13, .b = 0x682d401abb09},
-    {.a = 0x4ad1e273eaf1, .b = 0x067db45454a9},
-    {.a = 0xe2c42591368a, .b = 0x15fc4c7613fe},
-    {.a = 0x2a3c347a1200, .b = 0x68d30288910a},
-    {.a = 0x16f3d5ab1139, .b = 0xf59a36a2546d},
-    {.a = 0x937a4fff3011, .b = 0x64e3c10394c2},
-    {.a = 0x35c3d2caee88, .b = 0xb736412614af},
-    {.a = 0x693143f10368, .b = 0x324f5df65310},
-    {.a = 0xa3f97428dd01, .b = 0x643fb6de2217},
-    {.a = 0x63f17a449af0, .b = 0x82f435dedf01},
-    {.a = 0xc4652c54261c, .b = 0x0263de1278f3},
-    {.a = 0xd49e2826664f, .b = 0x51284c3686a6},
-    {.a = 0x3df14c8000a1, .b = 0x6a470d54127c},
-};
-
 static void bip_parse_datetime(const MfClassicBlock* block, DateTime* parsed_data) {
     furi_assert(block);
     furi_assert(parsed_data);

+ 71 - 33
scenes/metroflip_scene_calypso.c

@@ -108,22 +108,19 @@ void update_page_info(void* context, FuriString* parsed_data) {
             ctx->card->card_number);
         return;
     }
-    if(ctx->page_id <= 3) {
+    if(ctx->page_id == 0) {
         switch(ctx->card->card_type) {
         case CALYPSO_CARD_NAVIGO: {
-            furi_string_cat_printf(
-                parsed_data,
-                "\e#%s %u:\n",
-                get_navigo_type(ctx->card->navigo->holder.card_status),
-                ctx->card->card_number);
-            furi_string_cat_printf(parsed_data, "\e#Contract %d:\n", ctx->page_id + 1);
-            show_navigo_contract_info(&ctx->card->navigo->contracts[ctx->page_id], parsed_data);
+            furi_string_cat_printf(parsed_data, "\e#Navigo %u:\n", ctx->card->card_number);
+            furi_string_cat_printf(parsed_data, "\e#Environment:\n");
+            show_navigo_environment_info(
+                &ctx->card->navigo->environment, &ctx->card->navigo->holder, parsed_data);
             break;
         }
         case CALYPSO_CARD_OPUS: {
             furi_string_cat_printf(parsed_data, "\e#Opus %u:\n", ctx->card->card_number);
-            furi_string_cat_printf(parsed_data, "\e#Contract %d:\n", ctx->page_id + 1);
-            show_opus_contract_info(&ctx->card->opus->contracts[ctx->page_id], parsed_data);
+            furi_string_cat_printf(parsed_data, "\e#Environment:\n");
+            show_opus_environment_info(&ctx->card->opus->environment, parsed_data);
             break;
         }
         case CALYPSO_CARD_RAVKAV: {
@@ -132,28 +129,42 @@ void update_page_info(void* context, FuriString* parsed_data) {
             } else {
                 furi_string_cat_printf(parsed_data, "\e#RavKav %u:\n", ctx->card->card_number);
             }
-            furi_string_cat_printf(parsed_data, "\e#Contract %d:\n", ctx->page_id + 1);
-            show_ravkav_contract_info(&ctx->card->ravkav->contracts[ctx->page_id], parsed_data);
+            furi_string_cat_printf(parsed_data, "\e#Environment:\n");
+            show_ravkav_environment_info(&ctx->card->ravkav->environment, parsed_data);
             break;
         }
         default: {
             furi_string_cat_printf(parsed_data, "\e#Unknown %u:\n", ctx->card->card_number);
+            furi_string_cat_printf(
+                parsed_data, "Country: %s\n", get_country_string(ctx->card->country_num));
+            if(guess_card_type(ctx->card->country_num, ctx->card->network_num) !=
+               CALYPSO_CARD_UNKNOWN) {
+                furi_string_cat_printf(
+                    parsed_data,
+                    "Network: %s\n",
+                    get_network_string(
+                        guess_card_type(ctx->card->country_num, ctx->card->network_num)));
+            } else {
+                furi_string_cat_printf(parsed_data, "Network: %d\n", ctx->card->network_num);
+            }
             break;
         }
         }
-    } else if(ctx->page_id == 4) {
-        furi_string_cat_printf(parsed_data, "\e#Environment:\n");
+    } else if(ctx->page_id == 1 || ctx->page_id == 2 || ctx->page_id == 3 || ctx->page_id == 4) {
+        furi_string_cat_printf(parsed_data, "\e#Contract %d:\n", ctx->page_id);
         switch(ctx->card->card_type) {
         case CALYPSO_CARD_NAVIGO: {
-            show_navigo_environment_info(&ctx->card->navigo->environment, parsed_data);
+            show_navigo_contract_info(
+                &ctx->card->navigo->contracts[ctx->page_id - 1], parsed_data);
             break;
         }
         case CALYPSO_CARD_OPUS: {
-            show_opus_environment_info(&ctx->card->opus->environment, parsed_data);
+            show_opus_contract_info(&ctx->card->opus->contracts[ctx->page_id - 1], parsed_data);
             break;
         }
         case CALYPSO_CARD_RAVKAV: {
-            show_ravkav_environment_info(&ctx->card->ravkav->environment, parsed_data);
+            show_ravkav_contract_info(
+                &ctx->card->ravkav->contracts[ctx->page_id - 1], parsed_data);
             break;
         }
         default: {
@@ -254,13 +265,13 @@ void metroflip_back_button_widget_callback(GuiButtonType result, InputType type,
             if(ctx->page_id == 6 && ctx->card->events_count < 1) {
                 ctx->page_id -= 1;
             }
-            if(ctx->page_id == 4 && ctx->card->contracts_count < 4) {
+            if(ctx->page_id == 5 && ctx->card->contracts_count < 4) {
                 ctx->page_id -= 1;
             }
-            if(ctx->page_id == 3 && ctx->card->contracts_count < 3) {
+            if(ctx->page_id == 4 && ctx->card->contracts_count < 3) {
                 ctx->page_id -= 1;
             }
-            if(ctx->page_id == 2 && ctx->card->contracts_count < 2) {
+            if(ctx->page_id == 3 && ctx->card->contracts_count < 2) {
                 ctx->page_id -= 1;
             }
             ctx->page_id -= 1;
@@ -306,13 +317,13 @@ void metroflip_next_button_widget_callback(GuiButtonType result, InputType type,
             return;
         }
         if(ctx->page_id < 10) {
-            if(ctx->page_id == 0 && ctx->card->contracts_count < 2) {
+            if(ctx->page_id == 1 && ctx->card->contracts_count < 2) {
                 ctx->page_id += 1;
             }
-            if(ctx->page_id == 1 && ctx->card->contracts_count < 3) {
+            if(ctx->page_id == 2 && ctx->card->contracts_count < 3) {
                 ctx->page_id += 1;
             }
-            if(ctx->page_id == 2 && ctx->card->contracts_count < 4) {
+            if(ctx->page_id == 3 && ctx->card->contracts_count < 4) {
                 ctx->page_id += 1;
             }
             if(ctx->page_id == 4 && ctx->card->events_count < 1) {
@@ -488,23 +499,23 @@ static NfcCommand metroflip_scene_calypso_poller_callback(NfcGenericEvent event,
                 //     TAG, "Environment bit_representation: %s", environment_bit_representation);
                 start = 13;
                 end = 16;
-                int country_num =
+                card->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;
-                int network_num =
+                card->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);
-                card->card_type = guess_card_type(country_num, network_num);
+                card->card_type = guess_card_type(card->country_num, card->network_num);
                 switch(card->card_type) {
                 case CALYPSO_CARD_NAVIGO: {
                     card->navigo = malloc(sizeof(NavigoCardData));
 
-                    card->navigo->environment.country_num = country_num;
-                    card->navigo->environment.network_num = network_num;
+                    card->navigo->environment.country_num = card->country_num;
+                    card->navigo->environment.network_num = card->network_num;
 
                     CalypsoApp* IntercodeEnvHolderStructure = get_intercode_structure_env_holder();
 
@@ -731,7 +742,27 @@ static NfcCommand metroflip_scene_calypso_poller_callback(NfcGenericEvent event,
                             card->navigo->contracts[i - 1].zones_available = true;
                         }
 
-                        // 13.7. ContractValidityJourneys  -- pas sûr de le mettre lui
+                        // 13.7. ContractValidityJourneys
+                        contract_key = "ContractValidityJourneys";
+                        if(is_calypso_node_present(
+                               bit_representation, contract_key, IntercodeContractStructure)) {
+                            int positionOffset = get_calypso_node_offset(
+                                bit_representation, contract_key, IntercodeContractStructure);
+                            int start = positionOffset,
+                                end = positionOffset +
+                                      get_calypso_node_size(
+                                          contract_key, IntercodeContractStructure) -
+                                      1;
+                            int decimal_value = bit_slice_to_dec(bit_representation, start, end);
+                            // first 5 bits -> CounterStructureNumber
+                            // last 8 bits -> CounterLastLoad
+                            // other bits -> RFU
+                            card->navigo->contracts[i - 1].counter.struct_number = decimal_value >>
+                                                                                   11;
+                            card->navigo->contracts[i - 1].counter.last_load = decimal_value &
+                                                                               0xFF;
+                            card->navigo->contracts[i - 1].counter_present = true;
+                        }
 
                         // 15.0. ContractValiditySaleDate
                         contract_key = "ContractValiditySaleDate";
@@ -851,6 +882,12 @@ static NfcCommand metroflip_scene_calypso_poller_callback(NfcGenericEvent event,
 
                     // Ticket counts (contracts 1-4)
                     for(int i = 0; i < 4; i++) {
+                        if(card->navigo->contracts[i].present == 0) {
+                            continue;
+                        }
+                        if(card->navigo->contracts[i].counter_present == 0) {
+                            continue;
+                        }
                         start = 0;
                         end = 5;
                         card->navigo->contracts[i].counter.count = bit_slice_to_dec(
@@ -1244,8 +1281,8 @@ static NfcCommand metroflip_scene_calypso_poller_callback(NfcGenericEvent event,
                 case CALYPSO_CARD_OPUS: {
                     card->opus = malloc(sizeof(OpusCardData));
 
-                    card->opus->environment.country_num = country_num;
-                    card->opus->environment.network_num = network_num;
+                    card->opus->environment.country_num = card->country_num;
+                    card->opus->environment.network_num = card->network_num;
 
                     CalypsoApp* OpusEnvHolderStructure = get_opus_env_holder_structure();
 
@@ -1583,13 +1620,13 @@ static NfcCommand metroflip_scene_calypso_poller_callback(NfcGenericEvent event,
                 case CALYPSO_CARD_UNKNOWN: {
                     start = 3;
                     end = 6;
-                    country_num =
+                    int 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 = 15;
                     end = 18;
-                    network_num =
+                    int 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);
@@ -1645,6 +1682,7 @@ static NfcCommand metroflip_scene_calypso_poller_callback(NfcGenericEvent event,
                             }
                             bit_representation[response_length * 8] = '\0';
                             card->ravkav->contracts[i - 1].present = 1;
+                            card->events_count = 3;
                             card->contracts_count++;
 
                             // ContractVersion

+ 1 - 0
scenes/metroflip_scene_calypso.h

@@ -2,6 +2,7 @@
 #include <gui/modules/widget_elements/widget_element.h>
 #include "../api/calypso/transit/navigo.h"
 #include "../api/calypso/transit/opus.h"
+#include "../api/calypso/transit/ravkav.h"
 #include "../api/calypso/calypso_i.h"
 #include <datetime.h>
 #include <stdbool.h>

+ 0 - 25
scenes/metroflip_scene_charliecard.c

@@ -103,11 +103,6 @@
 #define CHARLIE_N_TRANSACTION_HISTORY 10
 #define CHARLIE_N_PASSES              4
 
-typedef struct {
-    uint64_t a;
-    uint64_t b;
-} MfClassicKeyPair;
-
 // always from the same set of keys (cf. default keys dict for list w/o multiplicity)
 // we only care about the data in the first half of the sectors
 // second half sectors keys seemingly change position sometimes across cards?
@@ -115,25 +110,6 @@ typedef struct {
 // accounting for this such that reading is faster (else it seems to fall back on dict
 // approach for remaining keys)...
 
-static const MfClassicKeyPair charliecard_1k_keys[] = {
-    {.a = 0x3060206F5B0A, .b = 0xF1B9F5669CC8},
-    {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89},
-    {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89},
-    {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89},
-    {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89},
-    {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89},
-    {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89},
-    {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89},
-    {.a = 0x3A09594C8587, .b = 0x62387B8D250D},
-    {.a = 0xF238D78FF48F, .b = 0x9DC282D46217},
-    {.a = 0xAFD0BA94D624, .b = 0x92EE4DC87191},
-    {.a = 0xB35A0E4ACC09, .b = 0x756EF55E2507},
-    {.a = 0x447AB7FD5A6B, .b = 0x932B9CB730EF},
-    {.a = 0x1F1A0A111B5B, .b = 0xAD9E0A1CA2F7},
-    {.a = 0xD58023BA2BDC, .b = 0x62CED42A6D87},
-    {.a = 0x2548A443DF28, .b = 0x2ED3B15E7C0F},
-};
-
 typedef struct {
     uint16_t dollars;
     uint8_t cents;
@@ -1259,7 +1235,6 @@ void metroflip_scene_charliecard_on_enter(void* context) {
 
     // Start worker
     view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
-    nfc_scanner_alloc(app->nfc);
     app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfClassic);
     nfc_poller_start(app->poller, metroflip_scene_charlicard_poller_callback, app);
 

+ 1 - 1
scenes/metroflip_scene_clipper.c

@@ -230,7 +230,7 @@ static int16_t get_i16be(const uint8_t* field) {
         return raw;
 }
 
-static bool clipper_parse(const NfcDevice* device, FuriString* parsed_data) {
+bool clipper_parse(const NfcDevice* device, FuriString* parsed_data) {
     furi_assert(device);
     furi_assert(parsed_data);
 

+ 2 - 2
scenes/metroflip_scene_config.h

@@ -1,6 +1,6 @@
 ADD_SCENE(metroflip, start, Start)
-ADD_SCENE(metroflip, ravkav, RavKav)
-ADD_SCENE(metroflip, navigo, Calypso)
+ADD_SCENE(metroflip, auto, Auto)
+ADD_SCENE(metroflip, calypso, Calypso)
 ADD_SCENE(metroflip, charliecard, CharlieCard)
 ADD_SCENE(metroflip, clipper, Clipper)
 ADD_SCENE(metroflip, metromoney, Metromoney)

+ 1 - 1
scenes/metroflip_scene_itso.c

@@ -26,7 +26,7 @@ uint64_t swap_uint64(uint64_t val) {
     return (val << 32) | (val >> 32);
 }
 
-static bool itso_parse(const NfcDevice* device, FuriString* parsed_data) {
+bool itso_parse(const NfcDevice* device, FuriString* parsed_data) {
     furi_assert(device);
     furi_assert(parsed_data);
 

+ 0 - 24
scenes/metroflip_scene_metromoney.c

@@ -15,30 +15,6 @@
 
 #define TAG "Metroflip:Scene:Metromoney"
 
-typedef struct {
-    uint64_t a;
-    uint64_t b;
-} MfClassicKeyPair;
-
-static const MfClassicKeyPair metromoney_1k_keys[] = {
-    {.a = 0x2803BCB0C7E1, .b = 0x4FA9EB49F75E},
-    {.a = 0x9C616585E26D, .b = 0xD1C71E590D16},
-    {.a = 0x9C616585E26D, .b = 0xA160FCD5EC4C},
-    {.a = 0x9C616585E26D, .b = 0xA160FCD5EC4C},
-    {.a = 0x9C616585E26D, .b = 0xA160FCD5EC4C},
-    {.a = 0x9C616585E26D, .b = 0xA160FCD5EC4C},
-    {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF},
-    {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF},
-    {.a = 0x112233445566, .b = 0x361A62F35BC9},
-    {.a = 0x112233445566, .b = 0x361A62F35BC9},
-    {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF},
-    {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF},
-    {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF},
-    {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF},
-    {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF},
-    {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF},
-};
-
 static bool metromoney_parse(const NfcDevice* device, const MfClassicData* data, Metroflip* app) {
     furi_assert(device);
 

+ 1 - 1
scenes/metroflip_scene_myki.c

@@ -34,7 +34,7 @@ static uint8_t myki_calculate_luhn(uint64_t number) {
     return (10 - (sum % 10)) % 10;
 }
 
-static bool myki_parse(const NfcDevice* device, FuriString* parsed_data) {
+bool myki_parse(const NfcDevice* device, FuriString* parsed_data) {
     furi_assert(device);
     furi_assert(parsed_data);
 

+ 1 - 1
scenes/metroflip_scene_opal.c

@@ -117,7 +117,7 @@ static void opal_days_minutes_to_datetime(uint16_t days, uint16_t minutes, DateT
     out->day = days;
 }
 
-static bool opal_parse(const NfcDevice* device, FuriString* parsed_data) {
+bool opal_parse(const NfcDevice* device, FuriString* parsed_data) {
     furi_assert(device);
     furi_assert(parsed_data);
 

+ 0 - 344
scenes/metroflip_scene_ravkav.c

@@ -1,344 +0,0 @@
-#include "../metroflip_i.h"
-
-#include <nfc/protocols/iso14443_4b/iso14443_4b_poller.h>
-
-#define TAG "Metroflip:Scene:RavKav"
-
-static NfcCommand metroflip_scene_ravkav_poller_callback(NfcGenericEvent event, void* context) {
-    furi_assert(event.protocol == NfcProtocolIso14443_4b);
-    NfcCommand next_command = NfcCommandContinue;
-    MetroflipPollerEventType stage = MetroflipPollerEventTypeStart;
-
-    Metroflip* app = context;
-    FuriString* parsed_data = furi_string_alloc();
-    Widget* widget = app->widget;
-    furi_string_reset(app->text_box_store);
-
-    const Iso14443_4bPollerEvent* iso14443_4b_event = event.event_data;
-
-    Iso14443_4bPoller* iso14443_4b_poller = event.instance;
-
-    BitBuffer* tx_buffer = bit_buffer_alloc(Metroflip_POLLER_MAX_BUFFER_SIZE);
-    BitBuffer* rx_buffer = bit_buffer_alloc(Metroflip_POLLER_MAX_BUFFER_SIZE);
-
-    if(iso14443_4b_event->type == Iso14443_4bPollerEventTypeReady) {
-        if(stage == MetroflipPollerEventTypeStart) {
-            nfc_device_set_data(
-                app->nfc_device, NfcProtocolIso14443_4b, nfc_poller_get_data(app->poller));
-
-            Iso14443_4bError error;
-            size_t response_length = 0;
-
-            do {
-                // Select file of balance
-                select_app[6] = 42;
-                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);
-                    break;
-                }
-
-                // Check the response after selecting 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,
-                        "Select 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);
-                    break;
-                }
-
-                // Now send the read command
-                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);
-                    break;
-                }
-
-                // 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);
-                    break;
-                }
-
-                // Process the response data
-                if(response_length < 3) {
-                    FURI_LOG_I(TAG, "Response too short: %d bytes", response_length);
-                    stage = MetroflipPollerEventTypeFail;
-                    view_dispatcher_send_custom_event(
-                        app->view_dispatcher, MetroflipCustomEventPollerFail);
-                    break;
-                }
-
-                uint32_t value = 0;
-                for(uint8_t i = 0; i < 3; i++) {
-                    value = (value << 8) | bit_buffer_get_byte(rx_buffer, i);
-                }
-
-                float result = value / 100.0f;
-                FURI_LOG_I(TAG, "Value: %.2f ILS", (double)result);
-                furi_string_printf(parsed_data, "\e#Rav-Kav:\n\n");
-                furi_string_cat_printf(parsed_data, "\e#Contract:\n");
-                if(result != 0.0f) {
-                    furi_string_cat_printf(parsed_data, "Balance: %.2f ILS\n", (double)result);
-                } else {
-                    furi_string_cat_printf(parsed_data, "Not a stored value Rav-Kav\n");
-                }
-
-                // Select app for profile
-                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);
-                    break;
-                }
-
-                // 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);
-                    break;
-                }
-                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);
-                    break;
-                }
-
-                // 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);
-                    break;
-                }
-                char bit_representation[response_length * 8 + 1];
-                bit_representation[0] = '\0';
-                for(size_t i = 0; i < response_length; i++) {
-                    char bits[9];
-                    uint8_t byte = bit_buffer_get_byte(rx_buffer, i);
-                    byte_to_binary(byte, bits);
-                    strlcat(bit_representation, bits, sizeof(bit_representation));
-                }
-                int start = 54, end = 83;
-                char bit_slice[end - start + 1];
-                strncpy(bit_slice, bit_representation + start, end - start + 1);
-                bit_slice[end - start + 1] = '\0';
-                int decimal_value = binary_to_decimal(bit_slice);
-                uint64_t result_timestamp = decimal_value + epoch + (3600 * 3);
-                DateTime dt = {0};
-                datetime_timestamp_to_datetime(result_timestamp, &dt);
-                furi_string_cat_printf(parsed_data, "\nActivation date:\n");
-                locale_format_datetime_cat(parsed_data, &dt, true);
-                furi_string_cat_printf(parsed_data, "\n\n");
-
-                // 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);
-                    break;
-                }
-
-                // 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);
-                    break;
-                }
-                furi_string_cat_printf(parsed_data, "\e#Latest Events:\n");
-                // Now send the read command
-                for(size_t i = 1, j = 6; i < 7 && j > 0; i++, j--) {
-                    read_file[2] = i;
-                    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);
-                        break;
-                    }
-
-                    // 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);
-                        break;
-                    }
-                    char bit_representation[response_length * 8 + 1];
-                    bit_representation[0] = '\0';
-                    for(size_t i = 0; i < response_length; i++) {
-                        char bits[9];
-                        uint8_t byte = bit_buffer_get_byte(rx_buffer, i);
-                        byte_to_binary(byte, bits);
-                        strlcat(bit_representation, bits, sizeof(bit_representation));
-                    }
-                    int start = 23, end = 52;
-                    char bit_slice[end - start + 2];
-                    strncpy(bit_slice, bit_representation + start, end - start + 1);
-                    bit_slice[end - start + 1] = '\0';
-                    int decimal_value = binary_to_decimal(bit_slice);
-                    uint64_t result_timestamp = decimal_value + epoch + (3600 * 3);
-                    DateTime dt = {0};
-                    datetime_timestamp_to_datetime(result_timestamp, &dt);
-                    furi_string_cat_printf(parsed_data, "\nEvent 0%d:\n", j);
-                    locale_format_datetime_cat(parsed_data, &dt, true);
-                    furi_string_cat_printf(parsed_data, "\n\n");
-                }
-
-                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);
-                stage = MetroflipPollerEventTypeSuccess;
-                next_command = NfcCommandStop;
-            } while(false);
-
-            if(stage != MetroflipPollerEventTypeSuccess) {
-                next_command = NfcCommandStop;
-            }
-        }
-    }
-    bit_buffer_free(tx_buffer);
-    bit_buffer_free(rx_buffer);
-
-    return next_command;
-}
-
-void metroflip_scene_ravkav_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, NfcProtocolIso14443_4b);
-    nfc_poller_start(app->poller, metroflip_scene_ravkav_poller_callback, app);
-
-    metroflip_app_blink_start(app);
-}
-
-bool metroflip_scene_ravkav_on_event(void* context, SceneManagerEvent event) {
-    Metroflip* app = context;
-    bool consumed = false;
-
-    if(event.type == SceneManagerEventTypeCustom) {
-        if(event.event == MetroflipPollerEventTypeCardDetect) {
-            Popup* popup = app->popup;
-            popup_set_header(popup, "Scanning..", 68, 30, AlignLeft, AlignTop);
-            consumed = true;
-        } else if(event.event == MetroflipCustomEventPollerFileNotFound) {
-            Popup* popup = app->popup;
-            popup_set_header(popup, "Read Error,\n wrong card", 68, 30, AlignLeft, AlignTop);
-            consumed = true;
-        } else if(event.event == MetroflipCustomEventPollerFail) {
-            Popup* popup = app->popup;
-            popup_set_header(popup, "Error, try\n again", 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_ravkav_on_exit(void* context) {
-    Metroflip* app = context;
-
-    if(app->poller) {
-        nfc_poller_stop(app->poller);
-        nfc_poller_free(app->poller);
-    }
-    metroflip_app_blink_stop(app);
-    widget_reset(app->widget);
-
-    // Clear view
-    popup_reset(app->popup);
-}

+ 1 - 6
scenes/metroflip_scene_smartrider.c

@@ -21,11 +21,6 @@
 
 uint8_t smartrider_sector_num = 0;
 
-static const uint8_t STANDARD_KEYS[3][6] = {
-    {0x20, 0x31, 0xD1, 0xE5, 0x7A, 0x3B},
-    {0x4C, 0xA6, 0x02, 0x9F, 0x94, 0x73},
-    {0x19, 0x19, 0x53, 0x98, 0xE3, 0x2F}};
-
 typedef struct {
     uint32_t timestamp;
     uint16_t cost;
@@ -149,7 +144,7 @@ static bool smartrider_parse(const NfcDevice* device, FuriString* parsed_data) {
     }
 
     const MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, 0);
-    if(!sec_tr || memcmp(sec_tr->key_a.data, STANDARD_KEYS[0], 6) != 0) {
+    if(!sec_tr || memcmp(sec_tr->key_a.data, SMARTRIDER_STANDARD_KEYS[0], 6) != 0) {
         FURI_LOG_E(TAG, "Key verification failed for sector 0");
         return false;
     }

+ 3 - 41
scenes/metroflip_scene_start.c

@@ -12,53 +12,15 @@ void metroflip_scene_start_on_enter(void* context) {
     submenu_set_header(submenu, "Metroflip");
 
     submenu_add_item(
-        submenu, "Rav-Kav", MetroflipSceneRavKav, metroflip_scene_start_submenu_callback, app);
-
-    submenu_add_item(
-        submenu, "Calypso", MetroflipSceneCalypso, metroflip_scene_start_submenu_callback, app);
-
-    submenu_add_item(
-        submenu,
-        "CharlieCard",
-        MetroflipSceneCharlieCard,
-        metroflip_scene_start_submenu_callback,
-        app);
-
-    submenu_add_item(
-        submenu, "Clipper", MetroflipSceneClipper, metroflip_scene_start_submenu_callback, app);
-
-    submenu_add_item(
-        submenu,
-        "SmartRider",
-        MetroflipSceneSmartrider,
-        metroflip_scene_start_submenu_callback,
-        app);
-
-    submenu_add_item(
-        submenu, "OV-Chipkaart", MetroflipSceneOVC, metroflip_scene_start_submenu_callback, app);
-
-    submenu_add_item(
-        submenu, "myki", MetroflipSceneMyki, metroflip_scene_start_submenu_callback, app);
-
-    submenu_add_item(
-        submenu, "Troika", MetroflipSceneTroika, metroflip_scene_start_submenu_callback, app);
-
-    submenu_add_item(
-        submenu, "Opal", MetroflipSceneOpal, metroflip_scene_start_submenu_callback, app);
-
-    submenu_add_item(
-        submenu, "ITSO", MetroflipSceneItso, metroflip_scene_start_submenu_callback, app);
+        submenu, "Scan Card", MetroflipSceneAuto, metroflip_scene_start_submenu_callback, app);
 
     submenu_add_item(
         submenu,
-        "Metromoney",
-        MetroflipSceneMetromoney,
+        "OV-Chipkaart (unstable)",
+        MetroflipSceneOVC,
         metroflip_scene_start_submenu_callback,
         app);
 
-    submenu_add_item(
-        submenu, "Bip!", MetroflipSceneBip, metroflip_scene_start_submenu_callback, app);
-
     submenu_add_item(
         submenu, "About", MetroflipSceneAbout, metroflip_scene_start_submenu_callback, app);
 

+ 0 - 72
scenes/metroflip_scene_troika.c

@@ -15,78 +15,6 @@
 
 #define TAG "Metroflip:Scene:Troika"
 
-typedef struct {
-    uint64_t a;
-    uint64_t b;
-} MfClassicKeyPair;
-
-typedef struct {
-    const MfClassicKeyPair* keys;
-    uint32_t data_sector;
-} TroikaCardConfig;
-
-static const MfClassicKeyPair troika_1k_keys[] = {
-    {.a = 0xa0a1a2a3a4a5, .b = 0xfbf225dc5d58},
-    {.a = 0xa82607b01c0d, .b = 0x2910989b6880},
-    {.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99},
-    {.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99},
-    {.a = 0x73068f118c13, .b = 0x2b7f3253fac5},
-    {.a = 0xfbc2793d540b, .b = 0xd3a297dc2698},
-    {.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99},
-    {.a = 0xae3d65a3dad4, .b = 0x0f1c63013dba},
-    {.a = 0xa73f5dc1d333, .b = 0xe35173494a81},
-    {.a = 0x69a32f1c2f19, .b = 0x6b8bd9860763},
-    {.a = 0x9becdf3d9273, .b = 0xf8493407799d},
-    {.a = 0x08b386463229, .b = 0x5efbaecef46b},
-    {.a = 0xcd4c61c26e3d, .b = 0x31c7610de3b0},
-    {.a = 0xa82607b01c0d, .b = 0x2910989b6880},
-    {.a = 0x0e8f64340ba4, .b = 0x4acec1205d75},
-    {.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99},
-};
-
-static const MfClassicKeyPair troika_4k_keys[] = {
-    {.a = 0xEC29806D9738, .b = 0xFBF225DC5D58}, //1
-    {.a = 0xA0A1A2A3A4A5, .b = 0x7DE02A7F6025}, //2
-    {.a = 0x2AA05ED1856F, .b = 0xEAAC88E5DC99}, //3
-    {.a = 0x2AA05ED1856F, .b = 0xEAAC88E5DC99}, //4
-    {.a = 0x73068F118C13, .b = 0x2B7F3253FAC5}, //5
-    {.a = 0xFBC2793D540B, .b = 0xD3A297DC2698}, //6
-    {.a = 0x2AA05ED1856F, .b = 0xEAAC88E5DC99}, //7
-    {.a = 0xAE3D65A3DAD4, .b = 0x0F1C63013DBA}, //8
-    {.a = 0xA73F5DC1D333, .b = 0xE35173494A81}, //9
-    {.a = 0x69A32F1C2F19, .b = 0x6B8BD9860763}, //10
-    {.a = 0x9BECDF3D9273, .b = 0xF8493407799D}, //11
-    {.a = 0x08B386463229, .b = 0x5EFBAECEF46B}, //12
-    {.a = 0xCD4C61C26E3D, .b = 0x31C7610DE3B0}, //13
-    {.a = 0xA82607B01C0D, .b = 0x2910989B6880}, //14
-    {.a = 0x0E8F64340BA4, .b = 0x4ACEC1205D75}, //15
-    {.a = 0x2AA05ED1856F, .b = 0xEAAC88E5DC99}, //16
-    {.a = 0x6B02733BB6EC, .b = 0x7038CD25C408}, //17
-    {.a = 0x403D706BA880, .b = 0xB39D19A280DF}, //18
-    {.a = 0xC11F4597EFB5, .b = 0x70D901648CB9}, //19
-    {.a = 0x0DB520C78C1C, .b = 0x73E5B9D9D3A4}, //20
-    {.a = 0x3EBCE0925B2F, .b = 0x372CC880F216}, //21
-    {.a = 0x16A27AF45407, .b = 0x9868925175BA}, //22
-    {.a = 0xABA208516740, .b = 0xCE26ECB95252}, //23
-    {.a = 0xCD64E567ABCD, .b = 0x8F79C4FD8A01}, //24
-    {.a = 0x764CD061F1E6, .b = 0xA74332F74994}, //25
-    {.a = 0x1CC219E9FEC1, .b = 0xB90DE525CEB6}, //26
-    {.a = 0x2FE3CB83EA43, .b = 0xFBA88F109B32}, //27
-    {.a = 0x07894FFEC1D6, .b = 0xEFCB0E689DB3}, //28
-    {.a = 0x04C297B91308, .b = 0xC8454C154CB5}, //29
-    {.a = 0x7A38E3511A38, .b = 0xAB16584C972A}, //30
-    {.a = 0x7545DF809202, .b = 0xECF751084A80}, //31
-    {.a = 0x5125974CD391, .b = 0xD3EAFB5DF46D}, //32
-    {.a = 0x7A86AA203788, .b = 0xE41242278CA2}, //33
-    {.a = 0xAFCEF64C9913, .b = 0x9DB96DCA4324}, //34
-    {.a = 0x04EAA462F70B, .b = 0xAC17B93E2FAE}, //35
-    {.a = 0xE734C210F27E, .b = 0x29BA8C3E9FDA}, //36
-    {.a = 0xD5524F591EED, .b = 0x5DAF42861B4D}, //37
-    {.a = 0xE4821A377B75, .b = 0xE8709E486465}, //38
-    {.a = 0x518DC6EEA089, .b = 0x97C64AC98CA4}, //39
-    {.a = 0xBB52F8CCE07F, .b = 0x6B6119752C70}, //40
-};
-
 static bool troika_get_card_config(TroikaCardConfig* config, MfClassicType type) {
     bool success = true;
 

+ 85 - 0
scenes/nfc_detected_protocols.c

@@ -0,0 +1,85 @@
+#include "nfc_detected_protocols.h"
+
+#include <furi.h>
+
+struct NfcDetectedProtocols {
+    uint32_t protocols_detected_num;
+    NfcProtocol protocols_detected[NfcProtocolNum];
+    uint32_t selected_idx;
+};
+
+NfcDetectedProtocols* nfc_detected_protocols_alloc(void) {
+    NfcDetectedProtocols* instance = malloc(sizeof(NfcDetectedProtocols));
+
+    instance->protocols_detected_num = 0;
+    instance->selected_idx = 0;
+
+    return instance;
+}
+
+void nfc_detected_protocols_free(NfcDetectedProtocols* instance) {
+    furi_assert(instance);
+
+    free(instance);
+}
+
+void nfc_detected_protocols_reset(NfcDetectedProtocols* instance) {
+    furi_assert(instance);
+
+    instance->protocols_detected_num = 0;
+    memset(instance->protocols_detected, 0, sizeof(instance->protocols_detected));
+    instance->selected_idx = 0;
+}
+
+void nfc_detected_protocols_select(NfcDetectedProtocols* instance, uint32_t idx) {
+    furi_assert(instance);
+
+    instance->selected_idx = idx;
+}
+
+void nfc_detected_protocols_set(
+    NfcDetectedProtocols* instance,
+    const NfcProtocol* types,
+    uint32_t count) {
+    furi_assert(instance);
+    furi_assert(types);
+    furi_assert(count < NfcProtocolNum);
+
+    memcpy(instance->protocols_detected, types, count);
+    instance->protocols_detected_num = count;
+    instance->selected_idx = 0;
+}
+
+uint32_t nfc_detected_protocols_get_num(NfcDetectedProtocols* instance) {
+    furi_assert(instance);
+
+    return instance->protocols_detected_num;
+}
+
+NfcProtocol nfc_detected_protocols_get_protocol(NfcDetectedProtocols* instance, uint32_t idx) {
+    furi_assert(instance);
+    furi_assert(idx < instance->protocols_detected_num);
+
+    return instance->protocols_detected[idx];
+}
+
+void nfc_detected_protocols_fill_all_protocols(NfcDetectedProtocols* instance) {
+    furi_assert(instance);
+
+    instance->protocols_detected_num = NfcProtocolNum;
+    for(uint32_t i = 0; i < NfcProtocolNum; i++) {
+        instance->protocols_detected[i] = i;
+    }
+}
+
+NfcProtocol nfc_detected_protocols_get_selected(NfcDetectedProtocols* instance) {
+    furi_assert(instance);
+
+    return instance->protocols_detected[instance->selected_idx];
+}
+
+uint32_t nfc_detected_protocols_get_selected_idx(NfcDetectedProtocols* instance) {
+    furi_assert(instance);
+
+    return instance->selected_idx;
+}

+ 29 - 0
scenes/nfc_detected_protocols.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <stdint.h>
+#include <nfc/protocols/nfc_protocol.h>
+
+typedef struct NfcDetectedProtocols NfcDetectedProtocols;
+
+NfcDetectedProtocols* nfc_detected_protocols_alloc(void);
+
+void nfc_detected_protocols_free(NfcDetectedProtocols* instance);
+
+void nfc_detected_protocols_reset(NfcDetectedProtocols* instance);
+
+void nfc_detected_protocols_select(NfcDetectedProtocols* instance, uint32_t idx);
+
+void nfc_detected_protocols_set(
+    NfcDetectedProtocols* instance,
+    const NfcProtocol* types,
+    uint32_t count);
+
+uint32_t nfc_detected_protocols_get_num(NfcDetectedProtocols* instance);
+
+NfcProtocol nfc_detected_protocols_get_protocol(NfcDetectedProtocols* instance, uint32_t idx);
+
+void nfc_detected_protocols_fill_all_protocols(NfcDetectedProtocols* instance);
+
+NfcProtocol nfc_detected_protocols_get_selected(NfcDetectedProtocols* instance);
+
+uint32_t nfc_detected_protocols_get_selected_idx(NfcDetectedProtocols* instance);