qemu: simulated Modbus devices on the rig's RS485 bus
ui-drive.sh --rs485-devices ADDR:SLUG[,...] attaches the VM's RS485 UART to a socket, turns it into a pty with socat and serves the named corpus profiles on it with flare-edge tools/modbus-sim/mbsim.py, so warden-modbus in the guest discovers and identifies devices as it would real controllers. Verified: unit 5 as smartgen-hgm6100n4g appears in the status json as SmartGen HGM6110N, online, 8 ms, and RS485/Devices lists it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N3G6m9Aw5RyVY4ZowtKzEj
This commit is contained in:
+34
-2
@@ -14,7 +14,7 @@
|
||||
#
|
||||
# FAILS CLOSED on missing prerequisites.
|
||||
#
|
||||
# Usage: ui-drive.sh [--seed FILE] [--refs FILE] <zImage-virt> <script> [out-dir]
|
||||
# Usage: ui-drive.sh [--seed FILE] [--refs FILE] [--rs485-devices LIST] <zImage-virt> <script> [out-dir]
|
||||
# <script> is a qmp.py `drive` script: see its docstring for the commands.
|
||||
# --seed FILE a seed manifest (flat YAML key: value, see flare-edge
|
||||
# tools/seed-fixtures.py) run through that tool and staged
|
||||
@@ -25,17 +25,27 @@
|
||||
# convention as ota-apply.sh / portal-scenario.sh).
|
||||
# --refs FILE passed straight through to qmp.py drive's --refs (the
|
||||
# region/ocr reference JSON); see qmp.py's own docstring.
|
||||
# --rs485-devices ADDR:SLUG[,ADDR:SLUG...]
|
||||
# put simulated Modbus devices on the VM's RS485 bus: the
|
||||
# UART is attached to a unix socket (run.sh --rs485), socat
|
||||
# turns it into a pty, and flare-edge tools/modbus-sim/mbsim.py
|
||||
# serves the named corpus profiles on it, so warden-modbus in
|
||||
# the guest discovers and identifies devices the way it does
|
||||
# on a panel with real controllers on the header. Needs
|
||||
# FLARE_EDGE and socat.
|
||||
set -euo pipefail
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # qemu/tests/
|
||||
QDIR="$(cd "$HERE/.." && pwd)" # qemu/
|
||||
|
||||
SEED_FILE=""
|
||||
REFS_FILE=""
|
||||
RS485_DEVICES=""
|
||||
ARGS=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--seed) SEED_FILE="${2:?--seed needs a manifest path}"; shift 2 ;;
|
||||
--refs) REFS_FILE="${2:?--refs needs a path}"; shift 2 ;;
|
||||
--rs485-devices) RS485_DEVICES="${2:?--rs485-devices needs ADDR:SLUG[,...]}"; shift 2 ;;
|
||||
--) shift; ARGS+=("$@"); break ;;
|
||||
-*) echo "FATAL: unknown option '$1' (usage: $0 [--seed FILE] [--refs FILE] <zImage> <script> [out-dir])" >&2; exit 1 ;;
|
||||
*) ARGS+=("$1"); shift ;;
|
||||
@@ -89,13 +99,16 @@ RUN_OUT="$(mktemp -d "${TMPDIR:-/tmp}/wqd-out.XXXXXX")"
|
||||
export OUT="$RUN_OUT"
|
||||
export BUSYBOX="${BUSYBOX:-$QDIR/out/busybox-armv7l}"
|
||||
QEMU_PID=""
|
||||
SIM_PIDS=""
|
||||
cleanup() {
|
||||
for p in $SIM_PIDS; do kill "$p" 2>/dev/null || true; done
|
||||
if [ -n "$QEMU_PID" ]; then
|
||||
python3 "$HERE/qmp.py" "$WORK/qmp.sock" quit 2>/dev/null || true
|
||||
sleep 1
|
||||
kill "$QEMU_PID" 2>/dev/null || true
|
||||
fi
|
||||
cp "$WORK/console.log" "$OUTDIR/console.log" 2>/dev/null || true
|
||||
cp "$WORK/mbsim.log" "$OUTDIR/mbsim.log" 2>/dev/null || true
|
||||
rm -rf "$WORK" "$RUN_OUT"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
@@ -119,7 +132,7 @@ for _attempt in 1 2 3; do
|
||||
PORT=$((21000 + RANDOM % 20000))
|
||||
: > "$WORK/console.log"
|
||||
bash "$QDIR/run.sh" --kernel "$ZIMAGE" --display headless --qmp "$WORK/qmp.sock" \
|
||||
--ctl "$WORK/ctl.sock" \
|
||||
--ctl "$WORK/ctl.sock" ${RS485_DEVICES:+--rs485 "$WORK/rs.sock"} \
|
||||
--ssh-port "$PORT" --http-port $((PORT + 1)) --api-port $((PORT + 2)) \
|
||||
> "$WORK/console.log" 2>&1 &
|
||||
QEMU_PID=$!
|
||||
@@ -139,6 +152,25 @@ if [ -z "$QEMU_PID" ] || ! kill -0 "$QEMU_PID" 2>/dev/null; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$RS485_DEVICES" ]; then
|
||||
# qemu created rs.sock at launch (server=on,wait=off). socat gives the
|
||||
# simulator the serial device it expects; mbsim then answers the guest's
|
||||
# Modbus polls with the corpus profiles' own register maps.
|
||||
command -v socat >/dev/null || { echo "FATAL: --rs485-devices needs socat" >&2; exit 1; }
|
||||
[ -n "${FLARE_EDGE:-}" ] && [ -f "$FLARE_EDGE/tools/modbus-sim/mbsim.py" ] || {
|
||||
echo "FATAL: --rs485-devices needs FLARE_EDGE to point at a flare-edge checkout (mbsim.py)" >&2; exit 1; }
|
||||
for _i in $(seq 1 50); do [ -S "$WORK/rs.sock" ] && break; sleep 0.1; done
|
||||
socat "UNIX-CONNECT:$WORK/rs.sock" "PTY,link=$WORK/rs.pty,raw,echo=0" > "$WORK/socat.log" 2>&1 &
|
||||
SIM_PIDS="$SIM_PIDS $!"
|
||||
for _i in $(seq 1 50); do [ -e "$WORK/rs.pty" ] && break; sleep 0.1; done
|
||||
dev_args=()
|
||||
IFS=',' read -r -a _devs <<< "$RS485_DEVICES"
|
||||
for d in "${_devs[@]}"; do dev_args+=(--device "$d"); done
|
||||
python3 "$FLARE_EDGE/tools/modbus-sim/mbsim.py" --port "$WORK/rs.pty" "${dev_args[@]}" > "$WORK/mbsim.log" 2>&1 &
|
||||
SIM_PIDS="$SIM_PIDS $!"
|
||||
echo "== rs485 simulator: $RS485_DEVICES on $WORK/rs.pty"
|
||||
fi
|
||||
|
||||
echo "== waiting for warden-ui"
|
||||
deadline=$((SECONDS + 180))
|
||||
while [ $SECONDS -lt $deadline ]; do
|
||||
|
||||
Reference in New Issue
Block a user