Repository navigation
Development
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.
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.
- Admin UI: http://localhost:3399/admin/, Server > Plugin Config > Nemesis Delta. Enable it there if you skipped the config file, and set the options (see Configuration).
- Webapp: http://localhost:3399/signalk-nemesis/ (also listed under Webapps).
- API: http://localhost:3399/signalk/v1/api/nemesis/snapshot returns JSON.
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.
- Changes under
public/(the webapp) need only a browser reload with cache bypass (Ctrl+Shift+R). - Changes to
index.jsorlib/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.
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 installornpm linkthere 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 withreadlink -f ~/.signalk/node_modules/signalk-nemesisand 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).
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/.
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.
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.
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).
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:
tin ms since the epoch (the fix time, not the time it was received),latandlonin degrees,cogRadin radians true,sogMsin m/s. Unknown COG or SOG isnull. -
srcnames the source. Anything other than'ais'is drawn as a tracker fix (hollow,public/draw.jsisTracker) and does not count for Show = AIS in range. -
boatIdis the MMSI string for AIS. A source without MMSIs uses its own prefix, as CYCA usescyca:<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.jsdoes; 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.
The second source, the CYCA tracker, was written for CYCA alone. Around its adapter are:
-
lib/cyca-races.jsandlib/cyca-state.js: the race list and the selected race, kept on disk. -
lib/cyca-match.jsandlib/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/cycaendpoints and PUT handlers (Interfaces), and the CYCA tab (public/tab-cyca.js). Several places test for thecyca:prefix.
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 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 bylib/ais-adapter.jsas 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.
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.
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.