#!/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-.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 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/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"