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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
"""ethtool backend — the RECOMMENDED physical path (in-tree lan78xx driver).
Read: `ethtool -e <iface> raw on` -> full image on stdout (bytes)
Write: `ethtool -E <iface> magic 0x78A5 offset <o> value <v>` (one byte / call)
The driver issues EWEN internally on the -E path, so we never manage EWEN/EWDS
here. Per-byte writes are how the hardware works; the CLI batches a field's
bytes and calls write_bytes(), which loops one `ethtool -E` per byte.
This backend is `physical = True`; the CLI MUST escalate before any write.
Reads are free. We never call ethtool here from any read path other than
read_image(), and read_image() is non-destructive.
"""
from __future__ import annotations
import shutil
import subprocess
from .base import EepromBackend, BackendError
from ..eeprom import EEPROM_MAGIC, MAX_EEPROM_SIZE
class EthtoolBackend(EepromBackend):
physical = True
name = "ethtool"
def __init__(self, iface: str, size: int = MAX_EEPROM_SIZE, sudo: bool = True):
self.iface = iface
self.size = size
self.sudo = sudo
if shutil.which("ethtool") is None:
raise BackendError("ethtool not found on PATH; install ethtool")
def _cmd(self, args: list[str]) -> list[str]:
# sudo -n (non-interactive): if passwordless ethtool is configured it
# works; otherwise it fails FAST instead of hanging on a password prompt
# (there is no TTY). Callers treat that failure as "ethtool unavailable".
base = ["sudo", "-n"] if self.sudo else []
return base + ["ethtool"] + args
def read_image(self) -> bytes:
# `-e <iface> raw on` streams the raw EEPROM bytes to stdout.
proc = subprocess.run(
self._cmd(["-e", self.iface, "raw", "on"]),
capture_output=True)
if proc.returncode != 0:
raise BackendError(self._clean_err(proc.stderr, proc.returncode))
return proc.stdout
@staticmethod
def _clean_err(stderr: bytes, rc) -> str:
"""Map raw ethtool/sudo stderr to a clean, non-leaky message."""
err = stderr.decode(errors="replace").strip()
low = err.lower()
if "enodata" in low or "no data" in low:
return ("EEPROM reads as blank/unsigned (byte0 != 0xA5); "
"no valid image present")
if "sudo" in low or "password" in low or "terminal is required" in low:
return ("ethtool needs elevated access to read the EEPROM; "
"grant passwordless sudo for ethtool or run with privilege")
if "no such device" in low or "no device" in low:
return "no such network device (is the lan78xx netdev bound?)"
if "operation not permitted" in low:
return "operation not permitted; ethtool needs elevated access"
return f"ethtool error (rc={rc})"
def read_range(self, offset: int, length: int) -> bytes:
proc = subprocess.run(
self._cmd(["-e", self.iface, "raw", "on",
"offset", str(offset), "length", str(length)]),
capture_output=True)
if proc.returncode != 0:
raise BackendError(self._clean_err(proc.stderr, proc.returncode))
return proc.stdout
def write_bytes(self, offset: int, values: bytes) -> None:
for i, v in enumerate(values):
o = offset + i
proc = subprocess.run(
self._cmd(["-E", self.iface,
"magic", hex(EEPROM_MAGIC),
"offset", str(o), "value", str(v)]),
capture_output=True)
if proc.returncode != 0:
raise BackendError(
f"write failed at offset {o}: "
f"{self._clean_err(proc.stderr, proc.returncode)}")
def reload(self) -> None:
# No safe in-band reload; require a replug / usb reset. Surface clearly.
raise BackendError(
"reload the EEPROM by replugging the adapter or issuing a USB reset; "
"the driver does not expose a safe RELOAD on this path")