signalk-dead-mans-switch
A Signal K server plugin implementing a watch dead
man's switch: a periodic "are you still there?" check-in that escalates
through the standard SignalK notification states - alert -> warn ->
alarm -> emergency - until acknowledged.
Useful for solo or short-handed watchkeeping: if the person on watch stops responding, the alarm state keeps climbing instead of silently staying at a single low-priority notification.
How it works
- Once armed, the plugin waits
checkIntervalMinutesand then raises a notification at statealert: "are you still there? Acknowledge to reset the timer." - If nobody acknowledges within
ackWindowSeconds, it escalates towarn. - If still unacknowledged after
warnWindowSeconds, it escalates toalarm. - If still unacknowledged after
alarmWindowSeconds, it escalates toemergency- "watch incapacitated!" This is the top of SignalK's notification scale and is terminal: the plugin does not escalate any further on its own. (Sounding a horn, sending a distress alert, texting someone ashore, etc. onceemergencyis reached is left to other plugins/automations subscribed to this notification path - keeping those concerns out of this plugin.) - An acknowledgement at any stage - including while merely armed and
waiting - resets to a resting notification (
state: normal,message: "armed") and restarts the check-in interval from zero.
The escalation stage names are exactly the SignalK notification state
values (alert/warn/alarm/emergency), so any standard SignalK alarm
display can pick this notification up with no special-casing. The switch
always keeps a notification present at notificationPath rather than
clearing it when armed or disarmed - message reads "armed" or
"disarmed" so anything watching the path can always tell which resting
state it's actually in, rather than an absent value being ambiguous
between "armed and watching," "disarmed," and "this plugin has never
run."
Acknowledging
Two ways to ack:
- The companion webapp (installed automatically, available from the
Signal K server's webapps list) - one big button showing the current
stage, the remaining time in that stage's window, and doubling as the
ack/arm action (tap it to acknowledge, or to arm when disarmed). A
horizontal progress bar underneath fills up as the deadline approaches.
Disarm and the light/dark theme toggle sit together in the top corner.
The theme follows the same palette convention as
signalk-stowage-mgmt(dark mode is deliberately red-shifted to preserve night vision on watch). - A REST call to
POST /plugins/signalk-dead-mans-switch/ack- handy for wiring up a physical hardware ack button (e.g. an ESP32 with a big panic-style pushbutton at the helm) instead of relying on a screen.
If the webapp loses its connection to the SignalK server (network drop, server restart, etc.), it does not go blank or keep showing a frozen countdown as if nothing were wrong. It shows a clear "connection lost" banner and visibly dims the state button and progress bar, while still displaying the last known state (and when it was last confirmed) - so it's obvious the display can no longer be trusted, without losing the last real information either. Polling continues in the background and the banner clears automatically once the connection recovers.
Siren
The moment the switch reaches emergency, the webapp plays a bundled
siren sound (public/audio/emergency-siren.wav), looped, at full volume
(the audio element's own gain - it can't override the device's system/
OS volume). It stops the instant emergency is acknowledged.
Browsers block audio from starting with sound until the page has seen a real user gesture. The webapp "unlocks" the audio element on the very first tap/click anywhere on the page (arming, acking, toggling the theme - anything counts), so the siren can then start on its own later with no further interaction needed, as long as something was tapped at some point after the page loaded.
The bundled siren is "High Frequency Siren Model D" by loganzsound
(Freesound.org), CC0 - see public/audio/NOTICE.md.
While the switch is in alarm (one stage before emergency), the webapp
also plays a second bundled sound (public/audio/alarm-intercom.wav,
"Space-Intercom-Emergency" by electrobadger on Freesound.org, CC0) once
immediately and then again every 10 seconds, stopping the moment the
stage changes away from alarm in either direction (escalating to
emergency, which has its own continuous siren instead, or being
acknowledged).
Both sounds are controlled by the plugin's Play sounds in browser config option (on by default). Turn it off if this notification is already wired into a dedicated alarm system and the browser's own audio would just be redundant.
REST API
All endpoints are mounted at /plugins/signalk-dead-mans-switch.
| Method | Path | Description |
|---|---|---|
| GET | /status |
Current stage, seconds remaining, and active config |
| POST | /ack |
Acknowledge - resets to a resting "armed" notification, restarts the timer |
| POST | /arm |
(Re-)arm the switch |
| POST | /disarm |
Disarm - stops all timers, sets a resting "disarmed" notification |
POST /ack always restarts the check-in interval, even if the switch
is already just armed (nothing escalated) - handy as a general
"I'm here" refresh via the webapp or a hardware button. It's a no-op
only while disarmed (ok: false in the response); use /arm to
start watching again. An external acknowledgement signal seen on the
notification path (see below) is more conservative: it's ignored
while already armed, since some servers keep an "acknowledged" flag
sticky on the notification even after the switch resets - reacting to
it every time would otherwise keep restarting the timer forever.
Fully documented as an OpenAPI 3.0 definition in openApi.json
(exposed via plugin.getOpenApi(), per SignalK's plugin API
convention) - browsable in the SignalK server's Admin UI under
Documentation -> OpenAPI once the plugin is installed.
Interoperating with other plugins/devices
The switch's state isn't only driven by its own REST API - it also watches the notification path itself and reconciles immediately if something else writes to it directly, e.g. another plugin, a webapp doing its own PUT, a device publishing straight to SignalK, or a client using SignalK's v2 Notifications API. Watched two ways:
- an
app.streambundledelta subscription - instant when it works - a periodic
app.getSelfPath()poll (every 2s) as a fallback - real servers can filter deltas from a non-preferred source out of the delta chain before subscribers ever see them, and the v2 Notifications API's acknowledge/silence/clear actions may update a notification'sstatuswithout re-emitting a normal delta at all. Polling reads the actual current value directly, sidestepping whichever of those gaps would otherwise mean a real acknowledgement or external change is silently missed.
Either path reacts the same way to what it sees:
- writing one of the escalation states (
alert/warn/alarm/emergency) snaps the switch to that stage, with a freshly-started window for it - exactly as if the switch had escalated there itself - an acknowledgement via SignalK's own v2 Notifications API (
POST /signalk/v2/api/notifications/{id}/acknowledge, which is what clients like Freeboard's "Acknowledge" button actually call) resets the switch the same as calling/ack, but only while something is actually escalated - a no-op while already justarmed, since some servers keep the "acknowledged" flag sticky on the notification even after the switch resets, which would otherwise restart the timer forever. That action doesn't clear the notification or changestate. When a server exposes it,status.acknowledged: trueis trusted directly - the most authoritative signal available. As a fallback for servers wherestatusisn't populated, the spec also has the action strip"sound"frommethod(only"sound"whenstateisemergency), so that's checked too - clearing the notification, or writing any other state (e.g.
normal), while the switch is armed or escalated is treated as an external acknowledgement - same effect as calling/ack - while disarmed, external changes on the path are ignored entirely - a disarmed switch isn't managing it
Published notifications also set status.canSilence: false (inside a
status object mirroring SignalK's v2 Notifications API shape) -
silencing alone (muting sound without truly checking in) must never be
mistaken for an acknowledgement. There's no valid top-level
canSilence field in the SignalK notification spec; only
status.canSilence is meaningful.
Deltas the plugin publishes itself are recognized (by source) and never reprocessed, so this can't create a feedback loop. The REST API and companion webapp both reflect the reconciled state immediately - the webapp picks it up on its next poll (at most ~1s later), no webapp-side changes needed.
Configuration
Set via the plugin's config page in the Signal K admin UI:
| Option | Default | Description |
|---|---|---|
| Armed on plugin start | true |
Whether the switch starts armed when the plugin loads |
| Check-in interval | 15 minutes |
Wait time, once armed/acknowledged, before the first prompt |
| Alert window | 90 seconds |
Time to ack before alert escalates to warn |
| Warn window | 60 seconds |
Time to ack before warn escalates to alarm |
| Alarm window | 60 seconds |
Time to ack before alarm escalates to emergency |
| Notification sub-path | security.deadmansswitch |
Appended after notifications. |
| Play sounds in browser | true |
Whether the companion webapp plays the emergency siren and repeating alarm sound |
| Automatically switch light/dark theme based on sun position | false |
See "Automatic theme" below |
Automatic theme
When enabled, the webapp follows vessels.self.environment.sun
(preferred) or vessels.self.environment.mode (fallback) instead of a
manual toggle - the toggle button is hidden entirely while this is on.
environment.sun(set by signalk-derived-data to one ofdawn/sunrise/day/sunset/dusk/night) is checked first:daymeans light, anything else means dark. The point is protecting night vision, which matters from dusk through dawn, not just once it's fully dark.environment.mode(a simplerday/nightstring some setups use instead) is the fallback ifenvironment.sunisn't available.- If neither path has a usable value, the webapp falls back to its normal default (OS preference / last manually-picked theme) and the toggle stays hidden until a recommendation becomes available.
Needs a plugin like signalk-derived-data actually publishing one of
those paths - this plugin only reads them, it doesn't calculate sun
position itself.
Debug logging
Logs every state transition (with its reason), every notification
published, and every input received (REST calls and external changes
on the notification path) via SignalK's standard per-plugin debug
facility (app.debug()) - the same mechanism every other well-behaved
plugin uses. Enable it the standard way: the plugin's entry in the
server admin UI's debug log settings, or the DEBUG environment
variable including signalk-dead-mans-switch. There's no separate
toggle in this plugin's own config screen.
App icon
public/assets/icons/icon-512.png is used as the browser favicon and
as the app's icon in the SignalK admin UI's webapp list
(package.json's signalk.appIcon). It's not shown anywhere inside
the webapp's own UI.
Development
npm install
npm test
Frontend dependencies (Preact + htm) are vendored under public/vendor/
rather than loaded from a CDN, since the browser accessing this webapp is
very often talking only to the local Signal K server with no wider
internet access. See public/vendor/README.md for details.