signalk-chiplog 2.1.0 → 2.3.0
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.
- package/CHANGELOG.md +47 -1
- package/README.md +73 -34
- package/index.js +53 -17
- package/lib/api.js +15 -0
- package/lib/{forecast-schedule.js → background-schedule.js} +11 -11
- package/lib/database.js +44 -0
- package/lib/detection.js +9 -4
- package/lib/entries.js +7 -2
- package/lib/event-watcher.js +1 -3
- package/lib/events.js +38 -3
- package/lib/export.js +8 -0
- package/lib/influx-history.js +9 -4
- package/lib/landmark-finder.js +209 -0
- package/lib/landmarks.js +210 -0
- package/lib/logbook-pdf.js +45 -7
- package/lib/replay-job.js +1 -0
- package/package.json +3 -2
- package/public/app.css +121 -0
- package/public/entry/sw.js +1 -0
- package/public/js/animation/camera.mjs +180 -0
- package/public/js/animation/formats.mjs +45 -0
- package/public/js/animation/mercator.mjs +50 -0
- package/public/js/animation/mp4.mjs +77 -0
- package/public/js/animation/player.mjs +120 -0
- package/public/js/animation/renderer.mjs +326 -0
- package/public/js/animation/schedule.mjs +49 -0
- package/public/js/animation/storyboard.mjs +195 -0
- package/public/js/animation/tiles.mjs +219 -0
- package/public/js/animation/timeline.mjs +186 -0
- package/public/js/components/AnimationExport.mjs +173 -0
- package/public/js/components/AnimationView.mjs +439 -0
- package/public/js/components/ExportView.mjs +3 -8
- package/public/js/components/PassageView.mjs +29 -8
- package/public/js/components/Timeline.mjs +30 -7
- package/public/js/context.mjs +7 -0
- package/public/js/days.mjs +10 -0
- package/public/js/format.mjs +11 -0
- package/public/js/i18n.mjs +129 -0
- package/public/js/landmarks.mjs +128 -0
- package/public/js/main.mjs +8 -5
- package/public/vendor/mediabunny-LICENSE +373 -0
- package/public/vendor/mediabunny.min.mjs +50 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,50 @@ All notable changes to Chiplog are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.3.0] - 2026-09-18
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **InfluxDB query timeout** setting (`influxQueryTimeoutSeconds`, 30 s by default): how long a retrospective replay
|
|
14
|
+
waits for the InfluxDB server to answer before giving up on it as unreachable or overloaded, now configurable instead
|
|
15
|
+
of a fixed 30 seconds — useful against a Raspberry Pi that is simply slow to answer a six-hour chunk.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **Retrospective replay backfilling a gap before passages already logged live** no longer dated every reconstructed
|
|
20
|
+
passage to the most recent existing passage's end time. Detection's guard against an out-of-order departure looked at
|
|
21
|
+
the latest `end_time` in the whole logbook rather than only at passages that actually preceded the new one, so filling
|
|
22
|
+
in an earlier gap — installing Chiplog after the fact, or after a stop — pinned every reconstructed departure to that
|
|
23
|
+
unrelated, later date, and the replay summary reported no passage found for the requested period even though entries
|
|
24
|
+
had been created.
|
|
25
|
+
|
|
26
|
+
## [2.2.0] - 2026-09-18
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **Landmark bearings (amers)** — every position in the log is now also given the way a paper logbook gives one: a
|
|
31
|
+
distance and a bearing from the nearest landmark, "2,3 M ENE (065°) — Phare de Chauveau", under the coordinates in a
|
|
32
|
+
lighter grey, on the passage page and in the PDF logbook. The coordinates themselves are unchanged. Landmarks —
|
|
33
|
+
lighthouses, capes, named towers, harbours — are fetched from OpenStreetMap area by area and kept, so past passages
|
|
34
|
+
fill in as soon as their area is known and the same waters are never asked for twice; the bearing is computed when the
|
|
35
|
+
page is drawn, never stored. The landmark quoted is the one closest relative to its own range, so a lighthouse three
|
|
36
|
+
miles off wins over a marina alongside, and offshore the coordinates stay alone. A new setting, **Read each journal
|
|
37
|
+
line against the nearest landmark**, turns it off.
|
|
38
|
+
- **Animation** — a new page that replays passages on the map. Pick a start and an end date and every passage between
|
|
39
|
+
them plays in sequence, the time spent in port skipped, so a week's cruise takes seconds. The map follows the boat at
|
|
40
|
+
a scale chosen for each passage — a short hop stays readable instead of being magnified, a long crossing gets a wider
|
|
41
|
+
view without leaving the boat crawling across empty water — and a bubble on the chart shows the speed, the distance
|
|
42
|
+
covered since the animation began and the date and hour. Play, pause, four speeds (×0,5, ×1, ×2, ×4 — an hour of
|
|
43
|
+
sailing per second at ×1) and a slider over the animation's own time. A passage page links straight to it with its own
|
|
44
|
+
dates filled in.
|
|
45
|
+
- **Save an animation as an MP4** — Mobile (9:16), Portrait (3:4), Square (1:1), Landscape (4:3) or Widescreen (16:9),
|
|
46
|
+
encoded in the browser: nothing is uploaded, and the Signal K server renders nothing. Each frame's map is downloaded
|
|
47
|
+
just before that frame is drawn, so the film is the same whatever the connection was doing and no range is ever too
|
|
48
|
+
long to export. Needs a browser with WebCodecs (Chrome, Edge, Safari 17, Firefox 130 and later); without one the
|
|
49
|
+
animation still plays on screen.
|
|
50
|
+
- **Delete an alarm** — a logged alarm can now be removed from a passage's log, like a crew-entered line already could.
|
|
51
|
+
Deleting either the alarm or the line that later cleared it removes both, so no orphaned half is left behind.
|
|
52
|
+
|
|
9
53
|
## [2.1.0] - 2026-09-17
|
|
10
54
|
|
|
11
55
|
### Added
|
|
@@ -222,7 +266,9 @@ First release.
|
|
|
222
266
|
- REST API under `/plugins/signalk-chiplog/api`, documented in [docs/API.md](docs/API.md).
|
|
223
267
|
- Single SQLite database through Node's built-in `node:sqlite`: no native module to build.
|
|
224
268
|
|
|
225
|
-
[Unreleased]: https://github.com/ricard33/signalk-chiplog/compare/v2.
|
|
269
|
+
[Unreleased]: https://github.com/ricard33/signalk-chiplog/compare/v2.3.0...HEAD
|
|
270
|
+
[2.3.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.2.0...v2.3.0
|
|
271
|
+
[2.2.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.1.0...v2.2.0
|
|
226
272
|
[2.1.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.0.0...v2.1.0
|
|
227
273
|
[2.0.0]: https://github.com/ricard33/signalk-chiplog/compare/v1.2.0...v2.0.0
|
|
228
274
|
[1.2.0]: https://github.com/ricard33/signalk-chiplog/compare/v1.1.0...v1.2.0
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Chiplog
|
|
2
2
|
|
|
3
|
-
An automated logbook for [Signal K](https://signalk.org). Chiplog writes the logbook from the data already on
|
|
4
|
-
boat's Signal K server — passages, track, engine and sail, instrument readings, alarms — and lets the crew add what
|
|
3
|
+
An automated nautical logbook for [Signal K](https://signalk.org). Chiplog writes the logbook from the data already on
|
|
4
|
+
your boat's Signal K server — passages, track, engine and sail, instrument readings, alarms — and lets the crew add what
|
|
5
5
|
sensors cannot know from a tablet at the helm: manoeuvres, notes and handwriting.
|
|
6
6
|
|
|
7
7
|
- **One entry per passage**, opened when the boat leaves and closed as soon as it arrives, carrying on after short stops
|
|
@@ -11,6 +11,8 @@ sensors cannot know from a tablet at the helm: manoeuvres, notes and handwriting
|
|
|
11
11
|
- **Automatic events**: alarms, autopilot changes, strong wind, falling barometer.
|
|
12
12
|
- **Departure and arrival names**, looked up online and corrected once for good.
|
|
13
13
|
- **Consultation webapp**: logbook by day, map, timeline, corrections, export.
|
|
14
|
+
- **Replay a range of passages** on the map — an hour of sailing per second — and save it as an MP4 for a phone, a
|
|
15
|
+
square post or a widescreen, rendered entirely in your browser.
|
|
14
16
|
- **Tablet entry app**: big buttons for gloves and wet fingers, stylus handwriting, night mode, works through Wi-Fi
|
|
15
17
|
dropouts.
|
|
16
18
|
- **Abandon-ship copy**: JSON, CSV and GPX, downloadable or written to a USB drive.
|
|
@@ -169,6 +171,26 @@ Departures and arrivals are named automatically:
|
|
|
169
171
|
3. **Corrections are remembered.** Renaming a departure or arrival in the webapp also renames that place for every later
|
|
170
172
|
passage starting or ending nearby. Past passages keep the name they recorded.
|
|
171
173
|
|
|
174
|
+
### Landmarks (amers)
|
|
175
|
+
|
|
176
|
+
Every position in the log is also given the way a paper logbook gives one — as a distance and a bearing **from a
|
|
177
|
+
landmark**, under the coordinates, in a lighter grey:
|
|
178
|
+
|
|
179
|
+
```text
|
|
180
|
+
46°08.88′N 001°12.90′W
|
|
181
|
+
2,3 M ENE (065°) — Phare de Chauveau
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
- **The landmarks come from OpenStreetMap**, fetched area by area through Overpass and kept: lighthouses and major
|
|
185
|
+
lights, capes, named towers and other seamark landmarks, minor lights, isolated-danger and safe-water beacons,
|
|
186
|
+
harbours and marinas. The numbered marks of a channel are left out — "6 c" says nothing in a logbook.
|
|
187
|
+
- **The nearest useful one wins**, not simply the nearest: each kind carries a range (15 nm for a lighthouse, 3 for a
|
|
188
|
+
harbour…), narrowed by the light's own range when known, and the landmark closest relative to its range is the one
|
|
189
|
+
quoted. Offshore, beyond them all, the coordinates stay alone.
|
|
190
|
+
- **Past passages fill in by themselves** once their area has been fetched — the bearing is worked out when the page or
|
|
191
|
+
the PDF is drawn, never stored.
|
|
192
|
+
- Turn **Read each journal line against the nearest landmark** off to keep the boat off Overpass entirely.
|
|
193
|
+
|
|
172
194
|
## The logbook webapp 💻
|
|
173
195
|
|
|
174
196
|
Open **Chiplog** from the Signal K webapps, or `/signalk-chiplog/`. Reading needs no more than read-only access.
|
|
@@ -189,13 +211,20 @@ Open **Chiplog** from the Signal K webapps, or `/signalk-chiplog/`. Reading need
|
|
|
189
211
|
current noted at departure), and the log: every reading and event in order, including handwritten notes. A passage in
|
|
190
212
|
progress refreshes every minute. Each line's comment can be edited (read/write access); a manoeuvre or note the crew
|
|
191
213
|
logged themselves can also be deleted — automatic lines (alarms, autopilot, weather, corrections) can only be
|
|
192
|
-
annotated.
|
|
214
|
+
annotated. Under each position, in grey, its bearing and distance from the nearest landmark.
|
|
193
215
|
- **Corrections** (read/write access):
|
|
194
216
|
- rename the departure, or the arrival once the passage is closed — a passage in progress has none yet to rename;
|
|
195
217
|
- switch an engine period to sail or back;
|
|
196
218
|
- close a passage in progress, e.g. to confirm an arrival;
|
|
197
219
|
- merge with the previous or next passage;
|
|
198
220
|
- delete a passage (admin).
|
|
221
|
+
- **Animation** — pick two dates and every passage between them replays on the map, one after another, the port time
|
|
222
|
+
skipped. The map follows the boat at a scale chosen for each passage — a short hop kept readable rather than
|
|
223
|
+
magnified, a long crossing allowed a wider view but never so wide the boat crawls across empty water — while a bubble
|
|
224
|
+
shows the speed, the distance covered since the start and the date. Play, pause and a slider over the animation's own
|
|
225
|
+
time, at ×0,5, ×1, ×2 or ×4. **Export MP4** saves it as a video in one of five shapes (Mobile 9:16, Portrait 3:4,
|
|
226
|
+
Square 1:1, Landscape 4:3, Widescreen 16:9). Everything happens in the browser: nothing is rendered or encoded on the
|
|
227
|
+
Signal K server, and the map tiles are the only thing downloaded.
|
|
199
228
|
- **Export** — download the whole logbook or a date range as a PDF logbook to print, JSON, CSV or GPX, and write the
|
|
200
229
|
abandon-ship copy to the USB drive now (admin). The PDF is written in the webapp's language and the device's time
|
|
201
230
|
zone.
|
|
@@ -291,33 +320,36 @@ a regular Signal K user account.
|
|
|
291
320
|
|
|
292
321
|
In the Signal K admin, **Apps & Plugins → Configuration → Chiplog**.
|
|
293
322
|
|
|
294
|
-
| Setting | Default | What it does
|
|
295
|
-
| ---------------------------------------------------------------- | --------------------------------------------- |
|
|
296
|
-
| Stop duration within which a new departure continues the passage | 30 min | A passage closes when the boat stops; leaving again sooner reopens it, the stop kept as a stopover.
|
|
297
|
-
| Under-way speed without navigation.state | 1 kn | Used only without signalk-autostate: under way above it, stopped below half of it.
|
|
298
|
-
| Propulsion assumed without engine data | sail | When nothing says whether the engine is running. Set to engine on a motorboat.
|
|
299
|
-
| Instrument snapshot interval | 60 min | Readings on this clock boundary during a passage.
|
|
300
|
-
| Track point interval | 15 s | A track point at least this often while moving.
|
|
301
|
-
| Place matching radius | 200 m | A departure or arrival this close to a known place takes its name.
|
|
302
|
-
| Name departures and arrivals with online geocoding | on | Turn off to never send positions online; places are then named after their coordinates until corrected.
|
|
303
|
-
| Geocoding service | `https://nominatim.openstreetmap.org` | Any Nominatim-compatible service, e.g. a self-hosted one.
|
|
304
|
-
|
|
|
305
|
-
|
|
|
306
|
-
| Fetch the
|
|
307
|
-
|
|
|
308
|
-
|
|
|
309
|
-
|
|
|
310
|
-
|
|
|
311
|
-
|
|
|
312
|
-
|
|
|
313
|
-
|
|
|
314
|
-
|
|
|
315
|
-
|
|
|
316
|
-
|
|
|
317
|
-
| InfluxDB
|
|
318
|
-
| InfluxDB
|
|
319
|
-
| InfluxDB
|
|
320
|
-
| InfluxDB
|
|
323
|
+
| Setting | Default | What it does |
|
|
324
|
+
| ---------------------------------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
325
|
+
| Stop duration within which a new departure continues the passage | 30 min | A passage closes when the boat stops; leaving again sooner reopens it, the stop kept as a stopover. |
|
|
326
|
+
| Under-way speed without navigation.state | 1 kn | Used only without signalk-autostate: under way above it, stopped below half of it. |
|
|
327
|
+
| Propulsion assumed without engine data | sail | When nothing says whether the engine is running. Set to engine on a motorboat. |
|
|
328
|
+
| Instrument snapshot interval | 60 min | Readings on this clock boundary during a passage. |
|
|
329
|
+
| Track point interval | 15 s | A track point at least this often while moving. |
|
|
330
|
+
| Place matching radius | 200 m | A departure or arrival this close to a known place takes its name. |
|
|
331
|
+
| Name departures and arrivals with online geocoding | on | Turn off to never send positions online; places are then named after their coordinates until corrected. |
|
|
332
|
+
| Geocoding service | `https://nominatim.openstreetmap.org` | Any Nominatim-compatible service, e.g. a self-hosted one. |
|
|
333
|
+
| Read each journal line against the nearest landmark | on | Fetches the landmarks of the areas sailed through from OpenStreetMap, so each position is also given as a bearing and distance from one. Turn off to keep the coordinates alone. |
|
|
334
|
+
| Landmark service (Overpass API) | `https://overpass-api.de/api/interpreter` | Any Overpass-compatible service, e.g. a self-hosted one. |
|
|
335
|
+
| Fetch the tide forecast at departure | on | Turn off to never send the departure position online; the passage page then shows no tide. |
|
|
336
|
+
| Marine service | `https://marine-api.open-meteo.com/v1/marine` | Any Open-Meteo Marine-compatible service, e.g. a self-hosted one. Serves the tide, and the sea state and current of the weather forecast. |
|
|
337
|
+
| Fetch the marine weather forecast at departure | on | Turn off to never send the departure position to the weather services; the passage page and the PDF then show no forecast. |
|
|
338
|
+
| Weather service | `https://api.open-meteo.com/v1/forecast` | Any Open-Meteo-compatible forecast service, e.g. a self-hosted one. |
|
|
339
|
+
| USB export directory | — | Where the abandon-ship copy is written, e.g. `/media/usb`. Empty turns the USB copy off. |
|
|
340
|
+
| Automatic USB copy interval | 15 min | How often the USB copy is brought up to date. 0 turns the periodic copy off. |
|
|
341
|
+
| Copy to the USB drive at each arrival | on | Brings the USB copy up to date as soon as a passage ends. |
|
|
342
|
+
| Logbook language (PDF) | en | Language of the PDF logbooks on the USB drive (English or French). |
|
|
343
|
+
| Ship's time zone (PDF) | the server's | Time zone of the PDF logbooks on the USB drive, e.g. `Europe/Paris`. |
|
|
344
|
+
| Wind speed thresholds | 20, 30 kn | Logged when the 2-minute average true wind crosses them. |
|
|
345
|
+
| Barometric drop warning | 4 hPa / 3 h | 0 turns it off. |
|
|
346
|
+
| InfluxDB host (retrospective analysis) | — | Local or remote host of the InfluxDB 1.x database signalk-to-influxdb writes to. Empty turns the retrospective analysis page off. |
|
|
347
|
+
| InfluxDB port | 8086 | |
|
|
348
|
+
| InfluxDB database | — | |
|
|
349
|
+
| InfluxDB username / password | — | Leave empty if the database needs none. |
|
|
350
|
+
| InfluxDB protocol | http | `http` or `https`. |
|
|
351
|
+
| InfluxDB query timeout | 30 s | Each retrospective query gives up and reports an error past this, instead of hanging against an unreachable or overloaded database. |
|
|
352
|
+
| InfluxDB vessel context | this server's own | Only needed running the replay from a different Signal K server than the one that wrote the history, e.g. development pointed at a production database. |
|
|
321
353
|
|
|
322
354
|
## Signal K data used 🔌
|
|
323
355
|
|
|
@@ -378,6 +410,8 @@ at the time.
|
|
|
378
410
|
- **Place names.** With geocoding on, the position of each departure and arrival that matches no known place is sent to
|
|
379
411
|
the geocoding service — OpenStreetMap's public Nominatim by default. Nothing else is sent, and nothing at all when it
|
|
380
412
|
is off.
|
|
413
|
+
- **Landmarks.** With them on, the area a passage sailed through — a half-degree box, not its track — is sent to the
|
|
414
|
+
Overpass service, OpenStreetMap's public instance by default, once per area ever. Nothing at all when it is off.
|
|
381
415
|
- **Tide forecast.** With it on, the departure position of each passage is sent to the tide service — the public
|
|
382
416
|
Open-Meteo by default — once, at departure. Nothing at all when it is off.
|
|
383
417
|
- **Weather forecast.** With it on, the departure position of each passage is sent to the weather service and to the
|
|
@@ -464,9 +498,10 @@ context** in the plugin configuration.
|
|
|
464
498
|
### A retrospective analysis takes minutes then fails with no clear reason
|
|
465
499
|
|
|
466
500
|
The InfluxDB server did not answer — unreachable, overloaded, a firewall or a VPN not connected. Each query now gives up
|
|
467
|
-
after 30 seconds with the connection problem it ran into, rather than hanging
|
|
468
|
-
failure; check that the server named in the plugin configuration is reachable
|
|
469
|
-
is not overloaded
|
|
501
|
+
after **InfluxDB query timeout** (30 seconds by default) with the connection problem it ran into, rather than hanging
|
|
502
|
+
until some far longer, less informative failure; check that the server named in the plugin configuration is reachable
|
|
503
|
+
from wherever Signal K runs, and that it is not overloaded — or raise the timeout if it is simply slow to answer a
|
|
504
|
+
six-hour chunk.
|
|
470
505
|
|
|
471
506
|
### A retrospective analysis over several days makes the InfluxDB server unresponsive
|
|
472
507
|
|
|
@@ -486,7 +521,11 @@ between attempts; nothing is lost, it just takes longer to resolve.
|
|
|
486
521
|
## Limitations 🚧
|
|
487
522
|
|
|
488
523
|
- **Not yet:** a places page, and editing manoeuvre shortcuts from the webapps.
|
|
489
|
-
- **Offline charts** are not provided.
|
|
524
|
+
- **Offline charts** are not provided; the animation draws the tracks on a blank sea when there is no connection.
|
|
525
|
+
- **The MP4 export needs a browser with WebCodecs** — Chrome, Edge, Safari 17 or Firefox 130 and later. Without it the
|
|
526
|
+
animation still plays on screen, and the export button is simply not offered.
|
|
527
|
+
- **A long animation takes a while to export**: every frame waits for its map before it is drawn, so the video is the
|
|
528
|
+
same whatever the connection was doing, but a long range means a lot of tiles. Turning the seamarks off halves them.
|
|
490
529
|
- **One vessel per Signal K server**, and no per-crew-member authorship.
|
|
491
530
|
|
|
492
531
|
## Development 🧑💻
|
package/index.js
CHANGED
|
@@ -2,9 +2,10 @@ const { registerRoutes } = require('./lib/api');
|
|
|
2
2
|
const { openDatabase } = require('./lib/database');
|
|
3
3
|
const { createPassageDetector, DETECTION_DEFAULTS, TICK_INTERVAL_MS } = require('./lib/detection');
|
|
4
4
|
const { ApiError } = require('./lib/errors');
|
|
5
|
-
const {
|
|
5
|
+
const { createBackgroundSchedule } = require('./lib/background-schedule');
|
|
6
6
|
const { createEventWatcher, CHECK_INTERVAL_MS, EVENT_DEFAULTS } = require('./lib/event-watcher');
|
|
7
7
|
const { INFLUX_DEFAULTS } = require('./lib/influx-history');
|
|
8
|
+
const { createLandmarkFinder, LANDMARK_DEFAULTS } = require('./lib/landmark-finder');
|
|
8
9
|
const { OBSERVATION_DEFAULTS } = require('./lib/observation-recorder');
|
|
9
10
|
const { createPlaceNamer, GEOCODING_DEFAULTS } = require('./lib/place-names');
|
|
10
11
|
const { PROPULSION_DEFAULTS } = require('./lib/propulsion-detector');
|
|
@@ -26,6 +27,9 @@ const DEFAULT_PLACE_MATCH_RADIUS = 200;
|
|
|
26
27
|
const FIRST_NAMING_DELAY_MS = 5 * 1000;
|
|
27
28
|
const NAMING_ERROR_RETRY_MS = 5 * 60 * 1000;
|
|
28
29
|
const FIRST_FORECAST_DELAY_MS = 5 * 1000;
|
|
30
|
+
// Landmarks are not needed for the passage under way, only for reading it back,
|
|
31
|
+
// so the first lookup waits for the busier start-up work to be done.
|
|
32
|
+
const FIRST_LANDMARK_DELAY_MS = 30 * 1000;
|
|
29
33
|
|
|
30
34
|
const MOTION_LABELS = { underway: 'Under way', stopped: 'Stopped', unknown: 'Waiting for data' };
|
|
31
35
|
const MODE_LABELS = { autostate: 'navigation.state', fallback: 'speed fallback' };
|
|
@@ -56,7 +60,7 @@ module.exports = function (app) {
|
|
|
56
60
|
let settings = null;
|
|
57
61
|
let detector = null;
|
|
58
62
|
let namer = null;
|
|
59
|
-
let
|
|
63
|
+
let schedules = [];
|
|
60
64
|
// The open passage detection last reported, to notice a new one.
|
|
61
65
|
let lastActiveEntryId = null;
|
|
62
66
|
let usbExport = null;
|
|
@@ -132,6 +136,19 @@ module.exports = function (app) {
|
|
|
132
136
|
description: 'The public OpenStreetMap instance by default, or a self-hosted Nominatim',
|
|
133
137
|
default: GEOCODING_DEFAULTS.geocodingUrl
|
|
134
138
|
},
|
|
139
|
+
landmarksEnabled: {
|
|
140
|
+
type: 'boolean',
|
|
141
|
+
title: 'Read each journal line against the nearest landmark',
|
|
142
|
+
description:
|
|
143
|
+
'Fetches the lighthouses, capes, towers and harbours of the areas sailed through from OpenStreetMap, so every position in the logbook is also given as a bearing and distance from the nearest one (© OpenStreetMap contributors, ODbL). Without it, only the coordinates are shown',
|
|
144
|
+
default: LANDMARK_DEFAULTS.landmarksEnabled
|
|
145
|
+
},
|
|
146
|
+
overpassUrl: {
|
|
147
|
+
type: 'string',
|
|
148
|
+
title: 'Landmark service (Overpass API)',
|
|
149
|
+
description: 'The public Overpass instance by default, or a self-hosted one',
|
|
150
|
+
default: LANDMARK_DEFAULTS.overpassUrl
|
|
151
|
+
},
|
|
135
152
|
tidesEnabled: {
|
|
136
153
|
type: 'boolean',
|
|
137
154
|
title: 'Fetch the tide forecast at departure',
|
|
@@ -239,6 +256,14 @@ module.exports = function (app) {
|
|
|
239
256
|
enum: ['http', 'https'],
|
|
240
257
|
default: INFLUX_DEFAULTS.influxProtocol
|
|
241
258
|
},
|
|
259
|
+
influxQueryTimeoutSeconds: {
|
|
260
|
+
type: 'number',
|
|
261
|
+
title: 'InfluxDB query timeout (seconds)',
|
|
262
|
+
description:
|
|
263
|
+
'Each retrospective query is given up on and reported as an error past this, rather than hanging indefinitely against an unreachable or overloaded database',
|
|
264
|
+
default: INFLUX_DEFAULTS.influxQueryTimeoutSeconds,
|
|
265
|
+
minimum: 1
|
|
266
|
+
},
|
|
242
267
|
influxSelfContext: {
|
|
243
268
|
type: 'string',
|
|
244
269
|
title: 'InfluxDB vessel context',
|
|
@@ -252,10 +277,11 @@ module.exports = function (app) {
|
|
|
252
277
|
try {
|
|
253
278
|
const outcome = detector.tick();
|
|
254
279
|
usbExport.afterDetection(outcome);
|
|
255
|
-
// A passage just opened: fetch its forecasts
|
|
256
|
-
//
|
|
280
|
+
// A passage just opened: fetch its forecasts and the landmarks of where
|
|
281
|
+
// it is starting from now, not at the next idle poll or at the end of a
|
|
282
|
+
// retry delay left from an earlier failure.
|
|
257
283
|
if (outcome.activeEntryId !== null && outcome.activeEntryId !== lastActiveEntryId) {
|
|
258
|
-
|
|
284
|
+
schedules.forEach((schedule) => schedule.nudge());
|
|
259
285
|
}
|
|
260
286
|
lastActiveEntryId = outcome.activeEntryId;
|
|
261
287
|
const usbError = usbExport.status().lastError;
|
|
@@ -344,6 +370,8 @@ module.exports = function (app) {
|
|
|
344
370
|
placeMatchRadius: config.placeMatchRadius ?? DEFAULT_PLACE_MATCH_RADIUS,
|
|
345
371
|
geocodingEnabled: config.geocodingEnabled ?? GEOCODING_DEFAULTS.geocodingEnabled,
|
|
346
372
|
geocodingUrl: config.geocodingUrl || GEOCODING_DEFAULTS.geocodingUrl,
|
|
373
|
+
landmarksEnabled: config.landmarksEnabled ?? LANDMARK_DEFAULTS.landmarksEnabled,
|
|
374
|
+
overpassUrl: config.overpassUrl || LANDMARK_DEFAULTS.overpassUrl,
|
|
347
375
|
tidesEnabled: config.tidesEnabled ?? TIDE_DEFAULTS.tidesEnabled,
|
|
348
376
|
tideUrl: config.tideUrl || TIDE_DEFAULTS.tideUrl,
|
|
349
377
|
weatherEnabled: config.weatherEnabled ?? WEATHER_DEFAULTS.weatherEnabled,
|
|
@@ -364,6 +392,8 @@ module.exports = function (app) {
|
|
|
364
392
|
influxUsername: config.influxUsername || null,
|
|
365
393
|
influxPassword: config.influxPassword || null,
|
|
366
394
|
influxProtocol: config.influxProtocol || INFLUX_DEFAULTS.influxProtocol,
|
|
395
|
+
influxQueryTimeoutSeconds:
|
|
396
|
+
config.influxQueryTimeoutSeconds ?? INFLUX_DEFAULTS.influxQueryTimeoutSeconds,
|
|
367
397
|
influxSelfContext: config.influxSelfContext || null
|
|
368
398
|
};
|
|
369
399
|
|
|
@@ -411,23 +441,29 @@ module.exports = function (app) {
|
|
|
411
441
|
});
|
|
412
442
|
namingTimer = setTimeout(runNaming, FIRST_NAMING_DELAY_MS);
|
|
413
443
|
// Same idea as geocoding: network calls fetching the tide and weather
|
|
414
|
-
// forecasts near a recent departure,
|
|
415
|
-
// inside detection.
|
|
416
|
-
const
|
|
444
|
+
// forecasts near a recent departure, and the landmarks of the areas
|
|
445
|
+
// sailed through, each on its own chain rather than inside detection.
|
|
446
|
+
const networkOptions = { db: database, settings, userAgent: `signalk-chiplog/${version}` };
|
|
417
447
|
const log = (level, message) => (level === 'error' ? app.error(message) : app.debug(message));
|
|
418
448
|
lastActiveEntryId = null;
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
label: 'Tide',
|
|
422
|
-
|
|
449
|
+
schedules = [
|
|
450
|
+
createBackgroundSchedule({
|
|
451
|
+
label: 'Tide forecast',
|
|
452
|
+
resolver: createTideForecaster(networkOptions),
|
|
423
453
|
log,
|
|
424
454
|
firstDelayMs: FIRST_FORECAST_DELAY_MS
|
|
425
455
|
}),
|
|
426
|
-
|
|
427
|
-
label: 'Weather',
|
|
428
|
-
|
|
456
|
+
createBackgroundSchedule({
|
|
457
|
+
label: 'Weather forecast',
|
|
458
|
+
resolver: createWeatherForecaster(networkOptions),
|
|
429
459
|
log,
|
|
430
460
|
firstDelayMs: FIRST_FORECAST_DELAY_MS
|
|
461
|
+
}),
|
|
462
|
+
createBackgroundSchedule({
|
|
463
|
+
label: 'Landmark lookup',
|
|
464
|
+
resolver: createLandmarkFinder(networkOptions),
|
|
465
|
+
log,
|
|
466
|
+
firstDelayMs: FIRST_LANDMARK_DELAY_MS
|
|
431
467
|
})
|
|
432
468
|
];
|
|
433
469
|
lastStatus = null;
|
|
@@ -455,8 +491,8 @@ module.exports = function (app) {
|
|
|
455
491
|
clearTimeout(namingTimer);
|
|
456
492
|
namer?.stop();
|
|
457
493
|
namer = null;
|
|
458
|
-
|
|
459
|
-
|
|
494
|
+
schedules.forEach((schedule) => schedule.stop());
|
|
495
|
+
schedules = [];
|
|
460
496
|
usbExport?.stop();
|
|
461
497
|
usbExport = null;
|
|
462
498
|
replayJob?.cancel();
|
package/lib/api.js
CHANGED
|
@@ -4,6 +4,7 @@ const { ApiError, badRequest, conflict, notFound } = require('./errors');
|
|
|
4
4
|
const entries = require('./entries');
|
|
5
5
|
const events = require('./events');
|
|
6
6
|
const { renderExport } = require('./export');
|
|
7
|
+
const { listEntryLandmarks } = require('./landmarks');
|
|
7
8
|
const { isTimeZone, PDF_LANGUAGES } = require('./logbook-pdf');
|
|
8
9
|
const { toGeoJson, toGpx } = require('./formats');
|
|
9
10
|
const manoeuvreTypes = require('./manoeuvre-types');
|
|
@@ -229,6 +230,20 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
229
230
|
})
|
|
230
231
|
);
|
|
231
232
|
|
|
233
|
+
// The amers a line of this passage's journal can be read against (SPEC
|
|
234
|
+
// §4.13); which one each line takes, and the bearing, is worked out where
|
|
235
|
+
// the line is shown. Empty until the area's landmarks have been fetched.
|
|
236
|
+
readonly.get(
|
|
237
|
+
'/api/entries/:id/landmarks',
|
|
238
|
+
handle(({ db }, req) => {
|
|
239
|
+
const id = entryId(req);
|
|
240
|
+
entries.requireEntryRow(db, id);
|
|
241
|
+
const { limit, offset } = v.parsePagination(req.query);
|
|
242
|
+
const items = listEntryLandmarks(db, id);
|
|
243
|
+
return { total: items.length, limit, offset, items: items.slice(offset, offset + limit) };
|
|
244
|
+
})
|
|
245
|
+
);
|
|
246
|
+
|
|
232
247
|
readonly.get(
|
|
233
248
|
'/api/entries/:id/tide',
|
|
234
249
|
handle(({ db }, req) => {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
// Runs a
|
|
2
|
-
// each `resolveNext()` says when the next is due.
|
|
3
|
-
// when a passage opens, rather than leaving it to the
|
|
4
|
-
// delay. A nudge during a fetch runs it again as soon as
|
|
5
|
-
// never a second chain alongside.
|
|
1
|
+
// Runs a background resolver -- a departure forecast, a landmark lookup -- as
|
|
2
|
+
// its own chain of timeouts: each `resolveNext()` says when the next is due.
|
|
3
|
+
// `nudge()` runs it now -- when a passage opens, rather than leaving it to the
|
|
4
|
+
// idle poll or a retry delay. A nudge during a fetch runs it again as soon as
|
|
5
|
+
// that one ends, never a second chain alongside.
|
|
6
6
|
|
|
7
7
|
const ERROR_RETRY_MS = 5 * 60 * 1000;
|
|
8
8
|
|
|
9
|
-
function
|
|
9
|
+
function createBackgroundSchedule({ label, resolver, log, firstDelayMs }) {
|
|
10
10
|
let timer = null;
|
|
11
11
|
let running = false;
|
|
12
12
|
let nudged = false;
|
|
@@ -23,9 +23,9 @@ function createForecastSchedule({ label, forecaster, log, firstDelayMs }) {
|
|
|
23
23
|
nudged = false;
|
|
24
24
|
let result;
|
|
25
25
|
try {
|
|
26
|
-
result = await
|
|
26
|
+
result = await resolver.resolveNext();
|
|
27
27
|
} catch (err) {
|
|
28
|
-
log('error', `${label}
|
|
28
|
+
log('error', `${label} failed: ${err.stack ?? err}`);
|
|
29
29
|
result = { retryInMs: ERROR_RETRY_MS };
|
|
30
30
|
}
|
|
31
31
|
running = false;
|
|
@@ -36,7 +36,7 @@ function createForecastSchedule({ label, forecaster, log, firstDelayMs }) {
|
|
|
36
36
|
// Expected whenever the boat is out of reach of a network.
|
|
37
37
|
log(
|
|
38
38
|
'debug',
|
|
39
|
-
`${label}
|
|
39
|
+
`${label} unavailable, retrying in ${Math.round(result.retryInMs / 60000)} min: ${result.error.message}`
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
42
|
schedule(nudged ? 0 : result.retryInMs);
|
|
@@ -59,9 +59,9 @@ function createForecastSchedule({ label, forecaster, log, firstDelayMs }) {
|
|
|
59
59
|
stop() {
|
|
60
60
|
stopped = true;
|
|
61
61
|
clearTimeout(timer);
|
|
62
|
-
|
|
62
|
+
resolver.stop();
|
|
63
63
|
}
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
module.exports = {
|
|
67
|
+
module.exports = { createBackgroundSchedule };
|
package/lib/database.js
CHANGED
|
@@ -314,6 +314,50 @@ const MIGRATIONS = [
|
|
|
314
314
|
CREATE UNIQUE INDEX idx_log_entry_crew_unique_member
|
|
315
315
|
ON log_entry_crew (entry_id, crew_member_id)
|
|
316
316
|
WHERE crew_member_id IS NOT NULL;
|
|
317
|
+
`,
|
|
318
|
+
`
|
|
319
|
+
-- Landmarks -- amers (SPEC §4.13): the named features a journal line's
|
|
320
|
+
-- position is read against, "1.2 nm NE (053°) of Phare du Cap-Ferret".
|
|
321
|
+
-- Fetched from OpenStreetMap area by area and kept: the bearing itself is
|
|
322
|
+
-- worked out at read time, so a past passage fills in as soon as its area
|
|
323
|
+
-- is known, and nothing here is part of the logbook's record.
|
|
324
|
+
CREATE TABLE landmarks (
|
|
325
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
326
|
+
osm_type TEXT NOT NULL CHECK (osm_type IN ('node', 'way', 'relation')),
|
|
327
|
+
osm_id INTEGER NOT NULL,
|
|
328
|
+
name TEXT NOT NULL,
|
|
329
|
+
kind TEXT NOT NULL CHECK (
|
|
330
|
+
kind IN ('lighthouse', 'light', 'cape', 'landmark', 'beacon', 'harbour')
|
|
331
|
+
),
|
|
332
|
+
lat REAL NOT NULL,
|
|
333
|
+
lon REAL NOT NULL,
|
|
334
|
+
-- The nominal range of its light in metres, when OpenStreetMap gives one:
|
|
335
|
+
-- how far out this amer is still worth naming a position from.
|
|
336
|
+
light_range REAL,
|
|
337
|
+
created_at TEXT NOT NULL,
|
|
338
|
+
updated_at TEXT NOT NULL
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
CREATE UNIQUE INDEX idx_landmarks_osm ON landmarks (osm_type, osm_id);
|
|
342
|
+
|
|
343
|
+
-- Supports the bounding-box prefilter, as for places.
|
|
344
|
+
CREATE INDEX idx_landmarks_position ON landmarks (lat, lon);
|
|
345
|
+
|
|
346
|
+
-- The half-degree cells landmarks have already been fetched for. A fetch
|
|
347
|
+
-- covers its cell plus a margin, so every position inside the cell has all
|
|
348
|
+
-- the amers within range in the table -- one request per cell, ever.
|
|
349
|
+
CREATE TABLE landmark_areas (
|
|
350
|
+
cell_lat INTEGER NOT NULL,
|
|
351
|
+
cell_lon INTEGER NOT NULL,
|
|
352
|
+
fetched_at TEXT NOT NULL,
|
|
353
|
+
PRIMARY KEY (cell_lat, cell_lon)
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
-- Cleared once every cell a closed passage sailed through has been fetched.
|
|
357
|
+
-- An open passage stays pending: it keeps moving into new cells. Existing
|
|
358
|
+
-- passages default to pending, so their areas are fetched too.
|
|
359
|
+
ALTER TABLE log_entries ADD COLUMN landmarks_pending INTEGER NOT NULL DEFAULT 1
|
|
360
|
+
CHECK (landmarks_pending IN (0, 1));
|
|
317
361
|
`
|
|
318
362
|
];
|
|
319
363
|
|
package/lib/detection.js
CHANGED
|
@@ -314,11 +314,16 @@ function createPassageDetector({ db, readSelfPath, settings, clock = Date.now })
|
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
function openPassage(now, position, departure) {
|
|
317
|
+
const startIso = iso(departure.time);
|
|
318
|
+
// Only a passage that actually precedes this departure can push it later
|
|
319
|
+
// -- not just whichever entry happens to hold the latest end_time. A
|
|
320
|
+
// retrospective replay filling a gap before passages logged live (SPEC
|
|
321
|
+
// §4.10) would otherwise have every reconstructed departure clamped to
|
|
322
|
+
// the most recent (unrelated, later) passage's end.
|
|
317
323
|
const previousEnd = db
|
|
318
|
-
.prepare('SELECT MAX(end_time) AS endTime FROM log_entries')
|
|
319
|
-
.get().endTime;
|
|
320
|
-
const startTime =
|
|
321
|
-
previousEnd && iso(departure.time) < previousEnd ? previousEnd : iso(departure.time);
|
|
324
|
+
.prepare('SELECT MAX(end_time) AS endTime FROM log_entries WHERE start_time < ?')
|
|
325
|
+
.get(startIso).endTime;
|
|
326
|
+
const startTime = previousEnd && startIso < previousEnd ? previousEnd : startIso;
|
|
322
327
|
const start = departure.position ?? position;
|
|
323
328
|
const place = initialPlaceName(db, start, settings.placeMatchRadius);
|
|
324
329
|
|
package/lib/entries.js
CHANGED
|
@@ -216,13 +216,16 @@ function updateEntry(db, id, patch, { placeMatchRadius, now }) {
|
|
|
216
216
|
next.distance = patch.distance;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
// landmarks_pending: an edited departure or arrival position may sit in an
|
|
220
|
+
// area no landmarks have been fetched for (SPEC §4.13); the lookup checks
|
|
221
|
+
// and clears it again.
|
|
219
222
|
db.prepare(
|
|
220
223
|
`UPDATE log_entries SET
|
|
221
224
|
start_time = ?, end_time = ?,
|
|
222
225
|
start_lat = ?, start_lon = ?, end_lat = ?, end_lon = ?,
|
|
223
226
|
start_place_id = ?, end_place_id = ?, start_place_name = ?, end_place_name = ?,
|
|
224
227
|
start_place_pending = ?, end_place_pending = ?,
|
|
225
|
-
distance = ?, updated_at = ?
|
|
228
|
+
distance = ?, landmarks_pending = 1, updated_at = ?
|
|
226
229
|
WHERE id = ?`
|
|
227
230
|
).run(
|
|
228
231
|
next.start_time,
|
|
@@ -361,11 +364,13 @@ function mergeEntries(db, id, otherId, now) {
|
|
|
361
364
|
// index would otherwise reject the update below.
|
|
362
365
|
db.prepare('DELETE FROM log_entries WHERE id = ?').run(later.id);
|
|
363
366
|
|
|
367
|
+
// landmarks_pending: the merged passage now covers the later one's track
|
|
368
|
+
// as well, which may reach areas no landmarks have been fetched for.
|
|
364
369
|
db.prepare(
|
|
365
370
|
`UPDATE log_entries SET
|
|
366
371
|
state = ?, end_time = ?, stopped_since = ?, closed_by = ?,
|
|
367
372
|
end_lat = ?, end_lon = ?, end_place_id = ?, end_place_name = ?, end_place_pending = ?,
|
|
368
|
-
distance = ?, updated_at = ?
|
|
373
|
+
distance = ?, landmarks_pending = 1, updated_at = ?
|
|
369
374
|
WHERE id = ?`
|
|
370
375
|
).run(
|
|
371
376
|
later.state,
|
package/lib/event-watcher.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
const { createFreshnessTracker } = require('./detection');
|
|
2
2
|
const { activeEntryId, entryNearArrival } = require('./entries');
|
|
3
|
+
const { CRITICAL_STATES } = require('./events');
|
|
3
4
|
|
|
4
5
|
const METRES_PER_SECOND_PER_KNOT = 1852 / 3600;
|
|
5
6
|
|
|
6
7
|
const EVENT_DEFAULTS = { windSpeedThresholds: [20, 30], pressureDropThreshold: 4 };
|
|
7
8
|
|
|
8
9
|
const CHECK_INTERVAL_MS = 1000;
|
|
9
|
-
|
|
10
|
-
// Any notification reaching these states is critical, whatever its path.
|
|
11
|
-
const CRITICAL_STATES = new Set(['alarm', 'emergency']);
|
|
12
10
|
const NOTIFICATION_METADATA_KEYS = new Set([
|
|
13
11
|
'meta',
|
|
14
12
|
'values',
|