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

Update Opus card (Montreal) structures / parser + Add support for repeater calypso element

DocSystem 11 месяцев назад
Родитель
Сommit
a69307c7cb

+ 1 - 0
api/calypso/calypso_i.h

@@ -12,6 +12,7 @@ typedef enum {
     CALYPSO_CARD_VIVA,
     CALYPSO_CARD_PASSPASS,
     CALYPSO_CARD_TAM,
+    CALYPSO_CARD_TRANSPOLE,
     CALYPSO_CARD_OURA,
     CALYPSO_CARD_NAVIGO,
     CALYPSO_CARD_KORRIGO,

+ 46 - 0
api/calypso/calypso_util.c

@@ -1,3 +1,4 @@
+#include "metroflip_i.h"
 #include <stdlib.h>
 #include <string.h>
 #include "calypso_util.h"
@@ -50,6 +51,18 @@ CalypsoElement
     return container_element;
 }
 
+CalypsoElement make_calypso_repeater_element(const char* key, int size, CalypsoElement element) {
+    CalypsoElement repeater_element = {};
+
+    repeater_element.type = CALYPSO_ELEMENT_TYPE_REPEATER;
+    repeater_element.repeater = malloc(sizeof(CalypsoRepeaterElement));
+    repeater_element.repeater->size = size;
+    repeater_element.repeater->element = element;
+    strncpy(repeater_element.repeater->key, key, 36);
+
+    return repeater_element;
+}
+
 void free_calypso_element(CalypsoElement* element) {
     if(element->type == CALYPSO_ELEMENT_TYPE_FINAL) {
         free(element->final);
@@ -65,6 +78,9 @@ void free_calypso_element(CalypsoElement* element) {
         }
         free(element->container->elements);
         free(element->container);
+    } else if(element->type == CALYPSO_ELEMENT_TYPE_REPEATER) {
+        free_calypso_element(&element->repeater->element);
+        free(element->repeater);
     }
 }
 
@@ -219,6 +235,26 @@ int get_calypso_subnode_offset(
         }
 
         return count_offset;
+    } else if(elem->type == CALYPSO_ELEMENT_TYPE_REPEATER) {
+        // same as bitmap but instead of a bitmap, we have the count of how many times to repeat the inner element
+        CalypsoRepeaterElement* repeater = elem->repeater;
+
+        char bit_slice[repeater->size + 1];
+        strncpy(bit_slice, binary_string, repeater->size);
+        bit_slice[repeater->size] = '\0';
+
+        int C = bit_slice_to_dec(bit_slice, 0, repeater->size - 1);
+
+        int count_offset = repeater->size;
+        bool f = false;
+        for(int i = 0; i < C; i++) {
+            count_offset += get_calypso_subnode_offset(
+                binary_string + count_offset, key, &repeater->element, &f);
+            if(f) {
+                *found = true;
+                return count_offset;
+            }
+        }
     }
     return 0;
 }
@@ -261,6 +297,14 @@ int get_calypso_subnode_size(const char* key, CalypsoElement* element) {
                 return size;
             }
         }
+    } else if(element->type == CALYPSO_ELEMENT_TYPE_REPEATER) {
+        if(strcmp(element->repeater->key, key) == 0) {
+            return element->repeater->size;
+        }
+        int size = get_calypso_subnode_size(key, &element->repeater->element);
+        if(size != 0) {
+            return size;
+        }
     }
     return 0;
 }
