Dumped 8KB internal RAM and 64KB external RAM from SkyWalker-1 serial #00857 via Cypress FX2 vendor request 0xA0. Device reports FW v2.06.4 (build 2007-07-13). Tool also scans all vendor USB commands and probes device status registers.
293 lines
10 KiB
Python
293 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Genpix SkyWalker-1 firmware probe and dump tool.
|
|
|
|
The SkyWalker-1 uses a Cypress FX2 (EZ-USB) microcontroller.
|
|
FX2 devices support reading internal RAM (8KB at 0x0000-0x1FFF)
|
|
and external RAM via standard vendor requests:
|
|
- bRequest=0xA0 (FX2 firmware load/read)
|
|
- wValue=address, wIndex=0
|
|
|
|
This tool also queries Genpix-specific vendor commands to gather
|
|
device info before attempting a firmware dump.
|
|
"""
|
|
|
|
import sys
|
|
import struct
|
|
import argparse
|
|
from datetime import datetime
|
|
|
|
try:
|
|
import usb.core
|
|
import usb.util
|
|
except ImportError:
|
|
print("pyusb required: pip install pyusb")
|
|
sys.exit(1)
|
|
|
|
VENDOR_ID = 0x09C0
|
|
PRODUCT_ID = 0x0203
|
|
|
|
# Genpix vendor commands (from SkyWalker1Control.h)
|
|
CMD_GET_USB_SPEED = 0x07
|
|
CMD_FW_VERSION_READ = 0x0B
|
|
CMD_VENDOR_STRING_READ = 0x0C
|
|
CMD_PRODUCT_STRING_READ = 0x0D
|
|
CMD_RESET_FX2 = 0x13
|
|
CMD_FW_BCD_VERSION_READ = 0x14
|
|
CMD_GET_8PSK_CONFIG = 0x80
|
|
CMD_GET_SIGNAL_STRENGTH = 0x87
|
|
CMD_GET_SIGNAL_LOCK = 0x90
|
|
CMD_GET_SERIAL_NUMBER = 0x93
|
|
|
|
# FX2 standard vendor request for RAM access
|
|
FX2_RAM_REQUEST = 0xA0
|
|
|
|
# FX2 memory map
|
|
FX2_INTERNAL_RAM_SIZE = 0x2000 # 8KB internal RAM
|
|
FX2_EXTERNAL_RAM_SIZE = 0x10000 # Up to 64KB external
|
|
|
|
# Config status bits
|
|
CONFIG_BITS = {
|
|
0x01: "8PSK Started",
|
|
0x02: "BCM4500 FW Loaded",
|
|
0x04: "Intersil LNB On",
|
|
0x08: "DVB Mode",
|
|
0x10: "22kHz Tone",
|
|
0x20: "18V Selected",
|
|
0x40: "DC Tuned",
|
|
0x80: "Armed (streaming)",
|
|
}
|
|
|
|
|
|
def find_device():
|
|
dev = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID)
|
|
if dev is None:
|
|
print("SkyWalker-1 not found. Is it plugged in?")
|
|
sys.exit(1)
|
|
return dev
|
|
|
|
|
|
def vendor_in(dev, request, value=0, index=0, length=64, timeout=2000):
|
|
"""Send a vendor IN control transfer (device-to-host)."""
|
|
try:
|
|
return dev.ctrl_transfer(
|
|
usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN,
|
|
request, value, index, length, timeout
|
|
)
|
|
except usb.core.USBError as e:
|
|
return None
|
|
|
|
|
|
def detach_kernel_driver(dev):
|
|
"""Detach kernel driver if attached."""
|
|
for cfg in dev:
|
|
for intf in cfg:
|
|
if dev.is_kernel_driver_active(intf.bInterfaceNumber):
|
|
try:
|
|
dev.detach_kernel_driver(intf.bInterfaceNumber)
|
|
print(f" Detached kernel driver from interface {intf.bInterfaceNumber}")
|
|
return intf.bInterfaceNumber
|
|
except usb.core.USBError as e:
|
|
print(f" Warning: Could not detach kernel driver: {e}")
|
|
print(" Try running with sudo, or: sudo modprobe -r dvb_usb_gp8psk")
|
|
sys.exit(1)
|
|
return None
|
|
|
|
|
|
def probe_device_info(dev):
|
|
"""Query all known Genpix info commands."""
|
|
print("\n=== Genpix SkyWalker-1 Device Info ===\n")
|
|
|
|
# Firmware version (6 bytes)
|
|
data = vendor_in(dev, CMD_FW_VERSION_READ, length=6)
|
|
if data is not None and len(data) == 6:
|
|
fw_int = (data[2] << 16) | (data[1] << 8) | data[0]
|
|
build_date = f"20{data[5]:02d}-{data[4]:02d}-{data[3]:02d}"
|
|
print(f" FW Version: {data[2]}.{data[1]:02d}.{data[0]} (0x{fw_int:06x})")
|
|
print(f" FW Build: {build_date}")
|
|
else:
|
|
print(f" FW Version: (failed: {data})")
|
|
|
|
# BCD version
|
|
data = vendor_in(dev, CMD_FW_BCD_VERSION_READ, length=2)
|
|
if data is not None:
|
|
print(f" BCD Version: {bytes(data).hex()}")
|
|
|
|
# Vendor string
|
|
data = vendor_in(dev, CMD_VENDOR_STRING_READ, length=64)
|
|
if data is not None:
|
|
s = bytes(data).rstrip(b'\x00').decode('ascii', errors='replace')
|
|
print(f" Vendor: {s}")
|
|
|
|
# Product string
|
|
data = vendor_in(dev, CMD_PRODUCT_STRING_READ, length=64)
|
|
if data is not None:
|
|
s = bytes(data).rstrip(b'\x00').decode('ascii', errors='replace')
|
|
print(f" Product: {s}")
|
|
|
|
# USB speed
|
|
data = vendor_in(dev, CMD_GET_USB_SPEED, length=1)
|
|
if data is not None:
|
|
speeds = {0: "Low", 1: "Full (12Mbps)", 2: "High (480Mbps)"}
|
|
print(f" USB Speed: {speeds.get(data[0], f'Unknown ({data[0]})')}")
|
|
|
|
# Serial number
|
|
data = vendor_in(dev, CMD_GET_SERIAL_NUMBER, length=8)
|
|
if data is not None:
|
|
print(f" Serial: {bytes(data).hex()} ({bytes(data).rstrip(b'\\x00').decode('ascii', errors='replace')})")
|
|
|
|
# 8PSK config/status
|
|
data = vendor_in(dev, CMD_GET_8PSK_CONFIG, length=1)
|
|
if data is not None:
|
|
status = data[0]
|
|
print(f" Config: 0x{status:02x}")
|
|
for bit, desc in CONFIG_BITS.items():
|
|
state = "ON" if status & bit else "off"
|
|
print(f" [{state:>3}] {desc}")
|
|
|
|
print()
|
|
|
|
|
|
def dump_fx2_ram(dev, output_file, start=0x0000, size=FX2_INTERNAL_RAM_SIZE, chunk=64):
|
|
"""
|
|
Attempt to read FX2 internal RAM using the standard FX2 vendor request 0xA0.
|
|
|
|
The Cypress FX2 bootloader/firmware typically supports:
|
|
- bRequest = 0xA0
|
|
- wValue = start address
|
|
- wIndex = 0
|
|
- Direction = IN (device to host)
|
|
"""
|
|
print(f"=== Attempting FX2 RAM dump: 0x{start:04X} - 0x{start+size-1:04X} ({size} bytes) ===\n")
|
|
|
|
firmware = bytearray()
|
|
addr = start
|
|
errors = 0
|
|
consecutive_errors = 0
|
|
|
|
while addr < start + size:
|
|
remaining = (start + size) - addr
|
|
read_len = min(chunk, remaining)
|
|
|
|
data = vendor_in(dev, FX2_RAM_REQUEST, value=addr, index=0, length=read_len)
|
|
|
|
if data is None:
|
|
errors += 1
|
|
consecutive_errors += 1
|
|
firmware.extend(b'\xff' * read_len)
|
|
if consecutive_errors >= 5:
|
|
print(f"\n Stopped: {consecutive_errors} consecutive read failures at 0x{addr:04X}")
|
|
print(" Device may not support FX2 RAM readback (EEPROM firmware)")
|
|
break
|
|
else:
|
|
consecutive_errors = 0
|
|
firmware.extend(data)
|
|
|
|
if (addr - start) % 0x400 == 0:
|
|
pct = ((addr - start) / size) * 100
|
|
print(f" 0x{addr:04X} [{pct:5.1f}%] {'OK' if data is not None else 'FAIL'}", end='\r')
|
|
|
|
addr += read_len
|
|
|
|
print(f"\n\n Read {len(firmware)} bytes, {errors} chunk errors")
|
|
|
|
if firmware and any(b != 0xFF for b in firmware):
|
|
with open(output_file, 'wb') as f:
|
|
f.write(firmware)
|
|
print(f" Saved to: {output_file}")
|
|
|
|
# Quick analysis
|
|
non_ff = sum(1 for b in firmware if b != 0xFF)
|
|
non_zero = sum(1 for b in firmware if b != 0x00)
|
|
print(f" Non-0xFF bytes: {non_ff}/{len(firmware)}")
|
|
print(f" Non-0x00 bytes: {non_zero}/{len(firmware)}")
|
|
|
|
# Check for FX2 reset vector
|
|
if len(firmware) >= 3:
|
|
print(f" First 16 bytes: {firmware[:16].hex(' ')}")
|
|
if firmware[0] == 0x02:
|
|
jump_addr = (firmware[1] << 8) | firmware[2]
|
|
print(f" Reset vector: LJMP 0x{jump_addr:04X} (typical FX2 firmware)")
|
|
else:
|
|
print(" No valid data read — dump appears empty")
|
|
|
|
return firmware
|
|
|
|
|
|
def scan_vendor_commands(dev, start=0x00, end=0xFF):
|
|
"""Brute-force scan all vendor IN commands to find undocumented ones."""
|
|
print(f"=== Scanning vendor commands 0x{start:02X}-0x{end:02X} ===\n")
|
|
found = []
|
|
for cmd in range(start, end + 1):
|
|
data = vendor_in(dev, cmd, length=64, timeout=500)
|
|
if data is not None and len(data) > 0:
|
|
preview = bytes(data[:16]).hex(' ')
|
|
is_known = cmd in (
|
|
CMD_GET_USB_SPEED, CMD_FW_VERSION_READ, CMD_VENDOR_STRING_READ,
|
|
CMD_PRODUCT_STRING_READ, CMD_FW_BCD_VERSION_READ, CMD_GET_8PSK_CONFIG,
|
|
CMD_GET_SIGNAL_STRENGTH, CMD_GET_SIGNAL_LOCK, CMD_GET_SERIAL_NUMBER,
|
|
FX2_RAM_REQUEST,
|
|
)
|
|
marker = " [KNOWN]" if is_known else " [NEW!]"
|
|
print(f" 0x{cmd:02X}: [{len(data):3d} bytes] {preview}...{marker}")
|
|
found.append((cmd, data))
|
|
print(f"\n Found {len(found)} responding commands")
|
|
return found
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Genpix SkyWalker-1 firmware probe/dump tool")
|
|
parser.add_argument('--info', action='store_true', help="Query device info")
|
|
parser.add_argument('--dump', metavar='FILE', help="Dump FX2 RAM to file")
|
|
parser.add_argument('--scan', action='store_true', help="Scan all vendor commands")
|
|
parser.add_argument('--start', type=lambda x: int(x, 0), default=0x0000,
|
|
help="RAM dump start address (default: 0x0000)")
|
|
parser.add_argument('--size', type=lambda x: int(x, 0), default=FX2_INTERNAL_RAM_SIZE,
|
|
help=f"RAM dump size (default: 0x{FX2_INTERNAL_RAM_SIZE:X})")
|
|
parser.add_argument('--external', action='store_true',
|
|
help="Try to dump external RAM (64KB)")
|
|
args = parser.parse_args()
|
|
|
|
if not any([args.info, args.dump, args.scan]):
|
|
args.info = True
|
|
args.scan = True
|
|
|
|
print(f"Genpix SkyWalker-1 Firmware Tool")
|
|
print(f"{'=' * 40}")
|
|
|
|
dev = find_device()
|
|
print(f"\nFound device: Bus {dev.bus} Addr {dev.address}")
|
|
intf = detach_kernel_driver(dev)
|
|
|
|
try:
|
|
dev.set_configuration()
|
|
except usb.core.USBError:
|
|
pass # May already be configured
|
|
|
|
try:
|
|
if args.info:
|
|
probe_device_info(dev)
|
|
|
|
if args.scan:
|
|
scan_vendor_commands(dev)
|
|
print()
|
|
|
|
if args.dump:
|
|
if args.external:
|
|
dump_fx2_ram(dev, args.dump, args.start, FX2_EXTERNAL_RAM_SIZE)
|
|
else:
|
|
dump_fx2_ram(dev, args.dump, args.start, args.size)
|
|
|
|
finally:
|
|
if intf is not None:
|
|
try:
|
|
usb.util.release_interface(dev, intf)
|
|
dev.attach_kernel_driver(intf)
|
|
print("\nRe-attached kernel driver")
|
|
except usb.core.USBError:
|
|
print("\nNote: Run 'sudo modprobe dvb_usb_gp8psk' to reload driver")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|