Differences

This shows you the differences between two versions of the page.

Link to this comparison view

iothings:laboratoare:2025_code:lab11_1 [2025/12/15 16:58] (current)
dan.tudose created
Line 1: Line 1:
 +<code python sparrow-serial.py>​
 +#​!/​usr/​bin/​env python3
 +"""​
 +Sparrow Serial Watcher (macOS, ESP32-C6 native USB) - v6
  
 +What this solves:
 +- ESP32-C6 native USB Serial/JTAG on macOS often disconnects and re-enumerates on reset.
 +- After a hard reset, console output may resume only after a host "​touch"​.
 +- west espressif monitor (idf_monitor) appears to do a helpful open/​reset/​line-state dance.
 +- This script emulates a safe subset of that behavior.
 +
 +Key behaviors:
 +- Only considers /​dev/​*usbmodem* ports by default (macOS-safe).
 +- Avoids connecting to debug-console/​Bluetooth ports.
 +- On reconnect, performs a gentle DTR/RTS "​kick"​ (unless --no-kick).
 +- Uses settle + probe before accepting a port.
 +- Optional pinned port mode.
 +- Graceful Ctrl+C exit.
 +
 +Usage:
 +  python3 sparrow-serial.py
 +  python3 sparrow-serial.py --port /​dev/​cu.usbmodem1101
 +  python3 sparrow-serial.py --no-kick
 +  python3 sparrow-serial.py --settle-ms 1200 --probe-ms 2500
 +"""​
 +
 +import argparse
 +import time
 +import sys
 +
 +try:
 +    import serial
 +    from serial.tools import list_ports
 +except ImportError:​
 +    print("​Missing dependency: pyserial"​)
 +    print("​Install with: pip install pyserial"​)
 +    sys.exit(1)
 +
 +ESPRESSIF_VID = 0x303A
 +MIN_SCORE = 8
 +
 +
 +def score_port(p) -> int:
 +    """​Heuristic scoring for selecting the most likely ESP32-C6 USB Serial/JTAG port."""​
 +    score = 0
 +    dev = (p.device or ""​).lower()
 +    desc = (p.description or ""​).lower()
 +    manuf = (p.manufacturer or ""​).lower()
 +    hwid = (p.hwid or ""​).lower()
 +
 +    if "​usbmodem"​ in dev:
 +        score += 6
 +
 +    # Vendor hint if OS reports VID
 +    if getattr(p, "​vid",​ None) == ESPRESSIF_VID:​
 +        score += 10
 +
 +    # Text hints (vary across macOS builds)
 +    if "​espressif"​ in manuf or "​espressif"​ in desc or "​espressif"​ in hwid:
 +        score += 8
 +
 +    if "usb jtag" in desc or "​jtag"​ in desc or "​jtag"​ in hwid:
 +        score += 4
 +
 +    # Mild preference for cu.*
 +    if dev.startswith("/​dev/​cu."​):​
 +        score += 1
 +
 +    return score
 +
 +
 +def list_usbmodem_candidates():​
 +    """​Return only usbmodem* candidates (macOS-safe)."""​
 +    ports = list(list_ports.comports())
 +    return [p for p in ports if "​usbmodem"​ in (p.device or ""​).lower()]
 +
 +
 +def pick_best_usbmodem():​
 +    ports = list_usbmodem_candidates()
 +
 +    if not ports:
 +        print("​[scan] no candidates"​)
 +        return None
 +
 +    ranked = sorted(ports,​ key=score_port,​ reverse=True)
 +
 +    print("​[scan] top candidates:"​)
 +    for p in ranked[:5]:
 +        print(f" ​ - {p.device} (score={score_port(p)},​ desc={p.description},​ manuf={p.manufacturer})"​)
 +
 +    best = ranked[0]
 +    if score_port(best) < MIN_SCORE:
 +        return None
 +
 +    return best.device
 +
 +
 +def normalize_tty_cu(device:​ str):
 +    """​Given /​dev/​cu.usbmodemXXXX,​ return both cu and tty variants."""​
 +    if device.startswith("/​dev/​cu."​):​
 +        suffix = device[len("/​dev/​cu."​):​]
 +        return [device, f"/​dev/​tty.{suffix}"​]
 +    if device.startswith("/​dev/​tty."​):​
 +        suffix = device[len("/​dev/​tty."​):​]
 +        return [f"/​dev/​cu.{suffix}",​ device]
 +    return [device]
 +
 +
 +def try_open(dev:​ str, baud: int):
 +    try:
 +        ser = serial.Serial(dev,​ baudrate=baud,​ timeout=0.15)
 +        time.sleep(0.05) ​ # small settle after open
 +        return ser
 +    except Exception:
 +        return None
 +
 +
 +def kick_lines(ser):​
 +    """​
 +    Best-effort '​wake'​ sequence.
 +    On native USB Serial/JTAG this may or may not be honored,
 +    but empirically helps on macOS after hard resets.
 +    """​
 +    try:
 +        ser.dtr = False
 +        ser.rts = False
 +        time.sleep(0.05)
 +
 +        ser.dtr = True
 +        time.sleep(0.05)
 +        ser.dtr = False
 +        time.sleep(0.05)
 +
 +        ser.rts = True
 +        time.sleep(0.05)
 +        ser.rts = False
 +        time.sleep(0.05)
 +
 +        ser.dtr = True
 +        ser.rts = False
 +    except Exception:
 +        pass
 +
 +
 +def probe_for_data(ser,​ probe_ms: int) -> bool:
 +    """​Return True if any bytes arrive within the probe window."""​
 +    deadline = time.time() + probe_ms / 1000.0
 +    try:
 +        while time.time() < deadline:
 +            data = ser.read(128)
 +            if data:
 +                return True
 +            time.sleep(0.02)
 +    except Exception:
 +        return False
 +    return False
 +
 +
 +def open_with_probe(device:​ str, baud: int, probe_ms: int):
 +    """​
 +    Try opening cu/tty variants and only accept if we see data.
 +    """​
 +    for dev in normalize_tty_cu(device):​
 +        ser = try_open(dev,​ baud)
 +        if not ser:
 +            continue
 +
 +        if probe_for_data(ser,​ probe_ms):
 +            print(f"​[connected] {dev} @ {baud}"​)
 +            return ser, dev
 +
 +        try:
 +            ser.close()
 +        except Exception:
 +            pass
 +
 +    return None, None
 +
 +
 +def settle_present(port:​ str, settle_ms: int) -> bool:
 +    """​True if port is currently listed by OS; waits settle time if present."""​
 +    for p in list_ports.comports():​
 +        if p.device == port:
 +            time.sleep(settle_ms / 1000.0)
 +            return True
 +    return False
 +
 +
 +def do_kick_cycle(port:​ str, baud: int, settle_ms: int):
 +    """​
 +    Open -> kick -> close. Used after hard reset when output
 +    often resumes only after a host "​touch"​.
 +    """​
 +    if not settle_present(port,​ settle_ms):
 +        return
 +
 +    for dev in normalize_tty_cu(port):​
 +        ser = try_open(dev,​ baud)
 +        if not ser:
 +            continue
 +        print(f"​[kick] touching {dev}"​)
 +        kick_lines(ser)
 +        try:
 +            ser.close()
 +        except Exception:
 +            pass
 +        time.sleep(0.1)
 +        return
 +
 +
 +def read_lines(ser):​
 +    """​Read and print lines until disconnect or Ctrl+C."""​
 +    buf = b""​
 +    while True:
 +        data = ser.read(256)
 +        if not data:
 +            continue
 +        buf += data
 +        while b"​\n"​ in buf:
 +            line, buf = buf.split(b"​\n",​ 1)
 +            try:
 +                print(line.decode(errors="​replace"​))
 +            except Exception:
 +                print(line)
 +
 +
 +def auto_connect(baud:​ int, settle_ms: int, probe_ms: int, kick: bool):
 +    while True:
 +        device = pick_best_usbmodem()
 +        if not device:
 +            print("​[waiting] no Espressif usbmodem console detected yet")
 +            time.sleep(0.4)
 +            continue
 +
 +        # Give the OS time to finish re-enumeration
 +        time.sleep(settle_ms / 1000.0)
 +
 +        if kick:
 +            do_kick_cycle(device,​ baud, settle_ms)
 +
 +        ser, _opened = open_with_probe(device,​ baud, probe_ms)
 +        if ser:
 +            return ser
 +
 +        print("​[waiting] candidate present but no data yet")
 +        time.sleep(0.4)
 +
 +
 +def pinned_connect(port:​ str, baud: int, settle_ms: int, probe_ms: int, kick: bool):
 +    while True:
 +        if not settle_present(port,​ settle_ms):
 +            print(f"​[waiting] {port} not present yet")
 +            time.sleep(0.3)
 +            continue
 +
 +        if kick:
 +            do_kick_cycle(port,​ baud, settle_ms)
 +
 +        ser, _opened = open_with_probe(port,​ baud, probe_ms)
 +        if ser:
 +            return ser
 +
 +        print(f"​[waiting] {port} present but no data yet")
 +        time.sleep(0.4)
 +
 +
 +def main():
 +    ap = argparse.ArgumentParser()
 +    ap.add_argument("​--baud",​ type=int, default=115200)
 +    ap.add_argument("​--port",​ type=str, default="",​
 +                    help="​Optional:​ pin to an exact usbmodem port."​)
 +    # Updated stable defaults for your Sparrow/​macOS setup:
 +    ap.add_argument("​--settle-ms",​ type=int, default=900,​
 +                    help="​Wait after port appears before attempting open."​)
 +    ap.add_argument("​--probe-ms",​ type=int, default=2000,​
 +                    help="​How long to wait for first bytes before accepting port."​)
 +    ap.add_argument("​--no-kick",​ action="​store_true",​
 +                    help="​Disable the host-side '​kick'​ cycle."​)
 +
 +    args = ap.parse_args()
 +    kick = not args.no_kick
 +
 +    ser = None
 +
 +    try:
 +        while True:
 +            try:
 +                if args.port:
 +                    ser = pinned_connect(args.port,​ args.baud, args.settle_ms,​ args.probe_ms,​ kick)
 +                else:
 +                    ser = auto_connect(args.baud,​ args.settle_ms,​ args.probe_ms,​ kick)
 +
 +                read_lines(ser)
 +
 +            except (serial.SerialException,​ OSError) as e:
 +                print(f"​[disconnected] {e}")
 +                try:
 +                    if ser:
 +                        ser.close()
 +                except Exception:
 +                    pass
 +                time.sleep(0.2)
 +
 +    except KeyboardInterrupt:​
 +        print("​\n[exit] serial monitor stopped."​)
 +        try:
 +            if ser:
 +                ser.close()
 +        except Exception:
 +            pass
 +        return
 +
 +
 +if __name__ == "​__main__":​
 +    main()
 +
 +</​code>​
iothings/laboratoare/2025_code/lab11_1.txt · Last modified: 2025/12/15 16:58 by dan.tudose
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0