#!/usr/bin/env python3
"""
Lab 1 - thermal · GAUGE LOGGER  (runs on jedas, not on a board)
school.worhl.net

Reads the gauge board's serial line, stamps each reading with wall-clock
time, and appends it to a CSV. Prints a live status line so you find out
in the first minute if a sensor is dead, not in the fortieth.

    The regulating scalar never becomes a packet.

This program observes. It never writes to the gauge and has no path to
any node. If it crashes, the bath does not notice.

EVENT MARKERS - the point of the perturbation runs:
    Type a note and press Enter at any time. It is written into the log
    with the current timestamp and drawn as a labelled vertical line by
    plot_settling.py. This is what turns a curve into an argument.

        killed node 3
        plugged node 8 back in
        hair dryer puff through the notch
        cracked the lid

Usage:
    python3 gauge_logger.py                     # auto-detect the port
    python3 gauge_logger.py --port /dev/ttyACM0
    python3 gauge_logger.py --out run2.csv
    python3 gauge_logger.py --list              # show candidate ports

Stop with Ctrl-C. The CSV is flushed after every row, so a power cut
costs you the last line, not the run.

Requires: pyserial   (pip install --user pyserial)
"""

import argparse
import csv
import glob
import queue
import sys
import threading
import time
from datetime import datetime

try:
    import serial
except ImportError:
    sys.exit("pyserial is not installed.  pip install --user pyserial")


BAUD = 115200
RECONNECT_S = 2.0
PORT_GLOBS = ("/dev/ttyACM*", "/dev/ttyUSB*")


def candidate_ports():
    found = []
    for pattern in PORT_GLOBS:
        found.extend(sorted(glob.glob(pattern)))
    return found


def pick_port(requested):
    if requested:
        return requested
    ports = candidate_ports()
    if not ports:
        sys.exit("No serial ports found matching %s.\n"
                 "Is the gauge plugged in? Are you in the dialout group?"
                 % ", ".join(PORT_GLOBS))
    if len(ports) > 1:
        print("# several ports present: %s" % ", ".join(ports))
        print("# using %s  (override with --port)" % ports[0])
    return ports[0]


def stdin_reader(q):
    """Feed typed lines to the main loop without blocking it."""
    for line in sys.stdin:
        line = line.strip()
        if line:
            q.put(line)


def parse_reading(line):
    """Return (uptime_ms, celsius) or None for comments and junk."""
    line = line.strip()
    if not line or line.startswith("#"):
        return None
    parts = line.split(",")
    if len(parts) != 2:
        return None
    try:
        return int(parts[0]), float(parts[1])
    except ValueError:
        return None


def main():
    ap = argparse.ArgumentParser(description="Log the lab 1 gauge to CSV.")
    ap.add_argument("--port", help="serial device (default: auto-detect)")
    ap.add_argument("--out", default=None,
                    help="CSV path (default: gauge_YYYYmmdd_HHMMSS.csv)")
    ap.add_argument("--baud", type=int, default=BAUD)
    ap.add_argument("--list", action="store_true",
                    help="list candidate serial ports and exit")
    args = ap.parse_args()

    if args.list:
        ports = candidate_ports()
        print("\n".join(ports) if ports else "(none found)")
        return

    port = pick_port(args.port)
    out_path = args.out or datetime.now().strftime("gauge_%Y%m%d_%H%M%S.csv")

    started = time.time()
    started_iso = datetime.now().isoformat(timespec="seconds")

    events = queue.Queue()
    threading.Thread(target=stdin_reader, args=(events,), daemon=True).start()

    print("# logging %s -> %s" % (port, out_path))
    print("# started %s" % started_iso)
    print("# type a note + Enter to mark an event; Ctrl-C to stop")

    fh = open(out_path, "w", newline="")
    writer = csv.writer(fh)
    writer.writerow(["wall_time", "elapsed_s", "uptime_ms", "celsius", "event"])
    fh.flush()

    def write_row(uptime=None, celsius=None, event=""):
        now = time.time()
        writer.writerow([
            datetime.fromtimestamp(now).isoformat(timespec="milliseconds"),
            "%.3f" % (now - started),
            "" if uptime is None else uptime,
            "" if celsius is None else "%.3f" % celsius,
            event,
        ])
        fh.flush()

    write_row(event="log started")

    samples = 0
    ser = None

    try:
        while True:
            # --- drain typed event markers -------------------------------
            while True:
                try:
                    note = events.get_nowait()
                except queue.Empty:
                    break
                write_row(event=note)
                print("  [%s] MARK: %s"
                      % (time.strftime("%H:%M:%S"), note))

            # --- keep the serial link up ---------------------------------
            if ser is None:
                try:
                    ser = serial.Serial(port, args.baud, timeout=1)
                    print("# connected to %s" % port)
                except (OSError, serial.SerialException) as e:
                    print("# waiting for %s (%s)" % (port, e))
                    time.sleep(RECONNECT_S)
                    continue

            # --- read one line -------------------------------------------
            try:
                raw = ser.readline()
            except (OSError, serial.SerialException) as e:
                write_row(event="serial dropped: %s" % e)
                print("# serial dropped - will retry")
                try:
                    ser.close()
                except Exception:
                    pass
                ser = None
                time.sleep(RECONNECT_S)
                continue

            if not raw:
                continue

            line = raw.decode("utf-8", "replace").strip()
            reading = parse_reading(line)

            if reading is None:
                if line.startswith("#"):
                    print("  gauge: %s" % line.lstrip("# "))
                continue

            uptime_ms, celsius = reading
            write_row(uptime=uptime_ms, celsius=celsius)
            samples += 1

            elapsed_min = (time.time() - started) / 60.0
            print("\r  %6.1f min   %6.2f C   %d samples        "
                  % (elapsed_min, celsius, samples), end="", flush=True)

    except KeyboardInterrupt:
        print()
        write_row(event="log stopped")
        print("# stopped after %d samples, %.1f minutes"
              % (samples, (time.time() - started) / 60.0))
        print("# wrote %s" % out_path)
    finally:
        if ser is not None:
            try:
                ser.close()
            except Exception:
                pass
        fh.close()


if __name__ == "__main__":
    main()
