Version: [5612] AIS Watchkeeper 0.43.0

0.43.0 - Strip & settings polish. (1) On the route-leg strip, the ship
SYMBOLS now declutter like the labels do: where triangles crowd along
the
axis they fan out perpendicular on the same side, ordered by speed -
the
slowest stays nearest the route line, the fastest goes furthest out -
and
each drops back to the line as soon as it no longer overlaps a
neighbour.
Names follow their (possibly pushed-out) triangle. (2) The AIS stream
Settings box is reorganised into two tight columns with the toggles
side by
side, removing the dead space.
0.42.0 - Custom routes (for users outside the bundled network). The
whole RWS FIS-VNDS route network is bundled in data/routes.gpkg and
ships in
the plugin. NEW: an editable QGIS "Custom routes" LineString layer -
draw a
line near where ships sail, name it, and it becomes a fairway you can
bind to.
The drawn direction (first vertex to last) is treated as INBOUND, the
reverse
as outbound (used for the start/end-based in/out counts). Custom
routes show
in the Routes tab in BLUE (marked "(custom)") and take binding
PRIORITY: when
a vessel is within tolerance of both a custom and a native route, it
binds to
the custom one (native binding is unchanged when no custom route is in
range).
Custom routes persist in custom_routes.gpkg in the profile dir (user
data) and
reload next start. Mainly to let people outside the EU define their
own routes.
0.41.0 - Per-route bind width. Tidal fairways are wide (the
Westerschelde mouth is nearly 1 km across) and ships legally sail well
off
the centreline, so a single 150 m bind tolerance dropped them as
off-fairway.
The Routes table now has an editable "Bind width (m)" box per route:
widen it
(e.g. 650 m) for a broad tidal fairway and vessels sailing off-axis
bind
there, while narrow canals/parallel routes keep the tight default so
they
don't mis-bind. The route a vessel lands on is still chosen by nearest
+
course alignment, so a wider tolerance never steals a vessel that is
closer
to another route; an off-centre bind just reads as lower confidence.
Values
persist per route in route_tol.json (profile dir) and reload next
start.
0.40.0 - User reference markers on the route. A long fairway like the
Westerschelde (~105 km, no locks or bridges) had nothing on its strip
to map
km to the real world. Now there's an editable QGIS point layer "Route
reference markers": drop a point near a fairway, name it (a buoy, a
VTS sector
boundary, a berth) with QGIS's native editing, and the route manager
portrays
it on the strip exactly like a bridge or lock - snapped to the nearest
route
and route-km (teal -). Markers persist in markers.gpkg in the profile
dir
(user data, survives plugin upgrades) and reload next start. They are
also
wired into route-CPA: pick a vessel then a marker to get the vessel's
ETA /
distance-to-go to that marker, just like bridges and locks.
0.39.0 - Two fairway datasets, each for what it is best at. BINDING +
ROUTE MANAGER now run on the Rijkswaterstaat FIS-VNDS "route" layer:
full-
stretch, properly-named routes (e.g. id 20940 = "Westerschelde", ~105
km as
ONE line) with linear-referencing km. The fine-grained "vaarwegen"
passages
are now a DISPLAY-only chart layer - each individual stretch labelled
with its
own name (Honte, Pas van Terneuzen, Vlissingen Rede, ...), thin, drawn
only
when zoomed in past 1:12500. We do NOT trust the service's own
"routejunctie"
layer; instead route_ingest.py passes over the route geometries and
flags a
connection only where >=2 routes actually share a vertex (~2 m) or a
dangling
endpoint lands on another route's interior (<=10 m T-branch). The
result
(2056 routes, ~2240 derived junctions) is baked into a bundled
resource
data/routes.gpkg so binding runs fully offline; re-run route_ingest to
refresh
from the live service. This supersedes the 0.38.1 per-route name
override. The
old on-canvas route polyline layer ("Routes in coverage") and its
click-picker
are removed - the route UX is the Routes table. Also: incoming
sentences with
a leading NMEA 4.0 / IEC 61162-1 TAG block (\\s:...,c:...*HH\\, added
by many
aggregators & multiplexers) are now stripped before
classify/validate/decode -
such feeds previously decoded almost nothing; live-tested at >4000
vessels.
0.38.1 - Routelines tidy-up: the on-canvas reference lines are half as
thick
(0.45 mm) and only draw when zoomed in past 1:12500.
0.38.0 - Route binding now uses the vessel's track, not a single
fix. A vessel is bound only after at least 3 position hits, and the
route it
lands on is ranked by the COURSE MADE GOOD across those hits (the leg
bearings between them) rather than one reported COG - so when several
routes
run nearby, it binds to the one sharing the stretch the ship is
actually
travelling, not a closer crossing/parallel fairway. A vessel under 2
kn for
more than 3 consecutive hits is DETACHED from its route
(moored/anchored
vessels stop being glued to whatever fairway they sit beside); it
re-binds
once it is moving again with a fresh 3-hit track. Reported COG is kept
only
as a fallback before a track course exists. Tunable via MapMatcher
(min_track_hits, slow_speed_kn, slow_detach_hits, min_move_m).
0.37.1 - Fix a Windows crash (access violation) when disconnecting /
unplugging a serial AIS receiver. The reader thread was blocked in
pyserial's
overlapped read (GetOverlappedResult) while the port handle was
closed/
finalised underneath it. SerialReader.stop() now aborts the in-flight
read
the thread-safe way via cancel_read() (CancelIoEx) and waits for the
thread to
finish; the run loop clears self._serial in finally and stays quiet on
a
stop-induced abort (reporting a clear "device disconnected?" only on a
real
fault). The dialog keeps a reference to a retiring reader until its
thread
ends, so it can't be garbage-collected (and close its port) mid-read.
The
exact unplug-during-read fault lives inside pyserial's C call and
can't be
fully caught in Python, but the close-under-read / GC races (the
common crash)
are gone and ordinary removals unwind cleanly. Proven 12/12 (clean
stop via
cancel_read, device-removed graceful unwind, stop-before-start safe).
0.37.0 - Routes UX: anti-collision strip names + a Routes table; map
routes are now quiet & non-interactive. (1) Strip ship-names no longer
overlap - they behave like objects that can't collide, pushed out
perpendicular to the route line by the FULL length of the name before
them
(greedy packing on each side via font metrics; the scene grows to fit
dense
clusters). Replaces the too-small fixed stagger. (2) On the chart the
in-coverage routes are now drawn as a quiet THIN DASHED reference
layer
(0.9 px) and are NON-interactive - the map-click tool/filter is
removed
(it was awkward). (3) New "Routes" tab after Vessels: a table (Fairway
|
Inbound | Outbound | Idle) of routes with vessels in coverage, sorted
by
moving vessels (in+out) desc; clicking a row opens that route's leg in
the
route manager. New engine RouteMapModel.route_table(). Route-CPA chart
portrayal is unchanged - it still draws when you run a CPA inside a
leg.
Proven headless 15/15 (route_table counts/sort/idle; anti-collision
stacking;
Routes tab populate/sort/row-open; dashed thinner layer); full suite
129/129.
0.36.0 - TCP login: credentials via QGIS's native encrypted auth
store.
The security dialog now uses QgsAuthConfigSelect to pick or create a
stored
credential; the secret lives ENCRYPTED in QGIS's auth database
(master-
password protected) and is decrypted only at connect to render the
login -
the plugin never holds it beyond the session and never writes it to
its own
files. A session-only manual fallback remains for a quick test. Per
host:port
the dialog remembers (in QgsSettings) the non-secret bits - the auth
config
ID, TLS flags, login template and method - so the next connect
pre-fills;
the secret stays in the auth DB, only the ID is remembered. Verified
headless
9/9 in an isolated temp auth DB: master pw set, credential stored,
decrypt
round-trip, dialog renders the login from the stored config, the id
(not the
secret) is remembered, and a scan of the on-disk DB confirms the
secret is
NOT in plaintext. Full suite 114/114. (QgsAuthConfigSelect's add/edit
editor
+ the live authenticated connect still want an in-QGIS run.)
0.35.0 - TCP source: scan the endpoint for security + login dialog.
When you Connect to a Network (TCP) source, the plugin now PROBES
host:port
first (new endpoint_probe.py, pure-stdlib, off the wire it only
listens):
(1) reachable? (2) TLS? - it attempts a TLS handshake; a ServerHello
means
the endpoint is TLS, otherwise it re-probes as plaintext; (3) auth
likely? -
if no NMEA streams on connect (and/or a text banner/login prompt
arrives).
If the endpoint looks secured (TLS or auth-likely) a login dialog
opens,
pre-filled from the scan: a TLS toggle (+ allow self-signed), an auth
method
(none / username+password / token), and a server-specific login-line
template
with {user}/{pass}/{token} placeholders (there is no standard AIS
login, so
the exact line(s) are configurable). TcpReader gained use_tls /
tls_insecure
/ login_lines: it wraps the socket in TLS and sends the rendered login
on
connect. The probe is best-effort and gated on 'reachable AND
secured', so a
normal plaintext feed connects exactly as before. Credentials are used
for
the session only (not written to disk; encrypted-store persistence is
a
later refinement). Addresses the 2026-06-25 audit's 'TCP feed is
cleartext /
no auth' note. Proven headless: probe 14/14 (reachable / TLS-fallback
/
streaming-NMEA / silent / banner / unreachable; TLS-positive path
complete,
skipped live for lack of a test cert), dialog+reader 8/8 (TLS prefill,
method
switching, login rendering, login sent CRLF on connect). Suite 105/105
green.
Live test wants a real TLS / authenticated AIS server. 0.34.0 -
Route-leg window: wheel-zoom + drag-pan restored. The strip
still opens at the panel-fit 1:1 layout (the start point), but the
user can
now wheel-zoom about the cursor and drag-pan from there. The zoom/pan
is
PRESERVED across the 2 s refresh and CPA/ETA redraws; only an explicit
fit
(open a route / Refresh / resize / Rotate) snaps back to the
panel-fit. Done
by resetting the view transform only on fit=True (the tick keeps it)
and
re-enabling wheel-zoom in the strip. Constant-size labels stay
readable at
any zoom. Proven 6/6 (zoom enabled, fit start 1:1, preserved across
tick/redraw, reset on explicit fit); full suite 77/77.
0.33.1 - Fix: could not start a route-CPA (regression from 0.33.0).
The second pick was never recognised - _on_click called _exit_select()
(which clears _cpa_pick) BEFORE _resolve_cpa_picks() read the picks,
so it
always fell through to "pick two vessels...". Now the two picks are
snapshotted
before _exit_select(); _resolve_cpa_picks takes them as an argument.
Covered by
a new two-click regression test (vessel+vessel meeting and
vessel+bridge ETA,
with a 2 s tick between picks). 4/4 + suite 73/73.
0.33.0 - Routes-on-map UX refinements + route-CPA to bridges/locks.
(1) The in-coverage routes layer now AUTO-REFRESHES every ~10 s (and
the
leg-window detail redraws live), so it tracks coverage without any
button.
(2) The "Routes on map" button is REMOVED - routes show automatically;
this
is gated on the fairway matcher already existing (built by binding),
so it
never forces a build or hangs the GUI at startup. (3) Clicking a route
now
uses a canvas event filter (RouteClickFilter) instead of a custom map
tool:
it never consumes the event, so the user's normal pan/zoom/identify
keeps
working and routes stay clickable regardless of the active tool (and
it
survives tool switches). (4) NEW route-CPA to fixed objects: in the
leg's
Route-CPA mode you can pick a vessel + a bridge/lock and get that
vessel's
ETA + distance-to-go to the structure (engine
RouteMapModel.object_eta);
drawn as a label at the object and in the header. Picking two vessels
still
gives the meeting CPA. Also fixed: the strip header is now
context-aware
(route/km, or the active CPA/ETA) so it no longer gets clobbered on
redraw.
Proven headless 73/73 across 7 suites (object_eta in/out/stationary;
routes
layer; window open-to-detail; orientation; CPA; vars; rule). The
on-canvas
click + auto-refresh wiring wants an in-QGIS click-through.
0.32.0 - Routes drawn on the QGIS map; click a route to open its leg.
The in-window octolinear schematic overview is RETIRED. The "Routes on
map"
button (was "Route map") now draws the in-coverage routes straight on
the
QGIS canvas at their real lon/lat ("Routes in coverage" layer -
coloured per
route, named, carrying routeid). New engine
RouteMapModel.coverage_polylines()
returns each in-coverage route's segments. Clicking a route on the map
opens
that route's leg in the route-leg window (the strip) via
RouteClickTool - an
always-on map tool that SUBCLASSES QgsMapToolPan so panning still
works (drag
pans; a no-drag click on a route opens it, hit-tested via
QgsMapToolIdentify).
The route-leg window is now detail-strip only (open_route(rid)).
Proven
headless 17/17 (coverage polylines in/out of range; layer build,
attrs,
geometry, idempotent rebuild; window opens to detail; tool subclasses
pan);
regression 46/46. The map-tool click flow (mouse events, identify
tolerance,
CRS) is compile-checked and wants an in-QGIS click-through.
0.31.0 - Route monitor: portrait/landscape re-layout + panel-bound
sizing. (1) Portrait is now a REAL re-layout (the km-axis runs
vertically),
not a view rotation - so in portrait ALL labels read normally
(horizontal),
fixing the unreadable rotated labels. Landscape keeps vertical labels
along
the horizontal axis. The whole strip is drawn in (along, perp)
coordinates
mapped by orientation, so one code path serves both. (2) The route
axis now
fills the panel's long side (width in landscape, height in portrait)
at 1:1,
floored at 500 px; a smaller panel scrolls (scrollbars) instead of
shrinking.
Wheel-zoom is disabled in the strip (it scrolls instead); the view
re-fits on
panel resize. The "Rotate" button toggles portrait/landscape. Proven
headless
8/8 (axis orientation, label orientation per mode, opposite-side
placement,
object line on the axis, 500 px floor); regression 38/38. 0.30.0 -
Route monitor (strip) visual overhaul. (1) The vertical layout
is now laid out in SCREEN pixels (offsets divided by the live zoom)
and
redraws on zoom, so spacing looks the same at any zoom and labels no
longer
collide/overlap when zoomed out. (2) More vertical breathing room
overall.
(3) The symbol-key legend moved OUT of the canvas into a real label
under the
view - it can no longer collide with vessel/object labels and is never
truncated. (4) All ship-name labels now read the same way as the
bridge/lock
labels (rotated -90, upward). (5) Declutter stagger is now zoom-aware
(screen-
gap threshold, up to 5 levels) so crowded names step apart even when
zoomed
out. (6) NEW: a "Rotate" button in the toolbar rotates the whole view
90 deg to
toggle landscape <-> portrait. Bridge/lock markers keep the touch-axis
+
dot + name-at-top behaviour, now positioned in the same screen-space
scheme.
Proven headless: route-map polish 11/11 + object marker 7/7 +
regression
38/38. 0.29.4 - Renamed the visible UI from "AIS NMEA Source" to "AIS
Watchkeeper": the docked panel title (the QGIS tab), the Plugins-menu
entry
and the toolbar/menu action all now read "AIS Watchkeeper". Internal
package
name (ais_nmea_source) and the dock objectName are unchanged
(load-bearing).
0.29.3 - Completed the route-CPA vessel highlight: the blue dot is now
a rule ON THE LIVE AIS LAYER. vessel_layer._style, after loading the
QML,
converts the renderer to rule-based (which preserves all authored
ship-type
symbology exactly) and appends ONE rule that draws a blue circle on
any
target whose vessel_stationID matches @routeCPAVesselOne /
@routeCPAVesselTwo
(filter uses array_contains+coalesce so empty/unset variables = no
dots).
Idempotent (never double-adds) and best-effort. So the highlight now
rides
the real moving target. Proven 8/8 (rule present + base symbology
preserved +
matches/excludes/cleared); route-map side unchanged (37/37). 0.29.2 -
Route-CPA vessel highlight reworked to a project-variable +
symbology-rule scheme. When a route-CPA is active the two vessels'
MMSIs are
written to project variables @routeCPAVesselOne / @routeCPAVesselTwo
(blanked
when the CPA is cleared); the old blue-ring "Route CPA vessels"
overlay layer
is retired. The blue dot is now drawn by a rule on the LIVE AIS
layer's own
symbology (matching vessel_stationID against the two variables) - so
it rides
the real moving target, not a static snapped point. (This window
writes the
variables; the symbology rule lives on the liveFeedAIS layer / its
QML.)
Variable set/clear + old-layer retirement proven 4/4. 0.29.1 -
Bridge/lock marker tweak: the dotted line is now SHORT - it
touches the route axis and rises only to just below the name, where it
ends
in a small dot, then a gap, then the object name (instead of running
far up
past it). Verified (7/7). 0.29.0 - Bridges & locks on the route
monitor (live from the RWS
FIS-VNDS WFS). Opening a route's strip now fetches that route's
bridges
(layer 'brug') and locks ('sluiskolk') from the FIS-VNDS WFS,
server-side
filtered by routeid (so only the few on that route are fetched), and
draws
each as a dotted line that touches the route axis and runs far upward,
with
the object name at the top - above the ship labels - read along the
line.
Purple = bridge, orange = lock. The objects carry routeid + route-km
in the
same scheme as the bundled fairway network, so they place by km
directly (no
geometry snapping). This is the plugin's first network call: it is
per-route,
cached, time-limited and fully best-effort - offline or a WFS hiccup
just
leaves the strip without the markers and never blocks the live AIS
feed.
Proven: pure GeoJSON parser, a live fetch (route 31596 -> 24 bridges),
and
the strip rendering (12/12); the earlier route-viewer/CPA work still
passes
(36/36). 0.28.1 - Route viewer / route-CPA interaction tweaks. (1) The
two
vessels in an active route-CPA are now highlighted on the main QGIS
map with
a blue ring ("Route CPA vessels" layer). (2) In the strip view, vessel
names
are rotated 180 degrees (still outbound-above / inbound-below, placed
via
their bounding rect so each stays fully on its own side). (3) Click a
ship in
the strip -> the main QGIS map recentres + zooms to that ship (snapped
position) and flashes it. (4) Click the route-CPA bar in the strip ->
the
main map zooms to the CPA meeting point. (5) The strip legend moved
lower
(clear of the inbound names) in a larger, constant-size font. All five
verified headless (10/10) with no regression to 0.28.0 (26/26 still
green).
0.28.0 - Route viewer + route-CPA display upgrades. Route viewer:
(1) vessel names now sit on the SAME side as the vessel's lane -
outbound
above the axis, inbound below - instead of all on one side, which
declutters
the strip; near-neighbour names additionally stack outward. (2) The
marker
colour is now a speed heat ramp (green at 2 kn -> red at 20 kn,
clamped red
above 20). (3) Vessels slower than 2 kn are dropped from the strip
(unknown-speed targets are always kept). Route CPA: now also predicts
the
OVERTAKING case - two vessels on the same course, where the faster
catches
the slower - drawn GREEN (head-on stays blue). On the chart, the two
ship
names sit on opposite sides of the fairway centre-line (from the
fairway
linestring bearing), each with 'DTG : x.x km' (distance to go to the
CPA
point) beneath the name; line + labels are green for overtaking, blue
for
head-on. Engine maths in route_map_model (route_strip sog+filter,
route_cpa
overtaking+DTG) proven offline; route-viewer + chart rendering
verified
headless (26/26 checks). 0.27.4 - Audit hardening + full annotation
pass. Removed a dead
shadowed _update_qgis_cpa stub in the route-map window; HTML-escape
untrusted
AIS vessel names before they go into Qt rich text; tidied cosmetic
nits
(duplicate import, uninitialised Binding.sog, unused imports). Added a
docstring to every function in the plugin (annotation coverage 36 to
100 percent).
No behavioural change. See documents/AUDIT_2026-06-25.md.
0.27.3 - Route-CPA chart labels simplified to the sketch: the
inbound and outbound ship names now sit on the blue CPA line itself,
one
on each side of the fairway, reading perpendicular to the line;
'routeCPA'
and the 24h meeting time sit at the top of the line. (Names are along
the
fairway axis via parallel line-labelling.)

yes

CaptainAhab

2026-07-05T14:27:55.500360+00:00

3.22.0

3.99.0

no

Version management

Plugin details