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

Merge pull request #17 from luu176/main

v0.4
Luu 1 год назад
Родитель
Сommit
bd57de4766

+ 46 - 0
.github/workflows/build&push.yml

@@ -0,0 +1,46 @@
+name: Build and Upload FAP to Release
+
+on:
+  workflow_dispatch: 
+
+permissions:
+  contents: write 
+
+jobs:
+  build-and-upload:
+    name: Build and Upload FAP
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout Repository
+        uses: actions/checkout@v3
+
+      - name: Extract Version from Manifest
+        id: extract_version
+        run: |
+          VERSION=$(grep '^version:' manifest.yml | awk '{print $2}')
+          echo "VERSION=${VERSION}" >> $GITHUB_ENV
+
+      - name: Set up Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: '3.x'
+
+      - name: Install UFBT
+        run: |
+          python3 -m pip install --upgrade pip
+          pip install ufbt
+
+      - name: Initialize UFBT Environment
+        run: |
+          ufbt update
+          ufbt vscode_dist
+
+      - name: Build FAP Applications
+        run: ufbt faps
+
+      - name: Upload Build Outputs to Release
+        run: |
+          gh release upload v${{ env.VERSION }} /home/runner/.ufbt/build/metroflip.fap
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 1 - 9
.github/workflows/main.yml

@@ -1,5 +1,4 @@
 name: UFBT Build and Test
-
 on:
   push:
     branches:
@@ -7,36 +6,29 @@ on:
   pull_request:
     branches:
       - main
-
 jobs:
   build:
     name: Build and Test Application
     runs-on: ubuntu-latest
-
     steps:
       - name: Checkout Repository
         uses: actions/checkout@v3
-
       - name: Set up Python
         uses: actions/setup-python@v4
         with:
           python-version: '3.x'
-
       - name: Install UFBT
         run: |
           python3 -m pip install --upgrade pip
           pip install ufbt
-
       - name: Initialize UFBT Environment
         run: |
           ufbt update
           ufbt vscode_dist
-
       - name: Build FAP Applications
         run: ufbt faps
-
       - name: Upload Build Artifacts
         uses: actions/upload-artifact@v3
         with:
           name: build-output
-          path: build/
+          path: build/

+ 11 - 1
CHANGELOG.md

@@ -14,4 +14,14 @@
 - Added Troika parser (Moscow, Russia)
 - Added Myki parser (Melbourne (and surrounds), VIC, Australia)
 - Added Opal parser (Sydney (and surrounds), NSW, Australia)
-- Added ITSO parser (United Kingdom)
+- Added ITSO parser (United Kingdom)
+
+## v0.4
+
+- Updated Navigo parser (Paris, France) (thanks to: DocSystem)
+  - Now use a global Calypso parser, with defined structures
+  - Fix Busfault and NULL Pointer Dereferences
+- Updated all Desfire parsers (opal, itso, myki, etc..)
+  - Now doesnt crash when you click the back button while reading
+- Fix Charliecard parser
+

+ 72 - 17
README.md

@@ -1,45 +1,100 @@
 # Metroflip
-Metroflip is a multi-protocol metro card reader app for the Flipper Zero, inspired by the Metrodroid project. It enables the parsing and analysis of metro cards from transit systems around the world, providing a proof-of-concept for exploring transit card data in a portable format.
+Metroflip is a multi-protocol metro card reader app for the Flipper Zero, inspired by the Metrodroid project. It enables the parsing and analysis of metro cards from transit systems around the world, providing a proof-of-concept for exploring transit card data in a portable format. Please join the server [here](https://discord.gg/NR5hhbAXqS) if you have any questions for me.
 
 # Author
 [@luu176](https://github.com/luu176)
 
+---
+
+![image](screenshots/Menu-Top.png)
+
+# Setup Instructions
+
+## Using the Latest Release
+1. Download the `.fap` file from the [Releases section](https://github.com/luu176/Metroflip/releases).
+2. Drag and drop the `.fap` file into the `apps` folder on your Flipper Zero's SD card.
+
+## Manual Build Instructions
+To build Metroflip manually, follow these steps:
+
+1. **Install Git**  
+   Download and install Git on your Windows computer.  
+   Run the first command:  
+
+```git clone https://github.com/luu176/Metroflip.git```
+
+2. **Navigate to the Project Folder**  
+Run the second command:  
+
+```cd Metroflip```
+
+3. **Install Python**  
+Download and install Python from the [official website](https://www.python.org).  
+
+4. **Install UFBT**  
+Run the third command to install UFBT:  
+
+```pip install ufbt```
+
+5. **Update and Build the Project**  
+Run the following commands in order:  
+
+```ufbt update```
+```ufbt fap_metroflip```
+
+6. **Connect Your Flipper Zero**  
+Ensure your Flipper Zero is connected via USB and close the QFlipper application (if it’s open).  
+
+7. **Launch the Build**  
+Run the final command:  
+
+```ufbt launch```
+
+---
+
 # Metroflip - Card Support TODO List
 
 This is a list of metro cards and transit systems that need support or have partial support.
 
 ## ✅ Supported Cards
 - [x] **Rav-Kav**  
-  - Status: Partially Supported
+- Status: Partially Supported
 - [x] **Charliecard**  
-  - Status: Fully supported.
+- Status: Fully supported.
 - [x] **Metromoney**  
-  - Status: Fully supported.
+- Status: Fully supported.
 - [x] **Bip!**  
-  - Status: Fully supported.
+- Status: Fully supported.
 - [x] **Navigo**  
-  - Status: Fully supported.
+- Status: Fully supported.
 - [x] **Troika**
-  - Status: Fully supported.
+- Status: Fully supported.
 - [x] **Clipper**
-  - Status: Fully supported.
-- [x] **Myki**
-  - Status: Fully supported.
+- Status: Fully supported.
+- [x] **myki**
+- Status: Fully supported.
 - [x] **Opal**
-  - Status: Fully supported.
+- Status: Fully supported.
+- [x] **ITSO**
+- Status: Fully supported
 
 ---
 
-### Credits:
+# Credits
 - **App Author**: [@luu176](https://github.com/luu176)
 - **Charliecard Parser**: [@zacharyweiss](https://github.com/zacharyweiss)
 - **Rav-Kav Parser**: [@luu176](https://github.com/luu176)
-- **Navigo Parser**: [@luu176](https://github.com/luu176)
+- **Navigo Parser**: [@luu176](https://github.com/luu176), [@DocSystem](https://github.com/DocSystem)
 - **Metromoney Parser**: [@Leptopt1los](https://github.com/Leptopt1los)
-- **Bip! Parser**: [@rbasoalto](https://github.com/rbasoalto) [@gornekich](https://github.com/gornekich)
+- **Bip! Parser**: [@rbasoalto](https://github.com/rbasoalto), [@gornekich](https://github.com/gornekich)
 - **Clipper Parser**: [@ke6jjj](https://github.com/ke6jjj)
 - **Troika Parser**: [@gornekich](https://github.com/gornekich)
 - **Myki Parser**: [@gornekich](https://github.com/gornekich)
-- **Opal parser**: [@gornekich](https://github.com/gornekich)
-- **ITSO parser**: [@gsp8181](https://github.com/gsp8181), [@hedger](https://github.com/hedger), [@gornekich](https://github.com/gornekich)
-- **Info Slaves**: [@equipter](https://github.com/equipter), [TheDingo8MyBaby](https://github.com/TheDingo8MyBaby)
+- **Opal Parser**: [@gornekich](https://github.com/gornekich)
+- **ITSO Parser**: [@gsp8181](https://github.com/gsp8181), [@hedger](https://github.com/hedger), [@gornekich](https://github.com/gornekich)
+- **Info Slaves**: [@equipter](https://github.com/equipter), [TheDingo8MyBaby](https://github.com/TheDingo8MyBaby)
+
+---
+
+### Special Thanks
+Huge thanks to [@equipter](https://github.com/equipter) for helping out the community!

+ 251 - 0
api/calypso/calypso_util.c

@@ -0,0 +1,251 @@
+#include <stdlib.h>
+#include <string.h>
+#include "calypso_util.h"
+
+CalypsoElement make_calypso_final_element(
+    const char* key,
+    int size,
+    const char* label,
+    CalypsoFinalType final_type) {
+    CalypsoElement final_element = {};
+
+    final_element.type = CALYPSO_ELEMENT_TYPE_FINAL;
+    final_element.final = malloc(sizeof(CalypsoFinalElement));
+    final_element.final->size = size;
+    final_element.final->final_type = final_type;
+    strncpy(final_element.final->key, key, 36);
+    strncpy(final_element.final->label, label, 64);
+
+    return final_element;
+}
+
+CalypsoElement make_calypso_bitmap_element(const char* key, int size, CalypsoElement* elements) {
+    CalypsoElement bitmap_element = {};
+
+    bitmap_element.type = CALYPSO_ELEMENT_TYPE_BITMAP;
+    bitmap_element.bitmap = malloc(sizeof(CalypsoBitmapElement));
+    bitmap_element.bitmap->size = size;
+    bitmap_element.bitmap->elements = malloc(size * sizeof(CalypsoElement));
+    for(int i = 0; i < size; i++) {
+        bitmap_element.bitmap->elements[i] = elements[i];
+    }
+    strncpy(bitmap_element.bitmap->key, key, 36);
+
+    return bitmap_element;
+}
+
+void free_calypso_element(CalypsoElement* element) {
+    if(element->type == CALYPSO_ELEMENT_TYPE_FINAL) {
+        free(element->final);
+    } else {
+        for(int i = 0; i < element->bitmap->size; i++) {
+            free_calypso_element(&element->bitmap->elements[i]);
+        }
+        free(element->bitmap->elements);
+        free(element->bitmap);
+    }
+}
+
+void free_calypso_structure(CalypsoApp* structure) {
+    for(int i = 0; i < structure->elements_size; i++) {
+        free_calypso_element(&structure->elements[i]);
+    }
+    free(structure->elements);
+    free(structure);
+}
+
+int* get_bit_positions(const char* binary_string, int* count) {
+    int length = strlen(binary_string);
+    int* positions = malloc(length * sizeof(int));
+    int pos_index = 0;
+
+    for(int i = 0; i < length; i++) {
+        if(binary_string[length - 1 - i] == '1') {
+            positions[pos_index++] = i;
+        }
+    }
+
+    *count = pos_index;
+    return positions;
+}
+
+int is_bit_present(int* positions, int count, int bit) {
+    for(int i = 0; i < count; i++) {
+        if(positions[i] == bit) {
+            return 1;
+        }
+    }
+    return 0;
+}
+
+bool is_calypso_subnode_present(
+    const char* binary_string,
+    const char* key,
+    CalypsoBitmapElement* bitmap) {
+    char bit_slice[bitmap->size + 1];
+    strncpy(bit_slice, binary_string, bitmap->size);
+    bit_slice[bitmap->size] = '\0';
+    int count = 0;
+    int* positions = get_bit_positions(bit_slice, &count);
+    int 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) {
+                free(positions);
+                return true;
+            }
+            offset += element->final->size;
+        } else {
+            if(strcmp(element->bitmap->key, key) == 0) {
+                free(positions);
+                return true;
+            }
+            int sub_binary_string_size = element->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, element->bitmap)) {
+                free(positions);
+                return true;
+            }
+            offset += element->bitmap->size;
+        }
+    }
+    free(positions);
+    return false;
+}
+
+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) {
+                return true;
+            }
+            offset += structure->elements[i].final->size;
+        } else {
+            if(strcmp(structure->elements[i].bitmap->key, key) == 0) {
+                return true;
+            }
+            int sub_binary_string_size = structure->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)) {
+                return true;
+            }
+            offset += structure->elements[i].bitmap->size;
+        }
+    }
+    return false;
+}
+
+int get_calypso_subnode_offset(
+    const char* binary_string,
+    const char* key,
+    CalypsoBitmapElement* bitmap,
+    bool* found) {
+    char bit_slice[bitmap->size + 1];
+    strncpy(bit_slice, binary_string, bitmap->size);
+    bit_slice[bitmap->size] = '\0';
+
+    int count = 0;
+    int* positions = get_bit_positions(bit_slice, &count);
+
+    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) {
+                *found = true;
+                free(positions);
+                return count_offset;
+            }
+            count_offset += element.final->size;
+        } else {
+            if(strcmp(element.bitmap->key, key) == 0) {
+                *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;
+            }
+        }
+    }
+    free(positions);
+    return count_offset;
+}
+
+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;
+            }
+        }
+    }
+    return 0;
+}
+
+int get_calypso_subnode_size(const char* key, CalypsoElement* element) {
+    if(element->type == CALYPSO_ELEMENT_TYPE_FINAL) {
+        if(strcmp(element->final->key, key) == 0) {
+            return element->final->size;
+        }
+    } else {
+        if(strcmp(element->bitmap->key, key) == 0) {
+            return element->bitmap->size;
+        }
+        for(int i = 0; i < element->bitmap->size; i++) {
+            int size = get_calypso_subnode_size(key, &element->bitmap->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;
+                }
+            }
+        }
+    }
+    return 0;
+}

+ 80 - 0
api/calypso/calypso_util.h

@@ -0,0 +1,80 @@
+#include <stdbool.h>
+
+#ifndef CALYPSO_UTIL_H
+#define CALYPSO_UTIL_H
+
+typedef enum {
+    CALYPSO_APP_CONTRACT,
+} CalypsoAppType;
+
+typedef enum {
+    CALYPSO_FINAL_TYPE_UNKNOWN,
+    CALYPSO_FINAL_TYPE_NUMBER,
+    CALYPSO_FINAL_TYPE_DATE,
+    CALYPSO_FINAL_TYPE_TIME,
+    CALYPSO_FINAL_TYPE_PAY_METHOD,
+    CALYPSO_FINAL_TYPE_AMOUNT,
+    CALYPSO_FINAL_TYPE_SERVICE_PROVIDER,
+    CALYPSO_FINAL_TYPE_ZONES,
+    CALYPSO_FINAL_TYPE_TARIFF,
+    CALYPSO_FINAL_TYPE_NETWORK_ID,
+    CALYPSO_FINAL_TYPE_TRANSPORT_TYPE,
+    CALYPSO_FINAL_TYPE_CARD_STATUS,
+} CalypsoFinalType;
+
+typedef enum {
+    CALYPSO_ELEMENT_TYPE_BITMAP,
+    CALYPSO_ELEMENT_TYPE_FINAL
+} CalypsoElementType;
+
+typedef struct CalypsoFinalElement_t CalypsoFinalElement;
+typedef struct CalypsoBitmapElement_t CalypsoBitmapElement;
+
+typedef struct {
+    CalypsoElementType type;
+    union {
+        CalypsoFinalElement* final;
+        CalypsoBitmapElement* bitmap;
+    };
+} CalypsoElement;
+
+struct CalypsoFinalElement_t {
+    char key[36];
+    int size;
+    char label[64];
+    CalypsoFinalType final_type;
+};
+
+struct CalypsoBitmapElement_t {
+    char key[36];
+    int size;
+    CalypsoElement* elements;
+};
+
+typedef struct {
+    CalypsoAppType type;
+    CalypsoElement* elements;
+    int elements_size;
+} CalypsoApp;
+
+CalypsoElement make_calypso_final_element(
+    const char* key,
+    int size,
+    const char* label,
+    CalypsoFinalType final_type);
+
+CalypsoElement make_calypso_bitmap_element(const char* key, int size, CalypsoElement* elements);
+
+void free_calypso_structure(CalypsoApp* structure);
+
+int* get_bit_positions(const char* binary_string, int* count);
+
+int is_bit_present(int* positions, int count, int bit);
+
+bool is_calypso_node_present(const char* binary_string, const char* key, CalypsoApp* structure);
+
+int get_calypso_node_offset(const char* binary_string, const char* key, CalypsoApp* structure);
+
+int get_calypso_node_size(const char* key, CalypsoApp* structure);
+
+#endif // CALYPSO_UTIL_H

+ 379 - 0
api/calypso/cards/navigo.c

@@ -0,0 +1,379 @@
+#include <stdlib.h>
+#include "navigo.h"
+
+CalypsoApp* get_navigo_contract_structure() {
+    CalypsoApp* NavigoContractStructure = malloc(sizeof(CalypsoApp));
+    if(!NavigoContractStructure) {
+        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;
+
+    NavigoContractStructure->elements[0] = make_calypso_bitmap_element(
+        "Contract",
+        20,
+        (CalypsoElement[]){
+            make_calypso_final_element(
+                "ContractNetworkId", 24, "Identification du réseau", CALYPSO_FINAL_TYPE_UNKNOWN),
+
+            make_calypso_final_element(
+                "ContractProvider",
+                8,
+                "Identification de l’exploitant",
+                CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "ContractTariff", 16, "Code tarif", CALYPSO_FINAL_TYPE_TARIFF),
+
+            make_calypso_final_element(
+                "ContractSerialNumber", 32, "Numéro TCN", CALYPSO_FINAL_TYPE_UNKNOWN),
+
+            make_calypso_bitmap_element(
+                "ContractCustomerInfoBitmap",
+                2,
+                (CalypsoElement[]){
+                    make_calypso_final_element(
+                        "ContractCustomerProfile",
+                        6,
+                        "Statut du titulaire ou Taux de réduction applicable",
+                        CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "ContractCustomerNumber",
+                        32,
+                        "Numéro de client",
+                        CALYPSO_FINAL_TYPE_UNKNOWN),
+                }),
+
+            make_calypso_bitmap_element(
+                "ContractPassengerInfoBitmap",
+                2,
+                (CalypsoElement[]){
+                    make_calypso_final_element(
+                        "ContractPassengerClass",
+                        8,
+                        "Classe de service des voyageurs",
+                        CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "ContractPassengerTotal",
+                        8,
+                        "Nombre total de voyageurs",
+                        CALYPSO_FINAL_TYPE_UNKNOWN),
+                }),
+
+            make_calypso_final_element(
+                "ContractVehicleClassAllowed",
+                6,
+                "Classes de véhicule autorisé",
+                CALYPSO_FINAL_TYPE_UNKNOWN),
+
+            make_calypso_final_element(
+                "ContractPaymentPointer",
+                32,
+                "Pointeurs sur les événements de paiement",
+                CALYPSO_FINAL_TYPE_UNKNOWN),
+
+            make_calypso_final_element(
+                "ContractPayMethod", 11, "Code mode de paiement", CALYPSO_FINAL_TYPE_PAY_METHOD),
+
+            make_calypso_final_element(
+                "ContractServices", 16, "Services autorisés", CALYPSO_FINAL_TYPE_UNKNOWN),
+
+            make_calypso_final_element(
+                "ContractPriceAmount", 16, "Montant total", CALYPSO_FINAL_TYPE_AMOUNT),
+
+            make_calypso_final_element(
+                "ContractPriceUnit", 16, "Code de monnaie", CALYPSO_FINAL_TYPE_UNKNOWN),
+
+            make_calypso_bitmap_element(
+                "ContractRestrictionBitmap",
+                7,
+                (CalypsoElement[]){
+                    make_calypso_final_element(
+                        "ContractRestrictStart", 11, "", CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "ContractRestrictEnd", 11, "", CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "ContractRestrictDay", 8, "", CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "ContractRestrictTimeCode", 8, "", CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "ContractRestrictCode",
+                        8,
+                        "Code de restriction",
+                        CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "ContractRestrictProduct",
+                        16,
+                        "Produit de restriction",
+                        CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "ContractRestrictLocation",
+                        16,
+                        "Référence du lieu de restriction",
+                        CALYPSO_FINAL_TYPE_UNKNOWN),
+                }),
+
+            make_calypso_bitmap_element(
+                "ContractValidityInfoBitmap",
+                9,
+                (CalypsoElement[]){
+                    make_calypso_final_element(
+                        "ContractValidityStartDate",
+                        14,
+                        "Date de début de validité",
+                        CALYPSO_FINAL_TYPE_DATE),
+                    make_calypso_final_element(
+                        "ContractValidityStartTime",
+                        11,
+                        "Heure de début de validité",
+                        CALYPSO_FINAL_TYPE_TIME),
+                    make_calypso_final_element(
+                        "ContractValidityEndDate",
+                        14,
+                        "Date de fin de validité",
+                        CALYPSO_FINAL_TYPE_DATE),
+                    make_calypso_final_element(
+                        "ContractValidityEndTime",
+                        11,
+                        "Heure de fin de validité",
+                        CALYPSO_FINAL_TYPE_TIME),
+                    make_calypso_final_element(
+                        "ContractValidityDuration",
+                        8,
+                        "Durée de validité",
+                        CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "ContractValidityLimiteDate",
+                        14,
+                        "Date limite de première utilisation",
+                        CALYPSO_FINAL_TYPE_DATE),
+                    make_calypso_final_element(
+                        "ContractValidityZones",
+                        8,
+                        "Numéros des zones autorisées",
+                        CALYPSO_FINAL_TYPE_ZONES),
+                    make_calypso_final_element(
+                        "ContractValidityJourneys",
+                        16,
+                        "Nombre de voyages autorisés",
+                        CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "ContractPeriodJourneys",
+                        16,
+                        "Nombre de voyages autorisés par période",
+                        CALYPSO_FINAL_TYPE_UNKNOWN),
+                }),
+
+            make_calypso_bitmap_element(
+                "ContractJourneyData",
+                8,
+                (CalypsoElement[]){
+                    make_calypso_final_element(
+                        "ContractJourneyOrigin",
+                        16,
+                        "Code lieu d’origine",
+                        CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "ContractJourneyDestination",
+                        16,
+                        "Code lieu de destination",
+                        CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "ContractJourneyRouteNumbers",
+                        16,
+                        "Numéros des lignes autorisées",
+                        CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "ContractJourneyRouteVariants",
+                        8,
+                        "Variantes aux numéros des lignes autorisées",
+                        CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "ContractJourneyRun", 16, "Référence du voyage", CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "ContractJourneyVia", 16, "Code lieu du via", CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "ContractJourneyDistance", 16, "Distance", CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "ContractJourneyInterchanges",
+                        8,
+                        "Nombre de correspondances autorisées",
+                        CALYPSO_FINAL_TYPE_UNKNOWN),
+                }),
+
+            make_calypso_bitmap_element(
+                "ContractSaleData",
+                4,
+                (CalypsoElement[]){
+                    make_calypso_final_element(
+                        "ContractValiditySaleDate", 14, "Date de vente", CALYPSO_FINAL_TYPE_DATE),
+                    make_calypso_final_element(
+                        "ContractValiditySaleTime", 11, "Heure de vente", CALYPSO_FINAL_TYPE_TIME),
+                    make_calypso_final_element(
+                        "ContractValiditySaleAgent",
+                        8,
+                        "Identification de l’exploitant de vente",
+                        CALYPSO_FINAL_TYPE_SERVICE_PROVIDER),
+                    make_calypso_final_element(
+                        "ContractValiditySaleDevice",
+                        16,
+                        "Identification du terminal de vente",
+                        CALYPSO_FINAL_TYPE_UNKNOWN),
+                }),
+
+            make_calypso_final_element(
+                "ContractStatus", 8, "État du contrat", CALYPSO_FINAL_TYPE_UNKNOWN),
+
+            make_calypso_final_element(
+                "ContractLoyaltyPoints",
+                16,
+                "Nombre de points de fidélité",
+                CALYPSO_FINAL_TYPE_NUMBER),
+
+            make_calypso_final_element(
+                "ContractAuthenticator",
+                16,
+                "Code de contrôle de l’intégrité des données",
+                CALYPSO_FINAL_TYPE_UNKNOWN),
+
+            make_calypso_final_element(
+                "ContractData(0..255)", 0, "Données complémentaires", CALYPSO_FINAL_TYPE_UNKNOWN),
+        });
+
+    return NavigoContractStructure;
+}
+
+CalypsoApp* get_navigo_event_structure() {
+    CalypsoApp* NavigoEventStructure = malloc(sizeof(CalypsoApp));
+    if(!NavigoEventStructure) {
+        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;
+
+    NavigoEventStructure->elements[0] = make_calypso_final_element(
+        "EventDateStamp", 14, "Date de l’événement", CALYPSO_FINAL_TYPE_DATE);
+
+    NavigoEventStructure->elements[1] = make_calypso_final_element(
+        "EventTimeStamp", 11, "Heure de l’événement", CALYPSO_FINAL_TYPE_TIME);
+
+    NavigoEventStructure->elements[2] = make_calypso_bitmap_element(
+        "EventBitmap",
+        28,
+        (CalypsoElement[]){
+            make_calypso_final_element(
+                "EventDisplayData", 8, "Données pour l’affichage", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element("EventNetworkId", 24, "Réseau", CALYPSO_FINAL_TYPE_NUMBER),
+            make_calypso_final_element(
+                "EventCode", 8, "Nature de l’événement", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventResult", 8, "Code Résultat", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventServiceProvider",
+                8,
+                "Identité de l’exploitant",
+                CALYPSO_FINAL_TYPE_SERVICE_PROVIDER),
+            make_calypso_final_element(
+                "EventNotokCounter", 8, "Compteur événements anormaux", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventSerialNumber",
+                24,
+                "Numéro de série de l’événement",
+                CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventDestination", 16, "Destination de l’usager", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventLocationId", 16, "Lieu de l’événement", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventLocationGate", 8, "Identification du passage", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventDevice", 16, "Identificateur de l’équipement", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventRouteNumber", 16, "Référence de la ligne", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventRouteVariant",
+                8,
+                "Référence d’une variante de la ligne",
+                CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventJourneyRun", 16, "Référence de la mission", CALYPSO_FINAL_TYPE_NUMBER),
+            make_calypso_final_element(
+                "EventVehicleId", 16, "Identificateur du véhicule", CALYPSO_FINAL_TYPE_NUMBER),
+            make_calypso_final_element(
+                "EventVehicleClass", 8, "Type de véhicule utilisé", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventLocationType",
+                5,
+                "Type d’endroit (gare, arrêt de bus), ",
+                CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventEmployee", 240, "Code de l’employé impliqué", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventLocationReference",
+                16,
+                "Référence du lieu de l’événement",
+                CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventJourneyInterchanges",
+                8,
+                "Nombre de correspondances",
+                CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventPeriodJourneys", 16, "Nombre de voyage effectué", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventTotalJourneys",
+                16,
+                "Nombre total de voyage autorisé",
+                CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventJourneyDistance", 16, "Distance parcourue", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventPriceAmount",
+                16,
+                "Montant en jeu lors de l’événement",
+                CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventPriceUnit", 16, "Unité de montant en jeu", CALYPSO_FINAL_TYPE_UNKNOWN),
+            make_calypso_final_element(
+                "EventContractPointer",
+                5,
+                "Référence du contrat concerné",
+                CALYPSO_FINAL_TYPE_NUMBER),
+            make_calypso_final_element(
+                "EventAuthenticator", 16, "Code de sécurité", CALYPSO_FINAL_TYPE_UNKNOWN),
+
+            make_calypso_bitmap_element(
+                "EventData",
+                5,
+                (CalypsoElement[]){
+                    make_calypso_final_element(
+                        "EventDataDateFirstStamp",
+                        14,
+                        "Date de la première montée",
+                        CALYPSO_FINAL_TYPE_DATE),
+                    make_calypso_final_element(
+                        "EventDataTimeFirstStamp",
+                        11,
+                        "Heure de la première montée",
+                        CALYPSO_FINAL_TYPE_TIME),
+                    make_calypso_final_element(
+                        "EventDataSimulation",
+                        1,
+                        "Dernière validation (0=normal, 1=dégradé), ",
+                        CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "EventDataTrip", 2, "Tronçon", CALYPSO_FINAL_TYPE_UNKNOWN),
+                    make_calypso_final_element(
+                        "EventDataRouteDirection", 2, "Sens", CALYPSO_FINAL_TYPE_UNKNOWN),
+                }),
+        });
+
+    return NavigoEventStructure;
+}

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

@@ -0,0 +1,10 @@
+#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

+ 1 - 11
app/README.md

@@ -10,25 +10,15 @@ This is a list of metro cards and transit systems that are supported.
 
 ## Supported Cards
 - **Rav-Kav**  
-  - Status: Partially supported
 - **Navigo**  
-  - Status: Fully supported.
 - **Charliecard**  
-  - Status: Fully supported.
 - **Metromoney**  
-  - Status: Fully supported.
 - **Bip!**  
-  - Status: Fully supported.
 - **Clipper**  
-  - Status: Fully supported.
 - **Troika**  
-  - Status: Fully supported.
 - **Myki**  
-  - Status: Fully supported.
 - **Opal**  
-  - Status: Fully supported.
 - **ITSO**
-  - Status: Fully supported.
 
 More coming soon! 
 
@@ -36,7 +26,7 @@ More coming soon!
 - **App Author**: luu176
 - **Charliecard Parser**: zacharyweiss
 - **Rav-Kav Parser**: luu176
-- **Navigo Parser**: luu176
+- **Navigo Parser**: luu176, DocSystem
 - **Metromoney Parser**: Leptopt1los
 - **Bip! Parser**: rbasoaltor & gornekich
 - **Clipper Parser**: ke6jjj

+ 1 - 1
application.fam

@@ -5,7 +5,7 @@ App(
     entry_point="metroflip",
     stack_size=2 * 1024,
     fap_category="NFC",
-    fap_version="0.2",
+    fap_version="0.4",
     fap_icon="icon.png",
     fap_description="An implementation of metrodroid on the flipper",
     fap_author="luu176",

+ 4 - 2
manifest.yml

@@ -8,13 +8,15 @@ name: 'Metroflip'
 screenshots:
   - 'screenshots/Menu-Top.png'
   - 'screenshots/Menu-Middle.png'
+  - 'screenshots/Navigo.png'
+  - 'screenshots/Navigo2.png'
   - 'screenshots/Rav-Kav.png'
   - 'screenshots/App.png'
 short_description: 'An implementation of Metrodroid on the Flipper Zero'
 sourcecode:
   location:
-    commit_sha: 80b48d2a91698cfc47bc2d01059518eefc5ad4af
+    commit_sha: a5311068d33a2a49fc2f120940c3a6e4636855af
     origin: https://github.com/luu176/Metroflip
     subdir:
   type: git
-version: 0.3
+version: 0.4.0

+ 4 - 0
metroflip_i.h

@@ -43,6 +43,8 @@ extern const Icon I_RFIDDolphinReceive_97x61;
 
 #include "scenes/metroflip_scene.h"
 
+#include "scenes/navigo_structs.h"
+
 typedef struct {
     Gui* gui;
     SceneManager* scene_manager;
@@ -71,6 +73,8 @@ typedef struct {
     char currency[4];
     char card_type[32];
 
+    // Navigo specific context
+    NavigoContext* navigo_context;
 } Metroflip;
 
 enum MetroflipCustomEvent {

+ 2 - 4
scenes/metroflip_scene_charliecard.c

@@ -1127,10 +1127,8 @@ static bool charliecard_parse(FuriString* parsed_data, const MfClassicData* data
 
         const uint64_t key_a =
             bit_lib_bytes_to_num_be(sec_tr->key_a.data, COUNT_OF(sec_tr->key_a.data));
-        const uint64_t key_b =
-            bit_lib_bytes_to_num_be(sec_tr->key_b.data, COUNT_OF(sec_tr->key_b.data));
+
         if(key_a != charliecard_1k_keys[verify_sector].a) break;
-        if(key_b != charliecard_1k_keys[verify_sector].b) break;
 
         // parse card data
         const uint32_t card_number = mfg_sector_parse(data);
@@ -1242,7 +1240,7 @@ static NfcCommand
         metroflip_app_blink_stop(app);
     } else if(mfc_event->type == MfClassicPollerEventTypeFail) {
         FURI_LOG_I(TAG, "fail");
-        command = NfcCommandStop;
+        command = NfcCommandContinue;
     }
 
     return command;

+ 1 - 1
scenes/metroflip_scene_clipper.c

@@ -593,7 +593,7 @@ static NfcCommand metroflip_scene_clipper_poller_callback(NfcGenericEvent event,
         command = NfcCommandStop;
     } else if(mf_desfire_event->type == MfDesfirePollerEventTypeReadFailed) {
         view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventPollerSuccess);
-        command = NfcCommandReset;
+        command = NfcCommandContinue;
     }
 
     return command;

+ 2 - 2
scenes/metroflip_scene_credits.c

@@ -17,7 +17,7 @@ void metroflip_scene_credits_on_enter(void* context) {
     furi_string_cat_printf(str, "Inspired by Metrodroid\n\n");
     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: luu176\n\n");
+    furi_string_cat_printf(str, "Navigo Parser: \n luu176, 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");
@@ -25,7 +25,7 @@ void metroflip_scene_credits_on_enter(void* context) {
     furi_string_cat_printf(str, "Troika Parser:\n gornekich\n\n");
     furi_string_cat_printf(str, "ITSO Parser:\n gsp8181, hedger, gornekich\n\n");
     furi_string_cat_printf(str, "Opal Parser:\n gornekich\n\n");
-    furi_string_cat_printf(str, "Myki Parser:\n gornekich\n\n");
+    furi_string_cat_printf(str, "myki Parser:\n gornekich\n\n");
     furi_string_cat_printf(str, "Info Slaves:\n Equip, TheDingo8MyBaby\n\n");
 
     widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(str));

+ 2 - 2
scenes/metroflip_scene_itso.c

@@ -140,7 +140,7 @@ static NfcCommand metroflip_scene_itso_poller_callback(NfcGenericEvent event, vo
         command = NfcCommandStop;
     } else if(mf_desfire_event->type == MfDesfirePollerEventTypeReadFailed) {
         view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventPollerSuccess);
-        command = NfcCommandReset;
+        command = NfcCommandContinue;
     }
 
     return command;
@@ -197,7 +197,7 @@ bool metroflip_scene_itso_on_event(void* context, SceneManagerEvent event) {
 void metroflip_scene_itso_on_exit(void* context) {
     Metroflip* app = context;
     widget_reset(app->widget);
-
+    metroflip_app_blink_stop(app);
     if(app->poller) {
         nfc_poller_stop(app->poller);
         nfc_poller_free(app->poller);

+ 6 - 6
scenes/metroflip_scene_myki.c

@@ -6,7 +6,7 @@
 #include "../metroflip_i.h"
 #include <nfc/protocols/mf_desfire/mf_desfire_poller.h>
 
-#define TAG "Metroflip:Scene:Myki"
+#define TAG "Metroflip:Scene:myki"
 
 static const MfDesfireApplicationId myki_app_id = {.data = {0x00, 0x11, 0xf2}};
 static const MfDesfireFileId myki_file_id = 0x0f;
@@ -49,19 +49,19 @@ static bool myki_parse(const NfcDevice* device, FuriString* parsed_data) {
         typedef struct {
             uint32_t top;
             uint32_t bottom;
-        } MykiFile;
+        } mykiFile;
 
         const MfDesfireFileSettings* file_settings =
             mf_desfire_get_file_settings(app, &myki_file_id);
 
         if(file_settings == NULL || file_settings->type != MfDesfireFileTypeStandard ||
-           file_settings->data.size < sizeof(MykiFile))
+           file_settings->data.size < sizeof(mykiFile))
             break;
 
         const MfDesfireFileData* file_data = mf_desfire_get_file_data(app, &myki_file_id);
         if(file_data == NULL) break;
 
-        const MykiFile* myki_file = simple_array_cget_data(file_data->data);
+        const mykiFile* myki_file = simple_array_cget_data(file_data->data);
 
         // All myki card numbers are prefixed with "308425"
         if(myki_file->top != 308425UL) break;
@@ -123,7 +123,7 @@ static NfcCommand metroflip_scene_myki_poller_callback(NfcGenericEvent event, vo
         command = NfcCommandStop;
     } else if(mf_desfire_event->type == MfDesfirePollerEventTypeReadFailed) {
         view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventPollerSuccess);
-        command = NfcCommandReset;
+        command = NfcCommandContinue;
     }
 
     return command;
@@ -180,7 +180,7 @@ bool metroflip_scene_myki_on_event(void* context, SceneManagerEvent event) {
 void metroflip_scene_myki_on_exit(void* context) {
     Metroflip* app = context;
     widget_reset(app->widget);
-
+    metroflip_app_blink_stop(app);
     if(app->poller) {
         nfc_poller_stop(app->poller);
         nfc_poller_free(app->poller);

Разница между файлами не показана из-за своего большого размера
+ 913 - 155
scenes/metroflip_scene_navigo.c


+ 1 - 1
scenes/metroflip_scene_opal.c

@@ -244,7 +244,7 @@ static NfcCommand metroflip_scene_opal_poller_callback(NfcGenericEvent event, vo
         command = NfcCommandStop;
     } else if(mf_desfire_event->type == MfDesfirePollerEventTypeReadFailed) {
         view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventPollerSuccess);
-        command = NfcCommandReset;
+        command = NfcCommandContinue;
     }
 
     return command;

+ 1 - 1
scenes/metroflip_scene_start.c

@@ -28,7 +28,7 @@ void metroflip_scene_start_on_enter(void* context) {
         submenu, "Clipper", MetroflipSceneClipper, metroflip_scene_start_submenu_callback, app);
 
     submenu_add_item(
-        submenu, "Myki", MetroflipSceneMyki, metroflip_scene_start_submenu_callback, app);
+        submenu, "myki", MetroflipSceneMyki, metroflip_scene_start_submenu_callback, app);
 
     submenu_add_item(
         submenu, "Troika", MetroflipSceneTroika, metroflip_scene_start_submenu_callback, app);

+ 706 - 279
scenes/navigo.h

@@ -1,305 +1,732 @@
+#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 METRO_LIST_H
 #define METRO_LIST_H
 
-typedef struct {
-    const char* name;
-    const char* stations[14];
-} MetroLine;
-
 #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"};
+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] = "Urban Bus",
-    [2] = "Interurban Bus",
+    [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] = "Entry",
-    [2] = "Exit",
-    [4] = "Inspection",
-    [6] = "Interchange (entry)",
-    [7] = "Interchange (exit)"};
+    [1] = "Validation en entree",
+    [2] = "Validation en sortie",
+    [4] = "Controle volant (a bord)",
+    [5] = "Validation de test",
+    [6] = "Validation en correspondance (entree)",
+    [7] = "Validation en correspondance (sortie)",
+    [9] = "Annulation de validation",
+    [10] = "Validation en entree",
+    [13] = "Distribution",
+    [15] = "Invalidation"};
 
 #endif // NAVIGO_H
 
-const MetroLine METRO_LIST[] = {
-    [0] =
-        {"Cite",
-         {"Saint-Michel",
-          "Odeon",
-          "Cluny - La Sorbonne",
-          "Maubert - Mutualite",
-          "Luxembourg",
-          "Chatelet",
-          "Les Halles",
-          "Les Halles",
-          "Louvre - Rivoli",
-          "Pont Neuf",
-          "Cite",
-          "Hotel de Ville"}},
-    [1] =
-        {"Rennes",
-         {"Cambronne",
-          "Sevres - Lecourbe",
-          "Segur",
-          "Saint-Francois-Xavier",
-          "Duroc",
-          "Vaneau",
-          "Sevres - Babylone",
-          "Rue du Bac",
-          "Rennes",
-          "Saint-Sulpice",
-          "Mabillon",
-          "Saint-Germain-des-Pres"}},
-    [2] =
-        {"Villette",
-         {"Porte de la Villette",
-          "Aubervilliers - Pantin - Quatre Chemins",
-          "Fort d'Aubervilliers",
-          "La Courneuve - 8 Mai 1945",
-          "Hoche",
-          "Eglise de Pantin",
-          "Bobigny - Pantin - Raymond Queneau",
-          "Bobigny - Pablo Picasso"}},
-    [3] =
-        {"Montparnasse",
-         {"Pernety",
-          "Plaisance",
-          "Gaite",
-          "Edgar Quinet",
-          "Vavin",
-          "Montparnasse - Bienvenue",
-          "Saint-Placide",
-          "Notre-Dame-des-Champs"}},
-    [4] =
-        {"Nation",
-         {"Robespierre",
-          "Porte de Montreuil",
-          "Maraichers",
-          "Buzenval",
-          "Rue des Boulets",
-          "Porte de Vincennes",
-          "Picpus",
-          "Nation",
-          "Avron",
-          "Alexandre Dumas"}},
-    [5] =
-        {"Saint-Lazare",
-         {"Malesherbes",
-          "Monceau",
-          "Villiers",
-          "Quatre-Septembre",
-          "Opera",
-          "Auber",
-          "Havre - Caumartin",
-          "Saint-Lazare",
-          "Saint-Lazare",
-          "Saint-Augustin",
-          "Europe",
-          "Liege"}},
-    [6] =
-        {"Auteuil",
-         {"Porte de Saint-Cloud",
-          "Porte d'Auteuil",
-          "Eglise d'Auteuil",
-          "Michel-Ange - Auteuil",
-          "Michel-Ange - Molitor",
-          "Chardon-Lagache",
-          "Mirabeau",
-          "Exelmans",
-          "Jasmin"}},
-    [7] =
-        {"Republique",
-         {"Rambuteau",
-          "Arts et Metiers",
-          "Jacques Bonsergent",
-          "Goncourt",
-          "Temple",
-          "Republique",
-          "Oberkampf",
-          "Parmentier",
-          "Filles du Calvaire",
-          "Saint-Sebastien - Froissart",
-          "Richard-Lenoir",
-          "Saint-Ambroise"}},
-    [8] =
-        {"Austerlitz",
-         {"Quai de la Gare",
-          "Chevaleret",
-          "Saint-Marcel",
-          "Gare d'Austerlitz",
-          "Gare de Lyon",
-          "Quai de la Rapee"}},
-    [9] =
-        {"Invalides",
-         {"Champs-Elysees - Clemenceau",
-          "Concorde",
-          "Madeleine",
-          "Bir-Hakeim",
-          "Ecole Militaire",
-          "La Tour-Maubourg",
-          "Invalides",
-          "Saint-Denis - Universite",
-          "Varenne",
-          "Assemblee nationale",
-          "Solferino"}},
-    [10] =
-        {"Sentier",
-         {"Tuileries",
-          "Palais Royal - Musee du Louvre",
-          "Pyramides",
-          "Bourse",
-          "Grands Boulevards",
-          "Richelieu - Drouot",
-          "Bonne Nouvelle",
-          "Strasbourg - Saint-Denis",
-          "Chateau d'Eau",
-          "Sentier",
-          "Reaumur - Sebastopol",
-          "Etienne Marcel"}},
-    [11] =
-        {"Ile Saint-Louis",
-         {"Faidherbe - Chaligny",
-          "Reuilly - Diderot",
-          "Montgallet",
-          "Censier - Daubenton",
-          "Place Monge",
-          "Cardinal Lemoine",
-          "Jussieu",
-          "Sully - Morland",
-          "Pont Marie",
-          "Saint-Paul",
-          "Bastille",
-          "Chemin Vert",
-          "Breguet - Sabin",
-          "Ledru-Rollin"}},
-    [12] =
-        {"Daumesnil",
-         {"Porte Doree",
-          "Porte de Charenton",
-          "Bercy",
-          "Dugommier",
-          "Michel Bizot",
-          "Daumesnil",
-          "Bel-Air"}},
-    [13] =
-        {"Italie",
-         {"Porte de Choisy",
-          "Porte d'Italie",
-          "Cite universitaire",
-          "Maison Blanche",
-          "Tolbiac",
-          "Nationale",
-          "Campo-Formio",
-          "Les Gobelins",
-          "Place d'Italie",
-          "Corvisart"}},
-    [14] =
-        {"Denfert",
-         {"Cour Saint-Emilion",
-          "Porte d'Orleans",
-          "Bibliotheque Francois Mitterrand",
-          "Mouton-Duvernet",
-          "Alesia",
-          "Olympiades",
-          "Glaciere",
-          "Saint-Jacques",
-          "Raspail",
-          "Denfert-Rochereau"}},
-    [15] =
-        {"Felix Faure",
-         {"Falguiere",
-          "Pasteur",
-          "Volontaires",
-          "Vaugirard",
-          "Convention",
-          "Porte de Versailles",
-          "Balard",
-          "Lourmel",
-          "Boucicaut",
-          "Felix Faure",
-          "Charles Michels",
-          "Javel - Andre Citroen"}},
+static const char* METRO_STATION_LIST[32][16] =
+    {[1] =
+         {[0] = "Cite",
+          [1] = "Saint-Michel",
+          [4] = "Odeon",
+          [5] = "Cluny - La Sorbonne",
+          [6] = "Maubert - Mutualite",
+          [7] = "Luxembourg",
+          [8] = "Châtelet",
+          [9] = "Les Halles",
+          [10] = "Les Halles",
+          [12] = "Louvre - Rivoli",
+          [13] = "Pont Neuf",
+          [14] = "Cite",
+          [15] = "Hotel de Ville"},
+     [2] =
+         {[0] = "Rennes",
+          [2] = "Cambronne",
+          [3] = "Sevres - Lecourbe",
+          [4] = "Segur",
+          [6] = "Saint-François-Xavier",
+          [7] = "Duroc",
+          [8] = "Vaneau",
+          [9] = "Sevres - Babylone",
+          [10] = "Rue du Bac",
+          [11] = "Rennes",
+          [12] = "Saint-Sulpice",
+          [14] = "Mabillon",
+          [15] = "Saint-Germain-des-Pres"},
+     [3] =
+         {[0] = "Villette",
+          [4] = "Porte de la Villette",
+          [5] = "Aubervilliers - Pantin - Quatre Chemins",
+          [6] = "Fort d'Aubervilliers",
+          [7] = "La Courneuve - 8 Mai 1945",
+          [9] = "Hoche",
+          [10] = "Eglise de Pantin",
+          [11] = "Bobigny - Pantin - Raymond Queneau",
+          [12] = "Bobigny - Pablo Picasso"},
+     [4] =
+         {[0] = "Montparnasse",
+          [2] = "Pernety",
+          [3] = "Plaisance",
+          [4] = "Gaite",
+          [6] = "Edgar Quinet",
+          [7] = "Vavin",
+          [8] = "Montparnasse - Bienvenue",
+          [12] = "Saint-Placide",
+          [14] = "Notre-Dame-des-Champs"},
+     [5] =
+         {[0] = "Nation",
+          [2] = "Robespierre",
+          [3] = "Porte de Montreuil",
+          [4] = "Maraichers",
+          [5] = "Buzenval",
+          [6] = "Rue des Boulets",
+          [7] = "Porte de Vincennes",
+          [9] = "Picpus",
+          [10] = "Nation",
+          [12] = "Avron",
+          [13] = "Alexandre Dumas"},
+     [6] =
+         {[0] = "Saint-Lazare",
+          [1] = "Malesherbes",
+          [2] = "Monceau",
+          [3] = "Villiers",
+          [4] = "Quatre-Septembre",
+          [5] = "Opera",
+          [6] = "Auber",
+          [7] = "Havre - Caumartin",
+          [8] = "Saint-Lazare",
+          [9] = "Saint-Lazare",
+          [10] = "Saint-Augustin",
+          [12] = "Europe",
+          [13] = "Liege"},
+     [7] =
+         {[0] = "Auteuil",
+          [3] = "Porte de Saint-Cloud",
+          [7] = "Porte d'Auteuil",
+          [8] = "eglise d'Auteuil",
+          [9] = "Michel-Ange - Auteuil",
+          [10] = "Michel-Ange - Molitor",
+          [11] = "Chardon-Lagache",
+          [12] = "Mirabeau",
+          [14] = "Exelmans",
+          [15] = "Jasmin"},
+     [8] =
+         {[0] = "Republique",
+          [1] = "Rambuteau",
+          [3] = "Arts et Metiers",
+          [4] = "Jacques Bonsergent",
+          [5] = "Goncourt",
+          [6] = "Temple",
+          [7] = "Republique",
+          [10] = "Oberkampf",
+          [11] = "Parmentier",
+          [12] = "Filles du Calvaire",
+          [13] = "Saint-Sebastien - Froissart",
+          [14] = "Richard-Lenoir",
+          [15] = "Saint-Ambroise"},
+     [9] =
+         {[0] = "Austerlitz",
+          [1] = "Quai de la Gare",
+          [2] = "Chevaleret",
+          [4] = "Saint-Marcel",
+          [7] = "Gare d'Austerlitz",
+          [8] = "Gare de Lyon",
+          [10] = "Quai de la Rapee"},
+     [10] =
+         {[0] = "Invalides",
+          [1] = "Champs-elysees - Clemenceau",
+          [2] = "Concorde",
+          [3] = "Madeleine",
+          [4] = "Bir-Hakeim",
+          [7] = "ecole Militaire",
+          [8] = "La Tour-Maubourg",
+          [9] = "Invalides",
+          [11] = "Saint-Denis - Universite",
+          [12] = "Varenne",
+          [13] = "Assemblee nationale",
+          [14] = "Solferino"},
+     [11] =
+         {[0] = "Sentier",
+          [1] = "Tuileries",
+          [2] = "Palais Royal - Musee du Louvre",
+          [3] = "Pyramides",
+          [4] = "Bourse",
+          [6] = "Grands Boulevards",
+          [7] = "Richelieu - Drouot",
+          [8] = "Bonne Nouvelle",
+          [10] = "Strasbourg - Saint-Denis",
+          [11] = "Château d'Eau",
+          [13] = "Sentier",
+          [14] = "Reaumur - Sebastopol",
+          [15] = "etienne Marcel"},
+     [12] =
+         {[0] = "ile Saint-Louis",
+          [1] = "Faidherbe - Chaligny",
+          [2] = "Reuilly - Diderot",
+          [3] = "Montgallet",
+          [4] = "Censier - Daubenton",
+          [5] = "Place Monge",
+          [6] = "Cardinal Lemoine",
+          [7] = "Jussieu",
+          [8] = "Sully - Morland",
+          [9] = "Pont Marie",
+          [10] = "Saint-Paul",
+          [12] = "Bastille",
+          [13] = "Chemin Vert",
+          [14] = "Breguet - Sabin",
+          [15] = "Ledru-Rollin"},
+     [13] =
+         {[0] = "Daumesnil",
+          [1] = "Porte Doree",
+          [3] = "Porte de Charenton",
+          [7] = "Bercy",
+          [8] = "Dugommier",
+          [10] = "Michel Bizot",
+          [11] = "Daumesnil",
+          [12] = "Bel-Air"},
+     [14] =
+         {[0] = "Italie",
+          [2] = "Porte de Choisy",
+          [3] = "Porte d'Italie",
+          [4] = "Cite universitaire",
+          [9] = "Maison Blanche",
+          [10] = "Tolbiac",
+          [11] = "Nationale",
+          [12] = "Campo-Formio",
+          [13] = "Les Gobelins",
+          [14] = "Place d'Italie",
+          [15] = "Corvisart"},
+     [15] =
+         {[0] = "Denfert",
+          [1] = "Cour Saint-Emilion",
+          [2] = "Porte d'Orleans",
+          [3] = "Bibliotheque François Mitterrand",
+          [4] = "Mouton-Duvernet",
+          [5] = "Alesia",
+          [6] = "Olympiades",
+          [8] = "Glaciere",
+          [9] = "Saint-Jacques",
+          [10] = "Raspail",
+          [14] = "Denfert-Rochereau"},
+     [16] =
+         {[0] = "Felix Faure",
+          [1] = "Falguiere",
+          [2] = "Pasteur",
+          [3] = "Volontaires",
+          [4] = "Vaugirard",
+          [5] = "Convention",
+          [6] = "Porte de Versailles",
+          [9] = "Balard",
+          [10] = "Lourmel",
+          [11] = "Boucicaut",
+          [12] = "Felix Faure",
+          [13] = "Charles Michels",
+          [14] = "Javel - Andre Citroen"},
+     [17] =
+         {[0] = "Passy",
+          [2] = "Porte Dauphine",
+          [4] = "La Motte-Picquet - Grenelle",
+          [5] = "Commerce",
+          [6] = "Avenue emile Zola",
+          [7] = "Dupleix",
+          [8] = "Passy",
+          [9] = "Ranelagh",
+          [11] = "La Muette",
+          [13] = "Rue de la Pompe",
+          [14] = "Boissiere",
+          [15] = "Trocadero"},
+     [18] =
+         {[0] = "Etoile",
+          [1] = "Iena",
+          [3] = "Alma - Marceau",
+          [4] = "Miromesnil",
+          [5] = "Saint-Philippe du Roule",
+          [7] = "Franklin D. Roosevelt",
+          [8] = "George V",
+          [9] = "Kleber",
+          [10] = "Victor Hugo",
+          [11] = "Argentine",
+          [12] = "Charles de Gaulle - Itoile",
+          [14] = "Ternes",
+          [15] = "Courcelles"},
+     [19] =
+         {[0] = "Clichy - Saint Ouen",
+          [1] = "Mairie de Clichy",
+          [2] = "Gabriel Peri",
+          [3] = "Les Agnettes",
+          [4] = "Asnieres - Gennevilliers - Les Courtilles",
+          [9] = "La Chapelle)",
+          [10] = "Garibaldi",
+          [11] = "Mairie de Saint-Ouen",
+          [13] = "Carrefour Pleyel",
+          [14] = "Saint-Denis - Porte de Paris",
+          [15] = "Basilique de Saint-Denis"},
+     [20] =
+         {[0] = "Montmartre",
+          [1] = "Porte de Clignancourt",
+          [6] = "Porte de la Chapelle",
+          [7] = "Marx Dormoy",
+          [9] = "Marcadet - Poissonniers",
+          [10] = "Simplon",
+          [11] = "Jules Joffrin",
+          [12] = "Lamarck - Caulaincourt"},
+     [21] =
+         {[0] = "Lafayette",
+          [1] = "Chaussee d'Antin - La Fayette",
+          [2] = "Le Peletier",
+          [3] = "Cadet",
+          [4] = "Château Rouge",
+          [7] = "Barbes - Rochechouart",
+          [8] = "Gare du Nord",
+          [9] = "Gare de l'Est",
+          [10] = "Poissonniere",
+          [11] = "Château-Landon"},
+     [22] =
+         {[0] = "Buttes Chaumont",
+          [1] = "Porte de Pantin",
+          [2] = "Ourcq",
+          [4] = "Corentin Cariou",
+          [6] = "Crimee",
+          [8] = "Riquet",
+          [9] = "La Chapelle",
+          [10] = "Louis Blanc",
+          [11] = "Stalingrad",
+          [12] = "Jaures",
+          [13] = "Laumiere",
+          [14] = "Bolivar",
+          [15] = "Colonel Fabien"},
+     [23] =
+         {[0] = "Belleville",
+          [2] = "Porte des Lilas",
+          [3] = "Mairie des Lilas",
+          [4] = "Porte de Bagnolet",
+          [5] = "Gallieni",
+          [8] = "Place des Fetes",
+          [9] = "Botzaris",
+          [10] = "Danube",
+          [11] = "Pre Saint-Gervais",
+          [13] = "Buttes Chaumont",
+          [14] = "Jourdain",
+          [15] = "Telegraphe"},
+     [24] =
+         {[0] = "Pere Lachaise",
+          [1] = "Voltaire",
+          [2] = "Charonne",
+          [4] = "Pere Lachaise",
+          [5] = "Menilmontant",
+          [6] = "Rue Saint-Maur",
+          [7] = "Philippe Auguste",
+          [8] = "Saint-Fargeau",
+          [9] = "Pelleport",
+          [10] = "Gambetta",
+          [12] = "Belleville",
+          [13] = "Couronnes",
+          [14] = "Pyrenees"},
+     [25] =
+         {[0] = "Charenton",
+          [2] = "Croix de Chavaux",
+          [3] = "Mairie de Montreuil",
+          [4] = "Maisons-Alfort - Les Juilliottes",
+          [5] = "Creteil - L'echat",
+          [6] = "Creteil - Universite",
+          [7] = "Creteil - Prefecture",
+          [8] = "Saint-Mande",
+          [10] = "Berault",
+          [11] = "Château de Vincennes",
+          [12] = "Liberte",
+          [13] = "Charenton - ecoles",
+          [14] = "ecole veterinaire de Maisons-Alfort",
+          [15] = "Maisons-Alfort - Stade"},
+     [26] =
+         {[0] = "Ivry - Villejuif",
+          [3] = "Porte d'Ivry",
+          [4] = "Pierre et Marie Curie",
+          [5] = "Mairie d'Ivry",
+          [6] = "Le Kremlin-Bicetre",
+          [7] = "Villejuif - Leo Lagrange",
+          [8] = "Villejuif - Paul Vaillant-Couturier",
+          [9] = "Villejuif - Louis Aragon"},
+     [27] =
+         {[0] = "Vanves",
+          [2] = "Porte de Vanves",
+          [7] = "Malakoff - Plateau de Vanves",
+          [8] = "Malakoff - Rue etienne Dolet",
+          [9] = "Châtillon - Montrouge"},
+     [28] =
+         {[0] = "Issy",
+          [2] = "Corentin Celton",
+          [3] = "Mairie d'Issy",
+          [8] = "Marcel Sembat",
+          [9] = "Billancourt",
+          [10] = "Pont de Sevres"},
+     [29] =
+         {[0] = "Levallois",
+          [4] = "Boulogne - Jean Jaures",
+          [5] = "Boulogne - Pont de Saint-Cloud",
+          [8] = "Les Sablons",
+          [9] = "Pont de Neuilly",
+          [10] = "Esplanade de la Defense",
+          [11] = "La Defense",
+          [12] = "Porte de Champerret",
+          [13] = "Louise Michel",
+          [14] = "Anatole France",
+          [15] = "Pont de Levallois - Becon"},
+     [30] =
+         {[0] = "Pereire",
+          [1] = "Porte Maillot",
+          [4] = "Wagram",
+          [5] = "Pereire",
+          [8] = "Brochant",
+          [9] = "Porte de Clichy",
+          [12] = "Guy Moquet",
+          [13] = "Porte de Saint-Ouen"},
+     [31] = {
+         [0] = "Pigalle",
+         [2] = "Funiculaire de Montmartre (station inferieure)",
+         [3] = "Funiculaire de Montmartre (station superieure)",
+         [4] = "Anvers",
+         [5] = "Abbesses",
+         [6] = "Pigalle",
+         [7] = "Blanche",
+         [8] = "Trinite - d'Estienne d'Orves",
+         [9] = "Notre-Dame-de-Lorette",
+         [10] = "Saint-Georges",
+         [12] = "Rome",
+         [13] = "Place de Clichy",
+         [14] = "La Fourche"}};
+
+static const char* TRAIN_LINES_LIST[77] = {
+    [1] = "RER B",         [3] = "RER B",         [6] = "RER A",         [14] = "RER B",
+    [15] = "RER B",        [16] = "RER A",        [17] = "RER A",        [18] = "RER B",
+    [20] = "Transilien P", [21] = "Transilien P", [22] = "T4",           [23] = "Transilien P",
+    [26] = "RER A",        [28] = "RER B",        [30] = "Transilien L", [31] = "Transilien L",
+    [32] = "Transilien J", [33] = "RER A",        [35] = "Transilien J", [40] = "RER D",
+    [41] = "RER C",        [42] = "RER C",        [43] = "Transilien R", [44] = "Transilien R",
+    [45] = "RER D",        [50] = "Transilien H", [51] = "Transilien K", [52] = "RER D",
+    [53] = "Transilien H", [54] = "Transilien J", [55] = "RER C",        [56] = "Transilien H",
+    [57] = "Transilien H", [60] = "Transilien N", [61] = "Transilien N", [63] = "RER C",
+    [64] = "RER C",        [65] = "Transilien V", [70] = "RER B",        [72] = "Transilien J",
+    [73] = "Transilien J", [75] = "RER C",        [76] = "RER C"};
+
+static const char* TRAIN_STATION_LIST[77][19] = {
+    [1] = {[0] = "Châtelet-Les Halles", [1] = "Châtelet-Les Halles", [7] = "Luxembourg"},
+    [3] = {[0] = "Saint-Michel Notre-Dame"},
+    [6] = {[0] = "Auber", [6] = "Auber"},
+    [14] = {[4] = "Cite Universitaire"},
+    [15] = {[12] = "Port Royal"},
     [16] =
-        {"Passy",
-         {"Porte Dauphine",
-          "La Motte-Picquet - Grenelle",
-          "Commerce",
-          "Avenue Emile Zola",
-          "Dupleix",
-          "Passy",
-          "Ranelagh",
-          "La Muette",
-          "Rue de la Pompe",
-          "Boissiere",
-          "Trocadero"}},
+        {[1] = "Nation",
+         [2] = "Fontenay-sous-Bois | Vincennes",
+         [3] = "Joinville-le-Pont | Nogent-sur-Marne",
+         [4] = "Saint-Maur Creteil",
+         [5] = "Le Parc de Saint-Maur",
+         [6] = "Champigny",
+         [7] = "La Varenne-Chennevieres",
+         [8] = "Boissy-Saint-Leger | Sucy Bonneuil"},
     [17] =
-        {"Etoile",
-         {"Iena",
-          "Alma - Marceau",
-          "Miromesnil",
-          "Saint-Philippe du Roule",
-          "Franklin D. Roosevelt",
-          "George V",
-          "Kleber",
-          "Victor Hugo",
-          "Argentine",
-          "Charles de Gaulle - Etoile",
-          "Ternes",
-          "Courcelles"}},
+        {[1] = "Charles de Gaulle-Etoile",
+         [4] = "La Defense (Grande Arche)",
+         [5] = "Nanterre-Ville",
+         [6] = "Rueil-Malmaison",
+         [8] = "Chatou-Croissy",
+         [9] = "Le Vesinet-Centre | Le Vesinet-Le Pecq | Saint-Germain-en-Laye"},
     [18] =
-        {"Clichy - Saint Ouen",
-         {"Mairie de Clichy",
-          "Gabriel Peri",
-          "Les Agnettes",
-          "Asnieres - Gennevilliers - Les Courtilles",
-          "La Chapelle",
-          "Garibaldi",
-          "Mairie de Saint-Ouen",
-          "Carrefour Pleyel",
-          "Saint-Denis - Porte de Paris",
-          "Basilique de Saint-Denis"}},
-    [19] =
-        {"Montmartre",
-         {"Porte de Clignancourt",
-          "Porte de la Chapelle",
-          "Marx Dormoy",
-          "Marcadet - Poissonniers",
-          "Simplon",
-          "Jules Joffrin",
-          "Lamarck - Caulaincourt"}},
+        {[0] = "Denfert-Rochereau",
+         [1] = "Gentilly",
+         [2] = "Arcueil-Cachan | Laplace",
+         [3] = "Bagneux | Bourg-la-Reine",
+         [4] = "La Croix-de-Berny | Parc de Sceaux",
+         [5] = "Antony | Fontaine-Michalon | Les Baconnets",
+         [6] = "Massy-Palaiseau | Massy-Verrieres",
+         [7] = "Palaiseau Villebon | Palaiseau",
+         [8] = "Lozere",
+         [9] = "Le Guichet | Orsay-Ville",
+         [10] =
+             "Bures-sur-Yvette | Courcelle-sur-Yvette | Gif-sur-Yvette | La Hacquiniere | Saint-Remy-les-Chevreuse"},
     [20] =
-        {"Lafayette",
-         {"Chaussee d'Antin - La Fayette",
-          "Le Peletier",
-          "Cadet",
-          "Chateau Rouge",
-          "Barbes - Rochechouart",
-          "Gare du Nord",
-          "Gare de l'Est",
-          "Poissonniere",
-          "Chateau-Landon"}},
-    [21] = {
-        "Buttes Chaumont",
-        {"Porte de Pantin",
-         "Ourcq",
-         "Corentin Cariou",
-         "Crimee",
-         "Riquet",
-         "La Chapelle",
-         "Belleville",
-         "Botzaris",
-         "Pelleport",
-         "Place des Fetes",
-         "Cimetiere du Pere Lachaise"}}};
+        {[1] = "Gare de l'Est",
+         [4] = "Pantin",
+         [5] = "Noisy-le-Sec",
+         [6] = "Bondy",
+         [7] = "Gagny | Le Raincy Villemomble Montfermeil",
+         [9] = "Chelles Gournay | Le Chenay Gagny",
+         [10] = "Vaires Torcy",
+         [11] = "Lagny-Thorigny",
+         [13] = "Esbly",
+         [14] = "Meaux",
+         [15] = "Changis-Saint-Jean | Isles-Armentieres Congis | Lizy-sur-Ourcq | Trilport",
+         [16] = "Crouy-sur-Ourcq | La Ferte-sous-Jouarre | Nanteuil Saacy"},
+    [21] =
+        {[5] = "Rosny-Bois-Perrier | Rosny-sous-Bois | Val de Fontenay",
+         [6] = "Nogent Le-Perreux",
+         [7] = "Les Boullereaux Champigny",
+         [8] = "Villiers-sur-Marne Plessis-Trevise",
+         [9] = "Les Yvris Noisy-le-Grand",
+         [10] = "Emerainville Pontault-Combault | Roissy-en-Brie",
+         [11] = "Ozoir-la-Ferriere",
+         [12] = "Gretz-Armainvilliers | Tournan",
+         [15] =
+             "Courquetaine | Faremoutiers Pommeuse | Guerard La-Celle-sur-Morin | Liverdy en Brie | Marles-en-Brie | Mormant | Mortcerf | Mouroux | Ozouer le voulgis | Verneuil-l'Etang | Villepatour - Presles | Yebles - Guignes | Yebles",
+         [16] =
+             "Chailly Boissy-le-Châtel | Chauffry | Coulommiers | Jouy-sur-Morin Le-Marais | Nangis | Saint-Remy-la-Vanne | Saint-Simeon",
+         [17] =
+             "Champbenoist-Poigny | La Ferte-Gaucher | Longueville | Provins | Sainte-Colombe-Septveilles",
+         [18] = "Flamboin | Meilleray | Villiers St Georges"},
+    [22] =
+        {[7] =
+             "Allee de la Tour-Rendez-Vous | La Remise-a-Jorelle | Les Coquetiers | Les Pavillons-sous-Bois",
+         [8] = "Gargan",
+         [9] = "Freinville Sevran | L'Abbaye"},
+    [23] =
+        {[13] = "Couilly Saint-Germain Quincy | Les Champs-Forts | Montry Conde",
+         [14] = "Crecy-en-Brie La Chapelle | Villiers-Montbarbin"},
+    [26] =
+        {[5] = "Val de Fontenay",
+         [6] = "Bry-sur-Marne | Neuilly-Plaisance",
+         [7] = "Noisy-le-Grand (Mont d'Est)",
+         [8] = "Noisy-Champs",
+         [10] = "Lognes | Noisiel | Torcy",
+         [11] = "Bussy-Saint-Georges",
+         [12] = "Val d'europe",
+         [13] = "Marne-la-Vallee Chessy"},
+    [28] = {[4] = "Fontenay-aux-Roses | Robinson | Sceaux"},
+    [30] =
+        {[1] = "Gare Saint-Lazare",
+         [3] = "Pont Cardinet",
+         [4] =
+             "Asnieres | Becon-les-Bruyeres | Clichy Levallois | Courbevoie | La Defense (Grande Arche)",
+         [5] = "Puteaux | Suresnes Mont-Valerien",
+         [7] = "Garches Marne-la-Coquette | Le Val d'Or | Saint-Cloud",
+         [8] = "Vaucresson",
+         [9] = "Bougival | La Celle-Saint-Cloud | Louveciennes | Marly-le-Roi",
+         [10] = "L'Etang-la-Ville | Saint-Nom-la-Breteche Foret de Marly"},
+    [31] =
+        {[7] = "Chaville-Rive Droite | Sevres Ville-d'Avray | Viroflay-Rive Droite",
+         [8] = "Montreuil | Versailles-Rive Droite"},
+    [32] =
+        {[5] = "La Garenne-Colombes | Les Vallees | Nanterre-Universite",
+         [7] = "Houilles Carrieres-sur-Seine | Sartrouville",
+         [9] = "Maisons-Laffitte",
+         [10] = "Poissy",
+         [11] = "Villennes-sur-Seine",
+         [12] = "Les Clairieres de Verneuil | Vernouillet Verneuil",
+         [13] = "Aubergenville-Elisabethville | Les Mureaux",
+         [14] = "Epone Mezieres",
+         [16] = "Bonnieres | Mantes-Station | Mantes-la-Jolie | Port-Villez | Rosny-sur-Seine"},
+    [33] =
+        {[10] = "Acheres-Grand-Cormier | Acheres-Ville",
+         [11] = "Cergy-Prefecture | Neuville-Universite",
+         [12] = "Cergy-Saint-Christophe | Cergy-le-Haut"},
+    [35] =
+        {[4] = "Bois-Colombes",
+         [5] = "Colombes | Le Stade",
+         [6] = "Argenteuil | Argenteuil",
+         [8] = "Cormeilles-en-Parisis | Val d'Argenteuil | Val d'Argenteuil",
+         [9] = "Herblay | La Frette Montigny",
+         [10] = "Conflans-Fin d'Oise | Conflans-Sainte-Honorine",
+         [11] = "Andresy | Chanteloup-les-Vignes | Maurecourt",
+         [12] = "Triel-sur-Seine | Vaux-sur-Seine",
+         [13] = "Meulan Hadricourt | Thun-le-Paradis",
+         [14] = "Gargenville | Juziers",
+         [15] = "Issou Porcheville | Limay",
+         [16] = "Breval | Menerville"},
+    [40] =
+        {[1] = "Gare de Lyon",
+         [5] = "Le Vert de Maisons | Maisons-Alfort Alfortville",
+         [6] = "Villeneuve-Prairie",
+         [7] = "Villeneuve-Triage",
+         [8] = "Villeneuve-Saint-Georges",
+         [9] = "Juvisy | Vigneux-sur-Seine",
+         [10] = "Ris-Orangis | Viry-Châtillon",
+         [11] = "Evry Val de Seine | Grand-Bourg",
+         [12] = "Corbeil-Essonnes | Mennecy | Moulin-Galant",
+         [13] = "Ballancourt | Fontenay le Vicomte",
+         [14] = "La Ferte-Alais",
+         [16] = "Boutigny | Maisse",
+         [17] = "Boigneville | Buno-Gironville"},
+    [41] =
+        {[0] = "Musee d'Orsay | Saint-Michel Notre-Dame",
+         [1] = "Gare d'Austerlitz",
+         [2] = "Bibliotheque-Francois Mitterrand",
+         [4] = "Ivry-sur-Seine | Vitry-sur-Seine",
+         [5] = "Choisy-le-Roi | Les Ardoines",
+         [7] = "Villeneuve-le-Roi",
+         [8] = "Ablon",
+         [9] = "Athis-Mons"},
+    [42] =
+        {[9] = "Epinay-sur-Orge | Savigny-sur-Orge",
+         [10] = "Sainte-Genevieve-des-Bois",
+         [11] = "Saint-Michel-sur-Orge",
+         [12] = "Bretigny-sur-Orge | Marolles-en-Hurepoix",
+         [13] = "Bouray | Lardy",
+         [14] = "Chamarande | Etampes | Etrechy",
+         [16] = "Saint-Martin d'Etampes",
+         [17] = "Guillerval"},
+    [43] =
+        {[9] = "Montgeron Crosne | Yerres",
+         [10] = "Brunoy",
+         [11] = "Boussy-Saint-Antoine | Combs-la-Ville Quincy",
+         [12] = "Lieusaint Moissy",
+         [13] = "Cesson | Savigny-le-Temple Nandy",
+         [15] = "Le Mee | Melun",
+         [16] = "Chartrettes | Fontaine-le-Port | Livry-sur-Seine",
+         [17] =
+             "Champagne-sur-Seine | Hericy | La Grande Paroisse | Vernou-sur-Seine | Vulaines-sur-Seine Samoreau"},
+    [44] =
+        {[12] = "Essonnes-Robinson | Villabe",
+         [13] = "Coudray-Montceaux | Le Plessis-Chenet-IBM | Saint-Fargeau",
+         [14] = "Boissise-le-Roi | Ponthierry Pringy",
+         [15] = "Vosves",
+         [16] = "Bois-le-Roi",
+         [17] =
+             "Bagneaux-sur-Loing | Bourron-Marlotte Grez | Fontainebleau-Avon | Montereau | Montigny-sur-Loing | Moret Veneux-les-Sablons | Nemours Saint-Pierre | Saint-Mammes | Souppes | Thomery"},
+    [45] =
+        {[10] = "Grigny-Centre",
+         [11] = "Evry Courcouronnes | Orangis Bois de l'Epine",
+         [12] = "Le Bras-de-Fer - Evry Genopole"},
+    [50] =
+        {[0] = "Haussmann-Saint-Lazare",
+         [1] = "Gare du Nord | Magenta | Paris-Nord",
+         [5] = "Epinay-Villetaneuse | Saint-Denis | Sevres-Rive Gauche",
+         [6] = "La Barre-Ormesson",
+         [7] = "Champ de Courses d'Enghien | Enghien-les-Bains",
+         [8] = "Ermont-Eaubonne | Ermont-Halte | Gros-Noyer Saint-Prix",
+         [9] = "Saint-Leu-La-Foret | Taverny | Vaucelles",
+         [10] = "Bessancourt | Frepillon | Mery",
+         [11] = "Meriel | Valmondois",
+         [12] = "Bruyeres-sur-Oise | Champagne-sur-Oise | L'Isle-Adam Parmain | Persan Beaumont"},
+    [51] =
+        {[4] = "La Courneuve-Aubervilliers | La Plaine-Stade de France",
+         [5] = "Le Bourget",
+         [7] = "Blanc-Mesnil | Drancy",
+         [8] = "Aulnay-sous-Bois",
+         [9] = "Sevran Livry | Vert-Galant",
+         [10] = "Villeparisis",
+         [11] = "Compans | Mitry-Claye",
+         [12] = "Dammartin Juilly Saint-Mard | Thieux Nantouillet"},
+    [52] =
+        {[5] = "Stade de France-Saint-Denis",
+         [6] = "Pierrefitte Stains",
+         [7] = "Garges-Sarcelles",
+         [8] = "Villiers-le-Bel (Gonesse - Arnouville)",
+         [10] = "Goussainville | Les Noues | Louvres",
+         [11] = "La Borne-Blanche | Survilliers-Fosses"},
+    [53] =
+        {[6] = "Deuil Montmagny",
+         [7] = "Groslay",
+         [8] = "Sarcelles Saint-Brice",
+         [9] = "Domont | Ecouen Ezanville",
+         [10] = "Bouffemont Moisselles | Montsoult Maffliers",
+         [11] = "Belloy-Saint-Martin | Luzarches | Seugy | Viarmes | Villaines"},
+    [54] =
+        {[8] = "Cernay",
+         [9] = "Franconville Plessis-Bouchard | Montigny-Beauchamp",
+         [10] = "Pierrelaye",
+         [11] = "Pontoise | Saint-Ouen-l'Aumone-Liesse",
+         [12] = "Boissy-l'Aillerie | Osny",
+         [15] = "Chars | Montgeroult Courcelles | Santeuil Le Perchay | Us"},
+    [55] =
+        {[0] =
+             "Avenue Foch | Avenue Henri-Martin | Boulainvilliers | Kennedy Radio-France | Neuilly-Porte Maillot (Palais des congres)",
+         [1] = "Pereire-Levallois",
+         [2] = "Porte de Clichy",
+         [3] = "Saint-Ouen",
+         [4] = "Les Gresillons",
+         [5] = "Gennevilliers",
+         [6] = "Epinay-sur-Seine",
+         [7] = "Saint-Gratien"},
+    [56] = {[11] = "Auvers-sur-Oise | Chaponval | Epluches | Pont Petit"},
+    [57] = {[11] = "Presles Courcelles", [12] = "Nointel Mours"},
+    [60] =
+        {[1] = "Gare Montparnasse",
+         [4] = "Clamart | Vanves Malakoff",
+         [5] = "Bellevue | Bievres | Meudon",
+         [6] = "Chaville-Rive Gauche | Chaville-Velizy | Viroflay-Rive Gauche",
+         [7] = "Versailles-Chantiers",
+         [10] = "Saint-Cyr",
+         [11] = "Saint-Quentin-en-Yvelines - Montigny le Bretonneux | Trappes",
+         [12] = "Coignieres | La Verriere",
+         [13] = "Les Essarts-le-Roi",
+         [14] = "Le Perray | Rambouillet",
+         [15] = "Gazeran"},
+    [61] =
+        {[10] = "Fontenay-le-Fleury",
+         [11] = "Villepreux Les-Clayes",
+         [12] = "Plaisir Grignon | Plaisir Les-Clayes",
+         [13] = "Beynes | Mareil-sur-Mauldre | Maule | Nezel Aulnay",
+         [15] =
+             "Garancieres La-Queue | Montfort-l'Amaury Mere | Orgerus Behoust | Tacoigneres Richebourg | Villiers Neauphle Pontchartrain",
+         [16] = "Houdan"},
+    [63] = {[7] = "Porchefontaine | Versailles-Rive Gauche"},
+    [64] =
+        {[0] = "Invalides | Pont de l'alma",
+         [1] = "Champ de Mars-Tour Eiffel",
+         [2] = "Javel",
+         [3] = "Boulevard Victor - Pont du Garigliano | Issy-Val de Seine | Issy",
+         [5] = "Meudon-Val-Fleury"},
+    [65] =
+        {[8] = "Jouy-en-Josas | Petit-Jouy-les-Loges",
+         [9] = "Vauboyen",
+         [10] = "Igny",
+         [11] = "Massy-Palaiseau",
+         [12] = "Longjumeau",
+         [13] = "Chilly-Mazarin",
+         [14] = "Gravigny-Balizy | Petit-Vaux"},
+    [70] =
+        {[9] = "Parc des Expositions | Sevran-Beaudottes | Villepinte",
+         [10] = "Aeroport Charles de Gaulle"},
+    [72] = {[7] = "Sannois"},
+    [73] = {[11] = "Eragny Neuville | Saint-Ouen-l'Aumone (Eglise)"},
+    [75] =
+        {[7] = "Les Saules | Orly-Ville",
+         [9] = "Pont de Rungis Aeroport d'Orly | Rungis-La Fraternelle",
+         [10] = "Chemin d'Antony",
+         [12] = "Massy-Verrieres | Arpajon"},
+    [76] =
+        {[12] = "Egly | La Norville Saint-Germain-les-Arpajon",
+         [13] = "Breuillet Bruyeres-le-Châtel | Breuillet-Village | Saint-Cheron",
+         [14] = "Sermaise",
+         [15] = "Dourdan | Dourdan-la-Foret"},
+};
 
 #endif // METRO_LIST_H

+ 73 - 0
scenes/navigo_structs.h

@@ -0,0 +1,73 @@
+#include <datetime.h>
+#include <stdbool.h>
+#include <furi.h>
+
+typedef struct {
+    int transport_type;
+    int transition;
+    int service_provider;
+    int station_group_id;
+    int station_id;
+    int location_gate;
+    bool location_gate_available;
+    int device;
+    int door;
+    int side;
+    bool device_available;
+    int route_number;
+    bool route_number_available;
+    int mission;
+    bool mission_available;
+    int vehicle_id;
+    bool vehicle_id_available;
+    int used_contract;
+    bool used_contract_available;
+    DateTime date;
+} NavigoCardEvent;
+
+typedef struct {
+    int app_version;
+    int country_num;
+    int network_num;
+    DateTime end_dt;
+} NavigoCardEnv;
+
+typedef struct {
+    int card_status;
+    int commercial_id;
+} NavigoCardHolder;
+
+typedef struct {
+    int tariff;
+    int serial_number;
+    bool serial_number_available;
+    int pay_method;
+    bool pay_method_available;
+    double price_amount;
+    bool price_amount_available;
+    DateTime start_date;
+    DateTime end_date;
+    bool end_date_available;
+    int zones[5];
+    bool zones_available;
+    DateTime sale_date;
+    int sale_agent;
+    int sale_device;
+    int status;
+    int authenticator;
+} NavigoCardContract;
+
+typedef struct {
+    NavigoCardEnv environment;
+    NavigoCardHolder holder;
+    NavigoCardContract contracts[2];
+    NavigoCardEvent events[3];
+    int ticket_count;
+} NavigoCardData;
+
+typedef struct {
+    NavigoCardData* card;
+    int page_id;
+    // mutex
+    FuriMutex* mutex;
+} NavigoContext;

BIN
screenshots/Menu-Top.png


BIN
screenshots/Navigo.png


BIN
screenshots/Navigo2.png


Некоторые файлы не были показаны из-за большого количества измененных файлов