Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
115 commits
Select commit Hold shift + click to select a range
9708f5f
fix(gate): refuse a 2nd client instead of hanging when radio is in us…
nigelfenton Jul 15, 2026
87de036
feat(ic9700): CI-V SET-menu settings read/write facility (found the L…
nigelfenton Jul 15, 2026
55f4a54
fix(icom9700): return None (not floor pixels) when scope has no fresh…
nigelfenton Jul 16, 2026
6e17e85
chore: bump version to 0.2.0
nigelfenton Jul 16, 2026
63530ae
feat(hpsdr): HPSDR Protocol-1 adapter — Radioberry/HL2 IQ through the…
nigelfenton Jul 16, 2026
c378ae1
feat(hpsdr): live RF-gain slider, span-to-scale, native-centered pan
nigelfenton Jul 16, 2026
92d9fcf
feat(hpsdr): add RX audio (SSB demod) — get_audio()
nigelfenton Jul 16, 2026
50c6f7a
fix(hpsdr): conjugate IQ — corrects the inverted sideband / spectrum …
nigelfenton Jul 16, 2026
33a5a0e
fix(hpsdr): decouple EP2 sends from EP6 reads — stream was ~10x starved
nigelfenton Jul 16, 2026
57d6c67
docs(hpsdr): RETRACT the starvation claim in 33a5a0e — it was a measu…
nigelfenton Jul 16, 2026
3bcad1b
docs(hpsdr): TX design plan — phased, dummy-load-first, blocked on da…
nigelfenton Jul 16, 2026
d9a49d3
docs(hpsdr): Radioberry PA hat confirmed — T/R switch + I2C LPF bank …
nigelfenton Jul 16, 2026
3b405b4
docs(hpsdr): Phase 0 ALREADY PASSES — the 'AE sends no dax_tx' blocke…
nigelfenton Jul 16, 2026
c18640a
docs(hpsdr): LPF band-select is the biggest TX risk — sources CONFLIC…
nigelfenton Jul 16, 2026
3e9263c
docs(hpsdr): HPSDR clients have a filter-board PRESENT setting — host…
nigelfenton Jul 16, 2026
410fe31
docs(hpsdr): Phase 0b root cause — no dax_tx stream id, per-AE-CONNEC…
nigelfenton Jul 16, 2026
ddc164b
feat(9700): refuse a bare-carrier key — digital mode with no dax_tx s…
nigelfenton Jul 16, 2026
c977de7
docs(hpsdr): do NOT port the bare-carrier guard yet — it would CREATE…
nigelfenton Jul 16, 2026
68b06e5
docs(hpsdr): LPF question RESOLVED from source — the Radioberry's OWN…
nigelfenton Jul 16, 2026
b680821
docs(hpsdr): EP2 TX cadence MEASURED on the Pi — Python sustains it i…
nigelfenton Jul 16, 2026
dec86f1
docs(hpsdr): currentfreq tracks RX1 ONLY — split TX would use the WRO…
nigelfenton Jul 16, 2026
3bce08e
docs(hpsdr): Phase 2 — MOX pacing is settled, not open
nigelfenton Jul 16, 2026
e75dbd6
docs(hpsdr): CONFIRMED — PA temp/current + FWD/REV sensors exist, and…
nigelfenton Jul 16, 2026
ac40968
docs(hpsdr): telemetry decoded live — NO fwd/rev/current on this boar…
nigelfenton Jul 16, 2026
b6e9cf6
docs(hpsdr): sensors are a HARDWARE absence, not a config — Nigel's P…
nigelfenton Jul 16, 2026
ec7ac6a
docs(hpsdr): retarget TX at the incoming HL2 — it has the telemetry t…
nigelfenton Jul 16, 2026
a1fd077
feat(hpsdr): Phase 1c — decode EP6 response telemetry (temp/fwd/rev/c…
nigelfenton Jul 16, 2026
dad0ff6
feat(hpsdr): surface telemetry to AE as a real SWR meter — or none at…
nigelfenton Jul 16, 2026
65aa742
docs(hpsdr): filter-board SWR is the SAME sensor as the HL2's native …
nigelfenton Jul 16, 2026
f5c4934
docs: design note — treat the RTL dongle as a shared resource, not a …
nigelfenton Jul 16, 2026
942872a
docs: RTL is on an ANTENNA, not an IF tap — Nigel's instinct was righ…
nigelfenton Jul 16, 2026
2dc30ef
docs: sketch — name SDR dongles as sources, named by what they're CON…
nigelfenton Jul 16, 2026
1d1a23f
docs: a source's capability comes from its RF CHAIN, not its chip — a…
nigelfenton Jul 16, 2026
c787102
fix(core): iq_to_dbm subsampled before the FFT — FFT the whole block,…
nigelfenton Jul 16, 2026
e89436d
chore(release): v0.3.0 — HPSDR adapter, telemetry/SWR, bare-carrier g…
nigelfenton Jul 16, 2026
24eeb3e
Merge pull request #23 from nigelfenton/feat/hpsdr-adapter
nigelfenton Jul 16, 2026
9bb836f
fix(kenwood): set_span() advertised a span the dongle does not deliver
nigelfenton Jul 16, 2026
db72638
feat(core): AETHER_GATE_PROFILE instrumentation — and Nigel's questio…
nigelfenton Jul 16, 2026
d0fc028
fix(soapy): the 0.5 s tick was librtlsdr's 262144-byte USB lump — siz…
nigelfenton Jul 16, 2026
b8a4c75
fix(dev): scope probes passed civ_addr positionally into split_wf (#24)
Ozy311 Jul 22, 2026
4797c96
feat(icom): drive the LAN adapter from the radios.py row (--icom-mode…
Ozy311 Jul 22, 2026
3922f97
feat(cli): AETHER_GATE_* env defaults and a --rx-only transmit lock (…
Ozy311 Jul 22, 2026
b0d5ec9
feat(docker): container image, compose example, docs and CI (#27)
Ozy311 Jul 22, 2026
60ddfc2
fix(ci): lowercase the GHCR image name so the docker build can tag (#28)
nigelfenton Jul 22, 2026
0730ed3
feat(ci): publish immutable version tags on vX.Y.Z releases (#29)
nigelfenton Jul 22, 2026
b5bd385
fix(hpsdr): seed NCO on connect (#31) + wire real S-meter to AE (#30)…
nigelfenton Jul 22, 2026
8223cc4
feat(deploy): flashable Pi appliance image builder (chroot-bake offic…
nigelfenton Aug 1, 2026
3d20b31
docs(pi): prebuilt-image flash flow; Pi 3 boot verified; Pi 1/2 expli…
nigelfenton Aug 1, 2026
7a336ae
feat(deploy): SDRplay (RSP1a-family) support — pinned API 3.15.2 + So…
nigelfenton Aug 1, 2026
876ecca
fix(setup): SoapySDR device listing crashed on SoapySDRKwargs (no .get)
nigelfenton Aug 1, 2026
a431886
fix(engine): waterfall tiles encode VitaFrequency (Hz*2^20) — AE #441…
nigelfenton Aug 1, 2026
b88239d
chore(deploy): bake tcpdump into the appliance
nigelfenton Aug 1, 2026
30f8b9b
fix(soapy): read back the device's actual sample rate after setSample…
nigelfenton Aug 1, 2026
7471ede
fix(soapy): starvation-free audio at any sample rate — floor decimati…
nigelfenton Aug 1, 2026
e6985ac
fix(soapy): pan span = sample rate; AGC gain ramps per-sample
nigelfenton Aug 1, 2026
24f256f
fix(soapy): SSB sideband selection was a no-op — real(conj(z)) == rea…
nigelfenton Aug 1, 2026
4a84e97
feat(deploy): publishable Pi image — SDRplay out of the bake, proven …
nigelfenton Aug 7, 2026
ce310d0
fix(deploy): a missing source tree is not a deploy failure
nigelfenton Aug 7, 2026
544d57c
feat(soapy): real NBFM demodulation — FM was silently an SSB detector
nigelfenton Aug 7, 2026
9a75291
fix(engine): a mode change alone never reached the adapter
nigelfenton Aug 7, 2026
9a939aa
fix(soapy): never swallow a failed hardware retune
nigelfenton Aug 7, 2026
79634c1
fix(soapy): FM discriminator clipped on noise and buried the signal
nigelfenton Aug 7, 2026
3d8e02e
fix(soapy): drop the x3 FM trim that re-clipped what the rescale had …
nigelfenton Aug 7, 2026
8e907bc
feat(soapy): an S-meter — the adapter reported nothing at all
nigelfenton Aug 8, 2026
8ae3078
fix(soapy): offset-tune — the demodulator was parked on the DC spike
nigelfenton Aug 8, 2026
644317c
fix(soapy): the other two retune paths also parked the demod on DC
nigelfenton Aug 8, 2026
8290c2f
fix(soapy): the panadapter painted the spectrum 510 kHz off
nigelfenton Aug 8, 2026
77208ab
perf(audio): make the demod path fit its real-time budget (clip-clop …
nigelfenton Aug 11, 2026
2ca8872
feat(setup): one-click update from the web UI (#38)
nigelfenton Aug 11, 2026
40d9b59
chore: version 0.4.0
nigelfenton Aug 11, 2026
77b0cd9
fix(setup): the update guard missed a gate started by systemd
nigelfenton Aug 11, 2026
8227d63
chore: version 0.4.1
nigelfenton Aug 11, 2026
cbfe3e8
fix(setup): report the version on DISK, not the one imported at startup
nigelfenton Aug 11, 2026
3b006ca
chore: version 0.4.2
nigelfenton Aug 11, 2026
b6a2b8d
fix(setup): the running-gate check matched its own search string
nigelfenton Aug 11, 2026
fb51dc2
chore: version 0.4.3
nigelfenton Aug 11, 2026
1f44ab4
fix(deploy): disable the first-boot user wizard in the image
nigelfenton Aug 11, 2026
d1a0a52
fix(soapy): back off on a lost device, and take the gate off the air
nigelfenton Aug 11, 2026
8d24454
fix(soapy): detect a device that stops producing samples without erro…
nigelfenton Aug 11, 2026
7fb6793
fix(engine): declare the waterfall line duration we actually send at
nigelfenton Aug 12, 2026
0188094
fix(soapy): implement current_span_hz so the pan axis matches the IQ
nigelfenton Aug 12, 2026
c6a9128
fix(soapy): report the gain state instead of swallowing it, plus an A…
nigelfenton Aug 12, 2026
6f1a49f
feat(dossiers): radio dossier schema v0.1 + IC-9700 dossier + validator
nigelfenton Aug 24, 2026
b0a7265
dossiers: gate becomes a pinned consumer of shack-experiments/radio-d…
nigelfenton Aug 24, 2026
608c14c
feat(dossiers): runtime dossier loader — the 9700 adapter reads its f…
nigelfenton Aug 24, 2026
b8ad65b
dossiers: vendor rtl-sdr-v4.json (the soapy adapter's radio)
nigelfenton Aug 24, 2026
f06bbaf
fix(main): bound adapter.close() so a wedged driver cannot hold the exit
crypticpy Aug 31, 2026
33d4d78
fix(soapy): restart a dropped stream instead of going off the air
crypticpy Aug 31, 2026
2828a48
fix(gain): AE sends RF gain in dB, not 0..100 — and answer rfgain_info
crypticpy Aug 31, 2026
371b4bd
test(shutdown): wait for the gate to actually listen before signallin…
crypticpy Aug 31, 2026
5977232
feat(resolution): dial panadapter bin width from the control panel
crypticpy Aug 31, 2026
e9b489a
feat(zoom): let AE's pan zoom drive the SDR's sample rate
crypticpy Sep 1, 2026
3a27dbc
feat(device): expose the SDR controls the Flex protocol has no verb for
crypticpy Sep 1, 2026
999da97
fix(meters): measure the S-meter over the demodulator's passband
crypticpy Sep 1, 2026
aae008a
fix(levels): give the panadapter and the S meter one shared dBm calib…
crypticpy Sep 1, 2026
a345c2a
feat(pan): segment wide frames across datagrams instead of capping at…
crypticpy Sep 1, 2026
0034fb9
feat(levels): --dbm-trim so a found calibration survives a restart
crypticpy Sep 1, 2026
af2c54b
feat(meters): report signal above the noise floor, not total passband…
crypticpy Sep 1, 2026
9b92120
fix(pan): give the FFT the sample count the bin width claims
crypticpy Sep 1, 2026
7204890
fix(levels): calibrate the dBm anchor against a reference receiver
crypticpy Sep 1, 2026
ccc199d
feat(meters): report the noise floor and SNR, not just the signal
crypticpy Sep 1, 2026
491f401
fix(levels): follow the gain when a device SETTING moves it
crypticpy Sep 1, 2026
b8eb119
fix(levels): warn on an LNA-state change instead of miscorrecting it
crypticpy Sep 1, 2026
12e3e9a
docs: write down the rules the project already runs on
nigelfenton Sep 1, 2026
57a1501
fix(tests): exercise segmentation on every platform; skip SIGTERM whe…
crypticpy Sep 1, 2026
fd75885
feat(levels): key the dBFS->dBm anchor by driver, with --dbm-base to …
crypticpy Sep 1, 2026
d8bbe80
ci: run the numpy suites and the __main__-less modules under pytest
crypticpy Sep 1, 2026
c8e79b0
fix(audio): bound the demodulator's IQ backlog in time, not blocks
crypticpy Sep 2, 2026
590cd53
feat(soapy): pass a setting's ArgInfo range through /device
crypticpy Sep 2, 2026
d59feaa
Resolution, level calibration and stream resilience from an RSPdx-R2 …
nigelfenton Sep 2, 2026
37173cf
release: 0.5.0
nigelfenton Sep 2, 2026
355c9a0
fix(adapters): promote device-lost signalling into the base class (#41)
nigelfenton Sep 1, 2026
5fa3d0c
fix(adapters): promote device-lost signalling into the base class (#42)
nigelfenton Sep 2, 2026
ef4f6d4
release: 0.5.1
nigelfenton Sep 6, 2026
3afac7b
fix(audio): dax_rx no longer overwrites the speaker stream's id (#34)…
nigelfenton Sep 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Keep the build context small and secret-free.
.git
.gitignore
attic/
reference/
docs/images/
screenshots/
**/dev/
*.pcap
*.pcapng
*.png
*.jpg
__pycache__/
**/__pycache__/
*.py[cod]
.venv/
venv/
# never let a populated env file reach the image
gate.env
*.env
!gate.env.example
188 changes: 188 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# Aether-gate — build the container images, and smoke-test one.
#
# Builds both targets for amd64 + arm64 on every PR so the Dockerfile cannot
# rot. Publishing is OFF by default and needs no secrets: the push step only
# arms itself on the upstream repository's main branch, so a fork (where this
# will also run) builds and verifies but never publishes.
name: docker

on:
pull_request:
paths:
- "Dockerfile"
- ".dockerignore"
- "docker-compose.yml"
- "aether_gate/**"
- ".github/workflows/docker.yml"
push:
branches: [main]
# Cutting a vX.Y.Z tag publishes immutable version tags alongside the
# floating ones, so a deployment can pin an image instead of silently
# following main.
tags: ["v[0-9]+.[0-9]+.[0-9]+*"]
workflow_dispatch:

env:
REGISTRY: ghcr.io
# NB: deliberately NOT `github.repository` — that is `nigelfenton/Aether-gate`,
# and an OCI repository name must be lowercase, so buildx rejects the tag before
# it builds anything. The build job lowercases it at runtime (see "Resolve the
# image name"); GHCR paths are case-insensitive on pull, so consumers are
# unaffected.

jobs:
# The lan target is quick, so it gets built natively and actually RUN.
smoke:
name: build lan + smoke-test (sim adapter, no hardware)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# `load: true` needs an image in the local docker daemon, and exporting a
# gha cache needs the docker-container driver — the default `docker` driver
# supports neither combination ("Cache export is not supported for the
# docker driver"). setup-buildx gives us the container driver, which does
# both.
- uses: docker/setup-buildx-action@v3

- name: Build lan image
uses: docker/build-push-action@v6
with:
context: .
target: lan
load: true
tags: aether-gate:ci
cache-from: type=gha
cache-to: type=gha,mode=max

# The sim adapter needs no radio, so CI can prove the container actually
# works rather than merely that it builds: it must advertise itself on the
# discovery socket and answer on its control panel.
- name: Smoke-test (discovery + control panel + rx-only)
run: |
set -eux
docker run -d --name gate --network host \
-e AETHER_GATE_ADAPTER=sim \
-e AETHER_GATE_CTL_PORT=8731 \
-e AETHER_GATE_NO_UPDATE_CHECK=1 \
-e AETHER_GATE_RX_ONLY=1 \
-e AETHER_GATE_SERIAL=CISMOKE01 \
aether-gate:ci
# give it a moment to bind and start advertising
for i in $(seq 1 30); do
curl -fsS http://127.0.0.1:8731/ >/dev/null 2>&1 && break
sleep 1
done
echo "--- control panel answers ---"
curl -fsS http://127.0.0.1:8731/ >/dev/null

echo "--- a discovery broadcast arrives on :4992 ---"
timeout 10 python3 - <<'PY'
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("", 4992)); s.settimeout(8)
import time
deadline = time.time() + 8
data = b""
while time.time() < deadline:
try:
data, addr = s.recvfrom(2048)
except socket.timeout:
break
if b"CISMOKE01" in data:
break
text = data.decode("utf-8", "replace")
print("discovery:", text[:200])
# Match OUR serial specifically: any real FlexRadio on the same LAN
# also advertises here, so a generic "serial=" check would pass even
# if the container never started.
assert "serial=CISMOKE01" in text, text
PY

echo "--- rx-only is reported ---"
docker logs gate 2>&1 | tail -20

- name: Container stops gracefully (SIGTERM reaches PID 1)
run: |
set -eux
# exec-form ENTRYPOINT means python is PID 1; a shell-form regression
# would swallow SIGTERM and force docker to SIGKILL after the timeout.
start=$(date +%s)
docker stop -t 10 gate
took=$(( $(date +%s) - start ))
echo "stop took ${took}s"
test "$took" -lt 10
test "$(docker inspect -f '{{.State.ExitCode}}' gate)" != "137"

- name: Logs on failure
if: failure()
run: docker logs gate || true

build:
name: build ${{ matrix.target }} (amd64 + arm64)
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
target: [lan, full]
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3

# Actions expressions have no lowercase function, so do it in the shell.
# `${VAR,,}` is a bash lowercase expansion; the default shell here is bash.
# PUBLISH is decided once here so the login and build steps cannot drift.
- name: Resolve the image name (must be lowercase for OCI)
run: |
echo "IMAGE=${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV"
echo "PUBLISH=${{ github.repository_owner == 'nigelfenton' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) }}" >> "$GITHUB_ENV"

# What each ref publishes (both targets share one image name, so a version
# is always prefixed with its target -- otherwise `full` would overwrite
# `lan`'s :1.2.3):
#
# push to main -> :lan :full floating
# tag v1.2.3 -> :lan-1.2.3 :full-1.2.3 immutable, pin this
# :lan-1.2 :full-1.2 moves with patches
# :lan :full also refreshed
# tag v1.2.3-rc1 -> :lan-1.2.3-rc1 prerelease only
#
# A prerelease deliberately does NOT move :lan or :lan-1.2.
- name: Derive the image tags
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE }}
flavor: latest=false
tags: |
type=raw,value=${{ matrix.target }},enable=${{ github.ref == 'refs/heads/main' || (startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-')) }}
type=semver,pattern={{version}},prefix=${{ matrix.target }}-
type=semver,pattern={{major}}.{{minor}},prefix=${{ matrix.target }}-

# Publishing arms itself only on the upstream repo's main branch or a
# version tag, so this workflow is safe to run anywhere (a fork builds and
# verifies but never publishes). Nothing needs a secret until then.
- name: Log in to GHCR
if: env.PUBLISH == 'true'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build (and publish only from upstream main or a version tag)
uses: docker/build-push-action@v6
with:
context: .
target: ${{ matrix.target }}
platforms: linux/amd64,linux/arm64
push: ${{ env.PUBLISH == 'true' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.target }}
cache-to: type=gha,mode=max,scope=${{ matrix.target }}
34 changes: 30 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
name: tests

# Run the offline test suites on every push/PR. They are stdlib-only (no radio,
# no network, no AE) — the pyserial/SoapySDR imports are deferred, so nothing to
# pip-install. Fast and hermetic.
# Run the offline test suites on every push/PR (no radio, no network, no AE).
#
# Two jobs on purpose. `test` is stdlib-only with no pip step — the
# pyserial/SoapySDR imports are deferred, so it proves the core needs nothing
# installed. Its module list is HAND-MAINTAINED: a new stdlib suite has to be
# added there by hand or it is silently skipped. `pytest` installs numpy and
# pytest and collects the whole directory, which is the only way the numpy
# suites and the modules without a __main__ runner (test_wf_packet) get run.

on:
push:
Expand Down Expand Up @@ -44,8 +49,29 @@ jobs:
shell: bash
run: |
set -e
for t in test_smoke test_hamlib test_icom_teardown test_icom7300 test_tune_clamp test_civ_seqtrack test_icom_audio test_ic9700_mode test_ic9700_dispatch test_radio_wins test_ic9700_tx test_mox_ptt test_ic9700_power; do
for t in test_smoke test_hamlib test_icom_teardown test_icom7300 test_tune_clamp test_civ_seqtrack test_icom_audio test_ic9700_mode test_ic9700_dispatch test_radio_wins test_ic9700_tx test_mox_ptt test_ic9700_power test_busy_refuse test_ic9700_settings test_hpsdr test_demod_equivalence test_updater test_dossier test_env_config test_dbm_base test_audio_backlog test_device_lost test_dax_speaker_coexist; do
echo "::group::$t"
python -m aether_gate.tests.$t
echo "::endgroup::"
done

pytest:
name: pytest · ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Install the test dependencies
run: python -m pip install numpy pytest

- name: Run the whole suite
run: python -m pytest aether_gate/tests -q
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ __pycache__/
*.egg-info/
build/
dist/

# populated docker env (secrets)
gate.env
Loading