@@ -298,6 +342,8 @@ CALYPSO_CARD_TYPE guess_card_type(int country_num, int network_num) {
             return CALYPSO_CARD_PASSPASS;
         case 64:
             return CALYPSO_CARD_TAM; // Montpellier
+        case 149:
+            return CALYPSO_CARD_TRANSPOLE; // Lille
         case 502:
             return CALYPSO_CARD_OURA;
         case 901:

+ 11 - 0
api/calypso/calypso_util.h

@@ -28,6 +28,7 @@ typedef enum {
 } CalypsoFinalType;
 
 typedef enum {
+    CALYPSO_ELEMENT_TYPE_REPEATER,
     CALYPSO_ELEMENT_TYPE_CONTAINER,
     CALYPSO_ELEMENT_TYPE_BITMAP,
     CALYPSO_ELEMENT_TYPE_FINAL
@@ -36,6 +37,7 @@ typedef enum {
 typedef struct CalypsoFinalElement_t CalypsoFinalElement;
 typedef struct CalypsoBitmapElement_t CalypsoBitmapElement;
 typedef struct CalypsoContainerElement_t CalypsoContainerElement;
+typedef struct CalypsoRepeaterElement_t CalypsoRepeaterElement;
 
 typedef struct {
     CalypsoElementType type;
@@ -43,6 +45,7 @@ typedef struct {
         CalypsoFinalElement* final;
         CalypsoBitmapElement* bitmap;
         CalypsoContainerElement* container;
+        CalypsoRepeaterElement* repeater;
     };
 } CalypsoElement;
 
@@ -65,6 +68,12 @@ struct CalypsoContainerElement_t {
     CalypsoElement* elements;
 };
 
+struct CalypsoRepeaterElement_t {
+    char key[36];
+    int size;
+    CalypsoElement element;
+};
+
 typedef struct {
     CalypsoAppType type;
     CalypsoContainerElement* container;
@@ -80,6 +89,8 @@ CalypsoElement make_calypso_bitmap_element(const char* key, int size, CalypsoEle
 
 CalypsoElement make_calypso_container_element(const char* key, int size, CalypsoElement* elements);
 
+CalypsoElement make_calypso_repeater_element(const char* key, int size, CalypsoElement element);
+
 void free_calypso_structure(CalypsoApp* structure);
 
 int* get_bitmap_positions(const char* binary_string, int* count);

+ 170 - 87
api/calypso/cards/opus.c

@@ -2,25 +2,6 @@
 #include "opus.h"
 
 CalypsoApp* get_opus_contract_structure() {
-    /*
-    En1545FixedInteger(CONTRACT_UNKNOWN_A, 3),
-    En1545Bitmap(
-            En1545FixedInteger(CONTRACT_PROVIDER, 8),
-            En1545FixedInteger(CONTRACT_TARIFF, 16),
-            En1545Bitmap(
-                    En1545FixedInteger.date(CONTRACT_START),
-                    En1545FixedInteger.date(CONTRACT_END)
-            ),
-            En1545Container(
-                    En1545FixedInteger(CONTRACT_UNKNOWN_B, 17),
-                    En1545FixedInteger.date(CONTRACT_SALE),
-                    En1545FixedInteger.timeLocal(CONTRACT_SALE),
-                    En1545FixedHex(CONTRACT_UNKNOWN_C, 36),
-                    En1545FixedInteger(CONTRACT_STATUS, 8),
-                    En1545FixedHex(CONTRACT_UNKNOWN_D, 36)
-            )
-    )
-    */
     CalypsoApp* OpusContractStructure = malloc(sizeof(CalypsoApp));
 
     if(!OpusContractStructure) {
@@ -76,33 +57,6 @@ CalypsoApp* get_opus_contract_structure() {
 }
 
 CalypsoApp* get_opus_event_structure() {
-    /*
-    En1545Container(
-                En1545FixedInteger.date(EVENT),
-                En1545FixedInteger.timeLocal(EVENT),
-                En1545FixedInteger("UnknownX", 19), // Possibly part of following bitmap
-                En1545Bitmap(
-                        En1545FixedInteger(EVENT_UNKNOWN_A, 8),
-                        En1545FixedInteger(EVENT_UNKNOWN_B, 8),
-                        En1545FixedInteger(EVENT_SERVICE_PROVIDER, 8),
-                        En1545FixedInteger(EVENT_UNKNOWN_C, 16),
-                        En1545FixedInteger(EVENT_ROUTE_NUMBER, 16),
-                        // How 32 bits are split among next 2 fields is unclear
-                        En1545FixedInteger(EVENT_UNKNOWN_D, 16),
-                        En1545FixedInteger(EVENT_UNKNOWN_E, 16),
-                        En1545FixedInteger(EVENT_CONTRACT_POINTER, 5),
-                        En1545Bitmap(
-                                En1545FixedInteger.date(EVENT_FIRST_STAMP),
-                                En1545FixedInteger.timeLocal(EVENT_FIRST_STAMP),
-                                En1545FixedInteger("EventDataSimulation", 1),
-                                En1545FixedInteger(EVENT_UNKNOWN_F, 4),
-                                En1545FixedInteger(EVENT_UNKNOWN_G, 4),
-                                En1545FixedInteger(EVENT_UNKNOWN_H, 4),
-                                En1545FixedInteger(EVENT_UNKNOWN_I, 4)
-                        )
-                )
-        )
-    */
     CalypsoApp* OpusEventStructure = malloc(sizeof(CalypsoApp));
 
     if(!OpusEventStructure) {
@@ -126,39 +80,109 @@ CalypsoApp* get_opus_event_structure() {
         "Event",
         9,
         (CalypsoElement[]){
-            make_calypso_final_element("EventUnknownA", 8, "Unknown A", CALYPSO_FINAL_TYPE_NUMBER),
-            make_calypso_final_element("EventUnknownB", 8, "Unknown B", CALYPSO_FINAL_TYPE_NUMBER),
+            make_calypso_final_element("EventUnknownA", 8, "Unknown A", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventResult", 8, "Code Résultat", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventServiceProvider",
+                8,
+                "Identité de l’exploitant",
+                CALYPSO_FINAL_TYPE_SERVICE_PROVIDER),
             make_calypso_final_element(
-                "EventServiceProvider", 8, "Service provider", CALYPSO_FINAL_TYPE_SERVICE_PROVIDER),
-            make_calypso_final_element("EventUnknownC", 16, "Unknown C", CALYPSO_FINAL_TYPE_NUMBER),
+                "EventLocationId", 16, "Lieu de l’événement", CALYPSO_FINAL_TYPE_UNKNOWN),
             make_calypso_final_element(
-                "EventRouteNumber", 16, "Route number", CALYPSO_FINAL_TYPE_NUMBER),
-            make_calypso_final_element("EventUnknownD", 16, "Unknown D", CALYPSO_FINAL_TYPE_NUMBER),
-            make_calypso_final_element("EventUnknownE", 16, "Unknown E", CALYPSO_FINAL_TYPE_NUMBER),
+                "EventRouteNumber", 16, "Référence de la ligne", CALYPSO_FINAL_TYPE_UNKNOWN),
             make_calypso_final_element(
-                "EventContractPointer", 5, "Contract pointer", CALYPSO_FINAL_TYPE_NUMBER),
+                "EventUnknownD", 16, "Unknown D", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventUnknownE", 16, "Unknown E", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventContractPointer",
+                5,
+                "Référence du contrat concerné",
+                CALYPSO_FINAL_TYPE_UNKNOWN),
             make_calypso_bitmap_element(
                 "EventData",
                 7,
                 (CalypsoElement[]){
                     make_calypso_final_element(
-                        "EventFirstStampDate", 14, "First stamp date", CALYPSO_FINAL_TYPE_DATE),
+                        "EventDataDateFirstStamp",
+                        14,
+                        "Date de la première montée",
+                        CALYPSO_FINAL_TYPE_DATE),
                     make_calypso_final_element(
-                        "EventFirstStampTime", 11, "First stamp time", CALYPSO_FINAL_TYPE_TIME),
+                        "EventDataTimeFirstStamp",
+                        11,
+                        "Heure de la première montée",
+                        CALYPSO_FINAL_TYPE_TIME),
                     make_calypso_final_element(
                         "EventDataSimulation", 1, "Simulation", CALYPSO_FINAL_TYPE_UNKNOWN),
                     make_calypso_final_element(
-                        "EventUnknownF", 4, "Unknown F", CALYPSO_FINAL_TYPE_NUMBER),
+                        "EventDataRouteDirection", 4, "Sens", CALYPSO_FINAL_TYPE_UNKNOWN),
                     make_calypso_final_element(
-                        "EventUnknownG", 4, "Unknown G", CALYPSO_FINAL_TYPE_NUMBER),
+                        "EventUnknownG", 4, "Unknown G", CALYPSO_FINAL_TYPE_UNKNOWN),
                     make_calypso_final_element(
-                        "EventUnknownH", 4, "Unknown H", CALYPSO_FINAL_TYPE_NUMBER),
+                        "EventUnknownH", 4, "Unknown H", CALYPSO_FINAL_TYPE_UNKNOWN),
                     make_calypso_final_element(
-                        "EventUnknownI", 4, "Unknown I", CALYPSO_FINAL_TYPE_NUMBER),
+                        "EventUnknownI", 4, "Unknown I", CALYPSO_FINAL_TYPE_UNKNOWN),
                 }),
         });
 
     return OpusEventStructure;
