Skip to content

About

Automatic HF wefax decoder requiring SDR Console and any SDR receiver, with automatic tuning using schedules, image enhancements, frequency learning.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

WEFAX Decoder

An HF weather-fax (WEFAX, ITU-R F.343 / WMO) receiver and decoder for Northwood (GYA), DWD and other international stations.

It listens to a sound-card input, finds each transmission automatically and decodes it, correcting tuning offset, slant and tears. It then cleans and sharpens the chart, names it from the broadcast schedules, saves it to disk and shows it in a browser page. With SDR Console's CAT port it can also retune the radio through the day to collect the most important charts.

The decoder's page: a DWD chart being received, the live spectrum and waterfall top right, the auto-scheduler's plan and the last decoded chart

Features

  • Automatic detection of start tone, phasing and stop tone (IOC 576 and 288; 60, 90, 120 and 240 LPM).
  • AFC with drift tracking, a correlation FM discriminator that is robust to fades and QRN, and slant correction from the phasing lines.
  • A flywheel that uses the PC clock to detect and fill audio lost by the sound card, plus image-based repair of torn charts.
  • Enhancement that removes noise streaks and rebuilds broken chart lines without distorting them, with an FSRCNN x2 upscale. Clean charts get only light processing.
  • Chart identification from the OpenCPN worldwide schedule file, giving file names such as 20261011_1800Z_DWD_surface-pressure-analysis.png.
  • SDR Console control over a Kenwood TS-2000 CAT port: frequency read-back, tuning, and an auto-scheduler. You choose the 1st and 2nd priority stations, DX stations fill the gaps, and it learns which frequencies work at each time of day.
  • A browser page with the following:
    • a live view of the chart as it arrives (green while receiving, orange "NO SIGNAL" when a chart is due but not heard, red when idle);
    • a thumbnail strip for each day;
    • Raw and Enhanced views, mouse-wheel zoom, and rotation.

Example charts

Received in Plymouth, UK, decoded and enhanced by the app (decode quality shown in each title block):

Northwood (GYA)
Northwood surface analysis, 12Z run, received 15 Oct 2026 at 1436Z on 8040 kHz
Surface analysis, 15 Oct 2026, 92 %
Northwood surface prognosis T+24, 12Z run, received 15 Oct 2026 at 1712Z on 8040 kHz
Surface prognosis T+24, 15 Oct 2026, 96 %
DWD Hamburg/Pinneberg
DWD surface weather chart with station plots, received 14 Oct 2026 at 1050Z
Surface weather chart, 14 Oct 2026, 94 %
DWD H+60 GME surface pressure forecast, received 15 Oct 2026 at 0743Z
H+60 (GME) surface pressure, 15 Oct 2026, 95 %

Install

Python 3.9 or later.

pip install -r requirements.txt

onnxruntime (for the neural upscaler), pyserial (for CAT) and pillow (for the title block font) are optional. Without them the decoder uses a Lanczos upscale, cannot do CAT, and draws the title block in a plainer font.

Quick start

  1. Tune the receiver in USB with the dial 1.9 kHz below the published frequency. For Northwood (GYA) that means:

    Published Dial (USB)
    2618.5 kHz 2616.6 kHz
    4610 kHz 4608.1 kHz
    8040 kHz 8038.1 kHz
    11086.5 kHz 11084.6 kHz
  2. Start the decoder with python wefax_decoder.py. The page opens at http://127.0.0.1:8730/.

  3. Choose the sound-card input carrying the receiver audio in the Audio in box at the top of the page. This could be a virtual cable, a USB sound card or a line input. The border turns green when it is running, and the IN meter shows the level. The choice is saved.

Each fax appears when its transmission ends.

On Windows, start_wefax.bat starts the decoder with CAT on COM4 and the auto-scheduler on. Edit it to suit your set-up.

