signalk-bignumbers
v0.0.19
Published
The simplest SignalK racing display: one to four huge numbers, nothing to install per screen, digits that never jump, zero lag.
Maintainers
Readme
signalk-bignumbers
Big, high-contrast number displays for SignalK — mast and repeater screens for racing, showing one to four values large enough to read from the rail.

Why this exists
A racing repeater has to do three things:
- Perform. Show what the instrument is reading now, at whatever rate the sensor produces it.
- Be readable. At a glance, from metres away, at an angle, in spray, in full sun or full dark.
- Be trivial to deploy. Minutes from a bare screen to a live number, with nothing installed or configured on the display.
How it meets them:
Fast. Someone is trimming to the number, so late data is worse than
none. A delta arrives and paints: no queue, no batch, no
requestAnimationFrame, no animation, no rate limit — a 10 Hz source
updates the screen ten times a second. Nothing is smoothed, averaged or
damped; that belongs upstream in SignalK. See
Latency and load.
Large, clear numbers. One value fills the screen; up to four share it, all at the same digit size. Fixed high-contrast themes, not a colour picker: dark digits on a bright ground for sun, and night reds that light only the red sub-pixel to spare night vision. A value with no update for 3 seconds drops to dashes, so a dead feed looks dead rather than holding a stale reading.
Digits that don't jump. A number whose digits shift sideways can't be
read from a moving boat, so the layout is fixed before any value arrives:
the digit template (xx.x) reserves a column per digit, figures are
tabular-nums, leading zeros are hidden rather than removed, and a value
that can go negative reserves its minus column. 9.9 to 10.0 and back
moves nothing but the digits. A reading too big for its template shows
^^.^ rather than clipping a digit and passing 140 off as 40.
No bloat. Static files: two pages and one shared table of presentations. No build step, bundler, framework, dependency, backend or plugin. A live display uses about 3% of a Pi Zero 2 W's CPU.
Nothing to install or configure on the display. Anything with a browser is a display the moment you open a URL on it: phone, old iPad, laptop, Pi with an HDMI panel. Deploying one is open the URL, read the name off the screen, add it in the webapp; configuring it is picking a path and a presentation from two dropdowns. The display holds no config and no credentials, so changing what it shows never means touching it.
Not an MFD: no charts, gauges, graphs, history, AIS, alarms, nothing to touch.
Quick start
Five minutes, using a phone as the display.
1. Install the webapp — the only thing that gets installed anywhere. In SignalK's admin UI: Appstore → Available, find Big Numbers, Install, then restart the server.
2. Open this on the phone. Substitute your own server address for
signalk.local:3000 throughout — the same host and port you use to reach
SignalK's admin UI.
http://signalk.local:3000/signalk-bignumbers/instrument.html?display=phoneThe screen fills with phone and "Not configured yet" — its name, and
nothing stored under it yet. phone is just a label you picked in the
URL; use the same word in the next step.
3. Tell it what to show. From any browser, open
http://signalk.local:3000/signalk-bignumbers/ (also in SignalK's
Webapps menu). Log in at the top, + Add display, type phone, pick a path
and a presentation, name it, Save.
4. Watch the phone. Within about 5 seconds it switches from its name to the number. No reload, no restart.
That phone is done. Changing what it shows is Edit; going from one value to four is + Add another number. Nothing on the phone changes either time.
Proper install
The quick start is the whole software install. What's left is making a device stay on the air unattended.
The two differ in one thing: where the name comes from. On a phone you type it into the URL; on a Pi, systemd fills in the hostname, so one service file works on every Pi in the fleet.
Phone or tablet
For a repeater someone carries, or a screen up for one race. An old iPad in a waterproof case works on the mast.
- Open the display URL in Safari or Chrome, with a name you pick:
.../instrument.html?display=bow - Add to Home Screen — it launches fullscreen, no address bar.
- Turn off auto-lock. The page holds no wake-lock, so a screen timeout takes the display off the air. On iOS: Settings → Display & Brightness → Auto-Lock → Never.
- Keep it on power. A bright screen on live data drains a battery fast.
- Optional: iOS Guided Access (Settings → Accessibility → Guided Access) locks it to the page.
Name it for where it goes — bow, pit, helm — not for the device.
Swapping in a different phone is then one URL to open.
Raspberry Pi Zero 2 W (permanent mast display)
For a screen that lives on the boat: it comes up on power, has no battery or lock screen, and doesn't mind the rain. A Zero 2 W runs one with plenty of headroom.
Full walkthrough: docs/raspberry-pi-kiosk.md. In outline:
- Raspberry Pi OS (Trixie) console-only, no desktop.
sudo apt install -y cog— a minimal WPE WebKit shell that renders straight to the framebuffer via DRM/KMS. No X11, no Wayland, no compositor between the delta and the glass.- Test by hand:
cog --platform=drm "http://signalk.local:3000/signalk-bignumbers/instrument.html?display=$(hostname)" - Make it permanent with a systemd unit using
%Hin place of the hostname, so the file is identical on every Pi. - Free tty1 (
systemctl disable getty@tty1) and addconsoleblank=0to/boot/firmware/cmdline.txt.
Because the name is the hostname, deploying one is: name the Pi, plug it in, read the name off its screen, add it in the webapp.
Anything else
| Display | How it opens the page | |---|---| | Raspberry Pi Zero 2 W + HDMI panel | boots into it fullscreen, hostname as its name (guide) | | iPhone / iPad | Safari, Add to Home Screen | | Android phone or tablet | Chrome, Add to Home screen | | Laptop | any browser, fullscreen |
Mixed fleets are managed from one list, with no distinction between a mast Pi and a borrowed phone.
Reference
Installing and updating
It's a SignalK webapp — static files, no plugin and no backend. Normally you install and update it from the admin UI's Appstore, which restarts into the new version.
To run the development version instead, install from GitHub:
cd ~/.signalk
npm install github:ghotihook/signalk-bignumbers
sudo systemctl restart signalk # or however your server is rungithub:owner/repo clones the repo rather than looking the name up in
the npm registry, so this gets the current main. Add #<tag> or
#<branch> to pin one:
npm install github:ghotihook/signalk-bignumbers#0.0.19cd ~/.signalk matters: signalk-server scans that directory's
node_modules for webapps at startup, so anywhere else leaves it
invisible. Either way, Big Numbers appears in the Webapps menu
after the restart, served at
http://<your-server>:3000/signalk-bignumbers/.
The webapp
One row per display, showing what it's set to. Edit changes it and the display picks it up within about 5 seconds; Delete sends it back to showing its own name, ready to be reassigned. A display's name is fixed once created, and is letters, digits, dots and hyphens only — on a Pi it has to match the hostname character for character, and either way it goes into a URL.
Per value:
| Field | Meaning |
|---|---|
| Path | What to show, picked from the paths this server is currently reporting. A compound value appears once per key (navigation.attitude → roll). A path the server has stopped reporting stays selected on a saved display, marked not currently reporting. |
| Presentation | How to show it: the conversion from SignalK's SI units, the unit label, the digit layout and the sign handling, in one pick. Grouped by kind — Angle, Speed, Depth, Distance, Temperature, Time, Electrical, Other, Number. Each is labelled with its digit template (Knots — xx.x), with ± where it reserves a sign column. |
| Display name | The label above the number. |
| Colours | Fixed high-contrast pairs, grouped Day, Dusk and Night. See Colours. |
Path, Presentation and Display name are all required: Save and Preview outline anything left blank and say what it needs.
The presentations are listed in public/formats.js,
which the editor and the display both read, so the two can't disagree
about what one means. A few of them:
| Presentation | Shows |
|---|---|
| Wind angle, ±180° — ±xxx | -143°, folded back from 0…360 if the source sends it that way |
| Heading, course, direction, 0–360° — xxx | 183°, in a three-digit column so it can't shift |
| Knots — xx.x | 8.4 kt from SignalK's m/s |
| Celsius — ±xx.x | 18.3 °C from SignalK's Kelvin |
| Duration — hh:mm:ss | 01:02:05 from a count of seconds, e.g. time to go |
| Start timer — ±mm:ss | -00:10 once the gun has gone, where Duration would read 00:10 |
| Clock, local time — hh:mm:ss | 14:05:09 from a timestamp |
To show something no presentation covers, write the conversion keys into the URL directly — see Direct URLs. Written into a stored config instead, they show in the editor as Custom, and saving keeps them until you pick a listed presentation.
+ Add another number puts up to four values on the same screen.
They split it into equal horizontal bands, top to bottom in the order
listed, each with its own colours, all at the same digit size. Four on a
landscape screen sit two by two instead, left to right then down: stacked,
each band would be a wide strip with small digits. One wide value, such
as an hh:mm:ss timer, shrinks all four, since every band shares its
size. Bands sharing a background are divided by a hairline; otherwise the
colour change is the divide. At night keep every band on black — one
bright band costs the dark adaptation the night themes protect.
Preview opens the config in a new tab before saving. It needs the values filled in first, though not the display's name — an incomplete one previews as a screen you didn't configure: a value with no path drops out of the URL, and one with no presentation shows the raw SI number in a single digit column.
Colours
Fixed pairs, chosen for reading distance rather than taste. Day themes put black digits on a bright background: a brighter screen reads better, and reflections of sky and sails vanish against it where they'd show on black.
Dusk and night put light digits on black. Red and dim red light only the red sub-pixel, which the eye's night-vision rods barely register; amber lights green too, so it suits dusk rather than full dark. Dim red is for a screen whose backlight can't be turned down — the page can't dim it.
With more than one value, a different background per band lets a glance find the right number. The day colours stay distinct to red-green colour-blind crew — about one man in twelve — except lime beside yellow and lavender beside sky blue, so don't put either pair next to each other.
A display saved with a colour earlier versions offered moves to its replacement the next time it's edited and saved; until then it keeps its colours.
Configuration and permissions
Configs live in signalk-server's built-in applicationData store, keyed
by the display's name. Displays read it anonymously; saving requires a
SignalK login, which the webapp prompts for and keeps in the browser's
localStorage.
A display holds no credentials, so if signalk-server's security is on it needs Security → Settings → Allow Readonly Access enabled, or nothing reaches the screen at all. The webapp checks this on load and warns if it's off, and a display that can't read its config says so rather than falling through to the "not configured" screen.
The editor's path dropdown is the boat's own data: it reads
/signalk/v1/api/vessels/self and lists what the server is reporting
right now. With the instruments off there is nothing to pick from, so
set a display up with its sources live. A path already saved on a display
stays in its dropdown either way, marked not currently reporting.
An unconfigured display polls every 5s waiting for a config to appear; a running one re-checks its own every 5s and reloads if it changed. Restarting a display to change what it shows is never necessary.
Reading the screen
- A value with no update for 3 seconds fades to dashes. It never holds a stale reading.
- A reading too big for its presentation shows
^in every digit (^^.^) rather than widening past the screen edge — 140 m on axx.xdepth, say. Pick a presentation with more digits. - The dot top-right is green while the connection to SignalK is live, dark red when it isn't. A dropped connection retries every second and recovers on its own, showing the next live value rather than replaying what was missed.
Latency and load
Late data is worse than no data. A number two seconds old looks current, and nothing on screen says otherwise.
So as little as possible sits between the delta and the glass:
- Subscriptions use
policy: "instant", noperiod, nominPeriod. Values arrive as they change, not on a timer. - Each delta renders synchronously on arrival. No queue, no
requestAnimationFrame, nosetTimeout, no batching. - Nothing animates — a tweened number shows readings the boat never took.
Value changes carry
transition: none. - No smoothing, averaging or damping. Damping belongs upstream in SignalK, where every display gets it.
- No history, replay or backfill. A reconnected display shows the next live value, not what it missed.
There's no rate limit either, so a 10 Hz source updates the screen ten times a second. That stays cheap:
- A display receives only the paths it shows: the socket opens
?subscribe=noneand subscribes by path, so the rest of the bus is never sent. - Between deltas nothing paints. CPU follows the data rate, not the clock.
- An update writes the sign and digits, nothing else. Digit widths are
reserved, so a new value doesn't reflow or resize the text;
fitDisplay()runs at startup and on resize, not per value. - Past the refresh rate the browser coalesces writes into one paint, so a source faster than the panel costs parsing, not drawing.
If a Pi still can't keep up, cut values or slow the source — never buffer.
Direct URLs
A display can be configured entirely from its URL, with no stored config and no login — useful for testing, or a one-off screen:
instrument.html?path=environment.wind.speedApparent&name=AWS&format=speed-kn| Parameter | Meaning |
|---|---|
| path | SignalK path to subscribe to (required) |
| name | Label shown top-left of the band (required) |
| format | A presentation from public/formats.js — speed-kn, angle-180, temp-c, dur-hms and so on. Stands in for layout, neg, wrap, unit, factor and offset |
| field | Key to read from a compound value, e.g. roll |
| layout | Digit template, e.g. xxx or xx.xx — sets the autosize and decimal places |
| neg | true to reserve room for a minus sign |
| wrap | true to fold the converted value into −180…180 |
| unit | Shown in brackets after the name: STW (kt) |
| factor, offset | Unit conversion: shown = raw * factor + offset |
| bg, fg | Theme colours for this value's band; unsuffixed they also set the screen's. Hex only (#000000, #fff) — anything else is ignored |
| host | SignalK server, if not the one serving the page (whole screen) |
| cursor | 1 shows the mouse pointer, which is hidden otherwise (whole screen) |
| display | Stored-config identifier — used instead of all of the above |
For later values, suffix every per-value key with 2, 3 or 4.
The unsuffixed keys are the first value, so any single-value URL still
means what it did (wrapped here for readability — it's one line):
instrument.html?path=navigation.speedOverGround&name=SOG&format=speed-kn
&path2=environment.depth.belowTransducer&name2=DPT&format2=depth-m
&path3=navigation.attitude&field3=roll&name3=Heel&format3=angle-smallA missing path2 ends the list, so values can't have gaps. host,
cursor and display belong to the display as a whole and are never
suffixed.
format is shorthand, not a replacement: layout, neg, wrap, unit,
factor and offset still work on their own, and given alongside a
format they override it. So every URL written before presentations
existed still means exactly what it meant then, and a conversion the list
doesn't cover can still be spelled out key by key.
The one exception is the time presentations: durations and clocks are
reachable only through format, never through the raw keys.
bg and fg do double duty: suffixed (bg2, fg3) they colour just
that band; unsuffixed they set the first band's colours and the
screen's, which is what shows behind the "not configured" and error
screens — those exist before there are any bands.
The webapp's Preview button builds these URLs for you.
What's here
public/index.html— the webapp. Lists, adds, edits and deletes displays.public/instrument.html— the display itself. Fills the screen with up to four numbers, autosized to fit.public/formats.js— the presentations, read by both pages: each one's conversion, unit, digit layout and sign handling.docs/raspberry-pi-kiosk.md— building a display from a Pi.dev/dummy_signalk.py— a fake SignalK server that sweeps values through a range, for testing without a boat. Speaks only the delta protocol, so it drives?path=URLs, not?display=ones.
