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

Merge pull request #25 from DocSystem/dev

Unified Calypso parser
Luu 1 год назад
Родитель
Сommit
469933163e

+ 14 - 12
README.md

@@ -67,18 +67,19 @@ This is a list of metro cards and transit systems that need support or have part
 
 ## ✅ Supported Cards
 
-| **Card / Agency** | **Country / City**                  | **Card Type**     |
-|--------------------|-------------------------------------|-------------------|
-| **Bip!**          | 🇨🇱 Santiago de Chile, Chile        | Mifare Classic    |
-| **Charliecard**    | 🇺🇸 Boston, MA, USA                 | Mifare Classic    |
-| **Clipper**        | 🇺🇸 San Francisco, CA, USA          | Mifare DESFire    |
-| **ITSO**           | 🇬🇧 United Kingdom                 | Mifare DESFire    |
-| **Metromoney**     | 🇬🇪 Tbilisi, Georgia                | Mifare Classic    |
-| **myki**           | 🇦🇺 Melbourne (and surrounds), VIC, Australia | Mifare DESFire |
-| **Navigo**         | 🇫🇷 Paris, France                   | Calypso           |
-| **Opal**           | 🇦🇺 Sydney (and surrounds), NSW, Australia | Mifare DESFire |
-| **Rav-Kav**        | 🇮🇱 Israel                          | Calypso           |
-| **Troika**         | 🇷🇺 Moscow, Russia                  | Mifare Classic    |
+| **Card / Agency**  | **Country / City**                          | **Card Type**     |
+|--------------------|---------------------------------------------|-------------------|
+| **Bip!**           | 🇨🇱 Santiago de Chile, Chile                  | Mifare Classic    |
+| **Charliecard**    | 🇺🇸 Boston, MA, USA                           | Mifare Classic    |
+| **Clipper**        | 🇺🇸 San Francisco, CA, USA                    | Mifare DESFire    |
+| **ITSO**           | 🇬🇧 United Kingdom                            | Mifare DESFire    |
+| **Metromoney**     | 🇬🇪 Tbilisi, Georgia                          | Mifare Classic    |
+| **myki**           | 🇦🇺 Melbourne (and surrounds), VIC, Australia | Mifare DESFire    |
+| **Navigo**         | 🇫🇷 Paris, France                             | Calypso           |
+| **Opus**           | 🇨🇦 Montreal, QC, Canada                      | Calypso           |
+| **Opal**           | 🇦🇺 Sydney (and surrounds), NSW, Australia    | Mifare DESFire    |
+| **Rav-Kav**        | 🇮🇱 Israel                                    | Calypso           |
+| **Troika**         | 🇷🇺 Moscow, Russia                            | Mifare Classic    |
 
 
 