Defaults you may want to change (at the end of wefax_decoder.py, or on the command line):

  • --device is the audio input used until you choose one on the page. The default is Line 8 (Virtual Audio Cable) MME; if that isn't present, the system default input is used. --list-devices lists the inputs.
  • QTH (in the bar at the top of the page) is your location for the propagation model: lat, lon in decimal degrees (south and west negative) or a Maidenhead locator such as IO70ri. Press Enter to apply; the plan is rebuilt straight away and the setting is saved. Until it is set the box has a red border and Plymouth, UK is used. --qth at start-up does the same; a location set on the page takes precedence. Frequency choice and DX probing depend on it. Any station in the schedule file can be chosen as 1st or 2nd priority on the page.

To decode a recording instead of live audio, run python wefax_decoder.py --wav recording.wav.

Leave it running

The app is designed to run continuously, 24 hours a day. After the first run, leave it on AUTO for several weeks.

The app has knowledge of all fax frequencies and schedules from around the world. Set your two nearest priority stations in the top right of the browser window (in the UK I set Northwood and DWD). During schedule clashes these stations will take priority. Analysis and gale warning broadcasts will also take priority over prognosis forecasts.

Over many days and weeks the app will probe all the frequency options for each station and learn the best frequencies depending upon the time and season. As the year progresses it will continue to learn, as some frequencies get stronger or weaker as the seasons change.

Try not to interrupt reception or retune the radio manually, as this will upset the learning process.

The console window logs what the app is doing: each retune by the auto-scheduler, each transmission found and how it ended, the decode steps (AFC, tones, phasing, slant) and the result logged for the learning:

The console log while running on AUTO

Saved files

Everything is written under ./faxes/ (change this with --out):

Path Contents
raw/<name>.png Decoded chart: geometry corrected, no cleaning
enhanced/<name>.png Cleaned chart at 2x resolution, with a title block top left: station, frequency, date, chart, model run and decode quality
thumbs/<name>.jpg Thumbnail for the page
stations/<station>/<chart>.png The latest verified copy of each chart, e.g. stations/Northwood/Surface-Analysis.png - this is the folder to publish from. Only charts that pass every check below get here; a newer issue replaces it, a repeat of the same issue only if it decoded better. The .json beside it gives its quality and time
<name>.json Decode details: station and chart, IOC, LPM, AFC, clock ppm, quality, tears, etc. Quality is the share of the picture received on a steady signal (steady) times an SNR factor: 1 when the fax stood 12 dB or more above the noise during the picture (image_snr_db), falling to 0.6 at 0 dB. A chart counts as collected at 60 %
proplog.jsonl The auto-scheduler's log of every attempt (SNR, quality). A strong signal with no fax decoded is held back from it: three such slots in a row mean the audio reaching the decoder is broken (a virtual audio cable fault, the wrong input), so the page shows AUDIO FAULT? and nothing is counted against the frequencies until a fax decodes again
chart_lengths.json How long each chart runs, learnt from charts that ended on their stop tone or end-of-frame black. A recording is closed 20 s after its chart should have ended (or 15 s before the next slot, for charts not yet learnt), so a faded chart never holds up the auto-scheduler. If a picture is still coming in at that moment it is kept until its stop tone, end-of-frame black or the next chart's start (up to 90 s more)
settings.json Your audio input, CAT port, 1st/2nd priority stations and how many days to keep

Audio is not kept unless you add --save-audio.

How long faxes are kept: choose 3, 7, 14 or 28 days in the Keep box at the top of the page (default 28; the choice is saved). Once a night, at the first quiet moment after 00:05 local time (nothing being received or decoded), older faxes are deleted from raw/, enhanced/, thumbs/ and audio/. The stations/ copies, the learning history and your settings are never deleted.

Correct first time: how a chart is checked before it is published

