# BST-TIMEOUT: 300 # measured 31s/32s over two runs (101 cases, ~95 mbpoll execs # plus a background peer); the runner's default of 60 is too tight # # mbpoll 1.5.2 -- a Modbus MASTER. Everything it does that is not argument # parsing needs a peer that answers, and the image has no Modbus slave: no # serial line with a device on it, and nothing listening on 502. So the port # would be verifiable only as text. # # It is not, because libmodbus.a ships (-staticdev) and one small program is # enough to be the peer: data/mbpoll-slave (src/mbpoll_slave.c), cross-linked # against the very archive the image carries. With it every read and write form # runs FOR REAL over a real socket, against registers preset to values chosen so # each display format renders differently. # # Every expectation below is the text a native x86_64 build of the identical # source produces against the identical peer -- src/mbpoll_mknative.sh, which # regenerates the capture the needles were read from. # # THE ORDER OF THE TRANSACTION GROUP IS LOAD-BEARING: the writes mutate the # slave's tables and the readbacks assert what they left, ending in one whole- # table read. Reordering it turns a real check into a coincidence. . "$BST_LIB" require mbpoll mbpoll/present # BST_MBPOLL exists so the mutation control (src/mbpoll_control.sh) can run THIS # FILE, unchanged, against a build of the same source WITHOUT 0001 -- the only # way to establish that the version cases below are guards rather than decoration. # The target never sets it, exactly as the runner's own BST_UPTIME is never set # there; with it unset this is the installed path and nothing else changes. MB=${BST_MBPOLL:-/usr/bin/mbpoll} tmp=${TMPDIR:-/tmp} PORT=15502 DEADPORT=15599 # mb [args...] -- one mbpoll run, asserting BOTH its # exit status and a literal substring of its combined output. Both, because for # this tool neither alone discriminates: every argument refusal exits 1, and a # read that failed still prints its "-- Polling slave" header. mb() { _mid=$1; _mwant=$2; _mneedle=$3; shift 3 _mout=$("$MB" "$@" 2>&1); _mrc=$? if bst_execfail "$_mout"; then bst_unrun "$_mid" "$_mout"; return; fi case "$_mout" in *"$_mneedle"*) _mhit=1 ;; *) _mhit=0 ;; esac if [ "$_mrc" -eq "$_mwant" ] && [ "$_mhit" -eq 1 ]; then bst_ok "$_mid" else bst_notok "$_mid" "rc=$_mrc want=$_mwant want:'$_mneedle' got:'$(_bst_1line "$_mout")'"; fi } # --- A. what shipped --------------------------------------------------------- check mbpoll/installed-path test -x "$MB" check_eq mbpoll/which "$MB" "$(command -v mbpoll)" check mbpoll/one-binary test ! -e /usr/sbin/mbpoll # --- B. usage, version, warranty (no peer) ----------------------------------- mb mbpoll/usage-rc 0 "usage : mbpoll [ options ] device|host [ writevalues... ] [ options ]" -h mb mbpoll/usage-basename 0 "mbpoll -t4:int /dev/ttyUSB0 -- 123 -1568 8974 -12" -h mb mbpoll/usage-mode 0 "-m # mode (rtu or tcp, TCP is default)" -h mb mbpoll/usage-addr 0 "-a # Slave address (1-255 for rtu, 0-255 for tcp, 1 is default)" -h mb mbpoll/usage-count 0 "-c # Number of values to read (1-125, 1 is default)" -h mb mbpoll/usage-timeout 0 "-o # Time-out in seconds (0.01 - 10.00, 1.00 s is default)" -h mb mbpoll/usage-port 0 "-p # TCP port number (502 is default)" -h mb mbpoll/usage-baud 0 "-b # Baudrate (1200-921600, 19200 is default)" -h mb mbpoll/usage-parity 0 "-P # Parity (none, even, odd, even is default)" -h mb mbpoll/usage-float 0 "-t 4:float 32-bit float data type in output (holding) register table" -h mb mbpoll/usage-hex 0 "-t 4:hex 16-bit output (holding) register data type with hex display" -h mb mbpoll/usage-pollrate 0 "-l # Poll rate in ms, ( > 100, 1000 is default)" -h # The version the RECIPE declares is 1.5.2 and upstream renders it MAJOR.MINOR-PATCH. # Without 0001 this reads 1.0-0 -- git describe run in the out-of-tree build dir. mb mbpoll/version 0 "1.5-2" -V check_eq mbpoll/version-exact "1.5-2" "$("$MB" -V 2>&1)" mb mbpoll/warranty 0 "mbpoll is free software: you can redistribute it and/or modify" -w mb mbpoll/warranty-gpl3 0 "the Free Software Foundation, either version 3 of the License, or" -w # --- C. argument validation (no peer; every one of these exits before a socket) mb mbpoll/arg-no-device 1 "device or host parameter missing ! Try -h for help." mb mbpoll/arg-argv0 1 "$MB: device or host parameter missing" mb mbpoll/arg-bad-option 1 "Unrecognized option or missing option parameter ! Try -h for help." -Z 127.0.0.1 mb mbpoll/arg-getopt-says 1 "invalid option -- 'Z'" -Z 127.0.0.1 mb mbpoll/arg-addr-hi 1 "slave address out of range (256)" -1 -a 256 -p "$PORT" 127.0.0.1 mb mbpoll/arg-addr-lo-rtu 1 "slave address out of range (0)" -1 -m rtu -a 0 /dev/null mb mbpoll/arg-count-hi 1 "number of values out of range (126)" -1 -c 126 -p "$PORT" 127.0.0.1 mb mbpoll/arg-count-lo 1 "number of values out of range (0)" -1 -c 0 -p "$PORT" 127.0.0.1 mb mbpoll/arg-ref-hi 1 "start reference out of range (65537)" -1 -r 65537 -p "$PORT" 127.0.0.1 mb mbpoll/arg-timeout-hi 1 "timeout out of range (10.5)" -1 -o 10.5 -p "$PORT" 127.0.0.1 mb mbpoll/arg-pollrate-lo 1 "Illegal poll rate: 99" -1 -l 99 -p "$PORT" 127.0.0.1 mb mbpoll/arg-baud-lo 1 "rtu baudrate out of range (1199)" -1 -m rtu -b 1199 /dev/null mb mbpoll/arg-mode-bad 1 "Illegal mode: sctp" -1 -m sctp -p "$PORT" 127.0.0.1 mb mbpoll/arg-type-bad 1 "Illegal function: 2" -1 -t 2 -p "$PORT" 127.0.0.1 mb mbpoll/arg-fmt-bad 1 "Illegal format: quad" -1 -t 4:quad -p "$PORT" 127.0.0.1 mb mbpoll/arg-ro-di 1 "Unable to write read-only element" -1 -t 1 -p "$PORT" 127.0.0.1 1 mb mbpoll/arg-ro-ireg 1 "Unable to write read-only element" -1 -t 3 -p "$PORT" 127.0.0.1 1 mb mbpoll/arg-c-on-write 1 "-c parameter must not be specified for writing" -1 -c 2 -p "$PORT" 127.0.0.1 7 mb mbpoll/arg-u-not-rtu 1 "-u is available only in RTU mode" -1 -u -p "$PORT" 127.0.0.1 mb mbpoll/arg-addrlist-w 1 "You can give a slave address list only for reading" -1 -a 1,2 -p "$PORT" 127.0.0.1 7 mb mbpoll/arg-str-write 1 "You can use string format only for output" -1 -q -p "$PORT" -r 20 -t 4:string 127.0.0.1 AB mb mbpoll/arg-coil-range 1 "data out of range (2)" -1 -q -p "$PORT" -t 0 -r 10 127.0.0.1 2 # Upstream documents this: a negative write value must follow a bare `--`. mb mbpoll/arg-neg-no-dd 1 "Unrecognized option or missing option parameter" -1 -q -p "$PORT" -r 22 -t 4:float 127.0.0.1 -3.5 # --- D. the unreachable device (asserted for real; needs no peer) ------------- # This IS the ticket's "documented exit code for an unreachable device": 1, from # vFailureExit, after the banner has already been printed. mb mbpoll/conn-refused 1 "Connection failed: Connection refused." -1 -p "$DEADPORT" 127.0.0.1 mb mbpoll/conn-refused-banner 1 "mbpoll 1.5-2 - ModBus(R) Master Simulator" -1 -p "$DEADPORT" 127.0.0.1 mb mbpoll/conn-502 1 "Connection failed: Connection refused." -1 127.0.0.1 mb mbpoll/rtu-noent 1 "Connection failed: No such file or directory." -1 -m rtu -o 0.05 /dev/mbpoll-no-such-tty mb mbpoll/rtu-notty 1 "Connection failed: Inappropriate ioctl for device." -1 -m rtu -o 0.05 /dev/null # --- E. the transaction group, against the peer this suite ships -------------- slave=$tmp/bst-mbpoll-slave.$$ ready=$tmp/bst-mbpoll-ready.$$ rm -f "$ready" if [ ! -f "$BST_DATA/mbpoll-slave" ]; then bst_notok mbpoll/slave-shipped "data/mbpoll-slave is not in the image" elif cp "$BST_DATA/mbpoll-slave" "$slave" 2>/dev/null && chmod +x "$slave" 2>/dev/null; then bst_ok mbpoll/slave-shipped "$slave" "$PORT" "$ready" 300 >"$tmp/bst-mbpoll-slave.log.$$" 2>&1 & slavepid=$! n=0 while [ ! -f "$ready" ] && [ "$n" -lt 200 ]; do n=$((n+1)); usleep 50000 2>/dev/null || sleep 1; done if [ ! -f "$ready" ]; then bst_skip mbpoll/slave-listening "the shipped Modbus slave never bound 127.0.0.1:$PORT -- $(_bst_1line "$(cat "$tmp/bst-mbpoll-slave.log.$$" 2>/dev/null)")" kill "$slavepid" 2>/dev/null else bst_ok mbpoll/slave-listening # -- reads, each format rendering the SAME words differently --------------- mb mbpoll/read-hold-dec 0 "[2]: 4660" -1 -q -p "$PORT" -r 2 127.0.0.1 mb mbpoll/read-hold-hex 0 "[2]: 0x1234" -1 -q -p "$PORT" -r 2 -t 4:hex 127.0.0.1 mb mbpoll/read-hold-i16 0 "[4]: -32768" -1 -q -p "$PORT" -r 4 -t 4:int16 127.0.0.1 mb mbpoll/read-hold-dual 0 "[4]: 32768 (-32768)" -1 -q -p "$PORT" -r 4 -c 1 127.0.0.1 mb mbpoll/read-hold-str 0 "[5]: Hi" -1 -q -p "$PORT" -r 5 -c 2 -t 4:string 127.0.0.1 mb mbpoll/read-hold-str2 0 "[6]: !!" -1 -q -p "$PORT" -r 5 -c 2 -t 4:string 127.0.0.1 mb mbpoll/read-hold-i32 0 "[7]: 305419896" -1 -q -p "$PORT" -r 7 -t 4:int 127.0.0.1 mb mbpoll/read-hold-f32 0 "[9]: 12.5" -1 -q -p "$PORT" -r 9 -t 4:float 127.0.0.1 mb mbpoll/read-hold-be 0 "[7]: 1450709556" -1 -q -p "$PORT" -r 7 -t 4:int -B 127.0.0.1 mb mbpoll/read-ireg 0 "[1]: 3000" -1 -q -p "$PORT" -r 1 -t 3 127.0.0.1 mb mbpoll/read-ireg-hex 0 "[3]: 0xDEAD" -1 -q -p "$PORT" -r 3 -t 3:hex 127.0.0.1 mb mbpoll/read-coil 0 "[1]: 1" -1 -q -p "$PORT" -r 1 -c 8 -t 0 127.0.0.1 mb mbpoll/read-coil-8th 0 "[8]: 1" -1 -q -p "$PORT" -r 1 -c 8 -t 0 127.0.0.1 mb mbpoll/read-di 0 "[2]: 1" -1 -q -p "$PORT" -r 1 -c 8 -t 1 127.0.0.1 mb mbpoll/read-di-first 0 "[1]: 0" -1 -q -p "$PORT" -r 1 -c 8 -t 1 127.0.0.1 mb mbpoll/read-reflist 0 "[3]: 65535 (-1)" -1 -q -p "$PORT" -r 2,3 127.0.0.1 mb mbpoll/read-pdu0 0 "[1]: 4660" -1 -q -p "$PORT" -0 -r 1 127.0.0.1 mb mbpoll/read-slave-addr 0 "-- Polling slave 9..." -1 -q -p "$PORT" -a 9 -r 2 127.0.0.1 mb mbpoll/read-banner 0 "Protocol configuration: ModBus TCP" -1 -p "$PORT" -r 2 127.0.0.1 mb mbpoll/read-banner-comm 0 "Communication.........: 127.0.0.1, port $PORT, t/o 1.00 s, poll rate 1000 ms" -1 -p "$PORT" -r 2 127.0.0.1 mb mbpoll/read-banner-type 0 "Data type.............: 16-bit register, output (holding) register table" -1 -p "$PORT" -r 2 127.0.0.1 # -- the bytes on the wire, which is the only assertion that cannot be # satisfied by a plausible-looking number ------------------------------ mb mbpoll/wire-request 0 "[00][01][00][00][00][06][01][03][00][01][00][01]" -1 -q -v -p "$PORT" -r 2 -c 1 127.0.0.1 mb mbpoll/wire-response 0 "<00><01><00><00><00><05><01><03><02><12><34>" -1 -q -v -p "$PORT" -r 2 -c 1 127.0.0.1 # -- the exception reply the protocol defines ------------------------------ mb mbpoll/read-exception 1 "Read output (holding) register failed: Illegal data address" -1 -q -p "$PORT" -r 900 127.0.0.1 # -- writes, each followed by the read that proves it landed --------------- mb mbpoll/write-dec 0 "Written 1 references." -1 -q -p "$PORT" -r 20 127.0.0.1 4242 mb mbpoll/readback-dec 0 "[20]: 4242" -1 -q -p "$PORT" -r 20 127.0.0.1 mb mbpoll/write-hex 0 "Written 1 references." -1 -q -p "$PORT" -r 21 -t 4:hex 127.0.0.1 0xBEEF mb mbpoll/readback-hex 0 "[21]: 0xBEEF" -1 -q -p "$PORT" -r 21 -t 4:hex 127.0.0.1 mb mbpoll/write-f32 0 "Written 1 references." -1 -q -p "$PORT" -r 22 -t 4:float 127.0.0.1 -- -3.5 mb mbpoll/readback-f32 0 "[22]: -3.5" -1 -q -p "$PORT" -r 22 -t 4:float 127.0.0.1 mb mbpoll/write-i32 0 "Written 1 references." -1 -q -p "$PORT" -r 24 -t 4:int 127.0.0.1 -- -70000 mb mbpoll/readback-i32 0 "[24]: -70000" -1 -q -p "$PORT" -r 24 -t 4:int 127.0.0.1 mb mbpoll/write-many 0 "Written 3 references." -1 -q -p "$PORT" -r 26 127.0.0.1 11 22 33 mb mbpoll/readback-many 0 "[28]: 33" -1 -q -p "$PORT" -r 26 -c 3 127.0.0.1 mb mbpoll/write-single-W 0 "Written 1 references." -1 -q -p "$PORT" -W -r 30 127.0.0.1 555 mb mbpoll/readback-W 0 "[30]: 555" -1 -q -p "$PORT" -r 30 127.0.0.1 mb mbpoll/write-coil 0 "Written 1 references." -1 -q -p "$PORT" -t 0 -r 10 127.0.0.1 1 mb mbpoll/readback-coil 0 "[10]: 1" -1 -q -p "$PORT" -t 0 -r 10 -c 1 127.0.0.1 # -- one read of the whole table: every write above, in one answer --------- whole=$("$MB" -1 -q -p "$PORT" -r 1 -c 32 -t 4:hex 127.0.0.1 2>&1) for want in "[20]: 0x1092" "[21]: 0xBEEF" "[22]: 0x0000" "[23]: 0xC060" \ "[24]: 0xEE90" "[25]: 0xFFFE" "[26]: 0x000B" "[28]: 0x0021" "[30]: 0x022B"; do case "$whole" in *"$want"*) bst_ok "mbpoll/table${want%%:*}" ;; *) bst_notok "mbpoll/table${want%%:*}" "want:'$want' got:'$(_bst_1line "$whole")'" ;; esac done kill "$slavepid" 2>/dev/null wait "$slavepid" 2>/dev/null fi rm -f "$slave" "$ready" "$tmp/bst-mbpoll-slave.log.$$" else bst_notok mbpoll/slave-shipped "cannot stage $BST_DATA/mbpoll-slave" fi # --- F. the RTU path, and the probe that decides how much of it can run ------ # mbpoll picks RTU by looking for com/tty/ser in the DEVICE STRING, so the name # is what routes it into the serial branch at all. # # This image HAS a /dev/ttySER0: the wasm-serial WebSerial bridge, char 254,0, # registered in /proc/tty/drivers, with a node in /sys/class/tty. What it does # not have is a LINE -- the driver refuses open() with -ENODEV by design until a # browser-granted navigator.serial port attaches, and no runner attaches one. # So the probe below asks the kernel rather than asking whether a node exists: # `test -c` is true here and answers the wrong question. # # Either way the serial branch is scored FOR REAL -- mbpoll chose RTU from the # name, opened the node, and the driver answered -- and only the cases that need # a line skip. rtu=/dev/ttySER0 if [ ! -c "$rtu" ]; then for c in rtu-node rtu-driver rtu-selected-by-name rtu-reaches-driver rtu-line; do bst_skip "mbpoll/$c" "no $rtu character device on this image" done else bst_ok mbpoll/rtu-node if grep -q "^wasm-serial " /proc/tty/drivers 2>/dev/null; then bst_ok mbpoll/rtu-driver else bst_notok mbpoll/rtu-driver "/dev/ttySER0 exists but no wasm-serial row in /proc/tty/drivers" fi rtuout=$("$MB" -1 -o 0.05 -a 1 -r 1 "$rtu" 2>&1); rturc=$? # A TCP run against the same string answers "Connection refused"; only the # serial branch can produce a serial errno for it. That is what makes this an # assertion about mbpoll's mode selection and not about the device. case "$rtuout" in *"Connection failed: No such device."*|*"Protocol configuration: ModBus RTU"*) bst_ok mbpoll/rtu-selected-by-name ;; *"Connection failed: Connection refused."*) bst_notok mbpoll/rtu-selected-by-name "took the TCP branch for $rtu -- the com/tty/ser name heuristic stopped working" ;; *) bst_notok mbpoll/rtu-selected-by-name "rc=$rturc unrecognised: $(_bst_1line "$rtuout")" ;; esac case "$rtuout" in *"Connection failed: No such device."*) # The open reached the driver and the driver answered. That is the whole # of what this image can prove, and it is not nothing: it is exit 1 out of # modbus_connect on a real device node, not an argument refusal. if [ "$rturc" -eq 1 ]; then bst_ok mbpoll/rtu-reaches-driver else bst_notok mbpoll/rtu-reaches-driver "rc=$rturc want=1"; fi bst_skip mbpoll/rtu-line "wasm-serial refuses open(-ENODEV) until a browser-granted navigator.serial port attaches; none does under the local runner" ;; *"Protocol configuration: ModBus RTU"*) # A line IS attached -- run the serial path for real. bst_ok mbpoll/rtu-reaches-driver bst_ok mbpoll/rtu-line mb mbpoll/rtu-banner 1 "Protocol configuration: ModBus RTU" -1 -o 0.05 -a 1 -r 1 "$rtu" mb mbpoll/rtu-setup 1 "Communication.........: $rtu, 19200-8E1" -1 -o 0.05 -a 1 -r 1 "$rtu" mb mbpoll/rtu-timing 1 " t/o 0.05 s, poll rate 1000 ms" -1 -o 0.05 -a 1 -r 1 "$rtu" mb mbpoll/rtu-timeout 1 "Read output (holding) register failed: Connection timed out" -1 -q -o 0.05 -a 1 -r 1 "$rtu" # -b/-P/-s/-d are real tcsetattr() calls on a real line; a refused one # would fail at connect, before any of the above. mb mbpoll/rtu-9600n72 1 "Read output (holding) register failed: Connection timed out" -1 -q -o 0.05 -b 9600 -P none -s 2 -d 7 -a 1 -r 1 "$rtu" # -u is RTU-only, and it exits 0 even when it fails -- see Issues Found. mb mbpoll/rtu-slaveid 0 "Report slave ID failed(-1): Connection timed out" -1 -q -o 0.05 -u "$rtu" ;; *) bst_notok mbpoll/rtu-reaches-driver "rc=$rturc unrecognised: $(_bst_1line "$rtuout")" bst_notok mbpoll/rtu-line "probe gave a third answer; not classified" ;; esac fi