airmate-navdata-sync 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ # Holds the Airmate customer id and the Dynon serial.
2
+ config.toml
3
+
4
+ # Navdata (multi-GB, licensed per subscription) and the staging tree.
5
+ downloads/
6
+ downloads_prepared/
7
+ *.dup
8
+ *.DUP
9
+ *.dcf
10
+ *.key
11
+ *.mbtiles
12
+
13
+ __pycache__/
14
+ *.py[cod]
15
+ .venv/
16
+ venv/
17
+
18
+ # Build artefacts from `python -m build`.
19
+ dist/
20
+ build/
21
+ *.egg-info/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rémy Hubscher
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,188 @@
1
+ Metadata-Version: 2.5
2
+ Name: airmate-navdata-sync
3
+ Version: 0.2.0
4
+ Summary: Download an Airmate AIRAC cycle and lay it out for a Dynon SkyView USB stick
5
+ Project-URL: Homepage, https://github.com/Natim/skydemon-airmate-navdata-sync
6
+ Project-URL: Source, https://github.com/Natim/skydemon-airmate-navdata-sync
7
+ Project-URL: Issues, https://github.com/Natim/skydemon-airmate-navdata-sync/issues
8
+ Author-email: Rémy Hubscher <remy@yampa.ai>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: airac,airmate,aviation,dynon,navdata,skyview,vfr
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: GIS
21
+ Classifier: Topic :: Utilities
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: aiofile>=3.9
24
+ Requires-Dist: httpx[http2]>=0.28
25
+ Requires-Dist: rich>=13.9
26
+ Requires-Dist: tqdm>=4.67
27
+ Provides-Extra: mbtiles
28
+ Requires-Dist: pillow>=10.0; extra == 'mbtiles'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # skydemon-airmate-navdata-sync
32
+
33
+ Keeps the navigation data of a Dynon SkyView up to date: it downloads an
34
+ [Airmate](https://www.airmate.aero) AIRAC cycle, lays the files out exactly the
35
+ way the Dynon expects them, and copies the result onto every configured USB
36
+ stick without rewriting the gigabytes that did not change.
37
+
38
+ Everything specific to a subscription or a machine — the Airmate customer id,
39
+ the Dynon serial, the AIRAC cycle, the local paths — lives in `config.toml`,
40
+ which is git-ignored. The code contains no ids.
41
+
42
+ ## Requirements
43
+
44
+ - Python 3.11 or newer (the config reader uses the standard-library `tomllib`)
45
+ - An active Airmate subscription bound to your Dynon serial
46
+
47
+ ## Setup
48
+
49
+ Either install the release, which puts a `navdata-update` command on your PATH:
50
+
51
+ ```bash
52
+ pip install airmate-navdata-sync
53
+ navdata-update --init-config # writes ~/.config/navdata-sync/config.toml
54
+ ```
55
+
56
+ or work from a checkout, which keeps everything in one folder. `pip install -e .`
57
+ installs the dependencies from `pyproject.toml` and links the package in place,
58
+ so edits take effect without reinstalling:
59
+
60
+ ```bash
61
+ git clone https://github.com/Natim/skydemon-airmate-navdata-sync
62
+ cd skydemon-airmate-navdata-sync
63
+ python3 -m venv .venv && . .venv/bin/activate
64
+ pip install -e .
65
+ cp config.example.toml config.toml
66
+ ```
67
+
68
+ Then edit the config file. The two values you have to fill in are in the
69
+ `[airmate]` section:
70
+
71
+ - `id` — the customer id in your personal download URLs
72
+ (`https://www.airmate.aero/download/navdata/<id>/...`), visible on the Airmate
73
+ download page once logged in.
74
+ - `serial` — the serial of the Dynon the subscription is bound to. It is the
75
+ number embedded in the filenames Airmate offers you, as in
76
+ `airmate_av_data_eu_2608_<serial>.dup` and `CHARTS-<serial>.key`.
77
+
78
+ If you would rather not keep those on disk, leave the placeholders in place and
79
+ export them instead; the environment always wins over the file:
80
+
81
+ ```bash
82
+ export AIRMATE_ID=... AIRMATE_SERIAL=...
83
+ ```
84
+
85
+ Every other setting is documented inline in
86
+ [`config.example.toml`](config.example.toml).
87
+
88
+ Without `--config`, the first of these that exists is used: `./config.toml`,
89
+ then `~/.config/navdata-sync/config.toml` (honouring `XDG_CONFIG_HOME`), then
90
+ `config.toml` at the root of a checkout.
91
+
92
+ ## Usage
93
+
94
+ ```bash
95
+ navdata-update # download the configured cycle, then stage it
96
+ navdata-update --list # show which files the config resolves to
97
+ navdata-update --cycle 2609 # try the next cycle without editing the config
98
+ navdata-update --skip-download # rebuild the staging folder from the cache
99
+ navdata-update --sync # ...and copy it onto every configured USB stick
100
+ ```
101
+
102
+ From a checkout without installing, `./navdata-update.py` and
103
+ `python -m navdata_sync` take the same arguments.
104
+
105
+ Without `--sync` the run stops after staging and prints the configured USB
106
+ mounts, so you can inspect the result first.
107
+
108
+ Downloads are resumable and run four at a time behind one global progress bar;
109
+ interrupting the script and running it again picks up where it left off. Files
110
+ that are already complete are skipped, with one deliberate exception: the
111
+ `CHARTS-*.key` file is always re-fetched, because its contents change every
112
+ cycle while its size stays identical.
113
+
114
+ ## Keeping the configuration current
115
+
116
+ Two things drift and need a manual edit:
117
+
118
+ - **`data.cycle`** — the AIRAC cycle, as `<two-digit year><two-digit cycle>`
119
+ (`2608` is the 8th cycle of 2026). It changes every 28 days.
120
+ - **`data.raster`** — raster VFR charts are named after their *edition* date
121
+ (`VFR-FRANCE-OACI-16APR26.dcf`), which is unrelated to the AIRAC cycle. Copy
122
+ the new filenames from the Airmate download page when an edition is published.
123
+
124
+ `--list` is the quick way to check what the current configuration points at
125
+ before starting a multi-gigabyte download.
126
+
127
+ ## What ends up on the stick
128
+
129
+ ```
130
+ AIRMATE_AV_DATA_EU_<cycle>_<serial>.DUP navdata (uppercased, the Dynon is picky)
131
+ AIRMATE_OBSTACLE_DATA_EU_<cycle>_<serial>.DUP
132
+ CHARTS-<serial>.key unlocks the chart and raster layers
133
+ ChartData/Plates/... approach plates, from the region zips
134
+ Raster/VFR-*.dcf raster VFR charts
135
+ ```
136
+
137
+ The copy checksums each file (blake2b) and writes only those whose bytes
138
+ actually changed, in 4 MiB sequential chunks via `aiofile`. The staging folder
139
+ is rebuilt from scratch every run, so timestamps are always new and a
140
+ size+mtime comparison would recopy everything. Full sequential writes of
141
+ changed files are easier on a FAT-32 stick than rsync's random block deltas,
142
+ and opening the destination in place avoids a temp copy on a nearly-full
143
+ volume. Several sticks (left and right SkyView) are updated in parallel, each
144
+ with its own sequential writer.
145
+
146
+ ## Repository layout
147
+
148
+ ```
149
+ pyproject.toml packaging metadata, distributed as airmate-navdata-sync
150
+ navdata-update.py entry point for a checkout, mirrors the installed command
151
+ config.example.toml documented template for the git-ignored config.toml
152
+ navdata_sync/
153
+ config.py finds and validates config.toml, applies env overrides
154
+ catalog.py turns the config into the list of URLs to fetch
155
+ download.py resumable parallel downloader
156
+ prepare.py staging into the Dynon layout
157
+ usb.py checksum-and-copy onto one or more USB sticks
158
+ cli.py argument parsing and the run sequence
159
+ tools/
160
+ mbtiles_to_dcf.py converts an MBTiles file into an Airmate-style .dcf
161
+ ```
162
+
163
+ `tools/mbtiles_to_dcf.py` is a standalone experiment, not part of the update
164
+ flow, so it ships in the source distribution and the checkout rather than in the
165
+ wheel. It builds a `.dcf` raster layer from any MBTiles source; note that a layer
166
+ is only displayed if a matching entry exists in `CHARTS-<serial>.key`, which the
167
+ script cannot create. It needs Pillow, from the `mbtiles` extra
168
+ (`pip install -e '.[mbtiles]'` in a checkout); run
169
+ `tools/mbtiles_to_dcf.py --help` for the options.
170
+
171
+ ## Releasing
172
+
173
+ ```bash
174
+ pip install build twine
175
+ python -m build # writes dist/*.whl and dist/*.tar.gz
176
+ twine check dist/*
177
+ twine upload dist/*
178
+ ```
179
+
180
+ The version lives in `navdata_sync/__init__.py` and is read from there by the
181
+ build backend, so bump it in that one place.
182
+
183
+ ## A note on the data
184
+
185
+ The navdata is licensed per subscription and tied to your Dynon serial. Keep
186
+ `config.toml`, the downloads and the staging folder out of version control —
187
+ `.gitignore` already covers them. And, obviously: this is a personal convenience
188
+ script, so verify on the unit that the cycle you expect is the cycle it loaded.
@@ -0,0 +1,158 @@
1
+ # skydemon-airmate-navdata-sync
2
+
3
+ Keeps the navigation data of a Dynon SkyView up to date: it downloads an
4
+ [Airmate](https://www.airmate.aero) AIRAC cycle, lays the files out exactly the
5
+ way the Dynon expects them, and copies the result onto every configured USB
6
+ stick without rewriting the gigabytes that did not change.
7
+
8
+ Everything specific to a subscription or a machine — the Airmate customer id,
9
+ the Dynon serial, the AIRAC cycle, the local paths — lives in `config.toml`,
10
+ which is git-ignored. The code contains no ids.
11
+
12
+ ## Requirements
13
+
14
+ - Python 3.11 or newer (the config reader uses the standard-library `tomllib`)
15
+ - An active Airmate subscription bound to your Dynon serial
16
+
17
+ ## Setup
18
+
19
+ Either install the release, which puts a `navdata-update` command on your PATH:
20
+
21
+ ```bash
22
+ pip install airmate-navdata-sync
23
+ navdata-update --init-config # writes ~/.config/navdata-sync/config.toml
24
+ ```
25
+
26
+ or work from a checkout, which keeps everything in one folder. `pip install -e .`
27
+ installs the dependencies from `pyproject.toml` and links the package in place,
28
+ so edits take effect without reinstalling:
29
+
30
+ ```bash
31
+ git clone https://github.com/Natim/skydemon-airmate-navdata-sync
32
+ cd skydemon-airmate-navdata-sync
33
+ python3 -m venv .venv && . .venv/bin/activate
34
+ pip install -e .
35
+ cp config.example.toml config.toml
36
+ ```
37
+
38
+ Then edit the config file. The two values you have to fill in are in the
39
+ `[airmate]` section:
40
+
41
+ - `id` — the customer id in your personal download URLs
42
+ (`https://www.airmate.aero/download/navdata/<id>/...`), visible on the Airmate
43
+ download page once logged in.
44
+ - `serial` — the serial of the Dynon the subscription is bound to. It is the
45
+ number embedded in the filenames Airmate offers you, as in
46
+ `airmate_av_data_eu_2608_<serial>.dup` and `CHARTS-<serial>.key`.
47
+
48
+ If you would rather not keep those on disk, leave the placeholders in place and
49
+ export them instead; the environment always wins over the file:
50
+
51
+ ```bash
52
+ export AIRMATE_ID=... AIRMATE_SERIAL=...
53
+ ```
54
+
55
+ Every other setting is documented inline in
56
+ [`config.example.toml`](config.example.toml).
57
+
58
+ Without `--config`, the first of these that exists is used: `./config.toml`,
59
+ then `~/.config/navdata-sync/config.toml` (honouring `XDG_CONFIG_HOME`), then
60
+ `config.toml` at the root of a checkout.
61
+
62
+ ## Usage
63
+
64
+ ```bash
65
+ navdata-update # download the configured cycle, then stage it
66
+ navdata-update --list # show which files the config resolves to
67
+ navdata-update --cycle 2609 # try the next cycle without editing the config
68
+ navdata-update --skip-download # rebuild the staging folder from the cache
69
+ navdata-update --sync # ...and copy it onto every configured USB stick
70
+ ```
71
+
72
+ From a checkout without installing, `./navdata-update.py` and
73
+ `python -m navdata_sync` take the same arguments.
74
+
75
+ Without `--sync` the run stops after staging and prints the configured USB
76
+ mounts, so you can inspect the result first.
77
+
78
+ Downloads are resumable and run four at a time behind one global progress bar;
79
+ interrupting the script and running it again picks up where it left off. Files
80
+ that are already complete are skipped, with one deliberate exception: the
81
+ `CHARTS-*.key` file is always re-fetched, because its contents change every
82
+ cycle while its size stays identical.
83
+
84
+ ## Keeping the configuration current
85
+
86
+ Two things drift and need a manual edit:
87
+
88
+ - **`data.cycle`** — the AIRAC cycle, as `<two-digit year><two-digit cycle>`
89
+ (`2608` is the 8th cycle of 2026). It changes every 28 days.
90
+ - **`data.raster`** — raster VFR charts are named after their *edition* date
91
+ (`VFR-FRANCE-OACI-16APR26.dcf`), which is unrelated to the AIRAC cycle. Copy
92
+ the new filenames from the Airmate download page when an edition is published.
93
+
94
+ `--list` is the quick way to check what the current configuration points at
95
+ before starting a multi-gigabyte download.
96
+
97
+ ## What ends up on the stick
98
+
99
+ ```
100
+ AIRMATE_AV_DATA_EU_<cycle>_<serial>.DUP navdata (uppercased, the Dynon is picky)
101
+ AIRMATE_OBSTACLE_DATA_EU_<cycle>_<serial>.DUP
102
+ CHARTS-<serial>.key unlocks the chart and raster layers
103
+ ChartData/Plates/... approach plates, from the region zips
104
+ Raster/VFR-*.dcf raster VFR charts
105
+ ```
106
+
107
+ The copy checksums each file (blake2b) and writes only those whose bytes
108
+ actually changed, in 4 MiB sequential chunks via `aiofile`. The staging folder
109
+ is rebuilt from scratch every run, so timestamps are always new and a
110
+ size+mtime comparison would recopy everything. Full sequential writes of
111
+ changed files are easier on a FAT-32 stick than rsync's random block deltas,
112
+ and opening the destination in place avoids a temp copy on a nearly-full
113
+ volume. Several sticks (left and right SkyView) are updated in parallel, each
114
+ with its own sequential writer.
115
+
116
+ ## Repository layout
117
+
118
+ ```
119
+ pyproject.toml packaging metadata, distributed as airmate-navdata-sync
120
+ navdata-update.py entry point for a checkout, mirrors the installed command
121
+ config.example.toml documented template for the git-ignored config.toml
122
+ navdata_sync/
123
+ config.py finds and validates config.toml, applies env overrides
124
+ catalog.py turns the config into the list of URLs to fetch
125
+ download.py resumable parallel downloader
126
+ prepare.py staging into the Dynon layout
127
+ usb.py checksum-and-copy onto one or more USB sticks
128
+ cli.py argument parsing and the run sequence
129
+ tools/
130
+ mbtiles_to_dcf.py converts an MBTiles file into an Airmate-style .dcf
131
+ ```
132
+
133
+ `tools/mbtiles_to_dcf.py` is a standalone experiment, not part of the update
134
+ flow, so it ships in the source distribution and the checkout rather than in the
135
+ wheel. It builds a `.dcf` raster layer from any MBTiles source; note that a layer
136
+ is only displayed if a matching entry exists in `CHARTS-<serial>.key`, which the
137
+ script cannot create. It needs Pillow, from the `mbtiles` extra
138
+ (`pip install -e '.[mbtiles]'` in a checkout); run
139
+ `tools/mbtiles_to_dcf.py --help` for the options.
140
+
141
+ ## Releasing
142
+
143
+ ```bash
144
+ pip install build twine
145
+ python -m build # writes dist/*.whl and dist/*.tar.gz
146
+ twine check dist/*
147
+ twine upload dist/*
148
+ ```
149
+
150
+ The version lives in `navdata_sync/__init__.py` and is read from there by the
151
+ build backend, so bump it in that one place.
152
+
153
+ ## A note on the data
154
+
155
+ The navdata is licensed per subscription and tied to your Dynon serial. Keep
156
+ `config.toml`, the downloads and the staging folder out of version control —
157
+ `.gitignore` already covers them. And, obviously: this is a personal convenience
158
+ script, so verify on the unit that the cycle you expect is the cycle it loaded.
@@ -0,0 +1,66 @@
1
+ # Copy this file to config.toml and fill in your own values.
2
+ #
3
+ # cp config.example.toml config.toml
4
+ #
5
+ # config.toml is git-ignored because it holds the two values that identify your
6
+ # Airmate subscription: the customer id and the Dynon serial it is bound to.
7
+
8
+ [airmate]
9
+ # The customer id that appears in your personal download URLs:
10
+ # https://www.airmate.aero/download/navdata/<id>/...
11
+ # It is shown on the Airmate download page once you are logged in.
12
+ # May also be supplied through the AIRMATE_ID environment variable.
13
+ id = "YOUR_AIRMATE_ID"
14
+
15
+ # Serial number of the Dynon unit the subscription is bound to. It is the
16
+ # number embedded in the data filenames:
17
+ # airmate_av_data_eu_<cycle>_<serial>.dup CHARTS-<serial>.key
18
+ # May also be supplied through the AIRMATE_SERIAL environment variable.
19
+ serial = "000000"
20
+
21
+ # Only change this if Airmate moves its download endpoint.
22
+ base_url = "https://www.airmate.aero/download/navdata"
23
+
24
+ [data]
25
+ # AIRAC cycle to download, as <two-digit year><two-digit cycle number>.
26
+ # 2608 is the 8th cycle of 2026. Bump this every 28 days.
27
+ cycle = "2608"
28
+
29
+ # Approach plate bundles, published as <region>-Plates-<cycle>.zip.
30
+ # "FR" alone is enough if you only fly in France; "Europe" is a large download.
31
+ plates = ["FR", "Europe"]
32
+
33
+ # Raster VFR charts, published under <base_url>/Raster/.
34
+ # The trailing date is the chart *edition*, not the AIRAC cycle, so these names
35
+ # have to be updated by hand whenever a new edition is published. Remove the
36
+ # ones you do not need: each entry is a few hundred MB on the USB stick.
37
+ raster = [
38
+ "VFR-FRANCE-OACI-16APR26.dcf",
39
+ "VFR-EUROPE-HIRES-14MAY26.dcf",
40
+ "VFR-DENMARK-NAVIAIR-14MAY26.dcf",
41
+ "VFR-IRELAND-01JAN24.dcf",
42
+ "VFR-UK-01JAN24.dcf",
43
+ "VFR-ITALY-16APR26.dcf",
44
+ "VFR-SWITZERLAND-19MAR26.dcf",
45
+ ]
46
+
47
+ [paths]
48
+ # Relative paths below are resolved next to this config file, which keeps a git
49
+ # checkout self-contained. If this config lives in ~/.config, make them absolute
50
+ # instead: the AIRAC cycle is several gigabytes and does not belong there.
51
+
52
+ # Download cache. Partially downloaded files are resumed from here, so keeping
53
+ # it between runs saves bandwidth.
54
+ download_dir = "downloads"
55
+
56
+ # Staging area laid out exactly like the root of the Dynon USB stick. Rebuilt
57
+ # from scratch on every run.
58
+ prepared_dir = "downloads_prepared"
59
+
60
+ # Mount points of the Dynon USB sticks, used by --sync. One entry per unit
61
+ # (left and right SkyView). A single string still works. Leave the list empty
62
+ # to only build the staging directory.
63
+ usb_targets = [
64
+ "/run/media/YOUR_USER/LH D1000",
65
+ "/run/media/YOUR_USER/RH D1000",
66
+ ]
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env python3
2
+ """Entry point kept at the repo root; the logic lives in navdata_sync/."""
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
7
+
8
+ try:
9
+ from navdata_sync.cli import main
10
+ except ModuleNotFoundError as exc:
11
+ # The shebang is deliberately generic, so this is what an unactivated
12
+ # virtualenv looks like rather than a broken checkout.
13
+ raise SystemExit(
14
+ f"❌ dépendance manquante: {exc.name}\n"
15
+ " activez votre virtualenv, ou: pip install -e ."
16
+ ) from exc
17
+
18
+ if __name__ == "__main__":
19
+ raise SystemExit(main())
@@ -0,0 +1,3 @@
1
+ """Airmate navdata updater for a Dynon SkyView."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,71 @@
1
+ """Turns a Config into the list of files to fetch from Airmate.
2
+
3
+ Airmate publishes three kinds of URL:
4
+
5
+ <base>/<customer id>/... subscription-bound data, one file per serial
6
+ <base>/Plates/... approach plate bundles, one zip per region
7
+ <base>/Raster/... raster VFR charts, shared by everyone
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from enum import Enum
13
+
14
+ from .config import Config
15
+
16
+
17
+ class Kind(Enum):
18
+ """What a file becomes on the USB stick, which decides how it is staged."""
19
+
20
+ DATA = "data" # .dup, navdata and obstacles, uppercased at the root
21
+ KEY = "key" # CHARTS-<serial>.key, unlocks the raster layers
22
+ PLATES = "plates" # zip expanded at the root, creates ChartData/
23
+ RASTER = "raster" # .dcf copied under Raster/
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class RemoteFile:
28
+ url: str
29
+ name: str
30
+ kind: Kind
31
+
32
+ @property
33
+ def always_refresh(self) -> bool:
34
+ """Whether a byte-identical local copy still has to be re-downloaded.
35
+
36
+ The key file changes content every AIRAC cycle (chart filenames and auth
37
+ codes rotate) while keeping the exact same byte size, so a size-only
38
+ "already complete" check would keep a stale key on disk. The Dynon then
39
+ silently hides every layer whose filename or code no longer matches.
40
+ """
41
+ return self.kind is Kind.KEY
42
+
43
+
44
+ def build(config: Config) -> list[RemoteFile]:
45
+ account = f"{config.base_url}/{config.airmate_id}"
46
+ files = [
47
+ RemoteFile(
48
+ f"{account}/airmate_av_data_eu_{config.cycle}_{config.serial}.dup",
49
+ f"airmate_av_data_eu_{config.cycle}_{config.serial}.dup",
50
+ Kind.DATA,
51
+ ),
52
+ RemoteFile(
53
+ f"{account}/airmate_obstacle_data_eu_{config.cycle}_{config.serial}.dup",
54
+ f"airmate_obstacle_data_eu_{config.cycle}_{config.serial}.dup",
55
+ Kind.DATA,
56
+ ),
57
+ RemoteFile(
58
+ f"{account}/CHARTS-{config.serial}.key",
59
+ f"CHARTS-{config.serial}.key",
60
+ Kind.KEY,
61
+ ),
62
+ ]
63
+
64
+ for region in config.plates:
65
+ name = f"{region}-Plates-{config.cycle}.zip"
66
+ files.append(RemoteFile(f"{config.base_url}/Plates/{name}", name, Kind.PLATES))
67
+
68
+ for name in config.raster:
69
+ files.append(RemoteFile(f"{config.base_url}/Raster/{name}", name, Kind.RASTER))
70
+
71
+ return files
@@ -0,0 +1,109 @@
1
+ """Command line entry point: download, stage, optionally sync."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import asyncio
6
+ import dataclasses
7
+ from pathlib import Path
8
+
9
+ from . import catalog, download, prepare
10
+ from . import __version__
11
+ from .config import ConfigError, init_config, search_path
12
+ from .config import load as load_config
13
+
14
+
15
+ def build_parser() -> argparse.ArgumentParser:
16
+ parser = argparse.ArgumentParser(
17
+ prog="navdata-update",
18
+ description="Download an Airmate AIRAC cycle and lay it out for a "
19
+ "Dynon SkyView USB stick.",
20
+ )
21
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
22
+ searched = " or ".join(str(candidate) for candidate in search_path())
23
+ parser.add_argument(
24
+ "-c",
25
+ "--config",
26
+ type=Path,
27
+ metavar="PATH",
28
+ help=f"configuration file (default: the first of {searched})",
29
+ )
30
+ parser.add_argument(
31
+ "--init-config",
32
+ action="store_true",
33
+ help="write a starter configuration to --config, or to the per-user "
34
+ "location, then exit",
35
+ )
36
+ parser.add_argument(
37
+ "--cycle",
38
+ metavar="YYCC",
39
+ help="override data.cycle for this run, e.g. 2609",
40
+ )
41
+ parser.add_argument(
42
+ "--list",
43
+ action="store_true",
44
+ help="list the files the configuration resolves to and exit",
45
+ )
46
+ parser.add_argument(
47
+ "--skip-download",
48
+ action="store_true",
49
+ help="rebuild the staging folder from the download cache only",
50
+ )
51
+ parser.add_argument(
52
+ "--sync",
53
+ action="store_true",
54
+ help="copy the staging folder onto every configured USB stick",
55
+ )
56
+ return parser
57
+
58
+
59
+ def main(argv: list[str] | None = None) -> int:
60
+ args = build_parser().parse_args(argv)
61
+
62
+ if args.init_config:
63
+ try:
64
+ target = init_config(args.config)
65
+ except ConfigError as exc:
66
+ print(f"❌ {exc}")
67
+ return 2
68
+ print(
69
+ f"✅ Configuration créée: {target}\n"
70
+ " Renseignez airmate.id et airmate.serial avant le premier run."
71
+ )
72
+ return 0
73
+
74
+ try:
75
+ config = load_config(args.config)
76
+ except ConfigError as exc:
77
+ print(f"❌ {exc}")
78
+ return 2
79
+
80
+ if args.cycle:
81
+ config = dataclasses.replace(config, cycle=args.cycle)
82
+
83
+ # Checked before anything is downloaded or staged, since --sync is the whole
84
+ # point of the run when it is passed.
85
+ if args.sync and not config.usb_targets:
86
+ print("❌ --sync demande paths.usb_target ou paths.usb_targets dans la configuration")
87
+ return 2
88
+
89
+ files = catalog.build(config)
90
+
91
+ if args.list:
92
+ for file in files:
93
+ print(f"{file.kind.value:>7} {file.name}")
94
+ print(f"\ncycle {config.cycle} · {len(files)} fichiers · {config.download_dir}")
95
+ return 0
96
+
97
+ if not args.skip_download:
98
+ asyncio.run(download.run(files, config.download_dir))
99
+
100
+ missing = prepare.build(files, config.download_dir, config.prepared_dir)
101
+
102
+ if args.sync:
103
+ if missing:
104
+ print("❌ Synchronisation annulée: des fichiers manquent")
105
+ return 1
106
+ return prepare.sync(config.prepared_dir, config.usb_targets)
107
+
108
+ print(prepare.describe_sync(config.prepared_dir, config.usb_targets))
109
+ return 1 if missing else 0