app
LAN7800 EEPROM Programmer
Public Made by Adomby adom
MPLAB-Connect-parity EEPROM tool for the Microchip LAN7800 USB-3.1 to Gigabit-Ethernet controller (VID 0x0424 / PID 0x7800). Read an adapter's EEPROM to a .bin, edit MAC / VID / PID / bcdDevice / strings / serial and LED/GPIO with a byte-exact preview, and program hardware with verify-after-write. Reads are free; physical writes are gated behind --force-physical-write. Ships an AI-oriented CLI (single source of truth) plus a Hydrogen webapp that shells out to it.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
"""Bulk program mode: program many LAN7800 adapters in one run with an
auto-incrementing identity (MAC + serial), a RESUMABLE manifest, and
verify-after-write.
The sequence math and manifest here are hardware-free and unit-tested. The
per-unit burn reuses the exact LED-mux-gated UsbEepromBackend.program_image +
read-back verify that `program` uses.
SAFETY: a real identity write requires --force-physical-write AND (per the mesh)
a session-scoped allow for the run. The default and the dev/test path is
--dry-run: compute + log the whole sequence and write DRYRUN manifest rows, with
NO device access and NO writes.
"""
from __future__ import annotations
import csv
import json
import os
from datetime import datetime, timezone
from .eeprom import Eeprom, EepromError, parse_mac
# ---- MAC / serial sequence math (pure, no hardware) -----------------------
def mac_to_int(octets) -> int:
v = 0
for b in octets:
v = (v << 8) | b
return v
def int_to_mac(v: int) -> list:
if not 0 <= v <= 0xFFFFFFFFFFFF:
raise EepromError(f"MAC integer out of 48-bit range: 0x{v:012X}")
return [(v >> (8 * (5 - i))) & 0xFF for i in range(6)]
def mac_str(octets) -> str:
return ":".join(f"{b:02X}" for b in octets)
def serial_for(octets, scheme: str = "mac-hex") -> str:
"""Default scheme: iSerial = the MAC as uppercase hex with no separators,
so the serial tracks the MAC 1:1 (matches the factory 00800F780000)."""
if scheme == "mac-hex":
return "".join(f"{b:02X}" for b in octets)
raise EepromError(f"unknown serial scheme {scheme!r} (known: mac-hex)")
def apply_local_admin(octets) -> list:
"""Force the locally-administered bit (0x02) and clear the multicast bit
(0x01) of the first octet -- the '02:'-style prefix option."""
o = list(octets)
o[0] = (o[0] | 0x02) & ~0x01
return o
def sequence(base_mac: str, count: int, step: int = 1,
local_admin: bool = False, scheme: str = "mac-hex") -> list:
"""Compute the full [{index, mac, serial}] plan. Refuses a multicast base,
a run that overflows 48 bits, or one that crosses out of the base OUI
(top 24 bits) -- i.e. NIC space exhausted."""
if count < 1:
raise EepromError("count must be >= 1")
if step == 0:
raise EepromError("step must be non-zero")
base = parse_mac(base_mac)
if local_admin:
base = apply_local_admin(base)
if base[0] & 0x01:
raise EepromError(f"base MAC {mac_str(base)} is MULTICAST (low bit of "
"first octet set); a NIC MAC must be unicast")
base_int = mac_to_int(base)
base_oui = base_int >> 24
out = []
seen = set()
for i in range(count):
v = base_int + i * step
octets = int_to_mac(v) # raises on 48-bit overflow
if (v >> 24) != base_oui:
raise EepromError(
f"unit {i}: MAC {mac_str(octets)} crosses out of the base OUI "
f"{base_oui:06X} (NIC space exhausted) -- reduce count/step or "
"pick a base with more headroom")
m = mac_str(octets)
if m in seen:
raise EepromError(f"internal: duplicate MAC {m} in sequence")
seen.add(m)
out.append({"index": i, "mac": m, "serial": serial_for(octets, scheme)})
return out
# ---- per-unit image build -------------------------------------------------
def build_image(template: Eeprom, mac: str, serial: str,
set_serial: bool = True) -> bytes:
"""Clone the template and stamp this unit's identity. Signature is ensured;
program_image writes byte0 (0xA5) LAST for anti-brick ordering."""
e = Eeprom.from_bytes(template.to_bytes())
e.set_signature()
e.set_mac(mac)
if set_serial:
try:
e.set_string_inplace("serial", serial)
except EepromError as ex:
raise EepromError(
f"cannot stamp serial {serial!r}: {ex}. Provide a --template "
"whose 'serial' string slot fits the 12-char MAC-hex serial.")
return e.to_bytes()
# ---- resumable manifest (CSV + JSON) --------------------------------------
MANIFEST_FIELDS = ["index", "timestamp", "usb_path", "old_mac", "old_serial",
"new_mac", "new_serial", "verify"]
_DONE_STATES = ("PASS", "DRYRUN")
class Manifest:
"""Append-only run record, mirrored to <path>.csv and <path>.json. Resumable:
next_index() skips completed rows and burned_macs() blocks MAC reuse."""
def __init__(self, csv_path: str):
root = os.path.splitext(csv_path)[0]
self.csv_path = root + ".csv" if not csv_path.endswith(".csv") else csv_path
self.json_path = root + ".json"
self.rows = []
self._load()
def _load(self):
if os.path.exists(self.csv_path):
with open(self.csv_path, newline="") as f:
self.rows = [dict(r) for r in csv.DictReader(f)]
def burned_macs(self) -> set:
return {(r.get("new_mac") or "").upper()
for r in self.rows
if str(r.get("verify", "")).upper() in _DONE_STATES
and r.get("new_mac")}
def has_mac(self, mac: str) -> bool:
return mac.upper() in self.burned_macs()
def next_index(self) -> int:
done = [int(r["index"]) for r in self.rows
if str(r.get("verify", "")).upper() in _DONE_STATES]
return (max(done) + 1) if done else 0
def last_burned_mac(self):
"""MAC of the most recent REAL (PASS) burn, for the not-swapped guard.
DRYRUN rows are ignored (a preview never programmed a physical unit)."""
for r in reversed(self.rows):
if str(r.get("verify", "")).upper() == "PASS" and r.get("new_mac"):
return r["new_mac"]
return None
def append(self, *, index, usb_path, old_mac, old_serial,
new_mac, new_serial, verify):
if verify.upper() in _DONE_STATES and self.has_mac(new_mac):
raise EepromError(
f"REFUSING to reuse MAC {new_mac}: already burned in this run "
f"(manifest {self.csv_path}). Each MAC must be unique.")
self.rows.append({
"index": index,
"timestamp": datetime.now(timezone.utc).isoformat(),
"usb_path": usb_path or "",
"old_mac": old_mac or "",
"old_serial": old_serial or "",
"new_mac": new_mac or "",
"new_serial": new_serial or "",
"verify": verify,
})
self._flush()
def _flush(self):
with open(self.csv_path, "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=MANIFEST_FIELDS)
w.writeheader()
for r in self.rows:
w.writerow({k: r.get(k, "") for k in MANIFEST_FIELDS})
with open(self.json_path, "w") as f:
json.dump(self.rows, f, indent=2)