From 62f718cee7d543ca63e96e03d6a21977aa06ec1a Mon Sep 17 00:00:00 2001 From: Dr Bill Mcilhargey Date: Wed, 5 Aug 2026 13:28:14 +0000 Subject: [PATCH 01/12] readsb-wiedehopf: add package at 3.16.11 Package the actively maintained wiedehopf fork of readsb with its console viewer, procd integration, UCI configuration, USB hotplug support, and operator helper commands. Keep automatic public-IP geolocation opt-in, preserve service state during upgrades, and validate feeder configuration before applying it. Upstream: https://github.com/wiedehopf/readsb Signed-off-by: Dr Bill Mcilhargey --- utils/readsb-wiedehopf/Makefile | 170 ++ utils/readsb-wiedehopf/README.md | 647 ++++++++ utils/readsb-wiedehopf/files/readsb-feeder | 886 ++++++++++ utils/readsb-wiedehopf/files/readsb-geoip | 461 ++++++ utils/readsb-wiedehopf/files/readsb-setup | 1424 +++++++++++++++++ utils/readsb-wiedehopf/files/readsb-uuid | 153 ++ utils/readsb-wiedehopf/files/readsb.config | 127 ++ .../files/readsb.functions.sh | 1253 +++++++++++++++ utils/readsb-wiedehopf/files/readsb.hotplug | 178 +++ utils/readsb-wiedehopf/files/readsb.init | 440 +++++ 10 files changed, 5739 insertions(+) create mode 100644 utils/readsb-wiedehopf/Makefile create mode 100644 utils/readsb-wiedehopf/README.md create mode 100644 utils/readsb-wiedehopf/files/readsb-feeder create mode 100644 utils/readsb-wiedehopf/files/readsb-geoip create mode 100644 utils/readsb-wiedehopf/files/readsb-setup create mode 100644 utils/readsb-wiedehopf/files/readsb-uuid create mode 100644 utils/readsb-wiedehopf/files/readsb.config create mode 100644 utils/readsb-wiedehopf/files/readsb.functions.sh create mode 100644 utils/readsb-wiedehopf/files/readsb.hotplug create mode 100644 utils/readsb-wiedehopf/files/readsb.init diff --git a/utils/readsb-wiedehopf/Makefile b/utils/readsb-wiedehopf/Makefile new file mode 100644 index 00000000000000..c175b6be16ccb2 --- /dev/null +++ b/utils/readsb-wiedehopf/Makefile @@ -0,0 +1,170 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Copyright (C) 2026 OpenWrt.org +# +# This is free software, licensed under the GNU General Public License v2. +# See /LICENSE for more information. + +include $(TOPDIR)/rules.mk + +PKG_NAME:=readsb-wiedehopf +PKG_VERSION:=3.16.11 +PKG_RELEASE:=1 +PKG_SOURCE:=readsb-$(PKG_VERSION).tar.gz +PKG_SOURCE_URL:=https://codeload.github.com/wiedehopf/readsb/tar.gz/v$(PKG_VERSION)? +PKG_HASH:=ac0488a2deadd20b373a56928b7d297b5e9262566dd84ec9c72cffaedff2cc78 + +PKG_BUILD_DIR:=$(BUILD_DIR)/readsb-$(PKG_VERSION) + +PKG_LICENSE:=GPL-3.0-or-later BSD-3-Clause +PKG_LICENSE_FILES:=LICENSE COPYING + +PKG_MAINTAINER:=Dr Bill Mcilhargey + +PKG_BUILD_DEPENDS:=!USE_GLIBC:argp-standalone +PKG_BUILD_PARALLEL:=1 +PKG_BUILD_FLAGS:=gc-sections lto +PKG_FORTIFY_SOURCE:=0 + +include $(INCLUDE_DIR)/package.mk + +define Package/readsb-wiedehopf/default + SECTION:=utils + CATEGORY:=Utilities + TITLE:=Mode-S/ADSB/TIS decoder (wiedehopf fork) + URL:=https://github.com/wiedehopf/readsb + # Upstream creates viewadsb by copying the fully linked readsb binary. + DEPENDS:=+libncurses +librtlsdr +libzstd +zlib +endef + +define Package/readsb-wiedehopf + $(call Package/readsb-wiedehopf/default) + TITLE+= (readsb) + DEPENDS+=+jsonfilter +ca-bundle + PROVIDES:=readsb + CONFLICTS:=readsb +endef + +define Package/readsb-wiedehopf/description + readsb is a Mode-S/ADSB/TIS decoder for RTL-SDR and other SDR devices. + This is the wiedehopf fork (https://github.com/wiedehopf/readsb), an + actively maintained alternative to the `readsb` package (which tracks + the now-archived Mictronics upstream). It adds many features used by + aggregators (BeastReduce, UUIDs, globe history, heatmaps, JSON/binCraft + outputs, http API) and is the upstream used by tar1090, adsb.lol, + airplanes.live, adsb.fi and similar projects. + + Installs /usr/bin/readsb and conflicts with the readsb package; either + can satisfy a `readsb` dependency. + + Interactive setup is provided by /usr/sbin/readsb-setup (full guided + first-boot configuration: lat/lon, station UUID, aggregator feeders, + service reload). Per-area CLIs: readsb-uuid, readsb-geoip, readsb-feeder. + + Optional companion packages (not pulled in automatically): + * adsbexchange-stats -- ranking-dashboard stats uploader for ADSBx. + * resolveip -- per-host socket attribution in readsb-feeder when + multiple feeders share a remote port; degrades to "indeterminate" + when absent. + + See https://github.com/openwrt/packages/blob/master/utils/readsb-wiedehopf/README.md + for the full configuration reference, aggregator preset list, and + operational notes. +endef + +define Package/viewadsb-wiedehopf + $(call Package/readsb-wiedehopf/default) + TITLE+= (viewadsb) + PROVIDES:=viewadsb + CONFLICTS:=viewadsb +endef + +define Package/viewadsb-wiedehopf/description + Console viewer companion to readsb (wiedehopf fork). Connects to a + running readsb instance over the network and displays an interactive + list of received aircraft. +endef + +MAKE_FLAGS += \ + BLADERF=no \ + HACKRF=no \ + PLUTOSDR=no \ + SOAPYSDR=no \ + OPTIMIZE="$(TARGET_CFLAGS)" \ + RTLSDR=yes \ + UNAME="Linux" + +TARGET_LDFLAGS += \ + $(if $(CONFIG_USE_GLIBC),,-largp) \ + -lncurses \ + -Wl,--as-needed + +define Package/readsb-wiedehopf/conffiles +/etc/config/readsb +endef + +define Package/readsb-wiedehopf/install + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/readsb.config $(1)/etc/config/readsb + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/readsb.init $(1)/etc/init.d/readsb + $(INSTALL_DIR) $(1)/etc/hotplug.d/usb + $(INSTALL_BIN) ./files/readsb.hotplug $(1)/etc/hotplug.d/usb/30-readsb + $(INSTALL_DIR) $(1)/usr/lib/readsb + $(INSTALL_DATA) ./files/readsb.functions.sh $(1)/usr/lib/readsb/functions.sh + $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_BIN) ./files/readsb-geoip $(1)/usr/sbin/readsb-geoip + $(INSTALL_BIN) ./files/readsb-uuid $(1)/usr/sbin/readsb-uuid + $(INSTALL_BIN) ./files/readsb-feeder $(1)/usr/sbin/readsb-feeder + $(INSTALL_BIN) ./files/readsb-setup $(1)/usr/sbin/readsb-setup + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) $(PKG_BUILD_DIR)/readsb $(1)/usr/bin/readsb +endef + +define Package/readsb-wiedehopf/postinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] && exit 0 +[ "$${PKG_UPGRADE}" = "1" ] && { +/etc/init.d/readsb enabled && /etc/init.d/readsb restart +exit 0 +} +/etc/init.d/readsb enable +/etc/init.d/readsb start +# Print the orientation banner so `opkg install` shows it on the console. +echo +echo "readsb-wiedehopf installed and started." +/usr/sbin/readsb-setup --help +logger -t readsb -p daemon.notice -- "installed; run 'readsb-setup' to configure (location, UUID, feeders)" +exit 0 +endef + +define Package/readsb-wiedehopf/prerm +#!/bin/sh +# Stop and disable readsb before opkg removes /etc/init.d/readsb. +# Both calls are best-effort: a stopped daemon, missing procd +# registration or ubus timeout must not surface a non-zero exit OR +# stream chatter to opkg. Some builds emit "Command failed: Not found" +# to stdout instead of stderr, so redirect both streams. +[ -n "$${IPKG_INSTROOT}" ] && exit 0 +[ "$${PKG_UPGRADE}" = "1" ] && exit 0 +[ -x /etc/init.d/readsb ] && { + /etc/init.d/readsb stop >/dev/null 2>&1 + /etc/init.d/readsb disable >/dev/null 2>&1 +} +exit 0 +endef + +define Package/readsb-wiedehopf/postrm +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] && exit 0 +rm -rf /var/run/readsb +exit 0 +endef + +define Package/viewadsb-wiedehopf/install + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) $(PKG_BUILD_DIR)/viewadsb $(1)/usr/bin/viewadsb +endef + +$(eval $(call BuildPackage,readsb-wiedehopf)) +$(eval $(call BuildPackage,viewadsb-wiedehopf)) diff --git a/utils/readsb-wiedehopf/README.md b/utils/readsb-wiedehopf/README.md new file mode 100644 index 00000000000000..095ad167c06d5a --- /dev/null +++ b/utils/readsb-wiedehopf/README.md @@ -0,0 +1,647 @@ + + +# readsb-wiedehopf + +ADS-B / Mode-S decoder daemon for OpenWrt -- the actively maintained +[wiedehopf fork](https://github.com/wiedehopf/readsb) of readsb. Used +upstream by tar1090, adsb.lol, airplanes.live, adsb.fi, and similar +aggregators. + +## Contents + +* [Quick start](#quick-start) +* [What gets installed](#what-gets-installed) +* [Configuration model](#configuration-model) +* [Third-party network connections](#third-party-network-connections) +* [`/etc/config/readsb` -- main section options](#etcconfigreadsb----main-section-options) +* [SDR / hotplug behavior](#sdr--hotplug-behavior) + * [Single-SDR setup](#single-sdr-setup) + * [Multi-SDR setup](#multi-sdr-setup) +* [Boot-time behavior](#boot-time-behavior) +* [Aggregator feeders](#aggregator-feeders) + * [Built-in presets](#built-in-presets) + * [Adding a feeder](#adding-a-feeder) + * [`silent_fail` semantics](#silent_fail-semantics) + * [Per-feeder UUID override](#per-feeder-uuid-override) + * [adsbexchange supplemental stats](#adsbexchange-supplemental-stats) + * [adsblol map](#adsblol-map) +* [Out-of-scope aggregators](#out-of-scope-aggregators) +* [MLAT](#mlat) +* [Logging and diagnostics](#logging-and-diagnostics) + * [Log levels](#log-levels) +* [Companion packages](#companion-packages) +* [Conflicts](#conflicts) + +## Quick start + +```sh +opkg install readsb-wiedehopf +readsb-setup # guided first-boot: lat/lon, UUID, SDR, feeders, reload +``` + +The package is fully usable without `readsb-setup` -- the postinst step +enables and starts the daemon with safe defaults, and `opkg install` +prints a banner pointing at the wizard. Run `readsb-setup` when you're +ready to actually receive aircraft (you need at minimum a station +location and, for most aggregators, a UUID). + +The wizard walks through five steps; each is skippable and re-running +it is idempotent. Steps: + +1. station latitude / longitude (auto-fill from public IP, or enter manually) +2. station UUID (auto-generate) +3. SDR tuning -- gain / PPM / AGC / bias-T (**auto-skipped** if no SDR present) +4. at least one aggregator feeder (delegates to `readsb-feeder`) +5. apply changes (offers `service readsb reload`) + +For a non-interactive at-a-glance state report, e.g. from cron or for +health-check scripts: + +```sh +readsb-setup --status # configuration check (essentials, mode, + # ports, SDRs, feeders, companion packages) + # exit 0 if all essentials present, 1 if not +readsb-setup --health # runtime health (process metrics, host + # load, live data flow, per-feeder TCP + # probe + cmdline-presence + per-source + # log scan: readsb + each installed + # companion package) + # exit 0 = HEALTHY, 1 = DEGRADED, 2 = DOWN +readsb-setup --stats # ONLY the most recent periodic stats + # block (signal, decoded msgs, tracks, + # CPU). Same parser as --health. + # exit 0 = printed, 2 = daemon DOWN, + # 3 = no block buffered yet +``` + +The three are intentionally split: + +* **`--status`** -- *"is this box set up correctly?"* Walks UCI + USB + + the opkg control file. Stable across reloads. Safe to run before the + daemon is up. +* **`--health`** -- *"is this box working right now?"* Reads + `/proc//`, `/var/run/readsb/*.json`, the live readsb cmdline, + and the recent syslog buffer. Requires the daemon to be running. + The recent-log section is split per source: one block for the + daemon, then one per *installed* companion package (e.g. + `adsbexchange-stats`). Use for monitoring / cron health checks. +* **`--stats`** -- *"how is reception/decoding doing right now?"* + Parses the most recent periodic stats block out of `logread` and + prints just that block (signal dBFS, Mode-S preambles + CRCs, + decoded messages, positions, tracks, CPU). + +`--status` also runs an optional companion-package check for every +*enabled* feeder section: for any preset that has a recommended +companion package (currently `adsbexchange` -> `adsbexchange-stats`), +it reports whether that package is installed and (when applicable) +whether the bundled service is running. Missing/stopped findings are +printed with the exact `opkg install` / `service ... start` command +to fix them, and are mirrored to syslog. + +`--health` delegates the per-feeder runtime breakdown to +`readsb-feeder --health` (also runnable standalone, with an optional +`` argument). Per-feeder states: + +* `LIVE` -- in cmdline + (TCP probe ok OR active socket in + `/proc/net/tcp`) + log clean. +* `DEGRADED` -- in cmdline but recent error events are present, no probe + tool or active socket is available, or exact socket attribution is + indeterminate because multiple connectors share a remote port. +* `UNREACHABLE` -- in cmdline + TCP probe FAIL **and** no active + socket. The active-socket cross-check is the authoritative + tie-breaker because some aggregator firewalls (notably + `feed1.adsbexchange.com:30004`) drop drive-by SYN+close probes but + accept readsb's persistent feeder stream. +* `NOT-LOADED` -- in UCI but missing from the live cmdline; run + `service readsb reload`. +* `DISABLED` -- `enabled=0` in UCI; only shown when explicitly named. + +To dump the live UCI config in a human-readable form (without the noise +of `uci show`): + +```sh +readsb-setup --config # pretty-printed /etc/config/readsb + # plus /etc/config/ for each + # installed companion package +``` + +To re-display the orientation banner that's printed once at install +time (the "what can I do?" reference card): + +```sh +readsb-setup --help +``` + +## What gets installed + +| Path | Purpose | +| ----------------------------- | ----------------------------------------------------------------- | +| `/usr/bin/readsb` | the daemon (and `viewadsb` from the companion subpackage) | +| `/etc/config/readsb` | UCI config (declarative; see below) | +| `/etc/init.d/readsb` | procd init script (`service readsb start|stop|reload|status`) | +| `/etc/hotplug.d/usb/30-readsb`| RTL-SDR auto-detect on USB plug/unplug | +| `/usr/lib/readsb/functions.sh`| shared sh helpers (sourced by every helper CLI) | +| `/usr/sbin/readsb-setup` | guided first-boot wizard (also: `--status`, `--health`, `--stats`, `--config`, `--help` for the master CLI banner) | +| `/usr/sbin/readsb-feeder` | feeder management CLI (`--list`, `--probe`, `--health`, ...; see `readsb-feeder -h`) | +| `/usr/sbin/readsb-uuid` | station UUID wizard / generator (`--print`, `--auto`, `--force`) | +| `/usr/sbin/readsb-geoip` | public-IP-based lat/lon auto-fill | + +## Configuration model + +`/etc/config/readsb` is **declarative-only** by design. It contains: + +* exactly one `config readsb 'main'` section (daemon-wide settings) +* zero or more `config feeder ''` sections (outbound aggregator + connections, one per section) + +**Comments do not survive `uci commit`.** Every UCI commit -- whether +from the USB hotplug handler, from `readsb-uuid`, from `readsb-geoip`, +from LuCI, or from a manual `uci set ...; uci commit readsb` -- rewrites +the file in canonical form and strips every line starting with `#`. For +that reason, all human-facing documentation lives here in the README and +in each CLI's `--help`, not inside the config file. + +Mutating workflows (in order of preference): + +1. **interactive wizards** -- `readsb-setup`, `readsb-feeder`, `readsb-uuid` + (each prompt accepts `q`, `quit`, or `exit` -- or `Ctrl-D` -- to + abort cleanly; `readsb-setup` saves each completed step immediately, + so aborting retains completed steps but prevents later ones) +2. **non-interactive helper CLIs** -- e.g. `readsb-feeder --add`, `readsb-uuid --auto` +3. **plain UCI** -- `uci set readsb.main.lat=...; uci commit readsb` +4. **hand-edit `/etc/config/readsb`** -- always works, but the moment + anything else commits to UCI you lose any comments you added + +None of the helper CLIs auto-restart the daemon. After a batch of +changes, run: + +```sh +service readsb reload +``` + +## Third-party network connections + +The package does not contact geolocation services or aggregators by +default. Public-IP geolocation and every outbound feeder are separate, +explicit opt-ins. Local listeners such as ports `30001` through `30006` +do not create outbound connections by themselves and bind to loopback by +default. Set `net_bind_address` explicitly to expose them to other hosts. + +The setup wizard explains the destination and data flow before asking. +All third-party consent prompts default to **no**. Choosing a one-time +GeoIP lookup does not enable future automatic lookups; that is a separate +prompt and UCI option. Adding a feeder does not enable it unless the user +answers yes to the feeder connection prompt. + +| Connection | Destination / transport | Data disclosed | Default state | Opt in / opt out | +| ---------- | ----------------------- | -------------- | ------------- | ---------------- | +| Primary GeoIP | `ipapi.co` over HTTPS | the request reveals the router's public IP; the service returns approximate city-level coordinates | automatic lookup off (`geoip_auto=0`) | one time: choose auto-detect in `readsb-setup` and consent, or run `readsb-geoip`; persistent: set `geoip_auto=1`; opt out with `geoip_auto=0` | +| Fallback GeoIP | `ipwho.is` over HTTPS, attempted only if `ipapi.co` fails | same as primary GeoIP | automatic lookup off | controlled by the same one-time consent or `geoip_auto` setting | +| `adsblol` feeder | `in.adsb.lol:30004` over TCP | received Mode-S/ADS-B stream and station UUID when configured | no feeder section; no connection | add with `readsb-feeder`, then explicitly enable; disable with `readsb-feeder --disable ` | +| `airplaneslive` feeder | `feed.airplanes.live:30004` over TCP | same feeder data | no feeder section; no connection | same feeder opt-in / disable flow | +| `adsbfi` feeder | `feed.adsb.fi:30004` over TCP | same feeder data | no feeder section; no connection | same feeder opt-in / disable flow | +| `planespotters` feeder | `feed.planespotters.net:30004` over TCP | same feeder data | no feeder section; no connection | same feeder opt-in / disable flow | +| `theairtraffic` feeder | `feed.theairtraffic.com:30004` over TCP | same feeder data | no feeder section; no connection | same feeder opt-in / disable flow | +| `flyitaly` feeder | `dati.flyitalyadsb.com:4905` over TCP | same feeder data | no feeder section; no connection | same feeder opt-in / disable flow | +| `avdelphi` feeder | `data.avdelphi.com:24999` over TCP | same feeder data | no feeder section; no connection | same feeder opt-in / disable flow | +| `adsbexchange` feeder | `feed1.adsbexchange.com:30004` over TCP | same feeder data | no feeder section; no connection | same feeder opt-in / disable flow | +| `flyrealtraffic` feeder | `feed.flyrealtraffic.com:30004` over TCP | same feeder data | no feeder section; no connection | same feeder opt-in / disable flow | +| Custom feeder | user-supplied host and TCP port | received Mode-S/ADS-B stream and station UUID when configured | no feeder section; no connection | add with the custom preset and explicitly enable; disable or remove with `readsb-feeder` | + +`readsb-geoip --self-test` is also an explicit network operation: it +tests DNS and HTTPS access to both GeoIP providers. The optional +`adsbexchange-stats` companion is a separate package and is not +installed or started by default; consult that package's documentation +before enabling its external statistics upload. `readsb-feeder --url` +only prints a dashboard URL and does not fetch it. + +For non-interactive configuration: + +```sh +# Permit automatic GeoIP only when coordinates are missing: +uci set readsb.main.geoip_auto=1 +uci commit readsb + +# Revoke automatic GeoIP permission: +uci set readsb.main.geoip_auto=0 +uci commit readsb + +# Feeder sections remain inert until enabled: +readsb-feeder --add myfeed adsblol enabled=0 +readsb-feeder --enable myfeed +service readsb reload +``` + +## `/etc/config/readsb` -- main section options + +Only options that need explanation are documented here. The full list +ships in the conffile; setting an option to `''` (empty) means "let +readsb apply its built-in default". Boolean options take `'0'` or `'1'`. + +### Identity and location + +| Option | Default | Notes | +| ----------------- | -------------- | ----------------------------------------------------- | +| `lat`, `lon` | empty | station coordinates; required for CPR position decoding. Set with `readsb-setup`, `readsb-geoip`, or hand-edit. | +| `uuid` | empty | station UUID, shared as the default for every feeder section that doesn't override it. Generate with `readsb-uuid`. | +| `uuid_file` | empty | path readsb reads at startup and applies to every uuid-capable output that doesn't carry an embedded `uuid=`. Independent of `option uuid`; setting only the file is fine if you don't use `config feeder` sections. | + +### Boot-time waits + +| Option | Default | Notes | +| ---------------------- | ------- | ------------------------------------------------------ | +| `geoip_auto` | `0` | opt in to automatic public-IP geolocation when an enabled section has empty coordinates. This contacts `ipapi.co`, falling back to `ipwho.is`. Manual `readsb-geoip` use is always available. | +| `geoip_wait_timeout` | `60` | seconds the init script blocks until `readsb-geoip` resolves lat/lon when `geoip_auto` is enabled. Covers the cold-boot case where WAN isn't ready when `START=90` fires. Set to `0` for a single-shot lookup. | +| `geoip_wait_interval` | `10` | poll interval for the geoip wait loop. | +| `usb_wait_timeout` | `0` | seconds the init script blocks until a USB SDR appears in `/sys`. Default `0` (off) so net-only deployments pay no boot cost. Only honored when `option hotplug 1` is set. | +| `usb_wait_interval` | `2` | poll interval for the USB wait loop. | + +### SDR / RF + +The interactive way to set these on a unit with an attached RTL-SDR is +step 3 of `readsb-setup` (auto-skipped on net-only units). Hand-editing +also works. + +| Option | Default | Notes | +| ----------------- | ------- | ----------------------------------------------------- | +| `gain` | `auto` | `auto`, `max`, or a numeric dB value (`0`..`50`) | +| `device` | empty | RTL-SDR serial; auto-filled by the USB hotplug handler | +| `device_type` | empty | `rtlsdr`, `bladerf`, etc. (only `rtlsdr` is supported in this build) | +| `freq` | empty | center frequency; defaults to 1090 MHz | +| `ppm` | `0` | tuner PPM correction | +| `enable_agc`, `enable_biastee` | `0` | hardware-side toggles | + +### Network + +| Option | Default | Notes | +| ------------------- | ---------------- | ---------------------------------------------- | +| `net` | `1` | enable network I/O | +| `net_only` | `1` | run without an SDR (network-only consumer) | +| `net_bind_address` | `127.0.0.1` | listen only on this router by default; set an appropriate LAN address explicitly to permit remote clients | +| `net_bi_port` | `30004,30104` | inbound BEAST | +| `net_bo_port` | `30005` | outbound BEAST -- this is what you point external feeder clients (e.g. `piaware`, `fr24feed`) at; reachable from other hosts only once `net_bind_address` is widened | +| `net_ri_port` | `30001` | inbound raw | +| `net_ro_port` | `30002` | outbound raw | +| `net_sbs_port` | `30003` | outbound SBS/BaseStation | +| `net_beast_reduce_out_port` | `30006` | outbound reduced BEAST | + +### Periodic stats block + +The daemon emits a multi-line health/status summary into syslog on a +fixed interval. readsb writes this to stderr; procd routes stderr at +`daemon.err` on stock OpenWrt -- this matches dnsmasq/hostapd/ntpd +convention, not a severity claim. Filter with: + +```sh +logread -e readsb +``` + +| Option | Default | Notes | +| -------------- | ------- | -------------------------------------------------- | +| `stats` | `1` | set to `'0'` to silence the periodic block | +| `stats_every` | `120` | cadence in seconds (sensible range 60..900) | +| `stats_range` | `0` | set to `'1'` to add the per-range histogram | + +### `extra_args` + +Passthrough for upstream readsb flags not surfaced as a UCI option, e.g. +the camelCase `--write-binCraft-old` / `--write-json-binCraft-only=`, +`--dump-beast=,,`, `--receiver-focus`, +`--cpr-focus`, `--leg-focus`, `--trace-focus`, `--aggressive`. +Whitespace-separated; appended verbatim to the daemon command line. + +## SDR / hotplug behavior + +The USB hotplug handler (`/etc/hotplug.d/usb/30-readsb`) auto-configures +the first `config readsb` section on RTL-SDR plug/unplug events. To +opt a section out (e.g. for a hand-managed multi-SDR setup), set: + +``` +option hotplug '0' +``` + +The handler reacts to USB IDs from librtlsdr's `known_devices[]` table +and pins the section to the dongle's serial; on the last RTL-SDR being +removed, it switches the section back to `net_only=1`. + +### Single-SDR setup + +No hardware configuration is required. Plug the RTL-SDR in; the handler +sets `device_type=rtlsdr`, `device=`, and `net_only=0`, then +restarts the service if it is enabled. It preserves `enabled=0` as an +administrative choice. Unplug -> reverts to net-only. + +### Multi-SDR setup + +If two or more RTL-SDRs are attached to the same router (e.g. one for +1090 MHz ADS-B and one for 978 MHz UAT), the auto-pin needs to know +which dongle is which. The package follows the **wiedehopf / FlightAware +convention**: label each dongle with its target frequency in MHz via +`rtl_eeprom`, then the handler pins by exact `serial == freq` match: + +```sh +opkg install rtl-sdr # provides rtl_eeprom +# With ONLY the 1090 dongle plugged in: +rtl_eeprom -s 1090 +# Unplug, plug ONLY the 978 dongle, then: +rtl_eeprom -s 978 +# Now both can be plugged in; the handler will pin each section by freq. +``` + +The section's `option freq` (in Hz, MHz, or `1090MHz`-style) selects +which serial it claims. If no serial matches, the handler logs the +available serials at warn level and leaves `option device` empty so +you can set it manually. + +## Boot-time behavior + +The init script (`START=90`) handles three pre-flight conditions before +spawning the daemon: + +1. **USB-settle wait** -- only when at least one section sets + `option hotplug 1` and `option usb_wait_timeout` is non-zero. Polls + `/sys/bus/usb/devices/` every `usb_wait_interval` seconds until an + RTL-SDR appears or the timeout elapses. Off by default so net-only + units pay no boot cost. +2. **No-USB reconciliation** -- if a section is hotplug-managed and + no RTL-SDR is attached at boot, the section is normalized back to + net-only (clears `device_type` / `device`, sets `net_only=1`) before + the daemon starts. Avoids a stale `device=` from a + no-longer-attached dongle blocking startup. +3. **Geoip wait** -- if `geoip_auto` is enabled and any enabled section + has empty `lat`/`lon`, polls until `/usr/sbin/readsb-geoip` succeeds + or `geoip_wait_timeout` elapses. Avoids the cold-boot race where + WAN isn't routable when `START=90` fires. + +Already-attached USB devices are also re-injected into the hotplug +handler (with `READSB_HOTPLUG_SEED=1` to suppress the recursive +restart) so the boot path produces the same UCI state as a live +plug-in event. + +## Aggregator feeders + +Each enabled `config feeder ''` section becomes one outbound +`--net-connector` line on the readsb command line. There is no priority +ordering; every enabled feeder gets the same decoded message stream. + +Strategy: + +* **zero feeders** -- daemon still listens on `net_bi_port`/`net_bo_port` + etc. but does not push to any aggregator +* **one feeder** -- single aggregator +* **many feeders** -- parallel push to several aggregators (one outbound + TCP connection per enabled section). No upper limit beyond memory and + uplink bandwidth. + +To temporarily mute a feeder without losing the section: + +```sh +readsb-feeder --disable +service readsb reload +``` + +### Built-in presets + +Hosts and ports are baked in -- check syslog after enabling, endpoints +can change without notice. All presets use protocol `beast_reduce_plus_out`. + +| Preset | Endpoint | +| ---------------- | ----------------------------------- | +| `adsblol` | `in.adsb.lol:30004` | +| `airplaneslive` | `feed.airplanes.live:30004` | +| `adsbfi` | `feed.adsb.fi:30004` | +| `planespotters` | `feed.planespotters.net:30004` | +| `theairtraffic` | `feed.theairtraffic.com:30004` | +| `flyitaly` | `dati.flyitalyadsb.com:4905` | +| `avdelphi` | `data.avdelphi.com:24999` | +| `adsbexchange` | `feed1.adsbexchange.com:30004` | +| `flyrealtraffic` | `feed.flyrealtraffic.com:30004` | + +For anything else, use `preset 'custom'` and supply `host`, `port`, +and (optionally) `protocol`. Run `readsb-feeder --presets` on the device +to dump the live list. + +### Adding a feeder + +Interactive (recommended -- prompts for everything, validates as you go): + +```sh +readsb-feeder +service readsb reload +``` + +Non-interactive (scriptable): + +```sh +# from a preset +readsb-feeder --add adsblol adsblol enabled=1 silent_fail=1 + +# custom endpoint +readsb-feeder --add mycustom custom \ + host=feed.example.com port=30004 protocol=beast_reduce_plus_out \ + enabled=1 silent_fail=1 +service readsb reload +``` + +Other useful commands -- run `readsb-feeder -h` for the full list. All +commands are `--flag` style (matching `readsb-setup --status` / +`--config` / `--help`): + +```sh +readsb-feeder --list # show all sections + resolved endpoints +readsb-feeder --show # dump one section +readsb-feeder --probe # TCP-probe each enabled feeder host:port +readsb-feeder --url # public stats URL (where one is published) +readsb-feeder --companions # optional companion package(s) per enabled feeder +readsb-feeder --companions