+
+    /*
+    EventDateStamp: 10011111100001
+    EventTimeStamp: 01111110001
+    UNKNOWN: 0000000000000000000
+    EventBitmap: 110011110
+        EventResult: 00000000
+        EventServiceProvider: 00000010
+        EventLocationId: 0000000001100101
+        EventRouteNumber: 0000000011011011
+        EventContractPointer: 00100
+        EventDataBitmap: 0001111
+            EventDataDateFirstStamp: 10011111100001
+            EventDataTimeFirstStamp: 01111011100
+            EventDataSimulation: 0
+            EventDataRouteDirection: 0110
+    00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+    */
+
+    /*
+    EventDateStamp: 10011111100001
+    EventTimeStamp: 01111011100
+    UNKNOWN: 0000000000000000000
+    EventBitmap: 110011110
+        EventResult: 00000000
+        EventServiceProvider: 00000010
+        EventLocationId: 0000000011001001
+        EventRouteNumber: 0000000000000010
+        EventContractPointer: 00100
+        EventDataBitmap: 0001111
+            EventDataDateFirstStamp: 10011111100001
+            EventDataTimeFirstStamp: 01111011100
+            EventDataSimulation: 0
+            EventDataRouteDirection: 0001
+    00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+    */
+
+    /*
+    EventDateStamp: 10011111100001
+    EventTimeStamp: 01010011111
+    UNKNOWN: 0000000000000000000
+    EventBitmap: 110011110
+        EventResult: 00000000
+        EventServiceProvider: 00000010
+        EventLocationId: 0000000011001001
+        EventRouteNumber: 0000000000000010
+        EventContractPointer: 00100
+        EventDataBitmap: 0001111
+            EventDataDateFirstStamp: 10011111100001
+            EventDataTimeFirstStamp: 01000101111
+            EventDataSimulation: 0
+            EventDataRouteDirection: 0001
+    00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+    */
 }
 
 CalypsoApp* get_opus_env_holder_structure() {
@@ -196,50 +220,109 @@ CalypsoApp* get_opus_env_holder_structure() {
                 14,
                 "Date de fin de validité de l’application",
                 CALYPSO_FINAL_TYPE_DATE),
-            make_calypso_final_element(
-                "EnvPayMethod", 11, "Code mode de paiement", CALYPSO_FINAL_TYPE_PAY_METHOD),
-            make_calypso_final_element(
-                "EnvAuthenticator",
-                16,
-                "Code de contrôle de l’intégrité des données",
-                CALYPSO_FINAL_TYPE_UNKNOWN),
-            make_calypso_final_element(
-                "EnvSelectList",
-                32,
-                "Bitmap de tableau de paramètre multiple",
-                CALYPSO_FINAL_TYPE_UNKNOWN),
-            make_calypso_container_element(
+            make_calypso_bitmap_element(
                 "EnvData",
-                2,
+                4,
                 (CalypsoElement[]){
                     make_calypso_final_element(
                         "EnvDataCardStatus", 1, "Statut de la carte", CALYPSO_FINAL_TYPE_UNKNOWN),
                     make_calypso_final_element(
                         "EnvData2", 0, "Données complémentaires", CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "EnvData_CardUtilisation",
+                        1,
+                        "Utilisation de la carte",
+                        CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "EnvData4", 0, "Données complémentaires", CALYPSO_FINAL_TYPE_UNKNOWN),
                 }),
+            make_calypso_final_element("EnvUnknownA", 0, "Unknown A", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element("EnvUnknownB", 0, "Unknown B", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element("EnvUnknownC", 0, "Unknown C", CALYPSO_FINAL_TYPE_UNKNOWN),
         });
 
+    /*
+    HolderProf1Number "01"
+    HolderProf1Date - ok
+    HolderData_Language "FR"
+    HolderData_ResidenceCode 0
+    HolderBirthDate - ok
+     */
     OpusEnvHolderStructure->container->elements[2] = make_calypso_bitmap_element(
         "Holder",
-        2,
+        8,
         (CalypsoElement[]){
-            make_calypso_container_element(
-                "HolderData",
-                5,
+            make_calypso_bitmap_element(
+                "HolderBirthBitmap",
+                2,
                 (CalypsoElement[]){
                     make_calypso_final_element(
-                        "HolderUnknownA", 3, "Unknown A", CALYPSO_FINAL_TYPE_NUMBER),
-                    make_calypso_final_element(
-                        "HolderBirthDate", 8, "Birth date", CALYPSO_FINAL_TYPE_DATE),
+                        "HolderBirthDate", 32, "Date de naissance", CALYPSO_FINAL_TYPE_DATE),
                     make_calypso_final_element(
-                        "HolderUnknownB", 13, "Unknown B", CALYPSO_FINAL_TYPE_NUMBER),
-                    make_calypso_final_element(
-                        "HolderProfile", 17, "Profile", CALYPSO_FINAL_TYPE_DATE),
-                    make_calypso_final_element(
-                        "HolderUnknownC", 8, "Unknown C", CALYPSO_FINAL_TYPE_NUMBER),
+                        "HolderBirthUnknownA", 0, "Unknown A", CALYPSO_FINAL_TYPE_UNKNOWN),
                 }),
-            make_calypso_final_element("HolderUnknownD", 8, "Unknown D", CALYPSO_FINAL_TYPE_NUMBER),
+            make_calypso_repeater_element(
+                "HolderProfilesList",
+                4,
+                make_calypso_bitmap_element(
+                    "HolderProfile",
+                    3,
+                    (CalypsoElement[]){
+                        make_calypso_final_element(
+                            "HolderProfileNumber", 6, "Numéro de profil", CALYPSO_FINAL_TYPE_NUMBER),
+                        make_calypso_final_element(
+                            "HolderProfileDate", 14, "Date de profil", CALYPSO_FINAL_TYPE_DATE),
+                        make_calypso_final_element(
+                            "HolderProfileUnknownA", 0, "Unknown A", CALYPSO_FINAL_TYPE_UNKNOWN),
+                    })),
+            make_calypso_final_element(
+                "HolderData_Language", 6, "Langue de l'utilisateur", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "HolderUnknownA", 0, "Unknown A", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "HolderUnknownB", 0, "Unknown B", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "HolderUnknownC", 0, "Unknown C", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "HolderUnknownD", 0, "Unknown D", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "HolderUnknownE", 0, "Unknown E", CALYPSO_FINAL_TYPE_UNKNOWN),
         });
 
     return OpusEnvHolderStructure;
+
+    /*
+    missing: EnvDataCardStatus=false, EnvData_CardUtilisation=true, HolderProf1Number="01", HolderData_ResidenceCode=0
+
+    EnvApplicationVersionNumber: 000001
+    EnvBitmap: 0001111
+        EnvNetworkId: 000100100100000000000001
+        EnvApplicationIssuerId: 00100000
+        EnvApplicationValidityEndDate: 10100100100010
+        EnvDataBitmap: 0101
+            EnvDataCardStatus: 0
+            EnvData_CardUtilisation: 1
+    HolderBitmap: 00000111
+        HolderBirthBitmap: 01
+            HolderBirthDate: 00100000000001010000100000000111
+        HolderProfilesList: 0001
+            HolderProfileBitmap: 011
+                HolderProfileNumber: 000001
+                HolderProfileDate: 10100100100010
+        HolderData_Language: 110001
+    00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+    */
+
+    /*
+    EnvApplicationVersionNumber: 000001
+    EnvBitmap: 0001111
+        EnvNetworkId: 000100100100000000000001
+        EnvApplicationIssuerId: 00100000
+        EnvApplicationValidityEndDate: 10110101101010
+    0101010000011101
+    HolderBirthDate: 00011001011100100000001000000001
+    HolderProfCount: 0000
+    HolderData_Language: 110001
+    0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+    */
 }

