#!/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 '' | 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(' 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(' 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(' 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()