Skip to content

Development

Greg Wilkins edited this page Oct 26, 2026 · 3 revisions

How to work on the plugin: run it on a scratch Signal K server, feed it the recorded race, and run the tests.

The plugin has no runtime dependencies, so a checkout can be loaded by a server as it is: the server finds plugins and webapps in <config dir>/node_modules/ by their signalk-node-server-plugin and signalk-webapp keywords (signalk-server src/modules.ts, modulesWithKeyword). A plain symlink is enough; no npm is needed in the server's config directory.

The recipe below was checked with signalk-server 2.30.0 on Node 22.22.2 (the HALPI2 versions), from an empty config directory.

1. A scratch server with its own config directory (recommended)

This keeps your real ~/.signalk untouched. Pick any directory, here ~/sk-dev:

mkdir -p ~/sk-dev/server ~/sk-dev/config/node_modules ~/sk-dev/config/plugin-config-data
cd ~/sk-dev/server
npm init -y
npm install signalk-server@2.30.0
ln -s ~/src/signalk-nemesis ~/sk-dev/config/node_modules/signalk-nemesis

In this layout npm puts the server's bundled plugins in ~/sk-dev/server/node_modules/@signalk/, where the server does not look (it scans <config dir>/node_modules/ and its own node_modules/, signalk-server src/modules.ts getModulePaths). Link any you need, e.g. the resources provider for waypoints such as the nemesis fallback:

mkdir -p ~/sk-dev/config/node_modules/@signalk
ln -s ~/sk-dev/server/node_modules/@signalk/resources-provider ~/sk-dev/config/node_modules/@signalk/

Enable the plugin before the first start (or do it later in the admin UI, see step 2):

echo '{"enabled":true,"configuration":{}}' \
  > ~/sk-dev/config/plugin-config-data/signalk-nemesis.json

Start the server on a port of your choice, with that config directory:

cd ~/sk-dev/server
PORT=3399 npx signalk-server -c ~/sk-dev/config

To run on exactly the boat's Node version: PORT=3399 npx -y -p node@22.22.2 node node_modules/.bin/signalk-server -c ~/sk-dev/config.

A fresh config has security off (the data feed in step 3 also works with it on). An EADDRINUSE :::10110 message only means another server already owns the NMEA 0183 TCP port; it is harmless here.

2. Check it loaded

3. Feed it the recorded race

With no boats around, feed the seeded AIS simulation of the Gold Coast 2026 race (our boat is Kanreki, MMSI 503029690; the other boats get made-up MMSIs) into the server over its WebSocket:

cd ~/src/signalk-nemesis
node test/sim/live-feed.js --url ws://127.0.0.1:3399 --hours 6 --live

--hours 6 puts "now" 6 hours into the race; that history is sent at once, then --live keeps sending in real time (Ctrl-C stops it). It sends the boat names, our own position, a made-up true wind direction (the recording has none) and the finish as an NMEA RMB next waypoint, but no start time: set one on the TCF tab (Now, or a date and time) or run signalk-racer alongside.

With security on, add --user <name> (a user with read/write or admin permission). It prompts for the password, logs in and sends with that token; the password can also be piped in (echo pw | node test/sim/live-feed.js ... --user admin). Without it a secured server silently drops the data. Only feed a test server: the feed sends our own position and a next waypoint, which on a boat would mix with the real instruments.

4. Edit, reload, repeat

  • Changes under public/ (the webapp) need only a browser reload with cache bypass (Ctrl+Shift+R).
  • Changes to index.js or lib/ need a server restart (Ctrl-C and start it again). The plugin's state (nemesis list, TCFs, start time override, 5 days of track history) is kept in ~/sk-dev/config/plugin-config-data/signalk-nemesis/ and survives restarts; delete that directory to start clean.

Using an existing local server instead

The same symlink works in an existing server's config directory, typically ~/.signalk:

ln -s ~/src/signalk-nemesis ~/.signalk/node_modules/signalk-nemesis

