from __future__ import annotations import argparse import json from collections import Counter from collections.abc import Iterable, Mapping from pathlib import Path from typing import Any from .emulator.peripherals.x24164 import ( X24164_FACTORY_DEFAULT_BASE, X24164_FACTORY_DEFAULT_BYTES, X24164_LOGICAL_PAGE_COUNT, X24164_LOGICAL_PAGE_SIZE, factory_default_words_from_rom, ) from .formatting import h16, label_for from .rom import Rom JsonObject = dict[str, Any] DEFAULT_INPUT = Path("build/rom_decompiled.json") DEFAULT_ROM = Path("ROM/M27C512@DIP28_1.BIN") SHADOW_BASE = 0xF400 SHADOW_SIZE = 0x0100 SELECTOR_MAP_BASE = 0xC564 SELECTOR_MAP_COUNT = 0x0200 RECORD_RAM_BASE = 0xF7B0 RECORD_BYTES = 8 STATE_BYTES: tuple[tuple[int, str], ...] = ( (0xF402, "factory_signature_word"), (0xF404, "feature_or_option_flags_candidate"), (0xF730, "connect_display_state_candidate"), (0xF731, "session_latch_candidate"), (0xF732, "display_dispatch_selector_candidate"), (0xF76E, "eeprom_page_and_persist_flags"), (0xF790, "connection_latch_shadow_candidate"), (0xF791, "feature_flag_gate_candidate"), (0xFB03, "report_bridge_suppress_candidate"), ) def load_eeprom_layout_input(path: Path) -> JsonObject: with path.open("r", encoding="utf-8") as handle: payload = json.load(handle) if not isinstance(payload, dict) or "instructions" not in payload: raise ValueError(f"{path} does not look like h8536_decompiler JSON output") return payload def analyze_eeprom_layout(payload: Mapping[str, Any], *, rom_path: Path | None = DEFAULT_ROM) -> JsonObject: rom = _load_rom(rom_path) factory_entries = _factory_entries(rom) selector_map = _selector_map_entries(rom, factory_entries) xrefs = _xref_summary(payload) return { "kind": "eeprom_layout", "summary": { "confidence": "medium-high", "model": ( "The ROM treats the traced P9 bus as two X24164-style EEPROM banks, " "mirrors a 0x100-byte factory/default block into F400-F4FF, and loads " "sixteen 8-byte persistent records into F7B0-F82F at boot." ), "factory_default_count": len(factory_entries), "selector_persistence_mapping_count": len(selector_map), "persistent_record_count": X24164_LOGICAL_PAGE_COUNT, }, "physical_model": _physical_model(), "boot_flow": _boot_flow(), "factory_defaults": { "rom_base": X24164_FACTORY_DEFAULT_BASE, "rom_base_hex": h16(X24164_FACTORY_DEFAULT_BASE), "byte_count": X24164_FACTORY_DEFAULT_BYTES, "shadow_base": SHADOW_BASE, "shadow_base_hex": h16(SHADOW_BASE), "shadow_range_hex": f"{h16(SHADOW_BASE)}-{h16(SHADOW_BASE + SHADOW_SIZE - 1)}", "entries": factory_entries, "notable_entries": _notable_factory_entries(factory_entries, xrefs, selector_map), }, "persistent_records": _persistent_records(factory_entries), "serial_persistence_mapping": { "table_base": SELECTOR_MAP_BASE, "table_base_hex": h16(SELECTOR_MAP_BASE), "entry_count": SELECTOR_MAP_COUNT, "mapped_entry_count": len(selector_map), "entries": selector_map, "offset_histogram": _offset_histogram(selector_map), "high_byte_histogram": _high_byte_histogram(selector_map), "address_formula": ( "command 4 persists to EEPROM address " "(((F76E & 0x0F) << 8) | (mapping_low_byte & 0xFE)) when F76E.7 is set" ), }, "xrefs": xrefs, "state_byte_hints": _state_byte_hints(factory_entries, xrefs), "bench_implications": [ "A live EEPROM dump should first compare F400-F4FF shadow-equivalent offsets against the ROM factory table, especially F402/F404 and mapped offsets.", "Pages 0x0-0xF offset 0x00-0x07 are loaded into F7B0-F82F; page 0 carries the signature/options header and pages 1-F default to spaces, so non-space bytes there are high-value identity/config data.", "Serial command 0/4 can mirror values into F400 offsets selected by the ROM mapping table, but command 4 only persists when the continuation path reaches BD2B and F76E.7 is set.", "F76E is not just a page byte: bit7 gates EEPROM persistence, bit6 suppresses the 48FA dispatch path, and only its low nibble survives into the EEPROM page address.", "CONNECT OK is still likely gated by volatile table/session state as well as EEPROM-backed defaults; EEPROM differences may explain why bench and emulator diverge, but are unlikely to be the whole protocol by themselves.", ], "caveats": [ "The selector map proves where firmware mirrors/persists serial values, not what every field means to the panel UI.", "High bytes in the C564 selector map look structured, but the observed command-0/command-4 paths only use the low byte for F400/EEPROM offsets.", "Indexed F7B0-F82F record consumers can be missed by a static xref pass; dynamic emulator traces should be used once interesting record bytes are found.", ], } def format_text_report(analysis: Mapping[str, Any]) -> str: summary = analysis["summary"] lines = [ "H8/536 EEPROM Layout Report", "", f"Summary: {summary['model']}", f"Confidence: {summary['confidence']}", "", "Physical / Logical Model:", ] physical = analysis.get("physical_model", {}) for row in physical.get("banks", []): lines.append( f"- {row['name']}: logical {row['logical_range_hex']} control {row['control_write_hex']}/{row['control_read_hex']}" ) lines.append(f"- bus: {physical.get('bus', 'unknown')}") lines.append(f"- page model: {physical.get('page_model', 'unknown')}") lines.extend(["", "Boot Flow:"]) for step in analysis.get("boot_flow", []): lines.append(f"- {step['address_hex']} {step['name']}: {step['summary']}") defaults = analysis.get("factory_defaults", {}) lines.extend(["", "Factory Shadow Block:"]) lines.append( f"- ROM {defaults.get('rom_base_hex')} length 0x{int(defaults.get('byte_count', 0)):02X} " f"mirrors to {defaults.get('shadow_range_hex')}" ) for entry in defaults.get("notable_entries", [])[:32]: details = [] if entry.get("ascii"): details.append(f"ascii={entry['ascii']!r}") if entry.get("mapped_selectors_hex"): details.append(f"selectors={', '.join(entry['mapped_selectors_hex'][:8])}") if entry.get("xref_count"): details.append(f"xrefs={entry['xref_count']}") suffix = f" ({'; '.join(details)})" if details else "" lines.append( f"- {entry['shadow_address_hex']} offset {entry['offset_hex']} " f"default {entry['factory_word_hex']}{suffix}" ) lines.extend(["", "Persistent 8-Byte Records:"]) records = analysis.get("persistent_records", []) if records: first = records[0] last = records[-1] lines.append( f"- {len(records)} records: EEPROM {first['eeprom_range_hex']} .. {last['eeprom_range_hex']} " f"load into RAM {first['ram_range_hex']} .. {last['ram_range_hex']}" ) for record in records[:16]: lines.append( f" - record {record['record_index_hex']}: EEPROM {record['eeprom_range_hex']} " f"-> RAM {record['ram_range_hex']} default {record['default_text']!r}" ) mapping = analysis.get("serial_persistence_mapping", {}) lines.extend(["", "Serial Selector -> Shadow/EEPROM Mapping:"]) lines.append( f"- table {mapping.get('table_base_hex')} has {mapping.get('mapped_entry_count', 0)} nonzero low-byte mappings" ) lines.append(f"- formula: {mapping.get('address_formula')}") for entry in mapping.get("entries", [])[:80]: lines.append( f" - selector {entry['selector_hex']} map_word={entry['mapping_word_hex']} " f"-> {entry['shadow_address_hex']} page+{entry['eeprom_word_offset_hex']} " f"default={entry.get('factory_word_hex', 'unknown')}" ) omitted = int(mapping.get("mapped_entry_count", 0)) - min(80, len(mapping.get("entries", []))) if omitted > 0: lines.append(f" - ... {omitted} more mapped selectors omitted") lines.extend(["", "State Byte Hints:"]) for hint in analysis.get("state_byte_hints", []): lines.append( f"- {hint['address_hex']} {hint['name']}: {hint['summary']} " f"(xrefs={hint['xref_count']})" ) lines.extend(["", "Bench Implications:"]) for item in analysis.get("bench_implications", []): lines.append(f"- {item}") lines.extend(["", "Caveats:"]) for item in analysis.get("caveats", []): lines.append(f"- {item}") return "\n".join(lines).rstrip() + "\n" def write_eeprom_layout( input_path: Path, output_path: Path, *, rom_path: Path | None = DEFAULT_ROM, as_json: bool = False, ) -> JsonObject: analysis = analyze_eeprom_layout(load_eeprom_layout_input(input_path), rom_path=rom_path) output_path.parent.mkdir(parents=True, exist_ok=True) if as_json: output_path.write_text(json.dumps(analysis, indent=2, sort_keys=True) + "\n", encoding="utf-8") else: output_path.write_text(format_text_report(analysis), encoding="utf-8") return analysis def main(argv: list[str] | None = None, stdout: Any | None = None) -> int: parser = argparse.ArgumentParser(description="Mine ROM-backed X24164 EEPROM layout and persistence hints.") parser.add_argument("input", nargs="?", type=Path, default=DEFAULT_INPUT) parser.add_argument("--rom", type=Path, default=DEFAULT_ROM, help="ROM binary to mine") parser.add_argument("--json", action="store_true", help="emit structured JSON instead of readable text") parser.add_argument("--out", type=Path, default=None, help="write report to this path") args = parser.parse_args(argv) stream = stdout if stream is None: import sys stream = sys.stdout rom_path = args.rom if args.rom and args.rom.exists() else None analysis = analyze_eeprom_layout(load_eeprom_layout_input(args.input), rom_path=rom_path) rendered = json.dumps(analysis, indent=2, sort_keys=True) + "\n" if args.json else format_text_report(analysis) if args.out: args.out.parent.mkdir(parents=True, exist_ok=True) args.out.write_text(rendered, encoding="utf-8") print(f"wrote {args.out}", file=stream) else: print(rendered, end="", file=stream) return 0 def _load_rom(path: Path | None) -> Rom | None: if path is None or not path.exists(): return None return Rom(path.read_bytes()) def _physical_model() -> JsonObject: return { "bus": "P91/SCL and P97/SDA bit-banged two-wire bus through ROM routines C121/C08B/C0DB/C10C/C142", "page_size": X24164_LOGICAL_PAGE_SIZE, "page_count": X24164_LOGICAL_PAGE_COUNT, "page_model": "16 logical pages of 0x100 bytes; low 8 address bits are sent as the EEPROM word address", "banks": [ { "name": "lower_x24164_candidate", "logical_range": [0x0000, 0x07FF], "logical_range_hex": "0x000-0x7FF", "control_write": 0xA0, "control_write_hex": "A0", "control_read": 0xA1, "control_read_hex": "A1", }, { "name": "upper_x24164_candidate", "logical_range": [0x0800, 0x0FFF], "logical_range_hex": "0x800-0xFFF", "control_write": 0xE0, "control_write_hex": "E0", "control_read": 0xE1, "control_read_hex": "E1", }, ], } def _boot_flow() -> list[JsonObject]: return [ { "address": 0x40BB, "address_hex": h16(0x40BB), "name": "eeprom_boot_gate", "summary": "initializes queue/table scratch, checks P7DR.7, then checks F402 == H'6B6F before trusting persisted state", }, { "address": 0x4103, "address_hex": h16(0x4103), "name": "factory_default_fill", "summary": "copies ROM C964-CA63 into F400-F4FF and writes the same 0x100-byte defaults to each EEPROM page", }, { "address": 0x4187, "address_hex": h16(0x4187), "name": "record_header_blank", "summary": "overwrites page offsets 0x00-0x07 on pages 0x1-0xF with four H'2020 words after factory replication; page 0 keeps the signature/options header", }, { "address": 0x41D2, "address_hex": h16(0x41D2), "name": "persistent_record_load", "summary": "reads page offsets 0x00-0x07 from pages 0x0-0xF into F7B0-F82F; record 0 is a signature/options header, records 1-F are label/identity-like slots", }, { "address": 0xBD2B, "address_hex": h16(0xBD2B), "name": "serial_persist_path", "summary": "command-4 continuation writes the live value into F400+map[selector] and persists it through BFE0 when F76E.7 is set", }, ] def _factory_entries(rom: Rom | None) -> list[JsonObject]: if rom is None: return [] entries = [] for offset, word in factory_default_words_from_rom(rom.data): shadow_address = SHADOW_BASE + offset bytes_pair = [(word >> 8) & 0xFF, word & 0xFF] entries.append( { "offset": offset, "offset_hex": f"0x{offset:02X}", "rom_address": X24164_FACTORY_DEFAULT_BASE + offset, "rom_address_hex": h16(X24164_FACTORY_DEFAULT_BASE + offset), "shadow_address": shadow_address, "shadow_address_hex": h16(shadow_address), "factory_word": word, "factory_word_hex": f"0x{word:04X}", "bytes_hex": " ".join(f"{byte:02X}" for byte in bytes_pair), "ascii": _word_ascii(word), "default_eeprom_word_after_record_blank": 0x2020 if offset < RECORD_BYTES else word, "default_eeprom_word_after_record_blank_hex": "0x2020" if offset < RECORD_BYTES else f"0x{word:04X}", } ) return entries def _selector_map_entries(rom: Rom | None, factory_entries: list[JsonObject]) -> list[JsonObject]: if rom is None: return [] defaults_by_offset = {int(entry["offset"]): entry for entry in factory_entries} entries: list[JsonObject] = [] for selector in range(SELECTOR_MAP_COUNT): address = SELECTOR_MAP_BASE + selector * 2 if not rom.contains(address, 2): break word = rom.u16(address) low = word & 0xFF if low == 0: continue aligned = low & 0xFE shadow_address = SHADOW_BASE + low aligned_shadow_address = SHADOW_BASE + aligned default = defaults_by_offset.get(aligned) entry: JsonObject = { "selector": selector, "selector_hex": f"0x{selector:03X}", "entry_address": address, "entry_address_hex": h16(address), "mapping_word": word, "mapping_word_hex": f"0x{word:04X}", "mapping_high_byte": (word >> 8) & 0xFF, "mapping_high_byte_hex": f"0x{(word >> 8) & 0xFF:02X}", "mapping_low_byte": low, "mapping_low_byte_hex": f"0x{low:02X}", "shadow_offset": low, "shadow_offset_hex": f"0x{low:02X}", "shadow_address": shadow_address, "shadow_address_hex": h16(shadow_address), "aligned_shadow_address": aligned_shadow_address, "aligned_shadow_address_hex": h16(aligned_shadow_address), "eeprom_word_offset": aligned, "eeprom_word_offset_hex": f"0x{aligned:02X}", "command0_shadow_mirror": True, "command4_shadow_mirror": True, "command4_persist_when_f76e_bit7": True, } if default is not None: entry["factory_word"] = default["factory_word"] entry["factory_word_hex"] = default["factory_word_hex"] entry["factory_ascii"] = default["ascii"] entries.append(entry) return entries def _notable_factory_entries( factory_entries: list[JsonObject], xrefs: Mapping[str, Any], selector_map: list[JsonObject], ) -> list[JsonObject]: direct_counts: Counter[int] = Counter() for item in xrefs.get("direct_xrefs", []): if not isinstance(item, Mapping) or not isinstance(item.get("address"), int): continue address = int(item["address"]) if SHADOW_BASE <= address < SHADOW_BASE + SHADOW_SIZE: direct_counts[address & 0xFF] += 1 selectors_by_offset: dict[int, list[str]] = {} for entry in selector_map: selectors_by_offset.setdefault(int(entry["eeprom_word_offset"]), []).append(str(entry["selector_hex"])) notable = [] for entry in factory_entries: offset = int(entry["offset"]) word = int(entry["factory_word"]) mapped = selectors_by_offset.get(offset, []) xref_count = direct_counts.get(offset, 0) if word in (0x0000, 0xFFFF) and not mapped and not xref_count and offset not in (0x02, 0x04): continue item = dict(entry) item["xref_count"] = xref_count item["mapped_selectors_hex"] = mapped[:24] notable.append(item) return notable def _persistent_records(factory_entries: list[JsonObject]) -> list[JsonObject]: factory_by_offset = {int(entry["offset"]): int(entry["factory_word"]) for entry in factory_entries} records = [] for index in range(X24164_LOGICAL_PAGE_COUNT): eeprom_base = index * X24164_LOGICAL_PAGE_SIZE ram_base = RECORD_RAM_BASE + index * RECORD_BYTES default_words = [ factory_by_offset.get(offset, 0xFFFF) if index == 0 else 0x2020 for offset in range(0, RECORD_BYTES, 2) ] default_bytes = bytearray() for word in default_words: default_bytes.extend([(word >> 8) & 0xFF, word & 0xFF]) records.append( { "record_index": index, "record_index_hex": f"0x{index:X}", "eeprom_base": eeprom_base, "eeprom_base_hex": f"0x{eeprom_base:03X}", "eeprom_range_hex": f"0x{eeprom_base:03X}-0x{eeprom_base + RECORD_BYTES - 1:03X}", "ram_base": ram_base, "ram_base_hex": h16(ram_base), "ram_range_hex": f"{h16(ram_base)}-{h16(ram_base + RECORD_BYTES - 1)}", "word_offsets": [0, 2, 4, 6], "default_words": default_words, "default_words_hex": [f"0x{word:04X}" for word in default_words], "default_bytes_hex": default_bytes.hex(" ").upper(), "default_text": _ascii(default_bytes), "role_candidate": "page-0 signature/options header" if index == 0 else "8-byte persistent label/identity slot", } ) return records def _offset_histogram(entries: Iterable[Mapping[str, Any]]) -> list[JsonObject]: counts = Counter(int(entry["eeprom_word_offset"]) for entry in entries) return [ {"offset": offset, "offset_hex": f"0x{offset:02X}", "selector_count": count} for offset, count in sorted(counts.items()) ] def _high_byte_histogram(entries: Iterable[Mapping[str, Any]]) -> list[JsonObject]: counts = Counter(int(entry["mapping_high_byte"]) for entry in entries) return [ {"high_byte": value, "high_byte_hex": f"0x{value:02X}", "selector_count": count} for value, count in sorted(counts.items()) ] def _state_byte_hints(factory_entries: list[JsonObject], xrefs: Mapping[str, Any]) -> list[JsonObject]: defaults = {int(entry["shadow_address"]): entry for entry in factory_entries} refs_by_address: Counter[int] = Counter() examples_by_address: dict[int, list[JsonObject]] = {} for item in xrefs.get("direct_xrefs", []): if not isinstance(item, Mapping) or not isinstance(item.get("address"), int): continue address = int(item["address"]) refs_by_address[address] += 1 examples_by_address.setdefault(address, []).append(dict(item)) hints = [] for address, name in STATE_BYTES: default = defaults.get(address) summary = _state_summary(address, default) hints.append( { "address": address, "address_hex": h16(address), "name": name, "summary": summary, "factory_word_hex": default.get("factory_word_hex") if default else None, "xref_count": refs_by_address.get(address, 0), "xrefs": examples_by_address.get(address, [])[:12], } ) return hints def _state_summary(address: int, default: Mapping[str, Any] | None) -> str: factory = f"factory {default['factory_word_hex']}" if default else "volatile/no factory word" if address == 0xF402: return f"{factory}; boot accepts persisted state only when this word is H'6B6F" if address == 0xF404: return f"{factory}; bits 1-4 are tested with F791 gates in display/status routines" if address == 0xF76E: return "bit7 enables command-4 EEPROM persistence, bit6 suppresses 48FA dispatch, low nibble selects EEPROM page" if address == 0xF791: return "volatile gate tested alongside F404 option bits before setting report/display flags" if address == 0xF732: return "volatile display dispatch selector feeding the 493E pointer table and 48FA report bridge" return factory def _xref_summary(payload: Mapping[str, Any]) -> JsonObject: instructions = _instruction_sequence(payload.get("instructions")) functions = _function_ranges(payload) direct = [] dynamic = [] for ins in instructions: for ref in _references(ins): if _is_interesting_address(ref): direct.append(_xref_item(ins, ref, functions)) operands = str(ins.get("operands", "")) dynamic_kind = _dynamic_kind(operands) if dynamic_kind: item = _base_instruction_item(ins, functions) item["kind"] = dynamic_kind item["operand"] = operands dynamic.append(item) return { "direct_xref_count": len(direct), "dynamic_xref_count": len(dynamic), "direct_xrefs": direct, "dynamic_xrefs": dynamic, } def _xref_item(ins: Mapping[str, Any], address: int, functions: list[JsonObject]) -> JsonObject: item = _base_instruction_item(ins, functions) item.update( { "address": address, "address_hex": h16(address), "region": _region_for_address(address), "access": _access_direction(ins, address) or "read_write_candidate", } ) return item def _base_instruction_item(ins: Mapping[str, Any], functions: list[JsonObject]) -> JsonObject: address = int(ins["address"]) function = _function_for_address(functions, address) item: JsonObject = { "instruction_address": address, "instruction_address_hex": h16(address), "mnemonic": str(ins.get("mnemonic", "")), "operands": str(ins.get("operands", "")), "instruction": str(ins.get("text") or _instruction_text(ins)), } if function: item["function_start"] = function["start"] item["function_start_hex"] = h16(int(function["start"])) item["function_label"] = function["label"] return item def _is_interesting_address(address: int) -> bool: return ( SHADOW_BASE <= address < SHADOW_BASE + SHADOW_SIZE or RECORD_RAM_BASE <= address < RECORD_RAM_BASE + X24164_LOGICAL_PAGE_COUNT * RECORD_BYTES or any(address == item[0] for item in STATE_BYTES) ) def _region_for_address(address: int) -> str: if SHADOW_BASE <= address < SHADOW_BASE + SHADOW_SIZE: return "f400_shadow_defaults" if RECORD_RAM_BASE <= address < RECORD_RAM_BASE + X24164_LOGICAL_PAGE_COUNT * RECORD_BYTES: return "persistent_record_ram" return "state_or_gate_ram" def _dynamic_kind(operands: str) -> str | None: patterns = ( ("@(-H'0C00", "indexed_f400_shadow_access"), ("@(-H'0850", "indexed_persistent_record_store"), ("@(-H'084E", "indexed_persistent_record_store"), ("@(-H'084C", "indexed_persistent_record_store"), ("@(-H'084A", "indexed_persistent_record_store"), ("@(-H'3A9C", "selector_to_shadow_mapping_word"), ("@(-H'3A9B", "selector_to_shadow_mapping_low_byte"), ) upper = operands.upper() for pattern, kind in patterns: if pattern in upper: return kind return None def _function_ranges(payload: Mapping[str, Any]) -> list[JsonObject]: call_graph = payload.get("call_graph") if not isinstance(call_graph, Mapping): return [] nodes = call_graph.get("nodes") if not isinstance(nodes, list): return [] ranges: list[JsonObject] = [] for node in nodes: if not isinstance(node, Mapping): continue start = node.get("start") end = node.get("end") if isinstance(start, int) and isinstance(end, int): ranges.append({"start": start, "end": end, "label": str(node.get("label") or label_for(start))}) return sorted(ranges, key=lambda item: int(item["start"])) def _function_for_address(functions: list[JsonObject], address: int) -> JsonObject | None: for function in functions: if int(function["start"]) <= address <= int(function["end"]): return function return None def _instruction_sequence(value: object) -> list[JsonObject]: if isinstance(value, Mapping): values: Iterable[Any] = value.values() elif isinstance(value, list): values = value else: values = [] return sorted( [item for item in values if isinstance(item, dict) and isinstance(item.get("address"), int)], key=lambda item: int(item["address"]), ) def _references(ins: Mapping[str, Any]) -> list[int]: refs = ins.get("references", []) if not isinstance(refs, list): return [] output: list[int] = [] for ref in refs: if isinstance(ref, Mapping) and isinstance(ref.get("address"), int): output.append(int(ref["address"])) elif isinstance(ref, int): output.append(ref) return output def _access_direction(ins: Mapping[str, Any], address: int) -> str | None: root = _mnemonic_root(str(ins.get("mnemonic", ""))) if root in {"BTST", "CMP", "CMP:E", "CMP:G", "CMP:I", "MOVFPE", "TST"}: return "read" if root in {"BCLR", "BNOT", "BSET", "CLR", "INC", "INC:G", "NEG", "NOT"}: return "write" if root in {"ADD:Q", "ADD:G", "ADDS", "ADDX", "AND", "OR", "SUB", "SUBS", "SUBX", "XOR"}: return "write" if root in {"MOV:G", "MOV:S", "MOVTPE"}: source, destination = _source_destination_operands(str(ins.get("operands", ""))) if _operand_mentions_address(destination, address): return "write" if _operand_mentions_address(source, address): return "read" return None def _source_destination_operands(operands: str) -> tuple[str, str]: depth = 0 split_at: int | None = None for index, char in enumerate(operands): if char in "({": depth += 1 elif char in ")}" and depth: depth -= 1 elif char == "," and depth == 0: split_at = index if split_at is None: operand = operands.strip() return "", operand return operands[:split_at].strip(), operands[split_at + 1 :].strip() def _operand_mentions_address(operand: str, address: int) -> bool: operand_upper = operand.upper().replace(" ", "") negative = (0x10000 - address) & 0xFFFF return ( f"H'{address:04X}" in operand_upper or f"0X{address:04X}" in operand_upper or f"${address:04X}" in operand_upper or f"-H'{negative:04X}" in operand_upper or f"-0X{negative:04X}" in operand_upper or f"-${negative:04X}" in operand_upper ) def _instruction_text(ins: Mapping[str, Any]) -> str: operands = str(ins.get("operands", "")) return f"{ins.get('mnemonic', '')} {operands}".strip() def _mnemonic_root(mnemonic: str) -> str: return mnemonic.rsplit(".", 1)[0].upper() def _word_ascii(word: int) -> str: chars = [(word >> 8) & 0xFF, word & 0xFF] if all(0x20 <= byte <= 0x7E for byte in chars): return "".join(chr(byte) for byte in chars) if any(0x20 <= byte <= 0x7E for byte in chars): return "".join(chr(byte) if 0x20 <= byte <= 0x7E else "." for byte in chars) return "" def _ascii(data: bytes | bytearray) -> str: return "".join(chr(value) if 0x20 <= value <= 0x7E else "." for value in data) __all__ = [ "analyze_eeprom_layout", "format_text_report", "load_eeprom_layout_input", "main", "write_eeprom_layout", ]