+ 38 - 14
api/calypso/transit/opus.c

@@ -28,11 +28,20 @@ const char* get_opus_service_provider(int provider) {
     }
 }
 
-const char* get_opus_transport_type(int route_number) {
-    if(route_number >= 0x01 && route_number <= 0x04) {
-        return "Metro";
-    } else {
+const char* get_opus_transport_type(int location_id) {
+    switch(location_id) {
+    case 0x65:
         return "Bus";
+    case 0xc9:
+        return "Metro";
+    default: {
+        char* location_str = malloc(9 * sizeof(char));
+        if(!location_str) {
+            return "Unknown";
+        }
+        snprintf(location_str, 9, "0x%02X", location_id);
+        return location_str;
+    }
     }
 }
 
@@ -82,18 +91,21 @@ void show_opus_event_info(
     furi_string_cat_printf(
         parsed_data,
         "%s %s\n",
-        get_opus_transport_type(event->route_number),
+        get_opus_transport_type(event->location_id),
         get_opus_transport_line(event->route_number));
     furi_string_cat_printf(
         parsed_data, "Transporter: %s\n", get_opus_service_provider(event->service_provider));
-    if(event->used_contract_available) {
-        furi_string_cat_printf(
-            parsed_data,
-            "Contract: %d - %s\n",
-            event->used_contract,
-            get_opus_tariff(contracts[event->used_contract - 1].tariff));
-    }
+    furi_string_cat_printf(parsed_data, "Result: %d\n", event->result);
+    furi_string_cat_printf(
+        parsed_data,
+        "Contract: %d - %s\n",
+        event->used_contract,
+        get_opus_tariff(contracts[event->used_contract - 1].tariff));
+    furi_string_cat_printf(parsed_data, "Simulation: %s\n", event->simulation ? "true" : "false");
+    furi_string_cat_printf(parsed_data, "Date: ");
     locale_format_datetime_cat(parsed_data, &event->date, true);
+    furi_string_cat_printf(parsed_data, "\nFirst stamp: ");
+    locale_format_datetime_cat(parsed_data, &event->first_stamp_date, true);
     furi_string_cat_printf(parsed_data, "\n");
 }
 
@@ -101,7 +113,7 @@ void show_opus_contract_info(OpusCardContract* contract, FuriString* parsed_data
     furi_string_cat_printf(parsed_data, "Type: %s\n", get_opus_tariff(contract->tariff));
     furi_string_cat_printf(
         parsed_data, "Provider: %s\n", get_opus_service_provider(contract->provider));
-    furi_string_cat_printf(parsed_data, "Valid\nfrom: ");
+    furi_string_cat_printf(parsed_data, "Valid from: ");
     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);
@@ -111,7 +123,10 @@ void show_opus_contract_info(OpusCardContract* contract, FuriString* parsed_data
     // furi_string_cat_printf(parsed_data, "\nStatus: %d\n", contract->status);
 }
 
-void show_opus_environment_info(OpusCardEnv* environment, FuriString* parsed_data) {
+void show_opus_environment_info(
+    OpusCardEnv* environment,
+    OpusCardHolder* holder,
+    FuriString* parsed_data) {
     furi_string_cat_printf(parsed_data, "App Version: %d\n", environment->app_version);
     furi_string_cat_printf(
         parsed_data, "Country: %s\n", get_country_string(environment->country_num));
@@ -121,5 +136,14 @@ void show_opus_environment_info(OpusCardEnv* environment, FuriString* parsed_dat
         get_network_string(guess_card_type(environment->country_num, environment->network_num)));
     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, "\nIssuer: %d\n", environment->issuer_id);
+    furi_string_cat_printf(
+        parsed_data, "Card status: %s\n", environment->card_status ? "true" : "false");
+    furi_string_cat_printf(
+        parsed_data,
+        "Card utilisation: %s\n",
+        environment->card_utilisation ? "Used" : "Not used");
+    furi_string_cat_printf(parsed_data, "Holder birth date: ");
+    locale_format_datetime_cat(parsed_data, &holder->birth_date, false);
     furi_string_cat_printf(parsed_data, "\n");
 }

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

@@ -12,6 +12,9 @@ void show_opus_event_info(
 
 void show_opus_contract_info(OpusCardContract* contract, FuriString* parsed_data);
 
-void show_opus_environment_info(OpusCardEnv* environment, FuriString* parsed_data);
+void show_opus_environment_info(
+    OpusCardEnv* environment,
+    OpusCardHolder* holder,
+    FuriString* parsed_data);
 
 #endif // OPUS_H

+ 16 - 4
api/calypso/transit/opus_i.h

@@ -6,23 +6,35 @@
 
 typedef struct {
     int service_provider;
+    int result;
     int route_number;
-    bool route_number_available;
+    int route_direction;
+    int location_id;
     int used_contract;
-    bool used_contract_available;
+    bool simulation;
     DateTime date;
+    DateTime first_stamp_date;
 } OpusCardEvent;
 
 typedef struct {
     int app_version;
     int country_num;
     int network_num;
+    int issuer_id;
+    bool card_status;
+    bool card_utilisation;
     DateTime end_dt;
 } OpusCardEnv;
 
 typedef struct {
-    int card_status;
-    int commercial_id;
+    int number;
+    DateTime date;
+} OpusCardHolderProfile;
+
+typedef struct {
+    DateTime birth_date;
+    OpusCardHolderProfile profiles[4];
+    int language;
 } OpusCardHolder;
 
 typedef struct {

+ 165 - 48
scenes/metroflip_scene_calypso.c

@@ -120,7 +120,8 @@ void update_page_info(void* context, FuriString* parsed_data) {
         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#Environment:\n");
-            show_opus_environment_info(&ctx->card->opus->environment, parsed_data);
+            show_opus_environment_info(
+                &ctx->card->opus->environment, &ctx->card->opus->holder, parsed_data);
             break;
         }
         case CALYPSO_CARD_RAVKAV: {
@@ -172,44 +173,48 @@ void update_page_info(void* context, FuriString* parsed_data) {
         }
         }
     } else if(ctx->page_id >= 5) {
-        furi_string_cat_printf(parsed_data, "\e#Event %d:\n", ctx->page_id - 4);
-        switch(ctx->card->card_type) {
-        case CALYPSO_CARD_NAVIGO: {
-            show_navigo_event_info(
-                &ctx->card->navigo->events[ctx->page_id - 5],
-                ctx->card->navigo->contracts,
-                parsed_data);
-            break;
-        }
-        case CALYPSO_CARD_OPUS: {
-            show_opus_event_info(
-                &ctx->card->opus->events[ctx->page_id - 5],
-                ctx->card->opus->contracts,
-                parsed_data);
-            break;
-        }
-        case CALYPSO_CARD_RAVKAV: {
-            show_ravkav_event_info(&ctx->card->ravkav->events[ctx->page_id - 5], parsed_data);
-            break;
-        }
-        default: {
-            break;
-        }
-        }
-    } else if(ctx->page_id == 8 || ctx->page_id == 9 || ctx->page_id == 10) {
-        furi_string_cat_printf(parsed_data, "\e#Special Event %d:\n", ctx->page_id - 7);
-        switch(ctx->card->card_type) {
-        case CALYPSO_CARD_NAVIGO: {
-            show_navigo_special_event_info(
-                &ctx->card->navigo->special_events[ctx->page_id - 8], parsed_data);
-            break;
-        }
-        case CALYPSO_CARD_OPUS: {
-            break;
-        }
-        default: {
-            break;
-        }
+        if(ctx->page_id - 5 < ctx->card->events_count) {
+            furi_string_cat_printf(parsed_data, "\e#Event %d:\n", ctx->page_id - 4);
+            switch(ctx->card->card_type) {
+            case CALYPSO_CARD_NAVIGO: {
+                show_navigo_event_info(
+                    &ctx->card->navigo->events[ctx->page_id - 5],
+                    ctx->card->navigo->contracts,
+                    parsed_data);
+                break;
+            }
+            case CALYPSO_CARD_OPUS: {
+                show_opus_event_info(
+                    &ctx->card->opus->events[ctx->page_id - 5],
+                    ctx->card->opus->contracts,
+                    parsed_data);
+                break;
+            }
+            case CALYPSO_CARD_RAVKAV: {
+                show_ravkav_event_info(&ctx->card->ravkav->events[ctx->page_id - 5], parsed_data);
+                break;
+            }
+            default: {
+                break;
+            }
+            }
+        } else {
+            furi_string_cat_printf(
+                parsed_data, "\e#Special Event %d:\n", ctx->page_id - ctx->card->events_count - 4);
+            switch(ctx->card->card_type) {
+            case CALYPSO_CARD_NAVIGO: {
+                show_navigo_special_event_info(
+                    &ctx->card->navigo->special_events[ctx->page_id - ctx->card->events_count - 5],
+                    parsed_data);
+                break;
+            }
+            case CALYPSO_CARD_OPUS: {
+                break;
+            }
+            default: {
+                break;
+            }
+            }
         }
     }
 }
@@ -1296,6 +1301,16 @@ static NfcCommand metroflip_scene_calypso_poller_callback(NfcGenericEvent event,
                     card->opus->environment.app_version =
                         bit_slice_to_dec(environment_bit_representation, start, end);
 
+                    // EnvApplicationIssuerId
+                    env_key = "EnvApplicationIssuerId";
+                    positionOffset = get_calypso_node_offset(
+                        environment_bit_representation, env_key, OpusEnvHolderStructure);
+                    start = positionOffset,
+                    end = positionOffset + get_calypso_node_size(env_key, OpusEnvHolderStructure) -
+                          1;
+                    card->opus->environment.issuer_id =
+                        bit_slice_to_dec(environment_bit_representation, start, end);
+
                     // EnvApplicationValidityEndDate
                     env_key = "EnvApplicationValidityEndDate";
                     positionOffset = get_calypso_node_offset(
@@ -1310,26 +1325,51 @@ static NfcCommand metroflip_scene_calypso_poller_callback(NfcGenericEvent event,
                     datetime_timestamp_to_datetime(
                         end_validity_timestamp, &card->opus->environment.end_dt);
 
-                    // HolderDataCardStatus
-                    env_key = "HolderDataCardStatus";
+                    // EnvDataCardStatus
+                    env_key = "EnvDataCardStatus";
                     positionOffset = get_calypso_node_offset(
                         environment_bit_representation, env_key, OpusEnvHolderStructure);
                     start = positionOffset,
                     end = positionOffset + get_calypso_node_size(env_key, OpusEnvHolderStructure) -
                           1;
-                    card->opus->holder.card_status =
+                    card->opus->environment.card_status =
                         bit_slice_to_dec(environment_bit_representation, start, end);
 
-                    // HolderDataCommercialID
-                    env_key = "HolderDataCommercialID";
+                    // EnvData_CardUtilisation
+                    env_key = "EnvData_CardUtilisation";
                     positionOffset = get_calypso_node_offset(
                         environment_bit_representation, env_key, OpusEnvHolderStructure);
                     start = positionOffset,
                     end = positionOffset + get_calypso_node_size(env_key, OpusEnvHolderStructure) -
                           1;
-                    card->opus->holder.commercial_id =
+                    card->opus->environment.card_utilisation =
                         bit_slice_to_dec(environment_bit_representation, start, end);
 
+                    // HolderBirthDate
+                    env_key = "HolderBirthDate";
+                    positionOffset = get_calypso_node_offset(
+                        environment_bit_representation, env_key, OpusEnvHolderStructure);
+                    FURI_LOG_I(TAG, "HolderBirthDate positionOffset: %d", positionOffset);
+                    FURI_LOG_I(
+                        TAG,
+                        "HolderBirthDate size: %d",
+                        get_calypso_node_size(env_key, OpusEnvHolderStructure));
+                    start = positionOffset, end = positionOffset + 3;
+                    card->opus->holder.birth_date.year =
+                        bit_slice_to_dec(environment_bit_representation, start, end) * 1000 +
+                        bit_slice_to_dec(environment_bit_representation, start + 4, end + 4) *
+                            100 +
+                        bit_slice_to_dec(environment_bit_representation, start + 8, end + 8) * 10 +
+                        bit_slice_to_dec(environment_bit_representation, start + 12, end + 12);
+                    start += 16, end += 16;
+                    card->opus->holder.birth_date.month =
+                        bit_slice_to_dec(environment_bit_representation, start, end) * 10 +
+                        bit_slice_to_dec(environment_bit_representation, start + 4, end + 4);
+                    start += 8, end += 8;
+                    card->opus->holder.birth_date.day =
+                        bit_slice_to_dec(environment_bit_representation, start, end) * 10 +
+                        bit_slice_to_dec(environment_bit_representation, start + 4, end + 4);
+
                     // Free the calypso structure
                     free_calypso_structure(OpusEnvHolderStructure);
 
@@ -1543,8 +1583,21 @@ static NfcCommand metroflip_scene_calypso_poller_callback(NfcGenericEvent event,
                                 event_bit_representation, bits, sizeof(event_bit_representation));
                         }
 
+                        // EventResult
+                        const char* event_key = "EventResult";
+                        if(is_calypso_node_present(
+                               event_bit_representation, event_key, OpusEventStructure)) {
+                            int positionOffset = get_calypso_node_offset(
+                                event_bit_representation, event_key, OpusEventStructure);
+                            int start = positionOffset,
+                                end = positionOffset +
+                                      get_calypso_node_size(event_key, OpusEventStructure) - 1;
+                            card->opus->events[i - 1].result =
+                                bit_slice_to_dec(event_bit_representation, start, end);
+                        }
+
                         // EventServiceProvider
-                        const char* event_key = "EventServiceProvider";
+                        event_key = "EventServiceProvider";
                         if(is_calypso_node_present(
                                event_bit_representation, event_key, OpusEventStructure)) {
                             int positionOffset = get_calypso_node_offset(
@@ -1556,6 +1609,19 @@ static NfcCommand metroflip_scene_calypso_poller_callback(NfcGenericEvent event,
                                 bit_slice_to_dec(event_bit_representation, start, end);
                         }
 
+                        // EventLocationId
+                        event_key = "EventLocationId";
+                        if(is_calypso_node_present(
+                               event_bit_representation, event_key, OpusEventStructure)) {
+                            int positionOffset = get_calypso_node_offset(
+                                event_bit_representation, event_key, OpusEventStructure);
+                            int start = positionOffset,
+                                end = positionOffset +
+                                      get_calypso_node_size(event_key, OpusEventStructure) - 1;
+                            card->opus->events[i - 1].location_id =
+                                bit_slice_to_dec(event_bit_representation, start, end);
+                        }
+
                         // EventRouteNumber
                         event_key = "EventRouteNumber";
                         if(is_calypso_node_present(
@@ -1567,7 +1633,6 @@ static NfcCommand metroflip_scene_calypso_poller_callback(NfcGenericEvent event,
                                       get_calypso_node_size(event_key, OpusEventStructure) - 1;
                             card->opus->events[i - 1].route_number =
                                 bit_slice_to_dec(event_bit_representation, start, end);
-                            card->opus->events[i - 1].route_number_available = true;
                         }
 
                         // EventContractPointer
@@ -1581,12 +1646,37 @@ static NfcCommand metroflip_scene_calypso_poller_callback(NfcGenericEvent event,
                                       get_calypso_node_size(event_key, OpusEventStructure) - 1;
                             card->opus->events[i - 1].used_contract =
                                 bit_slice_to_dec(event_bit_representation, start, end);
-                            card->opus->events[i - 1].used_contract_available = true;
                             if(card->opus->events[i - 1].used_contract > 0) {
                                 card->events_count++;
                             }
                         }
 
+                        // EventDataSimulation
+                        event_key = "EventDataSimulation";
+                        if(is_calypso_node_present(
+                               event_bit_representation, event_key, OpusEventStructure)) {
+                            int positionOffset = get_calypso_node_offset(
+                                event_bit_representation, event_key, OpusEventStructure);
+                            int start = positionOffset,
+                                end = positionOffset +
+                                      get_calypso_node_size(event_key, OpusEventStructure) - 1;
+                            card->opus->events[i - 1].simulation =
+                                bit_slice_to_dec(event_bit_representation, start, end);
+                        }
+
+                        // EventDataRouteDirection
+                        event_key = "EventDataRouteDirection";
+                        if(is_calypso_node_present(
+                               event_bit_representation, event_key, OpusEventStructure)) {
+                            int positionOffset = get_calypso_node_offset(
+                                event_bit_representation, event_key, OpusEventStructure);
+                            int start = positionOffset,
+                                end = positionOffset +
+                                      get_calypso_node_size(event_key, OpusEventStructure) - 1;
+                            card->opus->events[i - 1].route_direction =
+                                bit_slice_to_dec(event_bit_representation, start, end);
+                        }
+
                         // EventDateStamp
                         event_key = "EventDateStamp";
                         int positionOffset = get_calypso_node_offset(
@@ -1610,6 +1700,33 @@ static NfcCommand metroflip_scene_calypso_poller_callback(NfcGenericEvent event,
                         card->opus->events[i - 1].date.hour = (decimal_value * 60) / 3600;
                         card->opus->events[i - 1].date.minute = ((decimal_value * 60) % 3600) / 60;
                         card->opus->events[i - 1].date.second = ((decimal_value * 60) % 3600) % 60;
+
+                        // EventDataDateFirstStamp
+                        event_key = "EventDataDateFirstStamp";
+                        positionOffset = get_calypso_node_offset(
+                            event_bit_representation, event_key, OpusEventStructure);
+                        start = positionOffset,
+                        end = positionOffset +
+                              get_calypso_node_size(event_key, OpusEventStructure) - 1;
+                        decimal_value = bit_slice_to_dec(event_bit_representation, start, end);
+                        uint64_t first_date_timestamp = (decimal_value * 24 * 3600) + epoch + 3600;
+                        datetime_timestamp_to_datetime(
+                            first_date_timestamp, &card->opus->events[i - 1].first_stamp_date);
+
+                        // EventDataTimeFirstStamp
+                        event_key = "EventDataTimeFirstStamp";
+                        positionOffset = get_calypso_node_offset(
+                            event_bit_representation, event_key, OpusEventStructure);
+                        start = positionOffset,
+                        end = positionOffset +
+                              get_calypso_node_size(event_key, OpusEventStructure) - 1;
+                        decimal_value = bit_slice_to_dec(event_bit_representation, start, end);
+                        card->opus->events[i - 1].first_stamp_date.hour =
+                            (decimal_value * 60) / 3600;
+                        card->opus->events[i - 1].first_stamp_date.minute =
+                            ((decimal_value * 60) % 3600) / 60;
+                        card->opus->events[i - 1].first_stamp_date.second =
+                            ((decimal_value * 60) % 3600) % 60;
                     }
 
                     // Free the calypso structure