def receive_packet(netdimm: NetDimm) -> Optional[bytes]: with netdimm.connection(): # First, attempt to grab the next packet available. status = read_send_status_register(netdimm) if status is None: return None # Now, grab the length of the available packet. length = (status >> 12) & 0xFFF if length == 0: return None # Now, see if the transfer was partially done, if so rewind it. loc = status & 0xFFF if loc > 0: write_send_status_register(netdimm, 0) # Now, grab and assemble the data itself. data: List[Optional[int]] = [None] * length tries: int = 0 while any(d is None for d in data): chunk = netdimm.peek(DATA_REGISTER, PeekPokeTypeEnum.TYPE_LONG) if ((chunk & 0xFF000000) >> 24) in {0x00, 0xFF}: tries += 1 if tries > MAX_EMPTY_READS: # We need to figure out where we left off. for loc, val in enumerate(data): if val is None: # We found a spot to resume from. write_send_status_register(netdimm, loc & 0xFFF) tries = 0 break else: # We should always find a spot to resume from or there's an issue, # since in this case we should be done. raise Exception("Logic error!") else: # Grab the location for this chunk, stick the data in the right spot. location = (((chunk >> 24) & 0xFF) - 1) * 3 for off, shift in enumerate([16, 8, 0]): actual = off + location if actual < length: data[actual] = (chunk >> shift) & 0xFF # Grab the actual return data. bytedata = bytes([d for d in data if d is not None]) if len(bytedata) != length: raise Exception("Logic error!") # Acknowledge the data transfer completed. write_send_status_register(netdimm, length & 0xFFF) # Return the actual data! return bytedata
def read_recv_status_register(netdimm: NetDimm) -> Optional[int]: with netdimm.connection(): valid = False status: int = 0 start = time.time() while not valid: status = 0 while status == 0 or status == 0xFFFFFFFF and ( time.time() - start <= MAX_READ_TIMEOUT): status = netdimm.peek(RECV_STATUS_REGISTER, PeekPokeTypeEnum.TYPE_LONG) valid = checksum_valid(status, RECV_STATUS_REGISTER_SEED) if not valid and (time.time() - start > MAX_READ_TIMEOUT): return None return status
def read_config_register(netdimm: NetDimm) -> Optional[int]: with netdimm.connection(): valid = False config: int = 0 start = time.time() while not valid: config = 0 while config == 0 or config == 0xFFFFFFFF and ( time.time() - start <= MAX_READ_TIMEOUT): config = netdimm.peek(CONFIG_REGISTER, PeekPokeTypeEnum.TYPE_LONG) valid = checksum_valid(config, CONFIG_REGISTER_SEED) if not valid and (time.time() - start > MAX_READ_TIMEOUT): return None return config
def main() -> int: parser = argparse.ArgumentParser( description= "Tools for requesting info from a NetDimm for Naomi/Chihiro/Triforce.") parser.add_argument( "ip", metavar="IP", type=str, help="The IP address that the NetDimm is configured on.", ) args = parser.parse_args() print("Requesting...", file=sys.stderr) netdimm = NetDimm(args.ip) info = netdimm.info() if info.game_crc_status == CRCStatusEnum.STATUS_CHECKING: validity = "checking..." elif info.game_crc_status == CRCStatusEnum.STATUS_VALID: validity = "valid" elif info.game_crc_status == CRCStatusEnum.STATUS_INVALID: validity = "invalid" elif info.game_crc_status == CRCStatusEnum.STATUS_BAD_MEMORY: validity = "bad memory module" elif info.game_crc_status == CRCStatusEnum.STATUS_DISABLED: validity = "startup crc checking disabled" print(f"DIMM Firmware Version: {info.firmware_version.value}") print(f"DIMM Memory Size: {info.memory_size} MB") print( f"Available Game Memory Size: {int(info.available_game_memory / 1024 / 1024)} MB" ) print( f"Current Game CRC: {hex(info.current_game_crc)[2:]} ({int(info.current_game_size / 1024 / 1024)} MB) ({validity})" ) return 0
def main() -> int: parser = argparse.ArgumentParser( description= "Tools for sending images to NetDimm for Naomi/Chihiro/Triforce.") parser.add_argument( "ip", metavar="IP", type=str, help="The IP address that the NetDimm is configured on.", ) parser.add_argument( "image", metavar="IMAGE", type=str, help="The image file we should send to the NetDimm.", ) parser.add_argument( "--key", metavar="HEX", type=str, help= "Key (as a 16 character hex string) to encrypt image file. Defaults to null key", ) parser.add_argument( "--target", metavar="TARGET", type=NetDimmTargetEnum, action=EnumAction, default=NetDimmTargetEnum.TARGET_NAOMI, help= "Target platform this image is going to. Defaults to 'naomi'. Choose from 'naomi', 'chihiro' or 'triforce'.", ) parser.add_argument( "--version", metavar="VERSION", type=NetDimmVersionEnum, action=EnumAction, default=NetDimmVersionEnum.VERSION_4_01, help= "NetDimm firmware version this image is going to. Defaults to '4.01'. Choose from '1.02', '2.06', '2.17', '3.03', '3.17', '4.01' or '4.02'.", ) parser.add_argument( '--patch-file', metavar='FILE', type=str, action='append', help=( 'Patch to apply to image on-the-fly while sending to the NetDimm. ' 'Can be specified multiple times to apply multiple patches. ' 'Patches will be applied in specified order. If not specified, the ' 'image is sent without patching.'), ) parser.add_argument( '--settings-file', metavar='FILE', type=str, action='append', help= ('Settings to apply to image on-the-fly while sending to the NetDimm. ' 'Currently only supported for the Naomi platform. For Naomi, the ' 'settings file should be a valid 128-byte EEPROM file as obtained ' 'from an emulator or as created using the "edit_settings" utility, ' 'or a 32-kbyte SRAM file as obtained from an emulator. You can apply ' 'both an EEPROM and a SRAM settings file by specifying this argument ' 'twice.'), ) parser.add_argument( '--disable-crc', action="store_true", help= "Disable checking memory screen after upload and boot straight into the game", ) parser.add_argument( '--disable-now-loading', action="store_true", help= "Disable displaying the \"NOW LOADING...\" screen when sending the game", ) # Prevent ERROR 33 GATEWAY IS NOT FOUND due to bad, or missing PIC chip. parser.add_argument( '--keyless-boot', action="store_true", help="Enable boot without Key Chip", ) # Give more time to slower netboot on some platforms. parser.add_argument( '--send-timeout', type=int, default=None, help="Specify a different send timeout in seconds", ) args = parser.parse_args() # If the user specifies a key (not normally done), convert it key: Optional[bytes] = None if args.key: if len(args.key) != 16: raise Exception("Invalid key length for image!") key = bytes( [int(args.key[x:(x + 2)], 16) for x in range(0, len(args.key), 2)]) print("sending...", file=sys.stderr) netdimm = NetDimm(args.ip, version=args.version, target=args.target, timeout=args.send_timeout, log=print) # Grab the binary, patch it with requested patches. with open(args.image, "rb") as fp: data = FileBytes(fp) for patch in args.patch_file or []: with open(patch, "r") as pp: differences = pp.readlines() differences = [d.strip() for d in differences if d.strip()] try: data = BinaryDiff.patch(data, differences) except Exception as e: print(f"Could not patch {args.image}: {str(e)}", file=sys.stderr) return 1 # Grab any settings file that should be included. if args.target == NetDimmTargetEnum.TARGET_NAOMI: for sfile in args.settings_file or []: with open(sfile, "rb") as fp: settings = fp.read() patcher = NaomiSettingsPatcher(data, get_default_naomi_trojan()) if len(settings) == NaomiSettingsPatcher.SRAM_SIZE: patcher.put_sram(settings) elif len(settings) == NaomiSettingsPatcher.EEPROM_SIZE: patcher.put_eeprom(settings) else: print( f"Could not attach {sfile}: not the right size to be an SRAM or EEPROM settings", file=sys.stderr) return 1 data = patcher.data else: for sfile in args.settings_file or []: print(f"Could not attach {sfile}: not a Naomi ROM!") return 1 # Send the binary, reboot into the game. netdimm.send(data, key, disable_crc_check=args.disable_crc, disable_now_loading=args.disable_now_loading) print("rebooting into game...", file=sys.stderr) netdimm.reboot() print("ok!", file=sys.stderr) if args.keyless_boot: print("keyless boot: infinite set_time_limit() loop") while True: netdimm.set_time_limit(10) time.sleep(5) return 0
def main() -> int: parser = argparse.ArgumentParser( description= "Utility for dumping/restoring an SRAM file on a SEGA Naomi.") subparsers = parser.add_subparsers(help='Action to take', dest='action') dump_parser = subparsers.add_parser( 'dump', help='Dump the SRAM from a SEGA Naomi and write it to a file.', description='Dump the SRAM from a SEGA Naomi and write it to a file.', ) dump_parser.add_argument( "ip", metavar="IP", type=str, help="The IP address that the NetDimm we should use is configured on.", ) dump_parser.add_argument( 'sram', metavar='SRAM', type=str, help='The SRAM file we should write the contents of the SRAM to.', ) dump_parser.add_argument( '--exe', metavar='EXE', type=str, default=os.path.join(root, 'homebrew', 'sramdump', 'sramdump.bin'), help= 'The helper executable that we should send to dump/restore SRAM files on the Naomi. Defaults to %(default)s.', ) dump_parser.add_argument( '--verbose', action="store_true", help="Display verbose debugging information.", ) restore_parser = subparsers.add_parser( 'restore', help='Restore the SRAM on a SEGA Naomi from a file.', description='Restore the SRAM on a SEGA Naomi from a file.', ) restore_parser.add_argument( "ip", metavar="IP", type=str, help="The IP address that the NetDimm we should use is configured on.", ) restore_parser.add_argument( 'sram', metavar='SRAM', type=str, help='The SRAM file we should read the contents of the SRAM from.', ) restore_parser.add_argument( '--exe', metavar='EXE', type=str, default=os.path.join(root, 'homebrew', 'sramdump', 'sramdump.bin'), help= 'The helper executable that we should send to dump/restore SRAM files on the Naomi. Defaults to %(default)s.', ) restore_parser.add_argument( '--verbose', action="store_true", help="Display verbose debugging information.", ) args = parser.parse_args() verbose = args.verbose if args.action == "dump": netdimm = NetDimm(args.ip, log=print if verbose else None) with open(args.exe, "rb") as fp: helperdata = FileBytes(fp) try: # Now, connect to the net dimm, send the menu and then start communicating with it. print("Connecting to net dimm...") print("Sending helper to net dimm...") netdimm.send(helperdata, disable_crc_check=True) netdimm.reboot() print("Waiting for Naomi to boot...") except NetDimmException: # Mark failure so we don't try to communicate below. print("Sending helper executable failed!") return 1 try: with netdimm.connection(): while True: msg = receive_message(netdimm, verbose=verbose) if not msg: continue if msg.id == MESSAGE_READY: print("Dumping SRAM...") send_message(netdimm, Message(MESSAGE_SRAM_READ_REQUEST), verbose=verbose) elif msg.id == MESSAGE_SRAM_READ: if len(msg.data) != 0x8000: print("Got wrong size for SRAM!") return 1 else: with open(args.sram, "wb") as fp: fp.write(msg.data) print(f"Wrote SRAM from Naomi to '{args.sram}'.") send_message(netdimm, Message(MESSAGE_DONE), verbose=verbose) break elif msg.id == MESSAGE_HOST_STDOUT: print(msg.data.decode('utf-8'), end="") elif msg.id == MESSAGE_HOST_STDERR: print(msg.data.decode('utf-8'), end="", file=sys.stderr) else: print("Got unexpected packet!") return 1 except NetDimmException: # Mark failure so we don't try to wait for power down below. print("Communicating with the helper failed!") return 1 if args.action == "restore": netdimm = NetDimm(args.ip, log=print if verbose else None) with open(args.exe, "rb") as fp: helperdata = FileBytes(fp) try: # Now, connect to the net dimm, send the menu and then start communicating with it. print("Connecting to net dimm...") print("Sending helper to net dimm...") netdimm.send(helperdata, disable_crc_check=True) netdimm.reboot() print("Waiting for Naomi to boot...") except NetDimmException: # Mark failure so we don't try to communicate below. print("Sending helper executable failed!") return 1 try: with netdimm.connection(): while True: msg = receive_message(netdimm, verbose=verbose) if not msg: continue if msg.id == MESSAGE_READY: send_message(netdimm, Message(MESSAGE_SRAM_WRITE_REQUEST), verbose=verbose) print("Restoring SRAM...") with open(args.sram, "rb") as fp: data = fp.read() send_message(netdimm, Message(MESSAGE_SRAM_WRITE, data), verbose=verbose) send_message(netdimm, Message(MESSAGE_DONE), verbose=verbose) break elif msg.id == MESSAGE_HOST_STDOUT: print(msg.data.decode('utf-8'), end="") elif msg.id == MESSAGE_HOST_STDERR: print(msg.data.decode('utf-8'), end="", file=sys.stderr) else: print("Got unexpected packet!") return 1 except NetDimmException: # Mark failure so we don't try to wait for power down below. print("Communicating with the helper failed!") return 1 return 0
def main() -> int: parser = argparse.ArgumentParser( description= "Tools for receiving images from NetDimm for Naomi/Chihiro/Triforce.") parser.add_argument( "ip", metavar="IP", type=str, help="The IP address that the NetDimm is configured on.", ) parser.add_argument( "image", metavar="IMAGE", type=str, help= "The image file we should write after receiving from to the NetDimm.", ) parser.add_argument( "--target", metavar="TARGET", type=NetDimmTargetEnum, action=EnumAction, default=NetDimmTargetEnum.TARGET_NAOMI, help= "Target platform this image is coming from. Defaults to 'naomi'. Choose from 'naomi', 'chihiro' or 'triforce'.", ) parser.add_argument( "--version", metavar="VERSION", type=NetDimmVersionEnum, action=EnumAction, default=NetDimmVersionEnum.VERSION_4_01, help= "NetDimm firmware version this image is coming from. Defaults to '4.01'. Choose from '1.02', '2.06', '2.17', '3.03', '3.17', '4.01' or '4.02'.", ) # Give more time to slower netboot on some platforms. parser.add_argument( '--receive-timeout', type=int, default=None, help="Specify a different receive timeout in seconds", ) args = parser.parse_args() print("receiving...", file=sys.stderr) netdimm = NetDimm(args.ip, version=args.version, target=args.target, timeout=args.receive_timeout) # Receive the binary. data = netdimm.receive() if data: with open(args.image, "wb") as fp: fp.write(data) print("ok!", file=sys.stderr) else: print("no valid game exists on net dimm!", file=sys.stderr) return 0
def main() -> int: parser = argparse.ArgumentParser( description= "Tools for peeking/poking values into running memory on a Naomi/Triforce/Chihiro.", ) subparsers = parser.add_subparsers(help='Action to take', dest='action') peek_parser = subparsers.add_parser( 'peek', help='Peek at a value in an 8/16/32-bit memory location', description='Peek at a value in an 8/16/32-bit memory location', ) peek_parser.add_argument( "ip", metavar="IP", type=str, help="The IP address that the NetDimm is configured on.", ) peek_parser.add_argument( "address", metavar="ADDR", type=str, help="The hex address of memory that you would like to peek into.", ) peek_parser.add_argument( "size", metavar="SIZE", type=int, help="The size in bytes you want to read. Valid values are 1, 2 and 4.", ) poke_parser = subparsers.add_parser( 'poke', help='Poke a value into an 8/16/32-bit memory location', description='Poke a value into an 8/16/32-bit memory location', ) poke_parser.add_argument( "ip", metavar="IP", type=str, help="The IP address that the NetDimm is configured on.", ) poke_parser.add_argument( "address", metavar="ADDR", type=str, help="The hex address of memory that you would like to poke into.", ) poke_parser.add_argument( "size", metavar="SIZE", type=int, help= "The size in bytes you want to write. Valid values are 1, 2 and 4.", ) poke_parser.add_argument( "data", metavar="VALUE", type=str, help="The hex value you wish to write into the address.", ) dump_parser = subparsers.add_parser( 'dump', help='Dump data from a memory location.', description='Dump data from a memory location.', ) dump_parser.add_argument( "ip", metavar="IP", type=str, help="The IP address that the NetDimm is configured on.", ) dump_parser.add_argument( "file", metavar="FILE", type=str, help="The file you want to dump data to.", ) dump_parser.add_argument( "address", metavar="ADDR", type=str, help="The hex address of memory that you would like to dump from.", ) dump_parser.add_argument( "size", metavar="SIZE", type=int, help="The size in bytes you want to read.", ) load_parser = subparsers.add_parser( 'load', help='Load data to a memory location.', description='Load data to a memory location.', ) load_parser.add_argument( "ip", metavar="IP", type=str, help="The IP address that the NetDimm is configured on.", ) load_parser.add_argument( "file", metavar="FILE", type=str, help="The file you want to load data to.", ) load_parser.add_argument( "address", metavar="ADDR", type=str, help="The hex address of memory that you would like to load to.", ) args = parser.parse_args() netdimm = NetDimm(args.ip) if args.action == "peek": if args.size == 1: data = netdimm.peek(int(args.address, 16), PeekPokeTypeEnum.TYPE_BYTE) & 0xFF elif args.size == 2: data = netdimm.peek(int(args.address, 16), PeekPokeTypeEnum.TYPE_SHORT) & 0xFFFF elif args.size == 4: data = netdimm.peek(int(args.address, 16), PeekPokeTypeEnum.TYPE_LONG) & 0xFFFFFFFF else: raise Exception(f"Invalid size selection {args.size}!") hexdata = hex(data)[2:] while len(hexdata) < (2 * args.size): hexdata = "0" + hexdata print(hexdata) elif args.action == "poke": if args.size == 1: netdimm.poke(int(args.address, 16), PeekPokeTypeEnum.TYPE_BYTE, int(args.data, 16) & 0xFF) elif args.size == 2: netdimm.poke(int(args.address, 16), PeekPokeTypeEnum.TYPE_SHORT, int(args.data, 16) & 0xFFFF) elif args.size == 4: netdimm.poke(int(args.address, 16), PeekPokeTypeEnum.TYPE_LONG, int(args.data, 16) & 0xFFFFFFFF) else: raise Exception(f"Invalid size selection {args.size}!") elif args.action == "dump": with open(args.file, "wb") as bfp: for i in range(args.size): data = netdimm.peek( int(args.address, 16) + i, PeekPokeTypeEnum.TYPE_BYTE) & 0xFF bfp.write(struct.pack("B", data)) print(f"Dumped {args.size} bytes to {args.file}") elif args.action == "load": with open(args.file, "rb") as bfp: for amount, b in enumerate(bfp.read()): netdimm.poke( int(args.address, 16) + amount, PeekPokeTypeEnum.TYPE_BYTE, b) print(f"Loaded {amount} bytes from {args.file}") return 0
def main() -> int: parser = argparse.ArgumentParser( description= "Provide an on-target menu for selecting games. Currently only works with Naomi." ) parser.add_argument( "ip", metavar="IP", type=str, help="The IP address that the NetDimm is configured on.", ) parser.add_argument( "romdir", metavar="ROMDIR", type=str, default=os.path.join(root, 'roms'), help= 'The directory of ROMs to select a game from. Defaults to %(default)s.', ) parser.add_argument( '--region', metavar="REGION", type=str, default=None, help= 'The region of the Naomi which we are running the menu on. Defaults to "japan".', ) parser.add_argument( '--exe', metavar='EXE', type=str, default=os.path.join(root, 'homebrew', 'netbootmenu', 'netbootmenu.bin'), help= 'The menu executable that we should send to display games on the Naomi. Defaults to %(default)s.', ) parser.add_argument( '--menu-settings-file', metavar='SETTINGS', type=str, default=os.path.join(root, '.netdimm_menu_settings.yaml'), help= 'The settings file we will use to store persistent settings. Defaults to %(default)s.', ) parser.add_argument( "--patchdir", metavar="PATCHDIR", type=str, default=os.path.join(root, 'patches'), help= 'The directory of patches we might want to apply to games. Defaults to %(default)s.', ) parser.add_argument( '--force-analog', action="store_true", help= "Force-enable analog control inputs. Use this if you have no digital controls and cannot set up analog options in the test menu.", ) parser.add_argument( '--force-players', type=int, default=0, help= "Force set the number of players for this cabinet. Valid values are 1-4. Use this if you do not want to set the player number in the system test menu.", ) parser.add_argument( '--force-use-filenames', action="store_true", help= "Force-enable using filenames for ROM display instead of the name in the ROM.", ) parser.add_argument( '--persistent', action="store_true", help= "Don't exit after successfully booting game. Instead, wait for power cycle and then send the menu again.", ) parser.add_argument( '--debug-mode', action="store_true", help="Enable extra debugging information on the Naomi.", ) parser.add_argument( '--fallback-font', metavar="FILE", type=str, default=None, help= "Any truetype font that should be used as a fallback if the built-in font can't render a character.", ) parser.add_argument( '--verbose', action="store_true", help="Display verbose debugging information.", ) args = parser.parse_args() verbose = args.verbose # Load the settings file settings = settings_load(args.menu_settings_file, args.ip) if args.region is not None: region = { "japan": NaomiRomRegionEnum.REGION_JAPAN, "usa": NaomiRomRegionEnum.REGION_USA, "export": NaomiRomRegionEnum.REGION_EXPORT, "korea": NaomiRomRegionEnum.REGION_KOREA, }.get(args.region, NaomiRomRegionEnum.REGION_JAPAN) settings.system_region = region settings_save(args.menu_settings_file, args.ip, settings) if args.force_analog: # Force the setting on, as a safeguard against cabinets that have no digital controls. settings.enable_analog = True settings_save(args.menu_settings_file, args.ip, settings) if args.force_use_filenames: settings.use_filenames = True settings_save(args.menu_settings_file, args.ip, settings) force_players = None if args.force_players is not None: if args.force_players >= 1 and args.force_players <= 4: force_players = args.force_players # Intentionally rebuild the menu every loop if we are in persistent mode, so that # changes to the ROM directory can be reflected on subsequent menu sends. while True: # First, load the rom directory, list out the contents and figure out which ones are naomi games. games: List[Tuple[str, str, bytes]] = [] romdir = os.path.abspath(args.romdir) success: bool = True for filename in [ f for f in os.listdir(romdir) if os.path.isfile(os.path.join(romdir, f)) ]: # Grab the header so we can parse it. with open(os.path.join(romdir, filename), "rb") as fp: data = FileBytes(fp) if verbose: print(f"Discovered file {filename}.") # Validate that it is a Naomi ROM. if len(data) < NaomiRom.HEADER_LENGTH: if verbose: print("Not long enough to be a ROM!") continue rom = NaomiRom(data) if not rom.valid: if verbose: print("Not a Naomi ROM!") continue # Get the name of the game. if settings.use_filenames: name = os.path.splitext(filename)[0].replace("_", " ") else: name = rom.names[settings.system_region] serial = rom.serial if verbose: print( f"Added {name} with serial {serial.decode('ascii')} to ROM list." ) games.append((os.path.join(romdir, filename), name, serial)) # Alphabetize them. games = sorted(games, key=lambda g: g[1]) # Now, create the settings section. last_game_id: int = 0 gamesconfig = b"" for index, (filename, name, serial) in enumerate(games): namebytes = name.encode('utf-8')[:127] while len(namebytes) < 128: namebytes = namebytes + b"\0" gamesconfig += namebytes + serial + struct.pack("<I", index) if filename == settings.last_game_file: last_game_id = index fallback_data = None if args.fallback_font is not None: with open(args.fallback_font, "rb") as fp: fallback_data = fp.read() config = struct.pack( "<IIIIIIIIBBBBBBBBBBBBIII", SETTINGS_SIZE, len(games), 1 if settings.enable_analog else 0, 1 if args.debug_mode else 0, last_game_id, settings.system_region.value, 1 if settings.use_filenames else 0, 1 if settings.disable_sound else 0, settings.joy1_calibration[0], settings.joy1_calibration[1], settings.joy2_calibration[0], settings.joy2_calibration[1], settings.joy1_calibration[2], settings.joy1_calibration[3], settings.joy1_calibration[4], settings.joy1_calibration[5], settings.joy2_calibration[2], settings.joy2_calibration[3], settings.joy2_calibration[4], settings.joy2_calibration[5], SETTINGS_SIZE + len(gamesconfig) if fallback_data is not None else 0, len(fallback_data) if fallback_data is not None else 0, force_players if (force_players is not None) else 0, ) if len(config) < SETTINGS_SIZE: config = config + (b"\0" * (SETTINGS_SIZE - len(config))) config = config + gamesconfig if fallback_data is not None: config = config + fallback_data # Now, load up the menu ROM and append the settings to it. if success: with open(args.exe, "rb") as fp: menudata = add_or_update_section(FileBytes(fp), 0x0D000000, config, verbose=verbose) try: # Now, connect to the net dimm, send the menu and then start communicating with it. print("Connecting to net dimm...") netdimm = NetDimm(args.ip, log=print if verbose else None) print("Sending menu to net dimm...") netdimm.send(menudata, disable_crc_check=True) netdimm.reboot() except NetDimmException: # Mark failure so we don't try to communicate below. success = False if args.persistent: print("Sending failed, will try again...") else: print("Sending failed...") # Now, talk to the net dimm and exchange packets to handle settings and game selection. selected_file = None if success: print("Talking to net dimm to wait for ROM selection...") time.sleep(5) last_game_selection: Optional[int] = None last_game_patches: List[Tuple[str, str]] = [] last_game_parsed_settings: Optional[NaomiSettingsWrapper] = None try: # Always show game send progress. netdimm = NetDimm(args.ip, log=print) with netdimm.connection(): while True: msg = receive_message(netdimm, verbose=verbose) if msg: if msg.id == MESSAGE_SELECTION: index = struct.unpack("<I", msg.data)[0] filename = games[index][0] print( f"Requested {games[index][1]} be loaded..." ) # Save the menu position. settings.last_game_file = filename settings_save(args.menu_settings_file, args.ip, settings) # Wait a second for animation on the Naomi. This assumes that the # below section takes a relatively short amount of time (well below # about 1 second) to patch and such. If you are on a platform with # limited speed and attempting to do extra stuff such as unzipping, # this can fail. It is recommended in this case to spawn off a new # thread that sends a MESSAGE_UNPACK_PROGRESS with no data once a # second starting directly after this time.sleep() call. When you # are finished patching and ready to send, kill the thread before # the MESSAGE_LOAD_PROGRESS message is sent below and then let the # message send normally. time.sleep(1.0) # First, grab a handle to the data itself. fp = open(filename, "rb") gamedata = FileBytes(fp) gamesettings = settings.game_settings.get( filename, GameSettings.default()) # Now, patch with selected patches. patchman = PatchManager([args.patchdir]) for patchfile in gamesettings.enabled_patches: print( f"Applying patch {patchman.patch_name(patchfile)} to game..." ) with open(patchfile, "r") as pp: differences = pp.readlines() differences = [ d.strip() for d in differences if d.strip() ] try: gamedata = BinaryDiff.patch( gamedata, differences) except Exception as e: print( f"Could not patch {filename} with {patchfile}: {str(e)}", file=sys.stderr) # Now, attach any eeprom settings. if gamesettings.force_settings and gamesettings.eeprom is not None: print( f"Applying EEPROM settings to {filename}..." ) patcher = NaomiSettingsPatcher( gamedata, get_default_trojan()) try: patcher.put_eeprom( gamesettings.eeprom, enable_debugging=args.debug_mode, verbose=verbose, ) gamedata = patcher.data except Exception as e: print( f"Could not apply EEPROM settings to {filename}: {str(e)}", file=sys.stderr) # Finally, send it! send_message(netdimm, Message( MESSAGE_LOAD_PROGRESS, struct.pack( "<ii", len(gamedata), 0)), verbose=verbose) selected_file = gamedata break elif msg.id == MESSAGE_LOAD_SETTINGS: index = struct.unpack("<I", msg.data)[0] filename = games[index][0] print( f"Requested settings for {games[index][1]}..." ) send_message(netdimm, Message(MESSAGE_LOAD_SETTINGS_ACK, msg.data), verbose=verbose) # Grab the configured settings for this game. gamesettings = settings.game_settings.get( filename, GameSettings.default()) last_game_selection = index # First, gather up the patches which might be applicable. patchman = PatchManager([args.patchdir]) patchfiles = patchman.patches_for_game( filename) patches = sorted([(p, patchman.patch_name(p)) for p in patchfiles], key=lambda p: p[1]) last_game_patches = patches # Grab any EEPROM settings which might be applicable. parsedsettings = None with open(filename, "rb") as fp: data = FileBytes(fp) eepromdata = gamesettings.eeprom if eepromdata is None: # Possibly they edited the ROM directly, still let them edit the settings. patcher = NaomiSettingsPatcher( data, get_default_trojan()) if patcher.has_eeprom: eepromdata = patcher.get_eeprom() manager = NaomiSettingsManager( get_default_settings_directory()) if eepromdata is None: # We need to make them up from scratch. parsedsettings = manager.from_rom( patcher.rom, region=settings.system_region) else: # We have an eeprom to edit. parsedsettings = manager.from_eeprom( eepromdata) # Now, create the message back to the Naomi. response = struct.pack("<IB", index, len(patches)) for patch in patches: response += struct.pack( "<B", 1 if (patch[0] in gamesettings.enabled_patches) else 0) patchname = patch[1].encode('utf-8')[:255] response += struct.pack( "<B", len(patchname)) + patchname def make_setting( setting: Setting, setting_map: Dict[str, int]) -> bytes: if setting.read_only is True: # We don't encode this setting since its not visible. return struct.pack( "<BI", 0, setting.current or setting.default or 0) settingname = setting.name.encode( 'utf-8')[:255] if len(settingname) == 0: # We can't display this setting, it has no name! return struct.pack( "<BI", 0, setting.current or setting.default or 0) settingdata = struct.pack( "<B", len(settingname)) + settingname if setting.values is not None: settingdata += struct.pack( "<I", len(setting.values)) for val, label in setting.values.items( ): settingdata += struct.pack( "<I", val) valname = label.encode( 'utf-8')[:255] settingdata += struct.pack( "<B", len(valname)) + valname else: settingdata += struct.pack("<I", 0) settingdata += struct.pack( "<I", setting.current or setting.default or 0) if setting.read_only is False: settingdata += struct.pack( "<i", READ_ONLY_NEVER) elif isinstance(setting.read_only, ReadOnlyCondition): settingdata += struct.pack( "<iII", setting_map[ setting.read_only.name], 1 if setting.read_only.negate else 0, len(setting.read_only.values)) for val in setting.read_only.values: settingdata += struct.pack( "<I", val) else: raise Exception("Logic error!") return settingdata if parsedsettings is not None: # Remember the settings we parsed so we can save them later. last_game_parsed_settings = parsedsettings # Now add data for the force settings toggle. totalsettings = len( parsedsettings.system.settings) + len( parsedsettings.game.settings) response += struct.pack( "<B", 1 if (totalsettings > 0 and gamesettings.force_settings) else 0) # Construct system settings. response += struct.pack( "<B", len(parsedsettings.system.settings)) for setting in parsedsettings.system.settings: response += make_setting( setting, { s.name: i for (i, s) in enumerate(parsedsettings. system.settings) }) # Construct game settings response += struct.pack( "<B", len(parsedsettings.game.settings)) for setting in parsedsettings.game.settings: response += make_setting( setting, { s.name: i for (i, s) in enumerate(parsedsettings.game. settings) }) else: # This game has a SRAM chunk attached (atomiswave game), don't try to send settings. response += struct.pack("<BBB", 0, 0, 0) # Send settings over. send_message(netdimm, Message( MESSAGE_LOAD_SETTINGS_DATA, response), verbose=verbose) elif msg.id == MESSAGE_SAVE_SETTINGS_DATA: index, patchlen = struct.unpack( "<IB", msg.data[0:5]) msgdata = msg.data[5:] if index == last_game_selection: filename = games[index][0] gamesettings = settings.game_settings.get( filename, GameSettings.default()) last_game_selection = None print( f"Received updated settings for {games[index][1]}..." ) # Grab the updated patches. if patchlen > 0: patches_enabled = list( struct.unpack( "<" + ("B" * patchlen), msgdata[0:(1 * patchlen)])) msgdata = msgdata[(1 * patchlen):] if patchlen == len(last_game_patches): new_patches: Set[str] = set() for i in range(patchlen): if patches_enabled[i] != 0: new_patches.add( last_game_patches[i] [0]) gamesettings.enabled_patches = new_patches last_game_patches = [] # Grab system settings. force_settings, settinglen = struct.unpack( "<BB", msgdata[0:2]) msgdata = msgdata[2:] if settinglen > 0: settings_values = list( struct.unpack( "<" + ("I" * settinglen), msgdata[0:(4 * settinglen)])) msgdata = msgdata[(4 * settinglen):] if last_game_parsed_settings is not None: if len(settings_values) == len( last_game_parsed_settings. system.settings): for i, setting in enumerate( last_game_parsed_settings .system.settings): setting.current = settings_values[ i] # Grab game settings. settinglen = struct.unpack( "<B", msgdata[0:1])[0] msgdata = msgdata[1:] if settinglen > 0: settings_values = list( struct.unpack( "<" + ("I" * settinglen), msgdata[0:(4 * settinglen)])) msgdata = msgdata[(4 * settinglen):] if last_game_parsed_settings is not None: if len(settings_values) == len( last_game_parsed_settings. game.settings): for i, setting in enumerate( last_game_parsed_settings .game.settings): setting.current = settings_values[ i] if last_game_parsed_settings is not None: manager = NaomiSettingsManager( get_default_settings_directory()) gamesettings.eeprom = manager.to_eeprom( last_game_parsed_settings) gamesettings.force_settings = force_settings != 0 else: gamesettings.force_settings = False last_game_parsed_settings = None # Save the final updates. settings.game_settings[ filename] = gamesettings settings_save(args.menu_settings_file, args.ip, settings) send_message( netdimm, Message(MESSAGE_SAVE_SETTINGS_ACK), verbose=verbose) elif msg.id == MESSAGE_SAVE_CONFIG: if len(msg.data) == SETTINGS_SIZE: ( _, _, analogsetting, _, _, regionsetting, filenamesetting, soundsetting, *rest, ) = struct.unpack("<IIIIIIIIBBBBBBBBBBBB", msg.data[:44]) print("Requested configuration save...") joy1 = [ rest[0], rest[1], rest[4], rest[5], rest[6], rest[7] ] joy2 = [ rest[2], rest[3], rest[8], rest[9], rest[10], rest[11] ] settings.enable_analog = analogsetting != 0 settings.use_filenames = filenamesetting != 0 settings.system_region = NaomiRomRegionEnum( regionsetting) settings.disable_sound = soundsetting != 0 settings.joy1_calibration = joy1 settings.joy2_calibration = joy2 settings_save(args.menu_settings_file, args.ip, settings) send_message( netdimm, Message(MESSAGE_SAVE_CONFIG_ACK), verbose=verbose) elif msg.id == MESSAGE_HOST_STDOUT: print(msg.data.decode('utf-8'), end="") elif msg.id == MESSAGE_HOST_STDERR: print(msg.data.decode('utf-8'), end="", file=sys.stderr) except NetDimmException: # Mark failure so we don't try to wait for power down below. success = False if args.persistent: print("Communicating failed, will try again...") else: print("Communicating failed...") if success and selected_file is not None: try: # Always show game send progress. netdimm = NetDimm(args.ip, log=print) # Only want to send so many progress packets. old_percent = -1 old_time = time.time() def progress_callback(loc: int, size: int) -> None: nonlocal old_percent nonlocal old_time new_percent = int((loc / size) * 100) new_time = time.time() if new_percent != old_percent or (new_time - old_time) > 2.0: write_scratch1_register(netdimm, loc) old_percent = new_percent old_time = new_time # Finally, send it!. netdimm.send(selected_file, disable_crc_check=True, disable_now_loading=True, progress_callback=progress_callback) netdimm.reboot() # And clean up. selected_file.handle.close() selected_file = None except NetDimmException as e: # Mark failure so we don't try to wait for power down below. print(str(e)) success = False if args.persistent: print("Sending game failed, will try again...") else: print("Sending game failed...") if args.persistent: if success: # Wait for cabinet to disappear again before we start the process over. print( "Waiting for cabinet to be power cycled to resend menu...") failure_count: int = 0 on_windows: bool = platform.system() == "Windows" while True: # Constantly ping the net dimm to see if it is still alive. with open(os.devnull, 'w') as DEVNULL: try: if on_windows: call = ["ping", "-n", "1", "-w", "1", args.ip] else: call = ["ping", "-c1", "-W1", args.ip] subprocess.check_call(call, stdout=DEVNULL, stderr=DEVNULL) alive = True except subprocess.CalledProcessError: alive = False # We start with the understanding that the host is up, but if we # miss a ping its not that big of a deal. We just want to know that # we missed multiple pings as that tells us the host is truly gone. if alive: failure_count = 0 else: failure_count += 1 if failure_count >= 5: # We failed 5 pings in a row, so let's assume the host is # dead. break time.sleep(2.0 if failure_count == 0 else 1.0) # Now, wait for the cabinet to come back so we can send the menu again. print("Waiting for cabinet to be ready to receive the menu...") while True: try: netdimm = NetDimm(args.ip, log=print if verbose else None) info = netdimm.info() if info is not None: break except NetDimmException: # Failed to talk to the net dimm, its still down. pass else: # We sent the game, now exit! break return 0
def write_send_status_register(netdimm: NetDimm, value: int) -> None: with netdimm.connection(): netdimm.poke(SEND_STATUS_REGISTER, PeekPokeTypeEnum.TYPE_LONG, checksum_stamp(value, SEND_STATUS_REGISTER_SEED))
def write_scratch2_register(netdimm: NetDimm, value: int) -> None: with netdimm.connection(): netdimm.poke(SCRATCH2_REGISTER, PeekPokeTypeEnum.TYPE_LONG, value)
def read_scratch2_register(netdimm: NetDimm) -> Optional[int]: with netdimm.connection(): return netdimm.peek(SCRATCH2_REGISTER, PeekPokeTypeEnum.TYPE_LONG)
def send_packet(netdimm: NetDimm, data: bytes) -> bool: length = len(data) if length > MAX_PACKET_LENGTH: raise Exception("Packet is too long to send!") with netdimm.connection(): start = time.time() sent_length = False while True: if time.time() - start > MAX_READ_TIMEOUT: # Failed to request a new packet send in time. return False # First, attempt to see if there is any existing transfer in progress. status = read_recv_status_register(netdimm) if status is None: return False # Now, grab the length of the available packet. newlength = (status >> 12) & 0xFFF if newlength == 0: # Ready to start transferring! write_recv_status_register(netdimm, (length << 12) & 0xFFF000) sent_length = True elif sent_length is False or newlength != length: # Cancel old transfer. write_recv_status_register(netdimm, 0) sent_length = False elif newlength == length: # Ready to send data. break else: # Shouldn't be possible. raise Exception("Logic error!") # Now set the current transfer location. This can be rewound by the target # if it failed to receive all of the data. location = 0 while True: while location < length: # Sum up the next amount of data, up to 3 bytes. chunk: int = ((((location // 3) + 1) << 24) & 0xFF000000) for shift in [16, 8, 0]: if location < length: chunk |= (data[location] & 0xFF) << shift location += 1 else: break # Send it. netdimm.poke(DATA_REGISTER, PeekPokeTypeEnum.TYPE_LONG, chunk) # Now, see if the data transfer was successful. status = read_recv_status_register(netdimm) if status is None: # Give up, we can't read from the status. return False # See if the packet was sent successfully. If not, then our location will # be set to where the target needs data sent from. newlength = (status >> 12) & 0xFFF location = status & 0xFFF if newlength == 0 and location == 0: # We succeeded! Time to exit return True elif newlength != length: raise Exception("Logic error!")