Move bridge.py, demo.py, craft_client.py from tui/src/birdcage_tui/ to
src/birdcage/ so both TUI and MCP server can share the device layer
without a circular dependency on textual.
- DemoCraftClient in demo.py: duck-typed CraftClient replacement with
8 canned satellites, synthetic pass predictions, and time-varying LEO
arcs that drive real AOS/TCA/LOS pass events to the camera overlay
- Fix sky map EL signal fidelity: snap _el to _target_el at sweep start
so 2D scans compute correct per-row RSSI (was using stale elevation)
- Branch on demo_mode in app.py _setup_craft_client() to inject
DemoCraftClient instead of the HTTP CraftClient
- Add test_demo_craft.py: 5 tests exercising search, passes, tracking,
and WAITING state through the full TUI without mocks
- Update take_screenshots.py to cover all 8 screens (dashboard, control,
craft search, craft tracking, signal, system, console, camera)
Camera backend abstraction wrapping fswebcam/ffmpeg/libcamera-still via
subprocess, with DemoCamera fallback generating valid JPEG from raw bytes.
Capture pipeline writes JPEG + JSON sidecar always, optional FITS (astropy)
and EXIF (Pillow) when available. Thread-safe orchestrator with session
tracking, sequence numbering, and date-based output directories.
Trigger system: manual capture, configurable interval timer, and pass event
detection (AOS/TCA/LOS) with 0.5-degree TCA hysteresis. PassEventDetector
runs in the Craft tracking loop, fires callbacks to the camera overlay.
F6 overlay follows the F5 ConsoleOverlay pattern — ModalScreen with
install_screen persistence. Status panel, scrollable capture log, interval
controls, and AOS/TCA/LOS toggle buttons.
Tagline: "a generic AZ/EL positioner that doesn't care about wavelength"
added to TUI header subtitle.
51 new tests (77 total passing).
New F2 Control sub-mode that searches the Craft orbital catalog (22k+
objects), displays pass predictions, and drives the dish in real-time
using server-computed AZ/EL positions from /api/sky/up.
Tracking loop polls at ~1Hz, filters for the tracked target by
target_type + target_id (str, not int — handles satellites, planets,
stars, comets), and issues motor commands through the existing serial
bridge. Verified end-to-end with Carryout G2 hardware tracking NOAA 17.
New files:
- craft_client.py — stdlib HTTP client (urllib only, no deps)
- widgets/craft_panel.py — search table, pass info, tracking status
- tests/test_craft_mode.py — 5 unit tests with mocked API
- tests/test_craft_integration.py — 3 hardware integration tests
TUI-compatible Hamlib rotctld TCP server that works with the
existing SerialBridge/DemoDevice interface. F2 Control's Track
tab now starts/stops a real TCP server on the configured port.
Status callbacks report connection state, move count, and command
rate back to the TrackingPanel via thread-safe call_from_thread.
Includes 7 pilot tests exercising the full protocol path through
asyncio TCP clients against the DemoDevice.
F2 Control screen Manual mode now has a row of step-size buttons
(0.1°, 0.5°, 1°, 5°, 10°) that scale arrow key nudges, plus
AZ/EL velocity inputs with Apply button for runtime motor speed
tuning via firmware mv/ma commands.
Bridge: set_max_velocity(), set_max_acceleration() write methods.
Demo: mutable dynamics + console mv/ma write handling.
Tests: 8 Textual pilot tests covering button toggle, nudge math,
velocity population, and apply round-trip.
Replace sidebar + 5-screen layout with horizontal tab bar (F1-F4)
and persistent StatusStrip. Consolidate Position + Scan screens into
Signal (F3) with Monitor/Sweep/Sky Map sub-modes via ModeBar.
Screens:
- F1 Dashboard: system health, tracking panel, quick actions, presets
- F2 Control: motor tuning, compass rose, preset management
- F3 Signal: RSSI monitor, 1D sweep (SweepPlot), 2D sky map (heatmap)
- F4 System: NVS editor with regex filter, EEPROM, firmware info
- F5 Console: push/pop overlay (no longer a tab)
New widgets: StatusStrip, ModeBar, SweepPlot, QuickActions, PresetList,
ReceiverInfo, MotorTuning, NvsFilter, SystemHealth, TrackingPanel.
Removed: PositionScreen, ScanScreen, DeviceStatusBar (functionality
absorbed into new screens and StatusStrip).
App-level position poll feeds StatusStrip and active screen at ~2 Hz.
Fix shared threading.Event across instances (class-level mutable default).
Stop handler now calls cancel_operation() on the device bridge,
which sets a threading.Event that interrupts the 2s-timeout serial
read loop in send_with_timeout(). InterruptedError is caught
separately to prevent falling back to software sweep on cancel.
disconnect() uses acquire(timeout=5) with force-close fallback
instead of blocking lock acquisition — prevents deadlock when a
stuck worker holds the serial lock during shutdown.
Add 3 Textual async tests (pytest-asyncio) to verify Stop behavior:
firmware sweep stop, software sweep stop, and sweep restart.