A chart is framed by three things: its tuning offset, its line period (slant) and where each line starts. Each is decided from independent estimates, and a chart is only published (copied to stations/) when they hold up:

  • Tuning offset and line period are anchored to what this installation has learnt, per station, from its own charts decoded with phasing (the transmitters' clocks differ - here DWD about +5 ppm, Northwood about -33 ppm). A misleading chart (fine hatching, interference, broken phasing) cannot push them far from normal. If the phasing of five charts in a row agrees with itself but not with the learnt clock, the clock has changed (new sound card, other cable) and the new value is learnt.
  • Line start comes from the phasing pulses, the frame of the last verified copy of the same chart, and the picture's own seam. Phasing that fits well is trusted unless the reference frame contradicts it; otherwise two estimates must agree (within 2 % of the line), or one must be very clear. The seam, which a faint line in a chart can fool, confirms but never overrules.
  • Tears are only corrected where the flywheel recorded lost audio.
  • Before publishing: framing confirmed, slant and tuning within 80 ppm / 80 Hz of the station's learnt values, no unexplained tear, quality 50 % or better, and fax structure present.

A chart that fails is still saved in the gallery, marked NOT PUBLISHED with the reason on the page, but the station copy is left as it was and the chart counts as not collected, so the scheduler goes after a repeat.

SDR Console CAT

The decoder reads the tuned frequency from SDR Console, and can set it, over a Kenwood TS-2000 CAT port:

  1. Create a virtual COM port pair with com0com, VSP Manager or Eltima, for example COM3 and COM4.
  2. In SDR Console, go to Home > Serial Port (CAT) and add one end, such as COM3, at 57600 baud.
  3. Choose the other end of the pair, for example COM4, in the CAT port box at the top of the page. The border turns green when SDR Console answers, and the choice is saved. You can also give the port at start-up with --cat COM4; a port chosen on the page takes precedence. Add --cat-rx 2 to follow receiver 2.

python tools/comtest.py checks the COM ports if CAT does not respond. Without CAT, tell the decoder which station you are on with --station DWD, or pick it in the station box on the page.

The schedules come from the OpenCPN weatherfax plugin's worldwide WeatherFaxSchedules.xml in wefax/data/, with the Northwood list corrected. To update them, put a newer copy of the file next to wefax_decoder.py.

Auto-scheduler

The auto-scheduler needs CAT. Press AUTO on the page, or start with --cat COM4 --auto.

  • Priority stations: choose them with 1st priority and 2nd priority in the top bar. The default is Northwood then DWD, and the choice is saved. Their surface analyses, short-range forecasts and gale warnings are planned first, then wave and other charts.
  • Clashes: the 1st priority station wins over the 2nd.
  • Repeats: a chart broadcast several times is only taken until one good copy has been decoded. If the first try at a priority station's chart was poor or not decoded, its next repeat goes ahead of every other chart, priority or DX.
  • Gaps: charts from the same station can follow straight on. Gaps long enough for a whole chart, plus 75 s before and 45 s after, are filled with DX stations.
  • Charts in progress: once a chart has started, nothing else is planned over it, and the scheduler never retunes while a chart is being received, even if it runs longer than the schedule says. The only exception is a recording that has run over 25 minutes, longer than any single chart, which means a stop tone was missed; it is ended so later charts aren't lost. If the scheduler does have to leave a chart early, that attempt isn't counted against the frequency.
  • Frequency choice: each slot gets the frequency that has worked best at that time of day. With no history yet, it uses a simple ionospheric estimate based on path length and sun height: at the path mid-point for paths up to 2000 km, and also 1000 km in from each end on longer paths (the darkest point limits the MUF, the sunniest sets the absorption).
  • Comparing frequencies on air: for a chart sent on several frequencies at once (Northwood, DWD and many others), the scheduler tunes each one in turn before the chart starts, while the previous chart or the station's steady carrier is on air, and reads its SNR for about 4 s. It receives the chart on the best one if that is at least 3 dB better than the planned frequency. The scan finishes at least 10 s before the start tone. If nothing is on air on any frequency it keeps the plan. The readings are logged ("kind": "scan") and feed the next frequency choice. From the start tone to the picture the receiver is never retuned, so the phasing lines (which set the line timing and framing) arrive undisturbed. If nothing at all is heard 20 s after a chart's start time, the planned frequency may be off air: the other frequencies are tried and the scheduler stays on the best one, where the picture check picks the chart up (without its first few lines).
  • Centring the receiver: every retune hops 200 kHz above and below the target for half a second each before settling on it, so SDR Console moves its spectrum span and the frequency lands near the middle rather than at an edge. HOP_KHZ in wefax/cat.py sets the hop (0 turns it off).
  • Learning: DX combinations of station, frequency and time of day that keep failing are retried after 1, 2 and 4 days, then every 7 days, so improving propagation is noticed. Old results fade out gradually, so seasonal changes are followed. DX probes the propagation estimate calls hopeless are skipped unless that frequency has worked at that time before. The SNR logged for every attempt is measured while the chart is on air. A chart missed because an earlier recording was still running isn't counted against the frequency.
  • Upgrading: logs from versions before 16 Oct 2026 contain false failures (charts logged as failed while a good signal was on air). The first start of a newer version removes them once, keeping the original as proplog_before_v2.jsonl; nothing else is changed.
  • Empty slots: a priority-station slot that is silent three times while the same frequency decodes the charts either side is taken as not broadcast and left out of the plan, then tried again every 14 days (published schedules go out of date: in October 2026 Northwood no longer sent its ANPS T+24 charts and its 0936 slot carried a WAFC SIGWX chart; the built-in Northwood list is corrected for these).
  • Silent DX probes: if nothing is received 4 minutes into a DX slot and the SNR has stayed below 0 dB, the probe is logged as failed and the time is given to the next planned chart or another probe.
  • Manual tuning: using Tune or a chart chip pauses the scheduler for 20 minutes.
  • No DX: use --no-dx to stop the gap filling.

The page

  • Spectrum (top right, beside the Tune button, on wide windows): the receiver audio from 800 to 3000 Hz. The line is the spectrum averaged over 3 s with a dotted peak hold; the waterfall below shows the last ~20 s. Orange lines mark black (1500 Hz) and white (2300 Hz) shifted by the measured tuning offset; grey bands are where the noise for the SNR is measured. It is left out when there is not enough room.
  • Live panel (top left):
    • green while a chart is being received;
    • orange NO SIGNAL, with the incoming noise, when a chart is due on the tuned frequency but nothing usable is heard;
    • red IDLE otherwise.
  • Thumbnail strip: one UTC day at a time, newest at the top. Use −1 and +1 to change day.
  • Viewer:
    • Raw and Enhanced show the same part of the chart at the same size.
    • The mouse wheel zooms and dragging pans.
    • Fit and 1:1 reset the zoom.
    • Rotate −90° / +90° turns the chart. Rotation is remembered for each fax in that browser.

Useful options

Option Effect
--no-ml Use Lanczos instead of the FSRCNN neural upscaler
--scale 1 Save the enhanced image at the native 1810 px width
--mode chart, --mode greyscale Force the finishing mode (auto picks one per fax)
--ioc 288, --lpm 60 Override the detected values
--save-audio Also keep each transmission as a WAV file
--host 0.0.0.0 Allow viewing from other devices on your LAN
--port 8730 Web page port

python tools/reenhance.py re-runs the enhancement on every saved fax with the current settings. It keeps each previous enhanced image as *_old.png.

How it works

Detection. A cheap causal demodulator watches for the start tone (300 Hz for IOC 576, 675 Hz for IOC 288), the phasing pattern, the 450 Hz stop tone, and loss of signal. If the stop tone is missed on a back-to-back broadcast, the next chart's phasing still closes the current chart and starts a new one. A band-power test keeps a weak or fading fax counted as present. The whole transmission, with 12 s of pre-roll, is then decoded offline.

AFC. A two-tooth comb is fitted to the 1500 and 2300 Hz lines of the audio spectrum, then re-fitted in 20 s windows to track drift. Noise is spectrally flat, so the estimate is not pulled toward the centre at low SNR.

Demodulation. Zero-phase I/Q filtering is followed by a correlation discriminator that averages z[n]·z*[n−1] before taking the angle. Samples in fades and crashes therefore carry little weight. Clicks, fades and QRN crashes are flagged and later filled from the lines above and below.

Line timing and slant. The phasing pulse is tracked line by line. A robust fit gives the LPM, the exact line period and the line start. The period is refined by maximising vertical-edge sharpness, and a smooth residual follows sound-card clock drift. Leftover phasing lines at the top and stop-tone lines at the bottom are trimmed. If there is no stop tone, trailing noise is cut off where the fax tones disappear.

Tears and the flywheel. Lost audio samples shift every later line sideways.

  • Flywheel: each sound-card buffer is time-stamped by the PC clock as it arrives. Buffers can be late but never early, so the lowest arrival times trace the true clock. A lasting step in that trace shows how much audio was lost and where, and the gap is filled with silence before decoding.
  • Image check: any remaining jump is found by comparing blocks of lines. It is only corrected if the shift persists further down the chart.

Enhancement.

  1. Single-line noise streaks are removed. A streak is a deviation from the lines above and below that is coherent along the line.
  2. Clean charts then just get levels and the upscale.
  3. On noisy charts, 24 oriented line detectors rebuild broken isobars and fronts. A mark only counts as a line if it responds along its direction and not across it, so text and noise are left alone.
  4. The background gets light non-local-means denoising. Faint greys fade gradually into the paper rather than being cut off, so weak or noisy text stays readable. The result is upscaled 2x with FSRCNN.
  5. Paper is shown at 96% white rather than full white (WHITE_LEVEL in wefax/enhance.py), which is easier on the eye and keeps detail on screens that clip near white.

Testing without a radio

python tools/make_test_signal.py test.wav --snr 12 --ppm 150 --offset 40
python wefax_decoder.py --wav test.wav

This synthesises a chart with a tuning offset, drift, clock error, noise, QRN crashes and fades, and also writes the true image to test_truth.npy.

Project layout

wefax_decoder.py          main program: capture, decode, save, web server
start_wefax.bat           Windows launcher
wefax/
  frontend.py             live capture, tone detection, flywheel time-stamps
  dsp.py                  filters, demodulators, AFC comb, tone features
  decode.py               offline decoder: AFC, phasing, slant, gaps, edges
  realign.py              tear detection and repair
  enhance.py              streak removal, line rebuilding, upscaling
  preview.py              live preview image
  schedule.py             broadcast schedules and chart identification
  cat.py                  Kenwood TS-2000 CAT for SDR Console
  autosched.py            auto-scheduler, propagation model, learning log
  housekeep.py            nightly deletion of faxes older than the Keep setting
  label.py                title block on the enhanced charts
  server.py               web server and live updates
  web/index.html          the page
  data/                   OpenCPN WeatherFaxSchedules.xml (GPL-3.0, data)
  models/                 FSRCNN x2 ONNX weights (Apache-2.0)
tools/
  make_test_signal.py     synthetic test transmissions
  reenhance.py            re-run enhancement on saved faxes
  comtest.py              COM port / CAT diagnostic

Licence

The code in this project is released under the MIT License (see LICENSE).

Three bundled third-party files keep their own licences:

  • wefax/data/WeatherFaxSchedules.xml is from the OpenCPN weatherfax plugin (rgleason/weatherfax_pi) and stays under GPL-3.0 (see wefax/data/WeatherFaxSchedules_LICENSE.txt). It is a data file read at run time, not part of the code. If you redistribute it, the GPL-3.0 terms apply to that file. You can also leave it out and supply your own copy next to wefax_decoder.py.
  • wefax/fonts/DejaVuSans-Bold.ttf is the DejaVu Sans Bold font, under the Bitstream Vera licence (see wefax/fonts/DejaVu_LICENSE.txt).
  • wefax/models/fsrcnn_x2.onnx contains the FSRCNN x2 weights from Saafke/FSRCNN_Tensorflow, Apache-2.0 (see wefax/models/FSRCNN_LICENSE.txt). They were converted to ONNX and checked against the original graph.

About

Automatic HF wefax decoder requiring SDR Console and any SDR receiver, with automatic tuning using schedules, image enhancements, frequency learning.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages