Add SkyWalker-1 tuning tool and consolidated hardware reference
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.
This commit is contained in:
parent
76f0439576
commit
a2845c37fb
811
skywalker1-hardware-reference.md
Normal file
811
skywalker1-hardware-reference.md
Normal file
@ -0,0 +1,811 @@
|
|||||||
|
# Genpix SkyWalker-1 Hardware Reference
|
||||||
|
|
||||||
|
Consolidated technical reference for the Genpix SkyWalker-1 DVB-S USB 2.0 satellite receiver, derived from Linux kernel driver analysis, firmware reverse engineering (Ghidra), and Windows BDA driver source review.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
|
||||||
|
The Genpix SkyWalker-1 is a standalone USB 2.0 DVB-S satellite receiver built around two ICs:
|
||||||
|
|
||||||
|
- **Cypress CY7C68013A** (FX2LP) -- USB 2.0 Hi-Speed microcontroller (8051 core, 48 MHz)
|
||||||
|
- **Broadcom BCM4500** -- DVB-S / 8PSK satellite demodulator
|
||||||
|
|
||||||
|
The FX2 handles USB communication, LNB control, DiSEqC signaling, and orchestrates tuning via I2C commands to the BCM4500. The BCM4500 performs RF demodulation, FEC decoding, and outputs an MPEG-2 transport stream on an 8-bit parallel bus. The GPIF engine inside the FX2 transfers the transport stream directly into a USB bulk endpoint with zero firmware intervention in the data path.
|
||||||
|
|
||||||
|
### Supported Modulations
|
||||||
|
|
||||||
|
| Index | Modulation | Constant |
|
||||||
|
|-------|-----------|----------|
|
||||||
|
| 0 | DVB-S QPSK | `ADV_MOD_DVB_QPSK` |
|
||||||
|
| 1 | Turbo-coded QPSK | `ADV_MOD_TURBO_QPSK` |
|
||||||
|
| 2 | Turbo-coded 8PSK | `ADV_MOD_TURBO_8PSK` |
|
||||||
|
| 3 | Turbo-coded 16QAM | `ADV_MOD_TURBO_16QAM` |
|
||||||
|
| 4 | Digicipher II Combo | `ADV_MOD_DCII_C_QPSK` |
|
||||||
|
| 5 | Digicipher II I-stream (split) | `ADV_MOD_DCII_I_QPSK` |
|
||||||
|
| 6 | Digicipher II Q-stream (split) | `ADV_MOD_DCII_Q_QPSK` |
|
||||||
|
| 7 | Digicipher II Offset QPSK | `ADV_MOD_DCII_C_OQPSK` |
|
||||||
|
| 8 | DSS QPSK | `ADV_MOD_DSS_QPSK` |
|
||||||
|
| 9 | DVB-S BPSK | `ADV_MOD_DVB_BPSK` |
|
||||||
|
|
||||||
|
DVB-S2 is **not** supported (incompatible FEC architecture).
|
||||||
|
|
||||||
|
### RF Specifications
|
||||||
|
|
||||||
|
| Parameter | Value |
|
||||||
|
|-----------|-------|
|
||||||
|
| IF frequency range | 950 -- 2150 MHz |
|
||||||
|
| Symbol rate | 256 Ksps -- 30 Msps |
|
||||||
|
| Input connector | IEC F-type female |
|
||||||
|
| LNB voltage | 13/18V (or 14/19V with USE_EXTRA_VOLT) |
|
||||||
|
| LNB current | 450 mA continuous / 750 mA burst |
|
||||||
|
| Switch control | 22 kHz, Tone Burst, DiSEqC 1.0/1.2, Legacy Dish Network |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. USB Interface
|
||||||
|
|
||||||
|
### VID/PID Table
|
||||||
|
|
||||||
|
All Genpix products share VID `0x09C0`:
|
||||||
|
|
||||||
|
| PID | Product | cold_ids | warm_ids | Kernel Module | Notes |
|
||||||
|
|-----|---------|----------|----------|---------------|-------|
|
||||||
|
| 0x0200 | 8PSK-to-USB2 Rev.1 Cold | Yes | No | gp8psk | Requires FW01 upload to RAM |
|
||||||
|
| 0x0201 | 8PSK-to-USB2 Rev.1 Warm | No | Yes | gp8psk | Requires FW02 (BCM4500) |
|
||||||
|
| 0x0202 | 8PSK-to-USB2 Rev.2 | No | Yes | gp8psk | Boots from EEPROM |
|
||||||
|
| 0x0203 | **SkyWalker-1** | No | Yes | gp8psk | Boots from EEPROM |
|
||||||
|
| 0x0204 | SkyWalker-1 (alternate) | No | Yes | gp8psk | Boots from EEPROM |
|
||||||
|
| 0x0205 | SkyWalker-2 | -- | -- | -- | Not in kernel 6.16.5 |
|
||||||
|
| 0x0206 | SkyWalker CW3K | No | Yes | gp8psk | Requires CW3K_INIT (0x9D) |
|
||||||
|
|
||||||
|
PID 0x0203 was added to the kernel device table after v6.6.1.
|
||||||
|
|
||||||
|
### USB Endpoints and Streaming Properties
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| Control endpoint | EP0 (default, vendor requests) |
|
||||||
|
| Bulk IN endpoint | EP2 (0x82) -- MPEG-2 transport stream |
|
||||||
|
| Generic bulk CTRL endpoint | 0x01 (BCM4500 FW02 upload only) |
|
||||||
|
| URB count | 7 |
|
||||||
|
| URB buffer size | 8192 bytes each |
|
||||||
|
| Stream type | USB_BULK |
|
||||||
|
| FX2 controller type | CYPRESS_FX2 |
|
||||||
|
|
||||||
|
### Warm Boot Behavior
|
||||||
|
|
||||||
|
The SkyWalker-1 (PID 0x0203) enumerates directly as a "warm" device. The DVB-USB framework skips firmware download when `cold_ids` is NULL. No host-side firmware files (`dvb-usb-gp8psk-01.fw` or `dvb-usb-gp8psk-02.fw`) are required. These files were never open-sourced or included in the `linux-firmware` package.
|
||||||
|
|
||||||
|
| Device | Needs FW01? | Needs FW02? | Boot Source |
|
||||||
|
|--------|-------------|-------------|-------------|
|
||||||
|
| Rev.1 Cold (0x0200) | Yes | -- | RAM (empty) |
|
||||||
|
| Rev.1 Warm (0x0201) | No | Yes | RAM (FW01 loaded) |
|
||||||
|
| Rev.2 (0x0202) | No | No | EEPROM |
|
||||||
|
| SkyWalker-1 (0x0203) | No | No | EEPROM |
|
||||||
|
| SkyWalker CW3K (0x0206) | No | No | EEPROM |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Complete Vendor Command Reference
|
||||||
|
|
||||||
|
All vendor commands use USB control transfers:
|
||||||
|
- **USB Type**: `USB_TYPE_VENDOR`
|
||||||
|
- **Timeout**: 2000 ms
|
||||||
|
- **Retry**: Up to 3 attempts for IN operations if partial data received
|
||||||
|
- **Data buffer maximum**: 80 bytes (kernel driver)
|
||||||
|
|
||||||
|
### 3.1 Command Table
|
||||||
|
|
||||||
|
The vendor command dispatcher at CODE:0056 validates `bRequest` in the range 0x80--0x9D (30 entries) and dispatches via an indexed jump table at CODE:0076. Rev.2 supports only 0x80--0x9A (27 entries).
|
||||||
|
|
||||||
|
| Cmd | Name | Dir | wValue | wIndex | wLength | Purpose | Linux | Windows | v2.06 | Rev.2 | v2.13 |
|
||||||
|
|-----|------|-----|--------|--------|---------|---------|-------|---------|-------|-------|-------|
|
||||||
|
| 0x80 | GET_8PSK_CONFIG | IN | 0 | 0 | 1 | Read configuration status byte | Yes | Yes | OK | OK | OK |
|
||||||
|
| 0x81 | SET_8PSK_CONFIG | OUT | varies | 0 | 0 | Set config (reserved) | No | No | STALL | STALL | STALL |
|
||||||
|
| 0x82 | (reserved) | -- | -- | -- | -- | Reserved | No | No | STALL | STALL | STALL |
|
||||||
|
| 0x83 | I2C_WRITE | OUT | dev_addr | reg_addr | N | Write to I2C device | Yes | Yes | OK | OK | OK |
|
||||||
|
| 0x84 | I2C_READ | IN | dev_addr | reg_addr | N | Read from I2C device | Yes | Yes | OK | OK | OK |
|
||||||
|
| 0x85 | ARM_TRANSFER | OUT | 0/1 | 0 | 0 | Start (1) / stop (0) MPEG-2 stream | Yes | Yes | OK | OK | OK |
|
||||||
|
| 0x86 | TUNE_8PSK | OUT | 0 | 0 | 10 | Set tuning parameters (see Section 6) | Yes | Yes | OK | OK | OK |
|
||||||
|
| 0x87 | GET_SIGNAL_STRENGTH | IN | 0 | 0 | 6 | Read SNR and diagnostics | Yes | Yes | OK | OK | Changed |
|
||||||
|
| 0x88 | LOAD_BCM4500 | OUT | 1 | 0 | 0 | Initiate BCM4500 FW download | Yes* | No | STALL | STALL | STALL |
|
||||||
|
| 0x89 | BOOT_8PSK | IN | 0/1 | 0 | 1 | Power on (1) / off (0) demodulator | Yes | Yes | OK | OK | OK |
|
||||||
|
| 0x8A | START_INTERSIL | IN | 0/1 | 0 | 1 | Enable (1) / disable (0) LNB supply | Yes | Yes | OK | OK | OK |
|
||||||
|
| 0x8B | SET_LNB_VOLTAGE | OUT | 0/1 | 0 | 0 | 13V (0) or 18V (1) | Yes | Yes | OK | OK | OK |
|
||||||
|
| 0x8C | SET_22KHZ_TONE | OUT | 0/1 | 0 | 0 | Tone off (0) or on (1) | Yes | Yes | OK | OK | OK |
|
||||||
|
| 0x8D | SEND_DISEQC_COMMAND | OUT | msg[0] | 0 | len | DiSEqC message or tone burst | Yes | Yes | OK | OK | OK |
|
||||||
|
| 0x8E | SET_DVB_MODE | OUT | 1 | 0 | 0 | Enable DVB-S mode | Yes | No | STALL | STALL | STALL |
|
||||||
|
| 0x8F | SET_DN_SWITCH | OUT | cmd7bit | 0 | 0 | Legacy Dish Network switch protocol | Yes | Yes | OK | OK | OK |
|
||||||
|
| 0x90 | GET_SIGNAL_LOCK | IN | 0 | 0 | 1 | Read signal lock status | Yes | Yes | OK | OK | OK |
|
||||||
|
| 0x91 | I2C_ADDR_ADJUST | IN | 0/1 | 0 | 1 | Inc/dec internal IRAM counter (debug) | No | No | OK | OK | OK |
|
||||||
|
| 0x92 | GET_FW_VERS | IN | 0 | 0 | 6 | Read firmware version + build date | Yes | No | OK | OK | OK |
|
||||||
|
| 0x93 | GET_SERIAL_NUMBER | IN | 0 | 0 | 4 | Read 4-byte serial from I2C EEPROM | No | Yes | OK | OK | OK |
|
||||||
|
| 0x94 | USE_EXTRA_VOLT | OUT | 0/1 | 0 | 0 | Enable +1V LNB boost (14V/19V) | Yes | Yes | OK | OK | OK |
|
||||||
|
| 0x95 | GET_FPGA_VERS | IN | 0 | 0 | 1 | Read EEPROM hardware/platform ID | Yes | No | OK | OK | OK |
|
||||||
|
| 0x96 | SET_LNB_GPIO_MODE | OUT | 0/1 | 0 | 0 | Configure LNB GPIO output enables | No | No | OK | OK | OK |
|
||||||
|
| 0x97 | SET_GPIO_PINS | OUT | bitmap | 0 | 0 | Direct write to LNB GPIO pins | No | No | OK | OK | OK |
|
||||||
|
| 0x98 | GET_GPIO_STATUS | IN | 0 | 0 | 1 | Read LNB feedback GPIO pin | No | No | OK | OK | OK |
|
||||||
|
| 0x99 | GET_DEMOD_STATUS | IN | 0 | 0 | 1 | Read BCM4500 register 0xF9 | No | No | STALL | Proto | OK |
|
||||||
|
| 0x9A | INIT_DEMOD | OUT | 0 | 0 | 0 | Trigger demod re-init (3 attempts) | No | No | STALL | Proto | OK |
|
||||||
|
| 0x9B | (reserved) | -- | -- | -- | -- | Reserved | No | No | STALL | N/A | STALL |
|
||||||
|
| 0x9C | DELAY_COMMAND | OUT | delay | 0 | 0 | Host-controlled tuning delay + poll | No | No | STALL | N/A | OK |
|
||||||
|
| 0x9D | CW3K_INIT / SET_MODE_FLAG | OUT | 0/1 | 0 | 0 | CW3K init or conditional demod reset | Yes** | No | OK | N/A | Changed |
|
||||||
|
|
||||||
|
\* Linux driver only sends LOAD_BCM4500 for Rev.1 Warm (PID 0x0201). On SkyWalker-1, `bm8pskFW_Loaded` is already set and 0x88 routes to STALL.
|
||||||
|
|
||||||
|
\*\* Linux driver only sends CW3K_INIT for SkyWalker CW3K (PID 0x0206).
|
||||||
|
|
||||||
|
**Status key**: OK = implemented, STALL = routes to stall handler (endpoint stall returned), Proto = partial/prototype implementation, N/A = command index out of range (Rev.2 only supports 0x80--0x9A), Changed = implementation differs between versions.
|
||||||
|
|
||||||
|
### 3.2 Detailed Parameter Formats
|
||||||
|
|
||||||
|
**0x8D SEND_DISEQC_COMMAND**: When `wLength > 0`, the payload is a standard DiSEqC message (3--6 bytes) with `wValue` set to `msg[0]` (framing byte, typically 0xE0 or 0xE1). When `wLength == 0` and `wValue == 0`, a tone burst A is sent. When `wLength == 0` and `wValue != 0`, a tone burst B is sent.
|
||||||
|
|
||||||
|
**0x8F SET_DN_SWITCH**: `wValue` carries a 7-bit Dish Network switch command (LSB-first), bit-banged on GPIO P0.4 with specific timing. The 8th bit of the original switch command selects LNB voltage and is sent separately via SET_LNB_VOLTAGE.
|
||||||
|
|
||||||
|
**0x92 GET_FW_VERS**: Returns 6 bytes: `[minor_minor, minor, major, day, month, year-2000]`. Full version = `major<<16 | minor<<8 | minor_minor`. Build date = `(2000+year)/month/day`.
|
||||||
|
|
||||||
|
**0x93 GET_SERIAL_NUMBER**: Returns 4 bytes read from I2C EEPROM at device address 0x51 (7-bit), extracted at 8-bit intervals using a shift/rotate routine.
|
||||||
|
|
||||||
|
**0x94 USE_EXTRA_VOLT**: `wValue=1` writes 0x6A to XRAM 0xE0B6; `wValue=0` writes 0x62. The difference is bit 3 (0x08), which controls the extra voltage boost on the LNB power regulator.
|
||||||
|
|
||||||
|
**0x95 GET_FPGA_VERS**: Reads from I2C EEPROM at 0x51. Despite the name, there is no FPGA -- this returns the EEPROM-stored hardware platform ID. v2.06 reads offset 0x31 (2 bytes); v2.13/Rev.2 read offset 0x00 (1 byte).
|
||||||
|
|
||||||
|
**0x96--0x98**: Internal debug commands for LNB GPIO control. 0x96 configures output enables (OEB/OEA), 0x97 writes pin states, 0x98 reads a feedback pin. GPIO assignments differ between v2.06/v2.13 (Port B) and Rev.2 (Port A + Port B). See Section 10 for pin details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Configuration Status Byte
|
||||||
|
|
||||||
|
Returned by GET_8PSK_CONFIG (0x80). Stored in IRAM at version-dependent addresses.
|
||||||
|
|
||||||
|
```
|
||||||
|
Bit 7 (0x80): bmArmed - MPEG-2 stream transfer armed / GPIF active
|
||||||
|
Bit 6 (0x40): bmDCtuned - DC offset tuning complete (set for DCII modes)
|
||||||
|
Bit 5 (0x20): bmSEL18V - 18V LNB voltage selected (else 13V)
|
||||||
|
Bit 4 (0x10): bm22kHz - 22 kHz tone active
|
||||||
|
Bit 3 (0x08): bmDVBmode - DVB mode enabled
|
||||||
|
Bit 2 (0x04): bmIntersilOn - LNB power supply enabled
|
||||||
|
Bit 1 (0x02): bm8pskFW_Loaded - BCM4500 firmware loaded (always set on SkyWalker-1)
|
||||||
|
Bit 0 (0x01): bm8pskStarted - Device booted and running
|
||||||
|
```
|
||||||
|
|
||||||
|
| Version | IRAM Address |
|
||||||
|
|---------|-------------|
|
||||||
|
| v2.06 | 0x6D |
|
||||||
|
| Rev.2 v2.10.4 | 0x4E |
|
||||||
|
| v2.13 | 0x4F |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Boot Sequence
|
||||||
|
|
||||||
|
### 5.1 Two-Stage Firmware Architecture
|
||||||
|
|
||||||
|
The FX2 supports two firmware sources:
|
||||||
|
|
||||||
|
1. **Host RAM upload** (Rev.1 Cold only): The host writes 8051 code to FX2 RAM via USB 0xA0 vendor requests, using the built-in boot ROM. This requires `dvb-usb-gp8psk-01.fw` in binary hexline format.
|
||||||
|
|
||||||
|
2. **EEPROM boot** (Rev.2, SkyWalker-1, CW3K): The FX2 boot ROM reads firmware from an external I2C EEPROM in Cypress C2 format on power-up. No host interaction needed.
|
||||||
|
|
||||||
|
### 5.2 C2 EEPROM Format
|
||||||
|
|
||||||
|
SkyWalker-1 firmware is stored in Cypress C2 IIC second-stage boot format:
|
||||||
|
|
||||||
|
**Header (8 bytes):**
|
||||||
|
|
||||||
|
| Offset | Size | Field | SkyWalker-1 Value |
|
||||||
|
|--------|------|-------|-------------------|
|
||||||
|
| 0 | 1 | Marker | 0xC2 (external memory, large code model) |
|
||||||
|
| 1 | 2 | VID (LE) | 0x09C0 |
|
||||||
|
| 3 | 2 | PID (LE) | 0x0203 |
|
||||||
|
| 5 | 2 | DID (LE) | 0x0000 |
|
||||||
|
| 7 | 1 | Config | 0x40 (400 kHz I2C) |
|
||||||
|
|
||||||
|
**Code segments** follow: 2-byte length (BE) + 2-byte target address (BE) + data. Maximum segment size is 1023 bytes (FX2 I2C boot ROM buffer limit). All SkyWalker-1 variants use 10 segments.
|
||||||
|
|
||||||
|
**Terminator**: 0x80xx (high bit set) + 2-byte entry point address (0xE600 = CPUCS).
|
||||||
|
|
||||||
|
### 5.3 Power-On Boot Sequence
|
||||||
|
|
||||||
|
```
|
||||||
|
1. GET_8PSK_CONFIG (0x80) -- read config status byte
|
||||||
|
|-- Check bit 0: bm8pskStarted?
|
||||||
|
|
||||||
|
2. If not started:
|
||||||
|
|-- BOOT_8PSK (0x89, wValue=1)
|
||||||
|
|-- GET_FW_VERS (0x92) -- read firmware version
|
||||||
|
|
||||||
|
3. If bit 1 clear (bm8pskFW_Loaded):
|
||||||
|
|-- LOAD_BCM4500 (0x88) -- Rev.1 Warm only; STALLs on SkyWalker-1
|
||||||
|
|
||||||
|
4. If bit 2 clear (bmIntersilOn):
|
||||||
|
|-- START_INTERSIL (0x8A, wValue=1) -- enable LNB power supply
|
||||||
|
|
||||||
|
5. SET_DVB_MODE (0x8E, wValue=1) -- STALLs on all SkyWalker-1 FW versions
|
||||||
|
|
||||||
|
6. ARM_TRANSFER (0x85, wValue=0) -- abort any pending MPEG transfer
|
||||||
|
|
||||||
|
7. Device ready for tuning
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 Firmware Version Identification
|
||||||
|
|
||||||
|
The kernel reads firmware version on boot via GET_FW_VERS (0x92) and logs:
|
||||||
|
```
|
||||||
|
gp8psk: FW Version = 2.06.4 (0x20604) Build 2007/07/13
|
||||||
|
```
|
||||||
|
|
||||||
|
Kernel revision constants (from `gp8psk-fe.h`):
|
||||||
|
```
|
||||||
|
GP8PSK_FW_REV1 = 0x020604 (v2.06.4)
|
||||||
|
GP8PSK_FW_REV2 = 0x020704 (v2.07.4)
|
||||||
|
```
|
||||||
|
|
||||||
|
If `fw_vers >= GP8PSK_FW_REV2`, the kernel enables Rev.2-specific code paths. The v2.10 and v2.13 firmwares are newer than either kernel constant.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Tuning Protocol
|
||||||
|
|
||||||
|
### 6.1 TUNE_8PSK Command Format (0x86)
|
||||||
|
|
||||||
|
The host sends a 10-byte OUT payload via USB control transfer:
|
||||||
|
|
||||||
|
```
|
||||||
|
USB SETUP: bmRequestType=0x40, bRequest=0x86, wValue=0, wIndex=0, wLength=10
|
||||||
|
|
||||||
|
EP0BUF Layout:
|
||||||
|
Byte Content Encoding
|
||||||
|
---- ------------------ ----------------
|
||||||
|
[0] Symbol Rate byte 0 Little-endian LSB
|
||||||
|
[1] Symbol Rate byte 1
|
||||||
|
[2] Symbol Rate byte 2
|
||||||
|
[3] Symbol Rate byte 3 Little-endian MSB
|
||||||
|
[4] Frequency byte 0 Little-endian LSB
|
||||||
|
[5] Frequency byte 1
|
||||||
|
[6] Frequency byte 2
|
||||||
|
[7] Frequency byte 3 Little-endian MSB
|
||||||
|
[8] Modulation Type 0--9 (see Section 1 table)
|
||||||
|
[9] Inner FEC Rate Index into modulation-specific table
|
||||||
|
```
|
||||||
|
|
||||||
|
**Symbol Rate** is in samples per second (sps). The Windows driver multiplies ksps by 1000.
|
||||||
|
|
||||||
|
**Frequency** is the IF frequency in kHz (950000--2150000), computed by the host as `(RF_freq - LO_freq) * multiplier`.
|
||||||
|
|
||||||
|
### 6.2 Firmware EP0BUF Parsing
|
||||||
|
|
||||||
|
The firmware reads the 10-byte payload from EP0BUF (XRAM 0xE740--0xE749) and stores:
|
||||||
|
|
||||||
|
| Source | Destination | Notes |
|
||||||
|
|--------|-------------|-------|
|
||||||
|
| EP0BUF[8] (mod) | IRAM 0x4D | Direct copy |
|
||||||
|
| EP0BUF[9] (FEC) | IRAM 0x4F | Direct copy |
|
||||||
|
| EP0BUF[4--7] (freq) | XRAM 0xE0DB--0xE0DE | Byte-reversed (LE to BE) |
|
||||||
|
| EP0BUF[0--3] (SR) | XRAM 0xE0CB--0xE0CE | Byte-reversed (LE to BE) |
|
||||||
|
|
||||||
|
The byte reversal converts host little-endian to BCM4500 big-endian so values can be written directly to the demodulator via I2C.
|
||||||
|
|
||||||
|
### 6.3 Modulation Dispatch
|
||||||
|
|
||||||
|
After parsing, the firmware validates the modulation type (< 10) and dispatches via a jump table to modulation-specific handlers. Each handler:
|
||||||
|
|
||||||
|
1. Validates the FEC index against the maximum for that modulation
|
||||||
|
2. Looks up a preconfigured byte from an XRAM table
|
||||||
|
3. Writes configuration to four XRAM registers
|
||||||
|
|
||||||
|
**FEC Rate Lookup Tables** (populated from the CODE-space init table at boot):
|
||||||
|
|
||||||
|
| XRAM Base | Modulation | Max FEC Index | Notes |
|
||||||
|
|-----------|-----------|---------------|-------|
|
||||||
|
| 0xE0F9 | DVB-S QPSK, DSS, BPSK | 7 | Standard Viterbi rates (1/2, 2/3, 3/4, 5/6, 7/8, auto, none) |
|
||||||
|
| 0xE0B7 | Turbo QPSK | 5 | Turbo code rates |
|
||||||
|
| 0xE0B1 | Turbo 8PSK | 5 | Turbo code rates |
|
||||||
|
| 0xE0BC | Turbo 16QAM | 1 | Single code rate |
|
||||||
|
| 0xE0BD | DCII (all variants) | 9 | Combined code rate + modulation |
|
||||||
|
|
||||||
|
**BCM4500 XRAM Configuration after dispatch:**
|
||||||
|
|
||||||
|
| XRAM Addr | Register | DVB-S QPSK | Turbo (Q/8/16) | DCII | DSS/BPSK |
|
||||||
|
|-----------|----------|-----------|---------------|------|----------|
|
||||||
|
| 0xE0EB | FEC Code Rate | Table lookup | Table lookup | 0xFC (fixed) | Table lookup OR 0x80 |
|
||||||
|
| 0xE0EC | Modulation Type | 0x09 | 0x09 | From DCII table | 0x09 |
|
||||||
|
| 0xE0F5 | Demod Mode | 0x10 | 0x10 | 0x10/0x11/0x12/0x16 | 0x10 |
|
||||||
|
| 0xE0F6 | Turbo Flag | 0x00 | 0x01 | 0x00 | 0x00 |
|
||||||
|
|
||||||
|
**DCII Demod Mode values:**
|
||||||
|
|
||||||
|
| Modulation | XRAM 0xE0F5 Value |
|
||||||
|
|-----------|-------------------|
|
||||||
|
| DCII Combo (4) | 0x10 |
|
||||||
|
| DCII Offset QPSK (7) | 0x11 |
|
||||||
|
| DCII I-stream (5) | 0x12 |
|
||||||
|
| DCII Q-stream (6) | 0x16 |
|
||||||
|
|
||||||
|
DSS (8) and DVB BPSK (9) share the same handler; they use the DVB-S QPSK FEC table but OR the lookup value with 0x80.
|
||||||
|
|
||||||
|
### 6.4 Complete Tuning Sequence (Host to Satellite)
|
||||||
|
|
||||||
|
```
|
||||||
|
=== Phase 1: LNB Configuration (separate vendor commands) ===
|
||||||
|
1. SET_LNB_VOLTAGE (0x8B) -- GPIO P0.4 (no I2C)
|
||||||
|
H / Circular-L -> wValue=1 (18V)
|
||||||
|
V / Circular-R -> wValue=0 (13V)
|
||||||
|
2. SET_22KHZ_TONE (0x8C) -- GPIO P0.3 (no I2C)
|
||||||
|
High band -> wValue=1 (tone on)
|
||||||
|
Low band -> wValue=0 (tone off)
|
||||||
|
3. SEND_DISEQC_COMMAND (0x8D) -- if multi-switch needed
|
||||||
|
|
||||||
|
=== Phase 2: Tune Command ===
|
||||||
|
4. TUNE_8PSK (0x86) -- 10-byte payload
|
||||||
|
|
||||||
|
=== Phase 3: Firmware Internal Processing ===
|
||||||
|
5. EP0BUF parsing: mod/FEC to IRAM, freq/SR byte-reversed to XRAM
|
||||||
|
6. Modulation dispatch: FEC lookup, XRAM config registers set
|
||||||
|
7. GPIO P3.6: DVB mode select
|
||||||
|
|
||||||
|
=== Phase 4: BCM4500 I2C Programming (3 outer retries x 3 I2C addresses) ===
|
||||||
|
8. Poll BCM4500 ready: I2C READ regs 0xA2, 0xA8, 0xA4
|
||||||
|
9. Write page: I2C WRITE reg 0xA6 <- 0x00
|
||||||
|
10. Write config: I2C WRITE reg 0xA7 <- [freq, SR, FEC, mod, demod params]
|
||||||
|
11. Execute: I2C WRITE reg 0xA8 <- 0x03 (indirect write command)
|
||||||
|
12. Poll completion: I2C READ regs 0xA8, 0xA2
|
||||||
|
13. Verify: I2C READ reg 0xA7 (read-back compare)
|
||||||
|
|
||||||
|
=== Phase 5: Signal Acquisition (host polling) ===
|
||||||
|
14. GET_SIGNAL_LOCK (0x90) -- poll until non-zero
|
||||||
|
15. GET_SIGNAL_STRENGTH (0x87) -- read SNR
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.5 Signal Lock (GET_SIGNAL_LOCK, 0x90)
|
||||||
|
|
||||||
|
Returns 1 byte from BCM4500 register 0xA4. Bit 5 (0x20) indicates signal lock. Both the Linux and Windows drivers interpret any non-zero value as locked and report full lock status (`FE_HAS_LOCK | FE_HAS_SYNC | FE_HAS_VITERBI | FE_HAS_SIGNAL | FE_HAS_CARRIER`).
|
||||||
|
|
||||||
|
### 6.6 Signal Strength (GET_SIGNAL_STRENGTH, 0x87)
|
||||||
|
|
||||||
|
Returns 6 bytes. The first two bytes contain a 16-bit SNR value (little-endian, in dBu * 256 units):
|
||||||
|
|
||||||
|
```
|
||||||
|
Byte 0: SNR low byte (LSB)
|
||||||
|
Byte 1: SNR high byte (MSB)
|
||||||
|
Bytes 2-5: Reserved / BCM4500 diagnostic registers
|
||||||
|
```
|
||||||
|
|
||||||
|
**SNR scaling formula** (from Windows BDA driver):
|
||||||
|
```
|
||||||
|
snr_raw = (buf[1] << 8) | buf[0]
|
||||||
|
if snr_raw <= 0x0F00:
|
||||||
|
signal_strength = snr_raw * 17 // maps to 0--65535
|
||||||
|
else:
|
||||||
|
signal_strength = 0xFFFF // 100% at SNR >= 0x0F00
|
||||||
|
```
|
||||||
|
|
||||||
|
The firmware performs a multi-step I2C transaction to read signal quality: BCM4500 indirect register write/read via 0xA6/0xA7/0xA8, with read-back verification.
|
||||||
|
|
||||||
|
Version differences:
|
||||||
|
- v2.06: polls 3 registers (0xA2, 0xA8, 0xA4), loops up to 6 times
|
||||||
|
- v2.13: simplified polling (consolidated to 1 register), different call chain
|
||||||
|
- Rev.2: explicit write/read-back verification step
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. BCM4500 Demodulator Interface
|
||||||
|
|
||||||
|
### 7.1 I2C Bus
|
||||||
|
|
||||||
|
The BCM4500 is accessed via the FX2's I2C controller at XRAM 0xE678.
|
||||||
|
|
||||||
|
| Parameter | Value |
|
||||||
|
|-----------|-------|
|
||||||
|
| Primary I2C address | 0x10 (7-bit) |
|
||||||
|
| Alternate addresses | 0x3F, 0x7F (probed by v2.13 INT0 handler) |
|
||||||
|
| Bus speed | 400 kHz (set via C2 header config byte 0x40) |
|
||||||
|
| EEPROM address | 0x51 (7-bit, 24Cxx-family, for serial number / platform ID) |
|
||||||
|
|
||||||
|
The FX2 I2C controller is accessed through XRAM registers:
|
||||||
|
- 0xE678: I2C control/status register
|
||||||
|
- 0xE679: I2C data register
|
||||||
|
|
||||||
|
### 7.2 Indirect Register Protocol
|
||||||
|
|
||||||
|
The BCM4500 uses an indirect register access scheme through three I2C-accessible registers:
|
||||||
|
|
||||||
|
```
|
||||||
|
Register 0xA6: Page/address select (write page number)
|
||||||
|
Register 0xA7: Data register (read/write indirect data)
|
||||||
|
Register 0xA8: Command register (write 0x03 to execute indirect write)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Write sequence:**
|
||||||
|
```
|
||||||
|
1. I2C WRITE device 0x10, reg 0xA6 <- page_number (typically 0x00)
|
||||||
|
2. I2C WRITE device 0x10, reg 0xA7 <- data_bytes (N bytes)
|
||||||
|
3. I2C WRITE device 0x10, reg 0xA8 <- 0x03 (execute)
|
||||||
|
4. Poll reg 0xA8 until command complete
|
||||||
|
5. Read back reg 0xA7 to verify
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 Status Registers
|
||||||
|
|
||||||
|
| Register | Function |
|
||||||
|
|----------|----------|
|
||||||
|
| 0xA2 | BCM4500 status (polled for readiness) |
|
||||||
|
| 0xA4 | Lock/ready register; bit 5 = signal locked |
|
||||||
|
| 0xA8 | Command register; bit 0 = command done (polled) |
|
||||||
|
| 0xF9 | Demod status (read by v2.13 GET_DEMOD_STATUS / INT0 polling) |
|
||||||
|
|
||||||
|
### 7.4 Demod Scan
|
||||||
|
|
||||||
|
The tune function tries up to 3 different I2C address configurations per attempt, with 3 outer retries (total: up to 9 I2C programming attempts). This supports hardware variants where the BCM4500 may appear at different bus addresses.
|
||||||
|
|
||||||
|
The demod scan function (Rev.2 FUN_CODE_1dd0) iterates through parameter sets computed from the iteration index (address offsets multiplied by 0x11), calling the indirect write function (FUN_CODE_1670) for each.
|
||||||
|
|
||||||
|
v2.13 adds a more sophisticated probe at boot: INT0 polls addresses 0x7F and 0x3F up to 40 times (0x28), setting a "no demod found" flag (`_1_4`) if neither responds. This flag prevents tuning attempts on boards with absent or failed demodulators.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. GPIF Streaming Path
|
||||||
|
|
||||||
|
### 8.1 Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
BCM4500 Cypress FX2 (CY7C68013A) USB Host
|
||||||
|
Demodulator P3.5 GPIF Engine EP2 FIFO EP2 (0x82)
|
||||||
|
(I2C:0x10) <-----> (Master Read) (AUTOIN) ------------> Bulk IN
|
||||||
|
8-bit 0xE4xx wfm 4x buffer 7 URBs
|
||||||
|
parallel 8-bit x 8KB
|
||||||
|
```
|
||||||
|
|
||||||
|
The path is fully hardware-managed. The GPIF engine reads data from the BCM4500's 8-bit parallel transport stream output directly into the EP2 FIFO. The AUTOIN bit causes automatic USB commit when the FIFO buffer is full. The FLOWSTATE engine automatically re-triggers GPIF transactions when buffer space becomes available. No firmware intervention occurs in the data path after initial setup.
|
||||||
|
|
||||||
|
### 8.2 Key Register Configuration
|
||||||
|
|
||||||
|
All values are identical across the three firmware versions:
|
||||||
|
|
||||||
|
| Register | Address | Value | Function |
|
||||||
|
|----------|---------|-------|----------|
|
||||||
|
| IFCONFIG | 0xE601 | 0xEE | Internal 48 MHz clock, GPIF master mode, async, debug output |
|
||||||
|
| EP2FIFOCFG | 0xE618 | 0x0C | AUTOIN=1, ZEROLENIN=1, 8-bit data path |
|
||||||
|
| REVCTL | 0xE60B | 0x03 | NOAUTOARM + SKIPCOMMIT |
|
||||||
|
| CPUCS | 0xE600 | bits [4:3]=10 | 48 MHz CPU clock |
|
||||||
|
| FLOWSTATEA | 0xE668 | OR= 0x09 | FSEN (flow state enable) + FS[3] |
|
||||||
|
| GPIFIE | 0xE65C | OR= 0x3D | Waveform, TC, DONE, FIFO flag, WF2 interrupts |
|
||||||
|
|
||||||
|
**IFCONFIG decode (0xEE = 1110_1110):**
|
||||||
|
- Bit 7: IFCLKSRC=1 (internal clock)
|
||||||
|
- Bit 6: 3048MHZ=1 (48 MHz)
|
||||||
|
- Bit 5: IFCLKOE=1 (clock output to BCM4500)
|
||||||
|
- Bit 4: IFCLKPOL=0 (non-inverted)
|
||||||
|
- Bit 3: ASYNC=1 (RDY pin handshaking, not clock-edge sampling)
|
||||||
|
- Bit 2: GSTATE=1 (debug state output on PORTE)
|
||||||
|
- Bits 1:0: IFCFG=10 (GPIF internal master)
|
||||||
|
|
||||||
|
### 8.3 ARM_TRANSFER Sequences
|
||||||
|
|
||||||
|
**Start streaming (wValue=1):**
|
||||||
|
1. Set config_byte bit 7 (streaming active)
|
||||||
|
2. Load GPIF transaction count: GPIFTCB3:2 = 0x8000 (effectively infinite)
|
||||||
|
3. Reset GPIF address and EP2 FIFO byte count
|
||||||
|
4. Assert P3.5 LOW (BCM4500 transport stream enable)
|
||||||
|
5. Wait for initial GPIF transaction (poll GPIFTRIG bit 7)
|
||||||
|
6. De-assert P3.5 HIGH
|
||||||
|
7. Trigger continuous GPIF read: GPIFTRIG = 0x04 (read into EP2)
|
||||||
|
8. Set P0.7 LOW (streaming indicator)
|
||||||
|
|
||||||
|
**Stop streaming (wValue=0):**
|
||||||
|
1. Set P0.7 HIGH (streaming stopped)
|
||||||
|
2. Write EP2FIFOBCH = 0xFF (force-flush current buffer)
|
||||||
|
3. Wait for GPIF idle (poll GPIFTRIG bit 7)
|
||||||
|
4. Write OUTPKTEND = 0x82 (skip/discard partial EP2 packet)
|
||||||
|
5. Clear config_byte bit 7 (streaming inactive)
|
||||||
|
6. Set P3 bits 7:5 = 1 (de-assert all BCM4500 control lines)
|
||||||
|
|
||||||
|
### 8.4 Interrupt Handling
|
||||||
|
|
||||||
|
INT4 and INT6 (GPIF/FIFO events) share a common handler that sets a software flag and clears the hardware interrupt. The main loop polls this flag, enters CPU idle mode (PCON.0) between events, and checks EP2CS for buffer availability before re-arming the GPIF.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. LNB and DiSEqC Control
|
||||||
|
|
||||||
|
### 9.1 LNB Voltage
|
||||||
|
|
||||||
|
LNB voltage is controlled via GPIO P0.4 on all firmware versions. No I2C is involved.
|
||||||
|
|
||||||
|
| wValue | Voltage | GPIO P0.4 | Polarization |
|
||||||
|
|--------|---------|-----------|-------------|
|
||||||
|
| 0 | 13V | LOW | Vertical / Circular-Right |
|
||||||
|
| 1 | 18V | HIGH | Horizontal / Circular-Left |
|
||||||
|
|
||||||
|
**USE_EXTRA_VOLT** (0x94) enables a +1V boost (13V->14V, 18V->19V) for long cable runs, by writing to XRAM 0xE0B6 (0x62=normal, 0x6A=boosted; difference is bit 3).
|
||||||
|
|
||||||
|
### 9.2 22 kHz Tone
|
||||||
|
|
||||||
|
The 22 kHz tone is controlled via GPIO P0.3 on all firmware versions. P0.3 gates an external 22 kHz oscillator on the PCB. The firmware does not generate the 22 kHz signal directly.
|
||||||
|
|
||||||
|
| wValue | State | GPIO P0.3 | Band |
|
||||||
|
|--------|-------|-----------|------|
|
||||||
|
| 0 | OFF | LOW | Low band (9.75 GHz LO on universal LNB) |
|
||||||
|
| 1 | ON | HIGH | High band (10.6 GHz LO on universal LNB) |
|
||||||
|
|
||||||
|
### 9.3 DiSEqC Protocol
|
||||||
|
|
||||||
|
All firmware versions implement DiSEqC via Timer2-based GPIO bit-bang. The algorithm is identical across versions; only the data pin assignment differs (see Section 10).
|
||||||
|
|
||||||
|
**Timer2 configuration (identical across all versions):**
|
||||||
|
|
||||||
|
| Parameter | Value |
|
||||||
|
|-----------|-------|
|
||||||
|
| T2CON | 0x04 (auto-reload, running) |
|
||||||
|
| RCAP2H:RCAP2L | 0xF82F (reload = 63535) |
|
||||||
|
| CKCON.T2M | 0 (Timer2 clock = 48 MHz / 12 = 4 MHz) |
|
||||||
|
| Tick period | (65536 - 63535) / 4 MHz = 500.25 us |
|
||||||
|
|
||||||
|
**DiSEqC timing:**
|
||||||
|
|
||||||
|
| Parameter | Value |
|
||||||
|
|-----------|-------|
|
||||||
|
| Bit period | 1.5 ms (3 Timer2 ticks) |
|
||||||
|
| Byte period | 13.5 ms (9 bits: 8 data + 1 parity) |
|
||||||
|
| Tone burst | 12.5 ms (25 ticks) |
|
||||||
|
| Pre-TX settling delay | 7.5 ms (15 ticks) |
|
||||||
|
| Data '0' | 1.0 ms tone + 0.5 ms silence (2/3 duty) |
|
||||||
|
| Data '1' | 0.5 ms tone + 1.0 ms silence (1/3 duty) |
|
||||||
|
| Carrier frequency | 22 kHz (external oscillator gated by P0.3) |
|
||||||
|
|
||||||
|
**Manchester encoding** (decompiled from Rev.2 FUN_CODE_213c):
|
||||||
|
```
|
||||||
|
Each DiSEqC bit = 3 Timer2 ticks:
|
||||||
|
Tick 1: inter-bit gap (carrier OFF)
|
||||||
|
Tick 2: carrier ON via P0.3
|
||||||
|
Tick 3: if data='1', carrier OFF early; if data='0', carrier stays ON
|
||||||
|
End: carrier OFF
|
||||||
|
```
|
||||||
|
|
||||||
|
**Byte transmission**: 8 data bits MSB-first + 1 odd parity bit, each encoded as a Manchester symbol. The parity bit is '1' when the number of '1' data bits is even.
|
||||||
|
|
||||||
|
**Tone burst** (mini DiSEqC): 25 consecutive Timer2 ticks of carrier (12.5 ms).
|
||||||
|
|
||||||
|
### 9.4 SET_DN_SWITCH (0x8F) -- Legacy Dish Network Protocol
|
||||||
|
|
||||||
|
A 7-bit serial command bit-banged on GPIO P0.4 with specific timing:
|
||||||
|
|
||||||
|
1. Assert P0.4 HIGH (start pulse)
|
||||||
|
2. Delay ~32 cycles
|
||||||
|
3. De-assert P0.4
|
||||||
|
4. Delay ~8 cycles
|
||||||
|
5. Shift out 7 bits LSB-first via P0.4, with ~8 cycle delays between bits
|
||||||
|
|
||||||
|
The Linux kernel calls this via the `dishnetwork_send_legacy_command` frontend callback. The 8th bit (0x80) of the original switch command controls LNB voltage and is sent separately via SET_LNB_VOLTAGE.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. GPIO Pin Map
|
||||||
|
|
||||||
|
### Port 0 (SFR 0x80, a.k.a. IOA)
|
||||||
|
|
||||||
|
| Pin | v2.06 | Rev.2 v2.10 | v2.13 | Notes |
|
||||||
|
|-----|-------|-------------|-------|-------|
|
||||||
|
| P0.0 | -- | LNB control (0x97) | **DiSEqC data** | DiSEqC data pin moved across versions |
|
||||||
|
| P0.1 | -- | -- | -- | |
|
||||||
|
| P0.2 | Init set | Init set (0x84) | Init set | BCM4500 control |
|
||||||
|
| P0.3 | **22 kHz tone** | **22 kHz tone** | **22 kHz tone** | Gates external 22 kHz oscillator |
|
||||||
|
| P0.4 | **LNB 13V/18V** | **LNB 13V/18V** + DiSEqC data | **LNB 13V/18V** | Also SET_DN_SWITCH bit-bang (all versions) |
|
||||||
|
| P0.5 | -- | GPIO status (0x98) input | -- | LNB feedback on Rev.2 |
|
||||||
|
| P0.6 | -- | GPIO control (0x97) | -- | LNB control on Rev.2 |
|
||||||
|
| P0.7 | **DiSEqC data** | Streaming indicator | Streaming indicator | DiSEqC data on v2.06 only |
|
||||||
|
|
||||||
|
### Port 3 (SFR 0xB0)
|
||||||
|
|
||||||
|
| Pin | Function | Notes |
|
||||||
|
|-----|----------|-------|
|
||||||
|
| P3.0 | Init HIGH | |
|
||||||
|
| P3.4 | GPIO control | Used by FUN_CODE_1fcf (Rev.2) |
|
||||||
|
| P3.5 | **TS_EN** | Transport stream enable: LOW=active, HIGH=idle |
|
||||||
|
| P3.6 | **DVB mode** | BCM4500 mode select; DiSEqC port direction (Rev.2) |
|
||||||
|
| P3.7 | BCM4500 control | De-asserted (HIGH) when streaming stops |
|
||||||
|
|
||||||
|
### Port B (XRAM-mapped IOB)
|
||||||
|
|
||||||
|
Used by internal debug commands 0x96--0x98:
|
||||||
|
|
||||||
|
| Pin | v2.06/v2.13 | Rev.2 |
|
||||||
|
|-----|-------------|-------|
|
||||||
|
| IOB.0 | GPIO status input (0x98) | -- |
|
||||||
|
| IOB.1 | LNB control (0x97) | -- |
|
||||||
|
| IOB.2 | LNB control (0x97) | -- |
|
||||||
|
| IOB.3 | LNB GPIO mode (0x96) | -- |
|
||||||
|
| IOB.4 | -- | LNB GPIO mode (0x96) + control (0x97) |
|
||||||
|
|
||||||
|
**Init values (Rev.2):**
|
||||||
|
- P0 = 0x84 (P0.7=1, P0.2=1)
|
||||||
|
- P3 = 0xE1 (P3.7:5=1, P3.0=1)
|
||||||
|
- IPL1 = 0x9E
|
||||||
|
|
||||||
|
### DiSEqC Data Pin Summary
|
||||||
|
|
||||||
|
| Version | Data Pin | Carrier Pin |
|
||||||
|
|---------|----------|-------------|
|
||||||
|
| v2.06 | P0.7 | P0.3 |
|
||||||
|
| Rev.2 v2.10 | P0.4 | P0.3 |
|
||||||
|
| v2.13 | P0.0 | P0.3 |
|
||||||
|
|
||||||
|
The carrier pin (P0.3) is the same across all versions. The data pin is used only internally by the firmware's Manchester encoding logic to control whether the carrier is cut short or held for the full bit period.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Firmware Versions
|
||||||
|
|
||||||
|
### 11.1 Version Table
|
||||||
|
|
||||||
|
| Firmware | Version | Build Date | PID | Functions | Binary Size | Stack Ptr |
|
||||||
|
|----------|---------|------------|-----|-----------|-------------|-----------|
|
||||||
|
| v2.06.04 | 0x020604 | 2007-07-13 | 0x0203 | 61 | ~9,472 bytes | SP=0x72 |
|
||||||
|
| Rev.2 v2.10.04 | 0x020A04 | 2010-03-12 | 0x0202 | 107 | ~8,843 bytes | SP=0x4F |
|
||||||
|
| v2.13.01 | 0x020D01 | 2010-03-12 | 0x0203 | 88 | ~9,322 bytes | SP=0x50 |
|
||||||
|
|
||||||
|
Note: Rev.2 v2.10 targets a different product (PID 0x0202). The v2.13 family has three sub-variants (FW1/FW2/FW3) targeting different SkyWalker-1 hardware sub-revisions.
|
||||||
|
|
||||||
|
### 11.2 GET_FW_VERS (0x92) Format
|
||||||
|
|
||||||
|
Returns 6 bytes of hardcoded constants:
|
||||||
|
|
||||||
|
```
|
||||||
|
Byte 0: version minor_minor (e.g., 0x04)
|
||||||
|
Byte 1: version minor (e.g., 0x06)
|
||||||
|
Byte 2: version major (e.g., 0x02)
|
||||||
|
Byte 3: build day (e.g., 0x0D = 13)
|
||||||
|
Byte 4: build month (e.g., 0x07 = July)
|
||||||
|
Byte 5: build year - 2000 (e.g., 0x07 = 2007)
|
||||||
|
```
|
||||||
|
|
||||||
|
Full version number: `byte[2] << 16 | byte[1] << 8 | byte[0]`
|
||||||
|
|
||||||
|
Kernel constants for comparison:
|
||||||
|
```
|
||||||
|
GP8PSK_FW_REV1 = 0x020604
|
||||||
|
GP8PSK_FW_REV2 = 0x020704
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11.3 Binary Comparison Matrix
|
||||||
|
|
||||||
|
Byte-level similarity (percentage of matching bytes within shared length):
|
||||||
|
|
||||||
|
| | v2.06 | v2.13.1 | v2.13.2 | v2.13.3 | Rev.2 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| v2.06 | -- | 4.8% | 4.3% | 4.3% | 6.0% |
|
||||||
|
| v2.13.1 | | -- | 57.2% | 59.4% | 8.0% |
|
||||||
|
| v2.13.2 | | | -- | 83.5% | 5.8% |
|
||||||
|
| v2.13.3 | | | | -- | 5.8% |
|
||||||
|
| Rev.2 | | | | | -- |
|
||||||
|
|
||||||
|
The very low similarity between major versions (4--8%) indicates complete recompilation with different linker configurations. Functions are relocated even when logic is identical. Within the v2.13 family, FW2 and FW3 are 83.5% similar (minor hardware tuning), while FW1 differs more (57--59%, different demod interface).
|
||||||
|
|
||||||
|
### 11.4 Key Differences Between Versions
|
||||||
|
|
||||||
|
| Feature | v2.06 | Rev.2 v2.10 | v2.13 |
|
||||||
|
|---------|-------|-------------|-------|
|
||||||
|
| Vendor commands | 30 (0x80--0x9D) | 27 (0x80--0x9A) | 30 (0x80--0x9D) |
|
||||||
|
| INT0 purpose | USB re-enumeration | USB re-enumeration | Demod availability polling |
|
||||||
|
| Demod probe at init | No | No | Yes (40 attempts at 0x7F + 0x3F) |
|
||||||
|
| Retry loops | No | No | Yes (20-attempt with checksum verification) |
|
||||||
|
| HW revision detection | No | Yes (descriptor walker) | Yes (flag `_1_3`) |
|
||||||
|
| DiSEqC data pin | P0.7 | P0.4 | P0.0 |
|
||||||
|
| Config byte IRAM | 0x6D | 0x4E | 0x4F |
|
||||||
|
| Descriptor base | 0x1200 | 0x0E00 | 0x0E00 |
|
||||||
|
| Init table address | CODE:0B46 | CODE:0B48 | CODE:0B88 |
|
||||||
|
| BCM4500 status poll | 3 registers | 3 registers | 1 register (consolidated) |
|
||||||
|
| Anti-tampering string | No | No | Yes (at offset 0x1880) |
|
||||||
|
| New commands | -- | 0x99/0x9A proto | 0x99 GET_DEMOD_STATUS, 0x9A INIT_DEMOD, 0x9C DELAY_COMMAND |
|
||||||
|
| 0x9D behavior | HW revision mode switch | N/A (out of range) | Conditional demod reset |
|
||||||
|
|
||||||
|
### 11.5 EEPROM Format Details
|
||||||
|
|
||||||
|
All SkyWalker-1 C2 files use uniform 1023-byte segments:
|
||||||
|
|
||||||
|
```
|
||||||
|
Segment Address Length
|
||||||
|
------- ------- ------
|
||||||
|
1 0x0000 1023 Contains reset vector, interrupt handlers
|
||||||
|
2 0x03FF 1023
|
||||||
|
3 0x07FE 1023
|
||||||
|
4 0x0BFD 1023
|
||||||
|
5 0x0FFC 1023
|
||||||
|
6 0x13FB 1023
|
||||||
|
7 0x17FA 1023
|
||||||
|
8 0x1BF9 1023
|
||||||
|
9 0x1FF8 1023
|
||||||
|
10 0x23F7 varies (115--265 bytes depending on version)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11.6 Anti-Tampering (v2.13 only)
|
||||||
|
|
||||||
|
At firmware offset 0x1880, all v2.13 sub-variants contain:
|
||||||
|
```
|
||||||
|
"Tampering is detected. Attempt is logged. Warranty is voided ! \n"
|
||||||
|
```
|
||||||
|
Followed by I2C register write commands (`01 10 aa 82 02 41 41 83`). This string and mechanism are absent from v2.06 and Rev.2 firmware.
|
||||||
|
|
||||||
|
### 11.7 Rev.2 as Transitional Firmware
|
||||||
|
|
||||||
|
Rev.2 v2.10.4 sits architecturally between v2.06 and v2.13:
|
||||||
|
- Adopted v2.13's stack pointer (SP=0x4F) and descriptor base (0x0E00)
|
||||||
|
- Retained v2.06's INT0 USB re-enumeration behavior
|
||||||
|
- Lacks v2.13's demodulator polling, retry loops, and 3 additional vendor commands
|
||||||
|
- Has the most functions (107) but smallest binary (~8.7 KB) due to granular decomposition
|
||||||
|
- The INT0 repurposing was the last major architectural change between Rev.2 and v2.13
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Internal Debug Commands
|
||||||
|
|
||||||
|
Commands 0x91 and 0x96--0x98 are not used by any driver (Linux or Windows). They appear to be manufacturing/debug interfaces.
|
||||||
|
|
||||||
|
### 0x91 I2C_ADDR_ADJUST
|
||||||
|
|
||||||
|
Increments (wValue != 0) or decrements (wValue == 0) an internal IRAM counter and returns its current value (1 byte). The counter is at IRAM 0x66 (v2.06) or IRAM 0x18 (v2.13/Rev.2). Likely used for I2C bus address or tuner register index adjustment during development.
|
||||||
|
|
||||||
|
### 0x96 SET_LNB_GPIO_MODE
|
||||||
|
|
||||||
|
Configures GPIO output enable registers for LNB voltage regulator hardware:
|
||||||
|
|
||||||
|
| Mode | v2.06/v2.13 | Rev.2 |
|
||||||
|
|------|-------------|-------|
|
||||||
|
| Default (wValue=0) | OEB=0xF0 | OEB=0xE7, OEA=0x9E |
|
||||||
|
| Active (wValue=1) | IOB=(IOB & 0xF7) OR 0x06; OEB=0xFE | IOB.4 clear; P0.6, P0.0 set; OEA OR= 0x41 |
|
||||||
|
|
||||||
|
### 0x97 SET_GPIO_PINS
|
||||||
|
|
||||||
|
Direct GPIO pin write for LNB control:
|
||||||
|
|
||||||
|
| wValue Bit | v2.06/v2.13 Target | Rev.2 Target |
|
||||||
|
|-----------|-------------------|-------------|
|
||||||
|
| bit 1 | IOB.1 | P0.6 (Port A) |
|
||||||
|
| bit 2 | IOB.2 | P0.0 (Port A) |
|
||||||
|
| bit 3 | IOB.3 | IOB.4 (Port B) |
|
||||||
|
|
||||||
|
### 0x98 GET_GPIO_STATUS
|
||||||
|
|
||||||
|
Returns 1 byte (0 or 1) from a single GPIO input pin -- likely an LNB overcurrent or power-good feedback signal:
|
||||||
|
|
||||||
|
| Version | Pin Read |
|
||||||
|
|---------|----------|
|
||||||
|
| v2.06/v2.13 | IOB.0 (Port B bit 0) |
|
||||||
|
| Rev.2 | P0.5 (Port A bit 5) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
### Firmware Analysis
|
||||||
|
|
||||||
|
- Ghidra decompilation/disassembly of three firmware images:
|
||||||
|
- v2.06.04 (Ghidra port 8193) -- extracted from SkyWalker-1 EEPROM
|
||||||
|
- Rev.2 v2.10.04 (Ghidra port 8197) -- extracted from Rev.2 hardware
|
||||||
|
- v2.13.01 FW1 (Ghidra port 8194) -- extracted from Windows updater
|
||||||
|
- Firmware dumps: `/home/rpm/claude/ham/satellite/genpix/skywalker-1/firmware-dump/`
|
||||||
|
|
||||||
|
### Driver Source
|
||||||
|
|
||||||
|
- Linux kernel 6.16.5: `drivers/media/usb/dvb-usb/gp8psk.c`, `gp8psk.h`, `gp8psk-fe.c`, `gp8psk-fe.h`
|
||||||
|
- Linux kernel: `drivers/media/usb/dvb-usb/dvb-usb-firmware.c`
|
||||||
|
- Windows BDA driver: `SkyWalker1_Final_Release/Source/SkyWalker1Control.cpp`
|
||||||
|
- Windows BDA driver: `SkyWalker1_Final_Release/Include/SkyWalker1Control.h`, `SkyWalker1CommonDef.h`
|
||||||
|
|
||||||
|
### Hardware Documentation
|
||||||
|
|
||||||
|
- Cypress CY7C68013A (FX2LP) Technical Reference Manual
|
||||||
|
- Genpix Electronics official site: https://www.genpix-electronics.com/index.php?act=viewDoc&docId=9
|
||||||
|
- Device `dmesg` output from running SkyWalker-1 hardware (v2.06.04 firmware)
|
||||||
|
|
||||||
|
### Phase 1 Analysis Reports
|
||||||
|
|
||||||
|
1. `gp8psk-driver-analysis.md` -- Linux kernel driver analysis
|
||||||
|
2. `firmware-analysis-v206-vs-v213.md` -- Cross-version firmware comparison
|
||||||
|
3. `rev2-deep-analysis.md` -- Rev.2 deep function inventory (107 functions)
|
||||||
|
4. `gpif-streaming-analysis.md` -- GPIF/MPEG-2 streaming path
|
||||||
|
5. `kernel-fw01-analysis.md` -- Kernel firmware format and EEPROM boot
|
||||||
|
6. `vendor-commands-unknown.md` -- Complete vendor command decode (0x8F, 0x91--0x98)
|
||||||
|
7. `tuning-protocol-analysis.md` -- TUNE_8PSK protocol deep dive
|
||||||
834
tools/tune.py
Executable file
834
tools/tune.py
Executable file
@ -0,0 +1,834 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Genpix SkyWalker-1 DVB-S tuning and streaming tool.
|
||||||
|
|
||||||
|
Controls the SkyWalker-1 USB DVB-S satellite receiver via vendor USB
|
||||||
|
control transfers. Supports tuning, LNB control, DiSEqC switching,
|
||||||
|
MPEG-2 transport stream capture, and signal monitoring.
|
||||||
|
|
||||||
|
Hardware: Cypress FX2 (CY7C68013A) + Broadcom BCM4500 demodulator
|
||||||
|
USB: VID 0x09C0, PID 0x0203, EP2 bulk IN for TS data
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import struct
|
||||||
|
import argparse
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import signal
|
||||||
|
import os
|
||||||
|
|
||||||
|
try:
|
||||||
|
import usb.core
|
||||||
|
import usb.util
|
||||||
|
except ImportError:
|
||||||
|
print("pyusb required: pip install pyusb")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
VENDOR_ID = 0x09C0
|
||||||
|
PRODUCT_ID = 0x0203
|
||||||
|
|
||||||
|
# Streaming endpoint
|
||||||
|
EP2_ADDR = 0x82
|
||||||
|
EP2_URB_SIZE = 8192
|
||||||
|
|
||||||
|
# Vendor commands
|
||||||
|
CMD_GET_8PSK_CONFIG = 0x80
|
||||||
|
CMD_I2C_WRITE = 0x83
|
||||||
|
CMD_I2C_READ = 0x84
|
||||||
|
CMD_ARM_TRANSFER = 0x85
|
||||||
|
CMD_TUNE_8PSK = 0x86
|
||||||
|
CMD_GET_SIGNAL_STRENGTH = 0x87
|
||||||
|
CMD_LOAD_BCM4500 = 0x88
|
||||||
|
CMD_BOOT_8PSK = 0x89
|
||||||
|
CMD_START_INTERSIL = 0x8A
|
||||||
|
CMD_SET_LNB_VOLTAGE = 0x8B
|
||||||
|
CMD_SET_22KHZ_TONE = 0x8C
|
||||||
|
CMD_SEND_DISEQC = 0x8D
|
||||||
|
CMD_GET_SIGNAL_LOCK = 0x90
|
||||||
|
CMD_GET_FW_VERS = 0x92
|
||||||
|
CMD_GET_SERIAL_NUMBER = 0x93
|
||||||
|
CMD_USE_EXTRA_VOLT = 0x94
|
||||||
|
|
||||||
|
# Config status bits (GET_8PSK_CONFIG response)
|
||||||
|
CONFIG_BITS = {
|
||||||
|
0x01: ("8PSK Started", "bm8pskStarted"),
|
||||||
|
0x02: ("BCM4500 FW Loaded", "bm8pskFW_Loaded"),
|
||||||
|
0x04: ("LNB Power On", "bmIntersilOn"),
|
||||||
|
0x08: ("DVB Mode", "bmDVBmode"),
|
||||||
|
0x10: ("22 kHz Tone", "bm22kHz"),
|
||||||
|
0x20: ("18V Selected", "bmSEL18V"),
|
||||||
|
0x40: ("DC Tuned", "bmDCtuned"),
|
||||||
|
0x80: ("Armed (streaming)", "bmArmed"),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Modulation types for TUNE_8PSK byte 8
|
||||||
|
MODULATIONS = {
|
||||||
|
"qpsk": (0, "DVB-S QPSK"),
|
||||||
|
"turbo-qpsk": (1, "Turbo QPSK"),
|
||||||
|
"turbo-8psk": (2, "Turbo 8PSK"),
|
||||||
|
"turbo-16qam": (3, "Turbo 16QAM"),
|
||||||
|
"dcii-combo": (4, "DCII Combo"),
|
||||||
|
"dcii-i": (5, "DCII I-stream"),
|
||||||
|
"dcii-q": (6, "DCII Q-stream"),
|
||||||
|
"dcii-oqpsk": (7, "DCII Offset QPSK"),
|
||||||
|
"dss": (8, "DSS QPSK"),
|
||||||
|
"bpsk": (9, "DVB BPSK"),
|
||||||
|
}
|
||||||
|
|
||||||
|
# FEC rate indices per modulation group
|
||||||
|
FEC_RATES = {
|
||||||
|
"dvbs": {
|
||||||
|
"1/2": 0, "2/3": 1, "3/4": 2, "5/6": 3,
|
||||||
|
"7/8": 4, "auto": 5, "none": 6,
|
||||||
|
},
|
||||||
|
"turbo": {
|
||||||
|
"1/2": 0, "2/3": 1, "3/4": 2, "5/6": 3, "auto": 4,
|
||||||
|
},
|
||||||
|
"turbo-16qam": {
|
||||||
|
"3/4": 0, "auto": 0,
|
||||||
|
},
|
||||||
|
"dcii": {
|
||||||
|
"1/2": 0, "2/3": 1, "6/7": 2, "3/4": 3, "5/11": 4,
|
||||||
|
"1/2+": 5, "2/3+": 6, "6/7+": 7, "3/4+": 8, "auto": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Map modulation names to FEC group
|
||||||
|
MOD_FEC_GROUP = {
|
||||||
|
"qpsk": "dvbs",
|
||||||
|
"turbo-qpsk": "turbo",
|
||||||
|
"turbo-8psk": "turbo",
|
||||||
|
"turbo-16qam": "turbo-16qam",
|
||||||
|
"dcii-combo": "dcii",
|
||||||
|
"dcii-i": "dcii",
|
||||||
|
"dcii-q": "dcii",
|
||||||
|
"dcii-oqpsk": "dcii",
|
||||||
|
"dss": "dvbs",
|
||||||
|
"bpsk": "dvbs",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Default LNB LO frequencies (MHz)
|
||||||
|
LNB_LO_LOW = 9750 # Universal LNB low-band
|
||||||
|
LNB_LO_HIGH = 10600 # Universal LNB high-band
|
||||||
|
|
||||||
|
|
||||||
|
class SkyWalker1:
|
||||||
|
"""USB interface to the Genpix SkyWalker-1 DVB-S receiver."""
|
||||||
|
|
||||||
|
def __init__(self, verbose: bool = False):
|
||||||
|
self.dev = None
|
||||||
|
self.detached_intf = None
|
||||||
|
self.verbose = verbose
|
||||||
|
|
||||||
|
def open(self) -> None:
|
||||||
|
"""Find and claim the SkyWalker-1 USB device."""
|
||||||
|
self.dev = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID)
|
||||||
|
if self.dev is None:
|
||||||
|
print("SkyWalker-1 not found (VID 0x09C0, PID 0x0203). Is it plugged in?")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Detach kernel driver if bound
|
||||||
|
for cfg in self.dev:
|
||||||
|
for intf in cfg:
|
||||||
|
if self.dev.is_kernel_driver_active(intf.bInterfaceNumber):
|
||||||
|
try:
|
||||||
|
self.dev.detach_kernel_driver(intf.bInterfaceNumber)
|
||||||
|
self.detached_intf = intf.bInterfaceNumber
|
||||||
|
if self.verbose:
|
||||||
|
print(f" Detached kernel driver from interface {intf.bInterfaceNumber}")
|
||||||
|
except usb.core.USBError as e:
|
||||||
|
print(f"Cannot detach kernel driver: {e}")
|
||||||
|
print("The gp8psk module must be unbound first. Try one of:")
|
||||||
|
print(" sudo modprobe -r dvb_usb_gp8psk")
|
||||||
|
print(" echo '<bus-path>' | sudo tee /sys/bus/usb/drivers/gp8psk/unbind")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.dev.set_configuration()
|
||||||
|
except usb.core.USBError:
|
||||||
|
pass # May already be configured
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""Release device and re-attach kernel driver."""
|
||||||
|
if self.dev is None:
|
||||||
|
return
|
||||||
|
if self.detached_intf is not None:
|
||||||
|
try:
|
||||||
|
usb.util.release_interface(self.dev, self.detached_intf)
|
||||||
|
self.dev.attach_kernel_driver(self.detached_intf)
|
||||||
|
if self.verbose:
|
||||||
|
print("Re-attached kernel driver")
|
||||||
|
except usb.core.USBError:
|
||||||
|
print("Note: run 'sudo modprobe dvb_usb_gp8psk' to reload driver")
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.open()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *exc):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
# -- Low-level USB transfers --
|
||||||
|
|
||||||
|
def _vendor_in(self, request: int, value: int = 0, index: int = 0,
|
||||||
|
length: int = 64, retries: int = 3) -> bytes:
|
||||||
|
"""Vendor IN control transfer (device-to-host), with retry."""
|
||||||
|
for attempt in range(retries):
|
||||||
|
try:
|
||||||
|
data = self.dev.ctrl_transfer(
|
||||||
|
usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN,
|
||||||
|
request, value, index, length, 2000
|
||||||
|
)
|
||||||
|
if self.verbose:
|
||||||
|
raw = bytes(data).hex(' ')
|
||||||
|
print(f" USB IN req=0x{request:02X} val=0x{value:04X} "
|
||||||
|
f"idx=0x{index:04X} -> [{len(data)}] {raw}")
|
||||||
|
if len(data) == length:
|
||||||
|
return bytes(data)
|
||||||
|
# Partial read, retry
|
||||||
|
if self.verbose:
|
||||||
|
print(f" Partial read ({len(data)}/{length}), retry {attempt + 1}")
|
||||||
|
continue
|
||||||
|
except usb.core.USBError as e:
|
||||||
|
if self.verbose:
|
||||||
|
print(f" USB IN req=0x{request:02X} FAILED: {e}")
|
||||||
|
if attempt == retries - 1:
|
||||||
|
raise
|
||||||
|
return bytes(data)
|
||||||
|
|
||||||
|
def _vendor_out(self, request: int, value: int = 0, index: int = 0,
|
||||||
|
data: bytes = b'') -> int:
|
||||||
|
"""Vendor OUT control transfer (host-to-device)."""
|
||||||
|
if self.verbose:
|
||||||
|
raw = data.hex(' ') if data else "(no data)"
|
||||||
|
print(f" USB OUT req=0x{request:02X} val=0x{value:04X} "
|
||||||
|
f"idx=0x{index:04X} data=[{len(data)}] {raw}")
|
||||||
|
return self.dev.ctrl_transfer(
|
||||||
|
usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_OUT,
|
||||||
|
request, value, index, data, 2000
|
||||||
|
)
|
||||||
|
|
||||||
|
# -- Device info commands --
|
||||||
|
|
||||||
|
def get_config(self) -> int:
|
||||||
|
"""Read 8PSK config status byte (GET_8PSK_CONFIG 0x80)."""
|
||||||
|
data = self._vendor_in(CMD_GET_8PSK_CONFIG, length=1)
|
||||||
|
return data[0]
|
||||||
|
|
||||||
|
def get_fw_version(self) -> dict:
|
||||||
|
"""Read firmware version (GET_FW_VERS 0x92). Returns dict."""
|
||||||
|
data = self._vendor_in(CMD_GET_FW_VERS, length=6)
|
||||||
|
return {
|
||||||
|
"major": data[2],
|
||||||
|
"minor": data[1],
|
||||||
|
"patch": data[0],
|
||||||
|
"version": f"{data[2]}.{data[1]:02d}.{data[0]}",
|
||||||
|
"date": f"20{data[5]:02d}-{data[4]:02d}-{data[3]:02d}",
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_signal_lock(self) -> bool:
|
||||||
|
"""Read signal lock status (GET_SIGNAL_LOCK 0x90)."""
|
||||||
|
data = self._vendor_in(CMD_GET_SIGNAL_LOCK, length=1)
|
||||||
|
return data[0] != 0
|
||||||
|
|
||||||
|
def get_signal_strength(self) -> dict:
|
||||||
|
"""Read signal strength (GET_SIGNAL_STRENGTH 0x87). Returns SNR info."""
|
||||||
|
data = self._vendor_in(CMD_GET_SIGNAL_STRENGTH, length=6)
|
||||||
|
snr_raw = struct.unpack_from('<H', data, 0)[0]
|
||||||
|
# SNR is in dBu * 256 units. Scale: snr * 17 maps to 0-65535.
|
||||||
|
snr_scaled = min(snr_raw * 17, 65535)
|
||||||
|
snr_pct = (snr_scaled / 65535) * 100
|
||||||
|
snr_db = snr_raw / 256.0
|
||||||
|
return {
|
||||||
|
"snr_raw": snr_raw,
|
||||||
|
"snr_db": snr_db,
|
||||||
|
"snr_pct": snr_pct,
|
||||||
|
"raw_bytes": bytes(data).hex(' '),
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- Power and boot commands --
|
||||||
|
|
||||||
|
def boot(self, on: bool = True) -> int:
|
||||||
|
"""Power on/off the 8PSK demodulator (BOOT_8PSK 0x89)."""
|
||||||
|
data = self._vendor_in(CMD_BOOT_8PSK, value=int(on), length=1)
|
||||||
|
return data[0]
|
||||||
|
|
||||||
|
def start_intersil(self, on: bool = True) -> int:
|
||||||
|
"""Enable/disable LNB power supply (START_INTERSIL 0x8A)."""
|
||||||
|
data = self._vendor_in(CMD_START_INTERSIL, value=int(on), length=1)
|
||||||
|
return data[0]
|
||||||
|
|
||||||
|
def set_lnb_voltage(self, high: bool) -> None:
|
||||||
|
"""Set LNB voltage: high=True for 18V (H/L), high=False for 13V (V/R)."""
|
||||||
|
self._vendor_out(CMD_SET_LNB_VOLTAGE, value=int(high))
|
||||||
|
|
||||||
|
def set_22khz_tone(self, on: bool) -> None:
|
||||||
|
"""Enable/disable 22 kHz tone (SET_22KHZ_TONE 0x8C)."""
|
||||||
|
self._vendor_out(CMD_SET_22KHZ_TONE, value=int(on))
|
||||||
|
|
||||||
|
def set_extra_voltage(self, on: bool) -> None:
|
||||||
|
"""Enable +1V LNB boost: 13->14V, 18->19V (USE_EXTRA_VOLT 0x94)."""
|
||||||
|
self._vendor_out(CMD_USE_EXTRA_VOLT, value=int(on))
|
||||||
|
|
||||||
|
# -- Tuning --
|
||||||
|
|
||||||
|
def tune(self, symbol_rate_sps: int, freq_khz: int,
|
||||||
|
mod_index: int, fec_index: int) -> None:
|
||||||
|
"""Send TUNE_8PSK (0x86) with 10-byte payload."""
|
||||||
|
payload = struct.pack('<II', symbol_rate_sps, freq_khz)
|
||||||
|
payload += bytes([mod_index, fec_index])
|
||||||
|
self._vendor_out(CMD_TUNE_8PSK, data=payload)
|
||||||
|
|
||||||
|
# -- Streaming --
|
||||||
|
|
||||||
|
def arm_transfer(self, on: bool) -> None:
|
||||||
|
"""Start/stop MPEG-2 transport stream (ARM_TRANSFER 0x85)."""
|
||||||
|
self._vendor_out(CMD_ARM_TRANSFER, value=int(on))
|
||||||
|
|
||||||
|
def read_stream(self, size: int = EP2_URB_SIZE,
|
||||||
|
timeout: int = 1000) -> bytes:
|
||||||
|
"""Read a chunk from the TS bulk endpoint (EP2 0x82)."""
|
||||||
|
try:
|
||||||
|
data = self.dev.read(EP2_ADDR, size, timeout)
|
||||||
|
return bytes(data)
|
||||||
|
except usb.core.USBTimeoutError:
|
||||||
|
return b''
|
||||||
|
except usb.core.USBError as e:
|
||||||
|
if self.verbose:
|
||||||
|
print(f" EP2 read error: {e}")
|
||||||
|
return b''
|
||||||
|
|
||||||
|
# -- DiSEqC --
|
||||||
|
|
||||||
|
def send_diseqc_tone_burst(self, mini_cmd: int) -> None:
|
||||||
|
"""Send tone burst (mini-DiSEqC). 0=SEC_MINI_A, 1=SEC_MINI_B."""
|
||||||
|
self._vendor_out(CMD_SEND_DISEQC, value=mini_cmd)
|
||||||
|
|
||||||
|
def send_diseqc_message(self, msg: bytes) -> None:
|
||||||
|
"""Send full DiSEqC message (3-6 bytes). wValue = framing byte."""
|
||||||
|
if len(msg) < 3 or len(msg) > 6:
|
||||||
|
raise ValueError(f"DiSEqC message must be 3-6 bytes, got {len(msg)}")
|
||||||
|
self._vendor_out(CMD_SEND_DISEQC, value=msg[0], data=msg)
|
||||||
|
|
||||||
|
|
||||||
|
# -- Signal bar rendering --
|
||||||
|
|
||||||
|
def signal_bar(pct: float, width: int = 40) -> str:
|
||||||
|
"""Render a signal strength bar."""
|
||||||
|
filled = int(pct / 100 * width)
|
||||||
|
filled = max(0, min(filled, width))
|
||||||
|
bar = '#' * filled + '-' * (width - filled)
|
||||||
|
return f"[{bar}] {pct:.1f}%"
|
||||||
|
|
||||||
|
|
||||||
|
def format_config_bits(status: int) -> list:
|
||||||
|
"""Return list of (bit_name, is_set) tuples for config byte."""
|
||||||
|
result = []
|
||||||
|
for bit, (name, _field) in CONFIG_BITS.items():
|
||||||
|
result.append((name, bool(status & bit)))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# -- Subcommand handlers --
|
||||||
|
|
||||||
|
def cmd_status(sw: SkyWalker1, args: argparse.Namespace) -> None:
|
||||||
|
"""Show device status, firmware version, signal info."""
|
||||||
|
print(f"Genpix SkyWalker-1 Status")
|
||||||
|
print(f"{'=' * 50}")
|
||||||
|
|
||||||
|
print(f"\nUSB: Bus {sw.dev.bus} Addr {sw.dev.address} "
|
||||||
|
f"(VID 0x{VENDOR_ID:04X}, PID 0x{PRODUCT_ID:04X})")
|
||||||
|
|
||||||
|
# Firmware version
|
||||||
|
try:
|
||||||
|
fw = sw.get_fw_version()
|
||||||
|
print(f"FW: {fw['version']} (built {fw['date']})")
|
||||||
|
except usb.core.USBError:
|
||||||
|
print("FW: (read failed)")
|
||||||
|
fw = None
|
||||||
|
|
||||||
|
# Config status
|
||||||
|
status = sw.get_config()
|
||||||
|
print(f"\nConfig: 0x{status:02X}")
|
||||||
|
bits = format_config_bits(status)
|
||||||
|
for name, is_set in bits:
|
||||||
|
state = "ON" if is_set else "off"
|
||||||
|
print(f" [{state:>3}] {name}")
|
||||||
|
|
||||||
|
# Signal lock and strength
|
||||||
|
locked = sw.get_signal_lock()
|
||||||
|
print(f"\nSignal Lock: {'LOCKED' if locked else 'no lock'}")
|
||||||
|
if locked:
|
||||||
|
sig = sw.get_signal_strength()
|
||||||
|
print(f"SNR: {sig['snr_db']:.1f} dB (raw 0x{sig['snr_raw']:04X})")
|
||||||
|
print(f"Quality: {signal_bar(sig['snr_pct'])}")
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
out = {
|
||||||
|
"usb": {"bus": sw.dev.bus, "address": sw.dev.address},
|
||||||
|
"config": status,
|
||||||
|
"config_bits": {field: bool(status & bit)
|
||||||
|
for bit, (_name, field) in CONFIG_BITS.items()},
|
||||||
|
"locked": locked,
|
||||||
|
}
|
||||||
|
if fw:
|
||||||
|
out["firmware"] = fw
|
||||||
|
if locked:
|
||||||
|
out["signal"] = sw.get_signal_strength()
|
||||||
|
print(f"\n{json.dumps(out, indent=2)}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_tune(sw: SkyWalker1, args: argparse.Namespace) -> None:
|
||||||
|
"""Tune to a transponder."""
|
||||||
|
freq_mhz = args.freq
|
||||||
|
sr_ksps = args.sr
|
||||||
|
mod_name = args.mod
|
||||||
|
fec_name = args.fec
|
||||||
|
pol = args.pol.upper() if args.pol else None
|
||||||
|
band = args.band
|
||||||
|
|
||||||
|
# Resolve LNB LO
|
||||||
|
if args.lnb_lo:
|
||||||
|
lnb_lo = args.lnb_lo
|
||||||
|
elif band == "high":
|
||||||
|
lnb_lo = LNB_LO_HIGH
|
||||||
|
else:
|
||||||
|
lnb_lo = LNB_LO_LOW
|
||||||
|
|
||||||
|
# Compute IF frequency
|
||||||
|
if_mhz = freq_mhz - lnb_lo
|
||||||
|
if_khz = int(if_mhz * 1000)
|
||||||
|
|
||||||
|
if if_khz < 950000 or if_khz > 2150000:
|
||||||
|
print(f"WARNING: IF frequency {if_mhz} MHz is outside 950-2150 MHz range")
|
||||||
|
print(f" Downlink: {freq_mhz} MHz, LNB LO: {lnb_lo} MHz")
|
||||||
|
if if_khz < 0:
|
||||||
|
print(" IF is negative -- check your LNB LO frequency")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Resolve modulation
|
||||||
|
if mod_name not in MODULATIONS:
|
||||||
|
print(f"Unknown modulation: {mod_name}")
|
||||||
|
print(f"Valid: {', '.join(MODULATIONS.keys())}")
|
||||||
|
sys.exit(1)
|
||||||
|
mod_index, mod_desc = MODULATIONS[mod_name]
|
||||||
|
|
||||||
|
# Resolve FEC
|
||||||
|
fec_group = MOD_FEC_GROUP[mod_name]
|
||||||
|
fec_table = FEC_RATES[fec_group]
|
||||||
|
if fec_name not in fec_table:
|
||||||
|
print(f"Invalid FEC '{fec_name}' for {mod_desc}")
|
||||||
|
print(f"Valid: {', '.join(fec_table.keys())}")
|
||||||
|
sys.exit(1)
|
||||||
|
fec_index = fec_table[fec_name]
|
||||||
|
|
||||||
|
sr_sps = sr_ksps * 1000
|
||||||
|
|
||||||
|
print(f"Tuning SkyWalker-1")
|
||||||
|
print(f"{'=' * 50}")
|
||||||
|
print(f" Downlink: {freq_mhz} MHz")
|
||||||
|
print(f" LNB LO: {lnb_lo} MHz")
|
||||||
|
print(f" IF Frequency: {if_mhz} MHz ({if_khz} kHz)")
|
||||||
|
print(f" Symbol Rate: {sr_ksps} ksps ({sr_sps} sps)")
|
||||||
|
print(f" Modulation: {mod_desc} (index {mod_index})")
|
||||||
|
print(f" FEC: {fec_name} (index {fec_index})")
|
||||||
|
if pol:
|
||||||
|
pol_desc = {"H": "Horizontal (18V)", "V": "Vertical (13V)",
|
||||||
|
"L": "Left circular (18V)", "R": "Right circular (13V)"}
|
||||||
|
print(f" Polarization: {pol_desc.get(pol, pol)}")
|
||||||
|
if band:
|
||||||
|
print(f" Band: {band} ({'22kHz on' if band == 'high' else '22kHz off'})")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Step 1: Check device status
|
||||||
|
status = sw.get_config()
|
||||||
|
print(f"[1/8] Config status: 0x{status:02X}")
|
||||||
|
|
||||||
|
# Step 2: Boot demodulator if needed
|
||||||
|
if not (status & 0x01):
|
||||||
|
print("[2/8] Booting 8PSK demodulator...")
|
||||||
|
sw.boot(on=True)
|
||||||
|
time.sleep(0.5)
|
||||||
|
status = sw.get_config()
|
||||||
|
if not (status & 0x01):
|
||||||
|
print(" FAILED: Device did not start")
|
||||||
|
sys.exit(1)
|
||||||
|
print(" OK")
|
||||||
|
else:
|
||||||
|
print("[2/8] Demodulator already running")
|
||||||
|
|
||||||
|
# Step 3: Enable LNB power if needed
|
||||||
|
if not (status & 0x04):
|
||||||
|
print("[3/8] Enabling LNB power supply...")
|
||||||
|
sw.start_intersil(on=True)
|
||||||
|
time.sleep(0.3)
|
||||||
|
status = sw.get_config()
|
||||||
|
if not (status & 0x04):
|
||||||
|
print(" FAILED: LNB power did not enable")
|
||||||
|
sys.exit(1)
|
||||||
|
print(" OK")
|
||||||
|
else:
|
||||||
|
print("[3/8] LNB power already on")
|
||||||
|
|
||||||
|
# Step 4: Set LNB voltage (polarization)
|
||||||
|
if pol:
|
||||||
|
high_voltage = pol in ("H", "L")
|
||||||
|
print(f"[4/8] Setting LNB voltage: {'18V' if high_voltage else '13V'}")
|
||||||
|
sw.set_lnb_voltage(high_voltage)
|
||||||
|
else:
|
||||||
|
print("[4/8] LNB voltage: not changed (no --pol specified)")
|
||||||
|
|
||||||
|
# Step 5: Extra voltage if requested
|
||||||
|
if args.extra_volt:
|
||||||
|
print("[5/8] Enabling +1V LNB boost")
|
||||||
|
sw.set_extra_voltage(True)
|
||||||
|
else:
|
||||||
|
print("[5/8] Extra voltage: off")
|
||||||
|
|
||||||
|
# Step 6: Set 22 kHz tone (band selection)
|
||||||
|
if band:
|
||||||
|
tone_on = (band == "high")
|
||||||
|
print(f"[6/8] 22 kHz tone: {'ON' if tone_on else 'OFF'}")
|
||||||
|
sw.set_22khz_tone(tone_on)
|
||||||
|
else:
|
||||||
|
print("[6/8] 22 kHz tone: not changed (no --band specified)")
|
||||||
|
|
||||||
|
# Step 7: Send tune command
|
||||||
|
print(f"[7/8] Sending TUNE_8PSK...")
|
||||||
|
if sw.verbose:
|
||||||
|
payload_hex = struct.pack('<II', sr_sps, if_khz).hex(' ')
|
||||||
|
print(f" Payload: {payload_hex} {mod_index:02x} {fec_index:02x}")
|
||||||
|
sw.tune(sr_sps, if_khz, mod_index, fec_index)
|
||||||
|
|
||||||
|
# Step 8: Wait for lock
|
||||||
|
timeout = args.timeout
|
||||||
|
print(f"[8/8] Waiting for signal lock (timeout {timeout}s)...")
|
||||||
|
deadline = time.time() + timeout
|
||||||
|
locked = False
|
||||||
|
dots = 0
|
||||||
|
while time.time() < deadline:
|
||||||
|
if sw.get_signal_lock():
|
||||||
|
locked = True
|
||||||
|
break
|
||||||
|
print(".", end="", flush=True)
|
||||||
|
dots += 1
|
||||||
|
time.sleep(0.5)
|
||||||
|
if dots:
|
||||||
|
print()
|
||||||
|
|
||||||
|
if locked:
|
||||||
|
sig = sw.get_signal_strength()
|
||||||
|
print(f"\n LOCKED")
|
||||||
|
print(f" SNR: {sig['snr_db']:.1f} dB (raw 0x{sig['snr_raw']:04X})")
|
||||||
|
print(f" Quality: {signal_bar(sig['snr_pct'])}")
|
||||||
|
else:
|
||||||
|
print(f"\n NO LOCK after {timeout}s")
|
||||||
|
print(" Check frequency, symbol rate, polarization, and dish alignment")
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
out = {
|
||||||
|
"tuned": True,
|
||||||
|
"locked": locked,
|
||||||
|
"freq_mhz": freq_mhz,
|
||||||
|
"if_khz": if_khz,
|
||||||
|
"sr_ksps": sr_ksps,
|
||||||
|
"modulation": mod_name,
|
||||||
|
"fec": fec_name,
|
||||||
|
}
|
||||||
|
if locked:
|
||||||
|
out["signal"] = sw.get_signal_strength()
|
||||||
|
print(f"\n{json.dumps(out, indent=2)}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_stream(sw: SkyWalker1, args: argparse.Namespace) -> None:
|
||||||
|
"""Stream MPEG-2 transport data to file or stdout."""
|
||||||
|
# Verify signal lock
|
||||||
|
if not sw.get_signal_lock():
|
||||||
|
print("No signal lock -- tune to a transponder first")
|
||||||
|
print(" Example: tune.py tune 12520 27500 --pol H --band high")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
output_file = None
|
||||||
|
output_fd = None
|
||||||
|
|
||||||
|
if args.stdout:
|
||||||
|
output_fd = sys.stdout.buffer
|
||||||
|
# Suppress all status output when piping
|
||||||
|
status_fd = sys.stderr
|
||||||
|
elif args.output:
|
||||||
|
output_file = args.output
|
||||||
|
output_fd = open(output_file, 'wb')
|
||||||
|
status_fd = sys.stdout
|
||||||
|
else:
|
||||||
|
print("Specify -o FILE or --stdout")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
duration = args.duration
|
||||||
|
total_bytes = 0
|
||||||
|
start_time = time.time()
|
||||||
|
last_report = start_time
|
||||||
|
running = True
|
||||||
|
|
||||||
|
def stop_handler(signum, frame):
|
||||||
|
nonlocal running
|
||||||
|
running = False
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, stop_handler)
|
||||||
|
signal.signal(signal.SIGTERM, stop_handler)
|
||||||
|
|
||||||
|
status_fd.write(f"Streaming TS data")
|
||||||
|
if output_file:
|
||||||
|
status_fd.write(f" to {output_file}")
|
||||||
|
if duration:
|
||||||
|
status_fd.write(f" for {duration}s")
|
||||||
|
status_fd.write("\n")
|
||||||
|
status_fd.flush()
|
||||||
|
|
||||||
|
# Arm the transfer
|
||||||
|
sw.arm_transfer(on=True)
|
||||||
|
status_fd.write(" Armed. Reading EP2...\n")
|
||||||
|
status_fd.flush()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while running:
|
||||||
|
if duration and (time.time() - start_time) >= duration:
|
||||||
|
break
|
||||||
|
|
||||||
|
chunk = sw.read_stream(EP2_URB_SIZE, timeout=2000)
|
||||||
|
if chunk:
|
||||||
|
output_fd.write(chunk)
|
||||||
|
total_bytes += len(chunk)
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
if now - last_report >= 1.0:
|
||||||
|
elapsed = now - start_time
|
||||||
|
bitrate = (total_bytes * 8) / elapsed if elapsed > 0 else 0
|
||||||
|
if bitrate >= 1e6:
|
||||||
|
rate_str = f"{bitrate / 1e6:.2f} Mbps"
|
||||||
|
else:
|
||||||
|
rate_str = f"{bitrate / 1e3:.1f} kbps"
|
||||||
|
status_fd.write(f"\r {total_bytes:,} bytes {rate_str} "
|
||||||
|
f"({elapsed:.0f}s) ")
|
||||||
|
status_fd.flush()
|
||||||
|
last_report = now
|
||||||
|
|
||||||
|
finally:
|
||||||
|
sw.arm_transfer(on=False)
|
||||||
|
status_fd.write(f"\n Stopped. Total: {total_bytes:,} bytes\n")
|
||||||
|
if output_file and output_fd:
|
||||||
|
output_fd.close()
|
||||||
|
status_fd.write(f" Saved to: {output_file}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_diseqc(sw: SkyWalker1, args: argparse.Namespace) -> None:
|
||||||
|
"""Send DiSEqC commands."""
|
||||||
|
if args.tone_burst is not None:
|
||||||
|
burst_val = {"A": 0, "a": 0, "B": 1, "b": 1}.get(args.tone_burst)
|
||||||
|
if burst_val is None:
|
||||||
|
print("Tone burst must be A or B")
|
||||||
|
sys.exit(1)
|
||||||
|
print(f"Sending tone burst: SEC_MINI_{args.tone_burst.upper()} (0x{burst_val:02X})")
|
||||||
|
sw.send_diseqc_tone_burst(burst_val)
|
||||||
|
print(" OK")
|
||||||
|
|
||||||
|
elif args.port is not None:
|
||||||
|
port = args.port
|
||||||
|
if port < 1 or port > 4:
|
||||||
|
print("DiSEqC 1.0 port must be 1-4")
|
||||||
|
sys.exit(1)
|
||||||
|
# DiSEqC 1.0 committed switch command:
|
||||||
|
# Framing=0xE0 (command from master, no reply, first tx)
|
||||||
|
# Address=0x10 (any switch)
|
||||||
|
# Command=0x38 (Write N0 - committed switches)
|
||||||
|
# Data=0xF0 | ((port-1) << 2) with option/position bits
|
||||||
|
# Bits: [7:4]=0xF (always), [3]=pol, [2]=band, [1:0]=port
|
||||||
|
# For simplicity, just switch port without changing pol/band bits
|
||||||
|
data_byte = 0xF0 | ((port - 1) << 2)
|
||||||
|
msg = bytes([0xE0, 0x10, 0x38, data_byte])
|
||||||
|
print(f"Sending DiSEqC 1.0: port {port}")
|
||||||
|
print(f" Message: {msg.hex(' ')}")
|
||||||
|
sw.send_diseqc_message(msg)
|
||||||
|
print(" OK")
|
||||||
|
|
||||||
|
elif args.raw:
|
||||||
|
raw_bytes = bytes(int(b, 16) for b in args.raw)
|
||||||
|
if len(raw_bytes) < 3 or len(raw_bytes) > 6:
|
||||||
|
print("Raw DiSEqC message must be 3-6 bytes")
|
||||||
|
sys.exit(1)
|
||||||
|
print(f"Sending raw DiSEqC: {raw_bytes.hex(' ')}")
|
||||||
|
sw.send_diseqc_message(raw_bytes)
|
||||||
|
print(" OK")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("Specify --port, --tone-burst, or --raw")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_lnb(sw: SkyWalker1, args: argparse.Namespace) -> None:
|
||||||
|
"""Control LNB voltage and 22 kHz tone."""
|
||||||
|
did_something = False
|
||||||
|
|
||||||
|
# Ensure LNB power is on first
|
||||||
|
status = sw.get_config()
|
||||||
|
if not (status & 0x04):
|
||||||
|
print("Enabling LNB power supply...")
|
||||||
|
sw.start_intersil(on=True)
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
if args.voltage is not None:
|
||||||
|
v = args.voltage
|
||||||
|
if v not in (13, 18):
|
||||||
|
print("Voltage must be 13 or 18")
|
||||||
|
sys.exit(1)
|
||||||
|
high = (v == 18)
|
||||||
|
print(f"Setting LNB voltage: {v}V")
|
||||||
|
sw.set_lnb_voltage(high)
|
||||||
|
did_something = True
|
||||||
|
|
||||||
|
if args.extra_volt:
|
||||||
|
print("Enabling +1V LNB boost")
|
||||||
|
sw.set_extra_voltage(True)
|
||||||
|
did_something = True
|
||||||
|
|
||||||
|
if args.tone is not None:
|
||||||
|
tone_on = args.tone.lower() in ("on", "1", "true", "yes")
|
||||||
|
print(f"22 kHz tone: {'ON' if tone_on else 'OFF'}")
|
||||||
|
sw.set_22khz_tone(tone_on)
|
||||||
|
did_something = True
|
||||||
|
|
||||||
|
if args.power is not None:
|
||||||
|
power_on = args.power.lower() in ("on", "1", "true", "yes")
|
||||||
|
if power_on:
|
||||||
|
print("Enabling LNB power supply")
|
||||||
|
sw.start_intersil(on=True)
|
||||||
|
else:
|
||||||
|
print("Disabling LNB power supply")
|
||||||
|
sw.start_intersil(on=False)
|
||||||
|
did_something = True
|
||||||
|
|
||||||
|
if not did_something:
|
||||||
|
# Just show current LNB state
|
||||||
|
status = sw.get_config()
|
||||||
|
print(f"LNB Status:")
|
||||||
|
print(f" Power: {'ON' if status & 0x04 else 'off'}")
|
||||||
|
print(f" Voltage: {'18V' if status & 0x20 else '13V'}")
|
||||||
|
print(f" 22 kHz: {'ON' if status & 0x10 else 'off'}")
|
||||||
|
print(f" Armed: {'YES' if status & 0x80 else 'no'}")
|
||||||
|
else:
|
||||||
|
# Read back config to confirm
|
||||||
|
time.sleep(0.1)
|
||||||
|
status = sw.get_config()
|
||||||
|
print(f"\nConfig: 0x{status:02X}")
|
||||||
|
print(f" Power: {'ON' if status & 0x04 else 'off'} "
|
||||||
|
f"Voltage: {'18V' if status & 0x20 else '13V'} "
|
||||||
|
f"22kHz: {'ON' if status & 0x10 else 'off'}")
|
||||||
|
|
||||||
|
|
||||||
|
# -- CLI --
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Genpix SkyWalker-1 DVB-S tuning and streaming tool",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""\
|
||||||
|
examples:
|
||||||
|
%(prog)s status
|
||||||
|
%(prog)s tune 12520 27500 --pol H --band high
|
||||||
|
%(prog)s tune 12520 27500 --pol H --band high --mod qpsk --fec auto
|
||||||
|
%(prog)s stream -o capture.ts --duration 60
|
||||||
|
%(prog)s stream --stdout | vlc -
|
||||||
|
%(prog)s diseqc --port 1
|
||||||
|
%(prog)s diseqc --tone-burst A
|
||||||
|
%(prog)s diseqc --raw E0 10 38 F0
|
||||||
|
%(prog)s lnb --voltage 18 --tone on
|
||||||
|
""")
|
||||||
|
parser.add_argument('-v', '--verbose', action='store_true',
|
||||||
|
help="Show raw USB traffic")
|
||||||
|
parser.add_argument('--json', action='store_true',
|
||||||
|
help="Output machine-readable JSON where supported")
|
||||||
|
|
||||||
|
sub = parser.add_subparsers(dest='command')
|
||||||
|
|
||||||
|
# status
|
||||||
|
sub.add_parser('status', help="Show device config, FW version, signal status")
|
||||||
|
|
||||||
|
# tune
|
||||||
|
p_tune = sub.add_parser('tune', help="Tune to a transponder")
|
||||||
|
p_tune.add_argument('freq', type=float,
|
||||||
|
help="Transponder downlink frequency in MHz (e.g. 12520)")
|
||||||
|
p_tune.add_argument('sr', type=int,
|
||||||
|
help="Symbol rate in ksps (e.g. 27500)")
|
||||||
|
p_tune.add_argument('--pol', choices=['H', 'V', 'L', 'R', 'h', 'v', 'l', 'r'],
|
||||||
|
help="Polarization: H/V (linear) or L/R (circular)")
|
||||||
|
p_tune.add_argument('--band', choices=['low', 'high'],
|
||||||
|
help="LNB band: low (tone off) or high (tone on)")
|
||||||
|
p_tune.add_argument('--lnb-lo', type=float, default=None,
|
||||||
|
help="LNB LO frequency in MHz (default: 9750 low, 10600 high)")
|
||||||
|
p_tune.add_argument('--mod', default='qpsk',
|
||||||
|
choices=list(MODULATIONS.keys()),
|
||||||
|
help="Modulation type (default: qpsk)")
|
||||||
|
p_tune.add_argument('--fec', default='auto',
|
||||||
|
help="FEC rate (default: auto). Options depend on modulation.")
|
||||||
|
p_tune.add_argument('--timeout', type=float, default=10,
|
||||||
|
help="Signal lock timeout in seconds (default: 10)")
|
||||||
|
p_tune.add_argument('--extra-volt', action='store_true',
|
||||||
|
help="Enable +1V LNB voltage boost for long cables")
|
||||||
|
|
||||||
|
# stream
|
||||||
|
p_stream = sub.add_parser('stream', help="Stream MPEG-2 TS data")
|
||||||
|
p_stream.add_argument('-o', '--output', help="Output file for TS data")
|
||||||
|
p_stream.add_argument('--stdout', action='store_true',
|
||||||
|
help="Write TS stream to stdout (pipe to vlc, ffmpeg, etc)")
|
||||||
|
p_stream.add_argument('--duration', type=float, default=None,
|
||||||
|
help="Capture duration in seconds (default: until CTRL-C)")
|
||||||
|
|
||||||
|
# diseqc
|
||||||
|
p_diseqc = sub.add_parser('diseqc', help="Send DiSEqC commands")
|
||||||
|
p_diseqc.add_argument('--port', type=int,
|
||||||
|
help="DiSEqC 1.0 switch port (1-4)")
|
||||||
|
p_diseqc.add_argument('--tone-burst', metavar='A|B',
|
||||||
|
help="Mini DiSEqC tone burst (A or B)")
|
||||||
|
p_diseqc.add_argument('--raw', nargs='+', metavar='HH',
|
||||||
|
help="Raw DiSEqC bytes in hex (e.g. E0 10 38 F0)")
|
||||||
|
|
||||||
|
# lnb
|
||||||
|
p_lnb = sub.add_parser('lnb', help="LNB voltage and tone control")
|
||||||
|
p_lnb.add_argument('--voltage', type=int, choices=[13, 18],
|
||||||
|
help="LNB voltage (13V or 18V)")
|
||||||
|
p_lnb.add_argument('--tone', help="22 kHz tone (on/off)")
|
||||||
|
p_lnb.add_argument('--extra-volt', action='store_true',
|
||||||
|
help="Enable +1V LNB voltage boost")
|
||||||
|
p_lnb.add_argument('--power', help="LNB power supply (on/off)")
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = build_parser()
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.command:
|
||||||
|
# Default to status if no subcommand
|
||||||
|
args.command = 'status'
|
||||||
|
args.json = getattr(args, 'json', False)
|
||||||
|
|
||||||
|
dispatch = {
|
||||||
|
'status': cmd_status,
|
||||||
|
'tune': cmd_tune,
|
||||||
|
'stream': cmd_stream,
|
||||||
|
'diseqc': cmd_diseqc,
|
||||||
|
'lnb': cmd_lnb,
|
||||||
|
}
|
||||||
|
|
||||||
|
handler = dispatch.get(args.command)
|
||||||
|
if handler is None:
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
with SkyWalker1(verbose=args.verbose) as sw:
|
||||||
|
handler(sw, args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Loading…
x
Reference in New Issue
Block a user