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:
parent
088c1a5ace
commit
9c9b69c733
5
.env
Normal file
5
.env
Normal 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
11
Caddyfile
Normal 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
25
Dockerfile
Normal 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
27
Makefile
Normal 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
31
docker-compose.yml
Normal 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
|
||||
BIN
public/screenshots/tui-console.png
Normal file
BIN
public/screenshots/tui-console.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
BIN
public/screenshots/tui-position.png
Normal file
BIN
public/screenshots/tui-position.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
BIN
public/screenshots/tui-signal.png
Normal file
BIN
public/screenshots/tui-signal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
BIN
public/screenshots/tui-system.png
Normal file
BIN
public/screenshots/tui-system.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
176
src/content/docs/guides/tui.mdx
Normal file
176
src/content/docs/guides/tui.mdx
Normal 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 $50–200 RV salvage yard dish could replace
|
||||
a $500–2000 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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
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 230–490 (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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
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
|
||||
$50–200 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.
|
||||
Loading…
x
Reference in New Issue
Block a user