Skip to content

Latest commit

 

History

626 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

VU2CPL Shack Automation

License: MIT

Node-RED flows + supporting scripts that automate the VU2CPL amateur radio shack: lightning protection, power control, radio + amplifier telemetry, solar conditions, DXCC alerting, and Pi fleet monitoring.

VU2CPL · Manoj · Bengaluru · MK83TE · Licensed 1993 · 9× BDXCC


Overview

A single Node-RED instance on a Raspberry Pi 4B (noderedpi4, 192.168.1.169) hosts 11 flow tabs and one consolidated Shack dashboard. Hardware talks to Node-RED via:

  • MQTT (Mosquitto on the same Pi) — Tasmota power outlets, AS3935 lightning sensor, RPi telemetry, UberSDR receiver metrics. Authenticated (anonymous disabled 2026-08-21) — role accounts iot/svc/nodered/ha/display with per-account ACLs; see MQTT_AUTH.md. The read-only display account feeds the AetherSDR panadapter status overlay via a Node-RED humanizer flow (nodered/aether-display/).
  • Direct TCP/UDP — FlexRadio (4992), DX clusters
  • Serial — SPE amplifier (via spe-remote gateway)
  • WebSocket — LP-700 power/SWR meter (via the lp700-server gateway) and Rotor-EZ rotator (via the rotator-remote gateway)
  • HTTP — Open-Meteo, NOAA SWPC, Club Log, RPi fleet agent

