import os import xml.etree.ElementTree as ET import struct import logging from tools import load_constants, find_files_with_sha1, find_xml_files, convert_to_int_or_string, get_int_or_string_value, convert_to_8bit, convert_to_int, get_device_param # Nastavení logování logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) ROM_DIR = 'ROM' XML_DIR = 'Extension' DIR_SAVE = 'MSX' def create_block_entry(constants: dict, block_type: str, address: int, sha1: str = None, param1: int = 0, param2: int = 0, param3: int = 0, file_skip_bytes: int = 0, file_size: int = 0) -> dict: block_entry = { "typ": constants['conf']['BLOCK'], "address": address, "block_typ": constants['block'][block_type], "param1": param1, "param2": param2, "param3": param3 } if sha1: block_entry['SHA1'] = sha1 block_entry['file_size'] = file_size block_entry['file_skip_bytes'] = file_skip_bytes return block_entry def parse_fw_block(root: ET.Element, subslot: int, files_with_sha1: dict, constants: dict) -> list: block = {} result = [] start = int(root.attrib.get("start", 0)) count = int(root.attrib.get("count", 4)) offset = int(root.attrib.get("offset", -1)) fix_offset = int(root.attrib.get("fix_offset", -1)) for element in root: if element.tag in ['SHA1', 'filename', 'device', 'mapper', 'sram', 'ram', 'device_param']: block[element.tag] = get_int_or_string_value(element) if element.tag == 'ram' : pattern = 1; if element.tag == 'device' : device_port = convert_to_8bit(element.attrib.get('port')) device_mask = convert_to_8bit(element.attrib.get('mask')) device_param = convert_to_8bit(element.attrib.get('param','0')) if device_port is not None: if device_mask is None: device_mask = 0xFF #Defaultni hodnota if element.tag == 'SHA1' : file_skip_bytes = convert_to_int(element.attrib.get('skip',0)) file_size = convert_to_int(element.attrib.get('size',0)) else: logger.error(f"Tag name: {element.tag} SKIP. Not expected here") if count > 4 - start: count = 4 - start address = ((subslot & 3) << 4) | ((start & 3) << 2) | ((count - 1) & 3) if 'SHA1' in block: if block['SHA1'] in files_with_sha1: result.append(create_block_entry(constants, 'ROM', address, sha1=block['SHA1'], file_size=file_size, file_skip_bytes=file_skip_bytes)) else: logger.warning(f"Missing ROM. SHA1: {block['SHA1']}") if 'ram' in block: size = convert_to_int(block['ram']) if size : result.append(create_block_entry(constants, 'RAM', address, param1=size//(1024 * 16), param2 = pattern)) # RAM v 16 kB blocích else : logger.warning("SRAM size not defined correctly") if 'mapper' in block: if block['mapper'] in constants['mapper']: param2 = 0 if offset > -1 and offset < 4 : param2 = 0x80 + offset if fix_offset > -1 and fix_offset < 4 : param2 = 0x80 + 0x40 + fix_offset result.append(create_block_entry(constants, 'MAPPER', address, param1=constants['mapper'][block['mapper']], param2=param2)) else: logger.warning(f"Unknown mapper type: {block['mapper']} see file mapper.json") if 'device' in block: if block['device'] in constants['device']: param_dev = block.get('device_param', 0) device_id = constants['device'][block['device']] param_dev = 0 parameter = 0 if (element.attrib): param_dev = get_device_param(constants, block['device'], element.attrib) result.append(create_block_entry(constants, 'DEVICE', address, param1=device_id, param2 = param_dev, param3=parameter)) if device_port is not None: result.append(create_block_entry(constants, 'IO_DEVICE', address, param1=device_port, param2 = device_mask, param3 = device_param)) else: logger.warning(f"Unknown device type: {block['device']}") if 'sram' in block: size = convert_to_int(block['sram']) if size : result.append(create_block_entry(constants, 'SRAM', address, param1=size//1024)) # SRAM v 1 kB else : logger.warning("SRAM size not defined correctly") return result def parse_fw_secondary(root: ET.Element, subslot: int, files_with_sha1: dict, constants: dict) -> list: blocks = [] for element in root: if element.tag == 'block': blocks.extend(parse_fw_block(element, subslot, files_with_sha1, constants)) return blocks def parse_fw(name: str, root: ET.Element, files_with_sha1: dict, constants: dict) -> list: results = [] for element in root: if element.tag == 'secondary': slot = int(element.attrib["slot"]) results.extend(parse_fw_secondary(element, slot, files_with_sha1, constants)) elif element.tag == 'block': results.extend(parse_fw_block(element, 0, files_with_sha1, constants)) elif element.tag == 'io_device': pass # Placeholder for future extensions elif element.tag == 'expander': if element.text == 'expander_wo' : expander = 3 else : expander = 1 result = [] result.append(create_block_entry(constants, 'EXPANDER', 0, param1=expander)) results.extend(result) else: logger.warning(f"Tag name: {element.tag} SKIP. Not expected here") return results def parse_fw_config(root: ET.Element, files_with_sha1: dict, constants: dict) -> dict: results = {} for element in root: if element.tag == 'fw': name = element.attrib["name"] if name not in constants['cart_dev']: logger.warning(f"FW name: {name} SKIP. Not in cart_device.json") continue results[name] = parse_fw(name, element, files_with_sha1, constants) else: logger.warning(f"Unknown tag: {element.tag} SKIP.") continue return results def prepare_roms(config: dict, files_with_sha1: dict) -> dict: roms = {} start = 0 id = 0 try: with open('ROM.bin', 'wb') as rom_file: for key in config: for block in config[key]: if 'SHA1' in block: if block['SHA1'] not in roms: filename = files_with_sha1[block['SHA1']] file_size = block["file_size"] file_skip_bytes = block["file_skip_bytes"] if file_size == 0 : file_size = os.path.getsize(filename) file_blocks = (file_size + 16383) // 16384 with open(filename, 'rb') as source_file: if file_skip_bytes > 0: source_file.seek(file_skip_bytes) data = source_file.read(file_size) rom_file.write(data) current_size = len(data) padding_needed = ((current_size + 16383) // 16384) * 16384 - current_size if padding_needed > 0: rom_file.write(b'\xff' * padding_needed) rom_name = f'{block["file_skip_bytes"]}#{block["file_size"]}#' + block['SHA1'] roms[rom_name] = {"start": start, "blocks": file_blocks, "id": id} id += 1 start += file_blocks * 16384 return roms except IOError as e: logger.error(f"Error writing to ROM.bin: {e}") return {} def set_address(address_array: bytearray, index: int, address: int): if index < 0 or index >= 256: raise ValueError("Index must be in the range 0 to 255.") start = index * 4 address_array[start:start+4] = struct.pack('I', address) def init_address(address_array: bytearray, addr: int): for i in range(256): set_address(address_array, i, addr) def create_fw_config(config: dict, constants: dict, roms: dict) -> tuple: data = bytearray() address_array = bytearray(256 * 4) addr = 0x10 + len(address_array) init_address(address_array, addr) data += struct.pack('BBBBBB', constants['conf']['END'], 0, 0, 0, 0, 0) logger.info(f"CONF addr:{addr:04X} < {constants['conf']['END']:02X} 00 00 00 00 00") addr += 6 for key in config: logger.info(f"NAME: {key} index: {constants['cart_dev'][key]:02X}") set_address(address_array, constants['cart_dev'][key], addr) for conf in config[key]: conf['param1'] = conf.get('param1', 0) conf['param2'] = conf.get('param2', 0) conf['param3'] = conf.get('param3', 0) if conf['typ'] == constants['conf']['BLOCK'] and conf['block_typ'] == constants['block']['ROM']: if 'SHA1' in conf : sha1_name = f'{conf["file_skip_bytes"]}#{conf["file_size"]}#' + conf['SHA1'] if sha1_name in roms: conf['param1'] = roms[sha1_name]['id'] conf['param2'] = (roms[sha1_name]['blocks'] >> 8) & 0xff conf['param3'] = roms[sha1_name]['blocks'] & 0xff else: logger.warning(f"Missing SHA1 in ROM configuration or SHA1 not found in ROMs: {conf}") continue # Formátování číselných parametrů pro logování param1_str = f"{conf['param1']:02X}" if isinstance(conf['param1'], int) else str(conf['param1']) param2_str = f"{conf['param2']:02X}" if isinstance(conf['param2'], int) else str(conf['param2']) param3_str = f"{conf['param3']:02X}" if isinstance(conf['param3'], int) else str(conf['param3']) logger.info(f"CONF addr:{addr:04X} < {conf['typ']:02X} {conf['address']:02X} {conf['block_typ']:02X} {param1_str} {param2_str} {param3_str}") data += struct.pack('BBBBBB', conf['typ'], conf['address'], conf['block_typ'], conf['param1'], conf['param2'], conf['param3']) addr += 6 data += struct.pack('BBBBBB', constants['conf']['END'], 0, 0, 0, 0, 0) addr += 6 return address_array, data, addr def save_fw_config(config: dict, file_name: str, path: str, files_with_sha1: dict, constants: dict, roms: dict, table: bytearray, data: bytearray, rom_start_addr: int): file_path = os.path.join(DIR_SAVE, path) os.makedirs(file_path, exist_ok=True) file_path = os.path.join(file_path, file_name + '.msx') try: with open(file_path, "wb") as outfile: head = struct.pack('BBBBI', ord('M'), ord('s'), ord('X'), 0, rom_start_addr) outfile.write(head) outfile.write(b'\x00' * 8) outfile.write(table) outfile.write(data) addr_rom_start = rom_start_addr + 4 * len(roms) rom_table = bytearray() for rom in roms: rom_table += struct.pack('I', addr_rom_start + roms[rom]['start']) outfile.write(rom_table) with open('ROM.bin', 'rb') as rom_file: rom_content = rom_file.read() outfile.write(rom_content) os.remove('ROM.bin') except IOError as e: logger.error(f"Error saving MSX config to {file_path}: {e}") def create_fw_conf(file_name: str, path: str, files_with_sha1: dict, constants: dict): file_path = os.path.join(XML_DIR, path, file_name + '.xml') try: tree = ET.parse(file_path) root = tree.getroot() if root.tag != "fwConfig": logger.warning(f"File {file_path} is not a valid msxConfig XML. Skipping.") return config = parse_fw_config(root, files_with_sha1, constants) roms = prepare_roms(config, files_with_sha1) table, data, rom_start_addr = create_fw_config(config, constants, roms) save_fw_config(config, file_name, path, files_with_sha1, constants, roms, table, data, rom_start_addr) except (ET.ParseError, FileNotFoundError) as e: logger.error(f"Error processing file {file_path}: {e}") files_with_sha1 = find_files_with_sha1(ROM_DIR) constants = load_constants() xml_files = find_xml_files(XML_DIR) for file_name, path in xml_files: print(f"NAME: {file_name}") create_fw_conf(file_name, path, files_with_sha1, constants)