# ... or for one specific preset +readsb-feeder --examples # ready-to-paste UCI blocks for scripted setups +readsb-feeder --set =... +readsb-feeder --enable +readsb-feeder --disable +readsb-feeder --remove +``` + +When a preset has an optional companion package (currently +`adsbexchange` only), the wizard prints the install command before +asking for confirmation, and `readsb-feeder --add` / `--enable` log the +same recommendation to syslog -- so headless setups see it too via +`logread -e readsb`. + +### `silent_fail` semantics + +Every feeder section accepts `option silent_fail '0'|'1'`, default `'1'`. +When set, brief connection failures (DNS hiccup, aggregator-side +restart, transient network drop) are retried silently. When unset, +each failed connection attempt produces a log line on the daemon's +stderr stream (visible via `logread -e readsb`). + +Keep the default unless you're actively debugging a feeder that won't +stay connected. + +### Per-feeder UUID override + +UUID resolution order per section: + +1. `option uuid` on the section -- use to give one aggregator its own + identity (e.g. you registered separately at adsbexchange) +2. `option uuid` in the readsb `main` section -- the common case, one + station UUID shared across all aggregators +3. omitted -- the aggregator de-dupes by source IP only + +### adsbexchange supplemental stats + +Feeding to ADSBx works on its own from the `adsbexchange` preset. The +optional supplemental stats uploader (rssi/throttled telemetry posted to +`/api/receive`, used only for the per-station ranking on the web +dashboard) is a **separate** companion package: + +```sh +opkg install adsbexchange-stats +``` + +It hard-depends on this package, reads `option uuid` from the readsb +`main` section, and reads `aircraft.json` from readsb's run dir. +Install only if you want the dashboard ranking; pure feeding does not +need it. + +The public per-UUID lookup URL is printed by +`readsb-feeder --url adsbexchange` whether the uploader is installed or +not. When the uploader **is** installed it also exposes the same URL +via its own `/etc/init.d/adsbexchange-stats showurl` action and a +project-info banner via `/etc/init.d/adsbexchange-stats info`. Both +appear on the companion package's `controls` line in +`readsb-setup --status` and `readsb-setup --help`. opkg has no +`Recommends`/`Suggests` field, so the link between the two packages +is one-way: `adsbexchange-stats` DEPENDS on `readsb-wiedehopf`, not +the other way around. + +### adsblol map + +Map redirect by source IP: + +``` +https://api.adsb.lol/0/my +``` + +Printed by `readsb-feeder --url adsblol`. Other Family A aggregators +publish dashboards keyed on the source IP you signed up with -- consult +each aggregator's website for the specific URL. + +## Out-of-scope aggregators + +These aggregators do **not** accept a raw BEAST push from readsb. They +require their own vendor feeder client which performs aggregator-specific +station registration, protocol framing, and MLAT: + +| Aggregator | Vendor client | Notes | +| --------------- | ------------- | -------------------------------------------------- | +| FlightAware | `piaware` | open-source TCL; FA-managed claim flow, own per-station feeder-id, bundled mlat-client. Not packaged for OpenWrt; runs on a separate host. | +| FlightRadar24 | `fr24feed` | closed binary, FR24-supplied builds. | +| RadarBox | `rbfeeder` | closed binary. | +| Planefinder | `pfclient` | closed binary. | +| AussieADSB | (interactive) | enrolment is per-station; port varies. | + +To feed those, leave them off here and run their official client on +another host pointed at this readsb's BEAST output (`net_bo_port`, +default `30005`). Each vendor client uses its **own** station ID -- +the `option uuid` in this package does NOT carry over to FlightAware's +feeder-id, FR24's sharing-key, etc. + +## MLAT + +Out of scope for this package. MLAT requires a separate `mlat-client` +process. The aggregators above each advertise an MLAT endpoint on +`mlat.:31090` (or `:31090` on the same host) -- consult the +aggregator's own docs. + +## Logging and diagnostics + +All script-side and daemon-side logging goes to syslog under the tag +`readsb` (and `readsb-geoip` for the geolocation helper). View with: + +```sh +logread -e readsb +``` + +To also persist logs to a file and/or forward them to a remote syslog +server, configure system-wide logging (this is the OpenWrt convention -- +packages don't impose log routing). Examples: + +```sh +# Persist to a file (rotated by busybox at log_size KiB): +uci set system.@system[0].log_file=/var/log/messages +uci set system.@system[0].log_size=200 +uci commit system && /etc/init.d/log restart + +# Mirror to a remote syslog server: +uci set system.@system[0].log_ip='192.0.2.10' +uci set system.@system[0].log_port='514' +uci set system.@system[0].log_proto='udp' +uci commit system && /etc/init.d/log restart +``` + +To raise script-side verbosity (debug-level lines from this package): + +```sh +uci set system.@system[0].log_level='debug' +uci commit system && /etc/init.d/log restart +``` + +Diagnostic helpers: + +```sh +readsb-setup --status # at-a-glance state report (rc 0/1) + # also checks optional companion packages +readsb-setup --stats # most recent stats block only + # (rc 0 = printed, 2 = down, 3 = no block yet) +readsb-setup --config # pretty-printed /etc/config/readsb dump + # + /etc/config/ for each installed companion +readsb-setup --help # re-print the post-install welcome banner +readsb-feeder --list # feeder sections + resolved endpoints +readsb-feeder --probe # TCP-probe each enabled feeder +readsb-feeder --companions # optional companion package(s) per feeder +readsb-geoip --self-test # read-only PASS/FAIL diagnostic +service readsb status # procd status +``` + +### Log levels + +All script-side logging follows RFC 5424 / OpenWrt severity convention. +Filter with `logread -p ` or by reading the `daemon.` +facility: + +| Level | Used for | +| -------- | ------------------------------------------------------------------- | +| `err` | hard failure that aborted the operation (UCI commit failed, no UUID source, geoip self-test FAILs, hotplug commit/restart failure) | +| `warn` | recoverable issue / degraded mode (feeder unreachable, geoip provider returned no coords, hotplug detected blocking kernel module, no SDR matched freq, geoip wait timed out) | +| `notice` | significant operator event (config mutation committed, service started, mode flip net-only<->SDR, UUID written, hotplug auto-pinned a dongle) | +| `info` | routine progress (feeder probe summary OK, geoip lookup result, init waits) | +| `debug` | trace; only visible with `system.@system[0].log_level=debug`. Per-feeder routing detail at startup, UCI load traces, geoip fallback flow | + +The daemon itself emits its periodic stats block on stderr; procd routes +that to `daemon.err` on stock OpenWrt. The severity tag is OpenWrt's +routing convention, **not** a severity claim from the daemon -- silence +the block with `option stats '0'` if it gets noisy. + +## Companion packages + +Not pulled in automatically (opkg has no `Recommends` field): + +* **adsbexchange-stats** -- optional ranking-dashboard stats uploader + for ADSBx. Hard-depends on this package; only install if you want + ADSBx's per-station web ranking. + +`readsb-setup --status` and `readsb-feeder --companions` walk every +*enabled* feeder section, look up the recommended companion package(s) +for each preset, and report whether each one is **installed** and +(when it ships an init script) **running**. Missing or stopped +packages are printed with the exact command to install / start them +and are mirrored to syslog so they also appear in +`logread -e readsb` for headless setups. + +## Conflicts + +This package `PROVIDES:=readsb` and `CONFLICTS:=readsb` (likewise for +`viewadsb`). Either this package or the upstream `readsb` package can +satisfy a `readsb` dependency, but the two cannot be installed +side-by-side. diff --git a/utils/readsb-wiedehopf/files/readsb-feeder b/utils/readsb-wiedehopf/files/readsb-feeder new file mode 100644 index 00000000000000..f54634d8c7db3f --- /dev/null +++ b/utils/readsb-wiedehopf/files/readsb-feeder @@ -0,0 +1,886 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-only +# Copyright (C) 2026 OpenWrt.org + +# readsb-feeder -- inspect / operate aggregator feeder sections. +# +# Adds, removes, enables, disables and probes per-aggregator outbound +# `config feeder ''` sections in /etc/config/readsb. Each enabled +# section becomes one outbound TCP connection at daemon startup. +# +# Usage: +# readsb-feeder -- interactive feeder wizard +# +# Read-only inspection (no UCI changes): +# readsb-feeder --list -- show all 'config feeder' sections +# readsb-feeder --show -- dump one section +# readsb-feeder --url [] -- public stats URL(s) per feeder +# readsb-feeder --probe [] -- TCP-probe each enabled feeder +# readsb-feeder --health [] -- per-feeder runtime health +# (probe + cmdline + log scan) +# readsb-feeder --presets -- list built-in preset names +# readsb-feeder --examples -- sample feeder UCI blocks +# readsb-feeder --companions [] -- optional companion package(s) +# +# Mutating (commits to UCI; does NOT restart the daemon): +# readsb-feeder --add [k=v]... +# readsb-feeder --set =... +# readsb-feeder --enable +# readsb-feeder --disable +# readsb-feeder --remove +# +# readsb-feeder -h, --help -- this options banner +# +# After any mutation: `service readsb reload`. +# +# Exit codes: +# 0 success (--probe / --health: ALL targets reachable; --health also +# requires LIVE in cmdline AND no recent errors) +# 1 usage error / unknown option +# 2 one or more targets unreachable / DEGRADED / mutation failed +# 3 no feeder sections defined / named section not found +# +# Full reference: +# https://github.com/openwrt/packages/blob/master/utils/readsb-wiedehopf/README.md + +# -h / --help must work before sourcing functions.sh so help is reachable +# when the package is staged but not yet installed. +case ${1:-} in + -h|--help) + sed -n '/^# readsb-feeder /,/^$/p' "$0" | sed 's/^# \{0,1\}//' + exit 0 + ;; +esac + +# shellcheck source=files/readsb.functions.sh +. /usr/lib/readsb/functions.sh +# shellcheck source=/dev/null +. /lib/functions.sh + +READSB_LOG_TAG=readsb-feeder + +# Stderr message shared by `url` and `probe` when nothing matched. +_no_match() { + local want=$1 verb=$2 + if [ -n "$want" ]; then + echo "no feeder named '$want' found (or it is disabled)" >&2 + else + echo "no enabled feeder sections${verb:+ to $verb}" >&2 + fi +} + +# Post-mutation message every mutating command emits (notice + stdout). +_emit_mutation() { + local what=$1 + _notice "$what" + echo "$what; run 'service readsb reload' to apply" +} + +# Commit pending UCI; fail with a uniform error and rc 2. +_commit() { + uci -q commit readsb || { echo "readsb-feeder: uci commit failed" >&2; return 2; } +} + +# --- command: --list ------------------------------------------------------- +cmd_list() { + local main_uuid total=0 + main_uuid=$(uci -q get readsb.main.uuid) + + printf '%-16s %-6s %-12s %-32s %-5s %s\n' \ + NAME ENABLED PRESET HOST PORT PROTOCOL + + _emit() { + local fcfg=$1 + if readsb_feeder_resolve "$fcfg" "$main_uuid"; then + printf '%-16s %-6s %-12s %-32s %-5s %s\n' \ + "$fcfg" "$readsb_feeder_enabled" "$readsb_feeder_preset" \ + "$readsb_feeder_host" "$readsb_feeder_port" "$readsb_feeder_proto" + else + printf '%-16s %-6s %-12s %-32s %-5s %s\n' \ + "$fcfg" "$readsb_feeder_enabled" "${readsb_feeder_preset:-?}" \ + "" "?" "?" + fi + total=$((total + 1)) + } + + config_load readsb + config_foreach _emit feeder + + if [ "$total" -eq 0 ]; then + echo "no feeder sections defined" >&2 + echo "add one interactively: readsb-feeder" >&2 + echo "or non-interactively: readsb-feeder --add " >&2 + return 3 + fi + return 0 +} + +# --- command: --url -------------------------------------------------------- +cmd_url() { + local want=$1 main_uuid printed=0 missing=0 + main_uuid=$(uci -q get readsb.main.uuid) + + _one() { + local fcfg=$1 url + [ -z "$want" ] || [ "$want" = "$fcfg" ] || return 0 + readsb_feeder_resolve "$fcfg" "$main_uuid" || return 0 + [ "$readsb_feeder_enabled" -eq 1 ] || [ -n "$want" ] || return 0 + if url=$(readsb_feeder_status_url "$readsb_feeder_preset" "$readsb_feeder_uuid"); then + printf '%-16s %s\n' "$fcfg" "$url" + printed=$((printed + 1)) + else + printf '%-16s (no public per-UUID stats URL published)\n' "$fcfg" >&2 + missing=$((missing + 1)) + fi + } + + config_load readsb + config_foreach _one feeder + + if [ "$printed" -eq 0 ] && [ "$missing" -eq 0 ]; then + _no_match "$want" + return 3 + fi + [ "$missing" -gt 0 ] && return 2 + return 0 +} + +# --- command: --probe ------------------------------------------------------ +# TCP reachability probe. The `/dev/tcp//` virtual device +# is a bash/ksh feature that BusyBox ash does NOT implement, so on a +# stock OpenWrt `/bin/sh` (busybox-ash) the redirection silently fails. +# The /dev/tcp branch is kept because it costs nothing (it's just an +# `exec` redirection) and lets ash environments that have been swapped +# for bash -- or callers running this helper directly under bash -- +# short-circuit without forking nc. +# `timeout`, when present, bounds that connect attempt to 3s; without it +# the kernel's own (much longer) connect timeout applies. `nc -w` is +# also optional in BusyBox; only use nc when its help advertises that +# bounded-connect option. When neither a bounded /dev/tcp attempt nor a +# bounded nc is available, the probe returns rc 2 ("can't tell") rather +# than reporting a false unreachable result. +# +# Some upstream firewalls (notably feed1.adsbexchange.com:30004) drop +# SYN+close drive-by probes while accepting persistent feeder streams; +# the caller therefore checks the kernel's actual socket state via +# `readsb_connector_active` first and only falls through here when no +# live socket exists. +_probe_tcp() { + local host=$1 port=$2 + if command -v timeout >/dev/null 2>&1; then + if (timeout 3 sh -c 'exec 9<>"/dev/tcp/$1/$2"' _ "$host" "$port") 2>/dev/null; then + return 0 + fi + elif (exec 9<>"/dev/tcp/$host/$port") 2>/dev/null; then + exec 9<&- 9>&- + return 0 + fi + command -v nc >/dev/null 2>&1 || return 2 + nc --help 2>&1 | grep -q -- '-w' || return 2 + nc -w 3 "$host" "$port" /dev/null 2>&1 +} + +cmd_probe() { + local want=$1 main_uuid bad=0 skipped=0 probed=0 rsb_pid + main_uuid=$(uci -q get readsb.main.uuid) + rsb_pid=$(readsb_pid) + + _one() { + local fcfg=$1 rc + [ -z "$want" ] || [ "$want" = "$fcfg" ] || return 0 + readsb_feeder_resolve "$fcfg" "$main_uuid" || return 0 + if [ "$readsb_feeder_enabled" -ne 1 ]; then + [ -n "$want" ] || return 0 + printf '%-16s DISABLED %s:%s (no probe sent)\n' \ + "$fcfg" "$readsb_feeder_host" "$readsb_feeder_port" + probed=$((probed + 1)) + return 0 + fi + probed=$((probed + 1)) + + # Active-socket-first: if the kernel shows readsb has a live + # ESTABLISHED socket to this remote, we have our answer without + # sending any new packets. + if [ -n "$rsb_pid" ]; then + readsb_connector_active \ + "$readsb_feeder_host" "$readsb_feeder_port" "$rsb_pid" + rc=$? + case $rc in + 0) + printf '%-16s LIVE %s:%s (readsb has active socket -- no probe sent)\n' \ + "$fcfg" "$readsb_feeder_host" "$readsb_feeder_port" + return 0 + ;; + esac + fi + + # No attributable active socket -- use a bounded probe to distinguish + # "configured but not yet connected" from "unreachable". + _probe_tcp "$readsb_feeder_host" "$readsb_feeder_port" + rc=$? + case $rc in + 0) printf '%-16s OK %s:%s\n' "$fcfg" "$readsb_feeder_host" "$readsb_feeder_port" ;; + 2) printf '%-16s SKIP %s:%s (no supported bounded TCP probe and no attributable active socket)\n' \ + "$fcfg" "$readsb_feeder_host" "$readsb_feeder_port" >&2 + _warn "probe '$fcfg' skipped: no supported bounded TCP probe for $readsb_feeder_host:$readsb_feeder_port" + skipped=$((skipped + 1)) ;; + *) printf '%-16s FAIL %s:%s\n' "$fcfg" "$readsb_feeder_host" "$readsb_feeder_port" >&2 + _warn "probe '$fcfg' unreachable: $readsb_feeder_host:$readsb_feeder_port" + bad=$((bad + 1)) ;; + esac + } + + config_load readsb + config_foreach _one feeder + + if [ "$probed" -eq 0 ]; then + _no_match "$want" probe + return 3 + fi + # An indeterminate result (SKIP) is not a failure -- it means no bounded + # probe tool was available, not that the feeder is unreachable. Keeping + # SKIP out of $bad lets --probe stay usable as a scripted health check + # that only exits nonzero on a confirmed FAIL, even on stock firmware + # where no bounded probe tool exists. + if [ "$bad" -gt 0 ]; then + if [ "$skipped" -gt 0 ]; then + _warn "probe summary: $bad of $probed feeder(s) failed, $skipped indeterminate" + else + _warn "probe summary: $bad of $probed feeder(s) failed" + fi + return 2 + fi + if [ "$skipped" -gt 0 ]; then + _log "probe summary: $probed feeder(s) checked, $skipped indeterminate (no bounded probe tool)" + return 0 + fi + _log "probe summary: $probed feeder(s) checked" + return 0 +} + +# --- command: --health ----------------------------------------------------- +# Per-feeder runtime check. Combines four signals into one state token +# per feeder: +# 1) TCP drive-by probe (can false-negative on firewalls) +# 2) presence in the live daemon's --net-connector cmdline +# 3) kernel-side ESTABLISHED socket owned by readsb pid with matching +# remote port (ground truth -- catches probe false-negatives) +# 4) recent error-event count for host:port in syslog +# +# States: +# LIVE in cmdline AND (probe ok OR active socket), 0 errors +# DEGRADED LIVE-shaped but with recent errors (or no probe tool + +# no active socket) +# UNREACHABLE in cmdline AND probe FAIL AND no active socket +# NOT-LOADED in UCI (enabled=1) but missing from cmdline +# -- run `service readsb reload` +# DISABLED enabled=0 in UCI; only shown when explicitly named +# +# Without : walks every section; exit 0 only when every enabled +# feeder is LIVE; exit 2 on any DEGRADED/UNREACHABLE/NOT-LOADED. +# With : same but for one section; rc 3 when doesn't exist. +cmd_health() { + local want=$1 main_uuid checked=0 bad=0 + main_uuid=$(uci -q get readsb.main.uuid) + + # Daemon-level preface: if not running, every feeder is by + # definition not live. Bail with rc 2. + local rsb_pid + rsb_pid=$(readsb_pid) + if [ -z "$rsb_pid" ]; then + echo "readsb-feeder --health: daemon not running" >&2 + echo " fix: /etc/init.d/readsb start" >&2 + echo " see: readsb-setup --health (full daemon-down report)" >&2 + return 2 + fi + + printf '%-16s %-11s %-32s %s\n' NAME STATE 'HOST:PORT' DETAILS + + _one_health() { + local fcfg=$1 rc state details errors hostport connector_live=0 active=0 + [ -z "$want" ] || [ "$want" = "$fcfg" ] || return 0 + readsb_feeder_resolve "$fcfg" "$main_uuid" || { + # Malformed section -- still report it. + printf '%-16s %-11s %-32s %s\n' \ + "$fcfg" 'MALFORMED' '?:?' "${readsb_feeder_error:-?}" >&2 + bad=$((bad + 1)) + checked=$((checked + 1)) + return 0 + } + hostport="$readsb_feeder_host:$readsb_feeder_port" + + # Disabled sections only when the operator asked for that name. + if [ "$readsb_feeder_enabled" -ne 1 ]; then + [ -n "$want" ] || return 0 + printf '%-16s %-11s %-32s %s\n' \ + "$fcfg" 'DISABLED' "$hostport" 'enabled=0 in UCI' + checked=$((checked + 1)) + return 0 + fi + + checked=$((checked + 1)) + + # Prefer the daemon's kernel socket state. When several connectors + # share a port, fall back to the bounded host-specific probe. + if readsb_connector_live \ + "$readsb_feeder_host" "$readsb_feeder_port"; then + connector_live=1 + readsb_connector_active \ + "$readsb_feeder_host" "$readsb_feeder_port" "$rsb_pid" + case $? in + 0) active=1; rc=0 ;; + 1) _probe_tcp "$readsb_feeder_host" "$readsb_feeder_port"; rc=$? ;; + 2) active=2; _probe_tcp "$readsb_feeder_host" "$readsb_feeder_port"; rc=$? ;; + esac + else + _probe_tcp "$readsb_feeder_host" "$readsb_feeder_port" + rc=$? + fi + errors=$(readsb_log_count_errors_for \ + "$readsb_feeder_host" "$readsb_feeder_port") + + if [ "$connector_live" = 1 ]; then + # In cmdline. Authoritative liveness comes from the kernel: + # does readsb actually have an ESTABLISHED socket to that + # remote port right now? If yes, the feed IS up regardless + # of what the drive-by probe said. + case $rc in + 0) + if [ "${errors:-0}" -gt 0 ]; then + state=DEGRADED + details="$errors recent error event(s) -- see logread -e readsb" + bad=$((bad + 1)) + else + state=LIVE + if [ "$active" = 1 ]; then + details='cmdline ok, socket active, no probe sent, log clean' + else + details='cmdline ok, tcp ok, log clean' + fi + fi + ;; + 2) + if [ "$active" = 2 ]; then + state=DEGRADED + details='socket attribution indeterminate and no supported bounded TCP probe is available' + bad=$((bad + 1)) + elif [ "$active" = 1 ]; then + if [ "${errors:-0}" -gt 0 ]; then + state=DEGRADED + details="socket active; $errors recent error event(s)" + bad=$((bad + 1)) + else + state=LIVE + details='probe unavailable; readsb has active socket (kernel /proc check)' + fi + else + state=DEGRADED + details='probe skipped (no supported bounded TCP probe) and no active socket' + bad=$((bad + 1)) + fi + ;; + *) + # Drive-by probe FAILED. Cross-check kernel state. + if [ "$active" = 1 ]; then + if [ "${errors:-0}" -gt 0 ]; then + state=DEGRADED + details="probe blocked but socket active; $errors recent error(s)" + bad=$((bad + 1)) + else + state=LIVE + details='active socket in /proc/net/tcp (drive-by probe blocked at upstream)' + fi + else + state=UNREACHABLE + details='cmdline ok, TCP probe FAILED, no active socket' + bad=$((bad + 1)) + fi + ;; + esac + else + # Not in cmdline -- the daemon hasn't loaded this connector. + # Probe result still useful (is the upstream alive at all?). + case $rc in + 0) details='upstream reachable -- run: service readsb reload' ;; + 2) details='probe skipped (no supported bounded TCP probe)' ;; + *) details='upstream unreachable AND not in cmdline' ;; + esac + state=NOT-LOADED + bad=$((bad + 1)) + fi + + printf '%-16s %-11s %-32s %s\n' "$fcfg" "$state" "$hostport" "$details" + + # When DEGRADED, surface the most recent matching log line so + # the operator doesn't have to grep syslog manually. + if [ "$state" = DEGRADED ] && [ "${errors:-0}" -gt 0 ]; then + local last + last=$(readsb_log_last_error_for \ + "$readsb_feeder_host" "$readsb_feeder_port") + [ -n "$last" ] && printf '%-16s %s\n' '' " last: $last" + fi + } + + config_load readsb + config_foreach _one_health feeder + + if [ "$checked" -eq 0 ]; then + _no_match "$want" check + return 3 + fi + if [ "$bad" -gt 0 ]; then + _warn "health summary: $bad of $checked feeder(s) DEGRADED / UNREACHABLE / NOT-LOADED" + return 2 + fi + _log "health summary: $checked feeder(s) LIVE" + return 0 +} + +# --- command: --presets ---------------------------------------------------- +cmd_presets() { + local p + for p in $(readsb_feeder_presets); do + # shellcheck disable=SC2046 + set -- $(readsb_feeder_preset "$p") + printf '%-16s %s:%s (%s)\n' "$p" "$1" "$2" "$3" + done + printf '%-16s (user-supplied host/port/protocol)\n' custom +} + +# --- command: --companions ------------------------------------------------- +# Show optional companion package(s) for a single preset, or for every +# enabled feeder when called without args. Read-only. +cmd_companions() { + local want=$1 reported=0 main_uuid + + if [ -n "$want" ]; then + if [ "$want" != custom ] && ! readsb_is_preset "$want"; then + echo "readsb-feeder --companions: unknown preset '$want'" >&2 + echo " see: readsb-feeder --presets" >&2 + return 1 + fi + if readsb_recommend_optional_pkgs "$want"; then + return 0 + fi + echo "preset '$want' has no optional companion packages" + return 0 + fi + + # All-enabled-feeders mode. + main_uuid=$(uci -q get readsb.main.uuid) + _one() { + local fcfg=$1 + readsb_feeder_resolve "$fcfg" "$main_uuid" || return 0 + [ "$readsb_feeder_enabled" -eq 1 ] || return 0 + [ "$readsb_feeder_preset" = custom ] && return 0 + [ -n "$(readsb_feeder_optional_pkgs "$readsb_feeder_preset")" ] || return 0 + readsb_recommend_optional_pkgs "$readsb_feeder_preset" "feeder '$fcfg'" + echo + reported=$((reported + 1)) + } + + config_load readsb + config_foreach _one feeder + + if [ "$reported" -eq 0 ]; then + echo "no enabled feeder sections have optional companion packages" + echo "(see: readsb-feeder --companions , or readsb-feeder --presets)" + fi + return 0 +} + +# --- command: --examples --------------------------------------------------- +# Print sample feeder UCI blocks for paste/redirection / scripted setups. +cmd_examples() { + local p + cat <<'EOF' +# Sample feeder UCI blocks. Pipe into uci-batch for a scripted setup, +# or copy/paste into your provisioning system. For interactive use +# prefer `readsb-feeder` (no args) or `readsb-setup`. + +EOF + for p in $(readsb_feeder_presets); do + printf "config feeder '%s'\n" "$p" + printf "\toption enabled '0'\n" + printf "\toption preset '%s'\n" "$p" + printf "\toption silent_fail '1'\n\n" + done + cat <<'EOF' +config feeder 'mycustom' + option enabled '0' + option preset 'custom' + option host 'feed.example.com' + option port '30004' + option protocol 'beast_reduce_plus_out' +EOF +} + +# --- command: --show ------------------------------------------------------- +cmd_show() { + local name=$1 + if [ -z "$name" ]; then + echo "usage: readsb-feeder --show " >&2 + return 1 + fi + if ! readsb_feeder_section_exists "$name"; then + echo "no feeder section named '$name'" >&2 + return 3 + fi + uci show "readsb.${name}" +} + +# --- mutating helpers ------------------------------------------------------ +# Whitelist of options accepted by `add` and `set`. +_feeder_opt_ok() { + case $1 in + enabled|preset|host|port|protocol|uuid|silent_fail) return 0 ;; + esac + return 1 +} + +# Validate a `key=value` token; sets $key/$val on success. +_parse_kv() { + case $1 in + *=*) ;; + *) echo "readsb-feeder: not a key=value token: '$1'" >&2; return 1 ;; + esac + key=${1%%=*} + val=${1#*=} + if ! _feeder_opt_ok "$key"; then + echo "readsb-feeder: option '$key' not allowed on feeder sections" >&2 + echo " allowed: enabled preset host port protocol uuid silent_fail" >&2 + return 1 + fi + case $key in + preset) + if [ "$val" != custom ] && ! readsb_is_preset "$val"; then + echo "readsb-feeder: unknown preset '$val'" >&2 + return 1 + fi ;; + host) + if ! wiz_v_host "$val" || [ "${val#*,}" != "$val" ]; then + echo "readsb-feeder: invalid host '$val'" >&2 + return 1 + fi ;; + protocol) + case $val in + ''|*[!A-Za-z0-9_+-]*) + echo "readsb-feeder: invalid protocol '$val'" >&2 + return 1 ;; + esac ;; + enabled|silent_fail) + case $val in 0|1) ;; *) + echo "readsb-feeder: '$key' must be 0 or 1 (got '$val')" >&2 + return 1 ;; + esac ;; + port) + case $val in ''|*[!0-9]*) + echo "readsb-feeder: 'port' must be numeric (got '$val')" >&2 + return 1 ;; + esac + if [ "$val" -lt 1 ] || [ "$val" -gt 65535 ]; then + echo "readsb-feeder: 'port' out of range 1..65535" >&2 + return 1 + fi ;; + uuid) + if [ -n "$val" ] && ! readsb_is_uuid "$val"; then + echo "readsb-feeder: 'uuid' is not RFC4122 (8-4-4-4-12 hex)" >&2 + return 1 + fi ;; + esac + return 0 +} + +# --- command: --add -------------------------------------------------------- +cmd_add() { + # busybox-ash exits the whole script on `shift N` past argv length, + # even with `|| true`. Check first. + if [ $# -lt 2 ]; then + echo "usage: readsb-feeder --add [key=value]..." >&2 + return 1 + fi + local name=$1 preset=$2; shift 2 + local key val have_host=0 have_port=0 + + if ! wiz_v_uci_name "$name"; then + echo "readsb-feeder: invalid section name '$name'" >&2 + echo " must start with a letter, then letters/digits/_ only" >&2 + return 1 + fi + if readsb_section_exists "$name"; then + echo "readsb-feeder: section '$name' already exists" >&2 + echo " use 'readsb-feeder --set $name ...' to modify it" >&2 + return 1 + fi + + if [ "$preset" != custom ] && ! readsb_is_preset "$preset"; then + echo "readsb-feeder: unknown preset '$preset'" >&2 + echo " see: readsb-feeder --presets" >&2 + return 1 + fi + + # Validate every k=v before mutating UCI; track host/port for custom. + for kv in "$@"; do + _parse_kv "$kv" || return 1 + if [ "$key" = preset ]; then + echo "readsb-feeder: pass the preset as the second argument to --add" >&2 + return 1 + fi + [ "$key" = host ] && have_host=1 + [ "$key" = port ] && have_port=1 + done + if [ "$preset" = custom ]; then + if [ "$have_host" -eq 0 ] || [ "$have_port" -eq 0 ]; then + echo "readsb-feeder: 'custom' preset requires host=... and port=..." >&2 + return 1 + fi + fi + + uci -q add readsb feeder >/dev/null || { echo "readsb-feeder: uci add failed" >&2; return 2; } + uci -q rename "readsb.@feeder[-1]=${name}" || { echo "readsb-feeder: uci rename failed" >&2; return 2; } + uci -q set "readsb.${name}.preset=${preset}" + # Default enabled=0 unless the caller overrides via key=value below. + uci -q set "readsb.${name}.enabled=0" + for kv in "$@"; do + _parse_kv "$kv" || return 1 + uci -q set "readsb.${name}.${key}=${val}" + done + _commit || return 2 + _emit_mutation "feeder '$name' added (preset=$preset)" + readsb_warn_companions "$preset" "$name" +} + +# --- command: --set -------------------------------------------------------- +cmd_set() { + if [ $# -lt 2 ]; then + echo "usage: readsb-feeder --set = [=]..." >&2 + return 1 + fi + local name=$1; shift + local key val effective_preset effective_host effective_port + + if ! readsb_feeder_section_exists "$name"; then + echo "readsb-feeder: no section named '$name'" >&2 + return 3 + fi + # Two-pass: validate everything before mutating UCI. + effective_preset=$(uci -q get "readsb.${name}.preset") + effective_host=$(uci -q get "readsb.${name}.host") + effective_port=$(uci -q get "readsb.${name}.port") + for kv in "$@"; do + _parse_kv "$kv" || return 1 + case $key in + preset) effective_preset=$val ;; + host) effective_host=$val ;; + port) effective_port=$val ;; + esac + done + if [ "$effective_preset" = custom ] && \ + { [ -z "$effective_host" ] || [ -z "$effective_port" ]; }; then + echo "readsb-feeder: 'custom' preset requires host=... and port=..." >&2 + return 1 + fi + for kv in "$@"; do + _parse_kv "$kv" || return 1 + if [ -z "$val" ]; then + uci -q delete "readsb.${name}.${key}" + else + uci -q set "readsb.${name}.${key}=${val}" + fi + done + _commit || return 2 + _emit_mutation "feeder '$name' updated: $*" +} + +# --- commands: --enable / --disable --------------------------------------- +cmd_enable_disable() { + local name=$1 want=$2 preset + if [ -z "$name" ]; then + echo "usage: readsb-feeder $( [ "$want" = 1 ] && echo --enable || echo --disable ) " >&2 + return 1 + fi + if ! readsb_feeder_section_exists "$name"; then + echo "readsb-feeder: no section named '$name'" >&2 + return 3 + fi + uci -q set "readsb.${name}.enabled=${want}" + _commit || return 2 + _emit_mutation "feeder '$name' enabled=${want}" + # Surface companion-package warnings on enable; nothing to warn about on disable. + if [ "$want" = 1 ]; then + preset=$(uci -q get "readsb.${name}.preset") + readsb_warn_companions "$preset" "$name" + fi +} + +# --- command: --remove ----------------------------------------------------- +cmd_remove() { + local name=$1 + if [ -z "$name" ]; then + echo "usage: readsb-feeder --remove " >&2 + return 1 + fi + if ! readsb_section_exists "$name"; then + echo "readsb-feeder: no section named '$name' (already gone)" >&2 + return 0 + fi + if ! readsb_feeder_section_exists "$name"; then + echo "readsb-feeder: section '$name' is not a feeder" >&2 + return 3 + fi + uci -q delete "readsb.${name}" || { echo "readsb-feeder: uci delete failed" >&2; return 2; } + _commit || return 2 + _emit_mutation "feeder '$name' removed" +} + +# --- command: bare invocation -> wizard ----------------------------------- +# Interactive feeder builder. Delegates to cmd_add() once values are +# gathered, so validation is identical between the wizard and --add. +cmd_wizard() { + wiz_available || { + echo "readsb-feeder: needs an interactive terminal to run the wizard" >&2 + echo " use 'readsb-feeder --add [k=v]...' instead" >&2 + return 1 + } + + local name preset enabled silent want_uuid uuid host port proto consent_endpoint + local preset_list + preset_list=$(readsb_feeder_presets) + + wiz_say "" + wiz_say "=== readsb-feeder ===" + wiz_say "Add one new aggregator feeder." + wiz_say "Abort at any prompt: 'q', 'quit', 'exit' (or Ctrl-D)." + wiz_say "Aborting before the apply step makes no UCI changes." + wiz_say "" + + # 1. preset + wiz_say "Available presets (host:port baked in):" + wiz_choose preset "choose a preset" $preset_list custom || return 1 + + # 1a. Early heads-up about optional companion package(s); offer to + # install/enable any that are missing or stopped right now. + if [ "$preset" != custom ] \ + && [ -n "$(readsb_feeder_optional_pkgs "$preset")" ]; then + wiz_say "" + wiz_say "--- optional companion package(s) -----------" + readsb_recommend_optional_pkgs "$preset" >/dev/tty + wiz_say "---------------------------------------------" + wiz_offer_install_companions "$preset" || return 1 + wiz_say "" + fi + + # 2. section name -- default to preset name; reject duplicates. + while :; do + wiz_ask_validated name "UCI section name" "$preset" wiz_v_uci_name \ + "name must start with a letter; letters/digits/_ only" || return 1 + if readsb_section_exists "$name"; then + wiz_say " '$name' already exists -- pick another" + continue + fi + break + done + + # 3. custom preset extras + host= port= proto= + if [ "$preset" = custom ]; then + wiz_ask_validated host "feed host" "" wiz_v_host \ + "host must be non-empty and contain no whitespace" || return 1 + wiz_ask_validated port "feed port" "30004" wiz_v_port \ + "port must be a number 1..65535" || return 1 + wiz_ask proto "feed protocol" "beast_reduce_plus_out" || return 1 + fi + + # 4. Third-party connection consent + retry behavior. + if [ "$preset" = custom ]; then + consent_endpoint="$host:$port" + else + # readsb_feeder_preset prints "host port protocol". + set -- $(readsb_feeder_preset "$preset") + consent_endpoint="$1:$2" + fi + wiz_say "" + wiz_say "Enabling this feeder opens a persistent outbound TCP connection" + wiz_say "to $consent_endpoint and sends the received Mode-S/ADS-B stream" + wiz_say "plus the station UUID when one is configured. It is off unless" + wiz_say "you explicitly enable it now or later." + wiz_yesno enabled "enable this third-party connection now?" N || return 1 + wiz_yesno silent "silently retry on connection failures? (recommended)" Y || return 1 + + # 5. per-feeder UUID override (rare). + wiz_yesno want_uuid "override UUID for this feeder only?" N || return 1 + uuid= + if [ "$want_uuid" = 1 ]; then + wiz_ask_validated uuid "per-feeder UUID (RFC4122 8-4-4-4-12 hex)" "" \ + wiz_v_uuid "not RFC4122; expected 8-4-4-4-12 hex" || return 1 + fi + + # 6. summary + confirm + wiz_say "" + wiz_say "--- summary -------------------------------" + wiz_say " section : $name" + wiz_say " preset : $preset" + wiz_say " enabled : $enabled" + wiz_say " silent : $silent" + [ "$preset" = custom ] && { + wiz_say " host : $host" + wiz_say " port : $port" + wiz_say " proto : $proto" + } + [ -n "$uuid" ] && wiz_say " uuid : $uuid" + wiz_say "-------------------------------------------" + wiz_confirm "commit this feeder?" || { wiz_say "aborted; nothing changed"; return 1; } + + # 7. delegate to cmd_add for the actual UCI work. + local extras + extras="enabled=$enabled silent_fail=$silent" + [ -n "$uuid" ] && extras="$extras uuid=$uuid" + if [ "$preset" = custom ]; then + extras="$extras host=$host port=$port protocol=$proto" + fi + # shellcheck disable=SC2086 + cmd_add "$name" "$preset" $extras || return $? + + # 8. post-commit reminder for optional companion package(s); offer + # install/enable for anything still missing or stopped. + if [ "$preset" != custom ] \ + && [ -n "$(readsb_feeder_optional_pkgs "$preset")" ]; then + wiz_say "" + wiz_say "--- companion package follow-up -------------" + readsb_recommend_optional_pkgs "$preset" "feeder '$name'" >/dev/tty + wiz_say "---------------------------------------------" + wiz_offer_install_companions "$preset" "feeder '$name'" || { + wiz_say "companion setup cancelled; feeder '$name' remains saved" + return 0 + } + fi +} + +# --- dispatch -------------------------------------------------------------- +# Bare invocation runs the wizard; all commands are --flag style. +# Guard the shift: busybox-ash exits the script if `shift` consumes +# more args than exist, even with `|| true`. +sub=${1:-__wizard} +[ $# -ge 1 ] && shift +case $sub in + __wizard) cmd_wizard ;; + --list) cmd_list ;; + --show) cmd_show "${1:-}" ;; + --url) cmd_url "${1:-}" ;; + --probe) cmd_probe "${1:-}" ;; + --health) cmd_health "${1:-}" ;; + --presets) cmd_presets ;; + --companions) cmd_companions "${1:-}" ;; + --examples) cmd_examples ;; + --add) cmd_add "$@" ;; + --set) cmd_set "$@" ;; + --enable) cmd_enable_disable "${1:-}" 1 ;; + --disable) cmd_enable_disable "${1:-}" 0 ;; + --remove) cmd_remove "${1:-}" ;; + *) + echo "readsb-feeder: unknown command '$sub'" >&2 + echo "" >&2 + sed -n '/^# readsb-feeder /,/^$/p' "$0" | sed 's/^# \{0,1\}//' >&2 + exit 1 + ;; +esac diff --git a/utils/readsb-wiedehopf/files/readsb-geoip b/utils/readsb-wiedehopf/files/readsb-geoip new file mode 100644 index 00000000000000..bed9edafdd7427 --- /dev/null +++ b/utils/readsb-wiedehopf/files/readsb-geoip @@ -0,0 +1,461 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-only +# +# Copyright (C) 2026 OpenWrt.org +# +# readsb-geoip -- best-effort fill of empty readsb lat/lon from +# public-IP geolocation. +# +# City-level only; for accurate CPR / range decoding, set lat/lon +# manually with `readsb-setup` or `uci set readsb.main.lat=...`. +# Non-interactive: only writes when both values are empty (or with +# --force) and then commits UCI. +# +# Usage: +# readsb-geoip [

] -- fill empty lat/lon (default: +# first readsb section); +# idempotent +# +# Read-only inspection (no UCI changes): +# readsb-geoip --self-test -- PASS/FAIL diagnostic of HTTP +# client, jsonfilter, network, +# provider response +# readsb-geoip --dry-run [
] -- everything except `uci commit` +# +# Mutating (commits to UCI; does NOT restart the daemon): +# readsb-geoip [
] -- the default action above +# readsb-geoip --force [
] -- overwrite existing lat/lon +# (DESTRUCTIVE) +# +# Modifiers (combine with any of the above): +# --verbose -- promote debug-level lines to +# info (visible in `logread -e +# readsb`) +# +# readsb-geoip -h, --help -- this options banner +# +# After any mutating run: `service readsb reload`. +# +# Exit codes: +# 0 success (lat/lon written, already set, or --self-test all PASS) +# 1 --self-test reported FAIL / fatal runtime error +# 2 --self-test reported WARN(s) only / missing tooling at runtime +# +# Full reference: +# https://github.com/openwrt/packages/blob/master/utils/readsb-wiedehopf/README.md + +# -h reachable without sourcing functions.sh / before `set -u`. +print_help() { + sed -n '/^# readsb-geoip /,/^$/p' "$0" | sed 's/^# \{0,1\}//' +} + +case ${1:-} in + -h|--help) print_help; exit 0 ;; +esac + +set -u + +# Use the package's shared logger with a geoip-specific tag. +READSB_LOG_TAG="readsb-geoip" +# shellcheck source=files/readsb.functions.sh +. /usr/lib/readsb/functions.sh + +# Verbose mode promotes _debug to _log (info). +verbose=0 +_verbose_apply() { + [ "$verbose" = "1" ] || return 0 + _debug() { _log "$@"; } +} + +# Stage breadcrumb. EXIT trap surfaces where we died -- a `set -u` trip +# inside vendor library code otherwise exits silently with rc=1. +last_stage="init" +on_exit() { + local rc=$? hint="" + case "$last_stage" in + done|*/done) _log "exit rc=$rc stage='$last_stage'"; return ;; + esac + [ "$rc" -ne 0 ] && hint=" (likely set -u on unset var; rerun: sh -x /usr/sbin/readsb-geoip --verbose --dry-run 2>&1 | logger -t readsb-trace)" + if [ "$rc" -ne 0 ]; then + _err "exit rc=$rc stage='$last_stage'${hint}" + else + _log "exit rc=$rc stage='$last_stage'" + fi +} +trap on_exit EXIT + +force=0 +dry_run=0 +self_test=0 +section="" +for arg in "$@"; do + case "$arg" in + -h|--help) print_help; exit 0 ;; + --force) force=1 ;; + --dry-run) dry_run=1 ;; + --verbose) verbose=1 ;; + --self-test) self_test=1 ;; + -*) _err "unknown option: $arg"; print_help >&2; exit 1 ;; + *) [ -z "$section" ] && section="$arg" ;; + esac +done +_verbose_apply + +last_stage="tool-detect" +have_uclient=0; have_wget=0; have_jsonfilter=0 +command -v uclient-fetch >/dev/null 2>&1 && have_uclient=1 +command -v wget >/dev/null 2>&1 && have_wget=1 +command -v jsonfilter >/dev/null 2>&1 && have_jsonfilter=1 + +_log "starting (force=$force dry_run=$dry_run verbose=$verbose self_test=$self_test section='${section:-}' uclient-fetch=$have_uclient wget=$have_wget jsonfilter=$have_jsonfilter)" + +# Self-test reports tooling problems via PASS/FAIL output; the normal +# path needs jsonfilter and at least one HTTP client. Per the script's +# documented exit-code contract, missing tooling exits rc=2 so that +# automation (cron, init-script geoip wait, etc.) does not treat the +# run as success when no lookup can possibly occur. +if [ "$self_test" -ne 1 ]; then + if [ "$have_jsonfilter" != "1" ]; then + _err "jsonfilter not installed (opkg install jsonfilter)" + last_stage="missing-jsonfilter/done" + exit 2 + fi + if [ "$have_uclient" = "0" ] && [ "$have_wget" = "0" ]; then + _err "no HTTP client found (need uclient-fetch or wget)" + last_stage="missing-http-client/done" + exit 2 + fi +fi + +# Strict numeric check; rejects argv-injection payloads in coords. +is_coord() { + printf '%s' "$1" | awk ' + /^[+-]?([0-9]+\.?[0-9]*|\.[0-9]+)$/ { exit 0 } + { exit 1 } + ' +} + +in_range() { + awk -v v="$1" -v b="$2" 'BEGIN { exit !(v+0 >= -b && v+0 <= b) }' +} + +# Strip anything outside [0-9a-fA-F:.] -- defends syslog from injection +# via a malicious provider response. +sanitize_ip() { + printf '%s' "$1" | tr -cd '0-9a-fA-F:.' +} + +# fetch : stdout = response body (may be empty). Always rc 0. +fetch() { + local url=$1 body="" rc client_err="" + + if [ "$have_uclient" = "1" ]; then + # mktemp gives a 0600 atomically-created file -- avoids the + # symlink-attack window of a predictable /tmp name. + local tmp_err + tmp_err=$(mktemp 2>/dev/null) || tmp_err="" + if [ -n "$tmp_err" ]; then + body=$(uclient-fetch -qO- --timeout=8 "$url" 2>"$tmp_err"); rc=$? + client_err=$(head -n 5 "$tmp_err" 2>/dev/null | tr '\n' ' ') + rm -f "$tmp_err" + else + body=$(uclient-fetch -qO- --timeout=8 "$url" 2>/dev/null); rc=$? + client_err="(stderr suppressed: mktemp unavailable)" + fi + if [ "$rc" -ne 0 ] || [ -z "$body" ]; then + _log "uclient-fetch $url failed (rc=$rc body_len=${#body}): ${client_err:-no stderr}" + body="" + fi + fi + + if [ -z "$body" ] && [ "$have_wget" = "1" ]; then + body=$(wget -qO- --timeout=8 "$url" 2>/dev/null); rc=$? + [ -z "$body" ] && _log "wget $url failed (rc=$rc)" + fi + + printf '%s' "$body" +} + +# parse_provider [] +# Echoes "lat lon ip" on success (ip = "-" if absent), empty otherwise. +parse_provider() { + local body=$1 lat_path=$2 lon_path=$3 ip_path=${4:-} lat lon ip="-" + [ -z "$body" ] && return 1 + lat=$(printf '%s' "$body" | jsonfilter -e "$lat_path" 2>/dev/null) + lon=$(printf '%s' "$body" | jsonfilter -e "$lon_path" 2>/dev/null) + [ -n "$lat" ] && [ -n "$lon" ] || return 1 + if [ -n "$ip_path" ]; then + ip=$(printf '%s' "$body" | jsonfilter -e "$ip_path" 2>/dev/null) + ip=$(sanitize_ip "$ip") + [ -n "$ip" ] || ip="-" + fi + printf '%s %s %s\n' "$lat" "$lon" "$ip" +} + +lookup() { + local body coords lat="" lon="" ip="-" provider="" + + body=$(fetch "https://ipapi.co/json/") + coords=$(parse_provider "$body" '@.latitude' '@.longitude' '@.ip') && provider="ipapi.co" + if [ -z "$coords" ] && [ -n "$body" ]; then + # ipapi.co rate-limit body: {"error":true,"reason":"RateLimited"} + local err_reason + err_reason=$(printf '%s' "$body" | jsonfilter -e '@.reason' 2>/dev/null) + _warn "ipapi.co returned no lat/lon (body_len=${#body}${err_reason:+ reason=\"$err_reason\"})" + fi + + if [ -z "$coords" ]; then + _debug "trying fallback provider ipwho.is" + body=$(fetch "https://ipwho.is/?fields=success,message,ip,latitude,longitude") + coords=$(parse_provider "$body" '@.latitude' '@.longitude' '@.ip') && provider="ipwho.is" + if [ -z "$coords" ] && [ -n "$body" ]; then + local success msg + success=$(printf '%s' "$body" | jsonfilter -e '@.success' 2>/dev/null) + msg=$(printf '%s' "$body" | jsonfilter -e '@.message' 2>/dev/null) + _warn "ipwho.is returned no lat/lon (body_len=${#body} success=${success:-?} message=\"${msg:-}\")" + fi + fi + + [ -n "$coords" ] || { _warn "all geoip providers returned empty"; return 1; } + set -- $coords + lat=$1; lon=$2; ip=${3:--} + is_coord "$lat" && is_coord "$lon" || { _warn "$provider returned non-numeric coords (lat=$lat lon=$lon ip=$ip); rejecting"; return 1; } + in_range "$lat" 90 && in_range "$lon" 180 || { _warn "$provider returned out-of-range coords (lat=$lat lon=$lon ip=$ip); rejecting"; return 1; } + _log "geoip provider $provider returned lat=$lat lon=$lon public_ip=$ip" + printf '%s\n' "$coords" +} + +update_section() { + local sec=$1 + local cur_lat cur_lon coords lat lon + + cur_lat=$(uci -q get "readsb.${sec}.lat") || cur_lat="" + cur_lon=$(uci -q get "readsb.${sec}.lon") || cur_lon="" + + if [ "$force" -ne 1 ] && [ -n "$cur_lat" ] && [ -n "$cur_lon" ]; then + _debug "section '${sec}' already has lat=$cur_lat lon=$cur_lon; skipping" + return 0 + fi + + _log "looking up coordinates for section '${sec}' (cur_lat='${cur_lat}' cur_lon='${cur_lon}' force=$force)" + + coords=$(lookup) || { + _warn "geoip lookup failed for '${sec}'; lat/lon left unset (set manually: uci set readsb.${sec}.lat=...; uci set readsb.${sec}.lon=...; uci commit readsb)" + return 1 + } + set -- $coords + lat=$1; lon=$2 + + if [ "$dry_run" = "1" ]; then + _notice "[dry-run] would set readsb.${sec}.lat=${lat} readsb.${sec}.lon=${lon}" + return 0 + fi + + # Mutating UCI: surface any failure (set or commit) instead of + # emitting a success notice on a half-written / un-committed change. + # Returning non-zero lets callers (cron, init-script geoip wait, + # --self-test piping) treat this as the failure it actually is. + if ! uci set "readsb.${sec}.lat=${lat}"; then + _err "failed to set readsb.${sec}.lat (uci set rc=$?); aborting" + return 1 + fi + if ! uci set "readsb.${sec}.lon=${lon}"; then + _err "failed to set readsb.${sec}.lon (uci set rc=$?); aborting" + return 1 + fi + if ! uci commit readsb; then + _err "uci commit readsb failed (rc=$?); lat/lon for '${sec}' may not be persisted" + return 1 + fi + _notice "auto-detected location for '${sec}': lat=${lat} lon=${lon} (city-level only; for accurate CPR set exact coords with 'readsb-setup' or 'uci set readsb.${sec}.lat=...; uci set readsb.${sec}.lon=...; uci commit readsb')" +} + +# Source /lib/functions.sh and config_load with set -u disabled -- +# vendor library code references several variables that may be unset. +load_uci_safely() { + last_stage="${1:-uci}/source-functions.sh" + : "${IPKG_INSTROOT:=}" + set +u + # shellcheck source=/dev/null + . /lib/functions.sh + last_stage="${1:-uci}/config-load" + config_load readsb + set -u +} + +# --- self-test ------------------------------------------------------------- +self_test_fail=0 +self_test_warn=0 +st_emit() { + # st_emit + local status=$1 name=$2 detail=$3 + case "$status" in + FAIL) self_test_fail=$((self_test_fail + 1)); _err "self-test FAIL: $name${detail:+ -- $detail}" ;; + WARN) self_test_warn=$((self_test_warn + 1)); _warn "self-test WARN: $name${detail:+ -- $detail}" ;; + *) _log "self-test $status: $name${detail:+ -- $detail}" ;; + esac + printf '[%s] %-30s %s\n' "$status" "$name" "$detail" +} + +# st_check_provider [] +# Sets $st_provider / $st_lat / $st_lon / $st_ip on PASS (caller declares). +st_check_provider() { + local name=$1 url=$2 lat_path=$3 lon_path=$4 ip_path=$5 + shift 5 + local body coords + body=$(fetch "$url") + if [ -z "$body" ]; then + st_emit FAIL "HTTPS: $name" "no response (TLS/network failure; see log above)" + return 1 + fi + coords=$(parse_provider "$body" "$lat_path" "$lon_path" "$ip_path") + if [ -n "$coords" ]; then + set -- $coords + st_lat=$1; st_lon=$2; st_ip=${3:--} + st_emit PASS "HTTPS: $name" "lat=$st_lat lon=$st_lon public_ip=$st_ip" + [ -z "$st_provider" ] && st_provider=$name + return 0 + fi + # Aggregate any provider-supplied status/reason fields for diagnostics. + local detail="connected but no coords (body_len=${#body}" + for path in "$@"; do + local key=${path#@.} val + val=$(printf '%s' "$body" | jsonfilter -e "$path" 2>/dev/null) + [ -n "$val" ] && detail="$detail $key=$val" + done + detail="$detail)" + st_emit WARN "HTTPS: $name" "$detail" + return 1 +} + +run_self_test() { + local st_provider="" st_lat="" st_lon="" st_ip="" + + _log "self-test starting" + echo "readsb-geoip self-test" + echo "----------------------" + + if [ "$have_jsonfilter" = "1" ]; then + st_emit PASS "jsonfilter installed" "$(command -v jsonfilter)" + else + st_emit FAIL "jsonfilter installed" "missing; opkg install jsonfilter" + fi + + if [ "$have_uclient" = "1" ] || [ "$have_wget" = "1" ]; then + st_emit PASS "HTTP client available" "uclient-fetch=$have_uclient wget=$have_wget" + else + st_emit FAIL "HTTP client available" "neither uclient-fetch nor wget; opkg install uclient-fetch" + fi + + if [ -s /etc/ssl/certs/ca-certificates.crt ]; then + st_emit PASS "CA bundle present" "/etc/ssl/certs/ca-certificates.crt" + elif [ -d /etc/ssl/certs ] && [ "$(ls -1 /etc/ssl/certs 2>/dev/null | wc -l)" -gt 0 ]; then + st_emit WARN "CA bundle present" "/etc/ssl/certs/ca-certificates.crt missing; some certs in /etc/ssl/certs" + else + st_emit FAIL "CA bundle present" "no CA store; opkg install ca-bundle ca-certificates" + fi + + if command -v nslookup >/dev/null 2>&1; then + for host in ipapi.co ipwho.is; do + if nslookup "$host" >/dev/null 2>&1; then + st_emit PASS "DNS: $host" "resolved" + else + st_emit FAIL "DNS: $host" "nslookup failed (DNS down or host blocked)" + fi + done + else + st_emit SKIP "DNS resolution" "nslookup not installed; HTTPS tests below cover connectivity" + fi + + if [ "$have_jsonfilter" != "1" ] || \ + { [ "$have_uclient" != "1" ] && [ "$have_wget" != "1" ]; }; then + echo + echo "Aborting connectivity tests: missing prerequisites above." + _err "self-test aborted: missing prerequisites (fail=$self_test_fail warn=$self_test_warn)" + return 1 + fi + + st_check_provider "ipapi.co" "https://ipapi.co/json/" '@.latitude' '@.longitude' '@.ip' '@.reason' || true + st_check_provider "ipwho.is" "https://ipwho.is/?fields=success,message,ip,latitude,longitude" '@.latitude' '@.longitude' '@.ip' '@.success' '@.message' || true + + if [ -n "$st_provider" ]; then + if is_coord "$st_lat" && is_coord "$st_lon" && \ + in_range "$st_lat" 90 && in_range "$st_lon" 180; then + st_emit PASS "coord validation" "lat=$st_lat lon=$st_lon in range" + else + st_emit FAIL "coord validation" "$st_provider returned bad coords (lat=$st_lat lon=$st_lon)" + fi + else + st_emit FAIL "coord validation" "no provider returned usable coords" + fi + + if [ -f /etc/config/readsb ]; then + load_uci_safely "self-test" + local _sec_count=0 _need=0 + _st_count() { _sec_count=$((_sec_count + 1)); } + _st_need() { + local en lat lon + config_get_bool en "$1" enabled 0 + [ "$en" -eq 1 ] || return 0 + config_get lat "$1" lat + config_get lon "$1" lon + [ -n "$lat" ] && [ -n "$lon" ] || _need=$((_need + 1)) + } + config_foreach _st_count readsb + config_foreach _st_need readsb + if [ "$_sec_count" -eq 0 ]; then + st_emit WARN "UCI sections" "no readsb sections found (run 'readsb-setup' to create one)" + elif [ "$_need" -eq 0 ]; then + st_emit PASS "UCI sections" "$_sec_count section(s); all enabled instances already have lat/lon" + else + st_emit PASS "UCI sections" "$_sec_count section(s); $_need enabled instance(s) need geoip" + fi + else + st_emit FAIL "UCI sections" "readsb UCI config missing -- reinstall readsb-wiedehopf or run 'readsb-setup'" + fi + + echo "----------------------" + echo "Result: fail=$self_test_fail warn=$self_test_warn" + if [ "$self_test_fail" -gt 0 ]; then + _err "self-test complete (fail=$self_test_fail warn=$self_test_warn)" + elif [ "$self_test_warn" -gt 0 ]; then + _warn "self-test complete (fail=$self_test_fail warn=$self_test_warn)" + else + _notice "self-test complete (fail=0 warn=0)" + fi +} + +if [ "$self_test" = "1" ]; then + run_self_test + last_stage="self-test/done" + [ "$self_test_fail" -gt 0 ] && exit 1 + [ "$self_test_warn" -gt 0 ] && exit 2 + exit 0 +fi + +if [ -n "$section" ]; then + last_stage="update-section($section)" + _log "updating single section '$section' (force=$force dry_run=$dry_run)" + update_section "$section" + rc=$? + last_stage="update-section($section)/done" + exit $rc +fi + +load_uci_safely "main" + +last_stage="count-sections" +sec_count=0 +count_sec() { sec_count=$((sec_count + 1)); } +config_foreach count_sec readsb +_debug "loaded /etc/config/readsb (sections=$sec_count)" +if [ "$sec_count" -eq 0 ]; then + _warn "no readsb sections defined; nothing to do (run 'readsb-setup' to bootstrap)" + last_stage="no-sections/done" + exit 0 +fi + +last_stage="update-all" +config_foreach update_section readsb +last_stage="done" +_debug "all sections processed" diff --git a/utils/readsb-wiedehopf/files/readsb-setup b/utils/readsb-wiedehopf/files/readsb-setup new file mode 100644 index 00000000000000..028cb59b0fabcd --- /dev/null +++ b/utils/readsb-wiedehopf/files/readsb-setup @@ -0,0 +1,1424 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-only +# Copyright (C) 2026 OpenWrt.org + +# readsb-setup -- master CLI for readsb-wiedehopf. +# +# Bare invocation runs the interactive first-boot wizard +# (lat/lon -> UUID -> SDR tuning -> aggregator feeders -> reload). +# Each step is skippable; re-runs are idempotent. +# +# Read-only flags (no UCI changes): +# --status configuration check (essentials, mode, ports, SDRs, +# feeders, companion packages); exit 0 OK / 1 missing +# --health runtime health (daemon process, host load, live data, +# per-feeder probe + per-source log scan); exit 0/1/2 +# --stats most recent periodic stats block (signal/preambles/ +# decoded msgs/tracks/CPU); exit 0/1/2/3 +# --config pretty-printed /etc/config/readsb (+ companion configs) +# -h/--help master orientation banner (every CLI + every flag) +# +# Full reference: +# https://github.com/openwrt/packages/blob/master/utils/readsb-wiedehopf/README.md + +# --help -- print the post-install welcome banner. Static heredoc with +# no dependencies (works in recovery), then two dynamic sections when +# functions.sh is available: companion packages installed / not installed. +print_banner() { + cat <<'BANNER' + +============================================================ + readsb-wiedehopf -- Mode-S/ADS-B/TIS decoder for OpenWrt +============================================================ + Upstream : https://github.com/wiedehopf/readsb + Packaging : https://github.com/openwrt/packages + (utils/readsb-wiedehopf) + Maintainer : Dr Bill Mcilhargey + <19168174+billmcilhargey@users.noreply.github.com> + License : GPL-3.0-or-later AND BSD-3-Clause (upstream) + GPL-2.0-only (OpenWrt packaging) + Bug reports : https://github.com/openwrt/packages/issues + (please prefix the title with [readsb-wiedehopf]) + Documentation : readsb-setup --help (this banner) + readsb-setup --status / --health / --stats + man-style README shipped with the package source + +============================================================ + getting started +============================================================ + The daemon starts with safe defaults at install time, but + to actually receive aircraft you need at minimum a station + location and (for most aggregators) a UUID. + + Configure interactively: + readsb-setup -- guided first-boot wizard + readsb-feeder -- guided add of one new feeder + + Inspect / verify (read-only, no prompts): + readsb-setup --status -- configuration check (essentials, mode, + ports, SDRs, feeders, companion packages) + readsb-setup --health -- runtime health (process metrics, live + data flow, feeder probe + per-source + log scan: readsb + each installed + companion package) + readsb-setup --stats -- ONLY the most recent periodic stats + block (signal, decoded msgs, tracks, + CPU); same parser as --health, no other + output -- ideal for cron/quick-check + readsb-setup --config -- pretty-printed /etc/config/readsb + + /etc/config/ for each installed + companion package + readsb-setup -h, --help -- show this banner + + Aggregator feeder management (each accepts -h / --help): + readsb-feeder --list -- show configured feeders + readsb-feeder --show -- dump one feeder section + readsb-feeder --presets -- list built-in preset names + readsb-feeder --examples -- print sample feeder UCI blocks + readsb-feeder --companions -- optional companion packages per preset + readsb-feeder --url [] -- public per-station stats URL(s) + readsb-feeder --probe [] -- TCP-probe each enabled feeder + readsb-feeder --health [] -- per-feeder runtime check (probe + + cmdline + recent error log scan) + readsb-feeder --add [k=v]... + -- non-interactive feeder creation + readsb-feeder --set =... + -- modify an existing feeder + readsb-feeder --enable -- shortcut for: --set enabled=1 + readsb-feeder --disable -- shortcut for: --set enabled=0 + readsb-feeder --remove -- delete a feeder section + + Other helpers (each accepts -h / --help): + readsb-uuid -- interactive UUID wizard (shows current, + offers generate/regenerate) + readsb-uuid --print -- print the current UUID; nothing if unset + readsb-uuid --auto -- print existing or generate (no prompts; + safe for cron / scripts) + readsb-uuid --force -- regenerate even if set (DESTRUCTIVE) + readsb-geoip -- auto-fill empty lat/lon from public IP + readsb-geoip --force -- overwrite existing lat/lon + readsb-geoip --dry-run -- everything except 'uci commit' + readsb-geoip --self-test -- read-only PASS/FAIL diagnostic + + Daemon control: + service readsb {start|stop|restart|reload|status} + logread -e readsb -- daemon + helper logs + + Full reference: + https://github.com/openwrt/packages/blob/master/utils/readsb-wiedehopf/README.md +BANNER + + # Sourcing functions.sh is safe in postinst (its own files are + # unpacked first); skip cleanly if absent. + if [ -r /usr/lib/readsb/functions.sh ]; then + # shellcheck disable=SC1091 + . /usr/lib/readsb/functions.sh + _banner_print_companions + fi + + cat <<'BANNER' +============================================================ + +BANNER +} + +# Print one block per known optional companion package, separated into +# "installed" vs "not installed". Banner-only. +_banner_print_companions() { + local pkg purpose init init_path state \ + installed_lines='' missing_lines='' all_pkgs + + all_pkgs=$(readsb_companion_pkgs_all 2>/dev/null) + [ -n "$all_pkgs" ] || return 0 + + for pkg in $all_pkgs; do + purpose=$(readsb_pkg_purpose "$pkg") + [ -n "$purpose" ] || purpose="(no description available)" + + if readsb_pkg_installed "$pkg"; then + # shellcheck disable=SC2046 + set -- $(readsb_pkg_status "$pkg" 2>/dev/null) + state=${1:-no-service} + init=${2:-} + init_path= + [ -n "$init" ] && init_path="/etc/init.d/$init" + + installed_lines="${installed_lines} $pkg + purpose : $purpose +" + if [ -n "$init_path" ]; then + local extras controls + extras=$(readsb_pkg_extra_actions "$pkg") + controls="start|stop|restart|reload|status|enable|disable" + if [ -n "$extras" ]; then + for x in $extras; do + controls="$controls|$x" + done + fi + installed_lines="${installed_lines} init script : $init_path + state : $state + controls : $init_path {$controls} +" + else + installed_lines="${installed_lines} init script : (none -- library/data package) +" + fi + installed_lines="${installed_lines} package help : opkg info $pkg | opkg files $pkg +" + # If the package ships a same-named binary on PATH, point + # the operator at its own --help / -h. + if command -v "$pkg" >/dev/null 2>&1; then + installed_lines="${installed_lines} cli help : $pkg --help (or: $pkg -h) +" + fi + installed_lines="${installed_lines} +" + else + missing_lines="${missing_lines} $pkg -- NOT INSTALLED + purpose : $purpose + install : opkg update && opkg install $pkg +" + fi + done + + if [ -n "$installed_lines" ]; then + printf '\n[ optional companion packages: installed ]\n' + printf '%s' "$installed_lines" + fi + if [ -n "$missing_lines" ]; then + printf '\n[ optional companion packages: not installed ]\n' + printf '%s' "$missing_lines" + printf '\n' + fi +} + +mode=wizard +case ${1:-} in + -h|--help) print_banner; exit 0 ;; + --status) mode=status ;; + --health) mode=health ;; + --stats) mode=stats ;; + --config) mode=config ;; + '') ;; + *) + echo "readsb-setup: unexpected argument '$1'" >&2 + echo "" >&2 + print_banner >&2 + exit 1 + ;; +esac + +# shellcheck source=files/readsb.functions.sh +. /usr/lib/readsb/functions.sh + +READSB_LOG_TAG=readsb-setup + +# --- --config mode --------------------------------------------------------- +# Pretty-printed dump of the live UCI config. Read-only. +if [ "$mode" = config ]; then + echo "============================================================" + echo " readsb-wiedehopf -- /etc/config/readsb" + echo "============================================================" + + if ! uci -q show readsb >/dev/null 2>&1; then + echo " (no readsb UCI config found; install/reinstall the package)" + exit 1 + fi + + echo + echo "[ main section ]" + if uci -q show readsb.main >/dev/null 2>&1; then + uci -q show readsb.main 2>/dev/null \ + | grep -v "=readsb$" \ + | sed 's/^readsb\.main\./ /' \ + | sort + else + echo " (no main section -- config is broken)" + fi + + echo + echo "[ feeder sections ]" + feeders=$(uci -q show readsb 2>/dev/null \ + | awk -F. '/^readsb\.[^.]+=feeder$/ { sub(/=feeder$/, "", $2); print $2 }' \ + | sort -u) + if [ -z "$feeders" ]; then + echo " (none configured -- run: readsb-setup, or: readsb-feeder)" + else + for f in $feeders; do + echo " config feeder '$f'" + uci -q show "readsb.$f" 2>/dev/null \ + | grep -v "=feeder$" \ + | sed "s|^readsb\.${f}\.| |" \ + | sort + echo + done + fi + + # Per-companion config dumps. Iterate every known companion, but + # only emit when the package is installed AND ships a UCI config we + # know how to dump. + for cpkg in $(readsb_companion_pkgs_all 2>/dev/null); do + readsb_pkg_installed "$cpkg" || continue + cuci=$(readsb_pkg_uci_config "$cpkg") + [ -n "$cuci" ] || continue + [ -f "/etc/config/$cuci" ] || continue + uci -q show "$cuci" >/dev/null 2>&1 || continue + + echo + echo "============================================================" + echo " companion: $cpkg -- /etc/config/$cuci" + echo "============================================================" + # Group output by section: emit one header per section, then the + # option/list lines indented under it. No per-package schema baked + # in; works for any section type via `uci show `. + uci -q show "$cuci" 2>/dev/null | awk -v cfg="$cuci" ' + BEGIN { prefix = cfg "." } + { + # Each line is one of: + # .= (section) + # ..= (option) + # Avoid `-F.` because section/option names may have + # underscores or hyphens but never dots, and values may + # contain anything. + if (substr($0, 1, length(prefix)) != prefix) next + rest = substr($0, length(prefix)+1) + eq = index(rest, "=") + if (eq == 0) next + lhs = substr(rest, 1, eq-1) + rhs = substr(rest, eq+1) + dot = index(lhs, ".") + if (dot == 0) { + sec = lhs + type[sec] = rhs + if (!(sec in seen)) { seen[sec] = 1; order[++nsec] = sec } + } else { + sec = substr(lhs, 1, dot-1) + key = substr(lhs, dot+1) + opts[sec] = opts[sec] key "=" rhs "\n" + if (!(sec in seen)) { seen[sec] = 1; order[++nsec] = sec } + } + } + END { + for (i = 1; i <= nsec; i++) { + s = order[i] + t = (s in type) ? type[s] : "?" + printf(" config %s '\''%s'\''\n", t, s) + n = split(opts[s], lines, "\n") + sorted_n = 0 + for (j = 1; j <= n; j++) if (lines[j] != "") sorted[++sorted_n] = lines[j] + # small-set insertion sort: stable output + for (j = 1; j < sorted_n; j++) { + for (k = j+1; k <= sorted_n; k++) { + if (sorted[k] < sorted[j]) { + tmp = sorted[j]; sorted[j] = sorted[k]; sorted[k] = tmp + } + } + } + for (j = 1; j <= sorted_n; j++) print " " sorted[j] + delete sorted; sorted_n = 0 + print "" + } + } + ' + done + + echo "============================================================" + echo " see also: readsb-setup --status (configuration check)" + echo " readsb-setup --health (runtime check)" + echo " readsb-setup --stats (most recent stats block)" + echo " uci show readsb (machine-readable form)" + echo "============================================================" + exit 0 +fi + +# --- --stats mode ---------------------------------------------------------- +# ONLY the most recent periodic stats block emitted by readsb -- a +# focused subset of --health for operators who just want "how is +# reception/decoding doing right now?" without the surrounding noise. +# Source: `readsb_render_stats_summary` in functions.sh. +# +# Exit codes: +# 0 block displayed +# 1 logread unavailable +# 2 daemon not running +# 3 no complete block currently buffered +if [ "$mode" = stats ]; then + echo "============================================================" + echo " readsb-wiedehopf -- recent stats" + echo "============================================================" + + if ! command -v logread >/dev/null 2>&1; then + echo + echo " logread is unavailable on this system; cannot read the" + echo " recent syslog buffer for the stats block." + echo "============================================================" + exit 1 + fi + + rsb_pid=$(readsb_pid) + if [ -z "$rsb_pid" ]; then + echo + echo " daemon state : NOT RUNNING (no stats are being produced)" + echo " hint : /etc/init.d/readsb start" + echo " (then wait s -- default 120s --" + echo " before re-running: readsb-setup --stats)" + echo "============================================================" + exit 2 + fi + + echo + if readsb_render_stats_summary " "; then + echo + echo "============================================================" + echo " for runtime health (process / feeders / log scan):" + echo " readsb-setup --health" + echo " for the raw daemon block (unparsed syslog lines):" + echo " logread -e readsb | awk '/Statistics:/,/ms for network input/'" + echo "============================================================" + exit 0 + fi + + # Diagnose: `option stats '0'` is the most common cause of a quiet + # buffer right after a restart. + stats_uci=$(uci -q get readsb.main.stats 2>/dev/null) + stats_every_uci=$(uci -q get readsb.main.stats_every 2>/dev/null) + echo " last stats block: (none in current syslog buffer)" + echo + if [ "$stats_uci" = "0" ]; then + echo " reason : readsb.main.stats='0' -- periodic stats" + echo " block emission is DISABLED in UCI." + echo " to enable : uci set readsb.main.stats=1; uci commit readsb" + echo " service readsb reload" + else + echo " likely cause : the daemon just (re)started and has not" + echo " completed its first stats interval yet," + echo " or the syslog ring buffer rotated past" + echo " the most recent block." + echo " stats_every : ${stats_every_uci:-120} (UCI default 120s)" + echo " retry : wait one stats_every interval, then re-run" + echo " 'readsb-setup --stats'" + fi + echo "============================================================" + exit 3 +fi + +# --- --status mode --------------------------------------------------------- +# Read-only inspection. Exits non-zero if any essential UCI value is +# missing. All blocks degrade gracefully ("(n/a)") on missing /proc / +# JSON / log buffer. + +# --- small inline helpers (only used by --status) -------------------------- + +# Format an integer number of seconds as "Nd Nh Nm" / "Nh Nm" / "Nm Ns" +# / "Ns". Empty / non-numeric input -> "(n/a)". +_status_human_secs() { + local s=$1 d h m + case $s in + ''|*[!0-9]*) echo '(n/a)'; return 0 ;; + esac + d=$((s / 86400)); s=$((s % 86400)) + h=$((s / 3600)); s=$((s % 3600)) + m=$((s / 60)); s=$((s % 60)) + if [ $d -gt 0 ]; then printf '%dd %dh %dm\n' "$d" "$h" "$m" + elif [ $h -gt 0 ]; then printf '%dh %dm %ds\n' "$h" "$m" "$s" + elif [ $m -gt 0 ]; then printf '%dm %ds\n' "$m" "$s" + else printf '%ds\n' "$s" + fi +} + +# Pretty-print KiB as "12.4 MB" / "256 KB" / "1.2 GB". Non-numeric +# input -> "(n/a)". +_status_human_kb() { + local k=$1 + case $k in + ''|*[!0-9]*) echo '(n/a)'; return 0 ;; + esac + if [ "$k" -ge 1048576 ]; then awk -v k="$k" 'BEGIN { printf "%.1f GB\n", k/1048576 }' + elif [ "$k" -ge 1024 ]; then awk -v k="$k" 'BEGIN { printf "%.1f MB\n", k/1024 }' + else printf '%d KB\n' "$k" + fi +} + +# Echo first line of /proc/uptime first field as integer seconds. +_status_sys_uptime_secs() { + local u + [ -r /proc/uptime ] || { echo ''; return 1; } + read -r u _ < /proc/uptime + # strip fractional part + echo "${u%%.*}" +} + +# Echo /proc/loadavg first three fields, space-separated. +_status_loadavg() { + local a b c _ + [ -r /proc/loadavg ] || { echo '(n/a)'; return 1; } + read -r a b c _ < /proc/loadavg + echo "$a $b $c" +} + +# Echo " " or empty on failure. +_status_meminfo_kb() { + [ -r /proc/meminfo ] || return 1 + awk '/^MemTotal:/{t=$2} /^MemAvailable:/{a=$2} + END { if (t) printf "%d %d\n", t, (a?a:0) }' /proc/meminfo +} + +# Set globals from /proc//{stat,status}. CLK_TCK-aware. On +# success: _PRC_AGE_S, _PRC_CPU_S, _PRC_CPU_PCT, _PRC_RSS_KB, +# _PRC_THREADS, _PRC_VSZ_KB. Returns 1 on failure. +_status_proc_metrics() { + local pid=$1 stat after sys_up_s clk after_no_paren + local utime stime starttime sys_up_j proc_age_j cpu_j + [ -n "$pid" ] && [ -r "/proc/$pid/stat" ] || return 1 + stat=$(cat "/proc/$pid/stat" 2>/dev/null) || return 1 + # Strip " (comm) " prefix safely (comm may contain spaces or + # parens). Everything after the LAST ')' is field 3 onwards. + after_no_paren=${stat##*) } + # shellcheck disable=SC2086 + set -- $after_no_paren + # Original field N (>= 3) is now positional $((N - 2)). + # utime=14 -> $12, stime=15 -> $13, starttime=22 -> $20 + utime=${12:-0}; stime=${13:-0}; starttime=${20:-0} + clk=$(getconf CLK_TCK 2>/dev/null); [ -n "$clk" ] && [ "$clk" -gt 0 ] 2>/dev/null || clk=100 + sys_up_s=$(_status_sys_uptime_secs) + [ -n "$sys_up_s" ] || return 1 + sys_up_j=$((sys_up_s * clk)) + proc_age_j=$((sys_up_j - starttime)) + [ "$proc_age_j" -gt 0 ] || proc_age_j=1 + cpu_j=$((utime + stime)) + _PRC_AGE_S=$((proc_age_j / clk)) + _PRC_CPU_S=$((cpu_j / clk)) + # integer percent across process lifetime + _PRC_CPU_PCT=$((cpu_j * 100 / proc_age_j)) + + if [ -r "/proc/$pid/status" ]; then + _PRC_RSS_KB=$(awk '/^VmRSS:/{print $2; exit}' "/proc/$pid/status") + _PRC_VSZ_KB=$(awk '/^VmSize:/{print $2; exit}' "/proc/$pid/status") + _PRC_THREADS=$(awk '/^Threads:/{print $2; exit}' "/proc/$pid/status") + fi + [ -n "$_PRC_RSS_KB" ] || _PRC_RSS_KB=0 + [ -n "$_PRC_VSZ_KB" ] || _PRC_VSZ_KB=0 + [ -n "$_PRC_THREADS" ] || _PRC_THREADS=1 + return 0 +} + +# Extract the value of --flag=VALUE from a cmdline string. Only the +# `--flag=VALUE` form is recognized; the space-separated `--flag VALUE` +# form is intentionally NOT supported here because readsb's own init +# script always emits the `=`-joined form (see append_param() in +# readsb.init), and supporting the split form would require lookahead +# state that this small awk extractor deliberately avoids. Echoes +# empty if not present. +_status_cmdline_arg() { + local cmdline=$1 flag=$2 + echo "$cmdline" | awk -v f="--$flag=" ' + { + for (i = 1; i <= NF; i++) { + if (substr($i, 1, length(f)) == f) { + print substr($i, length(f) + 1); exit + } + } + }' +} + +# Count occurrences of "--flag=" in a cmdline. +_status_cmdline_count() { + local cmdline=$1 flag=$2 + echo "$cmdline" | tr ' ' '\n' | grep -c -- "^--$flag=" +} + +# Echo first JSON string value matching "key" : "<...>". Top-level only, +# coarse extractor (no nested-object handling) -- adequate for the small +# flat files readsb writes (receiver.json + the few top-level fields of +# aircraft.json). +_status_json_str() { + local file=$1 key=$2 + [ -r "$file" ] || return 1 + sed -n "s/.*\"${key}\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" \ + "$file" | head -1 +} + +# Echo first JSON numeric value matching "key" : . Top-level coarse +# extractor; same caveats as _status_json_str. +_status_json_num() { + local file=$1 key=$2 + [ -r "$file" ] || return 1 + sed -n "s/.*\"${key}\"[[:space:]]*:[[:space:]]*\\(-\\{0,1\\}[0-9.][0-9.]*\\).*/\\1/p" \ + "$file" | head -1 +} + +if [ "$mode" = status ]; then + # Configuration check ("is this box set up correctly?"). Static / + # structural inspection only -- runtime/live questions live in + # --health. Sections, top-down: + # [ identity & location ] -- UUID + lat/lon (sets `missing` count) + # [ daemon ] -- enabled / running / mode / version + # [ ports ] -- UCI-configured listen ports (compact) + # [ SDR ] -- USB enumeration + per-device strings + # + tuning options + # [ feeders ] -- count + `readsb-feeder --list` rows + # [ optional companion packages ] + # + # Exit non-zero only when an essential UCI value (UUID / lat / lon) + # is missing. For runtime / "is it actually working" questions the + # closing footer points at `readsb-setup --health`. + missing=0 + + cur_lat=$(uci -q get readsb.main.lat 2>/dev/null) + cur_lon=$(uci -q get readsb.main.lon 2>/dev/null) + cur_uuid=$(uci -q get readsb.main.uuid 2>/dev/null) + cur_gain=$(uci -q get readsb.main.gain 2>/dev/null) + cur_ppm=$(uci -q get readsb.main.ppm 2>/dev/null) + cur_agc=$(uci -q get readsb.main.enable_agc 2>/dev/null) + cur_bias=$(uci -q get readsb.main.enable_biastee 2>/dev/null) + enabled=$(uci -q get readsb.main.enabled 2>/dev/null) + fc=$(readsb_count_feeders) + sc=$(readsb_count_sdrs) + running=no + [ -x /etc/init.d/readsb ] && /etc/init.d/readsb running 2>/dev/null && running=yes + + echo "============================================================" + echo " readsb-wiedehopf -- status (configuration check)" + echo "============================================================" + + # --- [ identity & location ] -------------------------------------- + echo + echo "[ identity & location ]" + hostname=$(uci -q get system.@system[0].hostname 2>/dev/null) + [ -n "$hostname" ] || hostname=$(cat /proc/sys/kernel/hostname 2>/dev/null) + [ -n "$hostname" ] || hostname='(unknown)' + printf ' hostname : %s\n' "$hostname" + if readsb_is_uuid "$cur_uuid"; then + printf ' station UUID : %s\n' "$cur_uuid" + else + printf ' station UUID : MISSING (run: readsb-uuid)\n' + missing=$((missing + 1)) + fi + if [ -n "$cur_lat" ] && [ -n "$cur_lon" ]; then + printf ' location : lat=%s lon=%s\n' "$cur_lat" "$cur_lon" + else + printf ' location : MISSING (run: readsb-setup, or: readsb-geoip)\n' + missing=$((missing + 1)) + fi + + # --- [ daemon ] --------------------------------------------------- + echo + echo "[ daemon ]" + printf ' service enabled : %s\n' "${enabled:-(unset)}" + printf ' daemon running : %s\n' "$running" + + # Configured mode (UCI snapshot). For LIVE mode (what the running + # daemon is actually doing right now), see `readsb-setup --health`. + _dt_uci=$(uci -q get readsb.main.device_type) + _no_uci=$(uci -q get readsb.main.net_only) + if [ "$_no_uci" = 1 ] || [ -z "$_dt_uci" ]; then + printf ' mode (UCI) : net-only\n' + else + printf ' mode (UCI) : SDR (%s)\n' "$_dt_uci" + fi + + # Version is purely structural here -- read from the opkg control + # file. (`readsb-setup --health` cross-checks against the LIVE + # version reported in receiver.json so drift is visible.) + if [ -r /usr/lib/opkg/info/readsb-wiedehopf.control ]; then + rsb_version=$(awk -F': *' '/^Version:/{print $2; exit}' \ + /usr/lib/opkg/info/readsb-wiedehopf.control) + printf ' version (opkg) : %s\n' "${rsb_version:-(unknown)}" + fi + + # Defensive: if UCI intent (device_type set, net_only=0) disagrees + # with what hotplug actually sees, surface it. This is the most + # common config-drift hazard at install time on devices that + # enumerate the SDR slowly. + _sdr_now=$(readsb_count_sdrs 2>/dev/null || echo 0) + if [ "$_no_uci" != 1 ] && [ -n "$_dt_uci" ] && [ "$_sdr_now" -eq 0 ]; then + printf ' ! mismatch : UCI says SDR (%s) but no RTL-SDR is attached\n' "$_dt_uci" + printf ' fix: attach the SDR, or set net_only=1\n' + elif [ "$_sdr_now" -gt 0 ] && [ -z "$_dt_uci" ]; then + printf ' ! mismatch : %s RTL-SDR(s) attached but UCI device_type is unset\n' "$_sdr_now" + printf ' fix: /etc/init.d/readsb restart (re-runs hotplug seeding)\n' + fi + + # --- [ ports ] ---------------------------------------------------- + # Listen ports as configured in UCI (compact list -- only those + # that are actually set, no `(off)` rows). For the LIVE port set + # (what the daemon is actually accepting on right now) see + # `readsb-setup --health`. + echo + echo "[ ports (UCI-configured) ]" + _emit_port() { + local label=$1 ucikey=$2 v + v=$(uci -q get "readsb.main.$ucikey") + [ -n "$v" ] && printf ' %-15s : %s\n' "$label" "$v" + } + _emit_port 'beast-out' net_bo_port + _emit_port 'beast-in' net_bi_port + _emit_port 'raw-in' net_ri_port + _emit_port 'raw-out' net_ro_port + _emit_port 'SBS' net_sbs_port + _emit_port 'json' net_json_port + _emit_port 'api' net_api_port + _emit_port 'beast-reduce' net_beast_reduce_out_port + # Fall-through note when nothing is set. + if ! uci -q show readsb.main 2>/dev/null \ + | grep -q -E 'net_(bo|bi|ri|ro|sbs|json|api|beast_reduce_out)_port='; then + printf ' (no listen ports configured -- net-out only)\n' + fi + + # --- [ SDR ] ------------------------------------------------------ + echo + echo "[ SDR ]" + printf ' detected : %s\n' "$sc" + if [ "$sc" -gt 0 ]; then + # enumerate per-device serial / vendor / product strings + _sdr_describe() { + local d=$1 vid=$2 pid=$3 ser='NOSERIAL' vendor='' product='' + [ -r "$d/serial" ] && read -r ser < "$d/serial" + [ -r "$d/manufacturer" ] && read -r vendor < "$d/manufacturer" + [ -r "$d/product" ] && read -r product < "$d/product" + # fall back to USB IDs when string descriptors are absent + [ -n "$vendor" ] || vendor='(no vendor str)' + [ -n "$product" ] || product='(no product str)' + [ -n "$ser" ] || ser='NOSERIAL' + printf ' %s:%s serial=%-12s %s / %s\n' \ + "$vid" "$pid" "\"$ser\"" "$vendor" "$product" + } + readsb_for_each_sdr _sdr_describe + printf ' gain : %s\n' "${cur_gain:-(unset, default auto)}" + printf ' ppm : %s\n' "${cur_ppm:-0}" + printf ' AGC : %s\n' "${cur_agc:-0}" + printf ' bias-T : %s\n' "${cur_bias:-0}" + else + printf ' (net-only deployment -- no RTL-SDR attached)\n' + fi + + # --- [ feeders ] -------------------------------------------------- + echo + echo "[ feeders ]" + if [ "$fc" -gt 0 ]; then + printf ' config feeder : %s section(s)\n\n' "$fc" + readsb-feeder --list 2>/dev/null | sed 's/^/ /' + else + printf ' (no feeders configured -- run: readsb-setup, or:\n' + printf ' readsb-feeder)\n' + fi + + # --- [ optional companion packages ] ------------------------------ + echo + echo "[ optional companion packages ]" + have_companions=0 + main_uuid_for_status=$cur_uuid + _status_companions() { + local fcfg=$1 pkg pkgs state init purpose + readsb_feeder_resolve "$fcfg" "$main_uuid_for_status" || return 0 + [ "$readsb_feeder_enabled" -eq 1 ] || return 0 + [ "$readsb_feeder_preset" = custom ] && return 0 + pkgs=$(readsb_feeder_optional_pkgs "$readsb_feeder_preset") + [ -n "$pkgs" ] || return 0 + + have_companions=1 + printf ' feeder %-16s preset %s:\n' "'$fcfg'" "$readsb_feeder_preset" + for pkg in $pkgs; do + # shellcheck disable=SC2046 + set -- $(readsb_pkg_status "$pkg" 2>/dev/null) + state=${1:-missing} + init=${2:-} + purpose=$(readsb_pkg_purpose "$pkg") + [ -n "$purpose" ] || purpose="(no description available)" + case $state in + missing) + printf ' %s -- NOT INSTALLED\n' "$pkg" + printf ' purpose : %s\n' "$purpose" + printf ' install : opkg update && opkg install %s\n' "$pkg" + ;; + stopped) + printf ' %s -- installed but NOT RUNNING (init: %s)\n' "$pkg" "$init" + printf ' purpose : %s\n' "$purpose" + printf ' enable : /etc/init.d/%s enable\n' "$init" + printf ' start : /etc/init.d/%s start\n' "$init" + printf ' logs : logread -e %s\n' "$init" + ;; + running) + printf ' %s -- installed and running (init: %s)\n' "$pkg" "$init" + local extras x + extras=$(readsb_pkg_extra_actions "$pkg") + for x in $extras; do + printf ' %-7s : /etc/init.d/%s %s\n' "$x" "$init" "$x" + done + ;; + no-service) + printf ' %s -- installed (no service component)\n' "$pkg" + ;; + esac + done + # Mirror missing/stopped fix-it hints to syslog. + readsb_warn_companions "$readsb_feeder_preset" "$fcfg" + } + + # /lib/functions.sh needed for config_load/config_foreach. Loaded + # here (not at file top) so the wizard path doesn't pay for it. + # shellcheck disable=SC1091 + . /lib/functions.sh + config_load readsb + config_foreach _status_companions feeder + + if [ "$have_companions" = 0 ]; then + printf ' (none recommended for the currently enabled feeder presets)\n' + fi + + echo + echo "============================================================" + if [ "$missing" -gt 0 ]; then + echo " $missing essential setting(s) missing -- run 'readsb-setup' to fix" + echo " for runtime health (process metrics, live data flow," + echo " feeder probe, log scan) run: readsb-setup --health" + echo "============================================================" + exit 1 + fi + echo " all essentials present" + echo " for runtime health (process metrics, live data flow," + echo " feeder probe, log scan) run: readsb-setup --health" + echo "============================================================" + exit 0 +fi + +# --- --health mode -------------------------------------------------------- +# Runtime check ("is this box working right now?"). Live-only; +# structural questions live in --status. +# +# Exit codes: +# 0 HEALTHY -- daemon running, enabled feeders LIVE+reachable, no errors +# 1 DEGRADED -- daemon running but at least one issue +# 2 DOWN -- daemon not running +if [ "$mode" = health ]; then + rsb_pid=$(readsb_pid) + if [ -z "$rsb_pid" ]; then + echo "============================================================" + echo " readsb-wiedehopf -- runtime health" + echo "============================================================" + echo + echo "[ daemon process ]" + echo " state : NOT RUNNING" + echo " hint : /etc/init.d/readsb start" + echo " (then re-run: readsb-setup --health)" + echo + echo "============================================================" + echo " health summary : DOWN (daemon not running)" + echo "============================================================" + exit 2 + fi + + rsb_cmdline=$(readsb_cmdline "$rsb_pid") + degraded=0 + + echo "============================================================" + echo " readsb-wiedehopf -- runtime health" + echo "============================================================" + + # --- [ daemon process ] ------------------------------------------- + echo + echo "[ daemon process ]" + printf ' PID : %s\n' "$rsb_pid" + _PRC_AGE_S= _PRC_CPU_S= _PRC_CPU_PCT= _PRC_RSS_KB= _PRC_VSZ_KB= _PRC_THREADS= + if _status_proc_metrics "$rsb_pid"; then + printf ' process uptime : %s\n' "$(_status_human_secs "$_PRC_AGE_S")" + printf ' CPU lifetime : %s used (~%s%% avg of one core)\n' \ + "$(_status_human_secs "$_PRC_CPU_S")" "$_PRC_CPU_PCT" + printf ' resident memory : %s (virtual: %s)\n' \ + "$(_status_human_kb "$_PRC_RSS_KB")" \ + "$(_status_human_kb "$_PRC_VSZ_KB")" + printf ' threads : %s\n' "$_PRC_THREADS" + else + printf ' process metrics : (proc fs unavailable)\n' + fi + + # --- [ host load ] ------------------------------------------------ + echo + echo "[ host load ]" + sys_up_s=$(_status_sys_uptime_secs) + printf ' system uptime : %s\n' "$(_status_human_secs "$sys_up_s")" + printf ' load average : %s (1m / 5m / 15m)\n' "$(_status_loadavg)" + mem_kb=$(_status_meminfo_kb) + if [ -n "$mem_kb" ]; then + # shellcheck disable=SC2086 + set -- $mem_kb + mem_total=$1; mem_avail=$2 + printf ' memory : %s total / %s available\n' \ + "$(_status_human_kb "$mem_total")" \ + "$(_status_human_kb "$mem_avail")" + else + printf ' memory : (n/a)\n' + fi + if [ -r /proc/sys/kernel/osrelease ]; then + read -r kver < /proc/sys/kernel/osrelease + printf ' kernel : %s\n' "$kver" + fi + + # --- [ runtime ] -------------------------------------------------- + echo + echo "[ runtime ]" + # Live version from receiver.json + drift check vs opkg. + live_version= + [ -r /var/run/readsb/receiver.json ] && \ + live_version=$(_status_json_str /var/run/readsb/receiver.json version) + opkg_version= + [ -r /usr/lib/opkg/info/readsb-wiedehopf.control ] && \ + opkg_version=$(awk -F': *' '/^Version:/{print $2; exit}' \ + /usr/lib/opkg/info/readsb-wiedehopf.control) + if [ -n "$live_version" ]; then + printf ' version (live) : %s\n' "$live_version" + # Compare upstream version tokens only. The daemon emits its + # upstream version possibly followed by build metadata + # ("3.16.11 compiled on 260509"); opkg appends a packaging + # release suffix ("3.16.11-22") and may carry an epoch prefix + # ("1:3.16.11-22"). Reduce both to the bare upstream token + # before comparing -- otherwise drift fires falsely. + live_bare=${live_version%% *} + opkg_bare=${opkg_version#*:} + opkg_bare=${opkg_bare%%-*} + if [ -n "$opkg_bare" ] && [ -n "$live_bare" ] && \ + [ "$opkg_bare" != "$live_bare" ]; then + printf ' ! version drift : opkg has %s but daemon is running %s\n' \ + "$opkg_version" "$live_version" + printf ' fix: /etc/init.d/readsb restart\n' + degraded=1 + fi + else + printf ' version : (receiver.json unavailable)\n' + fi + + # Live mode (what the daemon is actually doing). + dt_live=$(_status_cmdline_arg "$rsb_cmdline" device-type) + if echo " $rsb_cmdline " | grep -q -- ' --net-only'; then + rsb_mode='net-only' + elif [ -n "$dt_live" ]; then + rsb_mode="SDR ($dt_live)" + else + rsb_mode='net-only' + fi + printf ' mode (live) : %s\n' "$rsb_mode" + + # Drift check between UCI intent and live cmdline. + _dt_uci=$(uci -q get readsb.main.device_type) + _no_uci=$(uci -q get readsb.main.net_only) + case "$rsb_mode" in + net-only*) + if [ -n "$_dt_uci" ] && [ "$_no_uci" != 1 ]; then + printf ' ! mode drift : UCI says SDR (%s) but daemon is net-only\n' "$_dt_uci" + printf ' fix: /etc/init.d/readsb restart\n' + degraded=1 + fi + ;; + esac + + # Live listen ports (compact -- only those actually present). + _emit_live_port() { + local label=$1 flag=$2 v + v=$(_status_cmdline_arg "$rsb_cmdline" "$flag") + [ -n "$v" ] && printf ' %-15s : %s\n' "$label" "$v" + } + _emit_live_port 'beast-out' net-bo-port + _emit_live_port 'beast-in' net-bi-port + _emit_live_port 'raw-in' net-ri-port + _emit_live_port 'raw-out' net-ro-port + _emit_live_port 'SBS' net-sbs-port + _emit_live_port 'json' net-json-port + _emit_live_port 'api' net-api-port + _emit_live_port 'beast-reduce' net-beast-reduce-out-port + + # Condensed flag list (excluding the per-feeder --net-connector spam). + flags=$(echo "$rsb_cmdline" | tr ' ' '\n' \ + | awk 'NR>1 && $0 !~ /^--net-connector=/' \ + | tr '\n' ' ' | sed 's/ *$//') + [ -n "$flags" ] && printf ' active flags : %s\n' "$flags" + + # --- [ live data flow ] ------------------------------------------- + echo + echo "[ live data flow ]" + if [ -r /var/run/readsb/aircraft.json ]; then + ac_count=$(grep -c '"hex"' /var/run/readsb/aircraft.json 2>/dev/null) + [ -n "$ac_count" ] || ac_count=0 + ac_msgs=$(_status_json_num /var/run/readsb/aircraft.json messages) + ac_now=$(_status_json_num /var/run/readsb/aircraft.json now) + printf ' aircraft tracked: %s\n' "$ac_count" + printf ' total messages : %s\n' "${ac_msgs:-(n/a)}" + if [ -n "$ac_now" ]; then + ac_now_int=${ac_now%%.*} + snapshot_age=$(( $(date +%s 2>/dev/null || echo 0) - ac_now_int )) + [ "$snapshot_age" -lt 0 ] 2>/dev/null && snapshot_age=0 + printf ' snapshot age : %ds (aircraft.json mtime tick)\n' "$snapshot_age" + # A snapshot more than 30s old means write_json is wedged or + # the daemon is hung -- always degraded. + if [ "$snapshot_age" -gt 30 ]; then + printf ' ! data stale : aircraft.json has not been refreshed in %ds\n' "$snapshot_age" + degraded=1 + fi + fi + else + printf ' aircraft.json : (n/a -- write_json disabled or daemon not yet ready)\n' + fi + if [ -r /var/run/readsb/receiver.json ]; then + rcv_refresh=$(_status_json_num /var/run/readsb/receiver.json refresh) + rcv_lat=$(_status_json_num /var/run/readsb/receiver.json lat) + rcv_lon=$(_status_json_num /var/run/readsb/receiver.json lon) + if [ -n "$rcv_refresh" ]; then + refresh_ms=${rcv_refresh%%.*} + printf ' json refresh : every %d ms\n' "$refresh_ms" + fi + if [ -n "$rcv_lat" ] && [ -n "$rcv_lon" ]; then + printf ' receiver lat/lon: %s, %s (live, from receiver.json)\n' \ + "$rcv_lat" "$rcv_lon" + fi + fi + + # --- [ outbound feeders ] ----------------------------------------- + echo + echo "[ outbound feeders ]" + uci_feeders=$(readsb_count_feeders) + live_connectors=$(_status_cmdline_count "$rsb_cmdline" net-connector) + printf ' net-connectors : %s live (UCI configured: %s)\n' \ + "${live_connectors:-0}" "$uci_feeders" + + # Defer the per-feeder breakdown to readsb-feeder --health -- it + # already does the TCP probe + cmdline-presence + log scan with the + # right granularity. We summarize and bubble up the rc. + if [ "$uci_feeders" -gt 0 ]; then + # Capture stdout so we can both display and grade it. + _health_feeder_out=$(readsb-feeder --health 2>&1) + _health_feeder_rc=$? + # Indent the per-feeder report by two spaces for visual nesting. + echo "$_health_feeder_out" | sed 's/^/ /' + case $_health_feeder_rc in + 0) ;; # all LIVE+reachable + 2) degraded=1 ;; # any DEGRADED / UNREACHABLE / drift + 3) ;; # no enabled feeders + *) degraded=1 ;; + esac + else + printf ' (no feeders configured -- run: readsb-setup, or: readsb-feeder)\n' + fi + + # --- [ recent log scan: readsb ] ---------------------------------- + echo + echo "[ recent log scan: readsb ]" + if command -v logread >/dev/null 2>&1; then + # Parsed read-out of the most recent COMPLETE periodic stats + # block. Falls back below to the single-line "Statistics:" header + # when no complete block is currently buffered. + if readsb_render_stats_summary " "; then + # Renderer flips readsb_stats_loss_seen=1 when the daemon + # reported non-zero samples_dropped/lost; propagate to our + # DEGRADED grade so cron/monitoring notice USB overruns. + [ "$readsb_stats_loss_seen" = 1 ] && degraded=1 + else + # No complete block buffered -- keep the old one-line + # fallback so something is always shown. + last_stats=$(readsb_log_last_stats) + if [ -n "$last_stats" ]; then + printf ' last stats line :\n' + printf ' %s\n' "$last_stats" + else + printf ' last stats block: (none in current syslog buffer)\n' + fi + fi + err_total=$(readsb_log_count_errors) + printf ' error events : %s (in last 200 readsb-tagged lines)\n' "$err_total" + if [ "$err_total" -gt 0 ]; then + # Show one representative line. Pattern mirrors + # readsb_log_count_errors -- see functions.sh for why we + # avoid a blanket daemon.err match. + err_sample=$(readsb_log_recent 200 | awk ' + /daemon\.warn|daemon\.crit|daemon\.alert|daemon\.emerg|Bad connection|connection lost|dropping data|[^0-9][1-9][0-9]* samples (dropped|lost)/ { last = $0 } + END { if (last) print last } + ') + [ -n "$err_sample" ] && printf ' most recent :\n %s\n' "$err_sample" + degraded=1 + fi + else + printf ' (logread unavailable on this system)\n' + fi + + # --- [ recent log scan: ] ----------------------------- + # One section per *installed* companion that declares a syslog tag. + # Skipped silently otherwise. + if command -v logread >/dev/null 2>&1; then + comp_pkgs=$(readsb_companion_pkgs_all 2>/dev/null) + for cpkg in $comp_pkgs; do + readsb_pkg_installed "$cpkg" || continue + ctag=$(readsb_pkg_log_tag "$cpkg") + [ -n "$ctag" ] || continue + + echo + echo "[ recent log scan: $cpkg ]" + last_line=$(readsb_pkg_log_last_line "$ctag") + if [ -n "$last_line" ]; then + printf ' last log line :\n' + printf ' %s\n' "$last_line" + else + printf ' last log line : (none in current syslog buffer)\n' + fi + cerr=$(readsb_pkg_log_count_errors "$ctag") + printf ' error events : %s (in last 200 %s-tagged lines)\n' \ + "$cerr" "$ctag" + if [ "$cerr" -gt 0 ]; then + cerr_sample=$(readsb_pkg_log_last_error "$ctag") + [ -n "$cerr_sample" ] && printf ' most recent :\n %s\n' "$cerr_sample" + # Flag DEGRADED so cron/monitoring sees the issue. + degraded=1 + fi + done + fi + + echo + echo "============================================================" + if [ "$degraded" -eq 0 ]; then + echo " health summary : HEALTHY" + echo "============================================================" + exit 0 + fi + echo " health summary : DEGRADED -- see warnings above" + echo " for static/config questions run: readsb-setup --status" + echo " for the stats block alone run: readsb-setup --stats" + echo " for per-feeder detail run: readsb-feeder --health []" + echo "============================================================" + exit 1 +fi + +# --- wizard mode (default) ------------------------------------------------- + +wiz_available || { + echo "readsb-setup: no controlling tty; run interactively from a shell" >&2 + echo " for a non-interactive state summary use: readsb-setup --status" >&2 + exit 2 +} + +wiz_say "" +wiz_say "============================================================" +wiz_say " readsb-wiedehopf -- guided setup" +wiz_say "============================================================" +wiz_say "Configures location, station UUID, SDR tuning, aggregator" +wiz_say "feeders, and reloads the service. Each step is skippable;" +wiz_say "re-runs are idempotent." +wiz_say "" +wiz_say "To abort at any prompt: type 'q', 'quit', or 'exit' (Ctrl-D" +wiz_say "also works). Each completed step is saved immediately; aborting" +wiz_say "stops the wizard but keeps changes from completed steps." +wiz_say "" +wiz_say "Read-only inspection (no prompts):" +wiz_say " readsb-setup --status -- configuration check (essentials," +wiz_say " mode, ports, SDRs, feeders, companions)" +wiz_say " readsb-setup --health -- runtime health (process metrics," +wiz_say " live data flow, feeder probe + log scan)" +wiz_say " readsb-setup --stats -- only the most recent stats block" +wiz_say " (signal, decoded msgs, tracks, CPU)" +wiz_say " readsb-setup --config -- pretty-printed /etc/config/readsb" +wiz_say " (+ each installed companion's UCI config)" +wiz_say " readsb-setup --help -- re-print the post-install banner" +wiz_say "" +wiz_say "Per-area CLIs (each accepts -h / --help):" +wiz_say " readsb-feeder -- guided add of one new feeder" +wiz_say " readsb-feeder --list -- show configured feeders" +wiz_say " readsb-feeder --probe -- TCP-probe each enabled feeder" +wiz_say " readsb-feeder --health -- per-feeder runtime check (probe +" +wiz_say " cmdline + recent error log scan)" +wiz_say " readsb-feeder --url -- public per-station stats URL(s)" +wiz_say " readsb-uuid -- interactive UUID wizard" +wiz_say " readsb-uuid --print -- just print the current UUID" +wiz_say " readsb-geoip -- auto-fill lat/lon from public IP" +wiz_say "" +wiz_say "(see 'readsb-setup --help' for the full command list)" +wiz_say "" + + +changes_made=0 +sc=$(readsb_count_sdrs) + +# Set readsb.main.=; bail with rc 2 on UCI failure. +_set_main() { + uci -q set "readsb.main.$1=$2" || { _err "uci set $1 failed"; exit 2; } +} +_commit_main() { + uci -q commit readsb || { _err "uci commit failed"; exit 2; } +} + +# Run readsb-uuid (sibling binary, always installed); on success update +# changes_made and announce the new UUID. +_run_uuid() { + local flags=$1 new_uuid + if readsb-uuid $flags >/dev/null; then + new_uuid=$(uci -q get readsb.main.uuid) + wiz_say " UUID: $new_uuid" + changes_made=1 + else + wiz_say " readsb-uuid failed" + fi +} + +# --- step 1: location ------------------------------------------------------ +wiz_say "[1/5] station location (lat/lon)" +cur_lat=$(uci -q get readsb.main.lat 2>/dev/null) +cur_lon=$(uci -q get readsb.main.lon 2>/dev/null) +cur_geoip_auto=$(uci -q get readsb.main.geoip_auto 2>/dev/null) +if [ -n "$cur_lat" ] && [ -n "$cur_lon" ]; then + wiz_say " current: lat=$cur_lat lon=$cur_lon" + wiz_yesno upd " change location?" N || exit 1 +else + wiz_say " no location set yet; readsb needs lat/lon for CPR decoding" + upd=1 +fi + +if [ "$upd" = 1 ]; then + wiz_say " source:" + wiz_choose loc_src " pick a source" \ + "auto-detect from public IP (city-level, requires WAN)" \ + "enter coordinates manually" \ + "skip" || exit 1 + case $loc_src in + auto-detect*) + wiz_say " This lookup sends an HTTPS request from this router to" + wiz_say " ipapi.co; if that fails, it tries ipwho.is. The services" + wiz_say " see the router's public IP and return city-level coordinates." + wiz_yesno geoip_once " allow this one-time third-party lookup?" N || exit 1 + if [ "$geoip_once" = 1 ]; then + wiz_say " running readsb-geoip --force ..." + readsb-geoip --force + rc=$? + if [ "$rc" = 0 ]; then + changes_made=1 + _notice "geoip auto-fill OK" + wiz_say " geoip done" + else + _warn "geoip auto-fill failed (rc=$rc); user can retry" + wiz_say " geoip failed; you can re-run later or set lat/lon manually" + fi + else + wiz_say " lookup skipped; no third-party geolocation request sent" + fi + ;; + enter*) + wiz_ask_validated lat " latitude (-90 .. 90)" "$cur_lat" wiz_v_lat \ + "latitude must be a number between -90 and 90" || exit 1 + wiz_ask_validated lon " longitude (-180 .. 180)" "$cur_lon" wiz_v_lon \ + "longitude must be a number between -180 and 180" || exit 1 + _set_main lat "$lat" + _set_main lon "$lon" + _commit_main + _notice "set lat=$lat lon=$lon" + changes_made=1 + ;; + *) ;; + esac +fi + +wiz_say " automatic lookup on future starts: $([ "$cur_geoip_auto" = 1 ] && echo enabled || echo disabled)" +wiz_say " When enabled, it contacts the same providers only if an enabled" +wiz_say " readsb section has no latitude or longitude." +wiz_yesno geoip_auto " allow automatic third-party lookup when coordinates are missing?" \ + "$([ "$cur_geoip_auto" = 1 ] && echo Y || echo N)" || exit 1 +if [ "$geoip_auto" != "${cur_geoip_auto:-0}" ]; then + _set_main geoip_auto "$geoip_auto" + _commit_main + _notice "set geoip_auto=$geoip_auto" + changes_made=1 +fi +wiz_say "" + +# --- step 2: UUID ---------------------------------------------------------- +wiz_say "[2/5] station UUID" +cur_uuid=$(uci -q get readsb.main.uuid 2>/dev/null) +if readsb_is_uuid "$cur_uuid"; then + wiz_say " current: $cur_uuid" + wiz_yesno regen " regenerate? (loses any aggregator registration tied to this UUID)" N || exit 1 + [ "$regen" = 1 ] && _run_uuid -f +else + wiz_say " no station UUID set" + wiz_yesno gen " generate one now?" Y || exit 1 + [ "$gen" = 1 ] && _run_uuid --auto +fi +wiz_say "" + +# --- step 3: SDR tuning ---------------------------------------------------- +# Auto-skipped on net-only deployments. +wiz_say "[3/5] SDR tuning" +if [ "$sc" -eq 0 ]; then + wiz_say " no RTL-SDR detected on USB; skipping (net-only deployment)" + wiz_say " (re-run 'readsb-setup' after plugging an SDR to tune gain etc.)" +else + wiz_say " $sc RTL-SDR(s) detected" + cur_gain=$(uci -q get readsb.main.gain 2>/dev/null) + cur_ppm=$(uci -q get readsb.main.ppm 2>/dev/null) + cur_agc=$(uci -q get readsb.main.enable_agc 2>/dev/null) + cur_bias=$(uci -q get readsb.main.enable_biastee 2>/dev/null) + wiz_say " current: gain=${cur_gain:-auto} ppm=${cur_ppm:-0} agc=${cur_agc:-0} bias-t=${cur_bias:-0}" + wiz_yesno tune " tune SDR settings?" N || exit 1 + if [ "$tune" = 1 ]; then + wiz_ask_validated new_gain " gain ('auto', 'max', or 0..50 dB)" "${cur_gain:-auto}" wiz_v_gain \ + "gain must be 'auto', 'max', or a number 0..50" || exit 1 + wiz_ask_validated new_ppm " PPM correction (-100..100, 0 for none)" "${cur_ppm:-0}" wiz_v_ppm \ + "PPM must be a number between -100 and 100" || exit 1 + + wiz_yesno new_agc " enable AGC? (most setups: no)" \ + "$([ "$cur_agc" = 1 ] && echo Y || echo N)" || exit 1 + wiz_yesno new_bias " enable bias-T? (powers an LNA on the antenna line; default: no)" \ + "$([ "$cur_bias" = 1 ] && echo Y || echo N)" || exit 1 + + _set_main gain "$new_gain" + _set_main ppm "$new_ppm" + _set_main enable_agc "$new_agc" + _set_main enable_biastee "$new_bias" + _commit_main + _notice "set gain=$new_gain ppm=$new_ppm agc=$new_agc bias-t=$new_bias" + changes_made=1 + fi +fi +wiz_say "" + +# --- step 4: feeders ------------------------------------------------------- +# Management menu loop: add / manage existing / done. +wiz_say "[4/5] aggregator feeders" + +# Reused after each mutating action so state is always fresh. +_step4_show_state() { + local _fc + _fc=$(readsb_count_feeders) + if [ "$_fc" -gt 0 ]; then + wiz_say " $_fc 'config feeder' section(s) currently configured:" + readsb-feeder --list 2>/dev/null | sed 's/^/ /' >/dev/tty + else + wiz_say " no feeders defined yet" + fi +} + +# Echo space-separated names of all `config feeder` sections (sorted). +_step4_feeder_names() { + uci -q show readsb 2>/dev/null \ + | awk -F. '/^readsb\.[^.]+=feeder$/ { sub(/=feeder$/, "", $2); print $2 }' \ + | sort -u +} + +_step4_show_state +wiz_say "" + +while :; do + fc=$(readsb_count_feeders) + if [ "$fc" -gt 0 ]; then + wiz_choose action " next feeder action" \ + "add a new feeder" \ + "manage an existing feeder" \ + "done with feeders" \ + || exit 1 + else + wiz_choose action " next feeder action" \ + "add a new feeder" \ + "done with feeders" \ + || exit 1 + fi + + case $action in + add*) + if readsb-feeder; then + changes_made=1 + else + wiz_say " feeder add cancelled" + fi + wiz_say "" + _step4_show_state + wiz_say "" + ;; + manage*) + # shellcheck disable=SC2046 + set -- $(_step4_feeder_names) + if [ "$#" -eq 0 ]; then + wiz_say " (no feeder sections to manage)" + wiz_say "" + continue + fi + wiz_choose target " pick a feeder" "$@" "back" || continue + [ "$target" = back ] && continue + + cur_enabled=$(uci -q get "readsb.${target}.enabled") + cur_preset=$(uci -q get "readsb.${target}.preset") + wiz_say " '$target': preset=${cur_preset:-?} enabled=${cur_enabled:-0}" + + wiz_choose op " action for '$target'" \ + "enable" \ + "disable" \ + "remove" \ + "back" \ + || continue + + case $op in + enable) + if readsb-feeder --enable "$target" >/dev/tty 2>&1; then + changes_made=1 + # Enabling triggers syslog companion warnings; + # also offer interactive install while we have a tty. + cur_preset=$(uci -q get "readsb.${target}.preset") + wiz_offer_install_companions "$cur_preset" "feeder '$target'" || exit 1 + fi + ;; + disable) + if readsb-feeder --disable "$target" >/dev/tty 2>&1; then + changes_made=1 + fi + ;; + remove) + wiz_yesno yn " really remove feeder '$target'?" N || continue + if [ "$yn" = 1 ]; then + if readsb-feeder --remove "$target" >/dev/tty 2>&1; then + changes_made=1 + fi + fi + ;; + back) ;; + esac + wiz_say "" + _step4_show_state + wiz_say "" + ;; + done*) + break + ;; + esac +done +wiz_say "" + +# --- step 5: reload -------------------------------------------------------- +wiz_say "[5/5] reload service" +if [ "$changes_made" = 0 ]; then + wiz_say " no changes were made; nothing to reload" + wiz_say "" + wiz_say "done. Run 'readsb-setup --status' any time for a summary." + exit 0 +fi + +if [ -x /etc/init.d/readsb ]; then + wiz_yesno do_reload " run 'service readsb reload' now?" Y || do_reload=0 + if [ "$do_reload" = 1 ]; then + if /etc/init.d/readsb reload; then + _notice "applied changes via 'service readsb reload'" + wiz_say " reload OK" + else + _warn "'service readsb reload' returned non-zero" + wiz_say " reload failed; check 'logread -e readsb'" + fi + else + _notice "changes committed but reload deferred by user" + wiz_say " remember to run 'service readsb reload' to apply" + fi +else + wiz_say " /etc/init.d/readsb not present; nothing to reload" +fi +wiz_say "" +wiz_say "done. Useful follow-ups:" +wiz_say " readsb-setup --status -- configuration check (essentials)" +wiz_say " readsb-setup --health -- runtime health (is it working?)" +wiz_say " readsb-setup --config -- pretty-printed /etc/config/readsb" +wiz_say " (+ each installed companion's UCI config)" +wiz_say " readsb-setup --help -- re-print the post-install banner" +wiz_say " readsb-feeder --list -- see what's configured" +wiz_say " readsb-feeder --health -- per-feeder probe + log scan" +wiz_say " readsb-feeder --url -- public stats URLs" +wiz_say " readsb-feeder --companions -- optional companion packages per feeder" +wiz_say " logread -e readsb -- daemon + helper logs" +exit 0 diff --git a/utils/readsb-wiedehopf/files/readsb-uuid b/utils/readsb-wiedehopf/files/readsb-uuid new file mode 100644 index 00000000000000..1e17f41975ab27 --- /dev/null +++ b/utils/readsb-wiedehopf/files/readsb-uuid @@ -0,0 +1,153 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-only +# Copyright (C) 2026 OpenWrt.org + +# readsb-uuid -- inspect / generate / regenerate the station UUID. +# +# Manages the station UUID stored in readsb.@readsb[0].uuid (i.e. +# /etc/config/readsb, the `main` section). Aggregators that +# authenticate by UUID need a stable value over time -- DO NOT +# regenerate after registering. +# +# Usage: +# readsb-uuid -- interactive wizard (falls back to +# --auto when stdin is not a tty) +# +# Read-only inspection (no UCI changes): +# readsb-uuid -p, --print -- print the existing UUID; print +# nothing (rc 0) if unset +# +# Mutating (commits to UCI; does NOT restart the daemon): +# readsb-uuid -a, --auto -- print the current UUID if valid, +# otherwise generate one (cron safe) +# readsb-uuid -f, --force -- regenerate even if a UUID is set +# (DESTRUCTIVE -- aggregator +# registration tied to the old UUID +# is lost) +# +# readsb-uuid -h, --help -- this options banner +# +# After any mutating run: `service readsb reload`. +# +# Exit codes: +# 0 success (UUID printed and/or written; wizard cancelled cleanly) +# 1 no entropy source available +# 2 UCI write failed / unknown argument +# +# Full reference: +# https://github.com/openwrt/packages/blob/master/utils/readsb-wiedehopf/README.md + +# -h reachable without sourcing functions.sh. +print_help() { + sed -n '/^# readsb-uuid /,/^$/p' "$0" | sed 's/^# \{0,1\}//' +} + +case ${1:-} in + -h|--help) print_help; exit 0 ;; +esac + +# shellcheck source=files/readsb.functions.sh +. /usr/lib/readsb/functions.sh + +READSB_LOG_TAG=readsb-uuid + +mode=wizard +while [ "$#" -gt 0 ]; do + case $1 in + -p|--print) mode=print ;; + -a|--auto) mode=auto ;; + -f|--force) mode=force ;; + -h|--help) print_help; exit 0 ;; + *) + echo "readsb-uuid: unknown argument: $1 (try -h)" >&2 + exit 2 + ;; + esac + shift +done + +# Bare invocation falls back to --auto when there is no controlling +# terminal, so cron / hotplug / postinst paths keep working unchanged. +if [ "$mode" = wizard ] && ! wiz_available; then + mode=auto +fi + +# Atomic UUID write: set + commit, with consistent error reporting. +_write_uuid() { + local new=$1 + if ! uci -q set "readsb.main.uuid=$new"; then + _err "uci set failed (is the readsb 'main' section present?)" + return 2 + fi + uci -q commit readsb || { _err "uci commit failed"; return 2; } + _notice "wrote readsb.main.uuid=$new (run '/etc/init.d/readsb reload' to apply)" + return 0 +} + +# Generate + write a new UUID; print it on success. +_gen_and_write() { + local new + new=$(readsb_gen_uuid) || { + _err "no UUID source: /proc/sys/kernel/random/uuid missing and uuidgen not installed" + return 1 + } + _write_uuid "$new" || return $? + echo "$new" +} + +current=$(uci -q get readsb.main.uuid) + +case $mode in + print) + [ -n "$current" ] && echo "$current" + exit 0 + ;; + force) + _gen_and_write + exit $? + ;; + auto) + # Old default behavior: print existing if valid, else generate. + if [ -n "$current" ]; then + if readsb_is_uuid "$current"; then + echo "$current" + exit 0 + fi + _warn "existing UUID '$current' is malformed; regenerating" + fi + _gen_and_write + exit $? + ;; + wizard) + wiz_say "" + wiz_say "=== readsb-uuid ===" + wiz_say "Manage the station UUID stored in readsb.main.uuid." + wiz_say "Aggregators authenticate by UUID; regenerating after" + wiz_say "registering loses that registration." + wiz_say "" + wiz_say "Abort: 'q', 'quit', 'exit' (or Ctrl-D); UUID is left untouched." + wiz_say "" + if [ -n "$current" ] && readsb_is_uuid "$current"; then + wiz_say " current: $current" + wiz_yesno regen " regenerate? (DESTRUCTIVE)" N || exit 0 + if [ "$regen" = 1 ]; then + _gen_and_write + exit $? + fi + # Keeping the existing UUID -- print it for convenience. + echo "$current" + exit 0 + fi + if [ -n "$current" ]; then + wiz_say " current: $current (malformed -- not a valid UUID)" + else + wiz_say " no station UUID set" + fi + wiz_yesno gen " generate one now?" Y || exit 0 + if [ "$gen" = 1 ]; then + _gen_and_write + exit $? + fi + exit 0 + ;; +esac diff --git a/utils/readsb-wiedehopf/files/readsb.config b/utils/readsb-wiedehopf/files/readsb.config new file mode 100644 index 00000000000000..934a077eb103d9 --- /dev/null +++ b/utils/readsb-wiedehopf/files/readsb.config @@ -0,0 +1,127 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright (C) 2026 OpenWrt.org +# +# /etc/config/readsb -- declarative UCI config for the readsb-wiedehopf +# package. Manage interactively with `readsb-setup`, `readsb-feeder`, +# `readsb-uuid`, `readsb-geoip` (each accepts -h / --help) instead of +# editing this file by hand. Comments in UCI config files are NOT +# preserved across `uci commit`; documentation lives in those CLIs and +# in the package README. +# +# Full reference, aggregator preset list, operational notes, and the +# list of out-of-scope aggregators (FlightAware, FR24, RadarBox, +# Planefinder) are in: +# https://github.com/openwrt/packages/blob/master/utils/readsb-wiedehopf/README.md + +config readsb 'main' + option enabled '1' + option hotplug '1' + option beast_crc_off '0' + option beast_df045_on '0' + option beast_df1117_on '0' + option beast_fec_off '0' + option beast_mlat_off '0' + option beast_modeac '0' + option beast_baudrate '' + option beast_serial '' + option dcfilter '0' + option debug '' + option device '' + option device_type '' + option enable_agc '0' + option enable_biastee '0' + option fix '1' + option freq '' + option forward_mlat '0' + option forward_mlat_sbs '0' + option gain 'auto' + option gnss '0' + option json_location_accuracy '2' + option json_separate_alt_ground '' + option json_trace_hist_only '' + option lat '' + option lon '' + option geoip_auto '0' + option geoip_wait_timeout '60' + option geoip_wait_interval '10' + option usb_wait_timeout '0' + option usb_wait_interval '2' + option max_range '450' + option metric '0' + option modeac '0' + option modeac_auto '0' + option net '1' + option net_api_port '' + option net_asterix_in_port '' + option net_asterix_out_port '' + option net_asterix_reduce '0' + option net_beast_reduce_filter_alt '' + option net_beast_reduce_filter_dist '' + option net_beast_reduce_interval '' + option net_beast_reduce_optimize_for_mlat '0' + option net_beast_reduce_out_port '30006' + option net_bi_port '30004,30104' + option net_bind_address '127.0.0.1' + option net_bo_port '30005' + option net_buffer '' + option net_connector_delay '' + option net_garbage '' + option net_heartbeat '' + option net_ingest '0' + option net_json_port '' + option net_json_port_include_noposition '0' + option net_json_port_interval '' + option net_only '1' + option net_receiver_id '0' + option net_ri_port '30001' + option net_ro_port '30002' + option net_ro_interval '' + option net_ro_interval_beast_reduce '' + option net_ro_size '' + option net_sbs_in_port '' + option net_sbs_jaero_in_port '' + option net_sbs_jaero_port '' + option net_sbs_port '30003' + option net_sbs_reduce '0' + option net_uat_in_port '' + option net_uat_replay_port '' + option net_verbatim '0' + option net_vrs_interval '' + option net_vrs_port '' + option no_fix_df '0' + option onlyaddr '0' + option ppm '0' + option position_persistence '' + option preamble_threshold '' + option process_only '' + option range_outline_hours '24' + option sdr_buffer_size '' + option show_only '' + option stats '1' + option stats_every '120' + option stats_range '0' + option uuid_file '' + option write_globe_history '' + option write_json '/var/run/readsb' + option write_json_every '1' + option write_json_globe_index '0' + option write_json_gzip '0' + option write_prom '' + option write_receiver_id_json '0' + option write_state '' + option write_state_every '' + option write_state_only_on_exit '0' + option db_file '' + option db_file_lt '0' + option decode_threads '' + option ac_hash_bits '' + option api_shutdown_delay '' + option full_trace_dir '' + option heatmap '' + option heatmap_dir '' + option jaero_timeout '' + option json_reliable '' + option json_trace_interval '' + option tar1090_use_api '0' + option uuid '' + option extra_args '' diff --git a/utils/readsb-wiedehopf/files/readsb.functions.sh b/utils/readsb-wiedehopf/files/readsb.functions.sh new file mode 100644 index 00000000000000..5cb6ef02ed80c8 --- /dev/null +++ b/utils/readsb-wiedehopf/files/readsb.functions.sh @@ -0,0 +1,1253 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright (C) 2026 OpenWrt.org +# +# shellcheck shell=sh +# Shared POSIX sh helpers for readsb-wiedehopf, sourced by every +# helper script. See `readsb-setup --help` for the user CLI map. + +# --- logging --------------------------------------------------------------- +# Tag overridable via READSB_LOG_TAG. View: logread -e readsb +: "${READSB_LOG_TAG:=readsb}" +_log() { logger -t "$READSB_LOG_TAG" -p daemon.info -- "$@"; } +_notice() { logger -t "$READSB_LOG_TAG" -p daemon.notice -- "$@"; } +_warn() { logger -t "$READSB_LOG_TAG" -p daemon.warn -- "$@"; } +_err() { logger -t "$READSB_LOG_TAG" -p daemon.err -- "$@"; } +_debug() { logger -t "$READSB_LOG_TAG" -p daemon.debug -- "$@"; } + +# --- USB SDR identification ------------------------------------------------ +# RTL2832U USB IDs from librtlsdr's known_devices[]. To regenerate: +# awk '/static rtlsdr_dongle_t known_devices/,/^};/' src/librtlsdr.c \ +# | awk -F'[ ,{}]+' '/0x[0-9a-fA-F]+, 0x[0-9a-fA-F]+/ \ +# { printf "%s:%s\n", substr($2,3), substr($3,3) }' +readsb_is_sdr_id() { + case "$1" in + 0bda:2832|0bda:2838|\ + 0413:6680|0413:6f0f|\ + 0458:707f|\ + 0ccd:00a9|0ccd:00b3|0ccd:00b4|0ccd:00b5|0ccd:00b7|0ccd:00b8|\ + 0ccd:00b9|0ccd:00c0|0ccd:00c6|0ccd:00d3|0ccd:00d7|0ccd:00e0|\ + 1209:2832|\ + 1554:5020|\ + 15f4:0131|15f4:0133|\ + 185b:0620|185b:0650|185b:0680|\ + 1b80:d393|1b80:d394|1b80:d395|1b80:d397|1b80:d398|1b80:d39d|\ + 1b80:d3a4|1b80:d3a8|1b80:d3af|1b80:d3b0|\ + 1d19:1101|1d19:1102|1d19:1103|1d19:1104|\ + 1f4d:a803|1f4d:b803|1f4d:c803|1f4d:d286|1f4d:d803) return 0 ;; + esac + return 1 +} + +# Invoke for each attached RTL-SDR. +readsb_for_each_sdr() { + local cb=$1 d vid pid + for d in /sys/bus/usb/devices/*; do + [ -r "$d/idVendor" ] && [ -r "$d/idProduct" ] || continue + read -r vid < "$d/idVendor" + read -r pid < "$d/idProduct" + readsb_is_sdr_id "$vid:$pid" || continue + "$cb" "$d" "$vid" "$pid" + done +} + +# 0 on the first attached RTL-SDR (no full enumeration). +readsb_is_sdr_present() { + local d vid pid + for d in /sys/bus/usb/devices/*; do + [ -r "$d/idVendor" ] && [ -r "$d/idProduct" ] || continue + read -r vid < "$d/idVendor" + read -r pid < "$d/idProduct" + readsb_is_sdr_id "$vid:$pid" && return 0 + done + return 1 +} + +# One line per attached RTL-SDR: serial, or "NOSERIAL" if missing. +readsb_list_sdrs() { + readsb_for_each_sdr _readsb_emit_serial +} +_readsb_emit_serial() { + local d=$1 ser="NOSERIAL" s + if [ -r "$d/serial" ]; then + IFS= read -r s < "$d/serial" + s=$(readsb_sanitize_serial "$s") + [ -n "$s" ] && ser=$s + fi + echo "$ser" +} + +# 0 when an attached SDR has exactly this sanitized serial. +readsb_sdr_serial_present() { + readsb_list_sdrs | grep -Fqx -e "$1" +} + +# Integer count of attached RTL-SDRs. +readsb_count_sdrs() { + _readsb_sdr_count=0 + readsb_for_each_sdr _readsb_inc_count + echo "$_readsb_sdr_count" +} +_readsb_inc_count() { _readsb_sdr_count=$((_readsb_sdr_count + 1)); } + +# Strip control bytes from a librtlsdr serial while preserving printable identity. +readsb_sanitize_serial() { + printf '%s' "$1" | LC_ALL=C tr -d '\000-\037\177' +} + +# 0 when a numeric librtlsdr device index exists for the current SDR count. +readsb_device_index_valid() { + awk -v d="$1" -v count="$2" 'BEGIN { + exit !(d ~ /^[0-9]+$/ && d+0 >= 0 && d+0 < count) + }' +} + +# Normalize a UCI freq value to integer MHz. Accepts "1090", +# "1090MHz", "1090m", "1090000000". Empty -> "1090". Bad -> "". +readsb_freq_to_mhz() { + local f=$1 + [ -n "$f" ] || { echo 1090; return 0; } + f=$(printf '%s' "$f" | tr -d ' \t' | sed 's/[Mm][Hh]\{0,1\}[Zz]\{0,1\}$//') + case "$f" in + ''|*[!0-9]*) echo ""; return 1 ;; + esac + if [ "$f" -ge 1000000 ]; then + echo $((f / 1000000)) + else + echo "$f" + fi +} + +# --- generic poll-with-timeout --------------------------------------------- +# readsb_wait_until