then restart the server and enable the plugin in the admin UI. Caveats:

  • Don't run npm install or npm link there for this. The link is not in ~/.signalk/package.json, so an npm run in that directory (including installing or updating any plugin from the Appstore) can remove it as extraneous: checked with a copied (unpacked) plugin, which npm 10 removed; a symlink is expected to go the same way. Look with readlink -f ~/.signalk/node_modules/signalk-nemesis and recreate it if gone.
  • With security on, feed with --user (step 3), and the webapp asks you to log in (the "login needed" link goes to the server's login page).

Without a Signal K server

A stand-in server runs the plugin on the simulated race and serves the webapp, for quick UI work:

node test/sim/dev-server.js 3300 6     # port, hours into the race

then open http://127.0.0.1:3300/signalk-nemesis/.

Tests

npm test                  # unit and end-to-end replay tests (node:test)
npm run test:browser      # webapp in headless Chromium (Playwright)
npm run test:node22       # both, on Node 22.22.2

The first browser run on a machine needs npm install in the checkout and npx playwright install chromium. npm run format applies the code style.

Generated images

npm run icon                   # the icon, public/assets/signalk-nemesis*.{svg,png}
node tools/readme-shots.js 6   # the README screenshots, docs/screenshots/, 6 hours into the race

Both render in headless Chromium, so they need Playwright installed as for the browser tests.

Adding a data source

For contributors adding a position source, such as YB Tracking read directly or a satellite AIS subscription. Please talk to Greg before writing much code (see the end of this section).

What every source does

A source adapter turns its feed into normalised reports and hands them to the plugin (brief section 4, wiki/overview.md):

{boatId, t, lat, lon, cogRad, sogMs, src}
  • SI units: t in ms since the epoch (the fix time, not the time it was received), lat and lon in degrees, cogRad in radians true, sogMs in m/s. Unknown COG or SOG is null.
  • src names the source. Anything other than 'ais' is drawn as a tracker fix (hollow, public/draw.js isTracker) and does not count for Show = AIS in range.
  • boatId is the MMSI string for AIS. A source without MMSIs uses its own prefix, as CYCA uses cyca:<entryId>.
  • The adapter never throws into the plugin: a failed poll is recorded and retried. One request chain at a time, and no blocking work.
  • The adapter drops fixes timed more than 2 minutes in the future (as lib/cyca-adapter.js does; the plugin does not check). Late fixes are fine: the track store inserts them in time order (lib/track-store.js).

Everything after the adapter (track store, VMG, lead, TCF, snapshot, drawing) is independent of the source. lib/ais-adapter.js and lib/cyca-adapter.js are the two examples.

What is CYCA specific today

The second source, the CYCA tracker, was written for CYCA alone. Around its adapter are:

  • lib/cyca-races.js and lib/cyca-state.js: the race list and the selected race, kept on disk.
  • lib/cyca-match.js and lib/cyca-mapping.js: suggesting the AIS boat for each tracker boat. Links are kept by name, across races.
  • lib/cyca-merge.js: a linked tracker boat shows as its AIS boat, its fixes filling only the gaps of more than 10 minutes either side of an AIS report.
  • lib/cyca-backfill.js: nemesis boats' tracks filled from the tracker over the display period.
  • The wiring in index.js, the /cyca endpoints and PUT handlers (Interfaces), and the CYCA tab (public/tab-cyca.js). Several places test for the cyca: prefix.

YB Tracking directly

YB is a tracker like CYCA's (the CYCA feed carries YB fixes), so it needs the same race picker, linking, merge and backfill. Please don't add a second copy of the cyca-* files. The better change is to turn that layer into a general tracker layer with CYCA and YB as two providers. That refactor should be agreed first.

Useful already: test/sim/yb-kml.js reads a YB race KML (https://yb.tl/<raceKey>.kml), and was checked against a boat's own log for the 2026 Tollgate Islands race: <when> is the fix time. Unlike CYCA, YB gives real fix times, not 10 minute slots.

Satellite AIS

Satellite AIS boats have MMSIs, so no linking is needed, but:

  • Same boat, two sources. Terrestrial and satellite fixes share the MMSI. Give satellite reports their own src (e.g. 'sat-ais') so they draw as tracker fixes and do not count as in range. Decide what happens when both have the same transmission a few seconds apart: near duplicates make the change in distance between reports noisy.
  • Not through Signal K vessel contexts. A separate plugin writing satellite positions into vessels.urn:mrn:imo:mmsi:<MMSI> would be read by lib/ais-adapter.js as terrestrial AIS, and would put other vessels' data into the server's data model. Fetch it in a Nemesis source adapter instead.
  • Cost. A paid feed needs a deliberate poll rate, a limit to the boats or area asked for, and an API key in the plugin config (not in the repository or the logs).
  • Latency. Satellite fixes can arrive many minutes after their time. Stamp them with the fix time; staleness then shows them correctly.

Testing

Every change comes with tests (npm test, and npm run test:browser for the webapp). Use the recorded races: test/sim/cyca-sim.js serves a fake CYCA feed from the Gold Coast 2026 tracks, and test/plugin-cyca-replay.test.js runs the plugin on it end to end with the AIS simulator. A new source should have a fake feed of the same kind and a replay test like it.

Before you start

Open an issue or contact Greg with what you plan: the source, how boats are identified, how its fixes merge with AIS, and the configuration it needs. The tracker-layer refactor in particular is easier to agree up front than to review afterwards.