Stock firmware at 0x0DDD reads back each init block from register A7
after writing and verifies the data matches (with bit 7 XOR'd on the
block address byte). Without this readback, the BCM4500 may not
finalize the write — our init blocks were "silently failing" (I2C
succeeds, status reports booted, but SNR reads all zeros).
Changes:
- bcm_write_init_block: add pre-write bcm_poll_ready(), post-write
A7 readback with XOR(0x80) on byte[0] and full data comparison
- i2c_rd buffer: expand from 8 to 24 bytes for 16-byte block readback
This is the most likely root cause of the BCM4500 "boots but doesn't
work" issue (Task #5). Needs hardware test to confirm.
FixedAttenuator supports --attenuator fixed:XX for non-programmable
inline SMA pads (set_db is a no-op returning the fixed value).
Udev rules grant non-root USB access for NanoVNA and HMC472A.
Apply .gitattributes normalization to convert all CRLF line
endings inherited from Windows-origin source files to Unix LF.
175 files, zero content changes.
Two root causes prevented BCM4500 init block writes from completing:
1. Init block data arrays included length prefix bytes from the stock
firmware's XDATA format (17-byte blocks at code:0x0B4F). The stock
firmware reads byte 0 as length and writes bytes 1..N to A7.
Blocks 0 and 1 had the length prefix (0x06, 0x07) as the first
data byte, corrupting the DSP's indirect register FIFO.
2. The BCM3440 gateway's A8 register does not clear bit 0 after
indirect write commands (0x03), even though the BCM4500 processes
them successfully (confirmed via direct address 0x08 where A8
transitions from 0x03 → 0x02). bcm_poll_ready() now treats
gateway timeout as success with a settling delay.
Boot now completes reliably in ~0.96s through all stages:
GPIO → power → reset → PLL/DSP load → init blocks 0,1,2 → 0xFF.
HMC472ASerial class implements usb-serial-json-v1 protocol over the
ESP32-S3's native USB CDC port. Auto-detection scans /dev/ttyACM* and
probes with the identify command to find the right port.
--attenuator flag now defaults to 'auto' (USB first, HTTP fallback).
Also accepts direct serial port paths or HTTP URLs for explicit control.
New tool (tools/rf_testbench.py) automates five test sequences using a
NanoVNA as a CW source and HMC472A digital attenuator (0-31.5 dB, 0.5 dB
steps via REST API) to characterize the SkyWalker-1 receiver:
- AGC linearity mapping across 64 attenuation steps
- IF band flatness sweep (950-1500 MHz)
- Frequency accuracy via peak detection
- Minimum detectable signal search
- BPSK mode 9 CW probe (Viterbi rate 1/2 K=7)
Includes SKYWALKER_MOCK=1 mode, path-loss calibration from NanoVNA S21
sweeps, and safe-state cleanup (attenuator to max on exit, LNB power
never enabled in direct-input mode).
Also adds Applications & Use Cases guide, RF Test Bench docs page, fixes
h21cm cable loss (was 3x too high), and updates sidebar.
demo.py: hotplug mock used [0x08, 0x50, 0x51, 0x61], corrected to
[0x08, 0x10, 0x51] per hardware bus scan. 0x50 was never on the bus;
0x61 was speculative.
device.py: known device map had 0x60 "BCM3440 tuner" and 0x61
"ISL6421 LNB controller" — neither appears on the SkyWalker-1 I2C bus.
Replaced with 0x10 "Tuner / LNB controller" per confirmed scan.
The bus scan on real hardware returns [0x08, 0x10, 0x51], matching the
firmware source (#define BCM4500_ADDR 0x08), bus-architecture docs, and
boot-debug-findings.md. The 0x61 "BCM3440" entry was speculative and
never appeared on the actual SkyWalker-1 I2C bus.
Updated: server known_devices map, mock_device, and test assertions.
Wrap get_usb_speed, get_serial_number, and get_last_error in try/except
so a STALL on one command returns "unavailable" instead of crashing the
entire status read. Discovered when vendor command 0x07 (get_usb_speed)
STALLs on real hardware despite working firmware version readback.
Extract MockSkyWalker1 to shared mock_device.py (used by both unit tests
and server lifespan). Server checks SKYWALKER_MOCK env var at startup —
when set, uses mock device instead of USB hardware, enabling full MCP
transport testing via claude -p without the dongle connected.
Verified: 64 unit tests pass, claude -p integration tests exercise
identify_frequency, get_device_status, and sweep_spectrum through the
complete JSON-RPC pipeline.
Motor watchdog: asyncio background task auto-halts motor after 30s of
continuous drive, fires even if LLM client disconnects. Integrated into
move_dish (start on continuous, cancel on halt/goto/gotox) and lifespan
teardown.
Test suite: 64 tests covering all 17 MCP tools — device status, spectrum
sweep validation, tune/blind-scan boundary checks, motor safety (stepped,
continuous opt-in, watchdog lifecycle), jog/store limits, LNB/I2C, TS
capture, frequency identification, and path traversal protection. Uses
MockSkyWalker1 + MockContext for direct async function testing without
USB hardware.
Fixes: FastMCP 2.x description→instructions constructor change,
parents[4] path resolution for tools directory import.
Four new tools transforming the SkyWalker-1 from satellite TV receiver into
a general-purpose RF observatory:
- skywalker-mcp: FastMCP server exposing 20 tools, 4 resources, 2 prompts.
Thread-safe DeviceBridge with motor safety (continuous drive opt-in),
input validation on all frequency/symbol rate/step parameters,
try/finally on TS capture, path traversal sanitization, and reduced
lock scope so emergency motor halt isn't blocked during long surveys.
- h21cm.py: Hydrogen 21 cm drift-scan radiometer at 1420.405 MHz with
Doppler velocity calculation, control band comparison, and CSV output.
- beacon_logger.py: Long-term Ku-band beacon SNR/AGC logger with auto-relock,
dual CSV/JSONL output, signal handlers, and systemd unit generation.
- arc_survey.py: Multi-satellite orbital arc census with USALS motor control,
per-slot catalog persistence, resume support, and defensive motor halt
on all error/interrupt paths.
Documentation: experimenter's roadmap guide + 4 tool reference pages (48 pages total).
Genpix is selling remaining SkyWalker-1 inventory on eBay with all
net proceeds going to the Leleka Foundation (Ukrainian medical and
social relief). Added a thank-you note on the landing page and a
tip callout on the hardware overview.
Addresses all remaining Apollo review items:
- C-3: DiSEqC Timer2 spin loops now have timeout protection via
diseqc_wait_ticks() with per-tick I2C_TIMEOUT countdown. Tone burst
refactored to use the shared helper instead of bare while(!TF2) loops.
New ERR_DISEQC_TIMER (0x0C) error code on Timer2 failure.
- I-3: Hotplug scan hp_prev copy moved from start to end of scan.
Aborted scans no longer corrupt the previous-good baseline, preventing
false "removed" events on consecutive scan failures.
- S-1: Watchdog-fired detection in main loop — when wdt_armed==2 (ISR
fired), sets ERR_WDT_FIRED (0x0D) readable via 0xBC and clears
BM_STARTED|BM_FW_LOADED|BM_ARMED so host sees system is down.
- I-2: Comment explaining (WORD)sd_poll_count cast as SDCC optimization.
- I-1: CKCON bit ownership comment in diseqc_tone_burst() Timer2 setup.
Code: 13,079 / 15,360 bytes (85%). XRAM: 218 / 512. Stack: 132 bytes.
Safety review identified infinite-hang paths in do_tune(), GPIF streaming,
EP0/EP2 FIFO waits, and the 0xB4 I2C bus scan. The firmware controls an LNB
power supply (750mA at 18V) on a Cypress FX2LP with no hardware watchdog.
Key changes:
- Software watchdog via Timer0 ISR (~2s timeout, cuts LNB power on stall)
- Replace fx2lib i2c_write() in do_tune() with timeout-protected helper
- ep0_wait_data() helper replaces 7 bare EP0 BUSY spin loops
- GPIF idle wait and EP2 FIFO full wait now have timeouts
- 0xB4 I2C bus scan uses i2c_wait_done()/i2c_wait_stop() instead of bare spins
- Return-value checks on all I2C writes in sweep/scan functions
- EP0 payload length validation on all vendor commands with data phase
- Zero-fill EP0BUF before indirect reads (0x87, 0xB7) for deterministic output
- i2c_wait_stop() now sets last_error on timeout
- New error codes: ERR_TUNE_FAIL through ERR_DISEQC_LEN
- BCM_LOCK_BIT constant replaces hardcoded 0x20 in lock checks
- DiSEqC Tone Burst B rejected with ERR_NOT_SUPPORTED
- DiSEqC message length error sets ERR_DISEQC_LEN
- hp_changes counter saturates instead of wrapping
- stream_diag_poll() only updates status on successful I2C reads
- do_tune() forward declaration eliminates implicit-function warning
- do_tune() sets ERR_BCM_NOT_READY when demod not booted
- wdt_kick() in all long-running sweep/scan loops
Code: 13,057 / 15,360 bytes (85%). XRAM: 218 / 512 bytes (43%).
Stack: 132 bytes free. Zero new SDCC warnings.
Phase D firmware hardening: vendor commands 0xBD (streaming diagnostics)
and 0xBE (I2C hot-plug detection) with Python library, bridge, and demo
support. All I2C operations use timeout-protected helpers, BCM4500 reads
are rate-limited during streaming, and frame counter reads use atomic
read-verify-reread pattern. Counters saturate instead of wrapping.
Generate motor.svg and survey.svg for the new TUI screens. Fix
MotorScreen bug: two Horizontal widgets shared id="motor-usals-inputs",
causing MountError. Replaced with classes="usals-input-row" since both
rows need the same styling. Regenerated all 13 screenshots.
Update tui.mdx with Device, Stream, and Config screen sections
including keyboard shortcuts, Tabs/Steps components, and safety
warnings. Update generate_screenshots.py to capture all 11 screens.
Regenerate all SVGs with current sidebar layout.
- New docs page (tools/tui.mdx) covering all 5 RF modes, keyboard
shortcuts, easter eggs, and splash screen with inline SVG screenshots
- Screenshot generation script using Textual headless Pilot API to
capture 8 screens in demo mode without hardware
- Fix dark mode toggle: migrate from removed App.dark to App.theme API
(Textual 7.x breaking change)
- Update social link to Gitea repo, add TUI to sidebar
New features:
- P1 green phosphor radar scope widget on Track screen with LUT-optimized rendering
- Splash screen with pre-baked ANSI half-block art from 16colo.rs/Mistigris
- Star Wars ASCII animation via telnet (ctrl+w) with IPv6 happy eyeballs and offline fallback
- Dark Side / New Hope toast notifications on theme toggle
- Kitty terminal detection with cat emoji on splash
Robustness (from Apollo code review):
- Circuit breaker in track loop after 10 consecutive errors
- Input validation for frequency, symbol rate, step size across scan/spectrum/track
- Consolidated sys.path manipulation into __init__.py
- Radar scope pre-computes dist/angle LUT per pixel on resize
Cleanup:
- Removed unused imports across lband, monitor, scan, signal_gauge
- Moved Pillow/textual-image to optional dev deps (splash uses pre-baked ANSI)
- Added 41-test pytest suite covering telnet IAC parsing, radar geometry, splash assets
Textual's ContentSwitcher expects regular Widget/Container children,
not Screen subclasses. Screen's on_mount fires for all children at
once regardless of visibility, so demo workers for all 5 modes
started simultaneously and competed for the bridge.
Container children get proper on_show/on_hide from ContentSwitcher's
visibility toggling — only the active panel's worker runs.
Separate entry point (skywalker-tui) that reuses skywalker_lib.py
unchanged. Five RF modes: spectrum, scan, monitor, lband, track —
each with threaded USB bridge workers for non-blocking I/O.
Includes --demo mode with synthetic signal generation (Gaussian
peaks, noise floor, AGC simulation) for development without hardware.
Custom widgets: spectrum bar chart, rolling waterfall, signal gauge,
sparkline history, transponder table, device status bar.
The 1711-line master reference was 100% duplicated across the existing
37 Starlight pages. Trimmed to bibliography only (Ghidra port numbers,
driver source paths, analysis reports, community links).
Salvaged FX2 USB controller registers (SETUPDAT, CPUCS, EP0BUF, etc.)
into bcm4500/register-map.mdx before removal. Sidebar label updated
from "Master Reference" to "Sources".
New page: bcm4500/register-map.mdx consolidates all register
addresses (direct, indirect page 0, FX2 XRAM/IRAM, I2C controller)
into a single lookup reference with cross-reference index.
Expanded sections:
- fw213-variants: hardware detection mechanism, distinguishing
characteristics table, FW1 demod type detection signatures
- demodulator: probe logic (3x3 retry, INT0 40-iteration boot probe
at 0x7F/0x3F, no_demod_flag graceful degradation)
- version-comparison: version ID gap aside (v2.07.04 is a kernel
constant, not a real firmware release)
Cross-links added to signal-monitoring and tuning-protocol pages.
Firmware v3.02.0 adds three new vendor commands:
- 0xB7 SIGNAL_MONITOR: fast 8-byte combined signal read
- 0xB8 TUNE_MONITOR: tune + dwell + read in one round-trip
- 0xB9 MULTI_REG_READ: batch read up to 64 indirect registers
New tools/skywalker.py provides five modes that use the BCM4500's
AGC registers as a crude power detector across 950-2150 MHz IF,
even without demodulator lock:
- spectrum: sweep analyzer with ASCII/waterfall/matplotlib display
- scan: automated transponder scanner (sweep + peak detect + blind scan)
- monitor: real-time signal strength for dish alignment
- lband: direct input analyzer with L-band allocation annotations
- track: carrier/beacon tracker with CSV/JSON logging and drift detection
Extracts shared SkyWalker1 class and constants into skywalker_lib.py;
tune.py now imports from the shared library.
Astro 5 + Starlight 0.37 site at site/ with teal/steel theme.
Content sourced from 14 reverse engineering docs, master reference,
and custom firmware source. Includes Tabs, Badge, Steps, Aside,
FileTree, and CardGrid components throughout. DiSEqC SVGs with
click-to-zoom via starlight-image-zoom. All internal links validated.
Pagefind search indexes all 32 pages.
Merges 11 separate analysis documents into a single 1700-line
authoritative reference covering hardware, USB, vendor commands,
BCM4500 demodulator, tuning protocol, GPIF streaming, LNB/DiSEqC,
GPIO map, firmware versions, I2C bus architecture, and all known
quirks/errata. Corrects BCM4500 I2C address to 0x08 (7-bit)
throughout.
Removed I2CS bmSTOP "bus reset" from bcm4500_boot() and debug modes.
Sending STOP with no active transaction puts the FX2 I2C controller
into an inconsistent state where subsequent START+ACK detection fails.
Root cause identified through incremental debug modes (wValue 0x80-0x85)
on live hardware: mode 0x82 (with bmSTOP) fails, mode 0x85 (identical
but without bmSTOP) succeeds. Raw I2C reads confirm BCM4500 is alive
the entire time -- only the controller state is corrupted.
BCM4500 now boots successfully in ~90ms. Three I2C devices found on
bus: 0x08 (BCM4500), 0x10 (tuner/LNB), 0x51 (EEPROM).
Also in this commit:
- Timeout-protected I2C functions replacing fx2lib bare while loops
- I2C bus scan and debug mode infrastructure
- Kernel driver blacklist for dvb_usb_gp8psk
- Test tools for incremental boot debugging
- Technical findings documented in docs/boot-debug-findings.md
Correct BCM4500 I2C address from 0x10 (8-bit wire) to 0x08 (7-bit)
since fx2lib shifts internally. Add i2c_combined_read() with repeated
START for proper BCM4500 register access. Add I2C bus scan (0xB4),
raw read (0xB5), and indirect protocol diagnostic (0xB6) commands.
Single-transaction indirect reads/writes for BCM4500 register protocol.
Verified on hardware: BCM4500 ACKs at 0x08, BOOT_8PSK returns config
0x03. Register reads still return zeros — BCM4500 needs DSP firmware
loaded via LOAD_BCM4500 (0x88) before registers become functional.
Custom firmware (SDCC + fx2lib) implements all stock vendor commands
(0x80-0x94) plus new commands for spectrum sweep (0xB0), raw BCM4500
register access (0xB1/0xB2), and blind scan (0xB3). Compiles to 6.3KB
of code with healthy RAM margins.
RAM loader (fw_load.py) uses the FX2 0xA0 vendor request to load
firmware into RAM without touching EEPROM -- power cycle restores
factory firmware. Supports Intel HEX and raw binary formats.
New tools:
- tools/eeprom_write.py: EEPROM firmware flash with backup, verify, dry-run
- tools/ts_analyze.py: MPEG-2 transport stream analyzer with PAT/PMT parsing
DVB-S2 investigation confirms BCM4500 hardware limitation (no LDPC/BCH silicon).
Fix --json flag on tune.py subcommands (argparse parent/child scoping).
All tools verified against live SkyWalker-1 hardware.
Python tool (tools/tune.py) implements all vendor USB control
commands for tuning, LNB control, DiSEqC switching, and MPEG-2
transport stream capture via pyusb. Includes CLI subcommands for
status, tune, stream, diseqc, and lnb operations.
Consolidated hardware reference merges all Phase 1 analysis into
a single 12-section document covering the complete USB interface,
all 30 vendor commands, BCM4500 demodulator protocol, GPIF
streaming path, DiSEqC timing, and cross-version firmware
comparison.
Complete reverse engineering of all unknown vendor commands (0x8F,
0x91-0x98) across v2.06, v2.13, and Rev.2 firmware versions. Full
TUNE_8PSK (cmd 0x86) protocol analysis including EP0BUF format,
modulation dispatch jump table, BCM4500 I2C indirect register
sequences, and FEC lookup tables.
Major correction: All firmware versions use GPIO bit-banging for DiSEqC,
NOT I2C-based control as previously reported. Deep decompilation of the
sub-functions (byte transmit, bit symbol, tone burst) across v2.06, Rev.2,
and v2.13 reveals identical Manchester encoding algorithms with only the
data GPIO pin changed per PCB revision:
- v2.06: P0.7, Rev.2: P0.4, v2.13: P0.0
- P0.3 (22kHz carrier gate) unchanged across all versions
New section 7: Complete DiSEqC timing chain analysis including:
- Timer2 configuration (RCAP2=0xF82F, 4MHz clock, 500us tick)
- Manchester encoding waveforms (3 ticks/bit, 1.5ms/bit, 667 baud)
- Byte transmission (8 data + odd parity = 13.5ms)
- Tone burst timing (25 ticks = 12.5ms)
- CPU clock compensation in delay function
- External 22kHz oscillator architecture
Maps all vendor USB control commands (0x80-0x9D) used by the kernel driver
against firmware implementations across all 4 extracted versions.
Key findings:
- PID 0x0203 confirmed in kernel 6.16.5 module aliases (our device)
- PID 0x0204 is a separate SkyWalker-1 hardware revision
- LOAD_BCM4500 (0x88) intentionally STALLs on Rev.2/SkyWalker hardware
- BCM4500 firmware loading protocol documented (64-byte chunked via EP0)
- Complete boot, tuning, DiSEqC, and streaming sequences mapped