diff --git a/firmware-dump/skywalker1_fx2_external.bin b/firmware-dump/skywalker1_fx2_external.bin new file mode 100644 index 0000000..9bacefb Binary files /dev/null and b/firmware-dump/skywalker1_fx2_external.bin differ diff --git a/firmware-dump/skywalker1_fx2_internal.bin b/firmware-dump/skywalker1_fx2_internal.bin new file mode 100644 index 0000000..b8b5110 Binary files /dev/null and b/firmware-dump/skywalker1_fx2_internal.bin differ diff --git a/tools/fw_dump.py b/tools/fw_dump.py new file mode 100644 index 0000000..05a45d7 --- /dev/null +++ b/tools/fw_dump.py @@ -0,0 +1,292 @@ +#!/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()