Add deployment infrastructure and TUI showcase guide

Dockerfile (multi-stage: base/dev/build/prod with caddy:2-alpine),
docker-compose.yml with caddy-docker-proxy labels for birdcage.warehack.ing,
Caddyfile, Makefile (up/down/dev/prod/clean), and .env.

TUI guide page with 4 screenshots (F1 position, F2 signal, F4 system,
F5 console), demo mode docs, and project backstory crediting Gabe Emerson
(KL1FI) and Chris Davidson.
This commit is contained in:
Ryan Malloy 2026-02-13 15:12:54 -07:00
parent 088c1a5ace
commit 9c9b69c733
10 changed files with 275 additions and 0 deletions

5
.env Normal file
View File

@ -0,0 +1,5 @@
COMPOSE_PROJECT_NAME=birdcage-docs
APP_ENV=prod
APP_PORT=80
PUBLIC_DOMAIN=birdcage.warehack.ing
VITE_HMR_HOST=birdcage.warehack.ing

11
Caddyfile Normal file
View File

@ -0,0 +1,11 @@
:80 {
root * /srv
encode gzip
handle /health {
respond "ok" 200
}
try_files {path} {path}/index.html {path}/ /index.html
file_server
}

25
Dockerfile Normal file
View File

@ -0,0 +1,25 @@
# ── base: shared dependency install ──────────────────────────────
FROM node:20-slim AS base
WORKDIR /app
ENV ASTRO_TELEMETRY_DISABLED=1
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
# ── dev: Astro dev server with HMR ─────────────────────────────
FROM base AS dev
ENV HOST=0.0.0.0
EXPOSE 4321
CMD ["npx", "astro", "dev", "--host", "0.0.0.0", "--port", "4321"]
# ── build: static site generation ───────────────────────────────
FROM base AS build
RUN npx astro build
# ── prod: Caddy serving static files ───────────────────────────
FROM caddy:2-alpine AS prod
COPY Caddyfile /etc/caddy/Caddyfile
COPY --from=build /app/dist /srv
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -qO- http://127.0.0.1:80/health || exit 1

27
Makefile Normal file
View File

@ -0,0 +1,27 @@
.PHONY: up down logs rebuild dev prod clean
up:
docker compose up -d --build
down:
docker compose down
logs:
docker compose logs -f
rebuild:
docker compose down
docker compose up -d --build
dev:
@sed -i 's/^APP_ENV=.*/APP_ENV=dev/' .env
@sed -i 's/^APP_PORT=.*/APP_PORT=4321/' .env
$(MAKE) rebuild
prod:
@sed -i 's/^APP_ENV=.*/APP_ENV=prod/' .env
@sed -i 's/^APP_PORT=.*/APP_PORT=80/' .env
$(MAKE) rebuild
clean:
docker compose down --rmi local -v

31
docker-compose.yml Normal file
View File

@ -0,0 +1,31 @@
services:
docs:
build:
context: .
target: ${APP_ENV:-dev}
container_name: birdcage-docs
restart: unless-stopped
environment:
- PUBLIC_DOMAIN=${PUBLIC_DOMAIN}
- VITE_HMR_HOST=${VITE_HMR_HOST}
networks:
- caddy
labels:
caddy: ${PUBLIC_DOMAIN}
caddy.reverse_proxy: "{{upstreams ${APP_PORT:-4321}}}"
caddy.reverse_proxy.flush_interval: "-1"
caddy.reverse_proxy.transport: http
caddy.reverse_proxy.transport.read_timeout: "0"
caddy.reverse_proxy.transport.write_timeout: "0"
caddy.reverse_proxy.transport.keepalive: 5m
caddy.reverse_proxy.transport.keepalive_idle_conns: "10"
caddy.reverse_proxy.stream_timeout: 24h
caddy.reverse_proxy.stream_close_delay: 5s
volumes:
- ./src:/app/src
- ./public:/app/public
- ./astro.config.mjs:/app/astro.config.mjs
networks:
caddy:
external: true

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

View File

