← Ordo Artificum

skyclock

WWV time code decoder

A command-line tool for synchronising your system clock from the NIST WWV shortwave time signal — no GPS, no NTP, no internet. Receives audio from any radio or sound card, detects the 1 kHz second-boundary ticks, and sets the clock to sub-second accuracy once it identifies the top of the minute.

Beta github.com/jfrancis42/skyclock

What it is

skyclock decodes the NIST WWV time signal broadcast from Fort Collins, Colorado on 2.5, 5, 10, 15, and 20 MHz. It reads audio from a connected radio — through a PC sound card or directly from an audio file — and uses the signal to set the system clock. No GPS, no NTP, no internet required.

The primary use case is off-grid or emergency time synchronisation: when you have a shortwave receiver but no reliable internet connection, skyclock lets you set the system clock from the atomic clock in Colorado.

Optional hamlib integration lets skyclock control a connected transceiver — tune to the correct frequency, select AM mode — before starting the decode loop.

Current status

Tick-sync mode — working

The default mode detects the 1 kHz second-boundary tick that WWV transmits at the start of every second. Once you provide the approximate current UTC time (--time HH:MM:SS), skyclock counts ticks to the next minute boundary and sets the clock to that exact second — no BCD decode required, no consecutive clean minutes needed. Works reliably even on noisy HF paths where the 100 Hz subcarrier is unusable. Exits automatically after setting the clock.

Full BCD decode — temporarily broken

The --full-decode flag enables the full time-code decoder, which reads BCD minute/hour/date directly from the 100 Hz subcarrier without needing a user-supplied time. This mode is currently undergoing rework and is not functional in v0.3.0-BETA. Tick-sync mode is unaffected.

Downloads — v0.3.0-BETA

Linux (x86-64) AppImage — self-contained, no install needed
skyclock-0.3.0-BETA-x86_64.AppImage
# make executable and run (tick-sync, default)
chmod +x skyclock-0.3.0-BETA-x86_64.AppImage
./skyclock-0.3.0-BETA-x86_64.AppImage --time 14:23:47
# full BCD decode mode (experimental)
./skyclock-0.3.0-BETA-x86_64.AppImage --full-decode

Requires FUSE2 on the host. If AppImages don't run on your distro: sudo apt install libfuse2 or extract with ./skyclock*.AppImage --appimage-extract.

Windows (64-bit) .zip with .exe and required DLLs
skyclock-0.3.0-BETA-windows-x64.zip
# extract the zip, then run from a command prompt
skyclock-0.3.0-BETA-windows-x64\skyclock.exe --time 14:23:47
# all required DLLs are included — no installer needed

Requires Windows 10 or later (64-bit). No install — extract the zip to any folder and run. Contains skyclock.exe and all required runtime DLLs (PortAudio, MinGW).

Raspberry Pi / Linux aarch64 .tar.gz — bare binary
skyclock-0.3.0-BETA-rpi-aarch64.tar.gz
# install portaudio dependency, then extract and run
sudo apt install libportaudio2
tar xzf skyclock-0.3.0-BETA-rpi-aarch64.tar.gz
./skyclock --time 14:23:47

Built for 64-bit Raspberry Pi OS (aarch64). Requires libportaudio2 on the host. Tested on Pi 4 and Pi 5.

Using tick-sync mode

Tick-sync is the default. Tune your radio to a WWV frequency (10 000 kHz is usually best in North America), set it to AM mode, and connect the audio output to your PC's microphone or line input. Then run:

# provide current UTC time (check time.is or any synced clock)
./skyclock --time 14:23:47
# seconds optional — if omitted, system clock seconds are used
./skyclock --time 14:23

skyclock will display a live signal bar and a countdown to the next minute boundary. When it detects the top of the minute, it sets the system clock to that second. The display shows the exact UTC timestamp even if you're running without root access — useful for verifying the signal before committing to a clock set.

# display while counting down
skyclock 0.3.0-BETA [████████████░░░░░░░░] SNR: 14.2 dB 10000 kHz
2026-04-06 14:23:51 UTC [ticks: 12]
Next minute: 14:24:00 UTC (9 s) [waiting...]
# after the minute boundary fires, clock is set and skyclock exits
2026-04-06 14:24:03 UTC [ticks: 15]
LOCKED — clock set to 14:24:00 UTC (tick-sync)

Without root: the display shows Would set: HH:MM:SS.mmm UTC (run as root, or enable setSystemClock) so you can see exactly what would have been applied.

The WWV time code

WWV transmits a one-minute frame of 60 bits, one bit per second. Each second begins with a 5 ms 1 kHz reference tick — the on-time point — followed immediately by a 100 Hz subcarrier burst whose duration encodes one BCD bit:

Duration Meaning
200 ms BCD zero
500 ms BCD one
800 ms Position marker — at seconds 0, 9, 19, 29, 39, and 49 of each frame

The 60 BCD bits encode the current UTC minute, hour, day of year, year (last two digits), UT1 correction (±0.9 s), DST status, and a leap-second warning flag. Six position markers at fixed offsets within the frame provide the synchronisation points the decoder uses to find frame alignment.

