A SignalK plugin that analyzes True Wind Direction (TWD) to detect, quantify and predict wind shifts — for tactical sailing, race preparation and plain curiosity about what the wind is doing.
Originally created by Johan Wallinder, extended in this fork with cycle prediction, tack awareness, multi-station tracking and a live dashboard.
Wind is rarely steady. On most race days the True Wind Direction oscillates around a mean — backing and veering in a semi-regular rhythm driven by thermals, gradient interplay and terrain. A boat that tacks in phase with those oscillations gains hugely on one that ignores them. The tactical questions are always the same:
- How big are the shifts? (the spread between min and max TWD)
- How often do they come? (the oscillation period)
- Which phase are we in right now? (lifted or headed, veering or backing)
- When is the next shift due? (the countdown that decides "tack now or hold")
Raw masthead data doesn't answer any of this directly — it looks like the green noise below. The underlying rhythm (hand-marked in red) is what we want the plugin to extract automatically:
Raw TWD (green) with the underlying oscillation traced by hand — the plugin's
job is to find this rhythm without the hand.
The same idea with the oscillation envelope marked: spread, period and phase
are the tactical currency.
The second ambition is pre-race preparation: hours (or a day) before the start, monitor the wind behavior at several weather stations around the racecourse — is the oscillation local or course-wide, is there a gradient between the inshore and offshore stations, what period should we expect where we'll be sailing? With the multi-station feature and data from Sjöfartsverket ViVa, that picture builds itself while the boat is still at the dock.
Raw TWD samples are collected into a short buffer (default 10 s) and reduced to one averaged data point using a proper circular mean (sin/cos components), so a wind oscillating around north doesn't produce nonsense. These averaged points form the time series everything else is computed from.
The averaged TWD is unwrapped into a continuous series (no 0°/360° jumps) and run through a zigzag detector: while the wind veers, the running maximum is tracked as a candidate peak; only when the wind has come back by at least the shift threshold (default 4°) is that candidate confirmed as a real peak, and the detector flips to hunting a trough. Wiggles smaller than the threshold never register — sensor noise cannot flood the statistics. Confirmed extremes are timestamped at the actual turning point, not at the moment of confirmation.
The cycle period is the average interval between consecutive peaks and
between consecutive troughs (one full oscillation). Certainty measures
how regular those intervals are: metronomic shifts score near 1.0, chaos
scores 0 — and a low score is the honest answer in unstable conditions, not
a malfunction. Because peaks and troughs alternate, the next extreme is
expected about half a cycle after the last one; timeToNextShift counts
down live from there. Old extremes expire so a shift from hours ago cannot
skew the estimate.
Two triggers pause data collection during maneuvers (the "don't chase your own tacks" filter): a heading change of more than ~11°, and the apparent wind angle crossing sides. AWA samples within ~3° of head-to-wind or beyond ~165° are ignored, since the AWA sign is pure noise there. Shore stations skip all of this — a lighthouse doesn't tack.
A misaligned wind sensor reads systematically different TWD on port vs starboard tack. The plugin keeps recent raw readings per tack, takes the circular mean of each side, and applies half the difference per tack — pulling both toward the common mean (a single global offset can never fix a side-to-side asymmetry). The means are computed from uncorrected data so the correction never feeds back into its own estimate. Caveat: if you tack on the shifts, genuine oscillation shows up as a port/starboard difference and gets partially calibrated away — leave this off unless you suspect sensor misalignment.
Not all shifts are oscillations. A cold front, developing sea breeze, or gradient wind change moves the mean TWD in one direction and keeps it there. The plugin detects these at two timescales:
Long-period regression (1 h and 3 h windows): a circular linear regression
on metricsHistory gives a gradientRate in °/hr. The 3 h window is more
robust against oscillation noise; the 1 h window reacts faster. A net
meanDrift1h value (how far the mean has actually moved in the last hour,
regardless of the regression slope) catches fast frontal passages that the
slope alone might understate.
Rapid shift detector (5 min vs prior 20 min): compares the circular mean
of the last 5 minutes to the baseline of the 5–25 minutes before that.
When they diverge by ≥ 10° the gradientShift.detected flag fires.
Hysteresis prevents flickering at the edge — once triggered, it clears only
when the gap drops below 5°.
Speed correlation: each analyzer maintains a 30-minute rolling speed
history (appendWindSpeed). When a detected gradient shift is simultaneously
accompanied by a ≥ 20% increase in 5-min mean speed relative to the prior
15-min mean, gradientShift.speedCorrelated fires — the classic signature of
a squall line or frontal passage ("looks like a cyclic shift but bigger, with
more wind").
Regime classification (regime path): combines oscillation quality
(certainty) with gradient strength to report "oscillating", "drifting",
"mixed" (oscillations riding a drifting mean — the tactically hardest case),
or "unknown". Multi-station stationConsensus (fraction of active ViVa
stations agreeing on gradient direction) distinguishes synoptic events from
local terrain effects.
With signalk-viva
installed and Track ViVa stations set to N, the plugin runs an
independent analyzer for each of the N nearest stations, in parallel with
the boat. Stations self-discover from whatever viva publishes (no path
configuration) and are re-ranked by distance every poll, so the active set
follows the boat if it moves mid-race. Station results publish under
environment.observations.viva.<station>.windshift.*; the boat stays on
environment.wind.windshift.*.
The built-in webapp shows a tactical metrics bar (average wind speed / gust, TWD, delta, trend, cycle, next-shift countdown, certainty gauge) over a "waterfall" chart of raw TWD, smoothed TWD, the min/max envelope, and wind speed + gust on a right-side knots axis. A Source dropdown switches between the boat and the tracked stations — every source is analyzed continuously in the background, so switching is instant: the chart seeds from server-side history and the metrics bar fills from the latest snapshot. Station views overlay the boat's smoothed TWD as a purple reference line, so a station and the boat can be compared directly on one chart. Cursor readouts are in compass degrees (direction) or knots (speed), and "minutes ago" on the time axis. Drag the chart to zoom in on a time window; double-click or press the "Reset zoom" button to return to the full view.
Time window buttons (30m / 1h / 3h / 6h / 24h) let you zoom the x-axis to any historical window. The plugin keeps 24 h of metrics history on disk, so right after a page load you can step back a full day; as live data accumulates the older history gradually rolls off (a page reload restores it).
Shift overlay (Last 2 / 3 / 5) replaces the waterfall with a comparison chart: each of the last N detected shift half-cycles is plotted on a common 0–100% time axis, with Δ° from the shift start on the y-axis. The most recent shift is drawn bright green, older ones in progressively dimmer shades. This makes it easy to see whether the current shift is tracking the same shape and amplitude as the last few — useful for building confidence before committing to a tack. Click Off to return to the normal waterfall.
Gradient metrics in the header: a Drift readout shows the 1-hour
gradient rate as ↗ 8.1°/h (veering) or ↘ 5.3°/h (backing), coloured
white when negligible, orange above 3°/h, and green/red above 5°/h. A
Regime badge shows "Oscillating", "Drifting", or "Mixed" with a station
consensus percentage when multiple stations are active.
When a rapid gradient shift is detected, a pulsing red alert banner appears
below the controls bar: ⚠ GRADIENT SHIFT: 18° veering + wind increase.
It clears automatically when the shift settles (gap back below 5°).
The "Avg Wind" header value is a 2-minute rolling average of received wind
speed samples, smoothing out short-term noise. Gust is the instrument's own
reported gust value — for ViVa shore stations this maps to Byvind; for the
boat it requires a gust sensor mapped to environment.wind.gust in the
instrument configuration.
Branches:
| Branch | Version | Status |
|---|---|---|
main |
v0.0.6 | Stable, single-source original |
feature/multi-station |
v0.3.5 | Multi-station, soak test complete, merge pending |
feature/gradient-detection |
v0.4.0 | Gradient detection — currently running on Pi |
Live soak test: the plugin runs 24/7 on a Raspberry Pi (KarukeraPi), analyzing the Vinga lighthouse TWD as the "boat" source plus the five nearest ViVa stations on the Bohuslän coast. Cycle detection locks in within ~1 h on oscillating days; certainty correctly stays low in messy morning gradient. The gradient detector is accumulating real-world data — waiting for a frontal passage to validate the rapid-shift and speed-correlation triggers.
Persistence: 24 h metrics history per source, saved every 5 min and on shutdown, restored on startup. The waterfall chart survives server restarts. Analyzer state (cycles, calibration, gradient history) rebuilds from live data within ~30 min.
Known limitations:
- Cycle metrics need a few completed ≥4° swings — expect zeros for the first ~30 min after a restart or in very light, steady air.
- The grey "Raw" line only draws when the source publishes raw TWD in real time (stations do; the boat does when instruments are live).
- Gradient thresholds (10° rapid-shift, 20% speed spike) are hard-coded — they will become config options once real-world data shows tuning needs.
- Not yet published to npm (install from GitHub, see below).
Roadmap: see PLAN.md for the full milestone history and upcoming work (upwind early warning, npm publish, forecast overlay).
Flipping between the boat and five ViVa stations (distances shown). Each
source keeps its own analysis, history and metrics.
A detected 3.5-minute cycle with the next shift predicted in 44 seconds,
during a 68° spread — the certainty gauge stays humble about it.
Current settings during the soak test: shore-station source path, maneuvers
ignored (boat swings at the mooring), five stations tracked. The status line
at the top shows live per-source analysis point counts.
Long-term view: raw data (green) inside the emitted min/max envelope
(blue/orange).
All paths are emitted for the boat and (where applicable) for each active
ViVa station. Boat prefix: environment.wind.windshift.*. Station prefix:
environment.observations.viva.<slug>.windshift.*.
| Path | Description | Unit |
|---|---|---|
…windshift.avg |
Smoothed (averaged) True Wind Direction | rad |
…windshift.min |
Minimum TWD in the tracking period | rad |
…windshift.max |
Maximum TWD in the tracking period | rad |
…windshift.delta |
Cyclic shift amplitude (max − min) | rad |
…windshift.cyclePeriod |
Average time between wind shifts (full cycle) | s |
…windshift.timeToNextShift |
Estimated time to next predicted shift | s |
…windshift.certainty |
Oscillation confidence score (0.0–1.0) | - |
…windshift.trend |
Short-term direction: 1 veering, −1 backing, 0 steady | - |
…windshift.calibrationOffset |
Half the port/starboard tack asymmetry | rad |
…windshift.isSettled |
1 if boat is settled, 0 during tack lockout | - |
| Path | Description | Unit |
|---|---|---|
…windshift.gradientRate |
Rate of change of mean TWD over last 1 h (+ veering) | rad/s |
…windshift.meanDrift1h |
Net circular drift of mean TWD over last 1 h | rad |
…windshift.regime |
0 unknown, 1 oscillating, 2 drifting, 3 mixed | - |
…windshift.gradientShift.detected |
1 when a sudden persistent shift ≥ 10° is active | - |
…windshift.gradientShift.degrees |
Magnitude and sign of the detected shift (+ veering) | rad |
…windshift.gradientShift.speedCorrelated |
1 when a ≥ 20% speed increase accompanies the shift | - |
Note: min, max, and meanDrift1h are kept continuous through the 0°/360°
boundary so charting tools can draw them without wrap artifacts.
- TWD Buffer Time: How long to average TWD to smooth out noise (seconds).
- Min/Max Calculation Time: The window of time to keep data for calculating spread (minutes).
- Shift Detection Threshold: How far the wind must swing back before an extreme counts as a shift (degrees, default 4).
- Dynamic Window: Auto-tune tracking period based on detected cycle.
- Auto-Calibrate: Detect and correct for tack-induced errors by comparing Port vs Starboard means.
- Tack Lockout Time: Seconds to ignore data after a maneuver (default 60s).
- TWD Source Path: Which SignalK path to analyze (default
environment.wind.directionTrue). - Ignore Maneuvers: Skip heading/AWA tack detection entirely.
- Track ViVa Stations: Number of nearest shore stations to analyze in parallel (0 = off).
For soak testing without going sailing, point the "boat" analysis at a nearby weather station instead of the masthead:
- TWD Source Path:
environment.observations.viva.vinga.wind.directionTrue - Ignore Maneuvers: on — otherwise the boat swinging at the mooring with wind and current keeps triggering the maneuver lockout, even though the station's TWD is unaffected by what the hull is doing.
Station data arrives at polling rate (30–60 s), so expect one analyzed point every minute or two: coarse but plenty for verifying cycle detection, prediction and long-run stability over days.
Served by the plugin (require a logged-in session):
/plugins/windshift/sources— available sources for the dashboard dropdown/plugins/windshift/history?source=<id>— 24 h metrics history per source/plugins/windshift/latest?source=<id>— latest metrics snapshot per source, includingpeaksandtroughsarrays (ms timestamps of detected shift extremes) used by the shift overlay
Latest (gradient detection, currently on the Pi):
cd ~/.signalk
npm install "https://github.com/theseal666/signalk-windshift.git#feature/gradient-detection"
sudo systemctl restart signalkMulti-station only (no gradient detection):
npm install "https://github.com/theseal666/signalk-windshift.git#feature/multi-station"Stable single-source (original, no multi-station):
npm install "https://github.com/theseal666/signalk-windshift.git"Once the plugin is installed and started:
http://<your-signalk-ip>/@jwallinder/windshift
Experimental and under active development — running a live 24/7 soak test against Swedish west coast weather stations (Vinga + 5 ViVa stations). See PLAN.md for the full development history and roadmap.