@@ -89,6 +90,7 @@ This is a list of metro cards and transit systems that need support or have part
 - **Charliecard Parser**: [@zacharyweiss](https://github.com/zacharyweiss)
 - **Rav-Kav Parser**: [@luu176](https://github.com/luu176)
 - **Navigo Parser**: [@luu176](https://github.com/luu176), [@DocSystem](https://github.com/DocSystem)
+- **Opus Parser**: [@DocSystem](https://github.com/DocSystem)
 - **Metromoney Parser**: [@Leptopt1los](https://github.com/Leptopt1los)
 - **Bip! Parser**: [@rbasoalto](https://github.com/rbasoalto), [@gornekich](https://github.com/gornekich)
 - **Clipper Parser**: [@ke6jjj](https://github.com/ke6jjj)

+ 41 - 0
api/calypso/calypso_i.h

@@ -0,0 +1,41 @@
+#include "transit/navigo_i.h"
+#include "transit/opus_i.h"
+#include <furi.h>
+
+#ifndef CALYPSO_I_H
+#define CALYPSO_I_H
+
+typedef enum {
+    CALYPSO_CARD_MOBIB,
+    CALYPSO_CARD_OPUS,
+    CALYPSO_CARD_VIVA,
+    CALYPSO_CARD_PASSPASS,
+    CALYPSO_CARD_TAM,
+    CALYPSO_CARD_OURA,
+    CALYPSO_CARD_NAVIGO,
+    CALYPSO_CARD_KORRIGO,
+    CALYPSO_CARD_TISSEO,
+    CALYPSO_CARD_ENVIBUS,
+    CALYPSO_CARD_GIRONDE,
+    CALYPSO_CARD_RAVKAV,
+    CALYPSO_CARD_UNKNOWN
+} CALYPSO_CARD_TYPE;
+
+typedef struct {
+    NavigoCardData* navigo;
+    OpusCardData* opus;
+
+    CALYPSO_CARD_TYPE card_type;
+    unsigned int card_number;
+
+    int contracts_count;
+} CalypsoCardData;
+
+typedef struct {
+    CalypsoCardData* card;
+    int page_id;
+    // mutex
+    FuriMutex* mutex;
+} CalypsoContext;
+
+#endif // CALYPSO_I_H

+ 202 - 76
api/calypso/calypso_util.c

@@ -34,23 +34,46 @@ CalypsoElement make_calypso_bitmap_element(const char* key, int size, CalypsoEle
     return bitmap_element;
 }
 
+CalypsoElement
+    make_calypso_container_element(const char* key, int size, CalypsoElement* elements) {
+    CalypsoElement container_element = {};
+
+    container_element.type = CALYPSO_ELEMENT_TYPE_CONTAINER;
+    container_element.container = malloc(sizeof(CalypsoContainerElement));
+    container_element.container->size = size;
+    container_element.container->elements = malloc(size * sizeof(CalypsoElement));
+    for(int i = 0; i < size; i++) {
+        container_element.container->elements[i] = elements[i];
+    }
+    strncpy(container_element.container->key, key, 36);
+
+    return container_element;
+}
+
 void free_calypso_element(CalypsoElement* element) {
     if(element->type == CALYPSO_ELEMENT_TYPE_FINAL) {
         free(element->final);
-    } else {
+    } else if(element->type == CALYPSO_ELEMENT_TYPE_BITMAP) {
         for(int i = 0; i < element->bitmap->size; i++) {
             free_calypso_element(&element->bitmap->elements[i]);
         }
         free(element->bitmap->elements);
         free(element->bitmap);
+    } else if(element->type == CALYPSO_ELEMENT_TYPE_CONTAINER) {
+        for(int i = 0; i < element->container->size; i++) {
+            free_calypso_element(&element->container->elements[i]);
+        }
+        free(element->container->elements);
+        free(element->container);
     }
 }
 
 void free_calypso_structure(CalypsoApp* structure) {
-    for(int i = 0; i < structure->elements_size; i++) {
-        free_calypso_element(&structure->elements[i]);
+    for(int i = 0; i < structure->container->size; i++) {
+        free_calypso_element(&structure->container->elements[i]);
     }
-    free(structure->elements);
+    free(structure->container->elements);
+    free(structure->container);
     free(structure);
 }
 
@@ -118,25 +141,25 @@ bool is_calypso_subnode_present(
 
 bool is_calypso_node_present(const char* binary_string, const char* key, CalypsoApp* structure) {
     int offset = 0;
-    for(int i = 0; i < structure->elements_size; i++) {
-        if(structure->elements[i].type == CALYPSO_ELEMENT_TYPE_FINAL) {
-            if(strcmp(structure->elements[i].final->key, key) == 0) {
+    for(int i = 0; i < structure->container->size; i++) {
+        if(structure->container->elements[i].type == CALYPSO_ELEMENT_TYPE_FINAL) {
+            if(strcmp(structure->container->elements[i].final->key, key) == 0) {
                 return true;
             }
-            offset += structure->elements[i].final->size;
+            offset += structure->container->elements[i].final->size;
         } else {
-            if(strcmp(structure->elements[i].bitmap->key, key) == 0) {
+            if(strcmp(structure->container->elements[i].bitmap->key, key) == 0) {
                 return true;
             }
-            int sub_binary_string_size = structure->elements[i].bitmap->size;
+            int sub_binary_string_size = structure->container->elements[i].bitmap->size;
             char bit_slice[sub_binary_string_size + 1];
             strncpy(bit_slice, binary_string, sub_binary_string_size);
             bit_slice[sub_binary_string_size] = '\0';
             if(is_calypso_subnode_present(
-                   binary_string + offset, key, structure->elements[i].bitmap)) {
+                   binary_string + offset, key, structure->container->elements[i].bitmap)) {
                 return true;
             }
-            offset += structure->elements[i].bitmap->size;
+            offset += structure->container->elements[i].bitmap->size;
         }
     }
     return false;
@@ -145,68 +168,72 @@ bool is_calypso_node_present(const char* binary_string, const char* key, Calypso
 int get_calypso_subnode_offset(
     const char* binary_string,
     const char* key,
-    CalypsoBitmapElement* bitmap,
+    CalypsoElement* elem,
     bool* found) {
-    char bit_slice[bitmap->size + 1];
-    strncpy(bit_slice, binary_string, bitmap->size);
-    bit_slice[bitmap->size] = '\0';
+    // recursive function to get the offset of a subnode in a calypso binary string
+    if(elem->type == CALYPSO_ELEMENT_TYPE_FINAL) {
+        if(strcmp(elem->final->key, key) == 0) {
+            *found = true;
+            return 0;
+        }
+        return elem->final->size;
+    } else if(elem->type == CALYPSO_ELEMENT_TYPE_BITMAP) {
+        CalypsoBitmapElement* bitmap = elem->bitmap;
 
-    int count = 0;
-    int* positions = get_bit_positions(bit_slice, &count);
+        char bit_slice[bitmap->size + 1];
+        strncpy(bit_slice, binary_string, bitmap->size);
+        bit_slice[bitmap->size] = '\0';
 
-    int count_offset = bitmap->size;
-    for(int i = 0; i < count; i++) {
-        CalypsoElement element = bitmap->elements[positions[i]];
-        if(element.type == CALYPSO_ELEMENT_TYPE_FINAL) {
-            if(strcmp(element.final->key, key) == 0) {
+        int count = 0;
+        int* positions = get_bit_positions(bit_slice, &count);
+        bool f = false;
+
+        int count_offset = bitmap->size;
+        for(int i = 0; i < count; i++) {
+            CalypsoElement element = bitmap->elements[positions[i]];
+            count_offset +=
+                get_calypso_subnode_offset(binary_string + count_offset, key, &element, &f);
+            if(f) {
                 *found = true;
                 free(positions);
                 return count_offset;
             }
-            count_offset += element.final->size;
-        } else {
-            if(strcmp(element.bitmap->key, key) == 0) {
+        }
+
+        free(positions);
+        return count_offset;
+    } else if(elem->type == CALYPSO_ELEMENT_TYPE_CONTAINER) {
+        // same as bitmap but without bitmap at the beginning
+        CalypsoContainerElement* container = elem->container;
+
+        int count_offset = 0;
+        bool f = false;
+        for(int i = 0; i < container->size; i++) {
+            CalypsoElement element = container->elements[i];
+            count_offset +=
+                get_calypso_subnode_offset(binary_string + count_offset, key, &element, &f);
+            if(f) {
                 *found = true;
-                free(positions);
-                return count_offset;
-            }
-            count_offset += get_calypso_subnode_offset(
-                binary_string + count_offset, key, element.bitmap, found);
-            if(*found) {
-                free(positions);
                 return count_offset;
             }
         }
+
+        return count_offset;
     }
-    free(positions);
-    return count_offset;
+    return 0;
 }
 
 int get_calypso_node_offset(const char* binary_string, const char* key, CalypsoApp* structure) {
-    int count = 0;
-    bool found = false;
-    for(int i = 0; i < structure->elements_size; i++) {
-        if(structure->elements[i].type == CALYPSO_ELEMENT_TYPE_FINAL) {
-            if(strcmp(structure->elements[i].final->key, key) == 0) {
-                return count;
-            }
-            count += structure->elements[i].final->size;
-        } else {
-            if(strcmp(structure->elements[i].bitmap->key, key) == 0) {
-                return count;
-            }
-            int sub_binary_string_size = structure->elements[i].bitmap->size;
-            char bit_slice[sub_binary_string_size + 1];
-            strncpy(bit_slice, binary_string + count, sub_binary_string_size);
-            bit_slice[sub_binary_string_size] = '\0';
-            count += get_calypso_subnode_offset(
-                binary_string + count, key, structure->elements[i].bitmap, &found);
-            if(found) {
-                return count;
-            }
-        }
+    CalypsoElement* element = malloc(sizeof(CalypsoElement));
+    element->type = CALYPSO_ELEMENT_TYPE_CONTAINER;
+    element->container = structure->container;
+    bool found;
+    int offset = get_calypso_subnode_offset(binary_string, key, element, &found);
+    if(!found) {
+        FURI_LOG_E("Metroflip:Scene:Calypso", "Key %s not found in calypso structure", key);
     }
-    return 0;
+    free(element);
+    return offset;
 }
 
 int get_calypso_subnode_size(const char* key, CalypsoElement* element) {
@@ -214,7 +241,7 @@ int get_calypso_subnode_size(const char* key, CalypsoElement* element) {
         if(strcmp(element->final->key, key) == 0) {
             return element->final->size;
         }
-    } else {
+    } else if(element->type == CALYPSO_ELEMENT_TYPE_BITMAP) {
         if(strcmp(element->bitmap->key, key) == 0) {
             return element->bitmap->size;
         }
@@ -224,28 +251,127 @@ int get_calypso_subnode_size(const char* key, CalypsoElement* element) {
                 return size;
             }
         }
+    } else if(element->type == CALYPSO_ELEMENT_TYPE_CONTAINER) {
+        if(strcmp(element->container->key, key) == 0) {
+            return element->container->size;
+        }
+        for(int i = 0; i < element->container->size; i++) {
+            int size = get_calypso_subnode_size(key, &element->container->elements[i]);
+            if(size != 0) {
+                return size;
+            }
+        }
     }
     return 0;
 }
 
 int get_calypso_node_size(const char* key, CalypsoApp* structure) {
-    for(int i = 0; i < structure->elements_size; i++) {
-        if(structure->elements[i].type == CALYPSO_ELEMENT_TYPE_FINAL) {
-            if(strcmp(structure->elements[i].final->key, key) == 0) {
-                return structure->elements[i].final->size;
-            }
-        } else {
-            if(strcmp(structure->elements[i].bitmap->key, key) == 0) {
-                return structure->elements[i].bitmap->size;
-            }
-            for(int j = 0; j < structure->elements[i].bitmap->size; j++) {
-                int size =
-                    get_calypso_subnode_size(key, &structure->elements[i].bitmap->elements[j]);
-                if(size != 0) {
-                    return size;
-                }
-            }
+    CalypsoElement* element = malloc(sizeof(CalypsoElement));
+    element->type = CALYPSO_ELEMENT_TYPE_CONTAINER;
+    element->container = structure->container;
+    int count = get_calypso_subnode_size(key, element);
+    free(element);
+    return count;
+}
+
+CALYPSO_CARD_TYPE guess_card_type(int country_num, int network_num) {
+    switch(country_num) {
+    case 56:
+        switch(network_num) {
+        case 1:
+            return CALYPSO_CARD_MOBIB;
+        default:
+            return CALYPSO_CARD_UNKNOWN;
+        }
+    case 124:
+        switch(network_num) {
+        case 1:
+            return CALYPSO_CARD_OPUS;
+        default:
+            return CALYPSO_CARD_UNKNOWN;
         }
+    case 131:
+        return CALYPSO_CARD_VIVA;
+    case 250:
+        switch(network_num) {
+        case 0:
+            return CALYPSO_CARD_PASSPASS;
+        case 64:
+            return CALYPSO_CARD_TAM; // Montpellier
+        case 502:
+            return CALYPSO_CARD_OURA;
+        case 901:
+            return CALYPSO_CARD_NAVIGO;
+        case 908:
+            return CALYPSO_CARD_KORRIGO;
+        case 916:
+            return CALYPSO_CARD_TISSEO;
+        case 920:
+            return CALYPSO_CARD_ENVIBUS;
+        case 921:
+            return CALYPSO_CARD_GIRONDE;
+        default:
+            return CALYPSO_CARD_UNKNOWN;
+        }
+    case 376:
+        switch(network_num) {
+        case 2:
+            return CALYPSO_CARD_RAVKAV;
+        default:
+            return CALYPSO_CARD_UNKNOWN;
+        }
+    default:
+        return CALYPSO_CARD_UNKNOWN;
+    }
+}
+
+const char* get_country_string(int country_num) {
+    switch(country_num) {
+    case 56:
+        return "Belgium";
+    case 124:
+        return "Canada";
+    case 131:
+        return "Portugal";
+    case 250:
+        return "France";
+    case 376:
+        return "Israel";
+    default: {
+        char* country = malloc(4 * sizeof(char));
+        snprintf(country, 4, "%d", country_num);
+        return country;
+    }
+    }
+}
+
+const char* get_network_string(CALYPSO_CARD_TYPE card_type) {
+    switch(card_type) {
+    case CALYPSO_CARD_MOBIB:
+        return "Mobib";
+    case CALYPSO_CARD_OPUS:
+        return "Opus";
+    case CALYPSO_CARD_VIVA:
+        return "Viva";
+    case CALYPSO_CARD_PASSPASS:
+        return "PassPass";
+    case CALYPSO_CARD_TAM:
+        return "TAM";
+    case CALYPSO_CARD_OURA:
+        return "Oura";
+    case CALYPSO_CARD_NAVIGO:
+        return "IDFM";
+    case CALYPSO_CARD_KORRIGO:
+        return "KorriGo";
+    case CALYPSO_CARD_TISSEO:
+        return "Tisseo";
+    case CALYPSO_CARD_ENVIBUS:
+        return "Envibus";
+    case CALYPSO_CARD_GIRONDE:
+        return "Gironde";
+    case CALYPSO_CARD_RAVKAV:
+        return "Rav-Kav";
+    default:
+        return "Unknown";
     }
-    return 0;
 }

+ 24 - 2
api/calypso/calypso_util.h

@@ -1,10 +1,13 @@
 #include <stdbool.h>
+#include "calypso_i.h"
 
 #ifndef CALYPSO_UTIL_H
 #define CALYPSO_UTIL_H
 
 typedef enum {
     CALYPSO_APP_CONTRACT,
+    CALYPSO_APP_EVENT,
+    CALYPSO_APP_ENV_HOLDER,
 } CalypsoAppType;
 
 typedef enum {
@@ -20,21 +23,25 @@ typedef enum {
     CALYPSO_FINAL_TYPE_NETWORK_ID,
     CALYPSO_FINAL_TYPE_TRANSPORT_TYPE,
     CALYPSO_FINAL_TYPE_CARD_STATUS,
+    CALYPSO_FINAL_TYPE_STRING,
 } CalypsoFinalType;
 
 typedef enum {
+    CALYPSO_ELEMENT_TYPE_CONTAINER,
     CALYPSO_ELEMENT_TYPE_BITMAP,
     CALYPSO_ELEMENT_TYPE_FINAL
 } CalypsoElementType;
 
 typedef struct CalypsoFinalElement_t CalypsoFinalElement;
 typedef struct CalypsoBitmapElement_t CalypsoBitmapElement;
+typedef struct CalypsoContainerElement_t CalypsoContainerElement;
 
 typedef struct {
     CalypsoElementType type;
     union {
         CalypsoFinalElement* final;
         CalypsoBitmapElement* bitmap;
+        CalypsoContainerElement* container;
     };
 } CalypsoElement;
 
@@ -51,10 +58,15 @@ struct CalypsoBitmapElement_t {
     CalypsoElement* elements;
 };
 
+struct CalypsoContainerElement_t {
+    char key[36];
+    int size;
+    CalypsoElement* elements;
+};
+
 typedef struct {
     CalypsoAppType type;
-    CalypsoElement* elements;
-    int elements_size;
+    CalypsoContainerElement* container;
 } CalypsoApp;
 
 CalypsoElement make_calypso_final_element(
@@ -65,6 +77,8 @@ CalypsoElement make_calypso_final_element(
 
 CalypsoElement make_calypso_bitmap_element(const char* key, int size, CalypsoElement* elements);
 
+CalypsoElement make_calypso_container_element(const char* key, int size, CalypsoElement* elements);
+
 void free_calypso_structure(CalypsoApp* structure);
 
 int* get_bit_positions(const char* binary_string, int* count);
@@ -77,4 +91,12 @@ int get_calypso_node_offset(const char* binary_string, const char* key, CalypsoA
 
 int get_calypso_node_size(const char* key, CalypsoApp* structure);
 
+// Calypso known Card types
+
+CALYPSO_CARD_TYPE guess_card_type(int country_num, int network_num);
+
+const char* get_country_string(int country_num);
+
+const char* get_network_string(CALYPSO_CARD_TYPE card_type);
+
 #endif // CALYPSO_UTIL_H

+ 190 - 19
api/calypso/cards/navigo.c → api/calypso/cards/intercode.c

@@ -1,19 +1,21 @@
 #include <stdlib.h>
-#include "navigo.h"
+#include "intercode.h"
 
-CalypsoApp* get_navigo_contract_structure() {
-    CalypsoApp* NavigoContractStructure = malloc(sizeof(CalypsoApp));
-    if(!NavigoContractStructure) {
+CalypsoApp* get_intercode_contract_structure() {
+    CalypsoApp* IntercodeContractStructure = malloc(sizeof(CalypsoApp));
+    if(!IntercodeContractStructure) {
         return NULL;
     }
 
     int app_elements_count = 1;
 
-    NavigoContractStructure->type = CALYPSO_APP_CONTRACT;
-    NavigoContractStructure->elements = malloc(app_elements_count * sizeof(CalypsoElement));
-    NavigoContractStructure->elements_size = app_elements_count;
+    IntercodeContractStructure->type = CALYPSO_APP_CONTRACT;
+    IntercodeContractStructure->container = malloc(sizeof(CalypsoContainerElement));
+    IntercodeContractStructure->container->elements =
+        malloc(app_elements_count * sizeof(CalypsoElement));
+    IntercodeContractStructure->container->size = app_elements_count;
 
-    NavigoContractStructure->elements[0] = make_calypso_bitmap_element(
+    IntercodeContractStructure->container->elements[0] = make_calypso_bitmap_element(
         "Contract",
         20,
         (CalypsoElement[]){
@@ -243,28 +245,30 @@ CalypsoApp* get_navigo_contract_structure() {
                 "ContractData(0..255)", 0, "Données complémentaires", CALYPSO_FINAL_TYPE_UNKNOWN),
         });
 
-    return NavigoContractStructure;
+    return IntercodeContractStructure;
 }
 
-CalypsoApp* get_navigo_event_structure() {
-    CalypsoApp* NavigoEventStructure = malloc(sizeof(CalypsoApp));
-    if(!NavigoEventStructure) {
+CalypsoApp* get_intercode_event_structure() {
+    CalypsoApp* IntercodeEventStructure = malloc(sizeof(CalypsoApp));
+    if(!IntercodeEventStructure) {
         return NULL;
     }
 
     int app_elements_count = 3;
 
-    NavigoEventStructure->type = CALYPSO_APP_CONTRACT;
-    NavigoEventStructure->elements = malloc(app_elements_count * sizeof(CalypsoElement));
-    NavigoEventStructure->elements_size = app_elements_count;
+    IntercodeEventStructure->type = CALYPSO_APP_EVENT;
+    IntercodeEventStructure->container = malloc(sizeof(CalypsoContainerElement));
+    IntercodeEventStructure->container->elements =
+        malloc(app_elements_count * sizeof(CalypsoElement));
+    IntercodeEventStructure->container->size = app_elements_count;
 
-    NavigoEventStructure->elements[0] = make_calypso_final_element(
+    IntercodeEventStructure->container->elements[0] = make_calypso_final_element(
         "EventDateStamp", 14, "Date de l’événement", CALYPSO_FINAL_TYPE_DATE);
 
-    NavigoEventStructure->elements[1] = make_calypso_final_element(
+    IntercodeEventStructure->container->elements[1] = make_calypso_final_element(
         "EventTimeStamp", 11, "Heure de l’événement", CALYPSO_FINAL_TYPE_TIME);
 
-    NavigoEventStructure->elements[2] = make_calypso_bitmap_element(
+    IntercodeEventStructure->container->elements[2] = make_calypso_bitmap_element(
         "EventBitmap",
         28,
         (CalypsoElement[]){
@@ -375,5 +379,172 @@ CalypsoApp* get_navigo_event_structure() {
                 }),
         });
 
-    return NavigoEventStructure;
+    return IntercodeEventStructure;
+}
+
+CalypsoApp* get_intercode_env_holder_structure() {
+    CalypsoApp* IntercodeEnvHolderStructure = malloc(sizeof(CalypsoApp));
+    if(!IntercodeEnvHolderStructure) {
+        return NULL;
+    }
+
+    int app_elements_count = 3;
+
+    IntercodeEnvHolderStructure->type = CALYPSO_APP_ENV_HOLDER;
+    IntercodeEnvHolderStructure->container = malloc(sizeof(CalypsoContainerElement));
+    IntercodeEnvHolderStructure->container->elements =
+        malloc(app_elements_count * sizeof(CalypsoElement));
+    IntercodeEnvHolderStructure->container->size = app_elements_count;
+
+    IntercodeEnvHolderStructure->container->elements[0] = make_calypso_final_element(
+        "EnvApplicationVersionNumber",
+        6,
+        "Numéro de version de l’application Billettique",
+        CALYPSO_FINAL_TYPE_NUMBER);
+    IntercodeEnvHolderStructure->container->elements[1] = make_calypso_bitmap_element(
+        "Env",
+        7,
+        (CalypsoElement[]){
+            make_calypso_final_element(
+                "EnvNetworkId", 24, "Identification du réseau", CALYPSO_FINAL_TYPE_NUMBER),
+            make_calypso_final_element(
+                "EnvApplicationIssuerId",
+                8,
+                "Identification de l’émetteur de l’application",
+                CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EnvApplicationValidityEndDate",
+                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(
+                "EnvData",
+                2,
+                (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),
+                }),
+        });
+    IntercodeEnvHolderStructure->container->elements[2] = make_calypso_bitmap_element(
+        "Holder",
+        8,
+        (CalypsoElement[]){
+            make_calypso_bitmap_element(
+                "HolderName",
+                2,
+                (CalypsoElement[]){
+                    make_calypso_final_element(
+                        "HolderSurname", 85, "Nom du porteur", CALYPSO_FINAL_TYPE_STRING),
+                    make_calypso_final_element(
+                        "HolderForename",
+                        85,
+                        "Prénom de naissance du porteur",
+                        CALYPSO_FINAL_TYPE_STRING),
+                }),
+            make_calypso_bitmap_element(
+                "HolderBirth",
+                2,
+                (CalypsoElement[]){
+                    make_calypso_final_element(
+                        "HolderBirthDate", 32, "Date de naissance", CALYPSO_FINAL_TYPE_DATE),
+                    make_calypso_final_element(
+                        "HolderBirthPlace",
+                        115,
+                        "Lieu de naissance (23 caractères)",
+                        CALYPSO_FINAL_TYPE_STRING),
+                }),
+            make_calypso_final_element(
+                "HolderBirthName",
+                85,
+                "Nom de naissance du porteur (17 caractères)",
+                CALYPSO_FINAL_TYPE_STRING),
+            make_calypso_final_element(
+                "HolderIdNumber", 32, "Identifiant Porteur", CALYPSO_FINAL_TYPE_NUMBER),
+            make_calypso_final_element(
+                "HolderCountryAlpha", 24, "Pays du titulaire", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "HolderCompany", 32, "Société du titulaire", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_bitmap_element(
+                "HolderProfiles",
+                4,
+                (CalypsoElement[]){
+                    make_calypso_bitmap_element(
+                        "HolderProfileBitmap",
+                        3,
+                        (CalypsoElement[]){
+                            make_calypso_final_element(
+                                "HolderNetworkId", 24, "Réseau", CALYPSO_FINAL_TYPE_UNKNOWN),
+                            make_calypso_final_element(
+                                "HolderProfileNumber",
+                                8,
+                                "Numéro du statut",
+                                CALYPSO_FINAL_TYPE_NUMBER),
+                            make_calypso_final_element(
+                                "HolderProfileDate",
+                                14,
+                                "Date de fin de validité du statut",
+                                CALYPSO_FINAL_TYPE_DATE),
+                        }),
+                }),
+            make_calypso_bitmap_element(
+                "HolderData",
+                12,
+                (CalypsoElement[]){
+                    make_calypso_final_element(
+                        "HolderDataCardStatus", 4, "Type de carte", CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "HolderDataTeleReglement", 4, "Télérèglement", CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "HolderDataResidence", 17, "Ville du domicile", CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "HolderDataCommercialID", 6, "Produit carte", CALYPSO_FINAL_TYPE_NUMBER),
+                    make_calypso_final_element(
+                        "HolderDataWorkPlace", 17, "Lieu de travail", CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "HolderDataStudyPlace", 17, "Lieu d'étude", CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "HolderDataSaleDevice",
+                        16,
+                        "Numéro logique de SAM",
+                        CALYPSO_FINAL_TYPE_NUMBER),
+                    make_calypso_final_element(
+                        "HolderDataAuthenticator", 16, "Signature", CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "HolderDataProfileStartDate1",
+                        14,
+                        "Date de début de validité du statut",
+                        CALYPSO_FINAL_TYPE_DATE),
+                    make_calypso_final_element(
+                        "HolderDataProfileStartDate2",
+                        14,
+                        "Date de début de validité du statut",
+                        CALYPSO_FINAL_TYPE_DATE),
+                    make_calypso_final_element(
+                        "HolderDataProfileStartDate3",
+                        14,
+                        "Date de début de validité du statut",
+                        CALYPSO_FINAL_TYPE_DATE),
+                    make_calypso_final_element(
+                        "HolderDataProfileStartDate4",
+                        14,
+                        "Date de début de validité du statut",
+                        CALYPSO_FINAL_TYPE_DATE),
+                }),
+        });
+
+    return IntercodeEnvHolderStructure;
 }

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

@@ -0,0 +1,12 @@
+#include "../calypso_util.h"
+
+#ifndef INTERCODE_STRUCTURES_H
+#define INTERCODE_STRUCTURES_H
+
+CalypsoApp* get_intercode_contract_structure();
+
+CalypsoApp* get_intercode_event_structure();
+
+CalypsoApp* get_intercode_env_holder_structure();
+
+#endif // INTERCODE_STRUCTURES_H

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

@@ -1,10 +0,0 @@
-#include "../calypso_util.h"
-
-#ifndef NAVIGO_STRUCTURES_H
-#define NAVIGO_STRUCTURES_H
-
-CalypsoApp* get_navigo_contract_structure();
-
-CalypsoApp* get_navigo_event_structure();
-
-#endif // NAVIGO_STRUCTURES_H

+ 245 - 0
api/calypso/cards/opus.c

@@ -0,0 +1,245 @@
+#include <stdlib.h>
+#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) {
+        return NULL;
+    }
+
+    int app_elements_count = 2;
+
+    OpusContractStructure->type = CALYPSO_APP_CONTRACT;
+    OpusContractStructure->container = malloc(sizeof(CalypsoContainerElement));
+    OpusContractStructure->container->elements =
+        malloc(app_elements_count * sizeof(CalypsoElement));
+    OpusContractStructure->container->size = app_elements_count;
+
+    OpusContractStructure->container->elements[0] =
+        make_calypso_final_element("ContractUnknownA", 3, "Unknown A", CALYPSO_FINAL_TYPE_NUMBER);
+    OpusContractStructure->container->elements[1] = make_calypso_bitmap_element(
+        "Contract",
+        4,
+        (CalypsoElement[]){
+            make_calypso_final_element(
+                "ContractProvider", 8, "Provider", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element("ContractTariff", 16, "Tariff", CALYPSO_FINAL_TYPE_TARIFF),
+            make_calypso_bitmap_element(
+                "ContractDates",
+                2,
+                (CalypsoElement[]){
+                    make_calypso_final_element(
+                        "ContractStartDate", 14, "Start date", CALYPSO_FINAL_TYPE_DATE),
+                    make_calypso_final_element(
+                        "ContractEndDate", 14, "End date", CALYPSO_FINAL_TYPE_DATE),
+                }),
+            make_calypso_bitmap_element(
+                "ContractSaleData",
+                6,
+                (CalypsoElement[]){
+                    make_calypso_final_element(
+                        "ContractUnknownB", 17, "Unknown B", CALYPSO_FINAL_TYPE_NUMBER),
+                    make_calypso_final_element(
+                        "ContractSaleDate", 14, "Sale date", CALYPSO_FINAL_TYPE_DATE),
+                    make_calypso_final_element(
+                        "ContractSaleTime", 11, "Sale time", CALYPSO_FINAL_TYPE_TIME),
+                    make_calypso_final_element(
+                        "ContractUnknownC", 36, "Unknown C", CALYPSO_FINAL_TYPE_NUMBER),
+                    make_calypso_final_element(
+                        "ContractStatus", 8, "Status", CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "ContractUnknownD", 36, "Unknown D", CALYPSO_FINAL_TYPE_NUMBER),
+                }),
+        });
+
+    return OpusContractStructure;
+}
+
+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) {
+        return NULL;
+    }
+
+    int app_elements_count = 4;
+
+    OpusEventStructure->type = CALYPSO_APP_EVENT;
+    OpusEventStructure->container = malloc(sizeof(CalypsoContainerElement));
+    OpusEventStructure->container->elements = malloc(app_elements_count * sizeof(CalypsoElement));
+    OpusEventStructure->container->size = app_elements_count;
+
+    OpusEventStructure->container->elements[0] =
+        make_calypso_final_element("EventDate", 14, "Event date", CALYPSO_FINAL_TYPE_DATE);
+    OpusEventStructure->container->elements[1] =
+        make_calypso_final_element("EventTime", 11, "Event time", CALYPSO_FINAL_TYPE_TIME);
+    OpusEventStructure->container->elements[2] =
+        make_calypso_final_element("EventUnknownX", 19, "Unknown X", CALYPSO_FINAL_TYPE_NUMBER);
+    OpusEventStructure->container->elements[3] = make_calypso_bitmap_element(
+        "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(
+                "EventServiceProvider", 8, "Service provider", CALYPSO_FINAL_TYPE_SERVICE_PROVIDER),
+            make_calypso_final_element("EventUnknownC", 16, "Unknown C", CALYPSO_FINAL_TYPE_NUMBER),
+            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),
+            make_calypso_final_element(
+                "EventContractPointer", 5, "Contract pointer", CALYPSO_FINAL_TYPE_NUMBER),
+            make_calypso_bitmap_element(
+                "EventData",
+                7,
+                (CalypsoElement[]){
+                    make_calypso_final_element(
+                        "EventFirstStampDate", 14, "First stamp date", CALYPSO_FINAL_TYPE_DATE),
+                    make_calypso_final_element(
+                        "EventFirstStampTime", 11, "First stamp time", 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),
+                    make_calypso_final_element(
+                        "EventUnknownG", 4, "Unknown G", CALYPSO_FINAL_TYPE_NUMBER),
+                    make_calypso_final_element(
+                        "EventUnknownH", 4, "Unknown H", CALYPSO_FINAL_TYPE_NUMBER),
+                    make_calypso_final_element(
+                        "EventUnknownI", 4, "Unknown I", CALYPSO_FINAL_TYPE_NUMBER),
+                }),
+        });
+
+    return OpusEventStructure;
+}
+
+CalypsoApp* get_opus_env_holder_structure() {
+    CalypsoApp* OpusEnvHolderStructure = malloc(sizeof(CalypsoApp));
+    if(!OpusEnvHolderStructure) {
+        return NULL;
+    }
+
+    int app_elements_count = 3;
+
+    OpusEnvHolderStructure->type = CALYPSO_APP_ENV_HOLDER;
+    OpusEnvHolderStructure->container = malloc(sizeof(CalypsoContainerElement));
+    OpusEnvHolderStructure->container->elements =
+        malloc(app_elements_count * sizeof(CalypsoElement));
+    OpusEnvHolderStructure->container->size = app_elements_count;
+
+    OpusEnvHolderStructure->container->elements[0] = make_calypso_final_element(
+        "EnvApplicationVersionNumber",
+        6,
+        "Numéro de version de l’application Billettique",
+        CALYPSO_FINAL_TYPE_NUMBER);
+    OpusEnvHolderStructure->container->elements[1] = make_calypso_bitmap_element(
+        "Env",
+        7,
+        (CalypsoElement[]){
+            make_calypso_final_element(
+                "EnvNetworkId", 24, "Identification du réseau", CALYPSO_FINAL_TYPE_NUMBER),
+            make_calypso_final_element(
+                "EnvApplicationIssuerId",
+                8,
+                "Identification de l’émetteur de l’application",
+                CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EnvApplicationValidityEndDate",
+                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(
+                "EnvData",
+                2,
+                (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),
+                }),
+        });
+
+    OpusEnvHolderStructure->container->elements[2] = make_calypso_bitmap_element(
+        "Holder",
+        2,
+        (CalypsoElement[]){
+            make_calypso_container_element(
+                "HolderData",
+                5,
+                (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),
+                    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),
+                }),
+            make_calypso_final_element("HolderUnknownD", 8, "Unknown D", CALYPSO_FINAL_TYPE_NUMBER),
+        });
+
+    return OpusEnvHolderStructure;
+}

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

@@ -0,0 +1,12 @@
+#include "../calypso_util.h"
+
+#ifndef OPUS_STRUCTURES_H
+#define OPUS_STRUCTURES_H
+
+CalypsoApp* get_opus_contract_structure();
+
+CalypsoApp* get_opus_event_structure();
+
+CalypsoApp* get_opus_env_holder_structure();
+
+#endif // OPUS_STRUCTURES_H

+ 513 - 0
api/calypso/transit/navigo.c

@@ -0,0 +1,513 @@
+#include "navigo.h"
+#include "navigo_lists.h"
+#include "../../../metroflip_i.h"
+
+const char* get_navigo_transport_type(int type) {
+    switch(type) {
+    case BUS_URBAIN:
+        return "Bus Urbain";
+    case BUS_INTERURBAIN:
+        return "Bus Interurbain";
+    case METRO:
+        return "Metro";
+    case TRAM:
+        return "Tram";
+    case TRAIN:
+        return "Train";
+    case PARKING:
+        return "Parking";
+    default:
+        return "Unknown";
+    }
+}
+
+const char* get_navigo_service_provider(int provider) {
+    switch(provider) {
+    case 2:
+        return "SNCF";
+    case 3:
+        return "RATP";
+    case 4:
+        return "IDF Mobilites";
+    case 10:
+        return "IDF Mobilites";
+    case 115:
+        return "CSO (VEOLIA)";
+    case 116:
+        return "R'Bus (VEOLIA)";
+    case 156:
+        return "Phebus";
+    case 175:
+        return "RATP (Veolia Transport Nanterre)";
+    default:
+        return "Unknown";
+    }
+}
+
+const char* get_transition_type(int transition) {
+    switch(transition) {
+    case 1:
+        return "Entry";
+    case 2:
+        return "Exit";
+    case 4:
+        return "Controle volant (a bord)";
+    case 5:
+        return "Test validation";
+    case 6:
+        return "Interchange - Entry";
+    case 7:
+        return "Interchange - Exit";
+    case 9:
+        return "Validation cancelled";
+    case 10:
+        return "Entry";
+    case 11:
+        return "Exit";
+    case 13:
+        return "Distribution";
+    case 15:
+        return "Invalidation";
+    default: {
+        char* transition_str = malloc(6 * sizeof(char));
+        snprintf(transition_str, 6, "%d", transition);
+        return transition_str;
+    }
+    }
+}
+
+const char* get_navigo_type(int type) {
+    switch(type) {
+    case NAVIGO_EASY:
+        return "Navigo Easy";
+    case NAVIGO_DECOUVERTE:
+        return "Navigo Decouverte";
+    case NAVIGO_STANDARD:
+        return "Navigo Standard";
+    case NAVIGO_INTEGRAL:
+        return "Navigo Integral";
+    case IMAGINE_R:
+        return "Imagine R";
+    default:
+        return "Navigo";
+    }
+}
+
+const char* get_navigo_tariff(int tariff) {
+    switch(tariff) {
+    case 0x0000:
+        return "Navigo Mois";
+    case 0x0001:
+        return "Navigo Semaine";
+    case 0x0002:
+        return "Navigo Annuel";
+    case 0x0003:
+        return "Navigo Jour";
+    case 0x0004:
+        return "Imagine R Junior";
+    case 0x0005:
+        return "Imagine R Etudiant";
+    case 0x000D:
+        return "Navigo Jeunes Week-end";
+    case 0x0015:
+        return "Paris-Visite"; // Theoric
+    case 0x1000:
+        return "Navigo Liberte+";
+    case 0x4000:
+        return "Navigo Mois 75%%";
+    case 0x4001:
+        return "Navigo Semaine 75%%";
+    case 0x4015:
+        return "Paris-Visite (Enfant)"; // Theoric
+    case 0x5000:
+        return "Tickets T+";
+    case 0x5004:
+        return "Tickets OrlyBus"; // Theoric
+    case 0x5005:
+        return "Tickets RoissyBus"; // Theoric
+    case 0x5006:
+        return "Bus-Tram"; // Theoric
+    case 0x5008:
+        return "Metro-Train-RER"; // Theoric
+    case 0x500b:
+        return "Paris <> Aeroports"; // Theoric
+    case 0x5010:
+        return "Tickets T+ (Reduit)"; // Theoric
+    case 0x5016:
+        return "Bus-Tram (Reduit)"; // Theoric
+    case 0x5018:
+        return "Metro-Train-RER (Reduit)"; // Theoric
+    case 0x501b:
+        return "Paris <> Aeroports (Reduit)"; // Theoric
+    case 0x8003:
+        return "Navigo Solidarite Gratuit";
+    default: {
+        char* tariff_str = malloc(6 * sizeof(char));
+        snprintf(tariff_str, 6, "%d", tariff);
+        return tariff_str;
+    }
+    }
+}
+
+bool is_ticket_count_available(int tariff) {
+    return tariff >= 0x5000 && tariff <= 0x501b;
+}
+
+const char* get_pay_method(int pay_method) {
+    switch(pay_method) {
+    case 0x30:
+        return "Apple Pay";
+    case 0x80:
+        return "Debit PME";
+    case 0x90:
+        return "Cash";
+    case 0xA0:
+        return "Mobility Check";
+    case 0xB3:
+        return "Payment Card";
+    case 0xA4:
+        return "Check";
+    case 0xA5:
+        return "Vacation Check";
+    case 0xB7:
+        return "Telepayment";
+    case 0xD0:
+        return "Remote Payment";
+    case 0xD7:
+        return "Voucher, Prepayment, Exchange Voucher, Travel Voucher";
+    case 0xD9:
+        return "Discount Voucher";
+    default:
+        return "Unknown";
+    }
+}
+
+const char* get_zones(int* zones) {
+    if(zones[0] && zones[4]) {
+        return "All Zones (1-5)";
+    } else if(zones[0] && zones[3]) {
+        return "Zones 1-4";
+    } else if(zones[0] && zones[2]) {
+        return "Zones 1-3";
+    } else if(zones[0] && zones[1]) {
+        return "Zones 1-2";
+    } else if(zones[0]) {
+        return "Zone 1";
+    } else if(zones[1] && zones[4]) {
+        return "Zones 2-5";
+    } else if(zones[1] && zones[3]) {
+        return "Zones 2-4";
+    } else if(zones[1] && zones[2]) {
+        return "Zones 2-3";
+    } else if(zones[1]) {
+        return "Zone 2";
+    } else if(zones[2] && zones[4]) {
+        return "Zones 3-5";
+    } else if(zones[2] && zones[3]) {
+        return "Zones 3-4";
+    } else if(zones[2]) {
+        return "Zone 3";
+    } else if(zones[3] && zones[4]) {
+        return "Zones 4-5";
+    } else if(zones[3]) {
+        return "Zone 4";
+    } else if(zones[4]) {
+        return "Zone 5";
+    } else {
+        return "Unknown";
+    }
+}
+
+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;
+}
+
+const char* get_navigo_metro_station(int station_group_id, int station_id) {
+    // Use NAVIGO_H constants
+    if(station_group_id < 32 && station_id < 16) {
+        return NAVIGO_METRO_STATION_LIST[station_group_id][station_id];
+    }
+    // cast station_group_id-station_id to a string
+    char* station = malloc(12 * sizeof(char));
+    if(!station) {
+        return "Unknown";
+    }
+    snprintf(station, 10, "%d-%d", station_group_id, station_id);
+    return station;
+}
+
+const char* get_navigo_train_line(int station_group_id) {
+    if(station_group_id < 77) {
+        return NAVIGO_TRAIN_LINES_LIST[station_group_id];
+    }
+    return "Unknown";
+}
+
+const char* get_navigo_train_station(int station_group_id, int station_id) {
+    if(station_group_id < 77 && station_id < 19) {
+        return NAVIGO_TRAIN_STATION_LIST[station_group_id][station_id];
+    }
+    // cast station_group_id-station_id to a string
+    char* station = malloc(12 * sizeof(char));
+    if(!station) {
+        return "Unknown";
+    }
+    snprintf(station, 10, "%d-%d", station_group_id, station_id);
+    return station;
+}
+
+const char* get_navigo_tram_line(int route_number) {
+    switch(route_number) {
+    case 16:
+        return "T6";
+    default: {
+        char* line = malloc(3 * sizeof(char));
+        if(!line) {
+            return "Unknown";
+        }
+        snprintf(line, 3, "T%d", route_number);
+        return line;
+    }
+    }
+}
+
+void show_navigo_event_info(
+    NavigoCardEvent* event,
+    NavigoCardContract* contracts,
+    FuriString* parsed_data) {
+    if(event->used_contract == 0) {
+        furi_string_cat_printf(parsed_data, "No event data\n");
+        return;
+    }
+    if(event->transport_type == BUS_URBAIN || event->transport_type == BUS_INTERURBAIN ||
+       event->transport_type == METRO || event->transport_type == TRAM) {
+        if(event->route_number_available) {
+            if(event->transport_type == METRO && event->route_number == 103) {
+                furi_string_cat_printf(
+                    parsed_data,
+                    "%s 3 bis\n%s\n",
+                    get_navigo_transport_type(event->transport_type),
+                    get_transition_type(event->transition));
+            } else if(event->transport_type == TRAM) {
+                furi_string_cat_printf(
+                    parsed_data,
+                    "%s %s\n%s\n",
+                    get_navigo_transport_type(event->transport_type),
+                    get_navigo_tram_line(event->route_number),
+                    get_transition_type(event->transition));
+            } else {
+                furi_string_cat_printf(
+                    parsed_data,
+                    "%s %d\n%s\n",
+                    get_navigo_transport_type(event->transport_type),
+                    event->route_number,
+                    get_transition_type(event->transition));
+            }
+        } else {
+            furi_string_cat_printf(
+                parsed_data,
+                "%s\n%s\n",
+                get_navigo_transport_type(event->transport_type),
+                get_transition_type(event->transition));
+        }
+        furi_string_cat_printf(
+            parsed_data,
+            "Transporter: %s\n",
+            get_navigo_service_provider(event->service_provider));
+        if(event->transport_type == METRO) {
+            furi_string_cat_printf(
+                parsed_data,
+                "Station: %s\nSector: %s\n",
+                get_navigo_metro_station(event->station_group_id, event->station_id),
+                get_navigo_metro_station(event->station_group_id, 0));
+        } else {
+            furi_string_cat_printf(
+                parsed_data, "Station ID: %d-%d\n", event->station_group_id, event->station_id);
+        }
+        if(event->location_gate_available) {
+            furi_string_cat_printf(parsed_data, "Gate: %d\n", event->location_gate);
+        }
+        if(event->device_available) {
+            if(event->transport_type == BUS_URBAIN || event->transport_type == BUS_INTERURBAIN) {
+                const char* side = event->side == 0 ? "right" : "left";
+                furi_string_cat_printf(parsed_data, "Door: %d\nSide: %s\n", event->door, side);
+            } else {
+                furi_string_cat_printf(parsed_data, "Device: %d\n", event->device);
+            }
+        }
+        if(event->mission_available) {
+            furi_string_cat_printf(parsed_data, "Mission: %d\n", event->mission);
+        }
+        if(event->vehicle_id_available) {
+            furi_string_cat_printf(parsed_data, "Vehicle: %d\n", event->vehicle_id);
+        }
+        if(event->used_contract_available) {
+            furi_string_cat_printf(
+                parsed_data,
+                "Contract: %d - %s\n",
+                event->used_contract,
+                get_navigo_tariff(contracts[event->used_contract - 1].tariff));
+        }
+        locale_format_datetime_cat(parsed_data, &event->date, true);
+        furi_string_cat_printf(parsed_data, "\n");
+    } else if(event->transport_type == TRAIN) {
+        if(event->route_number_available) {
+            furi_string_cat_printf(
+                parsed_data,
+                "RER %c\n%s\n",
+                (65 + event->route_number - 17),
+                get_transition_type(event->transition));
+        } else {
+            furi_string_cat_printf(
+                parsed_data,
+                "%s %s\n%s\n",
+                get_navigo_transport_type(event->transport_type),
+                get_navigo_train_line(event->station_group_id),
+                get_transition_type(event->transition));
+        }
+        furi_string_cat_printf(
+            parsed_data,
+            "Transporter: %s\n",
+            get_navigo_service_provider(event->service_provider));
+        furi_string_cat_printf(
+            parsed_data,
+            "Station: %s\n",
+            get_navigo_train_station(event->station_group_id, event->station_id));
+        /* if(event->route_number_available) {
+            furi_string_cat_printf(parsed_data, "Route: %d\n", event->route_number);
+        } */
+        if(event->location_gate_available) {
+            furi_string_cat_printf(parsed_data, "Gate: %d\n", event->location_gate);
+        }
+        if(event->device_available) {
+            if(event->service_provider == 2) {
+                furi_string_cat_printf(parsed_data, "Device: %d\n", event->device & 0xFF);
+            } else {
+                furi_string_cat_printf(parsed_data, "Device: %d\n", event->device);
+            }
+        }
+        if(event->mission_available) {
+            furi_string_cat_printf(parsed_data, "Mission: %d\n", event->mission);
+        }
+        if(event->vehicle_id_available) {
+            furi_string_cat_printf(parsed_data, "Vehicle: %d\n", event->vehicle_id);
+        }
+        if(event->used_contract_available) {
+            furi_string_cat_printf(
+                parsed_data,
+                "Contract: %d - %s\n",
+                event->used_contract,
+                get_navigo_tariff(contracts[event->used_contract - 1].tariff));
+        }
+        locale_format_datetime_cat(parsed_data, &event->date, true);
+        furi_string_cat_printf(parsed_data, "\n");
+    } else {
+        furi_string_cat_printf(
+            parsed_data,
+            "%s - %s\n",
+            get_navigo_transport_type(event->transport_type),
+            get_transition_type(event->transition));
+        furi_string_cat_printf(
+            parsed_data,
+            "Transporter: %s\n",
+            get_navigo_service_provider(event->service_provider));
+        furi_string_cat_printf(
+            parsed_data, "Station ID: %d-%d\n", event->station_group_id, event->station_id);
+        if(event->location_gate_available) {
+            furi_string_cat_printf(parsed_data, "Gate: %d\n", event->location_gate);
+        }
+        if(event->device_available) {
+            furi_string_cat_printf(parsed_data, "Device: %d\n", event->device);
+        }
+        if(event->mission_available) {
+            furi_string_cat_printf(parsed_data, "Mission: %d\n", event->mission);
+        }
+        if(event->vehicle_id_available) {
+            furi_string_cat_printf(parsed_data, "Vehicle: %d\n", event->vehicle_id);
+        }
+        if(event->used_contract_available) {
+            furi_string_cat_printf(
+                parsed_data,
+                "Contract: %d - %s\n",
+                event->used_contract,
+                get_navigo_tariff(contracts[event->used_contract - 1].tariff));
+        }
+        locale_format_datetime_cat(parsed_data, &event->date, true);
+        furi_string_cat_printf(parsed_data, "\n");
+    }
+}
+
+void show_navigo_contract_info(NavigoCardContract* contract, FuriString* parsed_data) {
+    furi_string_cat_printf(parsed_data, "Type: %s\n", get_navigo_tariff(contract->tariff));
+    if(is_ticket_count_available(contract->tariff)) {
+        furi_string_cat_printf(parsed_data, "Remaining Tickets: %d\n", contract->counter.count);
+    }
+    if(contract->serial_number_available) {
+        furi_string_cat_printf(parsed_data, "TCN Number: %d\n", contract->serial_number);
+    }
+    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");
+    }
+    if(contract->zones_available) {
+        furi_string_cat_printf(parsed_data, "%s\n", get_zones(contract->zones));
+    }
+    furi_string_cat_printf(parsed_data, "Sold on: ");
+    locale_format_datetime_cat(parsed_data, &contract->sale_date, false);
+    furi_string_cat_printf(parsed_data, "\n");
+    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);
+    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, "Authenticity Code: %d\n", contract->authenticator);
+}
+
+void show_navigo_environment_info(NavigoCardEnv* environment, FuriString* parsed_data) {
+    furi_string_cat_printf(
+        parsed_data,
+        "App Version: %s - v%d\n",
+        get_intercode_version(environment->app_version),
+        get_intercode_subversion(environment->app_version));
+    furi_string_cat_printf(
+        parsed_data, "Country: %s\n", get_country_string(environment->country_num));
+    furi_string_cat_printf(
+        parsed_data,
+        "Network: %s\n",
+        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, "\n");
+}

+ 47 - 0
api/calypso/transit/navigo.h

@@ -0,0 +1,47 @@
+#include "../calypso_util.h"
+#include "../cards/intercode.h"
+#include "navigo_i.h"
+#include <datetime.h>
+#include <stdbool.h>
+#include <furi.h>
+
+#ifndef NAVIGO_H
+#define NAVIGO_H
+
+const char* get_navigo_type(int type);
+
+const char* get_navigo_metro_station(int station_group_id, int station_id);
+
+const char* get_navigo_train_line(int station_group_id);
+
+const char* get_navigo_train_station(int station_group_id, int station_id);
+
+const char* get_navigo_tram_line(int route_number);
+
+void show_navigo_event_info(
+    NavigoCardEvent* event,
+    NavigoCardContract* contracts,
+    FuriString* parsed_data);
+
+void show_navigo_contract_info(NavigoCardContract* contract, FuriString* parsed_data);
+
+void show_navigo_environment_info(NavigoCardEnv* environment, FuriString* parsed_data);
+
+typedef enum {
+    BUS_URBAIN = 1,
+    BUS_INTERURBAIN = 2,
+    METRO = 3,
+    TRAM = 4,
+    TRAIN = 5,
+    PARKING = 8
+} NAVIGO_TRANSPORT_TYPE;
+
+typedef enum {
+    NAVIGO_EASY = 0,
+    NAVIGO_DECOUVERTE = 1,
+    NAVIGO_STANDARD = 2,
+    NAVIGO_INTEGRAL = 6,
+    IMAGINE_R = 14
+} NAVIGO_CARD_STATUS;
+
+#endif // NAVIGO_H

+ 4 - 8
scenes/navigo_structs.h → api/calypso/transit/navigo_i.h

@@ -1,6 +1,8 @@
 #include <datetime.h>
 #include <stdbool.h>
-#include <furi.h>
+
+#ifndef NAVIGO_I_H
+#define NAVIGO_I_H
 
 typedef struct {
     int transport_type;
@@ -70,12 +72,6 @@ typedef struct {
     NavigoCardHolder holder;
     NavigoCardContract contracts[4];
     NavigoCardEvent events[3];
-    unsigned int card_number;
 } NavigoCardData;
 
-typedef struct {
-    NavigoCardData* card;
-    int page_id;
-    // mutex
-    FuriMutex* mutex;
-} NavigoContext;
+#endif // NAVIGO_I_H

+ 6 - 69
scenes/navigo.h → api/calypso/transit/navigo_lists.h

@@ -1,70 +1,7 @@
-#include <gui/gui.h>
-#include <gui/modules/widget_elements/widget_element.h>
-#include "../api/calypso/calypso_util.h"
-#include "../api/calypso/cards/navigo.h"
-#include <datetime.h>
-#include <stdbool.h>
+#ifndef NAVIGO_LISTS_H
+#define NAVIGO_LISTS_H
 
-#ifndef METRO_LIST_H
-#define METRO_LIST_H
-
-#ifndef NAVIGO_H
-#define NAVIGO_H
-
-void metroflip_back_button_widget_callback(GuiButtonType result, InputType type, void* context);
-void metroflip_next_button_widget_callback(GuiButtonType result, InputType type, void* context);
-
-// Service Providers
-static const char* SERVICE_PROVIDERS[] = {
-    [2] = "SNCF",
-    [3] = "RATP",
-    [115] = "CSO (VEOLIA)",
-    [116] = "R'Bus (VEOLIA)",
-    [156] = "Phebus",
-    [175] = "RATP (Veolia Transport Nanterre)"};
-
-// Transport Types
-static const char* TRANSPORT_LIST[] = {
-    [1] = "Bus Urbain",
-    [2] = "Bus Interurbain",
-    [3] = "Metro",
-    [4] = "Tram",
-    [5] = "Train",
-    [8] = "Parking"};
-
-typedef enum {
-    BUS_URBAIN = 1,
-    BUS_INTERURBAIN = 2,
-    METRO = 3,
-    TRAM = 4,
-    TRAIN = 5,
-    PARKING = 8
-} TRANSPORT_TYPE;
-
-typedef enum {
-    NAVIGO_EASY = 0,
-    NAVIGO_DECOUVERTE = 1,
-    NAVIGO_STANDARD = 2,
-    NAVIGO_INTEGRAL = 6,
-    IMAGINE_R = 14
-} CARD_STATUS;
-
-// Transition Types
-static const char* TRANSITION_LIST[] = {
-    [1] = "Validation en entree",
-    [2] = "Validation en sortie",
-    [4] = "Controle volant (a bord)",
-    [5] = "Validation de test",
-    [6] = "Validation en correspondance (entree)",
-    [7] = "Validation en correspondance (sortie)",
-    [9] = "Annulation de validation",
-    [10] = "Validation en entree",
-    [13] = "Distribution",
-    [15] = "Invalidation"};
-
-#endif // NAVIGO_H
-
-static const char* METRO_STATION_LIST[32][16] =
+static const char* NAVIGO_METRO_STATION_LIST[32][16] =
     {[1] =
          {[0] = "Cite",
           [1] = "Saint-Michel",
@@ -435,7 +372,7 @@ static const char* METRO_STATION_LIST[32][16] =
          [13] = "Place de Clichy",
          [14] = "La Fourche"}};
 
-static const char* TRAIN_LINES_LIST[77] = {
+static const char* NAVIGO_TRAIN_LINES_LIST[77] = {
     [1] = "RER B",         [3] = "RER B",         [6] = "RER A",         [14] = "RER B",
     [15] = "RER B",        [16] = "RER A",        [17] = "RER A",        [18] = "RER B",
     [20] = "Transilien P", [21] = "Transilien P", [22] = "T4",           [23] = "Transilien P",
@@ -448,7 +385,7 @@ static const char* TRAIN_LINES_LIST[77] = {
     [64] = "RER C",        [65] = "Transilien V", [70] = "RER B",        [72] = "Transilien J",
     [73] = "Transilien J", [75] = "RER C",        [76] = "RER C"};
 
-static const char* TRAIN_STATION_LIST[77][19] = {
+static const char* NAVIGO_TRAIN_STATION_LIST[77][19] = {
     [1] = {[0] = "Châtelet-Les Halles", [1] = "Châtelet-Les Halles", [7] = "Luxembourg"},
     [3] = {[0] = "Saint-Michel Notre-Dame"},
     [6] = {[0] = "Auber", [6] = "Auber"},
@@ -729,4 +666,4 @@ static const char* TRAIN_STATION_LIST[77][19] = {
          [15] = "Dourdan | Dourdan-la-Foret"},
 };
 
-#endif // METRO_LIST_H
+#endif

+ 125 - 0
api/calypso/transit/opus.c

@@ -0,0 +1,125 @@
+#include "opus.h"
+#include "opus_lists.h"
+#include "../../../metroflip_i.h"
+
+const char* get_opus_service_provider(int provider) {
+    switch(provider) {
+    case 0x01:
+        return "STM";
+    case 0x02:
+        return "STM";
+    case 0x03:
+        return "RTL";
+    case 0x04:
+        return "RTM";
+    case 0x05:
+        return "RTC";
+    case 0x06:
+        return "STL";
+    case 0x10:
+        return "STLevis";
+    default: {
+        char* provider_str = malloc(10 * sizeof(char));
+        if(!provider_str) {
+            return "Unknown";
+        }
+        snprintf(provider_str, 10, "0x%02X", provider);
+        return provider_str;
+    }
+    }
+}
+
+const char* get_opus_transport_type(int route_number) {
+    if(route_number >= 0x01 && route_number <= 0x04) {
+        return "Metro";
+    } else {
+        return "Bus";
+    }
+}
+
+const char* get_opus_transport_line(int route_number) {
+    if(OPUS_LINES_LIST[route_number]) {
+        return OPUS_LINES_LIST[route_number];
+    } else {
+        char* line = malloc(4 * sizeof(char));
+        if(!line) {
+            return "Unknown";
+        }
+        snprintf(line, 4, "%d", route_number);
+        return line;
+    }
+}
+
+const char* get_opus_tariff(int tariff) {
+    switch(tariff) {
+    case 0xb1:
+        return "Monthly";
+    case 0xb2:
+    case 0xc9:
+        return "Weekly";
+    case 0x1c7:
+        return "Single Trips";
+    case 0xa34:
+        return "Monthly Student";
+    case 0xa3e:
+        return "Weekly";
+    default: {
+        char* tariff_str = malloc(9 * sizeof(char));
+        if(!tariff_str) {
+            return "Unknown";
+        }
+        snprintf(tariff_str, 9, "0x%02X", tariff);
+        return tariff_str;
+    }
+    }
+}
+
+void show_opus_event_info(
+    OpusCardEvent* event,
+    OpusCardContract* contracts,
+    FuriString* parsed_data) {
+    UNUSED(contracts);
+    furi_string_cat_printf(
+        parsed_data,
+        "%s %s\n",
+        get_opus_transport_type(event->route_number),
+        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));
+    }
+    locale_format_datetime_cat(parsed_data, &event->date, true);
+    furi_string_cat_printf(parsed_data, "\n");
+}
+
+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: ");
+    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");
+    // furi_string_cat_printf(parsed_data, "Sold on: ");
+    // locale_format_datetime_cat(parsed_data, &contract->sale_date, false);
+    // furi_string_cat_printf(parsed_data, "\nStatus: %d\n", contract->status);
+}
+
+void show_opus_environment_info(OpusCardEnv* environment, 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));
+    furi_string_cat_printf(
+        parsed_data,
+        "Network: %s\n",
+        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, "\n");
+}

+ 17 - 0
api/calypso/transit/opus.h

@@ -0,0 +1,17 @@
+#include "../calypso_util.h"
+#include "../cards/opus.h"
+#include "opus_i.h"
+
+#ifndef OPUS_H
+#define OPUS_H
+
+void show_opus_event_info(
+    OpusCardEvent* event,
+    OpusCardContract* contracts,
+    FuriString* parsed_data);
+
+void show_opus_contract_info(OpusCardContract* contract, FuriString* parsed_data);
+
+void show_opus_environment_info(OpusCardEnv* environment, FuriString* parsed_data);
+
+#endif // OPUS_H

+ 45 - 0
api/calypso/transit/opus_i.h

@@ -0,0 +1,45 @@
+#include <datetime.h>
+#include <stdbool.h>
+
+#ifndef OPUS_I_H
+#define OPUS_I_H
+
+typedef struct {
+    int service_provider;
+    int route_number;
+    bool route_number_available;
+    int used_contract;
+    bool used_contract_available;
+    DateTime date;
+} OpusCardEvent;
+
+typedef struct {
+    int app_version;
+    int country_num;
+    int network_num;
+    DateTime end_dt;
+} OpusCardEnv;
+
+typedef struct {
+    int card_status;
+    int commercial_id;
+} OpusCardHolder;
+
+typedef struct {
+    int provider;
+    int tariff;
+    DateTime start_date;
+    DateTime end_date;
+    DateTime sale_date;
+    int status;
+    bool present;
+} OpusCardContract;
+
+typedef struct {
+    OpusCardEnv environment;
+    OpusCardHolder holder;
+    OpusCardContract contracts[4];
+    OpusCardEvent events[3];
+} OpusCardData;
+
+#endif // OPUS_I_H

+ 241 - 0
api/calypso/transit/opus_lists.h

@@ -0,0 +1,241 @@
+/*
+<bus id="2" logo="stm_orange" name="METRO Ligne orange" />
+    <bus id="1" logo="stm_vert" name="METRO Ligne verte" />
+    <bus id="3" logo="stm_jaune" name="METRO Ligne jaune" />
+    <bus id="4" logo="stm_bleue" name="METRO Ligne bleue" />
+    <bus id="" logo="stm_local" name="10" />
+    <bus id="" logo="stm_local" name="11" />
+    <bus id="" logo="stm_local" name="12" />
+    <bus id="" logo="stm_local" name="13" />
+    <bus id="" logo="stm_local" name="14" />
+    <bus id="" logo="stm_local" name="15" />
+    <bus id="" logo="stm_local" name="16" />
+    <bus id="" logo="stm_local" name="17" />
+    <bus id="13" logo="stm_local" name="18" />
+    <bus id="" logo="stm_local" name="19" />
+    <bus id="" logo="stm_local" name="21" />
+    <bus id="" logo="stm_local" name="22" />
+    <bus id="15" logo="stm_local" name="24" />
+    <bus id="16" logo="stm_local" name="25" />
+    <bus id="231" logo="stm_local" name="26" />
+    <bus id="17" logo="stm_local" name="27" />
+    <bus id="18" logo="stm_local" name="28" />
+    <bus id="" logo="stm_local" name="29" />
+    <bus id="" logo="stm_local" name="30" />
+    <bus id="" logo="stm_local" name="31" />
+    <bus id="22" logo="stm_local" name="32" />
+    <bus id="23" logo="stm_local" name="33" />
+    <bus id="24" logo="stm_local" name="34" />
+    <bus id="" logo="stm_local" name="36" />
+    <bus id="26" logo="stm_local" name="37" />
+    <bus id="" logo="stm_local" name="39" />
+    <bus id="" logo="stm_local" name="40" />
+    <bus id="226" logo="stm_local" name="41" />
+    <bus id="29" logo="stm_local" name="43" />
+    <bus id="30" logo="stm_local" name="44" />
+    <bus id="31" logo="stm_local" name="45" />
+    <bus id="" logo="stm_local" name="46" />
+    <bus id="33" logo="stm_local" name="47" />
+    <bus id="34" logo="stm_local" name="48" />
+    <bus id="35" logo="stm_local" name="49" />
+    <bus id="36" logo="stm_local" name="51" />
+    <bus id="" logo="stm_local" name="52" />
+    <bus id="" logo="stm_local" name="53" />
+    <bus id="" logo="stm_local" name="54" />
+    <bus id="40" logo="stm_local" name="55" />
+    <bus id="" logo="stm_local" name="56" />
+    <bus id="" logo="stm_local" name="57" />
+    <bus id="" logo="stm_local" name="58" />
+    <bus id="44" logo="stm_local" name="61" />
+    <bus id="" logo="stm_local" name="63" />
+    <bus id="46" logo="stm_local" name="64" />
+    <bus id="" logo="stm_local" name="66" />
+    <bus id="48" logo="stm_local" name="67" />
+    <bus id="49" logo="stm_local" name="68" />
+    <bus id="50" logo="stm_local" name="69" />
+    <bus id="51" logo="stm_local" name="70" />
+    <bus id="" logo="stm_local" name="71" />
+    <bus id="" logo="stm_local" name="72" />
+    <bus id="" logo="stm_local" name="73" />
+    <bus id="" logo="stm_local" name="74" />
+    <bus id="" logo="stm_local" name="75" />
+    <bus id="" logo="stm_local" name="76" />
+    <bus id="" logo="stm_local" name="77" />
+    <bus id="" logo="stm_local" name="78" />
+    <bus id="59" logo="stm_local" name="80" />
+    <bus id="60" logo="stm_local" name="85" />
+    <bus id="61" logo="stm_local" name="86" />
+    <bus id="63" logo="stm_local" name="90" />
+    <bus id="" logo="stm_local" name="92" />
+    <bus id="65" logo="stm_local" name="93" />
+    <bus id="" logo="stm_local" name="94" />
+    <bus id="" logo="stm_local" name="95" />
+    <bus id="68" logo="stm_local" name="97" />
+    <bus id="" logo="stm_local" name="99" />
+    <bus id="" logo="stm_local" name="100" />
+    <bus id="" logo="stm_local" name="101" />
+    <bus id="" logo="stm_local" name="102" />
+    <bus id="73" logo="stm_local" name="103" />
+    <bus id="74" logo="stm_local" name="104" />
+    <bus id="75" logo="stm_local" name="105" />
+    <bus id="" logo="stm_local" name="106" />
+    <bus id="77" logo="stm_local" name="107" />
+    <bus id="" logo="stm_local" name="108" />
+    <bus id="79" logo="stm_local" name="109" />
+    <bus id="80" logo="stm_local" name="110" />
+    <bus id="81" logo="stm_local" name="112" />
+    <bus id="82" logo="stm_local" name="113" />
+    <bus id="" logo="stm_local" name="115" />
+    <bus id="" logo="stm_local" name="116" />
+    <bus id="" logo="stm_local" name="117" />
+    <bus id="" logo="stm_local" name="119" />
+    <bus id="87" logo="stm_local" name="121" />
+    <bus id="" logo="stm_local" name="123" />
+    <bus id="89" logo="stm_local" name="124" />
+    <bus id="90" logo="stm_local" name="125" />
+    <bus id="" logo="stm_local" name="126" />
+    <bus id="92" logo="stm_local" name="128" />
+    <bus id="93" logo="stm_local" name="129" />
+    <bus id="94" logo="stm_local" name="131" />
+    <bus id="" logo="stm_local" name="135" />
+    <bus id="234" logo="stm_local" name="136" />
+    <bus id="97" logo="stm_local" name="138" />
+    <bus id="98" logo="stm_local" name="139" />
+    <bus id="99" logo="stm_local" name="140" />
+    <bus id="100" logo="stm_local" name="141" />
+    <bus id="" logo="stm_local" name="144" />
+    <bus id="103" logo="stm_local" name="146" />
+    <bus id="" logo="stm_local" name="150" />
+    <bus id="" logo="stm_local" name="160" />
+    <bus id="108" logo="stm_local" name="161" />
+    <bus id="109" logo="stm_local" name="162" />
+    <bus id="110" logo="stm_local" name="164" />
+    <bus id="111" logo="stm_local" name="165" />
+    <bus id="" logo="stm_local" name="166" />
+    <bus id="113" logo="stm_local" name="168" />
+    <bus id="" logo="stm_local" name="170" />
+    <bus id="116" logo="stm_local" name="171" />
+    <bus id="" logo="stm_local" name="174" />
+    <bus id="" logo="stm_local" name="175" />
+    <bus id="120" logo="stm_local" name="177" />
+    <bus id="" logo="stm_local" name="178" />
+    <bus id="121" logo="stm_local" name="179" />
+    <bus id="" logo="stm_local" name="180" />
+    <bus id="" logo="stm_local" name="183" />
+    <bus id="126" logo="stm_local" name="185" />
+    <bus id="" logo="stm_local" name="186" />
+    <bus id="128" logo="stm_local" name="187" />
+    <bus id="" logo="stm_local" name="188" />
+    <bus id="130" logo="stm_local" name="189" />
+    <bus id="132" logo="stm_local" name="191" />
+    <bus id="134" logo="stm_local" name="192" />
+    <bus id="134" logo="stm_local" name="193" />
+    <bus id="136" logo="stm_local" name="195" />
+    <bus id="" logo="stm_local" name="196" />
+    <bus id="138" logo="stm_local" name="197" />
+    <bus id="" logo="stm_local" name="200" />
+    <bus id="141" logo="stm_local" name="201" />
+    <bus id="" logo="stm_local" name="202" />
+    <bus id="" logo="stm_local" name="203" />
+    <bus id="" logo="stm_local" name="204" />
+    <bus id="145" logo="stm_local" name="205" />
+    <bus id="" logo="stm_local" name="206" />
+    <bus id="147" logo="stm_local" name="207" />
+    <bus id="148" logo="stm_local" name="208" />
+    <bus id="149" logo="stm_local" name="209" />
+    <bus id="151" logo="stm_local" name="211" />
+    <bus id="" logo="stm_local" name="212" />
+    <bus id="" logo="stm_local" name="213" />
+    <bus id="" logo="stm_local" name="215" />
+    <bus id="155" logo="stm_local" name="216" />
+    <bus id="156" logo="stm_local" name="217" />
+    <bus id="" logo="stm_local" name="218" />
+    <bus id="" logo="stm_local" name="219" />
+    <bus id="" logo="stm_local" name="220" />
+    <bus id="" logo="stm_local" name="225" />
+    <bus id="" logo="stm_night" name="350" />
+    <bus id="" logo="stm_night" name="353" />
+    <bus id="" logo="stm_night" name="354" />
+    <bus id="" logo="stm_night" name="355" />
+    <bus id="" logo="stm_night" name="356" />
+    <bus id="" logo="stm_night" name="357" />
+    <bus id="" logo="stm_night" name="358" />
+    <bus id="" logo="stm_night" name="359" />
+    <bus id="" logo="stm_night" name="360" />
+    <bus id="" logo="stm_night" name="361" />
+    <bus id="" logo="stm_night" name="362" />
+    <bus id="" logo="stm_night" name="363" />
+    <bus id="" logo="stm_night" name="364" />
+    <bus id="" logo="stm_night" name="365" />
+    <bus id="" logo="stm_night" name="368" />
+    <bus id="" logo="stm_night" name="369" />
+    <bus id="" logo="stm_night" name="370" />
+    <bus id="" logo="stm_night" name="371" />
+    <bus id="" logo="stm_night" name="372" />
+    <bus id="" logo="stm_night" name="376" />
+    <bus id="" logo="stm_night" name="378" />
+    <bus id="" logo="stm_night" name="380" />
+    <bus id="" logo="stm_night" name="382" />
+    <bus id="237" logo="stm_express" name="401" />
+    <bus id="261" logo="stm_express" name="405" />
+    <bus id="" logo="stm_express" name="406" />
+    <bus id="" logo="stm_express" name="407" />
+    <bus id="" logo="stm_express" name="409" />
+    <bus id="165" logo="stm_express" name="410" />
+    <bus id="" logo="stm_express" name="411" />
+    <bus id="242" logo="stm_express" name="419" />
+    <bus id="" logo="stm_express" name="420" />
+    <bus id="" logo="stm_express" name="425" />
+    <bus id="218" logo="stm_express" name="427" />
+    <bus id="" logo="stm_express" name="428" />
+    <bus id="" logo="stm_express" name="430" />
+    <bus id="" logo="stm_express" name="432" />
+    <bus id="" logo="stm_express" name="435" />
+    <bus id="" logo="stm_express" name="439" />
+    <bus id="" logo="stm_express" name="440" />
+    <bus id="" logo="stm_express" name="444" />
+    <bus id="" logo="stm_express" name="445" />
+    <bus id="" logo="stm_express" name="448" />
+    <bus id="" logo="stm_express" name="449" />
+    <bus id="" logo="stm_express" name="460" />
+    <bus id="" logo="stm_express" name="465" />
+    <bus id="211" logo="stm_express" name="467" />
+    <bus id="" logo="stm_express" name="468" />
+    <bus id="" logo="stm_express" name="469" />
+    <bus id="169" logo="stm_express" name="470" />
+    <bus id="" logo="stm_express" name="475" />
+    <bus id="" logo="stm_express" name="480" />
+    <bus id="" logo="stm_express" name="485" />
+    <bus id="" logo="stm_express" name="486" />
+    <bus id="253" logo="stm_express" name="487" />
+    <bus id="" logo="stm_express" name="491" />
+    <bus id="255" logo="stm_express" name="495" />
+    <bus id="256" logo="stm_express" name="496" />
+    <bus id="257" logo="stm_shuttle" name="715" />
+    <bus id="219" logo="stm_shuttle" name="747" />
+    <bus id="" logo="stm_shuttle" name="777" />
+    <bus id="" logo="stm_local" name="252" />
+    <bus id="" logo="stm_local" name="253" />
+    <bus id="" logo="stm_local" name="254" />
+    <bus id="" logo="stm_local" name="256" />
+    <bus id="" logo="stm_local" name="257" />
+    <bus id="" logo="stm_local" name="258" />
+    <bus id="" logo="stm_local" name="259" />
+    <bus id="" logo="stm_local" name="260" />
+    <bus id="" logo="stm_local" name="262" />
+    <bus id="" logo="stm_local" name="263" />
+*/
+
+#ifndef OPUS_LISTS_H
+#define OPUS_LISTS_H
+
+static const char* OPUS_LINES_LIST[512] = {
+    [1] = "Green",
+    [2] = "Orange",
+    [3] = "Yellow",
+    [4] = "Blue",
+
+    [219] = "747",
+};
+
+#endif // OPUS_LISTS_H

+ 3 - 3
metroflip_i.h

@@ -44,7 +44,7 @@ extern const Icon I_RFIDDolphinReceive_97x61;
 
 #include "scenes/metroflip_scene.h"
 
-#include "scenes/navigo_structs.h"
+#include "api/calypso/calypso_i.h"
 
 typedef struct {
     Gui* gui;
@@ -75,8 +75,8 @@ typedef struct {
     char currency[4];
     char card_type[32];
 
-    // Navigo specific context
-    NavigoContext* navigo_context;
+    // Calypso specific context
+    CalypsoContext* calypso_context;
 } Metroflip;
 
 enum MetroflipCustomEvent {

+ 1477 - 0
scenes/metroflip_scene_calypso.c

@@ -0,0 +1,1477 @@
+#include "metroflip_scene_calypso.h"
+#include "../metroflip_i.h"
+#include <datetime.h>
+#include <dolphin/dolphin.h>
+#include <notification/notification_messages.h>
+#include <locale/locale.h>
+
+#include <nfc/protocols/iso14443_4b/iso14443_4b_poller.h>
+
+#define TAG "Metroflip:Scene:Calypso"
+
+int select_new_app(
+    int new_app_directory,
+    int new_app,
+    BitBuffer* tx_buffer,
+    BitBuffer* rx_buffer,
+    Iso14443_4bPoller* iso14443_4b_poller,
+    Metroflip* app,
+    MetroflipPollerEventType* stage) {
+    select_app[5] = new_app_directory;
+    select_app[6] = new_app;
+
+    bit_buffer_reset(tx_buffer);
+    bit_buffer_append_bytes(tx_buffer, select_app, sizeof(select_app));
+    FURI_LOG_D(
+        TAG,
+        "SEND %02x %02x %02x %02x %02x %02x %02x %02x",
+        select_app[0],
+        select_app[1],
+        select_app[2],
+        select_app[3],
+        select_app[4],
+        select_app[5],
+        select_app[6],
+        select_app[7]);
+    int error = iso14443_4b_poller_send_block(iso14443_4b_poller, tx_buffer, rx_buffer);
+    if(error != Iso14443_4bErrorNone) {
+        FURI_LOG_I(TAG, "Select File: iso14443_4b_poller_send_block error %d", error);
+        *stage = MetroflipPollerEventTypeFail;
+        view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventPollerFail);
+        return error;
+    }
+    return 0;
+}
+
+int read_new_file(
+    int new_file,
+    BitBuffer* tx_buffer,
+    BitBuffer* rx_buffer,
+    Iso14443_4bPoller* iso14443_4b_poller,
+    Metroflip* app,
+    MetroflipPollerEventType* stage) {
+    read_file[2] = new_file;
+    bit_buffer_reset(tx_buffer);
+    bit_buffer_append_bytes(tx_buffer, read_file, sizeof(read_file));
+    FURI_LOG_D(
+        TAG,
+        "SEND %02x %02x %02x %02x %02x",
+        read_file[0],
+        read_file[1],
+        read_file[2],
+        read_file[3],
+        read_file[4]);
+    Iso14443_4bError error =
+        iso14443_4b_poller_send_block(iso14443_4b_poller, tx_buffer, rx_buffer);
+    if(error != Iso14443_4bErrorNone) {
+        FURI_LOG_I(TAG, "Read File: iso14443_4b_poller_send_block error %d", error);
+        *stage = MetroflipPollerEventTypeFail;
+        view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventPollerFail);
+        return error;
+    }
+    return 0;
+}
+
+int check_response(
+    BitBuffer* rx_buffer,
+    Metroflip* app,
+    MetroflipPollerEventType* stage,
+    size_t* response_length) {
+    *response_length = bit_buffer_get_size_bytes(rx_buffer);
+    if(bit_buffer_get_byte(rx_buffer, *response_length - 2) != apdu_success[0] ||
+       bit_buffer_get_byte(rx_buffer, *response_length - 1) != apdu_success[1]) {
+        int error_code_1 = bit_buffer_get_byte(rx_buffer, *response_length - 2);
+        int error_code_2 = bit_buffer_get_byte(rx_buffer, *response_length - 1);
+        FURI_LOG_E(TAG, "Select profile app/file failed: %02x%02x", error_code_1, error_code_2);
+        if(error_code_1 == 0x6a && error_code_2 == 0x82) {
+            FURI_LOG_E(TAG, "Wrong parameter(s) P1-P2 - File not found");
+        } else if(error_code_1 == 0x69 && error_code_2 == 0x82) {
+            FURI_LOG_E(TAG, "Command not allowed - Security status not satisfied");
+        }
+        *stage = MetroflipPollerEventTypeFail;
+        view_dispatcher_send_custom_event(
+            app->view_dispatcher, MetroflipCustomEventPollerFileNotFound);
+        return 1;
+    }
+    return 0;
+}
+
+void update_page_info(void* context, FuriString* parsed_data) {
+    Metroflip* app = context;
+    CalypsoContext* ctx = app->calypso_context;
+    if(ctx->card->card_type != CALYPSO_CARD_NAVIGO && ctx->card->card_type != CALYPSO_CARD_OPUS) {
+        furi_string_cat_printf(
+            parsed_data,
+            "\e#%s %u:\n",
+            get_network_string(ctx->card->card_type),
+            ctx->card->card_number);
+        return;
+    }
+    if(ctx->page_id == 0 || ctx->page_id == 1 || ctx->page_id == 2 || ctx->page_id == 3) {
+        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);
+            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);
+            break;
+        }
+        default: {
+            furi_string_cat_printf(parsed_data, "\e#Unknown %u:\n", ctx->card->card_number);
+            break;
+        }
+        }
+    } else if(ctx->page_id == 4) {
+        furi_string_cat_printf(parsed_data, "\e#Environment:\n");
+        switch(ctx->card->card_type) {
+        case CALYPSO_CARD_NAVIGO: {
+            show_navigo_environment_info(&ctx->card->navigo->environment, parsed_data);
+            break;
+        }
+        case CALYPSO_CARD_OPUS: {
+            show_opus_environment_info(&ctx->card->opus->environment, parsed_data);
+            break;
+        }
+        default: {
+            break;
+        }
+        }
+    } else if(ctx->page_id == 5 || ctx->page_id == 6 || ctx->page_id == 7) {
+        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;
+        }
+        default: {
+            break;
+        }
+        }
+    }
+}
+
+void update_widget_elements(void* context) {
+    Metroflip* app = context;
+    CalypsoContext* ctx = app->calypso_context;
+    Widget* widget = app->widget;
+    if(ctx->card->card_type != CALYPSO_CARD_NAVIGO && ctx->card->card_type != CALYPSO_CARD_OPUS) {
+        widget_add_button_element(
+            widget, GuiButtonTypeRight, "Exit", metroflip_next_button_widget_callback, context);
+        return;
+    }
+    if(ctx->page_id < 7) {
+        widget_add_button_element(
+            widget, GuiButtonTypeRight, "Next", metroflip_next_button_widget_callback, context);
+    } else {
+        widget_add_button_element(
+            widget, GuiButtonTypeRight, "Exit", metroflip_next_button_widget_callback, context);
+    }
+    if(ctx->page_id > 0) {
+        widget_add_button_element(
+            widget, GuiButtonTypeLeft, "Back", metroflip_back_button_widget_callback, context);
+    }
+}
+
+void metroflip_back_button_widget_callback(GuiButtonType result, InputType type, void* context) {
+    Metroflip* app = context;
+    CalypsoContext* ctx = app->calypso_context;
+    UNUSED(result);
+
+    Widget* widget = app->widget;
+
+    if(type == InputTypePress) {
+        widget_reset(widget);
+
+        FURI_LOG_I(TAG, "Page ID: %d -> %d", ctx->page_id, ctx->page_id - 1);
+
+        if(ctx->page_id > 0) {
+            if(ctx->page_id == 4 && ctx->card->contracts_count < 4) {
+                ctx->page_id -= 1;
+            }
+            if(ctx->page_id == 3 && ctx->card->contracts_count < 3) {
+                ctx->page_id -= 1;
+            }
+            if(ctx->page_id == 2 && ctx->card->contracts_count < 2) {
+                ctx->page_id -= 1;
+            }
+            ctx->page_id -= 1;
+        }
+
+        FuriString* parsed_data = furi_string_alloc();
+
+        // Ensure no nested mutexes
+        furi_mutex_acquire(ctx->mutex, FuriWaitForever);
+        update_page_info(app, parsed_data);
+        furi_mutex_release(ctx->mutex);
+
+        widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
+        // widget_add_icon_element(widget, 0, 0, &I_RFIDDolphinReceive_97x61);
+
+        // Ensure no nested mutexes
+        furi_mutex_acquire(ctx->mutex, FuriWaitForever);
+        update_widget_elements(app);
+        furi_mutex_release(ctx->mutex);
+
+        furi_string_free(parsed_data);
+    }
+}
+
+void metroflip_next_button_widget_callback(GuiButtonType result, InputType type, void* context) {
+    Metroflip* app = context;
+    CalypsoContext* ctx = app->calypso_context;
+    UNUSED(result);
+
+    Widget* widget = app->widget;
+
+    if(type == InputTypePress) {
+        widget_reset(widget);
+
+        FURI_LOG_I(TAG, "Page ID: %d -> %d", ctx->page_id, ctx->page_id + 1);
+
+        if(ctx->card->card_type != CALYPSO_CARD_NAVIGO &&
+           ctx->card->card_type != CALYPSO_CARD_OPUS) {
+            ctx->page_id = 0;
+            scene_manager_search_and_switch_to_previous_scene(
+                app->scene_manager, MetroflipSceneStart);
+            return;
+        }
+        if(ctx->page_id < 7) {
+            if(ctx->page_id == 0 && ctx->card->contracts_count < 2) {
+                ctx->page_id += 1;
+            }
+            if(ctx->page_id == 1 && ctx->card->contracts_count < 3) {
+                ctx->page_id += 1;
+            }
+            if(ctx->page_id == 2 && ctx->card->contracts_count < 4) {
+                ctx->page_id += 1;
+            }
+            ctx->page_id += 1;
+        } else {
+            ctx->page_id = 0;
+            scene_manager_search_and_switch_to_previous_scene(
+                app->scene_manager, MetroflipSceneStart);
+            return;
+        }
+
+        FuriString* parsed_data = furi_string_alloc();
+
+        // Ensure no nested mutexes
+        furi_mutex_acquire(ctx->mutex, FuriWaitForever);
+        update_page_info(app, parsed_data);
+        furi_mutex_release(ctx->mutex);
+
+        widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
+
+        // Ensure no nested mutexes
+        furi_mutex_acquire(ctx->mutex, FuriWaitForever);
+        update_widget_elements(app);
+        furi_mutex_release(ctx->mutex);
+
+        furi_string_free(parsed_data);
+    }
+}
+
+void delay(int milliseconds) {
+    furi_thread_flags_wait(0, FuriFlagWaitAny, milliseconds);
+}
+
+static NfcCommand metroflip_scene_navigo_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) {
+            // Start Flipper vibration
+            NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
+            notification_message(notification, &sequence_set_vibro_on);
+            delay(50);
+            notification_message(notification, &sequence_reset_vibro);
+            nfc_device_set_data(
+                app->nfc_device, NfcProtocolIso14443_4b, nfc_poller_get_data(app->poller));
+
+            Iso14443_4bError error;
+            size_t response_length = 0;
+
+            do {
+                // Initialize the card data
+                CalypsoCardData* card = malloc(sizeof(CalypsoCardData));
+
+                // Select app ICC
+                error = select_new_app(
+                    0x00, 0x02, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                if(error != 0) {
+                    break;
+                }
+
+                // Check the response after selecting app
+                if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
+                    break;
+                }
+
+                // Now send the read command for ICC
+                error = read_new_file(0x01, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                if(error != 0) {
+                    break;
+                }
+
+                // Check the response after reading the file
+                if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
+                    break;
+                }
+
+                char icc_bit_representation[response_length * 8 + 1];
+                icc_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(icc_bit_representation, bits, sizeof(icc_bit_representation));
+                }
+                icc_bit_representation[response_length * 8] = '\0';
+
+                int start = 128, end = 159;
+                card->card_number = bit_slice_to_dec(icc_bit_representation, start, end);
+
+                // Select app for ticketing
+                error = select_new_app(
+                    0x20, 0x00, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                if(error != 0) {
+                    FURI_LOG_E(TAG, "Failed to select app for ticketing");
+                    break;
+                }
+
+                // Check the response after selecting app
+                if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
+                    FURI_LOG_E(TAG, "Failed to check response after selecting app for ticketing");
+                    break;
+                }
+
+                // Select app for environment
+                error = select_new_app(
+                    0x20, 0x1, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                if(error != 0) {
+                    break;
+                }
+
+                // Check the response after selecting app
+                if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
+                    break;
+                }
+
+                // read file 1
+                error = read_new_file(1, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                if(error != 0) {
+                    break;
+                }
+
+                // Check the response after reading the file
+                if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
+                    break;
+                }
+
+                char environment_bit_representation[response_length * 8 + 1];
+                environment_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(
+                        environment_bit_representation,
+                        bits,
+                        sizeof(environment_bit_representation));
+                }
+                // FURI_LOG_I(
+                //     TAG, "Environment bit_representation: %s", environment_bit_representation);
+                start = 13;
+                end = 16;
+                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 = 25;
+                end = 28;
+                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);
+                card->card_type = guess_card_type(country_num, 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;
+
+                    CalypsoApp* IntercodeEnvHolderStructure = get_intercode_env_holder_structure();
+
+                    // EnvApplicationVersionNumber
+                    const char* env_key = "EnvApplicationVersionNumber";
+                    int positionOffset = get_calypso_node_offset(
+                        environment_bit_representation, env_key, IntercodeEnvHolderStructure);
+                    int start = positionOffset,
+                        end = positionOffset +
+                              get_calypso_node_size(env_key, IntercodeEnvHolderStructure) - 1;
+                    card->navigo->environment.app_version =
+                        bit_slice_to_dec(environment_bit_representation, start, end);
+
+                    // EnvApplicationValidityEndDate
+                    env_key = "EnvApplicationValidityEndDate";
+                    positionOffset = get_calypso_node_offset(
+                        environment_bit_representation, env_key, IntercodeEnvHolderStructure);
+                    start = positionOffset,
+                    end = positionOffset +
+                          get_calypso_node_size(env_key, IntercodeEnvHolderStructure) - 1;
+                    float decimal_value =
+                        bit_slice_to_dec(environment_bit_representation, start, end);
+                    uint64_t end_validity_timestamp =
+                        (decimal_value * 24 * 3600) + (float)epoch + 3600;
+                    datetime_timestamp_to_datetime(
+                        end_validity_timestamp, &card->navigo->environment.end_dt);
+
+                    // HolderDataCardStatus
+                    env_key = "HolderDataCardStatus";
+                    positionOffset = get_calypso_node_offset(
+                        environment_bit_representation, env_key, IntercodeEnvHolderStructure);
+                    start = positionOffset,
+                    end = positionOffset +
+                          get_calypso_node_size(env_key, IntercodeEnvHolderStructure) - 1;
+                    card->navigo->holder.card_status =
+                        bit_slice_to_dec(environment_bit_representation, start, end);
+
+                    // HolderDataCommercialID
+                    env_key = "HolderDataCommercialID";
+                    positionOffset = get_calypso_node_offset(
+                        environment_bit_representation, env_key, IntercodeEnvHolderStructure);
+                    start = positionOffset,
+                    end = positionOffset +
+                          get_calypso_node_size(env_key, IntercodeEnvHolderStructure) - 1;
+                    card->navigo->holder.commercial_id =
+                        bit_slice_to_dec(environment_bit_representation, start, end);
+
+                    // Free the calypso structure
+                    free_calypso_structure(IntercodeEnvHolderStructure);
+
+                    // Select app for contracts
+                    error = select_new_app(
+                        0x20, 0x20, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                    if(error != 0) {
+                        FURI_LOG_E(TAG, "Failed to select app for contracts");
+                        break;
+                    }
+
+                    // Check the response after selecting app
+                    if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
+                        FURI_LOG_E(
+                            TAG, "Failed to check response after selecting app for contracts");
+                        break;
+                    }
+
+                    // Prepare calypso structure
+                    CalypsoApp* IntercodeContractStructure = get_intercode_contract_structure();
+                    if(!IntercodeContractStructure) {
+                        FURI_LOG_E(TAG, "Failed to load Intercode Contract structure");
+                        break;
+                    }
+
+                    // Now send the read command for contracts
+                    for(size_t i = 1; i < 5; i++) {
+                        error = read_new_file(
+                            i, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                        if(error != 0) {
+                            FURI_LOG_E(TAG, "Failed to read contract %d", i);
+                            break;
+                        }
+
+                        // Check the response after reading the file
+                        if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
+                            FURI_LOG_E(
+                                TAG, "Failed to check response after reading contract %d", i);
+                            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));
+                        }
+                        bit_representation[response_length * 8] = '\0';
+
+                        if(bit_slice_to_dec(
+                               bit_representation,
+                               0,
+                               IntercodeContractStructure->container->elements[0].bitmap->size -
+                                   1) == 0) {
+                            break;
+                        }
+
+                        card->navigo->contracts[i - 1].present = 1;
+                        card->contracts_count++;
+
+                        // 2. ContractTariff
+                        const char* contract_key = "ContractTariff";
+                        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;
+                            card->navigo->contracts[i - 1].tariff =
+                                bit_slice_to_dec(bit_representation, start, end);
+                        }
+
+                        // 3. ContractSerialNumber
+                        contract_key = "ContractSerialNumber";
+                        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;
+                            card->navigo->contracts[i - 1].serial_number =
+                                bit_slice_to_dec(bit_representation, start, end);
+                            card->navigo->contracts[i - 1].serial_number_available = true;
+                        }
+
+                        // 8. ContractPayMethod
+                        contract_key = "ContractPayMethod";
+                        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;
+                            card->navigo->contracts[i - 1].pay_method =
+                                bit_slice_to_dec(bit_representation, start, end);
+                            card->navigo->contracts[i - 1].pay_method_available = true;
+                        }
+
+                        // 10. ContractPriceAmount
+                        contract_key = "ContractPriceAmount";
+                        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;
+                            card->navigo->contracts[i - 1].price_amount =
+                                bit_slice_to_dec(bit_representation, start, end) / 100.0;
+                            card->navigo->contracts[i - 1].price_amount_available = true;
+                        }
+
+                        // 13.0. ContractValidityStartDate
+                        contract_key = "ContractValidityStartDate";
+                        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;
+                            float decimal_value =
+                                bit_slice_to_dec(bit_representation, start, end) * 24 * 3600;
+                            uint64_t start_validity_timestamp =
+                                (decimal_value + (float)epoch) + 3600;
+                            datetime_timestamp_to_datetime(
+                                start_validity_timestamp,
+                                &card->navigo->contracts[i - 1].start_date);
+                        }
+
+                        // 13.2. ContractValidityEndDate
+                        contract_key = "ContractValidityEndDate";
+                        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;
+                            float decimal_value =
+                                bit_slice_to_dec(bit_representation, start, end) * 24 * 3600;
+                            uint64_t end_validity_timestamp =
+                                (decimal_value + (float)epoch) + 3600;
+                            datetime_timestamp_to_datetime(
+                                end_validity_timestamp, &card->navigo->contracts[i - 1].end_date);
+                            card->navigo->contracts[i - 1].end_date_available = true;
+                        }
+
+                        // 13.6. ContractValidityZones
+                        contract_key = "ContractValidityZones";
+                        if(is_calypso_node_present(
+                               bit_representation, contract_key, IntercodeContractStructure)) {
+                            int start = get_calypso_node_offset(
+                                bit_representation, contract_key, IntercodeContractStructure);
+                            // binary form is 00011111 for zones 5, 4, 3, 2, 1
+                            for(int j = 0; j < 5; j++) {
+                                card->navigo->contracts[i - 1].zones[j] = bit_slice_to_dec(
+                                    bit_representation, start + 3 + j, start + 3 + j);
+                            }
+                            card->navigo->contracts[i - 1].zones_available = true;
+                        }
+
+                        // 13.7. ContractValidityJourneys  -- pas sûr de le mettre lui
+
+                        // 15.0. ContractValiditySaleDate
+                        contract_key = "ContractValiditySaleDate";
+                        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;
+                            float decimal_value =
+                                bit_slice_to_dec(bit_representation, start, end) * 24 * 3600;
+                            uint64_t sale_timestamp = (decimal_value + (float)epoch) + 3600;
+                            datetime_timestamp_to_datetime(
+                                sale_timestamp, &card->navigo->contracts[i - 1].sale_date);
+                        }
+
+                        // 15.2. ContractValiditySaleAgent - FIX NEEDED
+                        contract_key = "ContractValiditySaleAgent";
+                        /* if(is_calypso_node_present(
+                           bit_representation, contract_key, NavigoContractStructure)) { */
+                        int positionOffset = get_calypso_node_offset(
+                            bit_representation, contract_key, IntercodeContractStructure);
+                        int start = positionOffset,
+                            end = positionOffset +
+                                  get_calypso_node_size(contract_key, IntercodeContractStructure) -
+                                  1;
+                        card->navigo->contracts[i - 1].sale_agent =
+                            bit_slice_to_dec(bit_representation, start, end);
+                        // }
+
+                        // 15.3. ContractValiditySaleDevice
+                        contract_key = "ContractValiditySaleDevice";
+                        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;
+                            card->navigo->contracts[i - 1].sale_device =
+                                bit_slice_to_dec(bit_representation, start, end);
+                        }
+
+                        // 16. ContractStatus  -- 0x1 ou 0xff
+                        contract_key = "ContractStatus";
+                        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;
+                            card->navigo->contracts[i - 1].status =
+                                bit_slice_to_dec(bit_representation, start, end);
+                        }
+
+                        // 18. ContractAuthenticator
+                        contract_key = "ContractAuthenticator";
+                        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;
+                            card->navigo->contracts[i - 1].authenticator =
+                                bit_slice_to_dec(bit_representation, start, end);
+                        }
+                    }
+
+                    // Free the calypso structure
+                    free_calypso_structure(IntercodeContractStructure);
+
+                    // Select app for counters (remaining tickets on Navigo Easy)
+                    error = select_new_app(
+                        0x20, 0x69, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                    if(error != 0) {
+                        break;
+                    }
+
+                    // Check the response after selecting app
+                    if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
+                        break;
+                    }
+
+                    // read file 1
+                    error =
+                        read_new_file(1, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                    if(error != 0) {
+                        break;
+                    }
+
+                    // Check the response after reading the file
+                    if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
+                        break;
+                    }
+
+                    char counter_bit_representation[response_length * 8 + 1];
+                    counter_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(
+                            counter_bit_representation, bits, sizeof(counter_bit_representation));
+                    }
+                    // FURI_LOG_I(TAG, "Counter bit_representation: %s", counter_bit_representation);
+
+                    // Ticket counts (contracts 1-4)
+                    for(int i = 0; i < 4; i++) {
+                        start = 0;
+                        end = 5;
+                        card->navigo->contracts[i].counter.count = bit_slice_to_dec(
+                            counter_bit_representation, 24 * i + start, 24 * i + end);
+
+                        start = 6;
+                        end = 23;
+                        card->navigo->contracts[i].counter.relative_first_stamp_15mn =
+                            bit_slice_to_dec(
+                                counter_bit_representation, 24 * i + start, 24 * i + end);
+                    }
+
+                    // Select app for events
+                    error = select_new_app(
+                        0x20, 0x10, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                    if(error != 0) {
+                        break;
+                    }
+
+                    // Check the response after selecting app
+                    if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
+                        break;
+                    }
+
+                    // Load the calypso structure for events
+                    CalypsoApp* IntercodeEventStructure = get_intercode_event_structure();
+                    if(!IntercodeEventStructure) {
+                        FURI_LOG_E(TAG, "Failed to load Intercode Event structure");
+                        break;
+                    }
+
+                    // furi_string_cat_printf(parsed_data, "\e#Events :\n");
+                    // Now send the read command for events
+                    for(size_t i = 1; i < 4; i++) {
+                        error = read_new_file(
+                            i, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                        if(error != 0) {
+                            break;
+                        }
+
+                        // Check the response after reading the file
+                        if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
+                            break;
+                        }
+
+                        char event_bit_representation[response_length * 8 + 1];
+                        event_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(
+                                event_bit_representation, bits, sizeof(event_bit_representation));
+                        }
+
+                        // furi_string_cat_printf(parsed_data, "Event 0%d :\n", i);
+                        /* int count = 0;
+                    int start = 25, end = 52;
+                    char bit_slice[end - start + 2];
+                    strncpy(bit_slice, event_bit_representation + start, end - start + 1);
+                    bit_slice[end - start + 1] = '\0';
+                    int* positions = get_bit_positions(bit_slice, &count);
+                    FURI_LOG_I(TAG, "Positions: ");
+                    for(int i = 0; i < count; i++) {
+                        FURI_LOG_I(TAG, "%d ", positions[i]);
+                    } */
+
+                        // 2. EventCode
+                        const char* event_key = "EventCode";
+                        if(is_calypso_node_present(
+                               event_bit_representation, event_key, IntercodeEventStructure)) {
+                            int positionOffset = get_calypso_node_offset(
+                                event_bit_representation, event_key, IntercodeEventStructure);
+                            int start = positionOffset,
+                                end = positionOffset +
+                                      get_calypso_node_size(event_key, IntercodeEventStructure) -
+                                      1;
+                            int decimal_value =
+                                bit_slice_to_dec(event_bit_representation, start, end);
+                            card->navigo->events[i - 1].transport_type = decimal_value >> 4;
+                            card->navigo->events[i - 1].transition = decimal_value & 15;
+                        }
+
+                        // 4. EventServiceProvider
+                        event_key = "EventServiceProvider";
+                        if(is_calypso_node_present(
+                               event_bit_representation, event_key, IntercodeEventStructure)) {
+                            int positionOffset = get_calypso_node_offset(
+                                event_bit_representation, event_key, IntercodeEventStructure);
+                            start = positionOffset,
+                            end = positionOffset +
+                                  get_calypso_node_size(event_key, IntercodeEventStructure) - 1;
+                            card->navigo->events[i - 1].service_provider =
+                                bit_slice_to_dec(event_bit_representation, start, end);
+                        }
+
+                        // 8. EventLocationId
+                        event_key = "EventLocationId";
+                        if(is_calypso_node_present(
+                               event_bit_representation, event_key, IntercodeEventStructure)) {
+                            int positionOffset = get_calypso_node_offset(
+                                event_bit_representation, event_key, IntercodeEventStructure);
+                            start = positionOffset,
+                            end = positionOffset +
+                                  get_calypso_node_size(event_key, IntercodeEventStructure) - 1;
+                            int decimal_value =
+                                bit_slice_to_dec(event_bit_representation, start, end);
+                            card->navigo->events[i - 1].station_group_id = decimal_value >> 9;
+                            card->navigo->events[i - 1].station_id = (decimal_value >> 4) & 31;
+                        }
+
+                        // 9. EventLocationGate
+                        event_key = "EventLocationGate";
+                        if(is_calypso_node_present(
+                               event_bit_representation, event_key, IntercodeEventStructure)) {
+                            int positionOffset = get_calypso_node_offset(
+                                event_bit_representation, event_key, IntercodeEventStructure);
+                            start = positionOffset,
+                            end = positionOffset +
+                                  get_calypso_node_size(event_key, IntercodeEventStructure) - 1;
+                            card->navigo->events[i - 1].location_gate =
+                                bit_slice_to_dec(event_bit_representation, start, end);
+                            card->navigo->events[i - 1].location_gate_available = true;
+                        }
+
+                        // 10. EventDevice
+                        event_key = "EventDevice";
+                        if(is_calypso_node_present(
+                               event_bit_representation, event_key, IntercodeEventStructure)) {
+                            int positionOffset = get_calypso_node_offset(
+                                event_bit_representation, event_key, IntercodeEventStructure);
+                            start = positionOffset,
+                            end = positionOffset +
+                                  get_calypso_node_size(event_key, IntercodeEventStructure) - 1;
+                            int decimal_value =
+                                bit_slice_to_dec(event_bit_representation, start, end);
+                            card->navigo->events[i - 1].device = decimal_value;
+                            int bus_device = decimal_value >> 8;
+                            card->navigo->events[i - 1].door = bus_device / 2 + 1;
+                            card->navigo->events[i - 1].side = bus_device % 2;
+                            card->navigo->events[i - 1].device_available = true;
+                        }
+
+                        // 11. EventRouteNumber
+                        event_key = "EventRouteNumber";
+                        if(is_calypso_node_present(
+                               event_bit_representation, event_key, IntercodeEventStructure)) {
+                            int positionOffset = get_calypso_node_offset(
+                                event_bit_representation, event_key, IntercodeEventStructure);
+                            start = positionOffset,
+                            end = positionOffset +
+                                  get_calypso_node_size(event_key, IntercodeEventStructure) - 1;
+                            card->navigo->events[i - 1].route_number =
+                                bit_slice_to_dec(event_bit_representation, start, end);
+                            card->navigo->events[i - 1].route_number_available = true;
+                        }
+
+                        // 13. EventJourneyRun
+                        event_key = "EventJourneyRun";
+                        if(is_calypso_node_present(
+                               event_bit_representation, event_key, IntercodeEventStructure)) {
+                            int positionOffset = get_calypso_node_offset(
+                                event_bit_representation, event_key, IntercodeEventStructure);
+                            start = positionOffset,
+                            end = positionOffset +
+                                  get_calypso_node_size(event_key, IntercodeEventStructure) - 1;
+                            card->navigo->events[i - 1].mission =
+                                bit_slice_to_dec(event_bit_representation, start, end);
+                            card->navigo->events[i - 1].mission_available = true;
+                        }
+
+                        // 14. EventVehicleId
+                        event_key = "EventVehicleId";
+                        if(is_calypso_node_present(
+                               event_bit_representation, event_key, IntercodeEventStructure)) {
+                            int positionOffset = get_calypso_node_offset(
+                                event_bit_representation, event_key, IntercodeEventStructure);
+                            start = positionOffset,
+                            end = positionOffset +
+                                  get_calypso_node_size(event_key, IntercodeEventStructure) - 1;
+                            card->navigo->events[i - 1].vehicle_id =
+                                bit_slice_to_dec(event_bit_representation, start, end);
+                            card->navigo->events[i - 1].vehicle_id_available = true;
+                        }
+
+                        // 25. EventContractPointer
+                        event_key = "EventContractPointer";
+                        if(is_calypso_node_present(
+                               event_bit_representation, event_key, IntercodeEventStructure)) {
+                            int positionOffset = get_calypso_node_offset(
+                                event_bit_representation, event_key, IntercodeEventStructure);
+                            start = positionOffset,
+                            end = positionOffset +
+                                  get_calypso_node_size(event_key, IntercodeEventStructure) - 1;
+                            card->navigo->events[i - 1].used_contract =
+                                bit_slice_to_dec(event_bit_representation, start, end);
+                            card->navigo->events[i - 1].used_contract_available = true;
+                        }
+
+                        // EventDateStamp
+                        event_key = "EventDateStamp";
+                        int positionOffset = get_calypso_node_offset(
+                            event_bit_representation, event_key, IntercodeEventStructure);
+                        start = positionOffset,
+                        end = positionOffset +
+                              get_calypso_node_size(event_key, IntercodeEventStructure) - 1;
+                        int decimal_value = bit_slice_to_dec(event_bit_representation, start, end);
+                        uint64_t date_timestamp = (decimal_value * 24 * 3600) + epoch + 3600;
+                        datetime_timestamp_to_datetime(
+                            date_timestamp, &card->navigo->events[i - 1].date);
+
+                        // EventTimeStamp
+                        event_key = "EventTimeStamp";
+                        positionOffset = get_calypso_node_offset(
+                            event_bit_representation, event_key, IntercodeEventStructure);
+                        start = positionOffset,
+                        end = positionOffset +
+                              get_calypso_node_size(event_key, IntercodeEventStructure) - 1;
+                        decimal_value = bit_slice_to_dec(event_bit_representation, start, end);
+                        card->navigo->events[i - 1].date.hour = (decimal_value * 60) / 3600;
+                        card->navigo->events[i - 1].date.minute =
+                            ((decimal_value * 60) % 3600) / 60;
+                        card->navigo->events[i - 1].date.second =
+                            ((decimal_value * 60) % 3600) % 60;
+                    }
+
+                    // Free the calypso structure
+                    free_calypso_structure(IntercodeEventStructure);
+                    break;
+                }
+                case CALYPSO_CARD_OPUS: {
+                    card->opus = malloc(sizeof(OpusCardData));
+
+                    card->opus->environment.country_num = country_num;
+                    card->opus->environment.network_num = network_num;
+
+                    CalypsoApp* OpusEnvHolderStructure = get_opus_env_holder_structure();
+
+                    // EnvApplicationVersionNumber
+                    const char* env_key = "EnvApplicationVersionNumber";
+                    int positionOffset = get_calypso_node_offset(
+                        environment_bit_representation, env_key, OpusEnvHolderStructure);
+                    int start = positionOffset,
+                        end = positionOffset +
+                              get_calypso_node_size(env_key, OpusEnvHolderStructure) - 1;
+                    card->opus->environment.app_version =
+                        bit_slice_to_dec(environment_bit_representation, start, end);
+
+                    // EnvApplicationValidityEndDate
+                    env_key = "EnvApplicationValidityEndDate";
+                    positionOffset = get_calypso_node_offset(
+                        environment_bit_representation, env_key, OpusEnvHolderStructure);
+                    start = positionOffset,
+                    end = positionOffset + get_calypso_node_size(env_key, OpusEnvHolderStructure) -
+                          1;
+                    float decimal_value =
+                        bit_slice_to_dec(environment_bit_representation, start, end);
+                    uint64_t end_validity_timestamp =
+                        (decimal_value * 24 * 3600) + (float)epoch + 3600;
+                    datetime_timestamp_to_datetime(
+                        end_validity_timestamp, &card->opus->environment.end_dt);
+
+                    // HolderDataCardStatus
+                    env_key = "HolderDataCardStatus";
+                    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 =
+                        bit_slice_to_dec(environment_bit_representation, start, end);
+
+                    // HolderDataCommercialID
+                    env_key = "HolderDataCommercialID";
+                    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 =
+                        bit_slice_to_dec(environment_bit_representation, start, end);
+
+                    // Free the calypso structure
+                    free_calypso_structure(OpusEnvHolderStructure);
+
+                    // Select app for contracts
+                    error = select_new_app(
+                        0x20, 0x20, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                    if(error != 0) {
+                        FURI_LOG_E(TAG, "Failed to select app for contracts");
+                        break;
+                    }
+
+                    // Check the response after selecting app
+                    if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
+                        FURI_LOG_E(
+                            TAG, "Failed to check response after selecting app for contracts");
+                        break;
+                    }
+
+                    // Prepare calypso structure
+                    CalypsoApp* OpusContractStructure = get_opus_contract_structure();
+                    if(!OpusContractStructure) {
+                        FURI_LOG_E(TAG, "Failed to load Opus Contract structure");
+                        break;
+                    }
+
+                    // Now send the read command for contracts
+                    for(size_t i = 1; i < 5; i++) {
+                        error = read_new_file(
+                            i, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                        if(error != 0) {
+                            FURI_LOG_E(TAG, "Failed to read contract %d", i);
+                            break;
+                        }
+
+                        // Check the response after reading the file
+                        if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
+                            FURI_LOG_E(
+                                TAG, "Failed to check response after reading contract %d", i);
+                            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));
+                        }
+                        bit_representation[response_length * 8] = '\0';
+
+                        if(bit_slice_to_dec(
+                               bit_representation,
+                               0,
+                               OpusContractStructure->container->elements[1].bitmap->size - 1) ==
+                           0) {
+                            break;
+                        }
+
+                        card->opus->contracts[i - 1].present = 1;
+                        card->contracts_count++;
+
+                        // ContractProvider
+                        const char* contract_key = "ContractProvider";
+                        if(is_calypso_node_present(
+                               bit_representation, contract_key, OpusContractStructure)) {
+                            int positionOffset = get_calypso_node_offset(
+                                bit_representation, contract_key, OpusContractStructure);
+                            int start = positionOffset,
+                                end = positionOffset +
+                                      get_calypso_node_size(contract_key, OpusContractStructure) -
+                                      1;
+                            card->opus->contracts[i - 1].provider =
+                                bit_slice_to_dec(bit_representation, start, end);
+                        }
+
+                        // ContractTariff
+                        contract_key = "ContractTariff";
+                        if(is_calypso_node_present(
+                               bit_representation, contract_key, OpusContractStructure)) {
+                            int positionOffset = get_calypso_node_offset(
+                                bit_representation, contract_key, OpusContractStructure);
+                            int start = positionOffset,
+                                end = positionOffset +
+                                      get_calypso_node_size(contract_key, OpusContractStructure) -
+                                      1;
+                            card->opus->contracts[i - 1].tariff =
+                                bit_slice_to_dec(bit_representation, start, end);
+                        }
+
+                        // ContractStartDate
+                        contract_key = "ContractStartDate";
+                        if(is_calypso_node_present(
+                               bit_representation, contract_key, OpusContractStructure)) {
+                            int positionOffset = get_calypso_node_offset(
+                                bit_representation, contract_key, OpusContractStructure);
+                            int start = positionOffset,
+                                end = positionOffset +
+                                      get_calypso_node_size(contract_key, OpusContractStructure) -
+                                      1;
+                            float decimal_value =
+                                bit_slice_to_dec(bit_representation, start, end) * 24 * 3600;
+                            uint64_t start_validity_timestamp =
+                                (decimal_value + (float)epoch) + 3600;
+                            datetime_timestamp_to_datetime(
+                                start_validity_timestamp,
+                                &card->opus->contracts[i - 1].start_date);
+                        }
+
+                        // ContractEndDate
+                        contract_key = "ContractEndDate";
+                        if(is_calypso_node_present(
+                               bit_representation, contract_key, OpusContractStructure)) {
+                            int positionOffset = get_calypso_node_offset(
+                                bit_representation, contract_key, OpusContractStructure);
+                            int start = positionOffset,
+                                end = positionOffset +
+                                      get_calypso_node_size(contract_key, OpusContractStructure) -
+                                      1;
+                            float decimal_value =
+                                bit_slice_to_dec(bit_representation, start, end) * 24 * 3600;
+                            uint64_t end_validity_timestamp =
+                                (decimal_value + (float)epoch) + 3600;
+                            datetime_timestamp_to_datetime(
+                                end_validity_timestamp, &card->opus->contracts[i - 1].end_date);
+                        }
+
+                        // ContractStatus
+                        contract_key = "ContractStatus";
+                        if(is_calypso_node_present(
+                               bit_representation, contract_key, OpusContractStructure)) {
+                            int positionOffset = get_calypso_node_offset(
+                                bit_representation, contract_key, OpusContractStructure);
+                            int start = positionOffset,
+                                end = positionOffset +
+                                      get_calypso_node_size(contract_key, OpusContractStructure) -
+                                      1;
+                            card->opus->contracts[i - 1].status =
+                                bit_slice_to_dec(bit_representation, start, end);
+                        }
+
+                        // ContractSaleDate + ContractSaleTime
+                        contract_key = "ContractSaleDate";
+                        int positionOffset = get_calypso_node_offset(
+                            bit_representation, contract_key, OpusContractStructure);
+                        int start = positionOffset,
+                            end = positionOffset +
+                                  get_calypso_node_size(contract_key, OpusContractStructure) - 1;
+                        uint64_t sale_date_timestamp =
+                            (bit_slice_to_dec(bit_representation, start, end) + (float)epoch) +
+                            3600;
+                        datetime_timestamp_to_datetime(
+                            sale_date_timestamp, &card->opus->contracts[i - 1].sale_date);
+
+                        contract_key = "ContractSaleTime";
+                        positionOffset = get_calypso_node_offset(
+                            bit_representation, contract_key, OpusContractStructure);
+                        start = positionOffset,
+                        end = positionOffset +
+                              get_calypso_node_size(contract_key, OpusContractStructure) - 1;
+                        int decimal_value = bit_slice_to_dec(bit_representation, start, end);
+                        card->opus->contracts[i - 1].sale_date.hour = (decimal_value * 60) / 3600;
+                        card->opus->contracts[i - 1].sale_date.minute =
+                            ((decimal_value * 60) % 3600) / 60;
+                        card->opus->contracts[i - 1].sale_date.second =
+                            ((decimal_value * 60) % 3600) % 60;
+                    }
+
+                    // Free the calypso structure
+                    free_calypso_structure(OpusContractStructure);
+
+                    // Select app for events
+                    error = select_new_app(
+                        0x20, 0x10, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                    if(error != 0) {
+                        break;
+                    }
+
+                    // Check the response after selecting app
+                    if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
+                        break;
+                    }
+
+                    // Load the calypso structure for events
+                    CalypsoApp* OpusEventStructure = get_opus_event_structure();
+                    if(!OpusEventStructure) {
+                        FURI_LOG_E(TAG, "Failed to load Opus Event structure");
+                        break;
+                    }
+
+                    // Now send the read command for events
+                    for(size_t i = 1; i < 4; i++) {
+                        error = read_new_file(
+                            i, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                        if(error != 0) {
+                            break;
+                        }
+
+                        // Check the response after reading the file
+                        if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
+                            break;
+                        }
+
+                        char event_bit_representation[response_length * 8 + 1];
+                        event_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(
+                                event_bit_representation, bits, sizeof(event_bit_representation));
+                        }
+
+                        // EventServiceProvider
+                        const char* event_key = "EventServiceProvider";
+                        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].service_provider =
+                                bit_slice_to_dec(event_bit_representation, start, end);
+                        }
+
+                        // EventRouteNumber
+                        event_key = "EventRouteNumber";
+                        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_number =
+                                bit_slice_to_dec(event_bit_representation, start, end);
+                            card->opus->events[i - 1].route_number_available = true;
+                        }
+
+                        // EventContractPointer
+                        event_key = "EventContractPointer";
+                        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].used_contract =
+                                bit_slice_to_dec(event_bit_representation, start, end);
+                            card->opus->events[i - 1].used_contract_available = true;
+                        }
+
+                        // EventDate + EventTime
+                        event_key = "EventDate";
+                        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;
+                        uint64_t date_timestamp =
+                            (bit_slice_to_dec(event_bit_representation, start, end) +
+                             (float)epoch) +
+                            3600;
+                        datetime_timestamp_to_datetime(
+                            date_timestamp, &card->opus->events[i - 1].date);
+
+                        event_key = "EventTime";
+                        positionOffset = get_calypso_node_offset(
+                            event_bit_representation, event_key, OpusEventStructure);
+                        start = positionOffset,
+                        end = positionOffset +
+                              get_calypso_node_size(event_key, OpusEventStructure) - 1;
+                        int decimal_value = bit_slice_to_dec(event_bit_representation, start, end);
+                        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;
+                    }
+
+                    // Free the calypso structure
+                    free_calypso_structure(OpusEventStructure);
+
+                    break;
+                }
+                case CALYPSO_CARD_UNKNOWN: {
+                    start = 3;
+                    end = 6;
+                    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 =
+                        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);
+                    if(card->card_type == CALYPSO_CARD_RAVKAV) {
+                    }
+                    break;
+                }
+                default:
+                    break;
+                }
+
+                widget_add_text_scroll_element(
+                    widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
+
+                CalypsoContext* context = malloc(sizeof(CalypsoContext));
+                context->card = card;
+                context->page_id = 0;
+                context->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
+                app->calypso_context = context;
+
+                // Ensure no nested mutexes
+                furi_mutex_acquire(context->mutex, FuriWaitForever);
+                update_page_info(app, parsed_data);
+                furi_mutex_release(context->mutex);
+
+                widget_add_text_scroll_element(
+                    widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
+
+                // Ensure no nested mutexes
+                furi_mutex_acquire(context->mutex, FuriWaitForever);
+                update_widget_elements(app);
+                furi_mutex_release(context->mutex);
+
+                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_navigo_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_navigo_poller_callback, app);
+
+    metroflip_app_blink_start(app);
+}
+
+bool metroflip_scene_navigo_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_navigo_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);
+
+    if(app->calypso_context) {
+        CalypsoContext* ctx = app->calypso_context;
+        free(ctx->card->navigo);
+        free(ctx->card->opus);
+        free(ctx->card);
+        furi_mutex_free(ctx->mutex);
+        free(ctx);
+        app->calypso_context = NULL;
+    }
+}