@ -0,0 +1,176 @@
---
title: "The Birdcage TUI"
description: A five-screen terminal interface for controlling Winegard satellite dishes — from a $75 RV salvage find to something that looks like it belongs in a ground station.
sidebar:
order: 8
badge:
text: New
variant: tip
---
import { Aside, Steps, Tabs, TabItem } from '@astrojs/starlight/components';
Mission control for less than dinner for two.
Somewhere inside every Winegard Carryout G2 is a 2013-vintage NXP Cortex-M4 running firmware 02.02.48,
driving two Allegro A3981 stepper motors and a Broadcom BCM4515 DVB-S2 tuner through 12 submenus
and over 100 undocumented commands. In 2026, it takes commands from a Python TUI built on
[Textual](https://textual.textualize.io/) and doesn't seem to mind.
Gabe Emerson ([KL1FI](https://github.com/saveitforparts)) went first — publishing control scripts
for five different Winegard variants, proving that a $50200 RV salvage yard dish could replace
a $5002000 commercial amateur radio rotator. Chris Davidson ([cdavidson0522](https://github.com/cdavidson0522/winegard-sky-scan))
figured out the G2's RS-422 wiring and discovered `azscanwxp` — the firmware's built-in radio telescope mode.
Birdcage is what happens when you take all of that and give it a proper interface.
## Quick Start
<Steps>
1. **Clone and install** (from the `tui/` directory inside the Birdcage repo):
```bash
cd tui/
uv sync
```
2. **Run in demo mode** (no hardware required):
```bash
uv run birdcage-tui --demo
```
3. **Connect to a real dish:**
```bash
uv run birdcage-tui --port /dev/ttyUSB0 --firmware g2
```
</Steps>
<Aside type="tip">
Demo mode simulates a complete Carryout G2 — motor physics at 10°/s with settling noise,
a Gaussian RSSI signal model, and all 12 firmware submenus. Every screen works, every button
does something. You don't need a dish on the roof to try it.
</Aside>
## Five Screens
Navigate between screens with **F1****F5** keys or click the sidebar buttons.
The device status bar at the bottom persists across all screens — connection state,
serial port, firmware version, and current menu prompt are always visible.
### F1 — Position
![Birdcage TUI Position screen showing compass rose, AZ/EL readout, and sparkline history](/screenshots/tui-position.png)
The position screen is where you point the dish. A compass rose shows current azimuth
with a bearing indicator, while AZ and EL sparklines track movement history over time.
The numeric readout at top shows position to hundredths of a degree — the G2's stepper
resolution is 0.009° azimuth (40,000 steps/rev) and 0.014° elevation (24,960 steps/rev).
The AZ and EL sparklines give you immediate visual feedback: flat lines mean the dish
is parked, slopes mean it's slewing, and the amplitude of the noise floor after arrival
tells you how much stepper backlash you're dealing with.
### F2 — Signal
![Birdcage TUI Signal screen showing RSSI gauge, DVB and ADC sparklines](/screenshots/tui-signal.png)
Signal strength from two sources: the BCM4515 DVB tuner's RSSI (bounded, averaged)
and the raw ADC reading (single-shot). The gauge uses sub-character Unicode block elements
(▏▎▍▌▋▊▉█) for smooth visual resolution. Below the gauge, DVB RSSI and ADC RSSI
sparklines show signal trends over time — useful for peaking during manual dish adjustment.
The sample counter and iteration display at bottom track measurement throughput. On a
live dish with the LNA enabled (`lnbdc odu`), you'll see the noise floor sit around
RSSI 500 (ADC) or 230490 (DVB, polarity-dependent).
### F3 — Scan
The scan screen wraps the firmware's `azscanwxp` command — Davidson's radio telescope
mode. Define an AZ×EL grid, set the step resolution, and watch the sky heatmap fill
in with RSSI-colored cells as the dish sweeps. Results export to CSV for post-processing
into proper sky maps.
<Aside>
No screenshot for this one yet — the scan screen needs a live dish or a long-running demo
sweep to produce interesting output. It exists, it works, and it's waiting for first light.
</Aside>
### F4 — System
![Birdcage TUI System screen showing firmware ID, A3981 diagnostics, motor dynamics, and NVS table](/screenshots/tui-system.png)
The system dashboard. Top row shows the firmware identity banner: version 02.02.48,
K60 MCU at 96 MHz, antenna ID "12-IN G2". Below that, two side-by-side panels:
- **A3981 Diagnostics** — fault pin status for both stepper drivers (AZ/EL DIAG: OK or FAULT),
plus step size mode (AUTO means the driver handles microstepping transitions automatically).
- **Motor Dynamics** — max velocity and acceleration for each axis. The G2 defaults to
65.0°/s azimuth, 45.0°/s elevation, with 400.0°/s² acceleration — fast enough to track
LEO satellites at medium altitudes.
The NVS Table at the bottom is a scrollable browser of all 134 non-volatile storage values.
Current, saved, and default columns let you see what's been modified. The "Refresh NVS"
and "Export NVS JSON" buttons at the bottom do what you'd expect.
### F5 — Console
![Birdcage TUI Console screen showing firmware console with command prompt](/screenshots/tui-console.png)
Raw firmware console access with guardrails. Type commands directly to the dish's serial
interface, see responses with color-coded prompt tracking (`TRK>`, `MOT>`, `DVB>`, `NVS>`,
etc.). Command history with up/down arrows.
The safety gates matter here: the console warns before sending `q` at root level (which
kills the firmware shell — requires power cycle to recover), before `reboot`, and before
NVS writes. The firmware doesn't have an "are you sure?" prompt. Birdcage does.
## Demo Mode
Pass `--demo` and Birdcage substitutes a `DemoDevice` for the serial bridge. The simulator models:
- **Motor physics** — position changes at ~10°/s with configurable settling noise
(±0.05° random perturbation to simulate stepper backlash)
- **RSSI signal** — Gaussian model centered on a target position, so signal strength
increases as you point closer to the simulated source
- **All 12 submenus** — `TRK>`, `MOT>`, `DVB>`, `NVS>`, `A3981>`, `ADC>`, `OS>`, `STEP>`,
`PEAK>`, `EE>`, `GPIO>`, `LATLON>`, `DIPSWITCH>`. Menu navigation with `mot`, `dvb`, `nvs`,
etc. and `q` to go back all work correctly
- **Full NVS dump** — the complete 134-entry table from firmware 02.02.48, captured from a
live unit on 2026-02-12
Every screen, every widget, every button works in demo mode. It's the same code path —
the only difference is what's on the other end of the bridge.
## The Story
The hardware was never the hard part. RV satellite dishes show up at salvage yards for
$50200 because the RV got totaled or the owner switched to Starlink. The mechanicals are
built to survive highway speeds and hailstorms. The motors are Allegro A3981 stepper drivers
with 1/16 microstepping — more precise than most amateur radio rotators at ten times the price.
The gap was always software.
Gabe Emerson (KL1FI) bridged it first. Five repositories for five Winegard variants — the
Trav'ler (HAL 0.0.00 and HAL 2.05), the Trav'ler Pro, the original Carryout, and the
Carryout G2. Python scripts that send `a 0 180` to point azimuth south and `a 1 45` to tilt
elevation to 45°. A rotctld bridge so Gpredict could drive the dish. Proof that the idea
worked.
Chris Davidson took the G2 further — mapped the RS-422 wiring (four wires, not two, and
polarity matters or you get garbled data at the correct baud rate), discovered the `azscanwxp`
command buried in the motor submenu, and turned a TV satellite dish into an RF imager.
Birdcage picks up where they left off. We reverse-engineered over 100 commands across
12 firmware submenus using automated probing and interactive exploration. We documented
the full NVS table (134 entries), the GPIO pin map, the SPI bus layout (4 MHz to the motor
drivers, 6.857 MHz to the DVB tuner), the DiSEqC 2.x interface, and the boot sequence from
bootloader through motor calibration to prompt.
The TUI is what ties it together. Not because a terminal interface is fashionable, but because
when you're on a roof with a laptop and a USB-to-RS422 adapter, you want something that runs
over SSH and shows you everything at once.
A 2013 microcontroller. A 2026 terminal. A dish that costs less than the cable to connect it.
That's the project.