# No BST-TIMEOUT marker, deliberately: measured at 14 s for 78 cases on the # BRINTOS-393 build (2026-08-08), which fits the bare 60 s default with ~4x # headroom and the 300 s the gates and the nightly pass with ~21x. A marker is # for a suite too heavy for the default; declaring one here would state # something untrue about this suite's weight. . "$BST_LIB" # liblightmodbus 2.0.2 (git 59d2b405) -- "a lightweight, highly configurable, # platform-independent Modbus RTU library" (meta-oe recipes-extended). What # ships is /usr/lib/liblightmodbus.a and the lightmodbus/ header tree. There is # no program, no pkg-config file and no shared object. # # --------------------------------------------------------------------------- # WHY THIS SUITE RUNS A PROBE # # liblightmodbus installs NO PROGRAM, and its upstream test/test.c is a COVERAGE # driver rather than an oracle -- CMakeLists only builds it under COVERAGE_TEST, # it needs the `examine` addon no release build compiles, and it asserts almost # nothing about the bytes. So there is nothing on the target's command line that # could exercise this library, and a suite that only checked the payload's shape # would go green on a library that answers wrongly. That is the BRINTOS-217 # lesson: oniguruma compiled and packaged perfectly and then handed every caller # wrong offsets. # # data/liblightmodbus-probe (src/liblightmodbus_probe.c) is the consumer that # closes the gap. It is cross-linked with wasmcc-glibc against THE VERY ARCHIVE # THAT SHIPS, and it drives the library the way a Modbus application does: # builds RTU frames for all nine function codes, hands them to an in-process # slave, parses the answers back, and prints the exact wire bytes at every step. # # --------------------------------------------------------------------------- # WHY THE WIRE BYTES ARE THE ASSERTION, AND NOT "IT RETURNED 0" # # Modbus puts its 16-bit fields on the wire BIG-endian and the trailing CRC # LITTLE-endian, so no single byte order is right for a whole frame. # liblightmodbus resolves that with modbusMatchEndian(), whose direction is # decided by a macro CMake bakes into libconf.h at configure time from # TEST_BIG_ENDIAN. A cross build that mis-detects this arm produces a library # that compiles, links, packages, and puts every register index and count on the # wire byte-swapped while returning MODBUS_ERROR_OK throughout -- and an # in-process round trip would still agree with itself, because both halves are # wrong the same way. Only comparing the bytes to a known-good build catches it, # which is why liblightmodbus/wire-index-big-endian and the frame comparisons # below exist at all. # # --------------------------------------------------------------------------- # THE ORACLE IS A NATIVE x86_64 CONTROL, PLUS THE SPEC ITSELF # # data/liblightmodbus-native.txt is the same probe's output against a build of # the IDENTICAL commit on x86_64 (src/liblightmodbus_mknative.sh). meta-oe's # recipe carries one patch, Khem Raj's GNUInstallDirs change, which moves where # the archive installs and touches no code -- so "the identical source" is # literal. Every LMB- line must match byte for byte; the three values that # legitimately differ (pointer width, the two struct sizes that contain # pointers, and the git-describe string CMake bakes in) are printed LMBV-, kept # out of the control, and reported separately. # # A control alone would not be enough. It is generated from a build, so a # control regenerated against a broken build could quietly move the expected # value with it. The ANCHORED block at the end therefore re-asserts six values # as LITERALS taken from the Modbus Application Protocol specification's own # worked examples -- the FC22 read-modify-write result, the FC15 example frame, # the published CRC of the spec's FC03 request -- so those cannot move whatever # any build says. D=${TMPDIR:-/tmp}/bst-lmb.$$ rm -rf "$D"; mkdir -p "$D" # has # # A red carries THE LINE THE PROBE ACTUALLY PRINTED, not just the needle that # was missing -- every probe case prints its measured value, so a failure is # self-describing and does not cost a second guest boot to diagnose. has() { case "$3" in *"$2"*) bst_ok "$1" ;; *) _n=${2%% *} _got=$(printf '%s\n' "$3" | grep "^$_n " | head -1) bst_notok "$1" "${_got:-no $_n line}" ;; esac } # lmb_get -- the value the probe printed for one case lmb_get() { printf '%s\n' "$out" | grep "^$1 " | head -1 | cut -d' ' -f2-; } # --- payload shape ------------------------------------------------------------ # The archive, the umbrella header, the four function-code header directories # and the CMake-generated libconf.h. A missing libconf.h is the interesting one: # it is not in the git tree at all -- configure_file() writes it -- so its # absence would mean the port shipped headers that cannot even be included. inv="" for f in /usr/lib/liblightmodbus.a \ /usr/include/lightmodbus/lightmodbus.h \ /usr/include/lightmodbus/libconf.h \ /usr/include/lightmodbus/master.h \ /usr/include/lightmodbus/slave.h \ /usr/include/lightmodbus/parser.h \ /usr/include/lightmodbus/master/mbregs.h \ /usr/include/lightmodbus/master/mbcoils.h \ /usr/include/lightmodbus/slave/sregs.h \ /usr/include/lightmodbus/slave/scoils.h; do [ -e "$f" ] || inv="$inv $f" done if [ -n "$inv" ]; then bst_notok liblightmodbus/payload "absent:$inv"; else bst_ok liblightmodbus/payload; fi # The standing static-only invariant, on an arm whose linker cannot make a # shared object at all. so=$(find /usr/lib /lib -name 'liblightmodbus*.so*' 2>/dev/null | head -3) if [ -n "$so" ]; then bst_notok liblightmodbus/no-shared-object "found: $so"; else bst_ok liblightmodbus/no-shared-object; fi # --- the shipped configuration, read off the header rather than off the probe - # This is deliberately independent evidence: the probe reports what the ARCHIVE # was compiled with, and this reports what a CONSUMER compiling against the # shipped headers would get. They can disagree -- a header tree from one build # beside an archive from another -- and that disagreement is invisible to any # test that only asks one of them. conf=/usr/include/lightmodbus/libconf.h if [ -r "$conf" ]; then check_out liblightmodbus/header-endianness "define LIGHTMODBUS_LITTLE_ENDIAN" cat "$conf" check_out liblightmodbus/header-version 'define LIGHTMODBUS_VERSION "2.0"' cat "$conf" check_out liblightmodbus/header-master "define LIGHTMODBUS_MASTER_BASE" cat "$conf" check_out liblightmodbus/header-slave "define LIGHTMODBUS_SLAVE_BASE" cat "$conf" # A big-endian define on this arm would be the corruption described in the # header comment, so it is asserted as an absence, not merely as "the little # one is there" -- both can be defined, and lightmodbus.h #errors if they are. if grep -q 'define LIGHTMODBUS_BIG_ENDIAN' "$conf" 2>/dev/null; then bst_notok liblightmodbus/header-not-big-endian "libconf.h defines LIGHTMODBUS_BIG_ENDIAN" else bst_ok liblightmodbus/header-not-big-endian fi else bst_notok liblightmodbus/header-readable "cannot read $conf" fi # --- the probe ---------------------------------------------------------------- # do_install ships data/ at 0644 and the rootfs is read-only at browser runtime, # so the payload arrives non-executable and must be staged somewhere writable. # test -f on the shipped file, never test -x: asserting -x on $BST_DATA passes # against a hand-placed copy and fails against every image that ever built it. probe=$D/liblightmodbus-probe if [ ! -f "$BST_DATA/liblightmodbus-probe" ]; then bst_notok liblightmodbus/probe-shipped "data/liblightmodbus-probe not installed" elif cp "$BST_DATA/liblightmodbus-probe" "$probe" 2>/dev/null && chmod +x "$probe" 2>/dev/null; then bst_ok liblightmodbus/probe-shipped out=$("$probe" 2>&1) has liblightmodbus/probe-ran "LMB-probe-end OK" "$out" # --- build configuration the archive was compiled with --------------------- has liblightmodbus/version "LMB-version 2.0" "$out" has liblightmodbus/endianness "LMB-endianness little" "$out" has liblightmodbus/modules \ "LMB-modules SLAVE_BASE MASTER_BASE F01S F02S F03S F04S F05S F06S F15S F16S F22S F01M F02M F03M F04M F05M F06M F15M F16M F22M SLAVE_USER_FUNCTIONS MASTER_USER_FUNCTIONS" \ "$out" # The parser union IS the frame buffer, and 256 is the RTU maximum the # protocol fixes -- it carries no pointer, so it is comparable across arms. has liblightmodbus/parser-size "LMB-sizeof-parser 256" "$out" # --- pure arithmetic: the pieces every frame is built out of --------------- has liblightmodbus/bits-to-bytes "LMB-bits-to-bytes 0:0 1:1 7:1 8:1 9:2 16:2 17:3 2000:250" "$out" has liblightmodbus/swap-endian "LMB-swap-endian 3412 FF00 ADDE" "$out" has liblightmodbus/match-endian "LMB-match-endian 3412" "$out" # --- CRC ------------------------------------------------------------------ has liblightmodbus/crc-empty "LMB-crc-empty FFFF" "$out" has liblightmodbus/crc-ramp-fold "LMB-crc-ramp-fold" "$out" # --- the bit-array helpers a coil response is decoded with ---------------- has liblightmodbus/mask-write "LMB-mask-write-pattern 499224" "$out" has liblightmodbus/mask-read "LMB-mask-read-pattern 100100100100100100100100" "$out" has liblightmodbus/mask-oob "LMB-mask-read-oob 255 255" "$out" # --- request building, all nine function codes ---------------------------- for f in 01 02 03 04 05 06 15 16 22; do has "liblightmodbus/build-fc$f" "LMB-req$f " "$out" done has liblightmodbus/build-refuses-count0 "LMB-req03-count0-rc 6 builderr=3" "$out" has liblightmodbus/build-refuses-count126 "LMB-req03-count126-rc 6 builderr=3" "$out" has liblightmodbus/build-refuses-2001coils "LMB-req01-count2001-rc 6 builderr=3" "$out" # --- round trips: every function code driven master -> slave -> master ---- for f in 01 02 03 04 05 06 15 16 22; do has "liblightmodbus/roundtrip-fc$f" "LMB-rt$f-rc serr=0 merr=0 sparse=0 mparse=0" "$out" done has liblightmodbus/read-holding "LMB-rt03-vals 4096 4097 4098 4099" "$out" has liblightmodbus/read-input "LMB-rt04-vals 40962 40963 40964" "$out" has liblightmodbus/read-coils "LMB-rt01-vals 1 0 1 0 0 1 0 1 1 0 1 0 0 1 0 1" "$out" has liblightmodbus/read-dins "LMB-rt02-vals 1 1 0 0 0 0 1 1 1 1 0 0" "$out" has liblightmodbus/write-single-reg "LMB-rt06-slave-reg3 17185" "$out" has liblightmodbus/write-multi-regs "LMB-rt16-slave-regs 273 546 819" "$out" has liblightmodbus/write-single-coil "LMB-rt05-slave-coils A7A5A5A5" "$out" has liblightmodbus/write-multi-coils "LMB-rt15-slave-coils A7CDA5A5" "$out" # --- the error paths a bus application actually depends on ---------------- has liblightmodbus/excep-illegal-address "LMB-ex-range-excep addr=27 fn=131 code=2" "$out" has liblightmodbus/excep-illegal-function "LMB-ex-fun-excep addr=27 fn=193 code=1" "$out" has liblightmodbus/reject-bad-crc "LMB-ex-crc-rc serr=5 merr=5 sparse=1" "$out" has liblightmodbus/ignore-other-slave "LMB-ex-addr-rc serr=0 merr=5 sparse=0" "$out" has liblightmodbus/broadcast-applied "LMB-ex-bcast-slave-reg5 7777" "$out" has liblightmodbus/broadcast-unanswered "LMB-ex-bcast-resp -" "$out" has liblightmodbus/write-protection "LMB-ex-nowrite-excep addr=27 fn=134 code=4" "$out" has liblightmodbus/write-protection-held "LMB-ex-nowrite-slave-reg6 1006" "$out" has liblightmodbus/short-frame "LMB-ex-short-rc 5 parse=2" "$out" has liblightmodbus/null-refused \ "LMB-ex-null slaveinit=4 slaveparse=4 slaveend=4 masterinit=4 masterparse=4 masterend=4" "$out" has liblightmodbus/slave-addr0-refused "LMB-ex-slave-addr0 3" "$out" has liblightmodbus/mismatch-value "LMB-ex-mism-value 5 parse=16" "$out" has liblightmodbus/mismatch-index "LMB-ex-mism-index 5 parse=14" "$out" has liblightmodbus/mismatch-address "LMB-ex-mism-addr 5 parse=13" "$out" has liblightmodbus/mismatch-function "LMB-ex-mism-fun 5 parse=12" "$out" # --- allocator churn ------------------------------------------------------- # Every exchange allocates and frees the request, the response and the # master's data buffer. On a guest whose whole heap is a linear-memory arena # inside a browser tab, that is the part of this port most likely to behave # differently -- and a leak or a use-after-free shows up as wrong data long # before it shows up as a crash, which is why the probe re-checks the answers # on every one of the 500 cycles rather than only at the end. has liblightmodbus/alloc-churn "LMB-churn cycles=500 bad=0" "$out" has liblightmodbus/initend-churn "LMB-initend-churn cycles=200 OK" "$out" # --- byte-identity against the x86_64 control ------------------------------ # No exclusions: the probe already routes everything that may legitimately # differ to LMBV-, so any LMB- difference at all is a finding. ctl=$BST_DATA/liblightmodbus-native.txt gotf=$D/got.txt ctlf=$D/ctl.txt printf '%s\n' "$out" | grep '^LMB-' > "$gotf" grep '^LMB-' "$ctl" 2>/dev/null > "$ctlf" if [ ! -s "$ctlf" ]; then bst_notok liblightmodbus/native-byte-identical "control absent or empty: $ctl" elif cmp -s "$ctlf" "$gotf"; then bst_ok liblightmodbus/native-byte-identical else bst_notok liblightmodbus/native-byte-identical \ "first diff: $(diff "$ctlf" "$gotf" 2>/dev/null | sed -n '2,3p' | tr '\n' ' ' | cut -c1-90)" fi # --- reported, never compared --------------------------------------------- # The values that legitimately differ between a 64-bit host and this 32-bit # arm. Named so a reader can see them, asserted only for presence. for k in pointer-width sizeof-master sizeof-slave git-version; do if printf '%s\n' "$out" | grep -q "^LMBV-$k "; then bst_ok "liblightmodbus/reported-$k" else bst_notok "liblightmodbus/reported-$k" "no LMBV-$k line" fi done # --- ANCHORED: the Modbus specification's own numbers ---------------------- # These duplicate comparisons above ON PURPOSE. Everything else here is # scored against a control that is itself generated from a build, so a # control regenerated against a broken build could move the expected value # with it. These six cannot move: they are literals out of the Modbus # Application Protocol specification's worked examples and out of the RTU # framing rules, and they are exactly the values a wrong endianness # configuration changes. # # Big-endian on the wire: request 03 built for index 0x1234 must carry 12 34 # in that order, and its count 0x0002 as 00 02. check_eq liblightmodbus/wire-index-big-endian 1234 "$(lmb_get LMB-req03-wire-index)" check_eq liblightmodbus/wire-count-big-endian 0002 "$(lmb_get LMB-req03-wire-count)" # ...while the CRC that follows goes out LITTLE-endian, which is why no single # byte order is right for a whole frame. check_eq liblightmodbus/wire-crc-little-endian 8287 "$(lmb_get LMB-req03-wire-crc)" # The published CRC of the specification's own FC03 request 11 03 006B 0003, # which the spec prints on the wire as 76 87. check_eq liblightmodbus/spec-crc-example 8776 "$(lmb_get LMB-crc-vec2)" # The specification's FC15 example: write 10 coils from 0x0013, values CD 01. check_eq liblightmodbus/spec-fc15-frame "1B0F0013000A02CD01C1AB len=11" "$(lmb_get LMB-req15)" # The specification's FC22 worked example: content 0x0012, And 0x00F2, # Or 0x0025 -> 0x0017. This is read-modify-write done by the slave on its own # memory, so it is the one anchor that proves the SLAVE side computed rather # than echoed. check_eq liblightmodbus/spec-fc22-result 0017 "$(lmb_get LMB-rt22-slave-reg4)" else bst_notok liblightmodbus/probe-stage "cannot stage $BST_DATA/liblightmodbus-probe" fi rm -rf "$D"