+ 10 - 0
scenes/metroflip_scene_calypso.h

@@ -0,0 +1,10 @@
+#include <gui/gui.h>
+#include <gui/modules/widget_elements/widget_element.h>
+#include "../api/calypso/transit/navigo.h"
+#include "../api/calypso/transit/opus.h"
+#include "../api/calypso/calypso_i.h"
+#include <datetime.h>
+#include <stdbool.h>
+
+void metroflip_back_button_widget_callback(GuiButtonType result, InputType type, void* context);
+void metroflip_next_button_widget_callback(GuiButtonType result, InputType type, void* context);

+ 1 - 1
scenes/metroflip_scene_config.h

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

+ 1 - 0
scenes/metroflip_scene_credits.c

@@ -18,6 +18,7 @@ void metroflip_scene_credits_on_enter(void* context) {
     furi_string_cat_printf(str, "\e#Parser Credits:\n\n");
     furi_string_cat_printf(str, "Rav-Kav Parser: luu176\n\n");
     furi_string_cat_printf(str, "Navigo Parser: \n luu176, DocSystem \n\n");
+    furi_string_cat_printf(str, "Opus Parser: DocSystem\n\n");
     furi_string_cat_printf(str, "Metromoney Parser:\n Leptopt1los\n\n");
     furi_string_cat_printf(str, "Bip! Parser:\n rbasoalto, gornekich\n\n");
     furi_string_cat_printf(str, "CharlieCard Parser:\n zacharyweiss\n\n");

+ 0 - 1574
scenes/metroflip_scene_navigo.c

@@ -1,1574 +0,0 @@
-#include "../metroflip_i.h"
-#include <datetime.h>
-#include <dolphin/dolphin.h>
-#include <notification/notification_messages.h>
-#include <locale/locale.h>
-#include "navigo.h"
-
-#include <nfc/protocols/iso14443_4b/iso14443_4b_poller.h>
-
-#define TAG "Metroflip:Scene:Navigo"
-
-int select_new_app(
-    int new_app_directory,
-    int new_app,
-    BitBuffer* tx_buffer,
-    BitBuffer* rx_buffer,
-    Iso14443_4bPoller* iso14443_4b_poller,
-    Metroflip* app,
-    MetroflipPollerEventType* stage) {
-    select_app[5] = new_app_directory;
-    select_app[6] = new_app;
-
-    bit_buffer_reset(tx_buffer);
-    bit_buffer_append_bytes(tx_buffer, select_app, sizeof(select_app));
-    FURI_LOG_D(
-        TAG,
-        "SEND %02x %02x %02x %02x %02x %02x %02x %02x",
-        select_app[0],
-        select_app[1],
-        select_app[2],
-        select_app[3],
-        select_app[4],
-        select_app[5],
-        select_app[6],
-        select_app[7]);
-    int error = iso14443_4b_poller_send_block(iso14443_4b_poller, tx_buffer, rx_buffer);
-    if(error != Iso14443_4bErrorNone) {
-        FURI_LOG_I(TAG, "Select File: iso14443_4b_poller_send_block error %d", error);
-        *stage = MetroflipPollerEventTypeFail;
-        view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventPollerFail);
-        return error;
-    }
-    return 0;
-}
-
-int read_new_file(
-    int new_file,
-    BitBuffer* tx_buffer,
-    BitBuffer* rx_buffer,
-    Iso14443_4bPoller* iso14443_4b_poller,
-    Metroflip* app,
-    MetroflipPollerEventType* stage) {
-    read_file[2] = new_file;
-    bit_buffer_reset(tx_buffer);
-    bit_buffer_append_bytes(tx_buffer, read_file, sizeof(read_file));
-    FURI_LOG_D(
-        TAG,
-        "SEND %02x %02x %02x %02x %02x",
-        read_file[0],
-        read_file[1],
-        read_file[2],
-        read_file[3],
-        read_file[4]);
-    Iso14443_4bError error =
-        iso14443_4b_poller_send_block(iso14443_4b_poller, tx_buffer, rx_buffer);
-    if(error != Iso14443_4bErrorNone) {
-        FURI_LOG_I(TAG, "Read File: iso14443_4b_poller_send_block error %d", error);
-        *stage = MetroflipPollerEventTypeFail;
-        view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventPollerFail);
-        return error;
-    }
-    return 0;
-}
-
-int check_response(
-    BitBuffer* rx_buffer,
-    Metroflip* app,
-    MetroflipPollerEventType* stage,
-    size_t* response_length) {
-    *response_length = bit_buffer_get_size_bytes(rx_buffer);
-    if(bit_buffer_get_byte(rx_buffer, *response_length - 2) != apdu_success[0] ||
-       bit_buffer_get_byte(rx_buffer, *response_length - 1) != apdu_success[1]) {
-        int error_code_1 = bit_buffer_get_byte(rx_buffer, *response_length - 2);
-        int error_code_2 = bit_buffer_get_byte(rx_buffer, *response_length - 1);
-        FURI_LOG_E(TAG, "Select profile app/file failed: %02x%02x", error_code_1, error_code_2);
-        if(error_code_1 == 0x6a && error_code_2 == 0x82) {
-            FURI_LOG_E(TAG, "Wrong parameter(s) P1-P2 - File not found");
-        } else if(error_code_1 == 0x69 && error_code_2 == 0x82) {
-            FURI_LOG_E(TAG, "Command not allowed - Security status not satisfied");
-        }
-        *stage = MetroflipPollerEventTypeFail;
-        view_dispatcher_send_custom_event(
-            app->view_dispatcher, MetroflipCustomEventPollerFileNotFound);
-        return 1;
-    }
-    return 0;
-}
-
-const char* get_country(int country_num) {
-    switch(country_num) {
-    case 250:
-        return "France";
-    case 124:
-        return "Canada";
-    default: {
-        char* country = malloc(4 * sizeof(char));
-        snprintf(country, 4, "%d", country_num);
-        return country;
-    }
-    }
-}
-
-const char* get_network(int country_num, int network_num) {
-    switch(country_num) {
-    case 250:
-        switch(network_num) {
-        case 901:
-            return "IDFM";
-        default:
-            return "Unknown";
-        }
-    case 124:
-        switch(network_num) {
-        case 1:
-            return "STM";
-        default:
-            return "Unknown";
-        }
-    default:
-        return "Unknown";
-    }
-}
-
-const char* get_transport_type(int type) {
-    switch(type) {
-    case BUS_URBAIN:
-        return "Bus Urbain";
-    case BUS_INTERURBAIN:
-        return "Bus Interurbain";
-    case METRO:
-        return "Metro";
-    case TRAM:
-        return "Tram";
-    case TRAIN:
-        return "Train";
-    case PARKING:
-        return "Parking";
-    default:
-        return "Unknown";
-    }
-}
-
-const char* get_service_provider(int provider) {
-    switch(provider) {
-    case 2:
-        return "SNCF";
-    case 3:
-        return "RATP";
-    case 4:
-        return "IDF Mobilites";
-    case 10:
-        return "IDF Mobilites";
-    case 115:
-        return "CSO (VEOLIA)";
-    case 116:
-        return "R'Bus (VEOLIA)";
-    case 156:
-        return "Phebus";
-    case 175:
-        return "RATP (Veolia Transport Nanterre)";
-    default:
-        return "Unknown";
-    }
-}
-
-const char* get_transition_type(int transition) {
-    switch(transition) {
-    case 1:
-        return "Entry";
-    case 2:
-        return "Exit";
-    case 4:
-        return "Controle volant (a bord)";
-    case 5:
-        return "Test validation";
-    case 6:
-        return "Interchange - Entry";
-    case 7:
-        return "Interchange - Exit";
-    case 9:
-        return "Validation cancelled";
-    case 10:
-        return "Entry";
-    case 11:
-        return "Exit";
-    case 13:
-        return "Distribution";
-    case 15:
-        return "Invalidation";
-    default: {
-        char* transition_str = malloc(6 * sizeof(char));
-        snprintf(transition_str, 6, "%d", transition);
-        return transition_str;
-    }
-    }
-}
-
-const char* get_navigo_type(int type) {
-    switch(type) {
-    case NAVIGO_EASY:
-        return "Navigo Easy";
-    case NAVIGO_DECOUVERTE:
-        return "Navigo Decouverte";
-    case NAVIGO_STANDARD:
-        return "Navigo Standard";
-    case NAVIGO_INTEGRAL:
-        return "Navigo Integral";
-    case IMAGINE_R:
-        return "Imagine R";
-    default:
-        return "Navigo";
-    }
-}
-
-const char* get_tariff(int tariff) {
-    switch(tariff) {
-    case 0x0000:
-        return "Navigo Mois";
-    case 0x0001:
-        return "Navigo Semaine";
-    case 0x0002:
-        return "Navigo Annuel";
-    case 0x0003:
-        return "Navigo Jour";
-    case 0x0004:
-        return "Imagine R Junior";
-    case 0x0005:
-        return "Imagine R Etudiant";
-    case 0x000D:
-        return "Navigo Jeunes Week-end";
-    case 0x0015:
-        return "Paris-Visite"; // Theoric
-    case 0x1000:
-        return "Navigo Liberte+";
-    case 0x4000:
-        return "Navigo Mois 75%%";
-    case 0x4001:
-        return "Navigo Semaine 75%%";
-    case 0x4015:
-        return "Paris-Visite (Enfant)"; // Theoric
-    case 0x5000:
-        return "Tickets T+";
-    case 0x5004:
-        return "Tickets OrlyBus"; // Theoric
-    case 0x5005:
-        return "Tickets RoissyBus"; // Theoric
-    case 0x5006:
-        return "Bus-Tram"; // Theoric
-    case 0x5008:
-        return "Metro-Train-RER"; // Theoric
-    case 0x500b:
-        return "Paris <> Aeroports"; // Theoric
-    case 0x5010:
-        return "Tickets T+ (Reduit)"; // Theoric
-    case 0x5016:
-        return "Bus-Tram (Reduit)"; // Theoric
-    case 0x5018:
-        return "Metro-Train-RER (Reduit)"; // Theoric
-    case 0x501b:
-        return "Paris <> Aeroports (Reduit)"; // Theoric
-    case 0x8003:
-        return "Navigo Solidarite Gratuit";
-    default: {
-        char* tariff_str = malloc(6 * sizeof(char));
-        snprintf(tariff_str, 6, "%d", tariff);
-        return tariff_str;
-    }
-    }
-}
-
-bool is_ticket_count_available(int tariff) {
-    return tariff >= 0x5000 && tariff <= 0x501b;
-}
-
-const char* get_pay_method(int pay_method) {
-    switch(pay_method) {
-    case 0x30:
-        return "Apple Pay";
-    case 0x80:
-        return "Debit PME";
-    case 0x90:
-        return "Cash";
-    case 0xA0:
-        return "Mobility Check";
-    case 0xB3:
-        return "Payment Card";
-    case 0xA4:
-        return "Check";
-    case 0xA5:
-        return "Vacation Check";
-    case 0xB7:
-        return "Telepayment";
-    case 0xD0:
-        return "Remote Payment";
-    case 0xD7:
-        return "Voucher, Prepayment, Exchange Voucher, Travel Voucher";
-    case 0xD9:
-        return "Discount Voucher";
-    default:
-        return "Unknown";
-    }
-}
-
-const char* get_zones(int* zones) {
-    if(zones[0] && zones[4]) {
-        return "All Zones (1-5)";
-    } else if(zones[0] && zones[3]) {
-        return "Zones 1-4";
-    } else if(zones[0] && zones[2]) {
-        return "Zones 1-3";
-    } else if(zones[0] && zones[1]) {
-        return "Zones 1-2";
-    } else if(zones[0]) {
-        return "Zone 1";
-    } else if(zones[1] && zones[4]) {
-        return "Zones 2-5";
-    } else if(zones[1] && zones[3]) {
-        return "Zones 2-4";
-    } else if(zones[1] && zones[2]) {
-        return "Zones 2-3";
-    } else if(zones[1]) {
-        return "Zone 2";
-    } else if(zones[2] && zones[4]) {
-        return "Zones 3-5";
-    } else if(zones[2] && zones[3]) {
-        return "Zones 3-4";
-    } else if(zones[2]) {
-        return "Zone 3";
-    } else if(zones[3] && zones[4]) {
-        return "Zones 4-5";
-    } else if(zones[3]) {
-        return "Zone 4";
-    } else if(zones[4]) {
-        return "Zone 5";
-    } else {
-        return "Unknown";
-    }
-}
-
-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;
-}
-
-const char* get_metro_station(int station_group_id, int station_id) {
-    // Use NAVIGO_H constants
-    if(station_group_id < 32 && station_id < 16) {
-        return METRO_STATION_LIST[station_group_id][station_id];
-    }
-    // cast station_group_id-station_id to a string
-    char* station = malloc(12 * sizeof(char));
-    if(!station) {
-        return "Unknown";
-    }
-    snprintf(station, 10, "%d-%d", station_group_id, station_id);
-    return station;
-}
-
-const char* get_train_line(int station_group_id) {
-    if(station_group_id < 77) {
-        return TRAIN_LINES_LIST[station_group_id];
-    }
-    return "Unknown";
-}
-
-const char* get_train_station(int station_group_id, int station_id) {
-    if(station_group_id < 77 && station_id < 19) {
-        return TRAIN_STATION_LIST[station_group_id][station_id];
-    }
-    // cast station_group_id-station_id to a string
-    char* station = malloc(12 * sizeof(char));
-    if(!station) {
-        return "Unknown";
-    }
-    snprintf(station, 10, "%d-%d", station_group_id, station_id);
-    return station;
-}
-
-const char* get_tram_line(int route_number) {
-    switch(route_number) {
-    case 16:
-        return "T6";
-    default: {
-        char* line = malloc(3 * sizeof(char));
-        if(!line) {
-            return "Unknown";
-        }
-        snprintf(line, 3, "T%d", route_number);
-        return line;
-    }
-    }
-}
-
-void show_event_info(
-    NavigoCardEvent* event,
-    NavigoCardContract* contracts,
-    FuriString* parsed_data) {
-    if(event->used_contract == 0) {
-        furi_string_cat_printf(parsed_data, "No event data\n");
-        return;
-    }
-    if(event->transport_type == BUS_URBAIN || event->transport_type == BUS_INTERURBAIN ||
-       event->transport_type == METRO || event->transport_type == TRAM) {
-        if(event->route_number_available) {
-            if(event->transport_type == METRO && event->route_number == 103) {
-                furi_string_cat_printf(
-                    parsed_data,
-                    "%s 3 bis\n%s\n",
-                    get_transport_type(event->transport_type),
-                    get_transition_type(event->transition));
-            } else if(event->transport_type == TRAM) {
-                furi_string_cat_printf(
-                    parsed_data,
-                    "%s %s\n%s\n",
-                    get_transport_type(event->transport_type),
-                    get_tram_line(event->route_number),
-                    get_transition_type(event->transition));
-            } else {
-                furi_string_cat_printf(
-                    parsed_data,
-                    "%s %d\n%s\n",
-                    get_transport_type(event->transport_type),
-                    event->route_number,
-                    get_transition_type(event->transition));
-            }
-        } else {
-            furi_string_cat_printf(
-                parsed_data,
-                "%s\n%s\n",
-                get_transport_type(event->transport_type),
-                get_transition_type(event->transition));
-        }
-        furi_string_cat_printf(
-            parsed_data, "Transporter: %s\n", get_service_provider(event->service_provider));
-        if(event->transport_type == METRO) {
-            furi_string_cat_printf(
-                parsed_data,
-                "Station: %s\nSector: %s\n",
-                get_metro_station(event->station_group_id, event->station_id),
-                get_metro_station(event->station_group_id, 0));
-        } else {
-            furi_string_cat_printf(
-                parsed_data, "Station ID: %d-%d\n", event->station_group_id, event->station_id);
-        }
-        if(event->location_gate_available) {
-            furi_string_cat_printf(parsed_data, "Gate: %d\n", event->location_gate);
-        }
-        if(event->device_available) {
-            if(event->transport_type == BUS_URBAIN || event->transport_type == BUS_INTERURBAIN) {
-                const char* side = event->side == 0 ? "right" : "left";
-                furi_string_cat_printf(parsed_data, "Door: %d\nSide: %s\n", event->door, side);
-            } else {
-                furi_string_cat_printf(parsed_data, "Device: %d\n", event->device);
-            }
-        }
-        if(event->mission_available) {
-            furi_string_cat_printf(parsed_data, "Mission: %d\n", event->mission);
-        }
-        if(event->vehicle_id_available) {
-            furi_string_cat_printf(parsed_data, "Vehicle: %d\n", event->vehicle_id);
-        }
-        if(event->used_contract_available) {
-            furi_string_cat_printf(
-                parsed_data,
-                "Contract: %d - %s\n",
-                event->used_contract,
-                get_tariff(contracts[event->used_contract - 1].tariff));
-        }
-        locale_format_datetime_cat(parsed_data, &event->date, true);
-        furi_string_cat_printf(parsed_data, "\n");
-    } else if(event->transport_type == TRAIN) {
-        if(event->route_number_available) {
-            furi_string_cat_printf(
-                parsed_data,
-                "RER %c\n%s\n",
-                (65 + event->route_number - 17),
-                get_transition_type(event->transition));
-        } else {
-            furi_string_cat_printf(
-                parsed_data,
-                "%s %s\n%s\n",
-                get_transport_type(event->transport_type),
-                get_train_line(event->station_group_id),
-                get_transition_type(event->transition));
-        }
-        furi_string_cat_printf(
-            parsed_data, "Transporter: %s\n", get_service_provider(event->service_provider));
-        furi_string_cat_printf(
-            parsed_data,
-            "Station: %s\n",
-            get_train_station(event->station_group_id, event->station_id));
-        /* if(event->route_number_available) {
-            furi_string_cat_printf(parsed_data, "Route: %d\n", event->route_number);
-        } */
-        if(event->location_gate_available) {
-            furi_string_cat_printf(parsed_data, "Gate: %d\n", event->location_gate);
-        }
-        if(event->device_available) {
-            if(event->service_provider == 2) {
-                furi_string_cat_printf(parsed_data, "Device: %d\n", event->device & 0xFF);
-            } else {
-                furi_string_cat_printf(parsed_data, "Device: %d\n", event->device);
-            }
-        }
-        if(event->mission_available) {
-            furi_string_cat_printf(parsed_data, "Mission: %d\n", event->mission);
-        }
-        if(event->vehicle_id_available) {
-            furi_string_cat_printf(parsed_data, "Vehicle: %d\n", event->vehicle_id);
-        }
-        if(event->used_contract_available) {
-            furi_string_cat_printf(
-                parsed_data,
-                "Contract: %d - %s\n",
-                event->used_contract,
-                get_tariff(contracts[event->used_contract - 1].tariff));
-        }
-        locale_format_datetime_cat(parsed_data, &event->date, true);
-        furi_string_cat_printf(parsed_data, "\n");
-    } else {
-        furi_string_cat_printf(
-            parsed_data,
-            "%s - %s\n",
-            get_transport_type(event->transport_type),
-            get_transition_type(event->transition));
-        furi_string_cat_printf(
-            parsed_data, "Transporter: %s\n", get_service_provider(event->service_provider));
-        furi_string_cat_printf(
-            parsed_data, "Station ID: %d-%d\n", event->station_group_id, event->station_id);
-        if(event->location_gate_available) {
-            furi_string_cat_printf(parsed_data, "Gate: %d\n", event->location_gate);
-        }
-        if(event->device_available) {
-            furi_string_cat_printf(parsed_data, "Device: %d\n", event->device);
-        }
-        if(event->mission_available) {
-            furi_string_cat_printf(parsed_data, "Mission: %d\n", event->mission);
-        }
-        if(event->vehicle_id_available) {
-            furi_string_cat_printf(parsed_data, "Vehicle: %d\n", event->vehicle_id);
-        }
-        if(event->used_contract_available) {
-            furi_string_cat_printf(
-                parsed_data,
-                "Contract: %d - %s\n",
-                event->used_contract,
-                get_tariff(contracts[event->used_contract - 1].tariff));
-        }
-        locale_format_datetime_cat(parsed_data, &event->date, true);
-        furi_string_cat_printf(parsed_data, "\n");
-    }
-}
-
-void show_contract_info(NavigoCardContract* contract, FuriString* parsed_data) {
-    furi_string_cat_printf(parsed_data, "Type: %s\n", get_tariff(contract->tariff));
-    if(is_ticket_count_available(contract->tariff)) {
-        furi_string_cat_printf(parsed_data, "Remaining Tickets: %d\n", contract->counter.count);
-    }
-    if(contract->serial_number_available) {
-        furi_string_cat_printf(parsed_data, "TCN Number: %d\n", contract->serial_number);
-    }
-    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");
-    }
-    if(contract->zones_available) {
-        furi_string_cat_printf(parsed_data, "%s\n", get_zones(contract->zones));
-    }
-    furi_string_cat_printf(parsed_data, "Sold on: ");
-    locale_format_datetime_cat(parsed_data, &contract->sale_date, false);
-    furi_string_cat_printf(parsed_data, "\n");
-    furi_string_cat_printf(
-        parsed_data, "Sales Agent: %s\n", get_service_provider(contract->sale_agent));
-    furi_string_cat_printf(parsed_data, "Sales Terminal: %d\n", contract->sale_device);
-    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, "Authenticity Code: %d\n", contract->authenticator);
-}
-
-void show_environment_info(NavigoCardEnv* environment, FuriString* parsed_data) {
-    furi_string_cat_printf(
-        parsed_data,
-        "App Version: %s - v%d\n",
-        get_intercode_version(environment->app_version),
-        get_intercode_subversion(environment->app_version));
-    furi_string_cat_printf(parsed_data, "Country: %s\n", get_country(environment->country_num));
-    furi_string_cat_printf(
-        parsed_data,
-        "Network: %s\n",
-        get_network(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, "\n");
-}
-
-void update_page_info(void* context, FuriString* parsed_data) {
-    Metroflip* app = context;
-    NavigoContext* ctx = app->navigo_context;
-    if(ctx->page_id == 0) {
-        furi_string_cat_printf(
-            parsed_data,
-            "\e#%s %u:\n",
-            get_navigo_type(ctx->card->holder.card_status),
-            ctx->card->card_number);
-        furi_string_cat_printf(parsed_data, "\e#Contract 1:\n");
-        show_contract_info(&ctx->card->contracts[0], parsed_data);
-    } else if(ctx->page_id == 1) {
-        furi_string_cat_printf(
-            parsed_data,
-            "\e#%s %u:\n",
-            get_navigo_type(ctx->card->holder.card_status),
-            ctx->card->card_number);
-        furi_string_cat_printf(parsed_data, "\e#Contract 2:\n");
-        show_contract_info(&ctx->card->contracts[1], parsed_data);
-    } else if(ctx->page_id == 2) {
-        furi_string_cat_printf(
-            parsed_data,
-            "\e#%s %u:\n",
-            get_navigo_type(ctx->card->holder.card_status),
-            ctx->card->card_number);
-        furi_string_cat_printf(parsed_data, "\e#Contract 3:\n");
-        show_contract_info(&ctx->card->contracts[2], parsed_data);
-    } else if(ctx->page_id == 3) {
-        furi_string_cat_printf(
-            parsed_data,
-            "\e#%s %u:\n",
-            get_navigo_type(ctx->card->holder.card_status),
-            ctx->card->card_number);
-        furi_string_cat_printf(parsed_data, "\e#Contract 4:\n");
-        show_contract_info(&ctx->card->contracts[3], parsed_data);
-    } else if(ctx->page_id == 4) {
-        furi_string_cat_printf(parsed_data, "\e#Environment:\n");
-        show_environment_info(&ctx->card->environment, parsed_data);
-    } else if(ctx->page_id == 5) {
-        furi_string_cat_printf(parsed_data, "\e#Event 1:\n");
-        show_event_info(&ctx->card->events[0], ctx->card->contracts, parsed_data);
-    } else if(ctx->page_id == 6) {
-        furi_string_cat_printf(parsed_data, "\e#Event 2:\n");
-        show_event_info(&ctx->card->events[1], ctx->card->contracts, parsed_data);
-    } else if(ctx->page_id == 7) {
-        furi_string_cat_printf(parsed_data, "\e#Event 3:\n");
-        show_event_info(&ctx->card->events[2], ctx->card->contracts, parsed_data);
-    }
-}
-
-void update_widget_elements(void* context) {
-    Metroflip* app = context;
-    NavigoContext* ctx = app->navigo_context;
-    Widget* widget = app->widget;
-    if(ctx->page_id < 5) {
-        widget_add_button_element(
-            widget, GuiButtonTypeRight, "Next", metroflip_next_button_widget_callback, context);
-    } else {
-        widget_add_button_element(
-            widget, GuiButtonTypeRight, "Exit", metroflip_next_button_widget_callback, context);
-    }
-    if(ctx->page_id > 0) {
-        widget_add_button_element(
-            widget, GuiButtonTypeLeft, "Back", metroflip_back_button_widget_callback, context);
-    }
-}
-
-void metroflip_back_button_widget_callback(GuiButtonType result, InputType type, void* context) {
-    Metroflip* app = context;
-    NavigoContext* ctx = app->navigo_context;
-    UNUSED(result);
-
-    Widget* widget = app->widget;
-
-    if(type == InputTypePress) {
-        widget_reset(widget);
-
-        FURI_LOG_I(TAG, "Page ID: %d -> %d", ctx->page_id, ctx->page_id - 1);
-
-        if(ctx->page_id > 0) {
-            if(ctx->page_id == 4 && ctx->card->contracts[3].present == 0) {
-                ctx->page_id -= 1;
-            }
-            if(ctx->page_id == 3 && ctx->card->contracts[2].present == 0) {
-                ctx->page_id -= 1;
-            }
-            if(ctx->page_id == 2 && ctx->card->contracts[1].present == 0) {
-                ctx->page_id -= 1;
-            }
-            ctx->page_id -= 1;
-        }
-
-        FuriString* parsed_data = furi_string_alloc();
-
-        // Ensure no nested mutexes
-        furi_mutex_acquire(ctx->mutex, FuriWaitForever);
-        update_page_info(app, parsed_data);
-        furi_mutex_release(ctx->mutex);
-
-        widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
-        // widget_add_icon_element(widget, 0, 0, &I_RFIDDolphinReceive_97x61);
-
-        // Ensure no nested mutexes
-        furi_mutex_acquire(ctx->mutex, FuriWaitForever);
-        update_widget_elements(app);
-        furi_mutex_release(ctx->mutex);
-
-        furi_string_free(parsed_data);
-    }
-}
-
-void metroflip_next_button_widget_callback(GuiButtonType result, InputType type, void* context) {
-    Metroflip* app = context;
-    NavigoContext* ctx = app->navigo_context;
-    UNUSED(result);
-
-    Widget* widget = app->widget;
-
-    if(type == InputTypePress) {
-        widget_reset(widget);
-
-        FURI_LOG_I(TAG, "Page ID: %d -> %d", ctx->page_id, ctx->page_id + 1);
-
-        if(ctx->page_id < 7) {
-            if(ctx->page_id == 0 && ctx->card->contracts[1].present == 0) {
-                ctx->page_id += 1;
-            }
-            if(ctx->page_id == 1 && ctx->card->contracts[2].present == 0) {
-                ctx->page_id += 1;
-            }
-            if(ctx->page_id == 2 && ctx->card->contracts[3].present == 0) {
-                ctx->page_id += 1;
-            }
-            ctx->page_id += 1;
-        } else {
-            ctx->page_id = 0;
-            scene_manager_search_and_switch_to_previous_scene(
-                app->scene_manager, MetroflipSceneStart);
-            return;
-        }
-
-        FuriString* parsed_data = furi_string_alloc();
-
-        // Ensure no nested mutexes
-        furi_mutex_acquire(ctx->mutex, FuriWaitForever);
-        update_page_info(app, parsed_data);
-        furi_mutex_release(ctx->mutex);
-
-        widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
-
-        // Ensure no nested mutexes
-        furi_mutex_acquire(ctx->mutex, FuriWaitForever);
-        update_widget_elements(app);
-        furi_mutex_release(ctx->mutex);
-
-        furi_string_free(parsed_data);
-    }
-}
-
-void delay(int milliseconds) {
-    furi_thread_flags_wait(0, FuriFlagWaitAny, milliseconds);
-}
-
-static NfcCommand metroflip_scene_navigo_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) {
-            // Start Flipper vibration
-            NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
-            notification_message(notification, &sequence_set_vibro_on);
-            delay(50);
-            notification_message(notification, &sequence_reset_vibro);
-            nfc_device_set_data(
-                app->nfc_device, NfcProtocolIso14443_4b, nfc_poller_get_data(app->poller));
-
-            Iso14443_4bError error;
-            size_t response_length = 0;
-
-            do {
-                // Initialize the card data
-                NavigoCardData* card = malloc(sizeof(NavigoCardData));
-
-                // Select app ICC
-                error = select_new_app(
-                    0x00, 0x02, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
-                if(error != 0) {
-                    break;
-                }
-
-                // Check the response after selecting app
-                if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
-                    break;
-                }
-
-                // Now send the read command for ICC
-                error = read_new_file(0x01, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
-                if(error != 0) {
-                    break;
-                }
-
-                // Check the response after reading the file
-                if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
-                    break;
-                }
-
-                char icc_bit_representation[response_length * 8 + 1];
-                icc_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(icc_bit_representation, bits, sizeof(icc_bit_representation));
-                }
-                icc_bit_representation[response_length * 8] = '\0';
-
-                int start = 128, end = 159;
-                card->card_number = bit_slice_to_dec(icc_bit_representation, start, end);
-
-                // Select app for ticketing
-                error = select_new_app(
-                    0x20, 0x00, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
-                if(error != 0) {
-                    FURI_LOG_E(TAG, "Failed to select app for ticketing");
-                    break;
-                }
-
-                // Check the response after selecting app
-                if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
-                    FURI_LOG_E(TAG, "Failed to check response after selecting app for ticketing");
-                    break;
-                }
-
-                // Select app for contracts
-                error = select_new_app(
-                    0x20, 0x20, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
-                if(error != 0) {
-                    FURI_LOG_E(TAG, "Failed to select app for contracts");
-                    break;
-                }
-
-                // Check the response after selecting app
-                if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
-                    FURI_LOG_E(TAG, "Failed to check response after selecting app for contracts");
-                    break;
-                }
-
-                // Prepare calypso structure
-                CalypsoApp* NavigoContractStructure = get_navigo_contract_structure();
-                if(!NavigoContractStructure) {
-                    FURI_LOG_E(TAG, "Failed to load Navigo Contract structure");
-                    break;
-                }
-
-                // Now send the read command for contracts
-                for(size_t i = 1; i < 5; i++) {
-                    error =
-                        read_new_file(i, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
-                    if(error != 0) {
-                        FURI_LOG_E(TAG, "Failed to read contract %d", i);
-                        break;
-                    }
-
-                    // Check the response after reading the file
-                    if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
-                        FURI_LOG_E(TAG, "Failed to check response after reading contract %d", i);
-                        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));
-                    }
-                    bit_representation[response_length * 8] = '\0';
-
-                    /* int count = 0;
-                    int start = 0, end = NavigoContractStructure->elements[0].bitmap->size;
-                    char bit_slice[end - start + 1];
-                    strncpy(bit_slice, bit_representation + start, end - start);
-                    bit_slice[end - start] = '\0';
-                    int* positions = get_bit_positions(bit_slice, &count);
-
-                    FURI_LOG_I(TAG, "Contract %d bit positions: %d", i, count);
-
-                    // print positions
-                    for(int i = 0; i < count; i++) {
-                        char* key =
-                            (NavigoContractStructure->elements[0]
-                                         .bitmap->elements[positions[i]]
-                                         .type == CALYPSO_ELEMENT_TYPE_FINAL ?
-                                 NavigoContractStructure->elements[0]
-                                     .bitmap->elements[positions[i]]
-                                     .final->key :
-                                 NavigoContractStructure->elements[0]
-                                     .bitmap->elements[positions[i]]
-                                     .bitmap->key);
-                        int offset = get_calypso_node_offset(
-                            bit_representation, key, NavigoContractStructure);
-                        FURI_LOG_I(
-                            TAG, "Position: %d, Key: %s, Offset: %d", positions[i], key, offset);
-                    } */
-
-                    if(bit_slice_to_dec(
-                           bit_representation,
-                           0,
-                           NavigoContractStructure->elements[0].bitmap->size - 1) == 0) {
-                        break;
-                    }
-
-                    card->contracts[i - 1].present = 1;
-
-                    // 2. ContractTariff
-                    const char* contract_key = "ContractTariff";
-                    if(is_calypso_node_present(
-                           bit_representation, contract_key, NavigoContractStructure)) {
-                        int positionOffset = get_calypso_node_offset(
-                            bit_representation, contract_key, NavigoContractStructure);
-                        int start = positionOffset,
-                            end = positionOffset +
-                                  get_calypso_node_size(contract_key, NavigoContractStructure) - 1;
-                        card->contracts[i - 1].tariff =
-                            bit_slice_to_dec(bit_representation, start, end);
-                    }
-
-                    // 3. ContractSerialNumber
-                    contract_key = "ContractSerialNumber";
-                    if(is_calypso_node_present(
-                           bit_representation, contract_key, NavigoContractStructure)) {
-                        int positionOffset = get_calypso_node_offset(
-                            bit_representation, contract_key, NavigoContractStructure);
-                        int start = positionOffset,
-                            end = positionOffset +
-                                  get_calypso_node_size(contract_key, NavigoContractStructure) - 1;
-                        card->contracts[i - 1].serial_number =
-                            bit_slice_to_dec(bit_representation, start, end);
-                        card->contracts[i - 1].serial_number_available = true;
-                    }
-
-                    // 8. ContractPayMethod
-                    contract_key = "ContractPayMethod";
-                    if(is_calypso_node_present(
-                           bit_representation, contract_key, NavigoContractStructure)) {
-                        int positionOffset = get_calypso_node_offset(
-                            bit_representation, contract_key, NavigoContractStructure);
-                        int start = positionOffset,
-                            end = positionOffset +
-                                  get_calypso_node_size(contract_key, NavigoContractStructure) - 1;
-                        card->contracts[i - 1].pay_method =
-                            bit_slice_to_dec(bit_representation, start, end);
-                        card->contracts[i - 1].pay_method_available = true;
-                    }
-
-                    // 10. ContractPriceAmount
-                    contract_key = "ContractPriceAmount";
-                    if(is_calypso_node_present(
-                           bit_representation, contract_key, NavigoContractStructure)) {
-                        int positionOffset = get_calypso_node_offset(
-                            bit_representation, contract_key, NavigoContractStructure);
-                        int start = positionOffset,
-                            end = positionOffset +
-                                  get_calypso_node_size(contract_key, NavigoContractStructure) - 1;
-                        card->contracts[i - 1].price_amount =
-                            bit_slice_to_dec(bit_representation, start, end) / 100.0;
-                        card->contracts[i - 1].price_amount_available = true;
-                    }
-
-                    // 13.0. ContractValidityStartDate
-                    contract_key = "ContractValidityStartDate";
-                    if(is_calypso_node_present(
-                           bit_representation, contract_key, NavigoContractStructure)) {
-                        int positionOffset = get_calypso_node_offset(
-                            bit_representation, contract_key, NavigoContractStructure);
-                        int start = positionOffset,
-                            end = positionOffset +
-                                  get_calypso_node_size(contract_key, NavigoContractStructure) - 1;
-                        float decimal_value =
-                            bit_slice_to_dec(bit_representation, start, end) * 24 * 3600;
-                        uint64_t start_validity_timestamp = (decimal_value + (float)epoch) + 3600;
-                        datetime_timestamp_to_datetime(
-                            start_validity_timestamp, &card->contracts[i - 1].start_date);
-                    }
-
-                    // 13.2. ContractValidityEndDate
-                    contract_key = "ContractValidityEndDate";
-                    if(is_calypso_node_present(
-                           bit_representation, contract_key, NavigoContractStructure)) {
-                        int positionOffset = get_calypso_node_offset(
-                            bit_representation, contract_key, NavigoContractStructure);
-                        int start = positionOffset,
-                            end = positionOffset +
-                                  get_calypso_node_size(contract_key, NavigoContractStructure) - 1;
-                        float decimal_value =
-                            bit_slice_to_dec(bit_representation, start, end) * 24 * 3600;
-                        uint64_t end_validity_timestamp = (decimal_value + (float)epoch) + 3600;
-                        datetime_timestamp_to_datetime(
-                            end_validity_timestamp, &card->contracts[i - 1].end_date);
-                        card->contracts[i - 1].end_date_available = true;
-                    }
-
-                    // 13.6. ContractValidityZones
-                    contract_key = "ContractValidityZones";
-                    if(is_calypso_node_present(
-                           bit_representation, contract_key, NavigoContractStructure)) {
-                        int start = get_calypso_node_offset(
-                            bit_representation, contract_key, NavigoContractStructure);
-                        // binary form is 00011111 for zones 5, 4, 3, 2, 1
-                        for(int j = 0; j < 5; j++) {
-                            card->contracts[i - 1].zones[j] =
-                                bit_slice_to_dec(bit_representation, start + 3 + j, start + 3 + j);
-                        }
-                        card->contracts[i - 1].zones_available = true;
-                    }
-
-                    // 13.7. ContractValidityJourneys  -- pas sûr de le mettre lui
-
-                    // 15.0. ContractValiditySaleDate
-                    contract_key = "ContractValiditySaleDate";
-                    if(is_calypso_node_present(
-                           bit_representation, contract_key, NavigoContractStructure)) {
-                        int positionOffset = get_calypso_node_offset(
-                            bit_representation, contract_key, NavigoContractStructure);
-                        int start = positionOffset,
-                            end = positionOffset +
-                                  get_calypso_node_size(contract_key, NavigoContractStructure) - 1;
-                        float decimal_value =
-                            bit_slice_to_dec(bit_representation, start, end) * 24 * 3600;
-                        uint64_t sale_timestamp = (decimal_value + (float)epoch) + 3600;
-                        datetime_timestamp_to_datetime(
-                            sale_timestamp, &card->contracts[i - 1].sale_date);
-                    }
-
-                    // 15.2. ContractValiditySaleAgent - FIX NEEDED
-                    contract_key = "ContractValiditySaleAgent";
-                    /* if(is_calypso_node_present(
-                           bit_representation, contract_key, NavigoContractStructure)) { */
-                    int positionOffset = get_calypso_node_offset(
-                        bit_representation, contract_key, NavigoContractStructure);
-                    int start = positionOffset,
-                        end = positionOffset +
-                              get_calypso_node_size(contract_key, NavigoContractStructure) - 1;
-                    card->contracts[i - 1].sale_agent =
-                        bit_slice_to_dec(bit_representation, start, end);
-                    // }
-
-                    // 15.3. ContractValiditySaleDevice
-                    contract_key = "ContractValiditySaleDevice";
-                    if(is_calypso_node_present(
-                           bit_representation, contract_key, NavigoContractStructure)) {
-                        int positionOffset = get_calypso_node_offset(
-                            bit_representation, contract_key, NavigoContractStructure);
-                        int start = positionOffset,
-                            end = positionOffset +
-                                  get_calypso_node_size(contract_key, NavigoContractStructure) - 1;
-                        card->contracts[i - 1].sale_device =
-                            bit_slice_to_dec(bit_representation, start, end);
-                    }
-
-                    // 16. ContractStatus  -- 0x1 ou 0xff
-                    contract_key = "ContractStatus";
-                    if(is_calypso_node_present(
-                           bit_representation, contract_key, NavigoContractStructure)) {
-                        int positionOffset = get_calypso_node_offset(
-                            bit_representation, contract_key, NavigoContractStructure);
-                        int start = positionOffset,
-                            end = positionOffset +
-                                  get_calypso_node_size(contract_key, NavigoContractStructure) - 1;
-                        card->contracts[i - 1].status =
-                            bit_slice_to_dec(bit_representation, start, end);
-                    }
-
-                    // 18. ContractAuthenticator
-                    contract_key = "ContractAuthenticator";
-                    if(is_calypso_node_present(
-                           bit_representation, contract_key, NavigoContractStructure)) {
-                        int positionOffset = get_calypso_node_offset(
-                            bit_representation, contract_key, NavigoContractStructure);
-                        int start = positionOffset,
-                            end = positionOffset +
-                                  get_calypso_node_size(contract_key, NavigoContractStructure) - 1;
-                        card->contracts[i - 1].authenticator =
-                            bit_slice_to_dec(bit_representation, start, end);
-                    }
-                }
-
-                // Free the calypso structure
-                free_calypso_structure(NavigoContractStructure);
-
-                // Select app for environment
-                error = select_new_app(
-                    0x20, 0x1, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
-                if(error != 0) {
-                    break;
-                }
-
-                // Check the response after selecting app
-                if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
-                    break;
-                }
-
-                // read file 1
-                error = read_new_file(1, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
-                if(error != 0) {
-                    break;
-                }
-
-                // Check the response after reading the file
-                if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
-                    break;
-                }
-
-                char environment_bit_representation[response_length * 8 + 1];
-                environment_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(
-                        environment_bit_representation,
-                        bits,
-                        sizeof(environment_bit_representation));
-                }
-                // FURI_LOG_I(
-                //     TAG, "Environment bit_representation: %s", environment_bit_representation);
-                start = 0;
-                end = 5;
-                card->environment.app_version =
-                    bit_slice_to_dec(environment_bit_representation, start, end);
-                start = 13;
-                end = 16;
-                card->environment.country_num =
-                    bit_slice_to_dec(environment_bit_representation, start, end) * 100 +
-                    bit_slice_to_dec(environment_bit_representation, start + 4, end + 4) * 10 +
-                    bit_slice_to_dec(environment_bit_representation, start + 8, end + 8);
-                start = 25;
-                end = 28;
-                card->environment.network_num =
-                    bit_slice_to_dec(environment_bit_representation, start, end) * 100 +
-                    bit_slice_to_dec(environment_bit_representation, start + 4, end + 4) * 10 +
-                    bit_slice_to_dec(environment_bit_representation, start + 8, end + 8);
-                start = 45;
-                end = 58;
-                float decimal_value = bit_slice_to_dec(environment_bit_representation, start, end);
-                uint64_t end_validity_timestamp =
-                    (decimal_value * 24 * 3600) + (float)epoch + 3600;
-                datetime_timestamp_to_datetime(end_validity_timestamp, &card->environment.end_dt);
-
-                start = 95;
-                end = 98;
-                card->holder.card_status =
-                    bit_slice_to_dec(environment_bit_representation, start, end);
-
-                start = 99;
-                end = 104;
-                card->holder.commercial_id =
-                    bit_slice_to_dec(environment_bit_representation, start, end);
-
-                // Select app for counters (remaining tickets on Navigo Easy)
-                error = select_new_app(
-                    0x20, 0x69, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
-                if(error != 0) {
-                    break;
-                }
-
-                // Check the response after selecting app
-                if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
-                    break;
-                }
-
-                // read file 1
-                error = read_new_file(1, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
-                if(error != 0) {
-                    break;
-                }
-
-                // Check the response after reading the file
-                if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
-                    break;
-                }
-
-                char counter_bit_representation[response_length * 8 + 1];
-                counter_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(counter_bit_representation, bits, sizeof(counter_bit_representation));
-                }
-                // FURI_LOG_I(TAG, "Counter bit_representation: %s", counter_bit_representation);
-
-                // Ticket counts (contracts 1-4)
-                for(int i = 0; i < 4; i++) {
-                    start = 0;
-                    end = 5;
-                    card->contracts[i].counter.count =
-                        bit_slice_to_dec(counter_bit_representation, 24 * i + start, 24 * i + end);
-
-                    start = 6;
-                    end = 23;
-                    card->contracts[i].counter.relative_first_stamp_15mn =
-                        bit_slice_to_dec(counter_bit_representation, 24 * i + start, 24 * i + end);
-                }
-
-                // Select app for events
-                error = select_new_app(
-                    0x20, 0x10, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
-                if(error != 0) {
-                    break;
-                }
-
-                // Check the response after selecting app
-                if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
-                    break;
-                }
-
-                // Load the calypso structure for events
-                CalypsoApp* NavigoEventStructure = get_navigo_event_structure();
-                if(!NavigoEventStructure) {
-                    FURI_LOG_E(TAG, "Failed to load Navigo Event structure");
-                    break;
-                }
-
-                // furi_string_cat_printf(parsed_data, "\e#Events :\n");
-                // Now send the read command for events
-                for(size_t i = 1; i < 4; i++) {
-                    error =
-                        read_new_file(i, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
-                    if(error != 0) {
-                        break;
-                    }
-
-                    // Check the response after reading the file
-                    if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
-                        break;
-                    }
-
-                    char event_bit_representation[response_length * 8 + 1];
-                    event_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(event_bit_representation, bits, sizeof(event_bit_representation));
-                    }
-
-                    // furi_string_cat_printf(parsed_data, "Event 0%d :\n", i);
-                    /* int count = 0;
-                    int start = 25, end = 52;
-                    char bit_slice[end - start + 2];
-                    strncpy(bit_slice, event_bit_representation + start, end - start + 1);
-                    bit_slice[end - start + 1] = '\0';
-                    int* positions = get_bit_positions(bit_slice, &count);
-                    FURI_LOG_I(TAG, "Positions: ");
-                    for(int i = 0; i < count; i++) {
-                        FURI_LOG_I(TAG, "%d ", positions[i]);
-                    } */
-
-                    // 2. EventCode
-                    const char* event_key = "EventCode";
-                    if(is_calypso_node_present(
-                           event_bit_representation, event_key, NavigoEventStructure)) {
-                        int positionOffset = get_calypso_node_offset(
-                            event_bit_representation, event_key, NavigoEventStructure);
-                        int start = positionOffset,
-                            end = positionOffset +
-                                  get_calypso_node_size(event_key, NavigoEventStructure) - 1;
-                        int decimal_value = bit_slice_to_dec(event_bit_representation, start, end);
-                        card->events[i - 1].transport_type = decimal_value >> 4;
-                        card->events[i - 1].transition = decimal_value & 15;
-                    }
-
-                    // 4. EventServiceProvider
-                    event_key = "EventServiceProvider";
-                    if(is_calypso_node_present(
-                           event_bit_representation, event_key, NavigoEventStructure)) {
-                        int positionOffset = get_calypso_node_offset(
-                            event_bit_representation, event_key, NavigoEventStructure);
-                        start = positionOffset,
-                        end = positionOffset +
-                              get_calypso_node_size(event_key, NavigoEventStructure) - 1;
-                        card->events[i - 1].service_provider =
-                            bit_slice_to_dec(event_bit_representation, start, end);
-                    }
-
-                    // 8. EventLocationId
-                    event_key = "EventLocationId";
-                    if(is_calypso_node_present(
-                           event_bit_representation, event_key, NavigoEventStructure)) {
-                        int positionOffset = get_calypso_node_offset(
-                            event_bit_representation, event_key, NavigoEventStructure);
-                        start = positionOffset,
-                        end = positionOffset +
-                              get_calypso_node_size(event_key, NavigoEventStructure) - 1;
-                        int decimal_value = bit_slice_to_dec(event_bit_representation, start, end);
-                        card->events[i - 1].station_group_id = decimal_value >> 9;
-                        card->events[i - 1].station_id = (decimal_value >> 4) & 31;
-                    }
-
-                    // 9. EventLocationGate
-                    event_key = "EventLocationGate";
-                    if(is_calypso_node_present(
-                           event_bit_representation, event_key, NavigoEventStructure)) {
-                        int positionOffset = get_calypso_node_offset(
-                            event_bit_representation, event_key, NavigoEventStructure);
-                        start = positionOffset,
-                        end = positionOffset +
-                              get_calypso_node_size(event_key, NavigoEventStructure) - 1;
-                        card->events[i - 1].location_gate =
-                            bit_slice_to_dec(event_bit_representation, start, end);
-                        card->events[i - 1].location_gate_available = true;
-                    }
-
-                    // 10. EventDevice
-                    event_key = "EventDevice";
-                    if(is_calypso_node_present(
-                           event_bit_representation, event_key, NavigoEventStructure)) {
-                        int positionOffset = get_calypso_node_offset(
-                            event_bit_representation, event_key, NavigoEventStructure);
-                        start = positionOffset,
-                        end = positionOffset +
-                              get_calypso_node_size(event_key, NavigoEventStructure) - 1;
-                        int decimal_value = bit_slice_to_dec(event_bit_representation, start, end);
-                        card->events[i - 1].device = decimal_value;
-                        int bus_device = decimal_value >> 8;
-                        card->events[i - 1].door = bus_device / 2 + 1;
-                        card->events[i - 1].side = bus_device % 2;
-                        card->events[i - 1].device_available = true;
-                    }
-
-                    // 11. EventRouteNumber
-                    event_key = "EventRouteNumber";
-                    if(is_calypso_node_present(
-                           event_bit_representation, event_key, NavigoEventStructure)) {
-                        int positionOffset = get_calypso_node_offset(
-                            event_bit_representation, event_key, NavigoEventStructure);
-                        start = positionOffset,
-                        end = positionOffset +
-                              get_calypso_node_size(event_key, NavigoEventStructure) - 1;
-                        card->events[i - 1].route_number =
-                            bit_slice_to_dec(event_bit_representation, start, end);
-                        card->events[i - 1].route_number_available = true;
-                    }
-
-                    // 13. EventJourneyRun
-                    event_key = "EventJourneyRun";
-                    if(is_calypso_node_present(
-                           event_bit_representation, event_key, NavigoEventStructure)) {
-                        int positionOffset = get_calypso_node_offset(
-                            event_bit_representation, event_key, NavigoEventStructure);
-                        start = positionOffset,
-                        end = positionOffset +
-                              get_calypso_node_size(event_key, NavigoEventStructure) - 1;
-                        card->events[i - 1].mission =
-                            bit_slice_to_dec(event_bit_representation, start, end);
-                        card->events[i - 1].mission_available = true;
-                    }
-
-                    // 14. EventVehicleId
-                    event_key = "EventVehicleId";
-                    if(is_calypso_node_present(
-                           event_bit_representation, event_key, NavigoEventStructure)) {
-                        int positionOffset = get_calypso_node_offset(
-                            event_bit_representation, event_key, NavigoEventStructure);
-                        start = positionOffset,
-                        end = positionOffset +
-                              get_calypso_node_size(event_key, NavigoEventStructure) - 1;
-                        card->events[i - 1].vehicle_id =
-                            bit_slice_to_dec(event_bit_representation, start, end);
-                        card->events[i - 1].vehicle_id_available = true;
-                    }
-
-                    // 25. EventContractPointer
-                    event_key = "EventContractPointer";
-                    if(is_calypso_node_present(
-                           event_bit_representation, event_key, NavigoEventStructure)) {
-                        int positionOffset = get_calypso_node_offset(
-                            event_bit_representation, event_key, NavigoEventStructure);
-                        start = positionOffset,
-                        end = positionOffset +
-                              get_calypso_node_size(event_key, NavigoEventStructure) - 1;
-                        card->events[i - 1].used_contract =
-                            bit_slice_to_dec(event_bit_representation, start, end);
-                        card->events[i - 1].used_contract_available = true;
-                    }
-
-                    // EventDateStamp
-                    event_key = "EventDateStamp";
-                    int positionOffset = get_calypso_node_offset(
-                        event_bit_representation, event_key, NavigoEventStructure);
-                    start = positionOffset,
-                    end = positionOffset + get_calypso_node_size(event_key, NavigoEventStructure) -
-                          1;
-                    int decimal_value = bit_slice_to_dec(event_bit_representation, start, end);
-                    uint64_t date_timestamp = (decimal_value * 24 * 3600) + epoch + 3600;
-                    datetime_timestamp_to_datetime(date_timestamp, &card->events[i - 1].date);
-
-                    // EventTimeStamp
-                    event_key = "EventTimeStamp";
-                    positionOffset = get_calypso_node_offset(
-                        event_bit_representation, event_key, NavigoEventStructure);
-                    start = positionOffset,
-                    end = positionOffset + get_calypso_node_size(event_key, NavigoEventStructure) -
-                          1;
-                    decimal_value = bit_slice_to_dec(event_bit_representation, start, end);
-                    card->events[i - 1].date.hour = (decimal_value * 60) / 3600;
-                    card->events[i - 1].date.minute = ((decimal_value * 60) % 3600) / 60;
-                    card->events[i - 1].date.second = ((decimal_value * 60) % 3600) % 60;
-                }
-
-                // Free the calypso structure
-                free_calypso_structure(NavigoEventStructure);
-
-                UNUSED(TRANSITION_LIST);
-                UNUSED(TRANSPORT_LIST);
-                UNUSED(SERVICE_PROVIDERS);
-
-                widget_add_text_scroll_element(
-                    widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
-
-                NavigoContext* context = malloc(sizeof(NavigoContext));
-                context->card = card;
-                context->page_id = 0;
-                context->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
-                app->navigo_context = context;
-
-                // Ensure no nested mutexes
-                furi_mutex_acquire(context->mutex, FuriWaitForever);
-                update_page_info(app, parsed_data);
-                furi_mutex_release(context->mutex);
-
-                widget_add_text_scroll_element(
-                    widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
-
-                // Ensure no nested mutexes
-                furi_mutex_acquire(context->mutex, FuriWaitForever);
-                update_widget_elements(app);
-                furi_mutex_release(context->mutex);
-
-                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_navigo_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_navigo_poller_callback, app);
-
-    metroflip_app_blink_start(app);
-}
-
-bool metroflip_scene_navigo_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_navigo_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);
-
-    if(app->navigo_context) {
-        NavigoContext* ctx = app->navigo_context;
-        free(ctx->card);
-        furi_mutex_free(ctx->mutex);
-        free(ctx);
-        app->navigo_context = NULL;
-    }
-}

+ 1 - 1
scenes/metroflip_scene_start.c

@@ -15,7 +15,7 @@ void metroflip_scene_start_on_enter(void* context) {
         submenu, "Rav-Kav", MetroflipSceneRavKav, metroflip_scene_start_submenu_callback, app);
 
     submenu_add_item(
-        submenu, "Navigo", MetroflipSceneNavigo, metroflip_scene_start_submenu_callback, app);
+        submenu, "Calypso", MetroflipSceneCalypso, metroflip_scene_start_submenu_callback, app);
 
     submenu_add_item(
         submenu,