diff --git a/.env b/.env new file mode 100644 index 0000000..5cee167 --- /dev/null +++ b/.env @@ -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 diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..49b66df --- /dev/null +++ b/Caddyfile @@ -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 +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d899cbd --- /dev/null +++ b/Dockerfile @@ -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 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bb64530 --- /dev/null +++ b/Makefile @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b5f999f --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/public/screenshots/tui-console.png b/public/screenshots/tui-console.png new file mode 100644 index 0000000..37302a1 Binary files /dev/null and b/public/screenshots/tui-console.png differ diff --git a/public/screenshots/tui-position.png b/public/screenshots/tui-position.png new file mode 100644 index 0000000..add2f0f Binary files /dev/null and b/public/screenshots/tui-position.png differ diff --git a/public/screenshots/tui-signal.png b/public/screenshots/tui-signal.png new file mode 100644 index 0000000..efdfea1 Binary files /dev/null and b/public/screenshots/tui-signal.png differ diff --git a/public/screenshots/tui-system.png b/public/screenshots/tui-system.png new file mode 100644 index 0000000..f637e48 Binary files /dev/null and b/public/screenshots/tui-system.png differ diff --git a/src/content/docs/guides/tui.mdx b/src/content/docs/guides/tui.mdx new file mode 100644 index 0000000..0f9b8b9 --- /dev/null +++ b/src/content/docs/guides/tui.mdx @@ -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 + + +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 + ``` + + + + +## 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 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. + + + +### 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 +$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.