Two dashboards, both served by the same Node-RED instance:

  • /ui — Dashboard 1 (legacy Angular). Dark-themed (base #097479, bg #111111). Original, all features intact.
  • /shack — Vue 3 SPA served via uibuilder. 13 cards, all collapsed by default with live summary headers, true responsive layout (CSS column masonry), PWA-installable on iPad / iPhone ("Add to Home Screen"). The primary dashboard going forward. Cards are individually toggleable via a const CARDS = { … } block at the top of uibuilder/shack/src/index.js (v-if per card) — a forker who lacks a subsystem hides its card without deleting any code; rebuild_pi.sh Stage 13 sets these from a hardware Y/n round. See SHACK_CHANGELOG 2026-06-04.

A 2026-05-24 POC of Dashboard 2 (@flowfuse/node-red-dashboard, served at /dashboard) was retired on 2026-05-26 in favour of uibuilder + Vue. See SHACK_CHANGELOG 2026-05-26 for the retirement rationale.

Since 2026-08-25 there is also a Home Assistant satellite view: the HA "Radio" dashboard (on the household HA instance) mirrors the whole Vue card set with stock HA cards, across two tabs — Radio (operating) and Fleet (monitoring). The MQTT-native subsystems — Power, Solar, House Loads, Lightning, RPi Fleet, GPS NTP, Network, UberSDR — ride the shack's existing topics through retained MQTT-discovery configs published by ha_discovery_publish.py, needing no Node-RED changes at all. The gateway-backed ones (FlexRadio, SPE, LP-700, Rotator, DXCC, RBN) followed the same day over read-only Node-RED → MQTT state bridges: per tab, an inject tick → a function reading that tab's existing context → a retained shack/<subsystem>/state, touching no flow behaviour. Lightning and the rotator also carry controls, via HA rest_commands onto the same HTTP endpoints the dashboards use. One caveat: the DXCC card goes blank while the DXCC Tracker flow tab is disabled — it has been since 2026-08-27 — because that tab's bridge stops with it.


Hardware

Item Detail
Radio FlexRadio FLEX-6600, RGO One, Icom IC-705
Amplifier SPE Expert 1.5 KFA
Antennas Hex beam, 160 m inverted-L, 6 m LFA Yagi, Beverage / N6RK / K9AY receive
Power 21 Tasmota-controlled outlets across 5 shack devices, plus 4 house-load Tasmota circuits (FF/GF load, dryer-kitchen, utility — HA's load-shedding relays, toggleable from the Power card)
Solar Deye SG0*LP3 LV 3-phase hybrid inverter, read locally via its Solarman WiFi logger (no cloud, no Home Assistant in the path)
Power meter Telepost LP-700 (USB HID, owned by lp700-server on the Pi)
Rotator Idiom Press Rotor-EZ
Lightning AS3935 sensor on ESP-WROOM-32 bridge → WiFi → MQTT (see vu2cpl-as3935-bridge) + Open-Meteo CAPE polling. Indoor Pi-side daemon retained as standby fallback
Awards 9× BDXCC
DXpeditions VU7T, VU7MS (Lakshadweep), AT5P (Rameshwaram)
Grid MK83TE — Bengaluru
Licensed Since 1993

Subsystems

Eleven flow tabs, each handling one logical subsystem. Detailed node IDs, function bodies, and operational quirks live in CLAUDE.md. The summaries below are "what + why".

Lightning Antenna Protector

Auto-disconnects the antenna and radio when lightning is detected within a configurable threshold (default 25 km). Two strike sources:

  • Open-Meteo CAPE polling every 5 min — synthesises a strike distance from CAPE values and WMO weather codes (95/96/99 = thunderstorm).
  • AS3935 chip sensor — rated ~40 km range. Bridged onto MQTT by an ESP-WROOM-32 (vu2cpl-as3935-bridge, v0.2.0). Indoor as3935.service on the Pi is retained as standby fallback. Installed outdoors 2026-07-31 (sealed enclosure, 18650+TP4056+solar power chain, shade mount, TUN_CAP retuned) — off the bench and back to its full rated range.

A vertical BYPASS switch on the dashboard suspends auto-disconnect for 120 minutes (force-reconnects ant + radio on activation, never survives a Node-RED restart). The alert banner always shows current state with a muted "Last: …" recap after 30 s of silence. Reconnect fires automatically after a configurable clear period (default 20 min) once the storm passes.

Event logging: Strike events (including disconnect/reconnect transitions and sensor status changes) are persisted to nr_lightning_events.jsonl in the flows directory. The path is configured in the Init Defaults node (ec1fd4dece8c4dc0) on the Lightning tab and survives Node-RED restarts.

UI lives on the main Shack tab as the Lightning Protection group (width 12, order 9).

SPE Amplifier

Reads the SPE Expert 1.5 KFA over FTDI serial at 250 ms intervals (76-byte fixed frame, checksum + wraparound validation). Power-on is sent over the WebSocket and performed by the spe-remote gateway's DTR/RTS toggle (spe/power_control.py); power_spe_on.py is a standalone fallback for when the service is down.

Output-power bar — auto-ranging. The bar's full-scale value is picked from a ladder (5 / 10 / 25 / 50 / 100 / 250 / 500 / 1k / 1.5k / 2k / 5k W) — the smallest rung that still contains the current reading. So a 3 W tune carrier fills ~60 % of a 5 W bar, a 700 W blast fills ~70 % of a 1 k W bar, and a clean 1.5 kW key-down sits on the 1.5 k W rung. Matches the legacy /ui SPE Panel behaviour with a finer low-end ladder (so low-power tuning is actually readable). The amp's L / M / H power-level setting is still shown separately as a tile (PWR LVL / colour-coded amber/red for Middle/Maximum), but does not drive the bar scale — auto-ranging tracks the actual output regardless of the amp's selected level cap.

TUNE control + LED. The Tune button starts an ATU tune cycle, and its colour mirrors the amp's front-panel TUNE LED — the button stays neutral while the LED is off and turns solid amber (with a glow, and a ⚡ TUNE chip in the Vue card's collapsed header) while the amp is in TUNE mode. The LED state comes from the amp's RCU LCD stream (decoded in the spe-remote gateway), not the CSV status frame, so it's live whenever the gateway is running.

Power meter modes — RAW / AVG / PEAK. A pill toggle by the Output Power bar (both dashboards) switches what the bar shows: RAW (the live ~25 Hz reading), AVG (a ~1 s smoothed average), or PEAK (a peak-hold that pins the highest reading ~2.5 s then eases back down at a constant 600 W/s). AVG and PEAK are derived in the spe-remote gateway (p_out_avg / p_out_peak); each pill appears only once the gateway is sending that field, so an older gateway simply shows RAW. PEAK is a sampled peak (25 Hz) — good for catching sustained SSB/CW crests, not a true envelope-PEP wattmeter. The chosen mode persists in the browser and is shared between /ui and /shack.

FlexRadio

TCP API to the FLEX-6600 at 192.168.1.148:4992, plus UDP discovery. Per-slice state (frequency, mode, RX/TX, meter levels) aggregated into flexState flow context and rendered into the FlexRadio panel. clientHandleMap is built from the discovery message so we can label slices with the GUI client station name.

Transmit-state semantics — two distinct concepts that look the same:

  • slice.tx == 1 and the derived slice.isTx == 1 mean "this slice is the TX-armed slice" — i.e. which slice would transmit if you keyed up. It does NOT mean the radio is currently transmitting. (Aggregator handles split mode where both slices have tx==1: it picks the one with active==0 as the true TX.)
  • flexState.txstate (string) is the actual radio state — "READY" while receiving, something else (e.g. "TRANSMIT") while keyed.

The new Vue /shack dashboard uses txstate for the live RX/TX indicator in the FlexRadio card header. The old D1 panel mixes the two in places but renders correctly because of the surrounding context. Lesson: if you add any "is the radio transmitting right now?" logic to a flow node or template, key it off flexState.txstate, not slice.isTx.

Power Control (Tasmota fleet)

5 Tasmota devices, 21 outlets total. Each outlet has its own dashboard tile; the panel template is fully driven from MQTT stat/<device>/POWER<n> state messages. Outlets default to off on page-load to avoid the "stale ON" bug; a 30 s poll loop pings every outlet across every device so the dashboard never lies for more than ~30 s.

The 16 A master switch publishes energy data every 30 s for shack-wide consumption monitoring. The rotator outlet has a 5 min auto-off timer with idempotent retrigger guard + 10 s cooldown to prevent reset loops.

Below the 16 A voltage/current/power/today line, both dashboards carry two further rows (added 2026-08-25): a solar row of two half-width tiles — Grid (ON/OFF plus the three input phase voltages) and Battery (state of charge, with charge state and power on one line) — read directly from the Deye hybrid inverter's Solarman logger by solar_inverter_mqtt.py (1-min cron → retained shack/solar/inverter, and since 2026-08-25 an append-only ~/grid_voltage.csv — see Grid voltage log below) — and a house-loads row showing the four house Tasmota circuits (FF Load, GF Load, Dryer Kit, Utility): relay ON/OFF, live watts, and today's kWh. Each house tile is also the switch — click, confirm, and it toggles the relay via POST /power/house-toggle. The relays belong to Home Assistant's battery-SOC load-shedding automations, which may override a manual toggle on their next threshold rule.

Grid voltage log

solar_inverter_mqtt.py also appends every reading to ~/grid_voltage.csv (ts_iso, ts_epoch, status, l1_v, l2_v, l3_v, grid_on, batt_soc, batt_p_w). Nothing else in the shack stores a voltage time series — the MQTT message is retained, i.e. last value only — so this file is the sole record, and it is not regenerable. Added 2026-08-25 to evidence nightly over-voltage to the supply utility. Failed reads are logged as status=read_fail rather than left as a bare gap, so "the logger was busy" stays distinguishable from "the site lost power".

grid_voltage_report.py renders it into a Markdown report plus an SVG chart — daily per-phase min/mean/max, a night-window section, over-voltage episodes, and supply interruptions. The chart uses a fixed 200–300 V scale with the night window shaded and supply interruptions drawn as red "BESCOM off" bands (the phase traces break across them rather than plunging to the inverter's 0 V reading). Pure stdlib, runs on the Mac or the Pi:

python3 grid_voltage_report.py --csv ~/grid_voltage.csv --out report.md --svg chart.svg

Two caveats the report states on its own front page, because both change what the numbers prove: the inverter's grid input is downstream of the servo stabiliser (so readings are true incoming mains only while the stabiliser is in bypass), and a hybrid inverter exporting PV lifts the voltage at its own terminals (so daytime figures are arguable, while the night window has neither PV nor export).

Stabiliser watchdog

Currently disabled (2026-08-30, operator request). The cron entry was removed once the overnight experiment concluded; the script, its state file and the env-file credentials are all still in place. Re-enable with one line: * * * * * /usr/bin/python3 /home/vu2cpl/stabiliser_watch.py --cron

It ran correctly for its ~34 hours: 7 outage alerts + 7 recoveries, all real, no false positives.

stabiliser_watch.py tails the same CSV once a minute from cron and pushes a Telegram alert when the supply state changes. It exists because the question the log is answering — do the nightly outages resume now the stabiliser is back in circuit? — comes due while the operator is asleep. Three conditions are separated, because they implicate different things:

State Meaning Signature
🔴 outage Grid absent — the nightly-cut pattern under test grid_on=0 or logger unreachable, 2+ samples
🟠 dropout Stabiliser no longer in circuit (tripped to bypass or switched out) vmax above 245 V for 5+ consecutive samples
stale CSV stopped growing — the watchdog is blind no new row for 6 min

A ✅ recovery alert follows when it returns to regulating. State lives in ~/.stabiliser_watch.state so each transition alerts once rather than every minute.

Why voltage and not phase spread. Spread looks like the obvious discriminator and is not: across the bypass window it ranged 0.6–22 V and overlapped the regulated range completely. Absolute voltage separates them better — regulated output sits near 230 V while raw mains ran 250–264 V — so the threshold keys on vmax.

Why the confirmation count does the real work. The first hour of regulated output peaked at 242.2 V, leaving only 3.9 V of margin under the 245 V threshold across a 3-sample run — too thin to stake a 3 a.m. alert on. Measured against the real data, requiring 5 consecutive samples lifts the worst sustained regulated reading to 238.0 V (+7.0 V margin) while costing almost no detection: night coverage over the bypass window falls 96.7% → 95.5%, because genuine dropouts lasted many minutes. Raising the threshold instead would have been far more expensive — 250 V drops night coverage to 75.9%.

Settled 2026-08-30. Across the full regulated era (1966 live samples, including a night with no PV export) vmax peaked at 242.7 V and nothing crossed 245 V, so no false 🟠 fired — and none would have at the old 3-sample setting either. The retune was precautionary rather than necessary, but the ceiling is now measured rather than guessed: regulated output tops out around 243 V. Re-measure both constants only if the stabiliser is reconfigured to regulate higher.

Credentials. Token and chat-id resolve in three steps: the process environment, then the shack env files (/etc/default/vu2cpl-shack, then ~/.config/vu2cpl-shack.env, which wins — the same precedence monitor.sh uses), then the running Node-RED process as a fallback. The env-file step was added 2026-08-29 on the operator's instruction: before it, alerting depended on Node-RED being alive, so the watchdog went silent exactly when the shack was least healthy. flows_guard.py got the same treatment in the same pass.

The cost is a bot token at rest in a mode-600 file inside a 700 home directory. Neither env file is in the repo, and this repo is public — keep it that way. The env file is created by hand on a rebuild; see REBUILD_PI.md.

python3 stabiliser_watch.py --status   # current state, no alert
python3 stabiliser_watch.py --test     # verify the Telegram path

The bypass window is closed. The stabiliser was in bypass from 2026-08-25 13:11 IST (when logging began) to 2026-08-29 10:09 IST, when the operator brought it back online. Only that slice of grid_voltage.csv is true incoming mains; every row from 10:10 on 2026-08-29 onward is the stabiliser's regulated output (~232 V, all three phases within ~1 V of each other — the tight convergence is how you spot the boundary in the raw file, which carries no stabiliser-state column). Pass --bypass to label the window in the report, and filter the CSV to it before quoting any over-voltage figure to the utility.

What the sealed window showed, and what the follow-up experiment did. Across 25–29 Aug on true mains: 24.5% of samples above 253 V, peak 264.2 V, nothing near the 270 V stabiliser trip, and eight interruptions none of which fell overnight — against a Solarman cloud export that put 89% of 49 interruptions between 22:00 and 06:00 in the preceding month. That made the stabiliser look like the thing tripping. Putting it back in circuit on 29 Aug was the test, and the night of 29→30 Aug ran 480/480 samples with zero interruptions — the pattern did not resume. Restoring the suspected cause did not restore the effect, so the nightly-trip hypothesis is substantially weakened; the cuts appear to have stopped around 25 Aug for reasons outside the shack. The claims that still hold are chronic over-voltage and frequent interruptions by day (six across 29–30 Aug), and those are what a utility case should rest on.

Historic supply interruptions

For the period before local logging began, the only record is what the inverter had already uploaded to the manufacturer's cloud. deye_history_report.py parses Solarman's "PlantsDetails-History" .xlsx exports (5-minute telemetry, one file per day) into a supply-interruption report, a day-by-day timeline SVG, and a CSV:

python3 deye_history_report.py --dir "~/Downloads/deye data" \
    --out outages.md --svg outages.svg --csv outages.csv

That export carries no voltage channel, so it evidences outages rather than over-voltage. An interruption is only counted where grid power is zero and the battery is discharging to carry the house (or there is no solar to do so) — a full battery with PV covering the load also draws nothing from the grid and would otherwise be miscounted. The battery's state of charge falling is independent confirmation that supply was genuinely absent, which is the first thing a utility will question.

Solar Conditions

Polls NOAA SWPC every 15 min for SFI, K-index (8 × 3-hour planetary Kp), A-index (planetary Ap), Scales (R/S/G), GOES X-ray flux, and prop.kc2g.com for MUF / foF2 at home grid. Three ui-level gauges + band-condition heatmap.

LP-700 Power/SWR Meter

Migrated from direct USB-HID to a WebSocket client of the VU3ESV/LP-700-Server gateway running on the same Pi. The gateway owns /dev/hidraw*; Node-RED, the embedded web client, and any future Mac SwiftUI app all subscribe to telemetry concurrently. Updates at ~25 Hz. Buttons emit JSON command verbs (channel_step, range_step, etc.).

Rotator

Idiom Press Rotor-EZ on FTDI serial at 4800-8N1. Heading display + preset compass-rose buttons. Since 2026-06-06 the serial port is owned by the rotator-remote gateway (rotator-remote.service, :8090) — the Node-RED tab is a thin WebSocket client, so the browser, a future Mac app, and any other client share the rotor without serial contention. Power stays on Tasmota/MQTT.

DXCC Tracker

Disabled since 2026-08-27 (operator choice): the flow tab is disabled in flows.json and the Vue card hidden via CARDS.dxcc. Nothing is deleted — flip both flags back to re-enable.

DX cluster spot monitoring with real-time alerts for new entities, bands, and modes against Club Log worked/confirmed data. Subscribes to four cluster sources (VU2CPL, VU2OY, KIEL, N2WQ); resolves prefix via cty.xml; classifies against bands[mk] === 2 confirmed criterion; fires alerts to dashboard, MQTT, FlexRadio, and Telegram.

Full reference: DXCC.md / DXCC_Tracker_README.pdf.

RPi Fleet Monitor

Subscribes to rpi/<hostname>/{cpu,mem,temp,disk,uptime,ip,status} MQTT topics. Builds a fleet-status panel with one card per host; alerts on CPU >90 %, Temp >75 °C, Mem >90 %, Disk >90 %. Reboot / Shutdown buttons send POST /reboot or /shutdown to each host's rpi-agent.service listening on :7799.

Currently monitoring noderedpi4, openwebrxplus, gpsntp. Two more Pis + the Home Assistant Pi pending onboarding.

A separate Chrony / GPS Time Server card on the Shack Monitoring tools dashboard tab (Network Monitor group) shows live status of gpsntp.local — the stratum-1 GPS-disciplined NTP server. Single ui_template driven by retained MQTT topic shack/gpsntp/chrony (cron-published every minute by the Pi-side pi-gps-ntp-server project). Threshold-aware (orange when offset / dispersion / skew exceeds tolerance, ref source ≠ PPS, or fix < 3D).

Per-Pi onboarding runbook: DEPLOY_PI.md.

Internet & Network Monitor

Pings local infrastructure (router, AP, key servers) and the broader internet (DNS, well-known anycast hosts) at intervals; surfaces latency + reachability on a status grid. Used to diagnose whether a station-wide problem is local network or upstream.

RBN Skimmer Monitor

Reverse Beacon Network monitoring — tracks how the VU2CPL signal is being heard worldwide via skimmer reports. Calibration data fetched from sm7iun.se/rbnskew.csv every 6 h.


Repository Layout

├── flows.json                       Main Node-RED flow (canonical source)
│
├── as3935_mqtt.py                   AS3935 chip daemon — standby fallback (ESP32 bridge is primary publisher)
├── as3935_tune.py                   LC-tank TUN_CAP sweep helper
├── rpi_agent.py                     HTTP reboot/shutdown agent (→ rpi-agent.service)
├── rpi-agent.service                systemd unit for rpi_agent
├── monitor.sh                       MQTT telemetry cron (every minute)
├── solar_inverter_mqtt.py           Deye inverter → retained shack/solar/inverter + ~/grid_voltage.csv (1-min cron)
├── grid_voltage_report.py           grid_voltage.csv → Markdown + SVG evidence report (pure stdlib)
├── stabiliser_watch.py              Stabiliser trip / outage / dropout → Telegram (1-min cron, pure stdlib)
├── deye_history_report.py           Solarman xlsx cloud exports → supply-interruption report + timeline (pure stdlib)
├── monitor_redpitaya.sh             Fleet telemetry variant for the Red Pitaya (Alpine/BusyBox, Zynq XADC temp)
├── flows_guard.py                   Stale-tab wipe tripwire (git pre-commit hook + 1-min cron w/ Telegram alert)
├── flows_guard_middleware.js        Server-side deploy rejection (httpAdminMiddleware in settings.js)
├── power_spe_on.py                  SPE amp FTDI DTR/RTS power-on helper
├── enable_file_context.sh           One-shot Node-RED file context store enabler
├── rebuild_pi.sh                    Automated bare-metal rebuild script (paired with REBUILD_PI.md)
│
├── as3935.service                   systemd unit for as3935_mqtt.py (installed but disabled; re-enable as fallback if ESP32 fails)
│
├── README.md                        This file (umbrella overview)
├── DXCC.md                          DXCC Tracker reference
├── DXCC_Tracker_README.pdf          DXCC reference (rendered PDF)
├── CLAUDE.md                        Operator deep-reference: node IDs, gotchas, runtimes
├── REBUILD_PI.md                    Disaster-recovery runbook: blank SD → working shack
├── DEPLOY_PI.md                     Per-host fleet-member onboarding runbook
├── FORK_GUIDE.md                    Customisation runbook for forking to a different station
├── HANDOVER.md                      Session pickup notes
├── SHACK_CHANGELOG.md               Dated changelog of non-DXCC tab changes
├── SHACK_CHANGELOG.pdf              Changelog rendered PDF (always in sync with .md)
└── LICENSE                          MIT

Documentation map

Doc Audience When to read
README.md Anyone First — what is this repo
CLAUDE.md Operator (and the LLM context) Looking up a node ID, broker port, install command
REBUILD_PI.md Operator (disaster recovery) "The shack Pi died — rebuild from blank SD card"
DEPLOY_PI.md Operator Onboarding a different Pi as a fleet member (telemetry + reboot agent only)
FORK_GUIDE.md Different operator forking the repo "I'm not VU2CPL but I want to run this stack at my own QTH" — per-site customisation runbook (callsign, grid, MQTT broker, hardware substitutions, /shack rebadging)
DXCC.md / PDF Operator Working on the DXCC tab specifically
SHACK_CHANGELOG.md / PDF Future-self "What did I change on day X"
HANDOVER.md New session pickup "What was I in the middle of"

Standard commit sequence

After deploying changes in the Node-RED editor:

ssh vu2cpl@192.168.1.169
cd ~/.node-red/projects/vu2cpl-shack
nrsave "<description>"     # alias: git add flows.json && git commit -m
git push

Pull on the Mac side:

cd ~/projects/vu2cpl-shack
git pull

For DXCC tab changes the commit additionally regenerates the tab extract — see DXCC.md.


License

MIT — see LICENSE. Third-party code embedded in flows.json (notably from VU3ESV/LP-700-Server) retains its own terms.


73 de VU2CPL

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages