---
title: "Décoder les fichiers de tracé Navionics (.ntf / .nts)"
description: "Comment transformer un fichier « illisible » d'application de navigation en un tracé GPS exploitable — récit complet d'une rétro-ingénierie, expliqué pas à pas."
pubDate: 2026-05-26
lang: fr
tags: [navionics, protobuf, ntf, gps, retro-ingenierie]
author: "L. Benoist"
draft: false

# Bandeau technique
app: "Navionics"
os: "Android / iOS"
context:
  - key: "Format"
    value: ".ntf / .nts (Protocol Buffers)"
  - key: "Éditeur"
    value: "Navionics"
  - key: "Sortie"
    value: "GPX"
---

> **À qui s'adresse cet article.** À toute personne amenée à exploiter les
> données de l'application Navionics, même sans expérience de la
> rétro-ingénierie. Chaque notion technique est expliquée en langage simple
> avant d'être utilisée. Si vous suivez le tutoriel de bout en bout, vous
> obtiendrez le même résultat : le tracé GPS complet, sous une forme
> standard et lisible.
>
> **Avertissement.** Le format de ces fichiers n'est pas documenté
> publiquement. Tout ce qui suit a été *reconstitué* par observation et
> analyse — ce n'est pas une spécification officielle de Navionics. Les noms
> internes cités proviennent de l'application elle-même. Les emplacements de
> fichiers peuvent varier selon les versions.

---

## Sommaire

1. Le problème de départ
2. Quelques notions de base (à lire avant tout)
3. Les fichiers de Navionics et où les trouver
4. Étape 1 — Identifier le format
5. Étape 2 — Comprendre comment protobuf range les données
6. Étape 3 — Décoder le `.nts` (le résumé)
7. Étape 4 — Décoder le `.ntf` (le tracé)
8. Le découpage en détail — sur les vrais octets
9. Étape 5 — Donner un sens aux données
10. Étape 6 — Retrouver le « plan » officiel du format
11. Étape 7 — Écrire le décodeur final
12. La leçon : ce que chaque méthode apporte
13. Tutoriel reproductible — toutes les commandes
14. Annexes : outils, scripts, schéma, glossaire

---

## 1. Le problème de départ

On récupère, dans les données d'un téléphone, deux fichiers produits par
l'application de navigation marine **Navionics** : un fichier `.ntf` et un
fichier `.nts`, portant le même nom (un identifiant unique).

Ouverts dans un éditeur de texte, ils n'affichent que des caractères
incompréhensibles. Impossible de les lire directement. L'objectif : en
extraire le **tracé GPS** — la liste des positions du bateau, avec l'heure de
chaque point — sous une forme exploitable (par exemple un fichier GPX, le
format standard que lisent tous les logiciels de cartographie).

Cet article raconte **toute la démarche**, de ce fichier opaque jusqu'au tracé
lisible.

---

## 2. Quelques notions de base

Avant de commencer, quelques mots de vocabulaire. Ils reviendront partout.

**Un fichier « binaire ».** Un fichier texte contient des lettres et des
chiffres lisibles. Un fichier *binaire* contient des données brutes destinées
à la machine, pas à l'œil humain. Ce n'est pas pour autant « chiffré » (codé
secrètement) : c'est juste écrit dans un format compact que seul le bon
programme sait relire.

**Octet, et notation hexadécimale.** Un fichier est une suite d'**octets**
(des nombres de 0 à 255). Pour les afficher, on utilise souvent
l'**hexadécimal** (base 16) : c'est juste une façon d'écrire ces nombres de
manière compacte. `0x1a` est l'écriture hexadécimale du nombre 26.
L'hexadécimal n'est qu'une *manière d'afficher* — ce ne sont pas des données
différentes.

**Protocol Buffers (« protobuf »).** C'est une méthode, créée par Google, pour
ranger des données dans un fichier binaire de façon compacte. Beaucoup
d'applications l'utilisent. Un fichier protobuf n'est pas chiffré : il est
juste *organisé* selon des règles précises. Apprendre ces règles, c'est
apprendre à le lire.

**Le « schéma » (`.proto`).** Protobuf sépare deux choses : les *données*
(le fichier `.ntf`) et le *plan* qui décrit ces données (un fichier appelé
`.proto`). Le plan dit : « le premier renseignement est la latitude, le
deuxième la longitude… ». Sans le plan, on a les données mais pas leurs
étiquettes. Une grande partie du travail consistera, justement, à
**reconstituer ce plan**.

**Rétro-ingénierie.** C'est l'art de comprendre comment un système fonctionne
sans en avoir la documentation, en l'observant de l'extérieur. C'est
exactement ce qu'on fait ici.

**Une remarque rassurante.** La démarche ressemble à une enquête : on part
d'indices, on forme des hypothèses, on les teste, on se trompe parfois, on
corrige. Les impasses font partie du processus — on en rencontrera, et on
expliquera pourquoi elles sont utiles.

---

## 3. Les fichiers de Navionics et où les trouver

Navionics manipule trois types de fichiers liés aux déplacements :

| Extension | Contenu | Nature réelle |
|-----------|---------|---------------|
| `.ndc` | une **route** : un itinéraire planifié à l'avance | un fichier GPX (texte) simplement renommé |
| `.ntf` | un **tracé** : les positions réellement parcourues | un fichier binaire protobuf |
| `.nts` | le **résumé** du tracé | un fichier binaire protobuf |

**Distinction importante : route ≠ tracé.** Une *route* (`.ndc`) est un trajet
*prévu*, dessiné à la main avant de partir — elle ne prouve aucun déplacement
réel. Un *tracé* (`.ntf`) est l'enregistrement de positions *réellement
relevées* par le GPS pendant la navigation. Pour une analyse, les deux n'ont
pas du tout la même valeur.

Le `.ndc` est facile : c'est du texte, lisible dans n'importe quel éditeur.
Les `.ntf` et `.nts` sont le vrai sujet de cet article.

**Où les trouver.** Ces fichiers sont stockés dans l'espace de données réservé
à l'application Navionics sur le téléphone. À titre indicatif :

- sur **Android** : dans un dossier du type `Android/data/` au nom de
  l'application ;
- sur **iPhone** : dans le « conteneur » de l'application.

Chaque tracé est identifié par un **nom unique** (un long identifiant), et ce
même nom est partagé par le trio `.ndc` / `.ntf` / `.nts` du même
enregistrement. Les fichiers eux-mêmes ne contiennent pas de « titre » : c'est
le nom de fichier qui sert d'identité.

---

## 4. Étape 1 — Identifier le format

**But de l'étape :** comprendre à quoi on a affaire.

Premier réflexe devant un fichier inconnu : demander à l'ordinateur de
l'identifier. Sous Linux ou Mac, la commande `file` examine le début du
fichier et annonce son type :

```bash
file mon_fichier.ntf
```

Pour notre `.ntf`, la réponse est décevante : `data`. Traduction : « je ne
reconnais pas ». Beaucoup de formats laissent une signature au tout début du
fichier (quelques octets caractéristiques) ; `data` signifie qu'il n'y en a
pas. **Mais attention : « non reconnu » ne veut pas dire « chiffré ».**

Pour aller plus loin, on regarde le contenu brut du fichier, octet par octet.
C'est ce qu'on appelle un **hexdump** : un affichage qui montre, côte à côte,
les octets en hexadécimal et, quand c'est possible, les caractères lisibles
correspondants.

```bash
od -A x -t x1z mon_fichier.ntf | head
```

Dans ce hexdump, deux choses sautent aux yeux :

- des **morceaux de texte lisible** noyés dans le binaire — par exemple le nom
  d'un fuseau horaire comme `Africa/Dar_es_Salaam` ;
- juste avant ces textes, des petits octets récurrents (`08`, `12`, `1a`…).

Ces deux indices, ensemble, sont la **signature de protobuf**. Le texte lisible
montre qu'il n'y a pas de chiffrement (un fichier chiffré n'aurait *aucun*
mot lisible). Et les petits octets récurrents sont la marque de fabrique du
rangement protobuf.

**Conclusion de l'étape 1 :** le fichier n'est pas chiffré. Il est *sérialisé*
au format Protocol Buffers. Il « suffit » donc d'apprendre à lire ce format.

---

## 5. Étape 2 — Comprendre comment protobuf range les données

**But de l'étape :** apprendre les règles de rangement de protobuf. C'est la
seule partie un peu technique, mais une fois comprise, tout le reste découle.

### L'idée générale

Un fichier protobuf est une **liste de renseignements** mis bout à bout.
Chaque renseignement (protobuf dit un « champ ») est rangé en deux parties :

1. une **étiquette** qui dit *de quel renseignement il s'agit* et *quelle
   forme il a* ;
2. la **valeur** elle-même.

Il n'y a pas de sommaire, pas d'en-tête : juste cette succession
étiquette-valeur, répétée.

### L'étiquette

L'étiquette est un nombre qui contient, astucieusement compressées, deux
informations :

- le **numéro du champ** (champ n°1, n°2, n°3… — c'est protobuf qui numérote,
  il n'y a pas de noms à ce stade) ;
- le **type de forme** de la valeur (appelé *wire type*, « type sur le fil »).

Il n'existe que **quatre formes** possibles :

| Forme | Nom | Ce que c'est |
|-------|-----|--------------|
| 0 | varint | un nombre entier |
| 1 | 64 bits | un nombre sur 8 octets (souvent un nombre à virgule) |
| 2 | longueur + contenu | du texte, **ou une sous-liste de champs** |
| 5 | 32 bits | un nombre sur 4 octets |

La forme n°2 est la plus riche : elle sert aussi bien pour du texte que pour
ranger une **structure à l'intérieur d'une autre**. Protobuf est donc
*emboîtable* : un champ peut contenir une sous-structure, qui contient
elle-même des champs, et ainsi de suite. C'est pour cela que les données
décodées ressemblent à des boîtes dans des boîtes.

### Le « varint » : comment protobuf écrit les nombres

Protobuf range les nombres entiers d'une façon astucieuse appelée *varint* :
un petit nombre prend peu de place, un grand nombre en prend plus. Pour cet
article, retenez simplement que **c'est une façon compacte d'écrire un
entier** ; les outils savent le relire automatiquement, on n'a pas besoin de
le faire à la main.

### Un détail qui aura son importance : le « zigzag »

Quand un nombre peut être **négatif** (comme une coordonnée dans l'hémisphère
sud), protobuf utilise une variante d'encodage appelée **zigzag**. Sans entrer
dans le calcul, l'idée est : le zigzag « replie » les nombres pour que les
petits négatifs restent compacts.

La conséquence pratique : un nombre stocké dans le fichier **n'est pas
directement la valeur réelle**. Il faut lui appliquer une petite
transformation (le « décodage zigzag ») pour retrouver la vraie valeur, qui
peut être négative. On verra que c'est exactement le piège des coordonnées
GPS.

### Ce qu'il faut retenir de l'étape 2

Un fichier protobuf = une suite de champs. Chaque champ a un **numéro** et une
**forme**. Les champs peuvent s'emboîter. On peut lire la *structure* d'un
fichier protobuf sans rien connaître d'autre — mais on n'aura que des
numéros (« champ 3 »), pas encore de noms (« latitude »). Donner les noms,
ce sera l'étape 6.

---

## 6. Étape 3 — Décoder le `.nts` (le résumé)

**But de l'étape :** lire le fichier résumé, plus simple, pour se faire la main.

Il existe un outil officiel pour lire un fichier protobuf : **`protoc`**, le
« compilateur » de Protocol Buffers. Il possède une option, `--decode_raw`,
qui affiche la structure d'un fichier protobuf **sans avoir besoin du plan**.
C'est exactement ce qu'il nous faut pour commencer.

Le `.nts` est le plus simple des deux fichiers : il contient **un seul bloc**
protobuf. On peut donc l'envoyer directement à `protoc` :

```bash
protoc --decode_raw < mon_fichier.nts
```

> **Note pour les utilisateurs de Windows / PowerShell.** Le symbole `<`
> ci-dessus ne fonctionne pas dans PowerShell (il y est interdit). Il faut
> écrire à la place :
>
> ```powershell
> cmd /c "protoc --decode_raw < mon_fichier.nts"
> ```
>
> Explication dans la section tutoriel.

Le résultat ressemble à ceci (extrait) :

```
1: 2
2: 0
3 {
  1 { 1: 17946197   2: 83170666 }
  2 { 1: 28279287   2: 81508812 }
}
4 { 1 { 1: 1763751347   2: 601 }   2 { 1: 21600   2: "Africa/Dar_es_Salaam" } }
```

On *voit* la structure : un champ 1, un champ 2, un champ 3 qui contient
lui-même des sous-champs, etc. Les accolades `{ }` marquent les structures
emboîtées. Mais à ce stade, on ne sait pas encore ce que *signifie* « champ
3 ». On y reviendra.

On retiendra déjà que le `.nts` contient visiblement des coordonnées (les
grands nombres du champ 3), une date (le grand nombre `1763751347` ressemble à
un horodatage) et un nom de fuseau horaire.

**Bon à savoir : le `.nts` est un résumé recalculable.** Toutes ses
valeurs (zone géographique, durée, distance…) peuvent être recalculées à
partir du `.ntf`. C'est un index léger que l'application lit pour afficher
rapidement un aperçu du tracé. Cela donne une vérification gratuite : si le
résumé recalculé depuis le `.ntf` correspond au `.nts` fourni, le décodage est
juste.

---

## 7. Étape 4 — Décoder le `.ntf` (le tracé)

**But de l'étape :** lire le fichier de tracé — plus gros, et organisé
différemment.

Si on tente la même commande sur le `.ntf` :

```bash
protoc --decode_raw < mon_fichier.ntf
```

…on obtient une erreur : `Failed to parse input` (« impossible d'analyser »).

Pourquoi ? Parce que le `.ntf` n'est **pas un seul bloc** protobuf, mais une
**longue suite de blocs** mis bout à bout. Et chaque bloc est précédé d'un
petit nombre qui indique sa taille.

**Une image.** Le `.ntf` est comme un train de wagons accrochés les uns aux
autres, sans séparation visible. Avant chaque wagon, une étiquette indique sa
longueur. On ne peut pas lire le train d'un coup : il faut lire l'étiquette
(« le prochain wagon fait 47 octets »), détacher ce wagon, l'ouvrir, puis
passer au suivant. L'outil `protoc`, lui, s'attend à recevoir *un seul wagon* —
d'où l'erreur.

La solution : un petit programme qui fait le travail de **découpage**. Il lit
la taille, détache un bloc, recommence — et nous donne chaque bloc séparément.
Ce programme est fourni en annexe (`ntf_record.py`). Une fois le découpage
fait, chaque bloc isolé est, lui, un fichier protobuf normal que `protoc` sait
lire :

```bash
# combien de blocs dans le fichier ?
python3 ntf_record.py mon_fichier.ntf --count

# afficher la structure du bloc n°1
python3 ntf_record.py mon_fichier.ntf 1 | protoc --decode_raw
```

En examinant ces blocs, on découvre que le `.ntf` contient des **dizaines de
milliers** de blocs : ce sont les points du tracé, un bloc par point.

**Conclusion de l'étape 4 :** le `.ntf` est un flux de blocs ; chaque bloc est
un point du tracé ; il faut d'abord découper, ensuite décoder.

---

## 8. Le découpage en détail — sur les vrais octets

Le découpage est *la* mécanique de base : sans elle, rien n'est possible.
Elle paraît mystérieuse, mais elle est en réalité très simple — c'est de la
**lecture**, pas du calcul. Détaillons-la sur les vrais octets d'un fichier.

### L'image du colis

Imaginez des colis sur un tapis roulant, collés les uns aux autres, **sans
séparateur**. Sur chaque colis, une étiquette à l'avant indique sa taille :
« ce colis fait 49 cm ». Vous lisez l'étiquette, vous mesurez 49 cm, vous
coupez : le premier colis est isolé. Vous êtes alors pile devant l'étiquette
du suivant. Vous recommencez. Vous n'avez **jamais à deviner** où couper :
l'étiquette vous le dit à chaque fois.

Le `.ntf` est exactement cela :

```
[longueur][..... bloc 1 .....][longueur][..... bloc 2 .....][longueur][... bloc 3 ...]
```

### La procédure

On utilise un **curseur** : un repère qui pointe un endroit dans le fichier.
Au départ, il est à la position 0 (tout début).

1. À l'endroit du curseur, **lire le nombre « longueur »**.
2. Avancer le curseur juste après ce nombre.
3. **Prélever** autant d'octets que la longueur indiquée : c'est un bloc.
4. Avancer le curseur après le bloc.
5. **Recommencer** au point 1, tant qu'on n'a pas atteint la fin du fichier.

Aucun calcul savant : on lit une longueur, on prélève, on avance, on répète.

### Sur les vrais octets du fichier

Les 40 premiers octets d'un `.ntf` réel, en hexadécimal :

```
31 08 01 12 29 0a 09 08 b3 eb 82 c9 06 10 d9 04 12 1c 08 e0 a8 01 ...
└┘ └────────────────────── ... ──────────────────────────────┘
↑          ↑
longueur   contenu du bloc 1
```

**Bloc 1.** Le curseur est à la position 0. L'octet qui s'y trouve est `0x31`.
En décimal, `0x31` vaut **49**. C'est la longueur : le bloc 1 fait 49 octets.
Le curseur avance d'un cran (position 1), on prélève 49 octets — c'est le
bloc 1 — puis le curseur avance de 49 : il est à la **position 50**.

**Bloc 2.** Le curseur est à la position 50. L'octet est `0x35`, soit **53**.
Curseur +1 (position 51), on prélève 53 octets, curseur +53 : **position 104**.

**Bloc 3.** Le curseur est à la position 104. L'octet est `0x26`, soit **38**.
Curseur +1, on prélève 38 octets, curseur +38 : **position 143**.

Le tableau résume la mécanique :

| | Bloc 1 | Bloc 2 | Bloc 3 |
|---|---|---|---|
| curseur au départ | 0 | 50 | 104 |
| octet de longueur lu | `0x31` = 49 | `0x35` = 53 | `0x26` = 38 |
| curseur après le bloc | 50 | 104 | 143 |

Le « curseur après le bloc » d'une ligne devient le « curseur au départ » de
la suivante : la chaîne s'enchaîne d'elle-même, et continue à l'identique
jusqu'à la fin du fichier.

**Le mot juste : la longueur n'est pas calculée, elle est lue.** Elle est déjà
écrite dans le fichier, juste devant chaque bloc — l'application l'y a mise
exprès pour qu'on puisse découper. Le terme technique pour ce genre de format
est « préfixé par la longueur » (*length-prefixed*).

### Quand la longueur tient sur deux octets

Dans l'exemple ci-dessus, les longueurs (49, 53, 38) tenaient chacune sur
**un seul octet**, parce qu'elles sont petites. Mais une longueur peut
dépasser ce qu'un octet peut contenir (un octet ne va que jusqu'à 255). La
longueur s'écrit alors sur **plusieurs octets**, selon la règle du *varint*.

Cette règle repose sur **un seul bit** de chaque octet — le bit le plus à
gauche, qui sert de feu de signalisation :

- bit de gauche = **1** → « ça continue, l'octet suivant fait encore partie du
  nombre » ;
- bit de gauche = **0** → « c'est fini, dernier octet du nombre ».

Le truc pratique pour le repérer **sans convertir en binaire** : il suffit de
regarder la valeur de l'octet.

- octet **inférieur à `0x80`** (soit `00` à `7f`) → bit de gauche à 0 →
  **dernier octet** ;
- octet **égal ou supérieur à `0x80`** (soit `80` à `ff`) → bit de gauche à 1
  → **ça continue**.

Règle de poche : *un octet ≥ `0x80`, le nombre continue ; un octet < `0x80`, le
nombre s'arrête là.*

Exemples concrets :

| Longueur | Nombre d'octets | En hexadécimal | En binaire |
|----------|-----------------|----------------|------------|
| 49 | 1 | `31` | `00110001` |
| 127 | 1 | `7f` | `01111111` |
| 128 | 2 | `80 01` | `10000000  00000001` |
| 300 | 2 | `ac 02` | `10101100  00000010` |

Pour la longueur 300 → `ac 02` :

```
1er octet : ac = 1010 1100   bit de gauche = 1  → "ça continue"
2e octet  : 02 = 0000 0010   bit de gauche = 0  → "c'est fini"
```

Le premier octet `0xac` (= 172) est ≥ `0x80` : signal que le nombre n'est pas
terminé. On lit donc le deuxième octet, `0x02`, qui est < `0x80` : fin du
nombre. La longueur occupait 2 octets.

**C'est exactement ce que fait la fonction `read_varint`** du script (annexe
A) : elle lit les octets un par un et s'arrête au premier octet inférieur à
`0x80`. C'est aussi pourquoi elle renvoie la *nouvelle position du curseur* :
elle seule sait si la longueur a occupé un, deux ou trois octets, et fait donc
avancer le curseur du bon nombre de crans.

### Le code, relu avec tout ça en tête

Le cœur de `ntf_record.py` se lit maintenant comme du français :

```python
def records(buf):                     # buf = tout le contenu du fichier
    pos = 0                            # le curseur, au tout début
    while pos < len(buf):              # tant qu'on n'est pas au bout :
        ln, pos = read_varint(buf, pos)   # 1+2 : lire la longueur, avancer le curseur
        yield buf[pos:pos+ln]             # 3   : prélever 'ln' octets = un bloc
        pos += ln                         # 4   : avancer le curseur après le bloc
                                          # 5   : la boucle 'while' recommence
```

Quatre lignes. `ln` est la longueur lue ; `buf[pos:pos+ln]` désigne les octets
du curseur jusqu'à curseur + longueur (le prélèvement du colis) ; `pos += ln`
fait avancer le curseur. La boucle reprend toute seule jusqu'à la fin du
fichier.

---

## 9. Étape 5 — Donner un sens aux données

**But de l'étape :** passer des numéros (« champ 3 ») au sens (« latitude »).

À ce stade, on sait *lire la structure* mais pas *interpréter*. `protoc` nous
donne par exemple, pour un point :

```
1: 0
2 { 1 { 1: 2   2: 228 } }
3 { 1 { 1: 45   2: 0 } }
5 { 1: 12   2: 199 }
```

Des numéros et des nombres. Pour transformer ça en information utile, quatre
sous-étapes, puis une vérification.

### 9.1 Deviner le rôle de chaque champ

C'est un travail de détective. On compare beaucoup de points entre eux et on
raisonne sur les valeurs :

- un champ qui ne vaut jamais que 0 ou 1 → c'est probablement un
  **interrupteur** (oui/non) ;
- un champ qui vaut environ 1,7 milliard → c'est un **horodatage** (le nombre
  de secondes écoulées depuis 1970, une convention informatique universelle) ;
- un champ qui augmente régulièrement de point en point → un **compteur** ;
- un champ entre 0 et 36000 → probablement un **cap** (une direction en
  degrés, multipliée par 100).

On recoupe aussi avec ce qu'on connaît : le `.nts` (qui contient les mêmes
types de valeurs), et la réalité (on convertit des coordonnées candidates et
on vérifie qu'elles tombent bien en mer, dans une zone cohérente).

### 9.2 Appliquer les bonnes transformations

Les nombres bruts doivent souvent être retravaillés :

- les nombres qui peuvent être négatifs sont en **zigzag** : il faut appliquer
  le décodage zigzag pour retrouver la vraie valeur (voir étape 2) ;
- les **coordonnées** ne sont pas en degrés directement : elles sont
  multipliées par un million. Le nombre `8 973 097` stocké dans le fichier
  signifie en réalité `8,973097` degrés. Il faut donc diviser par 1 000 000 ;
- les **horodatages** (secondes depuis 1970) se convertissent en date et heure
  normales.

### 9.3 Recoller les points entre eux — *le point délicat*

C'est ici qu'intervient la nuance la plus importante de tout l'article.

Quand on regarde les points du `.ntf`, on s'aperçoit que **le premier point
contient des valeurs complètes** (une vraie date, une vraie position), mais
que **les points suivants ne contiennent que de tout petits nombres**.

Cela veut dire que les points suivants ne stockent pas leur position complète,
mais seulement un **écart**. C'est une technique de compression classique :
plutôt que de réécrire à chaque fois « 8,973097° de latitude », on écrit juste
« +0,000023° par rapport à la référence », ce qui prend bien moins de place.

Le premier point sert donc de **point de référence** (on l'appellera
l'**ancre**). Mais il y a alors **deux façons possibles** de comprendre les
écarts des points suivants — et c'est là qu'on peut se tromper :

> **Façon A — « par rapport au point précédent ».**
> Chaque écart se compte depuis le point d'avant.
> *« Le bateau a avancé de 3 mètres depuis la note précédente. »*
> Pour connaître la position réelle d'un point, il faut **additionner tous les
> écarts depuis le début**, dans l'ordre.
>
> **Façon B — « par rapport à l'ancre ».**
> Chaque écart se compte depuis le point de référence fixe (le premier point).
> *« Le bateau est à 41 mètres du point de départ. »*
> Chaque point se lit **tout seul** : il suffit d'ajouter son écart à l'ancre.

Ces deux façons sont visuellement identiques dans le fichier — les mêmes
petits nombres. **Rien dans le format ne dit laquelle est la bonne.** Il faut
le découvrir en essayant.

Et c'est exactement l'erreur qui a été commise en cours de route : on a
d'abord supposé la **façon A** (additionner depuis le point précédent).
Résultat : des positions absurdes — une latitude de 159 000 degrés, une date
en l'an 2243. En testant l'autre hypothèse, la **façon B** (chaque écart
compté depuis l'ancre), tout est rentré dans l'ordre instantanément.

**La bonne réponse pour Navionics est donc la façon B : chaque point est un
écart direct par rapport à l'ancre, et non par rapport au point précédent.**
Autrement dit, une fois l'ancre connue, chaque point se reconstitue
indépendamment des autres : on prend l'ancre, on ajoute l'écart du point, on a
sa position absolue.

C'est précisément cette règle-là — « écart direct, pas cumulé » — qu'aucun
plan, aucun schéma ne pouvait nous donner : elle ne se trouve qu'en testant
contre un vrai fichier et en regardant si le résultat a un sens.

### 9.4 Mettre en forme

Une fois chaque point transformé en (date, latitude, longitude, vitesse), on
dispose d'une liste de positions. On l'enregistre dans un format **standard**,
le **GPX** — un format de tracé que lisent tous les logiciels de cartographie.
On obtient enfin un fichier ouvrable dans Google Earth, un SIG, ou un logiciel
d'analyse.

### 9.5 Vérifier

Dernière sous-étape, essentielle : on recalcule, à partir du tracé
reconstitué, des grandeurs globales (la zone géographique couverte, la durée
totale) et on les compare au contenu du `.nts` (le résumé). **Si les deux
concordent, c'est que l'interprétation est juste.** C'est ce qu'on appelle une
validation croisée : deux sources indépendantes qui disent la même chose.

---

## 10. Étape 6 — Retrouver le « plan » officiel du format

**But de l'étape :** au lieu de *deviner* le rôle des champs, retrouver le
plan exact que les développeurs de Navionics ont utilisé.

Jusqu'ici, on a deviné. C'est efficace, mais on aimerait une **certitude**, et
les vrais noms des champs. Le plan (`.proto`) n'est pas livré avec
l'application — mais il est peut-être *récupérable*. Voici la chasse, étape
par étape. C'est la partie la plus « enquête » de l'article.

### 10.1 Première piste : chercher des fichiers `.proto` dans l'application

On peut récupérer l'application elle-même (sous Android, c'est un fichier
`.apk` ; on peut l'obtenir légalement, par exemple sur un site de
téléchargement d'APK, ou depuis un téléphone) et regarder à l'intérieur — un
`.apk` est en réalité une simple archive compressée.

En cherchant, on **trouve effectivement des fichiers `.proto`**. Déception : en
les ouvrant, on constate qu'ils portent tous la mention `package
google.protobuf`. Ce sont les fichiers *standards* de protobuf lui-même,
livrés avec n'importe quelle application qui utilise cette technologie. C'est
l'équivalent de trouver des pièces standard dans une voiture : ça confirme la
marque du moteur, mais ça n'apprend rien sur *ce* véhicule en particulier.

**Le critère simple pour trier :** un `.proto` marqué `package google.protobuf`
= standard, sans intérêt. Un `.proto` avec un autre nom = potentiellement le
plan recherché. Ici, on n'en trouve aucun. Première piste : impasse.

### 10.2 Deuxième piste : regarder dans le code de l'application

L'application contient son code, dans des fichiers appelés `.dex`. On peut les
inspecter. On y trouve bien des éléments liés aux tracés — mais **aucun ne
contient le plan du format**. En revanche, on remarque un détail décisif : le
code mentionne une partie « native ».

**Explication.** Une application est souvent composée de deux morceaux : une
partie écrite dans le langage habituel du téléphone, et une partie « native »,
écrite dans un langage plus proche de la machine (C++), réunie dans des
fichiers `.so`. Ici, le code montre que **toute la gestion des tracés se passe
dans la partie native**. Le plan du format n'est donc pas dans le code
inspecté : il est dans un fichier `.so`.

Cette piste-là n'a pas donné le plan, mais elle a fait quelque chose
d'important : elle a **indiqué où chercher**.

### 10.3 Troisième piste : la bibliothèque native

On récupère donc le bon fichier `.so` — la bibliothèque native principale de
Navionics (un fichier d'une trentaine de mégaoctets). C'est un **programme
compilé** : du code transformé en instructions machine, pas du tout lisible
tel quel.

Mais un programme compilé conserve presque toujours une liste de **noms
internes** : les noms des « structures » et des « fonctions » que les
développeurs ont utilisés. Ces noms sont nécessaires au bon fonctionnement du
programme, et ils survivent dans le fichier. On peut les extraire.

Et là, on touche le but : parmi ces noms, on trouve toute une famille de
structures clairement liées au format du tracé — `NavPoint`, `NavLatLon`,
`NavLocation`, `NavTime`, `NavVelocity`, `NavMeta`, et d'autres. **Ce sont les
structures du format Navionics.** `NavPoint` (« point de navigation »), avec son
suffixe au singulier, est manifestement la structure d'un point du tracé.

### 10.4 Lire le plan dans le programme

Reste à connaître, pour chaque structure, la liste de ses champs. Le programme
contient une fonction qui *écrit* chaque structure dans le fichier. En
examinant cette fonction — on « désassemble » le programme, c'est-à-dire qu'on
traduit le code machine en instructions lisibles — on voit défiler, dans
l'ordre, l'écriture de chaque champ : son **numéro** et sa **forme**.

De plus, le programme contient aussi les **noms** des champs, rangés à part.
En croisant les deux — la liste des numéros/formes d'un côté, la liste des noms
de l'autre — on reconstitue le plan complet.

Et ce plan est remarquablement clair, car les développeurs de Navionics ont
donné à leurs champs des noms **explicites, avec les unités** :

- `LatitudeDegX1000000` → « latitude en degrés, multipliée par 1 000 000 » ;
- `SpeedMsX100` → « vitesse en mètres par seconde, multipliée par 100 » ;
- `UTCUnixTimeS` → « date, en secondes depuis 1970 » ;
- `Millisecond` → la fraction de seconde ;
- et ainsi de suite.

Le nom du champ nous donne *gratuitement* l'unité et le facteur de conversion.

### 10.5 Le plan reconstitué

On obtient ainsi le plan complet du format — 22 structures. Les deux plus
importantes :

**Un point de tracé (`NavPoint`)** contient :
un interrupteur « point complet ou simple écart », un sous-bloc *temps*, un
sous-bloc *position*, et des sous-blocs optionnels *profondeur*, *vitesse*,
*température de l'eau*, *événement*, *poisson*.

**Une position (`NavLocation` → `NavLatLon`)** contient :
la latitude et la longitude (en millionièmes de degré), l'altitude, et la
précision de la mesure.

Le plan complet est fourni en annexe C.

### 10.6 Ce que le plan a confirmé

Le plan officiel a **validé toutes les hypothèses** faites à l'étape 5 en
devinant :

| On avait deviné… | Le plan confirme… |
|------------------|-------------------|
| un interrupteur « complet / écart » | le champ `IsFull` (oui/non) |
| un champ « position » | le champ `Location` |
| coordonnées multipliées par un million | le nom dit `…DegX1000000` |
| les coordonnées peuvent être négatives (zigzag) | le type confirme le zigzag |

On est passé de « interprétation très probable » à « structure confirmée par
le code de l'application ». Pour un dossier sérieux, c'est la différence entre
une supposition et une preuve.

### 10.7 Ce que le plan **ne dit pas**

Point crucial, déjà annoncé : le plan décrit la *forme* des données (« le
champ 3 est une position »). Il ne dit **pas** la règle d'interprétation des
écarts — la fameuse « façon A ou façon B » de l'étape 5.3. Cette règle n'est
ni dans le plan, ni dans la fonction d'écriture : elle se vérifie en testant.
**Le plan et le test sont donc complémentaires : il faut les deux.**

---

## 11. Étape 7 — Écrire le décodeur final

**But de l'étape :** réunir tout ce qui précède dans un programme qui décode le
`.ntf` automatiquement.

On dispose maintenant de tout :

- les **règles de protobuf** (étape 2) ;
- le découpage du flux en blocs (étape 4) ;
- le **plan** du format, avec noms et unités (étape 6) ;
- la **règle d'interprétation** : chaque point est un écart *direct par rapport
  à l'ancre* (étape 5.3).

Le décodeur final (fourni en annexe, `navionics_ntf_parser.py`) enchaîne :
découper le flux → décoder chaque bloc selon le plan → appliquer les
transformations (zigzag, division par un million, conversion des dates) →
recoller chaque point à l'ancre → écrire le tout en GPX.

**Le résultat**, sur le fichier analysé :

- **37 456 points** GPS, chacun horodaté ;
- une période de navigation de **plus de 61 heures** ;
- un fichier GPX prêt à être ouvert dans n'importe quel logiciel de
  cartographie.

**La vérification finale.** Ce résultat est identique, au chiffre près, à
celui obtenu tout au début par simple devinette (étape 5) : même nombre de
points, mêmes dates, même zone géographique. Deux chemins indépendants — la
devinette du début et le plan extrait du programme — aboutissent exactement au
même tracé. C'est la preuve que le décodage est correct.

---

## 12. La leçon : ce que chaque méthode apporte

Cet article a suivi un parcours en zigzag, avec des impasses. Ce n'est pas un
défaut du récit : **c'est ainsi que fonctionne réellement une
rétro-ingénierie.** Chaque méthode a apporté une pièce :

- **Deviner** (étape 5) a donné la structure générale *et* la règle des écarts
  — mais avec des noms de champs inventés.
- **Chercher des `.proto` tout faits** (étape 6.1) n'a rien donné — mais a
  écarté une hypothèse.
- **Inspecter le code de l'application** (étape 6.2) n'a pas donné le plan non
  plus — mais a révélé *où* il se trouvait (la partie native).
- **Analyser la bibliothèque native** (étapes 6.3 à 6.5) a donné le plan
  complet, avec les vrais noms et les unités — mais pas la règle des écarts.

Aucune méthode seule ne suffisait. La devinette de départ et le plan officiel
de la fin sont **complémentaires** : l'un a donné la logique, l'autre les
noms ; et c'est leur **réunion**, vérifiée contre le fichier réel, qui produit
un décodeur correct.

Pour quiconque débute : retenez surtout cette idée. On avance par hypothèses,
on accepte les impasses (elles vous disent où chercher ensuite), et **on
vérifie toujours** le résultat contre quelque chose d'indépendant.

---

## 13. Tutoriel reproductible — toutes les commandes

Cette section regroupe **toutes les commandes** pour refaire la manipulation de
bout en bout et obtenir le tracé GPX. Les scripts mentionnés sont en annexe.

### 13.1 Outils à installer

| Outil | À quoi il sert | Installation |
|-------|----------------|--------------|
| **Python 3** | exécuter les scripts de décodage | inclus sur Mac/Linux ; sur Windows : `winget install Python.Python.3.12` |
| **protoc** | afficher la structure d'un fichier protobuf | voir ci-dessous |
| **binutils aarch64** | examiner la bibliothèque native (étape 6) | `sudo apt install binutils-aarch64-linux-gnu` (Linux) |
| **un dézippeur** | ouvrir l'`.apk` et la bibliothèque (étape 6) | `unzip`, inclus partout |

> Les deux derniers ne servent **que** si vous voulez refaire vous-même
> l'extraction du plan (étape 6). Pour seulement décoder un `.ntf`, Python et
> les scripts fournis suffisent.

**Installer `protoc` :**

- *Windows* : `winget install protobuf` puis ouvrir un nouveau terminal.
- *Mac* : `brew install protobuf`.
- *Linux* : `sudo apt install protobuf-compiler`.

Vérifier que ça marche : `protoc --version` doit afficher un numéro.

### 13.2 Décoder le résumé `.nts`

```bash
# Linux / Mac :
protoc --decode_raw < fichier.nts
```

```powershell
# Windows (PowerShell) :
cmd /c "protoc --decode_raw < fichier.nts"
```

> **Pourquoi le `cmd /c` sous Windows ?** PowerShell interdit le symbole `<`.
> La commande `cmd /c "..."` confie l'opération à l'ancien terminal Windows,
> qui, lui, l'accepte. À défaut, on peut faire toute la manipulation sous
> Linux, ou dans le sous-système Linux de Windows (WSL).

### 13.3 Explorer le tracé `.ntf`

Le `.ntf` ne se décode pas directement (c'est un flux de blocs). On utilise le
script `ntf_record.py` (annexe A) pour le découper :

```bash
# nombre de blocs (points) dans le fichier
python3 ntf_record.py fichier.ntf --count

# afficher la structure du bloc n°1
python3 ntf_record.py fichier.ntf 1 | protoc --decode_raw
```

Sous Windows, encadrer la deuxième commande dans `cmd /c "..."` pour la même
raison que ci-dessus.

### 13.4 Décoder le tracé complet en GPX

C'est l'étape finale. Le script `navionics_ntf_parser.py` (annexe B) fait tout
automatiquement :

```bash
python3 navionics_ntf_parser.py fichier.ntf tracé.gpx
```

Le script affiche un résumé (nombre de points, période, zone, vitesses) et
écrit le fichier `tracé.gpx`. Ce GPX s'ouvre dans Google Earth, QGIS, ou tout
logiciel de cartographie.

### 13.5 (Optionnel) Refaire l'extraction du plan — étape 6

Réservé à ceux qui veulent reproduire la rétro-ingénierie complète.

```bash
# 1. ouvrir l'apk de l'application (c'est une archive)
unzip base.apk -d apk_extrait

# 2. la bibliothèque native est dans un "split" de l'apk
unzip split_config.arm64_v8a.apk lib/arm64-v8a/libnavnative.so

# 3. extraire la liste des noms internes liés au format
#    (les structures du tracé apparaissent sous la forme Nav...)
aarch64-linux-gnu-readelf -W --dyn-syms libnavnative.so | grep "Nav"

# 4. extraire les noms de champs de chaque structure
aarch64-linux-gnu-readelf -W --dyn-syms libnavnative.so | grep "FieldNumber"
```

La lecture précise du plan (numéros + formes des champs) demande de
*désassembler* la bibliothèque ; cette partie, plus avancée, est décrite à
l'étape 10.4 et utilise l'outil `objdump` (paquet `binutils-aarch64-linux-gnu`)
ou un logiciel d'analyse comme **Ghidra** (gratuit, édité par la NSA). Le plan
déjà reconstitué étant fourni en annexe C, il n'est **pas** nécessaire de
refaire cette extraction pour décoder un `.ntf`.

### 13.6 Récapitulatif du parcours

```
fichier .ntf (illisible)
   │
   │  étape 1 : file + hexdump        -> c'est du protobuf, non chiffré
   │  étape 4 : ntf_record.py         -> découpage en blocs (les points)
   │  étape 3 : protoc --decode_raw   -> structure visible (numéros)
   │  étape 6 : plan extrait du .so   -> noms et unités des champs
   │  étape 5 : règle des écarts      -> chaque point = écart depuis l'ancre
   │  étape 7 : navionics_ntf_parser  -> décodage complet
   ▼
tracé.gpx (lisible dans tout logiciel de cartographie)
```

---

## 14. Annexes

### Annexe A — `ntf_record.py` (découpe le flux `.ntf` en blocs)

```python
#!/usr/bin/env python3
# Découpe un fichier .ntf en ses blocs successifs.
# Le .ntf est une suite de [taille][bloc][taille][bloc]...
#
# Usage :
#   python3 ntf_record.py fichier.ntf --count   -> nombre de blocs
#   python3 ntf_record.py fichier.ntf N         -> envoie le bloc N
#   python3 ntf_record.py fichier.ntf N | protoc --decode_raw
import sys

def read_varint(buf, pos):
    result = shift = 0
    while True:
        b = buf[pos]; pos += 1
        result |= (b & 0x7f) << shift
        if not (b & 0x80):
            return result, pos
        shift += 7

def records(buf):
    pos = 0
    while pos < len(buf):
        ln, pos = read_varint(buf, pos)
        yield buf[pos:pos+ln]
        pos += ln

if __name__ == "__main__":
    data = open(sys.argv[1], "rb").read()
    if sys.argv[2] == "--count":
        print(sum(1 for _ in records(data)))
    else:
        n = int(sys.argv[2])
        for i, rec in enumerate(records(data)):
            if i == n:
                sys.stdout.buffer.write(rec)
                break
```

### Annexe B — `navionics_ntf_parser.py` (décodeur complet `.ntf` → GPX)

Le décodeur complet est fourni dans le fichier séparé
`navionics_ntf_parser.py`. Il est piloté par le plan de l'annexe C et applique
la règle « chaque point est un écart direct par rapport à l'ancre ».

Utilisation :

```bash
python3 navionics_ntf_parser.py fichier.ntf tracé.gpx
```

### Annexe C — Le plan du format (`navionics_track.proto`)

Le plan complet reconstitué — les 22 structures, avec numéros, formes et noms
de champs — est fourni dans le fichier séparé `navionics_track.proto`.

Rappel : il s'agit d'une **reconstitution** par rétro-ingénierie, pas d'une
spécification officielle de Navionics. Les structures principales sont
`NavPoint` (un point du tracé), `NavLocation` et `NavLatLon` (la position),
`NavTime` (l'horodatage) et `NavMeta` (le résumé contenu dans le `.nts`).

### Annexe D — Glossaire

| Terme | Explication simple |
|-------|--------------------|
| **Binaire** | fichier de données brutes, non destiné à l'œil humain (≠ chiffré) |
| **Hexadécimal** | manière compacte d'écrire des nombres ; sert à afficher les octets |
| **Octet** | la plus petite unité d'un fichier (un nombre de 0 à 255) |
| **Protobuf** | méthode de Google pour ranger des données dans un fichier binaire |
| **Schéma / `.proto`** | le « plan » qui décrit ce que contient un fichier protobuf |
| **Champ** | un renseignement dans un fichier protobuf (a un numéro et une forme) |
| **Varint** | façon compacte d'écrire un nombre entier en protobuf |
| **Zigzag** | variante d'encodage pour les nombres pouvant être négatifs |
| **Ancre** | le point de référence (le premier point complet du tracé) |
| **Sérialisé** | rangé sous forme de fichier selon des règles précises |
| **Rétro-ingénierie** | comprendre un système sans sa documentation, en l'observant |
| **`.so` / bibliothèque native** | partie d'une application écrite en langage proche de la machine |
| **Désassembler** | traduire un programme compilé en instructions lisibles |
| **GPX** | format de fichier standard pour les tracés GPS |

### Annexe E — Aide-mémoire des commandes

```bash
# Identifier le fichier
file fichier.ntf

# Voir le contenu brut (hexdump)
od -A x -t x1z fichier.ntf | head

# Décoder le résumé .nts
protoc --decode_raw < fichier.nts                  # Linux / Mac
cmd /c "protoc --decode_raw < fichier.nts"         # Windows PowerShell

# Explorer le tracé .ntf
python3 ntf_record.py fichier.ntf --count
python3 ntf_record.py fichier.ntf 1 | protoc --decode_raw

# Décoder le tracé complet en GPX
python3 navionics_ntf_parser.py fichier.ntf tracé.gpx
```