The 1 kHz tick has substantially higher SNR than the 100 Hz subcarrier because it's a pure tone well above the audio noise floor and well outside the roll-off range of typical radio audio filters. Tick-sync mode exploits this asymmetry: detect the easy signal, ignore the hard one.

How it works

The decoder uses Goertzel filters — single-frequency DFT evaluations — to measure signal power at exactly 1 kHz and 100 Hz every 10 ms. No FFT, no windowing, no spectrogram. Several signal-processing techniques adapted from D.L. Mills' NTP refclock_wwv driver are layered to handle real-world HF propagation.

Tick detection with comb filter and median

A rising edge in the 1 kHz Goertzel filter exceeding the tracked noise floor is classified as an on-time tick. A 100-slot circular comb filter accumulates the 1 kHz EMA power at each 10 ms epoch within the second; the argmax of this buffer gives a stable, noise-averaged estimate of the tick's phase. A 3-sample running median of successive comb peaks smooths occasional outliers. An anti-spoof gate rejects false triggers caused by 100 Hz MARKER harmonics: genuine ticks arrive before the subcarrier starts, so the 100 Hz power is near floor at tick time; harmonics produce simultaneous elevated power at both frequencies. A 950 ms lockout prevents multipath echoes from re-triggering.

Free-running prediction with anchor correction

After the first real tick, subsequent second boundaries are predicted by advancing exactly 1 s per step. WWV's cesium standard is accurate to microseconds; audio clock drift is under 1 ms per minute at 50 ppm. When a prediction fires late (to allow a real tick a chance to appear first), the internal anchor is back-corrected by the same amount so sub-window boundaries remain aligned with the true second boundary rather than accumulating drift each miss.

Coherent energy integration

Rather than summing per-block 100 Hz power, the decoder accumulates raw audio samples into three fixed sub-windows after each tick (early 30–230 ms, mid 230–530 ms, late 530–830 ms) and runs a single coherent Goertzel over each full window. Coherent integration over 200–300 ms narrows the effective DFT bin from ±50 Hz (10 ms block) to ±2.5 Hz, reducing broadband noise contamination by ~20×. A separate 60 ms noise window (920–980 ms) tracks the 100 Hz noise floor as coherent energy E ≈ σ², which is used to subtract noise bias before computing per-window fractions. Under HF flutter fading, short incoherent signal fragments sum correctly into the right window without any single fragment needing to cross a threshold.

Q-channel phase EMA

A complex Goertzel over the early window yields both I and Q components at 100 Hz. The Q component tracks the subcarrier's instantaneous phase, which drifts slowly with Doppler shift on sky-wave paths. A slow EMA of Q accumulates this phase estimate across frames for potential future coherent combining.

Per-digit BCD accumulator

Each of the nine BCD digit fields (minute tens/units, hour tens/units, day hundreds/tens/units, year tens/units) maintains a 10-element EMA of per-value correlation scores. Each decoded frame boosts the winning digit's score and decays all others. A field "converges" once the same value leads for three consecutive frames. The number of converged fields (0–9) serves as a live lock-quality indicator.

SSB / I-Q demodulation

An optional frequency-shift demodulator mixes the input with a complex oscillator and two-stage IIR low-pass filters I and Q before feeding the in-phase channel to the Goertzel detectors. This lets skyclock receive WWV on a USB-mode receiver tuned slightly off-frequency — the oscillator shifts the 100 Hz subcarrier and 1 kHz tick back to their nominal baseband positions. Only the I channel is used (not the envelope); the Goertzel requires a real sinusoidal input, and the AM envelope of a baseband tone is constant DC.

Building from source

skyclock is a self-contained C++17 application. On Linux (Debian/Ubuntu):

sudo apt install cmake build-essential libportaudio19-dev
sudo apt install libhamlib-dev # optional — enables rig control
sudo apt install ffmpeg # required only for --file mode
git clone https://github.com/jfrancis42/skyclock
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)
./build/skyclock --help

Cross-compile toolchain files for aarch64 Linux and Windows (MinGW-w64) are included in the repository.

Known limitations

Full BCD decode requires good audio

The --full-decode mode reads the 100 Hz subcarrier, which most radios heavily attenuate (AM audio filters typically roll off sharply below 100–200 Hz). Without a flat audio path down to 100 Hz, full decode is unreliable. Tick-sync mode sidesteps this entirely since the 1 kHz tick is always well within the passband.

Tick-sync requires a known approximate time

To identify which tick is the minute boundary, skyclock needs to know the approximate UTC time to within ±29 seconds. Provide this with --time HH:MM:SS. Without --time, it falls back to the system clock, which works if the system time is within ±29 s of UTC.

WWVH / overlapping transmissions

WWVH (Hawaii) transmits on the same frequencies. Both are receivable across much of North America and they overlap on some frequencies. Tuning to 5 MHz (WWV only) typically gives the cleanest single-station signal.

Signal processing techniques adapted from:
D.L. Mills, “WWV/H Reference Clock Driver”, refclock_wwv.c, NTP Reference Implementation, University of Delaware, Technical Report EE-97-8-1, 1997. https://www.ntp.org/