#!/usr/bin/env python3 """ Disassemble PS2000 Init Script binary to JSON. Reconstructs the parsing logic from sub_280C0, which is called from _ps2000_context_init+C0 and ps2000PreLaunch+F4. The init script is a sequence of IO commands that configure the MCU, ADC, FPGA, and other hardware during device initialization. Binary format: [opcode:1][payload:variable] For fixed-size commands, payload size is determined by opcode. For variable-size commands, payload starts with 16-bit length prefix. Usage: python3 init_tbl_disasm.py init_script.bin > init_script.json python3 init_tbl_disasm.py init_script.bin --out init_script.json """ import sys import json import struct import argparse # ========================================================================== # IO COMMAND TABLE # ========================================================================== # # Full list reconstructed from RTTI type names in .rodata: # - 0xA64D0..0xA6B40: Original 36 commands # - 0xA69D0..0xA6AE0: Additional flash/GPIO commands # # Format: opcode -> (name, type, payload_size) # payload_size: fixed byte count, or "variable" for length-prefixed data # # The numeric prefix in RTTI strings (e.g., "21IO_COMMAND_READ_XDATA") # is the Itanium ABI name length; the actual identifier follows. IO_COMMANDS = { # XDATA access (FX2/FX3 external memory) 0x00: ("IO_COMMAND_READ_XDATA", "read", 3), 0x01: ("IO_COMMAND_WRITE_XDATA", "write", 3), # Extended register access 0x02: ("IO_COMMAND_READ_EXTENDED_REGISTER", "read", 3), 0x03: ("IO_COMMAND_WRITE_EXTENDED_REGISTER", "write", 3), # I2C bus operations 0x04: ("IO_COMMAND_READ_I2C", "read", 4), 0x05: ("IO_COMMAND_READ_I2C_WITH_STATUS", "read", 4), 0x06: ("IO_COMMAND_WRITE_I2C", "write", 4), # SPI bus operations 0x07: ("IO_COMMAND_WRITE_SPI", "write", 4), 0x08: ("IO_COMMAND_WRITE_ADC_SPI", "write", 3), 0x09: ("IO_COMMAND_READ_WRITE_SPI", "readwrite", 4), # I2C via SPI bridge 0x0A: ("IO_COMMAND_WRITE_I2C_VIA_SPI", "write", 4), 0x0B: ("IO_COMMAND_READ_I2C_VIA_SPI", "read", 4), # ADC SPI operations 0x0C: ("IO_COMMAND_READ_WRITE_ADC_SPI", "readwrite", 3), # EEPROM synchronization 0x0D: ("IO_COMMAND_WAIT_FOR_EEPROM_WRITE", "wait", 0), 0x0E: ("IO_COMMAND_WAIT_FOR_FX3_EEPROM_WRITE","wait", 0), # FPGA register access 0x0F: ("IO_COMMAND_READ_FPGA_REGISTER", "read", 2), 0x10: ("IO_COMMAND_WRITE_FPGA_REGISTER", "write", 3), 0x11: ("IO_COMMAND_READ_WRITE_FPGA_REGISTER","readwrite", 3), # FIFO operations 0x12: ("IO_COMMAND_READ_FIFO", "read", 4), 0x13: ("IO_COMMAND_ABORT_READ_FIFO", "abort", 0), 0x14: ("IO_COMMAND_WRITE_FIFO", "write", "variable"), # Auto-in and timing 0x15: ("IO_SET_AUTOIN_LEN", "set", 2), 0x16: ("IO_DELAY_MS", "delay", 2), # EEPROM access 0x17: ("IO_READ_EEPROM", "read", 4), 0x18: ("IO_WRITE_EEPROM", "write", "variable"), # FX3 GPIO operations 0x19: ("IO_COMMAND_FX3_USE_GPIO", "gpio", 2), 0x1A: ("IO_COMMAND_FX3_UNUSE_GPIO", "gpio", 2), 0x1B: ("IO_COMMAND_FX3_IS_GPIF_DONE", "status", 0), 0x1C: ("IO_COMMAND_FX3_GPIO_PWM", "pwm", 4), # USB/GPIF frequency 0x1D: ("IO_COMMAND_USB_WRITE_GPIF_FREQ", "freq", 4), # FX3 register access 0x1E: ("IO_COMMAND_FX3_READ_REGISTER", "read", 3), # Flash memory operations 0x1F: ("IO_COMMAND_READ_FLASH_PAGE", "read", 4), 0x20: ("USB_IO_WRITE_FLASH_PAGE", "write", "variable"), 0x21: ("USB_IO_ERASE_FLASH_PAGE", "erase", 2), 0x22: ("USB_IO_SELECT_CSN_SPI", "select", 1), # EEPROM/Flash status checks 0x23: ("IO_COMMAND_CHECK_EMPTY_EEPROM", "check", 0), 0x24: ("IO_COMMAND_CHECK_FLASH_AVAIL", "check", 0), # Flash protection and erase 0x25: ("IO_COMMAND_PROTECT_FLASH", "protect", 2), 0x26: ("IO_COMMAND_ERASE_FLASH_PAGE", "erase", 2), # Generic packet and FPGA commands 0x27: ("IO_COMMAND_PACKET", "packet", "variable"), 0x28: ("FPGA_IO_COMMAND", "fpga", "variable"), # End of script marker 0xFF: ("END", "end", 0), } # ========================================================================== # DISASSEMBLER # ========================================================================== def disassemble(data: bytes) -> list: """ Disassemble binary init script into command list. Args: data: Raw binary script data Returns: List of command dictionaries """ cmds = [] pos = 0 while pos < len(data): op = data[pos] pos += 1 # Unknown opcode - record and stop if op not in IO_COMMANDS: cmds.append({ "offset": pos - 1, "error": f"unknown_opcode_0x{op:02X}", "raw": data[pos-1:min(pos+8, len(data))].hex() }) break name, cmd_type, payload_size = IO_COMMANDS[op] entry = { "offset": pos - 1, "op": name, "type": cmd_type } # END marker terminates the script if name == "END": cmds.append(entry) break # Variable-length commands: read 16-bit length prefix if payload_size == "variable": if pos + 2 > len(data): entry["error"] = "truncated_length" cmds.append(entry) break data_len = struct.unpack_from(' len(data): entry["error"] = "truncated_data" entry["expected_len"] = data_len entry["available"] = len(data) - pos cmds.append(entry) break entry["data_len"] = data_len entry["data_hex"] = data[pos:pos+data_len].hex() pos += data_len # Fixed-length payload else: if pos + payload_size > len(data): entry["error"] = "truncated_payload" entry["expected_bytes"] = payload_size entry["available"] = len(data) - pos cmds.append(entry) break payload = data[pos:pos+payload_size] pos += payload_size # Decode payload fields based on size if payload_size >= 1: entry["addr"] = f"0x{payload[0]:02X}" if payload_size >= 3: entry["value"] = f"0x{struct.unpack_from('= 4: entry["extra"] = f"0x{payload[3]:02X}" cmds.append(entry) return cmds # ========================================================================== # MAIN # ========================================================================== def main(): parser = argparse.ArgumentParser( description="Disassemble PS2000 init script binary to JSON", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: %(prog)s init_script.bin %(prog)s init_script.bin --out init_script.json %(prog)s init_script.bin --stats """ ) parser.add_argument("input", help="Input binary file") parser.add_argument("--out", "-o", default="-", help="Output JSON file (default: stdout)") parser.add_argument("--stats", action="store_true", help="Print command statistics") args = parser.parse_args() try: with open(args.input, "rb") as f: data = f.read() except IOError as e: print(f"Error reading file: {e}", file=sys.stderr) sys.exit(1) commands = disassemble(data) # Print statistics if requested if args.stats: op_counts = {} errors = 0 for cmd in commands: if "error" in cmd: errors += 1 else: op = cmd["op"] op_counts[op] = op_counts.get(op, 0) + 1 print(f"File: {args.input}", file=sys.stderr) print(f"Size: {len(data)} bytes", file=sys.stderr) print(f"Commands: {len(commands)}", file=sys.stderr) print(f"Errors: {errors}", file=sys.stderr) print("\nCommand breakdown:", file=sys.stderr) for op, count in sorted(op_counts.items(), key=lambda x: -x[1]): print(f" {op}: {count}", file=sys.stderr) print("", file=sys.stderr) # Build result result = { "file": args.input, "size": len(data), "command_count": len(commands), "commands": commands } output = json.dumps(result, indent=2) if args.out == "-": print(output) else: try: with open(args.out, "w") as f: f.write(output) print(f"Written to {args.out}", file=sys.stderr) except IOError as e: print(f"Error writing file: {e}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()