signalk-race-control
v0.12.11
Published
SignalK plugin and webapp for tracking elapsed and handicap-corrected (Time-on-Time) race time, with a per-boat editable TCF and boat names pulled from AIS when available.
Maintainers
Readme
signalk-race-control
A SignalK server plugin + webapp for planning, running, and reviewing races: elapsed and handicap-corrected time, a course chart with AIS replay, and a live projected finishing order — during and after the race.
- Named, plannable races — create races ahead of time, optionally with a scheduled start; switch between past and upcoming races from a dropdown to review results later. Nothing gets overwritten by starting the next race.
- Overnight / multi-day races — most races are same-day, so this is off by
default; a normal finish-time entry (like
21:14:07) already rolls over to the next calendar day on its own if it's earlier than the start, so an ordinary race finishing just after midnight needs nothing special. Check Overnight / multi-day race when creating one instead (fixed for that race, not changeable afterward) only once a single automatic day-rollover isn't enough — a multi-day distance race, say — and finish/start times are then entered with a full date, not just a time-of-day. Elapsed/corrected time already display past 24 hours either way (e.g.30:00:00). - Individual start times — override the race's single start time for one boat at a time (in its own Start time column, right next to Finish time): for a staggered/pursuit start, or to correct a boat that didn't actually get away with the fleet. Left blank, a boat just uses the race's own start time — shown there in a dimmed/italic style once the race starts, so it reads as "racing, using the fleet start" rather than as not having started; setting its own time (or clicking Now) switches it to a real override, shown normally. Starting or resetting the race clears every boat's override along with its finish time, ready for a clean re-run.
- Add/remove boats explicitly — type a name (autocompletes against the VET register, the cross-race boat registry, and any live AIS/self vessel — matching anywhere in the name, not just the start) and click Add Boat. Boats aren't auto-populated from AIS; you control exactly who's racing. Rows stay in the order boats were added — adding or removing one doesn't reshuffle the rest — until the race actually starts, when the table switches to rank order; whichever boat is marked self always stays on top regardless of phase. The Boat column stays pinned in place while scrolling sideways through the rest of the (wide) table, and the page itself isn't width-capped, so a larger screen shows more columns at once without scrolling.
- Elapsed / corrected time — Time-on-Time correction:
corrected = elapsed × TCF, where TCF is edited per boat directly in the webapp and persisted server-side, shared by everyone viewing the page. - VET-tall import — pick a boat's handicap straight from SSCA's VET-tall register.
VET-tall ("veteranbåt-tall") is Seilskøyteklubben Colin
Archer's (SSCA) own handicap register specifically for
traditional, gaff-rigged/classic wooden sailing boats, published as a downloadable
spreadsheet on ssca.no/aktiviteter/vet-tall
— this plugin resolves and downloads whichever sheet is linked there live, so it
tracks the current year's numbers automatically without needing an update. Each
sail-configuration variant (e.g. with/without topsail) is a separate alternative in
a per-boat dropdown; picking one applies that exact TCF, and the dropdown keeps
showing whichever alternative is currently in effect. Since it's specific to
traditional boats, it won't have every boat in a mixed fleet, so it's off by
default — turn it on in the plugin's settings (
vetEnabled) for clubs that actually race under VET-tall; see Cross-race TCF memory below for what happens to handicaps while it's off. - KTK import — the same idea as VET-tall, for Klassisk Treseiler
Klubb's (KTK) own KLR handicap
register, published each season as a table in a blog post — this plugin always
reads whichever post is most recent, so it tracks the current season without
needing an update. A KLR number isn't a TCF directly: corrected time is elapsed ×
(KLR / 100), so that conversion happens automatically. Off by default — turn it on
in the plugin's settings (
ktkEnabled) for clubs that race under KLR. If a boat is listed in both VET-tall and KTK, the dropdown shows alternatives from both (labeled by which is which), rather than picking one register over the other. - Cross-race TCF memory for boats outside every register — whenever a boat isn't matched in VET-tall or KTK (or both are disabled), the TCF you set for it by hand is remembered by boat name and applied automatically the next time a boat with that name is added to any race. A boat matched in either register never has its TCF carried over this way — it always starts from the default until you pick an alternative from the dropdown or edit it again.
- MMSI and sail number, remembered across races — set once per boat (MMSI can also be picked up automatically from a live AIS/self vessel with a matching name), both are remembered in a small cross-race registry: add a boat with the same name in a later race and its MMSI and sail number fill in on their own.
- Import a whole fleet from Manage2Sail — off by default (
raceImportEnabledin the plugin's settings, alongsidevetEnabled); once turned on, an Import boats from Manage2Sail section lets you paste a Manage2Sail event URL, pick one or more of its classes, and add every entry in them as a boat in the current race. Since a class's published handicap number isn't always a directly-usable Time-on-Time factor — Yardstick numbers, for instance, run the opposite direction (lower number = faster boat) and needTCF = 100 / number(or1000 /for the RYA scale) rather than being used as-is — the plugin converts it automatically once it recognizes the system. If a class's numbers don't clearly match a known system's scale, you're asked to pick which one applies (e.g. Yardstick vs. Portsmouth Yardstick) before anything is imported for it; anything else unrecognized (ORC, IRC, ...) is used as-is, same as before. Re-importing updates TCF on boats it already added (matched by name) instead of duplicating them. Not every entry has a named boat — where Manage2Sail has no boat name, the sail number is used as the identifier instead of falling back to the skipper's name, and every imported boat's sail number is also set in its own Sail # column (editable directly, like MMSI) whether or not it ended up as the name. - Editable finish times — click Now to record a finish as it happens, type a
specific
HH:MM:SSinto the finish-time field to correct a mistimed click, or Clear to undo. Handles races that cross midnight. DNS marks a boat as never having started at all, distinct from DNF (started but didn't finish) — both show up as their own status in the Excel export and rank last, and either one can be undone the same way. Recording a real finish time clears either. - Start timer — once a start line is set and a start is scheduled, an expandable Start timer panel shows a countdown to the gun alongside three live numbers for this vessel specifically (from its own SignalK GPS/speed, not any other tracked boat): distance to the line, ETA to reach it at current speed, and time to burn — the countdown minus that ETA. Positive means time to spare (you'll arrive early, so slow down or take a longer approach); negative means you're behind schedule to make the line before the gun. Disappears once the race actually starts, since the pre-start approach is moot by then.
- Course & chart — set the start line, an ordered list of rounding marks, and the finish line by whichever's easiest: type lat/lon directly, type a name to autocomplete against existing SignalK waypoints (e.g. ones already placed from a chart plotter) — or a real navigation aid's name (a buoy, beacon, light, mark — OpenSeaMap's own data, searched live) within whatever area the chart is currently showing (tagged "OpenSeaMap" to tell it apart from a saved waypoint) — and pick one to fill in its position, or use the position button — it reads Pick on map while that point is still unset (arm it, then click the spot on the chart below — a click is rarely pixel-perfect on the actual mark, so if there's a real navigation aid within 30m of wherever was clicked, it asks whether to snap to that aid's exact position and name instead of the raw click), and switches to Use my position once it has one, to re-centre it on wherever you are right now instead. Once a navigation aid's been picked (or snapped to) anywhere in the course, it's offered instantly in every other start/finish/mark row too — no repeat OpenSeaMap lookup for the same one, e.g. reusing a rounding mark as the finish pin. Setting a position this way (map click or self position — not while still typing lat/lon by hand) also suggests a name for it from OpenStreetMap, when there's a real charted place right there and the name field is still empty; typing or picking a waypoint always wins over the suggestion. A mark's own Pick on map only turns on once the start line has a position — before that the chart has nothing to anchor its zoom to and is still showing the whole world, not much use to click into (typed lat/lon, autocomplete, and "Use my position" all stay available regardless). Every other known waypoint (not part of this race's own course) shows on the chart too, as a small dim pin with its name, so it's there to reference — or type into a mark's name field — even before it's used. The chart itself runs almost the full page width. Scrolling over it doesn't zoom it by default — the page scrolls past normally, same as anywhere else on the page — click it once to turn scroll-to-zoom on for as long as the mouse stays over it; move off and scrolling past it again goes back to just scrolling the page. It auto-fits to the course, waypoints, and boat tracks once when a race is first opened — panning or zooming around after that doesn't get undone on its own; the ⊙ button under the zoom controls re-centers on demand, scoped to just this race's own start/finish/marks (not other waypoints or wherever the boats have since sailed to) — a stable "back to the course" reset rather than one that shifts as the race goes on. With no course entered yet, it centers on this vessel's own current position instead of doing nothing. It's a real background map, like freeboard-sk's: it uses whatever tile-based chart resource(s) this SignalK server has registered, or falls back to public OpenStreetMap tiles when none is configured (same fallback freeboard-sk itself uses). Registering more than one chart offers them as alternative base layers via the layer switcher (top right of the map) rather than stacking them all at once — a plain street map on top would otherwise bury a real nautical chart underneath it. When one of them is Eniro's plain map (not "Eniro sjø"), that's the one shown by default — the easiest to actually read at a glance; otherwise it's whichever one loaded first. OpenSeaMap's navigation aids (buoys, lights, marks) are offered there too, as an overlay on top of whichever base chart is active, on by default. Overlaid on all of that is each AIS-tracked boat's recorded track for the current race — drag the replay slider to step back through it, or leave it on Live. Click Play to have it step through on its own instead, at whatever speed the adjoining slider is set to (1x-60x); it starts over from the earliest recorded position when played from Live (there's nothing to play forward into from there), stops on its own once it catches up to the latest one, and dragging the main slider or clicking Live stops it early. Every actual recorded position (an AIS sample, or a manually-recorded mark rounding) shows as a small dot along the track, so it's clear which points are real; wherever the slider sits between two of them, the boat's position is linearly interpolated and drawn as a hollow ring instead of a solid dot, so an in-between position always reads as estimated rather than observed. It falls back to just the last real position (no ring) instead of interpolating across a gap longer than 5 minutes — AIS dropping out, or a mark rounding recorded far from any real fix — since a straight line across a gap that long would be a guess, not an estimate. A notable pickup in speed between two consecutive samples (a gust, better trim, planing) is marked right on the track as a ring in that boat's own colour, sized a bit larger for a bigger jump — hover it for the exact speed reached. The course is also published as SignalK waypoint/route resources for any chart plotter (e.g. freeboard-sk) that reads the standard resources API — both directions (reading existing waypoints for the autocomplete, and publishing the saved course) only do anything if your server has a resources provider installed; they're a no-op otherwise, never a failure. Once a race has started, the moment this vessel's own position actually crosses the start line (a real crossing between two GPS fixes, not just getting close to it), the course is also activated as this vessel's SignalK navigation route — so a chart plotter reading the standard Course API starts showing/guiding to the next mark automatically, without anyone having to find and select the route by hand right as the race gets underway. Requires both a resources provider and a server new enough to have the Course API; a no-op otherwise. Whenever this vessel's own true wind direction and a moving SOG are available from SignalK, the chart also draws a pair of 10-minute laylines from its live position — where a further 10 minutes on each close-hauled tack would put it, using a generic ~40° tack angle rather than this boat's own polar data. Absent whenever that wind/speed data isn't, same as everything else on this chart that depends on live SignalK data. A Wind shift instrument sits above the chart alongside it, once there's enough live wind history (30s or more) to call a trend from: it tracks a rolling ~3-minute average true wind direction as a baseline and compares the current reading against it, reporting Lift or Header (with the shift in degrees) or Steady below a small deadband — tack-aware, since the same shift is a lift on one tack and a header on the other, using this vessel's own heading to tell which. It's a wind-trend read, not a mark-aware tactical simulator — no probability-of-gain number, just whether the breeze has moved for or against whichever tack you're currently on.
- Estimated finish time & live rank — while a boat is still racing, if it has a
live AIS position and speed and the race has a finish line, the plugin projects a
finish time and corrected time from its remaining distance and speed, and ranks it
accordingly. The distance routes around whichever marks the boat hasn't rounded yet
(detected automatically from its recorded track passing within a configurable radius
—
markRoundingRadiusM, 100m by default — of each mark in order) rather than cutting straight to the finish — accuracy still depends on that detection actually catching each rounding. A rounding can also be recorded by hand: each boat's row has a numbered pill per mark in the Marks column — click one to mark it rounded right now, click again to clear it — for a boat with no MMSI/AIS at all, or to correct one the automatic detection missed. Either source counts toward the same rounding, and for a boat still racing with no AIS-based estimate to rank by, marks rounded (by either method) takes priority over raw elapsed time, so a boat further round the course still ranks ahead even without live tracking. A manual rounding also records a position for the replay chart — the boat's live position if it has AIS, otherwise the mark's own position (a reasonable stand-in, since rounding a mark means being at it) — so even a boat with no AIS at all shows up on the chart at each mark it's recorded rounding. - Stop / call off the race — freezes elapsed/corrected time for everyone without touching boats, finish times, or the course (unlike Reset, which clears the race back to not-started), and marks every boat that hadn't finished or already been marked DNS as DNF, capturing its last known AIS position if one's available. You can also schedule a call-off for a future time (mirrors Schedule Start), and mark or un-mark an individual boat DNF or DNS by hand at any point, independent of the whole race. There's no true pause: Resume discards the stop, un-DNFs everyone it DNF'd, and the clock jumps straight back to real elapsed time — the time spent stopped isn't excluded from anyone's result. Recording a real finish time on a DNF'd boat clears its DNF. Also happens automatically the moment every boat has either finished or been marked DNF — nothing left to time, so the race calls itself off rather than sitting there with an idle clock until someone remembers to click Stop.
- Compare to a "self" boat — click the star next to a boat's name to mark it as self. Every other boat then shows, in the vs Self column: a live-ticking countdown to the moment self would tie them on corrected time if that boat has already finished (going negative, in red, once self can no longer catch up even by finishing instantly), or the current corrected-time gap if both are still racing or both have finished. With no boat marked self, the column instead compares everyone against the current leader (tagged accordingly), so it's never just blank.
- Export to Excel — a genuine
.xlsxsnapshot of the current standings, with one worksheet per class (each ranked on its own; boats with no class go on a "No class" sheet, and a race with no classes gets a single "Results" sheet) (same ranking, same rows as the on-screen table: rank, boat, sail number, MMSI, TCF, start time, finish time, elapsed, corrected, status), with the time columns rendered in your browser's own timezone rather than the server's (and with the date included, for a multi-day race). Start/finish are real date-time cells, and Elapsed/Corrected are live formulas from them (elapsed × TCF), not numbers baked in at export time — correct a start or finish time directly in the spreadsheet afterward and the rest recalculates. Left blank for a boat with no finish time (DNF, DNS, or still racing) rather than a snapshot of elapsed-so-far that would go stale the moment the file is reopened later. - Download Offline Timer — a single self-contained
.htmlfile, seeded with the current race's boats, TCF, and multi-day setting, that runs the core of race timing (start/stop/resume/reset, add/remove boats, edit TCF, individual start times, record finishes, DNF, self-comparison) with no server connection to this plugin at all — including its own editable Race start field (Now/Clear, right under the Start/Stop/Resume/Reset buttons) for backdating or correcting the race's start time directly, without needing to click Start Race and lose already-recorded progress — a backup for keeping a race running if this plugin's server becomes unreachable mid-event. It saves everything to that browser's own local storage, so closing and reopening the same downloaded file picks up right where you left off. If VET-tall is enabled, the register is seeded in at download time and a Refresh VET register link lets the offline page re-fetch the current sheet straight from Google Sheets (no plugin server needed for that — just whatever internet connection the browser has), with the same alternatives dropdown and autocomplete as the main webapp. If KTK is enabled too, its KLR numbers are seeded in the same way, but — since KTK's own page doesn't allow that kind of direct browser fetch — only as a snapshot from download time, with no offline refresh equivalent. AIS boat names/positions, the course/chart, and importing a fleet from Manage2Sail still need the live server (Manage2Sail's own API doesn't allow browser-side fetches at all) — TCF is entered by hand for anything the VET register doesn't cover. Also has its own "Download results as CSV" button.
Install
The easiest way is the SignalK App Store (or npm install signalk-race-control).
The plugin has no runtime dependencies, so a checkout also works as-is — copy (or
symlink) it into your SignalK server's node_modules, e.g.:
cd ~/.signalk/node_modules
ln -s /path/to/race-control signalk-race-controlThen restart the SignalK server, enable "Race Control" under
Server → Plugin Config, and open the webapp from the SignalK Webapps list
(or navigate to /signalk-race-control/).
Using it
- Click + New Race, give it a name (e.g. "Onsdagsseilas 3"), and check Overnight / multi-day race only if a single automatic day-rollover on finish times won't be enough (can't be changed after creating the race) — leave it unchecked for the usual same-day race, even one that finishes after midnight. It becomes the active race, shared across every open browser tab/device.
- Add boats by name. Each gets a default TCF of 1.0 — edit it directly, or use the VET-alternatives dropdown once a matching register entry is found. Set an MMSI per boat if you want its AIS position tracked for the chart/estimate.
- Optionally expand Course & chart and enter the start line, marks (in rounding order — reorder with ↑/↓), and finish line, then Save Course.
- For a staggered/pursuit start, optionally expand Classes (staggered starts) and add a class per start group (e.g. "Cruisers", "Spinnaker"). Assign each boat to a class from the dropdown in its row (or when adding it), then give each class its own start time (Now, once the race has started, or type one directly). A boat with no class assigned — or a class with no start time set yet — falls back to the race's own start time, same as before classes existed. A boat's own Start time override (for an individual correction) still wins over either.
- Either click Start Race now, or set a date/time and click Schedule Start — the race starts itself automatically at that moment (even across a server restart). A real date's always needed here regardless of a single- vs. multi-day race, since a start is often scheduled well ahead of race day itself. Clicked it a little late? Fix the recorded moment directly in the Race start field next to the clock (Now/Clear) — unlike Reset, it never touches any boat's finish time, DNF, or individual start override. If a particular boat actually started at a different moment (a staggered/pursuit start, or a correction), set its own time in the Start time column instead of leaving it to follow the race's start.
- As boats finish, click Now to stamp the current time, or type the exact time (plus date, for a multi-day race) into the finish-time field. Clear undoes a finish.
- Stop calls the race off now (freezes the clock, DNFs whoever hasn't finished); Schedule Call-off does the same at a future time instead. Resume discards a stop and un-DNFs whoever it DNF'd. Reset (click once to arm, again to confirm) clears this race's start/finish/DNF state, every class's start time, and recorded tracks entirely so it can be re-run — boats, their class assignments, and TCF values are kept.
- Click a boat's ☆ to mark it self and see the vs Self column fill in for every other boat.
- Export to Excel downloads the current standings as a
.xlsxfile at any time — before, during, or after the race. Download Offline Timer grabs a standalone backup copy of the race instead — worth doing before the start if you want a safety net in case the server drops out mid-race. - Switch races anytime via the dropdown at the top to review an earlier race's results, or plan the next one. Delete Race (arm-then-confirm) removes one.
Corrected time is shown live throughout the race (using elapsed-so-far), and freezes once a boat's finish time is recorded. The Est. finish column shows a projected finish time/rank for boats still racing, when the plugin has enough to estimate one.
Notes
- All race state (names, schedules, boats, TCF, finish times, course, recorded tracks) is persisted to disk under the plugin's data directory, so it survives a server restart.
- While a race is running, every unfinished boat with an MMSI gets a position sample recorded roughly every 15 seconds — that's the track the chart replays. Recording stops for a boat once it finishes; Start/Reset clear previously recorded tracks for a fresh run.
- The VET-tall source page can be overridden in the plugin's settings
(
handicapSourceUrl) — point it at a specific year's Google Sheet link directly to skip the SSCA page lookup, e.g. if a club uses its own register. KTK's source page can likewise be overridden (ktkSourceUrl) — point it at a specific post directly to skip the label-listing lookup. - Whether the VET-tall register is used at all (
vetEnabled, off by default) is also a plugin setting, not a per-race or webapp-side option — same for KTK's KLR register (ktkEnabled) — change either under Server → Plugin Config → Race Control and restart the plugin (the server does this automatically on save) for the webapp to pick it up. - The mark-rounding radius used for the estimated finish time and remaining-distance
calculations (
markRoundingRadiusM, 100m by default) is also a plugin setting — loosen it for a fleet with noisier AIS tracks, or tighten it if marks sit close together and a boat's radius circles are overlapping.
