Add alternative operating modes: spectrum, scan, monitor, lband, track
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.
This commit is contained in:
parent
b21f4957f6
commit
23055f34ab
4 changed files with 1756 additions and 312 deletions
318
tools/tune.py
318
tools/tune.py
|
|
@ -18,316 +18,18 @@ import json
|
|||
import signal
|
||||
import os
|
||||
|
||||
try:
|
||||
import usb.core
|
||||
import usb.util
|
||||
except ImportError:
|
||||
print("pyusb required: pip install pyusb")
|
||||
sys.exit(1)
|
||||
# Add tools directory to path for library import
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
VENDOR_ID = 0x09C0
|
||||
PRODUCT_ID = 0x0203
|
||||
from skywalker_lib import (
|
||||
SkyWalker1, VENDOR_ID, PRODUCT_ID, EP2_URB_SIZE,
|
||||
MODULATIONS, FEC_RATES, MOD_FEC_GROUP,
|
||||
LNB_LO_LOW, LNB_LO_HIGH,
|
||||
CONFIG_BITS,
|
||||
signal_bar, format_config_bits,
|
||||
)
|
||||
|
||||
# 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
|
||||
import usb.core
|
||||
|
||||
|
||||
# -- Subcommand handlers --
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue