"""Interactive zone mapper: lights one LED slot at a time in white (all others off) and records which keys you see lit. Results go to tools/zone_map.json. .venv\\Scripts\\python tools\\zone_walk.py # slots 0..19 .venv\\Scripts\\python tools\\zone_walk.py --start 5 # resume from slot 5 """ import argparse import json import pathlib import threading import time import eve OUT = pathlib.Path(__file__).with_name("zone_map.json") class Streamer(threading.Thread): """Keeps re-sending the current frame so the keyboard never times out.""" def __init__(self, total, fps=10): super().__init__(daemon=True) self.colors = [(0, 0, 0)] * total self.fps = fps self.lock = threading.Lock() self.running = True def set(self, colors): with self.lock: self.colors = list(colors) def run(self): dev = eve.open_device() dev.set_nonblocking(True) try: while self.running: with self.lock: packets = eve.build_packets(self.colors) for p in packets: dev.write(p) while dev.read(eve.REPORT_LEN): pass time.sleep(1 / self.fps) finally: dev.close() def main(): ap = argparse.ArgumentParser() ap.add_argument("--start", type=int, default=0) ap.add_argument("--total", type=int, default=20) args = ap.parse_args() results = json.loads(OUT.read_text()) if OUT.exists() else {} streamer = Streamer(args.total) streamer.start() print("For each slot, type the keys that are lit (e.g. 'Esc, F1-F4, ~ 1 2 Tab Q')") print("Just press Enter if NOTHING is lit. Type q to stop.\n") try: for slot in range(args.start, args.total): colors = [(0, 0, 0)] * args.total colors[slot] = (255, 255, 255) streamer.set(colors) answer = input(f"Slot {slot:2} lit white -> which keys? ").strip() if answer.lower() == "q": break results[str(slot)] = answer or "(nothing)" OUT.write_text(json.dumps(results, indent=2)) finally: streamer.running = False streamer.join(timeout=2) print(f"\nSaved to {OUT}") if __name__ == "__main__": main()