#!/usr/bin/env bash
#
# survey-legacy-tower - read what a legacy tower (the old host program and the
# TMC-API Arduino sketch) is running, BEFORE it is migrated, so its
# hardware.ini is written from its own values rather than another tower's.
# Run it ON the legacy tower. Changes nothing on disk.
#
#   bash survey-legacy-tower               the machine, the old install, the camera
#   bash survey-legacy-tower --firmware    also ask the old firmware for its
#                                          registers, travel and north calibration:
#                                          pauses the old program for ~20 s
#
# The report is printed and saved to ~/legacy-survey-<date>.txt.
#
# The firmware part talks the old sketch's own serial protocol:
#   ?        endstop positions and the travel between them (set by its last home)
#   y        its north calibration from EEPROM (the same `y` the old host sends
#            after every home - it reloads the stored value, it stores nothing)
#   id<n>    read TMC5160 register n (write-only ones answer from the library's copy)
#
set -uo pipefail

FIRMWARE=0
[[ ${1:-} == --firmware ]] && FIRMWARE=1
OUT="$HOME/legacy-survey-$(date +%Y%m%d-%H%M%S).txt"
exec > >(tee "$OUT") 2>&1

hr() { printf '\n== %s ==\n' "$*"; }
reach() { timeout 6 bash -c "</dev/tcp/$1/$2" 2>/dev/null && echo "ok" || echo "NO"; }

hr "machine"
echo "board     : $(tr -d '\0' < /sys/class/dmi/id/product_name 2>/dev/null || echo '?')"
echo "os        : $(. /etc/os-release 2>/dev/null && echo "$PRETTY_NAME") ($(uname -r))"
echo "user      : $USER  groups: $(id -nG)"
echo "disk /    : $(df -h / | awk 'NR==2 {print $4 " free of " $2}')"
echo "memory    : $(free -h | awk '/^Mem/ {print $2}')"
echo "internet  : gitea:2222 $(reach git.ggs-solutions.eu 2222)  github $(reach github.com 443)  pypi $(reach pypi.org 443)  platformio $(reach dl.registry.platformio.org 443)"

hr "USB"
lsusb 2>/dev/null | grep -viE 'root hub' | sed 's/^/  /'
ls /dev/serial/by-id/ 2>/dev/null | sed 's/^/by-id: /'
ls /dev/ttyACM* 2>/dev/null | sed 's/^/acm  : /'

hr "Vimba X and the camera"
ls -d /opt/VimbaX* 2>/dev/null || echo "no /opt/VimbaX*"
for tool in /opt/VimbaX*/bin/ListCameras_VmbC /opt/VimbaX*/bin/ListCameras; do
    [[ -x $tool ]] || continue
    cti="$(dirname "$(dirname "$tool")")/cti"
    GENICAM_GENTL64_PATH="$cti" "$tool" 2>/dev/null | grep -E 'Camera (ID|Name)|Model|Transport Layer Path' | sed 's/^/  /'
    break
done

hr "the old install - how it runs"
echo "-- systemd units that look like it (system and user):"
{ systemctl list-unit-files --no-legend 2>/dev/null; systemctl --user list-unit-files --no-legend 2>/dev/null; } |
    grep -iE 'gimbal|fire[-_]|fwt|tower|camera|zkms' | sed 's/^/  /' || echo "  (none)"
for u in $(systemctl list-unit-files --no-legend 2>/dev/null | awk '{print $1}' | grep -iE 'gimbal|fire[-_]|fwt|tower'); do
    echo "  -- $u: $(systemctl is-active "$u" 2>/dev/null)"
    systemctl cat "$u" 2>/dev/null | grep -E '^(ExecStart|WorkingDirectory|User)=' | sed 's/^/     /'
    # The script a legacy unit runs (start_gimbal.service -> startup_gimbal.sh):
    # it starts the old program AND the picture transfer, so it matters which.
    for f in $(systemctl cat "$u" 2>/dev/null | sed -n 's/^ExecStart=//p' | tr ' ' '\n' | grep -E '\.sh$'); do
        [[ -r $f ]] || continue
        echo "     --- $f:"
        grep -vE '^[[:space:]]*$' "$f" | head -25 | sed -E 's/(pass(word)?|pw)([=: ]).*/\1\3*****/I' | sed 's/^/       /'
        for g in $(grep -oE '/[^ #&;]+\.sh' "$f" | sort -u); do
            [[ -r $g && $g != "$f" ]] || continue
            echo "     --- $g (first lines):"
            grep -vE '^[[:space:]]*$' "$g" | head -8 | sed -E 's/(pass(word)?|pw)([=: ]).*/\1\3*****/I' | sed 's/^/       /'
        done
    done
done
echo "-- processes that look like it:"
ps -eo pid,user,etime,args 2>/dev/null | grep -iE 'gimbal|fire_|fwt|vimba|zkms|tower|inotifywait' | grep -vE 'grep|survey-legacy|avahi' | sed 's/^/  /' || echo "  (none)"
echo "-- what holds the serial ports:"
for d in /dev/ttyACM*; do
    [[ -e $d ]] || continue
    pids="$(fuser "$d" 2>/dev/null | tr -s ' ')"
    if [[ -n ${pids// /} ]]; then for p in $pids; do echo "  $d: pid $p  $(ps -o args= -p "$p" 2>/dev/null)"; done
    else echo "  $d: free"; fi
done
echo "-- other ways it could start:"
crontab -l 2>/dev/null | grep -v '^#' | sed 's/^/  cron: /'
ls ~/.config/autostart/ 2>/dev/null | sed 's/^/  autostart: /'
tmux ls 2>/dev/null | sed 's/^/  tmux: /'
screen -ls 2>/dev/null | grep -E '^\s+[0-9]' | sed 's/^/  screen: /'
docker ps --format '{{.Names}}  {{.Image}}  {{.Status}}' 2>/dev/null | sed 's/^/  docker: /'
echo "-- home directory:"
ls -1 "$HOME" 2>/dev/null | sed 's/^/  /'
for c in /opt/fire_gimbal_control/config.ini "$HOME"/*/config.ini "$HOME"/*/*/config.ini; do
    [[ -r $c ]] || continue
    echo "-- $c (passwords masked)"
    grep -vE '^[[:space:]]*(;|#|$)' "$c" | sed -E 's/^(mqtt_pw[[:space:]]*=).*/\1 *****/' | sed 's/^/  /'
done

hr "the old firmware's source"
# What the running firmware cannot report: ENC_CONST, GLOBALSCALER and IHOLD_IRUN
# are write-only on the TMC5160, and the old sketch answers `i` for them from its
# own copy, which reads 0. Its register table is in its source - the newest build
# in the Arduino IDE's cache is most likely the one flashed.
sketch="$(ls -t "$HOME"/.cache/arduino/sketches/*/sketch/*.ino.cpp 2>/dev/null |
          xargs -r grep -l chipCS_enc 2>/dev/null | head -1)"
if [[ -z $sketch ]]; then
    echo "no build of the old sketch in ~/.cache/arduino; look for FWT_firmware*.ino by hand"
else
    echo "newest build: $sketch ($(date -r "$sketch" '+%Y-%m-%d %H:%M'))"
    echo "  source: $(sed -n 's/^#line 1 "\(.*\)"/\1/p' "$sketch" | head -1)"
    grep -hE '^#define (V_ENC_INIT|ONE_ROUND_STEPS)|gear_red_var *=|one_round_in_steps *=' "$sketch" | sed 's/^/  /'
    # ENC_CONST is decimal: 0x000C1388 is 12 + 0x1388/10000 = 12.5 microsteps per count.
    grep -hE '^#define R(0B|10|3A|3D|26|27|28) ' "$(dirname "$sketch")"/TMC5160.h 2>/dev/null | sed 's/^/  /'
    echo "  (R3A = ENC_CONST: hardware.ini [Motor.yaw] enc_const = whole + low 16 bits/10000)"
fi

[[ $FIRMWARE -eq 1 ]] || { hr "firmware"; echo "not asked (run with --firmware)"; echo; echo "saved: $OUT"; exit 0; }

hr "the old firmware"
# The Leonardo by name: an Arduino-branded one, or the LattePanda's own ("Unknown_LattePanda_Leonardo").
PORT="$(ls /dev/serial/by-id/*Leonardo* /dev/serial/by-id/*Arduino* 2>/dev/null | head -1)"
[[ -n $PORT ]] || { echo "no Leonardo on /dev/serial/by-id - see the USB section above"; exit 1; }
echo "port: $PORT"
# Something may hold it: the old program. The two units known to start it are
# paused here and started again at the end - start_gimbal.service (the legacy
# towers' launcher, which also runs the picture transfer) and
# fire-gimbal-control. Anything else is named, and left for a person to stop.
PAUSED=()
for u in start_gimbal fire-gimbal-control; do
    [[ $(systemctl is-active "$u" 2>/dev/null) == active ]] || continue
    echo "pausing $u.service (it runs the program that holds the port)"
    sudo systemctl stop "$u" && PAUSED+=("$u")
done
if (( ${#PAUSED[@]} )); then
    trap 'for u in "${PAUSED[@]}"; do echo "restarting $u.service"; sudo systemctl start "$u"; done' EXIT
    sleep 2
fi
holders="$(fuser "$(readlink -f "$PORT")" 2>/dev/null | tr -s ' ')"
if [[ -n ${holders// /} ]]; then
    echo "the port is in use by:"
    for p in $holders; do echo "  pid $p  $(ps -o args= -p "$p" 2>/dev/null)"; done
    echo "stop that program first (it is the old one, reading the same port), then run this again."
    exit 1
fi

python3 - "$PORT" <<'PYEOF'
import os, sys, termios, time, math

port = sys.argv[1]
fd = os.open(port, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
a = termios.tcgetattr(fd)
a[0] = 0; a[1] = 0; a[2] = termios.CS8 | termios.CREAD | termios.CLOCAL; a[3] = 0
a[4] = a[5] = termios.B115200
termios.tcsetattr(fd, termios.TCSANOW, a)
buf = b""

def lines_for(seconds):
    global buf
    out, end = [], time.time() + seconds
    while time.time() < end:
        try:
            buf += os.read(fd, 4096)
        except BlockingIOError:
            time.sleep(0.02)
        while b"\n" in buf:
            line, buf = buf.split(b"\n", 1)
            out.append(line.decode(errors="replace").strip())
    return out

def ask(cmd, seconds=0.8):
    lines_for(0.2)   # drop what was already streaming
    os.write(fd, cmd.encode() + b"\n")
    got = lines_for(seconds)
    return [l for l in got if l and not l.startswith("$")], [l for l in got if l.startswith("$")]

_, status = ask("", 1.0)
print("status line  :", status[-1] if status else "(none - is this the legacy firmware?)")

reply, _ = ask("?")
print("? (endstops) :", " | ".join(reply) or "(no answer)")
nums = [int(x) for x in reply if x.lstrip("-").isdigit()]
if len(nums) >= 3:
    r, l = nums[0], nums[1]
    print(f"  right {r}  left {l}  travel {abs(l - r)} counts = {abs(l - r) / 528.253966:.1f} deg at 528.25 counts/deg")

reply, _ = ask("y")
print("y (north)    :", " | ".join(reply) or "(no answer)")

REGS = [("GCONF", 0x00), ("GLOBALSCALER", 0x0B), ("IHOLD_IRUN", 0x10), ("TPOWERDOWN", 0x11),
        ("XACTUAL", 0x21), ("AMAX", 0x26), ("VMAX", 0x27), ("DMAX", 0x28), ("SW_MODE", 0x34),
        ("ENCMODE", 0x38), ("X_ENC", 0x39), ("ENC_CONST", 0x3A), ("ENC_DEVIATION", 0x3D),
        ("CHOPCONF", 0x6C), ("COOLCONF", 0x6D), ("PWMCONF", 0x70)]
vals = {}
print("registers    :")
for name, n in REGS:
    reply, _ = ask(f"id{n}")
    v = None
    for x in reply:
        if x.startswith("READ"):
            try: v = int(x.split(",")[-1])
            except ValueError: pass
    vals[name] = v
    shown = "(no answer)" if v is None else f"{v} = 0x{v & 0xFFFFFFFF:08X}"
    print(f"  {name:14s} 0x{n:02X}  {shown}")

gs, ii = vals.get("GLOBALSCALER"), vals.get("IHOLD_IRUN")
if ii is not None:
    irun, ihold = (ii >> 8) & 0x1F, ii & 0x1F
    scale = (gs or 256) / 256.0
    print(f"current      : IRUN {irun}, IHOLD {ihold}, GLOBALSCALER {gs if gs else '0 (=256, full)'}")
    for rs in (0.075, 0.05):
        i_run = scale * (irun + 1) / 32 * 0.325 / rs / math.sqrt(2) * 1000
        print(f"  with a {rs * 1000:.0f} mOhm sense resistor: run {i_run:.0f} mA RMS"
              "  (read the resistor off the board before trusting either)")
os.close(fd)
PYEOF
echo
echo "saved: $OUT"
