The erh:viam-chartplotter:chartplotter model is an rdk:component:generic
that hosts the chartplotter web UI and the NOAA ENC / WMS rendering server.
It serves the static frontend (dist/), proxies and caches NOAA WMS tiles
under /noaa-wms/, and renders ENC vector tiles under /noaa-enc/.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
port |
int | no | 8888 |
TCP port the embedded HTTP server listens on. |
noaa_cache_dir |
string | no | OS user-cache dir (<cache>/viam-chartplotter/) |
Directory under which the NOAA WMS proxy cache (noaa-wms/), the ENC store (noaa-enc/), and the OSM raster cache (osm/) live. Created on first run. |
noaa_cache_max_bytes |
int | no | 0 (unlimited) |
Soft cap for the WMS proxy cache (bytes). When exceeded the oldest tiles get evicted. 0 disables eviction. |
draft |
number | no | 6 |
Boat's draft in feet. Drives depth shading at chart-detail zoom: DEPMS covers 3.3 ft → draft, DEPMD covers draft → 2×draft, DEPDW (safe water, white) is ≥ 2×draft. Per-request override via ?sd=N on tile URLs. |
safe_depth_ft |
number | no | — | Legacy alias for draft. Used when draft is not set. |
myboat_icon_path |
string | no | bundled icon | Absolute path to a PNG used as the boat marker on the chart. Falls back to the bundled icon when unset. |
{
"name": "chartplotter",
"namespace": "rdk",
"type": "generic",
"model": "erh:viam-chartplotter:chartplotter",
"attributes": {
"port": 8888,
"draft": 6,
"noaa_cache_dir": "/var/lib/viam-chartplotter"
}
}Both bands key off the polygon's midpoint depth ((DRVAL1 + DRVAL2) / 2).
With the default draft = 6 ft:
z ≥ 12 (chart-detail, four-band):
| Midpoint depth | Band | Colour |
|---|---|---|
< 0 (drying) |
DEPIT | tan |
0 – 3.3 ft (< 1 m) |
DEPVS | saturated blue |
3.3 ft – draft |
DEPMS | light blue |
draft – 2×draft |
DEPMD | very light blue |
≥ 2×draft |
DEPDW | white (safe water) |
z ≤ 11 (coarse, two-band):
| Midpoint depth | Band | Colour |
|---|---|---|
< 0 |
DEPIT | tan |
0 – 2×draft |
DEPVS | saturated blue |
≥ 2×draft |
DEPDW | white (safe water) |
The chartplotter can show animated wind particles using
sakitam-fdd/wind-layer. It's wired
up as a togglable layer (off by default) and appears in the layers panel as
wind once the package is installed and wind data is published.
Wind data is served by the bundled NOAA weather cache at
/noaa-weather/gfs/latest.json (see below). The cache fetches the latest
GFS 0.25° UGRD/VGRD at 10 m above ground from NOMADS, parses the GRIB2
inline, and writes the JSON shape ol-wind consumes. No external
converter required — grib2json, ecCodes, Java, etc. are not needed.
To enable:
npm install(theol-windpackage is already listed inpackage.json).- Rebuild the frontend (
npm run build) and reload — togglewindon from the layers panel.
If ol-wind is missing or NOMADS is unreachable the chartplotter logs a
warning and leaves the layer out — nothing else breaks.
| Path | Purpose |
|---|---|
/noaa-weather/gfs/latest.json |
Latest GFS 0.25° UGRD + VGRD at 10 m, two-record JSON shaped for ol-wind. Disk-cached under <root>/noaa-weather/, soft TTL 90 min with stale-while-revalidate. Fetches the most recent published GFS cycle from NOMADS (walks back in 6 h steps until one returns 200). |
/noaa-weather/stats |
JSON cache stats: hits, refreshes, errors, current file size and mtime. |
ECMWF Open Data publishes a complete forecast every 6 h, and a chartplotter
fleet of any size would crush their free tier (and trip the rate limiter)
if every machine pulled directly. So one machine in the fleet runs the
erh:viam-chartplotter:wind-publisher model, which:
- Wakes up on a 15-minute heartbeat
- Walks the latest fully-published ECMWF cycle (newest first, falls back one cycle at a time if the freshest one is still publishing)
- Decodes 10u + 10v at each forecast hour (0…144 in 3 h steps = 49 fhs)
- Crops each fh into a fixed 6 × 6 grid of overlapping tiles
- Uploads each gzipped tile blob + a manifest + a
latest.jsonpointer to Cloudflare R2
Every other chartplotter in the fleet reads tiles from R2 (zero ECMWF
traffic). Default bucket: viam-chartplotter-ecmwf, default public URL:
https://pub-6ae2d2a870f74799a963dbc892ea400b.r2.dev.
You need exactly one machine running this component. Pick whichever one has reliable network and modest CPU to spare (the build phase is ~5–15 min per cycle but only fires after a new ECMWF cycle publishes, ~4×/day).
-
Create an R2 bucket (one-time, project-wide). Cloudflare dashboard → R2 → Create bucket → name it
viam-chartplotter-ecmwf. Enable the public r2.dev URL under Settings → Public access, and add a CORS policy so chartplotter browsers can fetch from it:[ { "AllowedOrigins": ["*"], "AllowedMethods": ["GET", "HEAD"], "AllowedHeaders": ["*"], "ExposeHeaders": ["ETag", "Content-Length"], "MaxAgeSeconds": 3600 } ] -
Create a Cloudflare API token scoped to that bucket with R2 Object Read & Write. The token format starting with
cfut_is the one that works with the auto-derive convenience (described below); the older "Access Key ID + Secret Access Key" pair from R2 → Manage R2 API Tokens also works if you prefer that route. -
Add the wind-publisher component to the chosen machine's Viam config. Minimal form (single API token, bucket defaults applied):
Other machines in the fleet should NOT have this component — only one publisher per bucket, otherwise multiple machines race to upload the same blobs.
-
Verify the first publish. Within a few minutes of startup the publisher logs should show
publisher: starting ecmwf cycle=… buildfollowed bypublisher: ecmwf cycle=… done in …. After that:curl -s https://pub-<your-hash>.r2.dev/wind/ecmwf/latest.json | jq '{cycle, publishedAt, fhs: .fhs | length, tiles: .tiles | length}' # {"cycle": "20260519T06", "publishedAt": "...", "fhs": 49, "tiles": 36}
-
Point the consumer chartplotters at the bucket. The default
wind_cdn_base_urlis already the project-wide R2 URL, so nothing to change on the consumer side unless you're using a private bucket.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
models |
string[] | yes | — | Models to publish. Currently only ["ecmwf"] is implemented. |
upload_enabled |
bool | no | false |
Off by default so adding the component doesn't immediately write to production. Flip to true after credentials are verified. |
r2_account_id |
string | yes (when upload_enabled) |
— | Cloudflare account ID. |
r2_api_token |
string | one of these two | — | Raw Cloudflare API token (single value, cfut_…). The Access Key ID is derived via Cloudflare's /verify endpoint, and the SigV4 Secret Access Key is computed as SHA-256 of the token value. |
r2_access_key_id + r2_secret_access_key |
string + string | one of these two | — | Legacy explicit S3 credentials, for setups that already store the derived secret. Either form works. |
r2_bucket |
string | no | viam-chartplotter-ecmwf |
Override only when running a staging fleet against a sandbox bucket. |
publish_offset_minutes |
int | no | 30 |
Minutes past the hour for the post-cycle wake-up. Tunable only if ECMWF starts publishing earlier. |
The same code path is exposed as a standalone CLI under
cmd/wind-publisher/. Useful for one-off publishes, dry-runs against
local disk, or backfilling a cycle the cron loop missed:
# Dry-run: build a cycle to local disk instead of R2
go run ./cmd/wind-publisher publish --out /tmp/publish-test ecmwf
# Real upload (env vars match the Viam attribute names with R2_ prefix)
export R2_ACCOUNT_ID=...
export R2_API_TOKEN=cfut_...
go run ./cmd/wind-publisher publish --r2 ecmwf
# Inspect the tile grid
go run ./cmd/wind-publisher tilesThe CLI shares the same raw-GRIB and tile-blob caches as the in-module
publisher (~/Library/Caches/viam-chartplotter-wind-publisher/ on
macOS, XDG cache dir on Linux), so re-running after a crash is fast.
The chartplotter component reads the CDN URL via its own config attribute (default points at the project bucket):
| Name | Type | Default | Description |
|---|---|---|---|
wind_cdn_base_url |
string | https://pub-6ae2d2a870f74799a963dbc892ea400b.r2.dev |
Base URL the frontend fetches ECMWF tiles from. Empty / unset → uses the project default. |
On boot the frontend probes <cdn>/wind/ecmwf/latest.json. If reachable
and < 24 h old, ECMWF becomes the default wind model and tile fetches
go straight to R2. If the CDN is stale (>24 h) the chartplotter falls
back to the local on-demand fetcher (which hits ECMWF directly — useful
as a last-resort but not at fleet scale). If the CDN is unreachable
entirely the chartplotter shows a wind-layer error rather than silently
falling back, to keep CDN outages from cascading 10K direct ECMWF
requests.
| Path | Purpose |
|---|---|
/noaa-enc/tile/{z}/{x}/{y}.png |
The live ENC vector tile the UI consumes. Supports ?sd=N (draft override, feet), ?style=, ?navaids=0, ?landfill=0, ?skip=…. |
/noaa-enc/compare/{z}/{x}/{y}.png |
Side-by-side panels — ours ‖ NOAA WMS ‖ diff ‖ OSM masked ‖ mask — for iterating renderer parity. |
/noaa-enc/compare/test?lat=&lon= |
HTML page stacking compare/ panels for the same lat/lon at z=7..16. Defaults to Charleston Harbor. |
/noaa-enc/debug?minLon=&minLat=&maxLon=&maxLat= |
JSON catalog/feature summary for the bbox. |
/noaa-enc/debug-tile/{z}/{x}/{y} |
JSON listing of the features painted in a tile, sampled. |
/noaa-enc/stats |
Cache and store stats. |
- Options to select color of: tracks, heading line, route line, ais targets and their tracks
- Routes ** See old routes ** save route ** Add: Undo button in case you accidentally delete a route like I just did ** Rum lines and courses
- AIS ** Friends ** Load old AIS tracks from history
The erh:viam-chartplotter:nav model is an rdk:service:navigation
implementation backed by an in-memory waypoint list that is mirrored to a JSON
file on disk so waypoints survive module restarts. The chartplotter UI auto-
detects this service, draws the current waypoints as an amber dashed route
from the boat through each waypoint, and exposes buttons to add a waypoint at
the boat's current position, drop one by clicking on the chart, or clear the
whole route.
| Name | Type | Required | Description |
|---|---|---|---|
movement_sensor |
string | no | Name of a movement sensor on the same machine. When set, the service's Location method reports that sensor's live position and compass heading, and the auto-arrival poller uses it to detect waypoint arrivals. |
data_path |
string | no | Absolute path to the JSON file used to persist waypoints. Defaults to <user-cache-dir>/viam-chartplotter/nav/<service-name>.json. |
arrival_radius_m |
number | no | When movement_sensor is set, the next waypoint is automatically marked visited (and disappears from the route) once the boat is within this many meters of it. Defaults to 200. Set to a negative number to disable, or omit to use the default. |
{
"name": "nav",
"namespace": "rdk",
"type": "navigation",
"model": "erh:viam-chartplotter:nav",
"attributes": {
"movement_sensor": "gps",
"data_path": "/var/lib/viam-chartplotter/nav.json",
"arrival_radius_m": 200
}
}Both attributes fields are optional — the dependency on movement_sensor
is reported automatically from the service's config validator, so no
explicit depends_on is needed. The minimal config is just model plus
name/type/namespace; in that case Location returns (0, 0) and
waypoints are written to the default cache path.
npm install
npm run dev
To create a production version of your app:
npm run buildYou can preview the production build with npm run preview.
To deploy your app, you may need to install an adapter for your target environment.
{ "name": "wind-publisher", "namespace": "rdk", "type": "generic", "model": "erh:viam-chartplotter:wind-publisher", "attributes": { "models": ["ecmwf"], "upload_enabled": true, "r2_account_id": "<your Cloudflare account ID>", "r2_api_token": "cfut_..." } }