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.
|
# 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.
|
# <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
|
# --seed FILE a seed manifest (flat YAML key: value, see flare-edge
|
||||||
# tools/seed-fixtures.py) run through that tool and staged
|
# tools/seed-fixtures.py) run through that tool and staged
|
||||||
@@ -25,17 +25,27 @@
|
|||||||
# convention as ota-apply.sh / portal-scenario.sh).
|
# convention as ota-apply.sh / portal-scenario.sh).
|
||||||
# --refs FILE passed straight through to qmp.py drive's --refs (the
|
# --refs FILE passed straight through to qmp.py drive's --refs (the
|
||||||
# region/ocr reference JSON); see qmp.py's own docstring.
|
# 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
|
set -euo pipefail
|
||||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # qemu/tests/
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # qemu/tests/
|
||||||
QDIR="$(cd "$HERE/.." && pwd)" # qemu/
|
QDIR="$(cd "$HERE/.." && pwd)" # qemu/
|
||||||
|
|
||||||
SEED_FILE=""
|
SEED_FILE=""
|
||||||
REFS_FILE=""
|
REFS_FILE=""
|
||||||
|
RS485_DEVICES=""
|
||||||
ARGS=()
|
ARGS=()
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--seed) SEED_FILE="${2:?--seed needs a manifest path}"; shift 2 ;;
|
--seed) SEED_FILE="${2:?--seed needs a manifest path}"; shift 2 ;;
|
||||||
--refs) REFS_FILE="${2:?--refs needs a 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 ;;
|
--) shift; ARGS+=("$@"); break ;;
|
||||||
-*) echo "FATAL: unknown option '$1' (usage: $0 [--seed FILE] [--refs FILE] <zImage> <script> [out-dir])" >&2; exit 1 ;;
|
-*) echo "FATAL: unknown option '$1' (usage: $0 [--seed FILE] [--refs FILE] <zImage> <script> [out-dir])" >&2; exit 1 ;;
|
||||||
*) ARGS+=("$1"); shift ;;
|
*) ARGS+=("$1"); shift ;;
|
||||||
@@ -89,13 +99,16 @@ RUN_OUT="$(mktemp -d "${TMPDIR:-/tmp}/wqd-out.XXXXXX")"
|
|||||||
export OUT="$RUN_OUT"
|
export OUT="$RUN_OUT"
|
||||||
export BUSYBOX="${BUSYBOX:-$QDIR/out/busybox-armv7l}"
|
export BUSYBOX="${BUSYBOX:-$QDIR/out/busybox-armv7l}"
|
||||||
QEMU_PID=""
|
QEMU_PID=""
|
||||||
|
SIM_PIDS=""
|
||||||
cleanup() {
|
cleanup() {
|
||||||
|
for p in $SIM_PIDS; do kill "$p" 2>/dev/null || true; done
|
||||||
if [ -n "$QEMU_PID" ]; then
|
if [ -n "$QEMU_PID" ]; then
|
||||||
python3 "$HERE/qmp.py" "$WORK/qmp.sock" quit 2>/dev/null || true
|
python3 "$HERE/qmp.py" "$WORK/qmp.sock" quit 2>/dev/null || true
|
||||||
sleep 1
|
sleep 1
|
||||||
kill "$QEMU_PID" 2>/dev/null || true
|
kill "$QEMU_PID" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
cp "$WORK/console.log" "$OUTDIR/console.log" 2>/dev/null || true
|
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"
|
rm -rf "$WORK" "$RUN_OUT"
|
||||||
}
|
}
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
@@ -119,7 +132,7 @@ for _attempt in 1 2 3; do
|
|||||||
PORT=$((21000 + RANDOM % 20000))
|
PORT=$((21000 + RANDOM % 20000))
|
||||||
: > "$WORK/console.log"
|
: > "$WORK/console.log"
|
||||||
bash "$QDIR/run.sh" --kernel "$ZIMAGE" --display headless --qmp "$WORK/qmp.sock" \
|
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)) \
|
--ssh-port "$PORT" --http-port $((PORT + 1)) --api-port $((PORT + 2)) \
|
||||||
> "$WORK/console.log" 2>&1 &
|
> "$WORK/console.log" 2>&1 &
|
||||||
QEMU_PID=$!
|
QEMU_PID=$!
|
||||||
@@ -139,6 +152,25 @@ if [ -z "$QEMU_PID" ] || ! kill -0 "$QEMU_PID" 2>/dev/null; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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"
|
echo "== waiting for warden-ui"
|
||||||
deadline=$((SECONDS + 180))
|
deadline=$((SECONDS + 180))
|
||||||
while [ $SECONDS -lt $deadline ]; do
|
while [ $SECONDS -lt $deadline ]; do
|
||||||
|
|||||||
Reference in New Issue
Block a user