Files
chuan 771a96fb51 fix: 修复仿真交互锁顺序并隔离会话历史
- 将网页输入和外设操作的仿真调度移到状态锁外,覆盖复位、退订和断线释放路径
- 通过真实 GPIO 边沿缓存送丝使能电平,移除快照及激光和波形复位中的反向 GPIO 读取
- 为每个 Renode 会话复制用户配置并设置独立的 Monitor 历史文件,保留原配置及其他设置
- 添加时钟锁和 GPIO 锁回归探针、会话配置与双实例隔离用例,并补充运行说明
2026-10-26 11:27:04 +08:00

44 KiB
Raw Permalink Blame History

Board protocol tests

Install the test dependencies from the repository root:

python -m pip install -r tests/requirements.txt

Run the same read-only identity/monitoring suite against either board:

python -m tests.board_protocol --port COM8 --expected-version 1.7.0.24
python -m tests.board_protocol --port COM12 --expected-version 1.7.0.24
python -m tests.board_protocol --port COM8 --compare-port COM12 --expected-version 1.7.0.24
python -m tests.board_protocol --port tcp://127.0.0.1:12321 --expected-version 1.7.0.24
python -m tests.board_protocol --port COM8 --compare-port tcp://127.0.0.1:12321 --expected-version 1.7.0.24

Options: --baud-rate (115200, serial only), --slave (8), --timeout (3 seconds), --samples (10), --interval (0.5 seconds), --supply-range MIN_V MAX_V (optional operating range; default requires a finite positive supply reading), --output (report directory). --expected-version is optional. Serial settings are 8N1. TCP carries the same RTU bytes and CRC, without an MBAP header. Close the host's connection to the tested endpoints first.

All groups leave a default 5ms gap after each response, matching the host application. --frame-interval-ms changes this gap; request latency excludes it. Requests are not automatically retried.

Physical reference priority

Under equivalent firmware, configuration and external stimulus, physical-board behavior is the reference for simulation acceptance. Retain discrepancies as failures for investigation; do not relax assertions or substitute responses to hide them. Different firmware versions, peripheral presence or dynamic measurements do not require byte-for-byte equality. Cases requiring external wiring are run against virtual COM/TCP only in the current acceptance round; COM8 participates in configuration and output-protocol checks.

Monitoring coverage boundaries

Temperature words use signed 16-bit tenths of a degree, following the register definition. Current LWS monitoring instead divides unsigned temperature words by ten. Raw words and both interpretations are retained under temperature_encoding; matching zero/positive readings do not validate the negative-temperature path.

TODO after hardware peripheral models are available:

  • Read A830/2 and apply the torch protocol's ±15V field applicability rules
  • Verify driver identity/version at 0428–042B; disconnected devices need not return nonzero values
  • Verify negative-temperature encoding and LWS presentation with the peripheral
  • Verify external status and analog responses against hardware stimulus

The current monitoring pass covers read-only responses, encoding and stated checks. These deferred hardware behaviors are not counted as passed. Vendor ID 0405, trigger input 0409 and current 041C are register-definition checks, rather than fields consumed by the current LWS monitoring view.

IO active-level configuration

python -m tests.board_protocol --port COM8 --group io-config --expected-version 1.7.0.24
python -m tests.board_protocol --port tcp://127.0.0.1:12321 --group io-config --expected-version 1.7.0.24
python -m tests.board_protocol --port COM8 --compare-port tcp://127.0.0.1:12321 --group io-config

This group follows the LWS IOServiceSelf and property-write paths. Inputs are read with 03 A900/61, extracting offsets 45–52; output active levels with 03 A950/10; manual output states with 03 A964/10. Each active level is written with function 06 through the same address mapping as InParamConfigModel / OutParamConfigModel. Configuration changes also verify that the unchanged external raw inputs remain unchanged. It verifies all 18 settings, restores the original value and verifies the complete configuration again. No standalone input-subregion or single-input-register reads are required because LWS does not use them here.

Original configurations may differ between devices; successful round-trips and restoration are evaluated independently. --samples and --interval apply only to the monitoring group. The controller must be connected and idle, with all manual output switches off. Preconditions and the baseline are checked again before each channel. Changing output polarity can change physical pin levels even though this group does not send manual output-switch commands. It does not test physical input stimulus or output wiring.

Before writes, io-recovery-<endpoint number>.json records the original active levels and identity. The record is updated with the active channel before each write. An uncertain write outcome triggers a fresh connection for restoration; the trial write is not retried. Failed restoration verification fails the run and stops further channels. Normal completion records phase=complete; an interrupted/failed run can record phase=restored with no active channel if cleanup succeeded. A process kill or permanent link loss cannot guarantee restoration: retain the recovery record and inspect the board before further tests.

Test contract

board_protocol/host_contract.py records the permitted read address/count pairs and write addresses, with references to the LWS caller. The runner refuses to send requests outside that group contract. Monitoring uses 0400/45 and A800/3; the standalone 0400/9 query used by the separate updater is not part of this LWS suite.

Requests not used by LWS must not become mandatory assertions merely because they appear reasonable under generic Modbus semantics. General inputs use A900/61; arbitrary subregion reads are outside the acceptance contract unless separately confirmed. Incorrect values, write acknowledgements or restoration results for the actual LWS requests still raise errors and return exit code 1.

Manual output switches

python -m tests.board_protocol --port COM8 --group io-outputs --expected-version 1.7.0.24
python -m tests.board_protocol --port COM12 --group io-outputs --expected-version 1.7.0.24
python -m tests.board_protocol --port tcp://127.0.0.1:12321 --group io-outputs --expected-version 1.7.0.24

The board must be connected and idle with all manual outputs off. This group tests O1–O10 one at a time at both active levels 0 and 1. It writes polarity at A950–A959 and sends ON/OFF commands using function 06 at A964–A96D, as in OutParamConfigModel. Original polarity and logical switches are restored after each case. It reads A964/10 (switches), A95A/10 (reported levels), and A950/10 (active-level configuration) as in the LWS configuration/monitor pages.

After each command, the target switch and level must match, all other channels must remain unchanged, and polarity configuration must remain unchanged. ON must report the configured active level; OFF must report its inverse. Low-active outputs therefore legitimately report ON=0/OFF=1. Incorrect post-command values fail the test; no altered address offsets, configuration changes or fallback reads are used.

The recovery record is saved before polarity changes and ON. An uncertain ON outcome or interruption triggers OFF and verified cleanup; the ON command is never retried. Failed restoration verification stops the run, including any second endpoint. Success means the original logical switch states and active-level configuration were restored after each polarity case. Initial reported levels are retained separately: readings before any explicit command may differ from readings after OFF, and this suite does not certify startup-state semantics. Final OFF levels are strictly checked.

These are protocol-level observations. Physical terminal voltage, attached loads and startup electrical state remain unverified without an external instrument. No motion, laser or feeder commands are sent by this group.

Digital IO and external signals

The existing-endpoint mode combines configuration, output, and external-input tests:

python -m tests.board_protocol --port COM8 --group digital-io
python -m tests.board_protocol --port COM12 --group digital-io --board-ws ws://127.0.0.1:12322/board
python -m tests.board_protocol --port tcp://127.0.0.1:12321 --group digital-io --board-ws ws://127.0.0.1:12322/board

Use the WebSocket URL belonging to the tested virtual board. Run each virtual endpoint separately; a single stimulus connection cannot drive two boards. Stop other applications from changing IO or starting jobs during a run.

The module covers:

  • 36 active-level configurations: six-axis positive/negative limits and driver alarms, I1–I8, and O1–O10. Each is changed with FC06, read back using LWS full blocks, and restored.
  • Ten output switches: ON/OFF, reported level, unrelated-channel isolation, configuration stability, and restoration. With --board-ws, actual simulated output GPIO snapshots are also checked.
  • 33 external inputs: six-axis limits and driver alarms, START/STOP/PAUSE, I1–I8, gas/water/laser alarm contacts, and the wire-break/ground-lock signal. Virtual inputs are changed through public WebSocket GPIO requests; status registers are never written. The test waits within --timeout for firmware polling, then checks the target and all other inputs.

The physical model is read with LWS MotionServiceSelf.GetPhysicModel (03 A25F/2). Inputs of excluded axes are marked unverified, not passed: firmware can skip their sampling. The model is not changed to make tests pass. For example, mask 0x2F enables XYZAC and excludes B.

Without a stimulus provider, all real input transitions remain unverified. To perform real contact tests interactively:

python -m tests.board_protocol --port COM8 --group digital-io --manual-inputs

Each prompt requests a low/high level and restoration. It requires the corresponding physical contact or fixture; pressing Enter alone does not pass a test. The firmware must remain idle. The test does not start a project or change alarm enablement, so input-level checks do not establish job-control behavior or alarm decisions. Laser-enable/PWM and gas-valve signals are recorded as read-only baseline observations; commanded actuation and terminal voltage remain unverified.

Each stage has its own recovery record. A failed assertion remains a failure. Independent input channels continue only after full input/configuration restoration is confirmed; communication errors, interrupts or failed restoration stop the run. Reports include complete TX/RX evidence and WebSocket application acknowledgements.

Recovery waits within the configured timeout for the old connection to release, probes identity with a read-only LWS monitor request, and checks the device identity before restoring. Only connection errors during this recovery probe are retried; unknown-outcome test writes are never repeated. Wrong CRCs, protocol exceptions and changed identities stop recovery rather than being hidden by retries.

Exit codes: 0 = all requested checks and coverage complete, 1 = failure, 2 = executed checks passed but functional coverage is incomplete, 130 = interrupted. A partial result is explicitly reported as PARTIAL, never as a full module pass.

If the virtual board is not running, start it in a separate terminal:

just run --no-debug --usb --com COM12

For direct TCP testing, stop the USB-mode instance and start network mode instead:

just stop
just run --no-debug --no-usb --uart-port 12321

The virtual COM bridge occupies the instance's single TCP client slot. Do not connect a direct TCP test client to that same instance while its virtual COM bridge is active. Serial/TCP comparisons therefore use separate runs against the same persistent images.

Complete isolated digital IO simulation

python -m tests.board_protocol --group digital-io --simulate --expected-version 1.7.0.24

This mode runs virtual COM and TCP separately. It creates temporary copies of both configured Flash images, starts its own Renode instances, chooses an unused COM port, and releases only the instances and port it created. Existing images must be present. The running board and its persistent images are not changed. Source-image hashes are checked after the runs.

Use --simulation-transport serial or tcp for one transport, --com COM14 to choose an unused virtual port, and --fast-serial / --no-fast-serial to override the .env timing setting. Fast mode accelerates serial timing; it does not certify physical waveform timing.

The disposable fixture configures XYZABC through the actual axis-model command. Firmware samples the GPIO inputs; no sample cache or firmware RAM is patched. The complete module covers:

Section Cases Assertions
Configuration 39 Six-axis, external START/STOP/PAUSE and general IO active levels; raw-input isolation, full-block readback and restoration
Outputs 20 Each O1–O10 tested at active level 0 and 1; ON/OFF, reported levels, simulated GPIO and configuration restoration
Inputs 33 Both levels of limits, driver alarms, external controls, general inputs and alarm contacts
Input modes 15 Press/release and connection-loss behavior for momentary and latched inputs
Alarm decisions 42 Both polarities of 18 axis alarms and laser/water/gas alarms, complete alarm bitmap, release
Homing 1 X searches its negative limit, external limit triggers completion, result and zero coordinate
Actions 6 Gas valve, laser enable/PWM at 50% and 100%, feed/retract, external start/pause/resume/stop

External-control polarity writes at A927–A929 follow the editable LWS configuration controls, despite the enum comments marking those addresses read-only. A write/readback discrepancy is recorded as a contract discrepancy for investigation, not automatically attributed to firmware. The X homing fixture reads the first four mechanical registers as required by this project; current LWS reads six.

The action fixture downloads and reads back a one-line project using the LWS request format. External controls follow the host path: GPIO changes produce A980 triggers, then the test issues the matching host command. Start/resume require actual X pulses. Pause allows deceleration, then requires both pulses and coordinates to remain stationary while virtual time advances; firmware retains its running flags during pause. Stop must return the controller to idle. Manual laser checks observe enable GPIO, firmware flags and PWM stream events. Feeder checks observe the emulated UART8 device state.

All 156 isolated functional cases must pass. An assertion failure stays failed; no alternative offsets or substituted reads are accepted. Cleanup checks that motion, laser, valve and feeder are stopped before the disposable instance exits. Reports retain request/response frames, applied GPIO acknowledgements, case outcomes and cleanup results. The parent directory contains summary.md and summary.json; serial/ and tcp/ contain detailed reports.

This scope does not certify terminal voltage, analog alarm thresholds, contact bounce, wire-break/ground-lock interlock logic, all homing algorithms, or real waveform accuracy. It does not test a real board or perform an OTA upgrade.

The default identity-monitoring group never starts/stops a board or writes registers. It issues two 0x03 queries per sample: 0400/45 for the complete monitor block (including identity/version), and A800/3 for connection state. It validates framing, CRC, counts, identity/version consistency, documented value encodings and a finite positive supply reading from the powered controller. Zero supply voltage fails; a specific operating range is checked only when --supply-range is provided. It stops testing an endpoint after a communication error instead of consuming possible residual bytes as a new response. The explicit io-config group temporarily writes configuration and the firmware may persist those writes; restoration is checked separately.

Reports go to .runtime/test-results/<timestamp>/report.json and report.md. JSON contains every TX/RX frame, register word, decoded sample, elapsed request time and check result. Reports retain only Renode's debug/USB/COM/fast-serial settings, excluding local image paths and process details. Exit code 1 means a protocol/encoding check, explicit expected-version check or communication operation failed. Cross-board differences, including firmware versions, are observations and do not fail a run.

For identity/monitoring, temperature/current/voltage range checks only validate the documented encoding. Zero values do not prove that an optional peripheral exists or that an ADC value is calibrated. Device/vendor IDs, hardware versions, accessory connection state and physical measurements may differ. No input stimulus or physical instrument is part of that group. The report's local Renode configuration is contextual metadata, not evidence of the remote device's identity.

The default simulation platform now supplies ADC inputs for approximately 24.0V and 60mA at idle. These are explicit environment baselines, not copied monitoring registers or a model of dynamic power consumption. Missing torch temperatures are still unverified rather than replaced with invented values.

Six-axis motion control

python -m tests.board_protocol --group motion --simulate --expected-version 1.7.0.24

The runner uses the same disposable-image and endpoint lifecycle as digital IO. It runs virtual COM and TCP by default; --simulation-transport, --com, and the serial-timing overrides also apply. This group currently requires --simulate: it changes mechanics, limits, coordinates and projects inside image copies, rather than changing a connected real board's machine setup.

Section Cases Assertions
Mechanical configuration 6 Project-required first four registers per axis, FC06 writes, signed offset words, readback/restoration and explicit fixture settings
Positive/negative steps 12 Target coordinates, per-axis integer pulse budget, other-axis isolation and DIR reversal
Direction polarity 6 Direction configuration reverses DIR without reversing positive coordinate semantics
Positioning 7 Six individual targets and one coordinated target, coordinates and pulse budgets
Continuous movement and stops 12 All axes, positive/negative movement, normal and fast stops, stable pulse counts and coordinates
Online speed 12 A000 and A0FD start paths, stable pre-change windows, D003 increase/decrease, active A011 stop and axis isolation
Coordinate commands 3 Clear/align without physical pulses; back-zero checks displacement-derived pulse budgets and final stability
Handwheel command format 12 Fifteen-register positive/negative targets from nonzero six-axis coordinates; arrival, displacement pulse budgets and other-axis isolation
Hard-limit protection 18 Movement stops after each axis's positive limit, negative limit or driver alarm; exact alarm bitmap
Soft-limit protection 12 Signed boundaries, readback, exact alarm bitmap, actual stop, other-axis isolation and recorded overshoot
Homing 12 Each axis with positive and negative limit sensors, real search pulses, completion and zero coordinate
Process lifecycle 3 Dry-run and weld-run pause/resume/stop, plus a finite dry-run that completes naturally

This module contains 115 cases. The distance-to-pulse conversion has been confirmed. Rotary back-zero and finite-track pulse discrepancies are failures, not deferred coverage; their root cause remains unresolved. Repeated 20mm XYZ project dry-runs produced 6396/6397 pulses against 6400, and TCP direct X steps produced 6395/6396. These observations do not yet identify whether firmware planning, timer/DMA simulation or GPIO counting caused the discrepancy. Pulse-budget assertions are active again; failures return exit code 1 without compatibility fallbacks. Earlier raw result files are retained unchanged.

Read/write packets follow MotionServiceSelf, property writes and the existing project fixture. Mechanical reads use the first four words as required by this project; current LWS reads six. Writes use individual FC06 commands. Signed offset-word checks do not validate actual nonzero homing offsets. Coordinate floats use ABCD order. Existing checks allow approximately one pulse of quantization tolerance; passing does not mean exact pulse equality. Budget records retain origin, target, resolution, counter start/end, expectation and actual difference. Motion on unrelated axes fails the case.

The homing fixture integrates actual PWM/DIR edge batches to drive each external limit contact. It releases the contact when the axis retreats and can trigger again when it returns. Contact transitions are recorded; the current acceptance requires contact activation, completion and the zero coordinate, rather than asserting an unconfirmed fixed contact sequence. DIR=0 as positive with configured direction polarity zero is an explicit fixture assumption, not a real-board direction calibration. Firmware-reported coordinates do not drive the sensor, and status/sample buffers are not patched. A reset, sequence gap, missing edge or pulse-count mismatch fails the fixture. --motion-timeout (20 seconds) bounds motion completion and homing separately from the RTU --timeout (3 seconds).

Pause and stop are checked against actual pulses and coordinates while virtual time advances. Tested stops wait a finite time for the pulse queue to drain before checking a separate stationary interval; cleanup does not establish the tested stop result. The stop-to-verification timestamp includes the stability window, not just physical stopping latency. Pause retains processing/moving flags [1, 1] under the existing fixture contract, with required lifecycle stages tracked independently. Running flags alone do not prove a stop. Cleanup allows a bounded pulse-settling interval, then requires all six axes stationary and all alarm contacts released. Failed cleanup stops the remaining scenarios; it is never counted as a pass.

Full movement and homing use --motion-timeout (20 seconds), while each RTU exchange retains its separate --timeout (3 seconds). A slower host must still reach the original target and exact pulse budget; extending the scenario deadline does not alter the expected movement. Arrival also waits for the hardware pulse queue to drain before checking the budget.

Online-speed cases explicitly configure and read back ±10000 travel limits so the longer sampling interval does not hit a default boundary before the change can stabilize. Each case stops, drains pulses, resets the fixture coordinate and restores the original limits; the separate soft-limit protection cases remain unchanged. Online speed checks compare rates measured from consecutive virtual-time windows before and after increasing/decreasing the command. The stability criterion (10% or two pulses per 250ms window) is not an absolute hardware speed specification. Scope excludes calibrated physical waveform timing, mechanical inertia, contact bounce, RTCP calibration/kinematics, rotary origin sensors, nonzero physical homing offsets, spline/arc geometry, and the deferred handwheel debugging/replay timing issue. These scenarios do not establish support for every declared motion register.

Projects and tracks

python -m tests.board_protocol --group projects --simulate --expected-version 1.7.0.24

This group requires disposable simulations. It fills all 20 project slots through actual create/download commands, tests shrinking an existing project, deleting/recreating the maximum ID, and leaves a separate deleted project absent for the persistence check. Original Flash images and any already running board are not changed.

Each transport runs two phases against the same copied Flash files: write/ downloads, reads and executes the fixtures; restart/ starts a new Renode process, verifies the retained catalog, names, headers, records, deletion and recent/default markers, then executes the retained control project. The restart phase never downloads the manifest's expected data again. expected-projects.json contains constructed expectations tied to this specific image-copy session.

The restart phase reconnects the inactive external contact fixture through GPIO and waits for alarms to clear. This restores external stimulus lost when the process exits; it does not rewrite persisted board configuration, project data or selection markers before their checks.

Coverage includes:

  • Project IDs, timestamps, two name batches, ten-character Chinese names, shorter replacement names, recent/default selection and deletion persistence
  • All 215 project-header words; a 300-track fixture crossing the header's second batch
  • Line, arc, circle, spline and intersection payloads, including six-axis float32 values
  • Spline payloads with 8, 9, 10, 11, 18, 19, 21, 40 and 80 coordinates, covering read-offset boundaries and extended write-byte-count wrapping
  • Delay, output on/off, wait-input and align-start records, plus separate spot-weld supplemental packets
  • Six sequential workstation IDs with encoded child IDs, and low/high contact-triggered workstations
  • Ordinary process 0 and empty-run process 50, with project-specific speeds, project switching and restart readback; empty-run checks compare only the defined speed field
  • Actual geometry execution, output/delay/input ordering, stop-closes-output policies, workstation production counts, and execution after process restart

Random workstations use an outbound/return path to their shared docking coordinate, matching the LWS pre-download validator. Overwriting a project selects that project before saving its processes. Cases using stop cleanup check all ten output GPIOs; individual readback cases check controller idle. Completion is evaluated only after final cleanup succeeds. Firmware versions are recorded independently and do not gate cross-transport acceptance.

Intersection execution currently verifies pulse activity and completion, without certifying the full spatial path. Spot-weld supplements are read back and exercised in dry-run; actual torch spot-weld behavior remains TODO until its external hardware model is available.

Normal records use the host's first-100/subsequent-120 read batches and offsets excluding their six-word header. Expected words are never taken from board responses. Each failed record remains failed while independent records continue after cleanup. Communication corruption or failed cleanup stops the endpoint. An exception response is recorded with its raw code and is never accepted as a successful round-trip.

Failed required assertions return exit code 1; there are no xfail exemptions or compatibility fallbacks. executed_all_stages distinguishes finishing every stage from passing every stage.

Use --simulation-transport serial or tcp to select one transport and --com to choose an unused virtual COM. The existing fast-serial overrides apply. Scope excludes physical-board testing, calibrated geometric/waveform accuracy, RTCP calibration, and unused or speculative request shapes.

Process parameters and peripheral integration

python -m tests.board_protocol --group process --simulate --expected-version 1.7.0.24

Each transport uses independent Flash copies and a write/restart pair of processes. The group covers ordinary process slots 0–7 (30 words), special slots 30–31 (32 words), and dry-run slots 50–51 (four-word host read, only speed is defined by EmptyProcVM). It writes all slots before selecting each again. Persistence follows the application's complete project import: save all processes, import the project header and track, then finish import. After restart it selects that project and checks the same constructed values without downloading them again.

Peripheral scenarios follow the active property-write callers: scalar torch, configuration and feeder fields use FC06; the 32-bit laser frequency uses a two-word FC10. Ordinary/sway laser settings load through the current nine-word torch reader, configuration through 27 words, and feeder parameters through 22 words. The unused bulk-save service methods are not promoted to required UI behavior. Image selection, feeder direction/speed, gas/laser/PWM/external-control scenarios, manual gas delays, finite light duration and active alarm contacts are checked independently. All actions require verified cleanup.

The test establishes a 1500W rated-power precondition before peak-power samples. Defined process fields exclude reserved offset 29; special processes retain offsets 30/31 and empty processes compare only speed. Feeder properties cover offsets 0–9 and 12, excluding unmapped language/reserved fields 10/11. In the 27-word configuration, offsets 8/10 are positive Y/X scan coefficients and 9/11 are signed Y/X center offsets; nine temperature thresholds occupy 15–23. Offsets 12–14 and 24 are not asserted by this loader. Samples respect the host UI ranges. The feeder save button sends FC06 013F=1; readback establishes the command behavior in the current instance, not power-cycle retention.

Torch scan registers 0000/0001/0006/0007/0008 require a connected torch, which is not modeled yet. These and actual peripheral acceptance/scan output remain explicit TODO coverage. Board laser fields 0002/0003/0004 are verified separately; register readback does not certify optical output. Modbus exception 0x88 maps to the host's NotConnected error and is not relabeled as an invalid parameter. Restart only restores inactive external GPIO levels before reading the retained project; it does not rewrite stored settings.

Gas opening (100 ms), delayed closing (150 ms) and finite laser enable (300 ms) are checked against new actual GPIO-edge counters and timestamps, with a fixed 20 ms scheduling tolerance. Snapshot-request time is not an edge timestamp. Stop has an independent running fixture and must close the laser, gas and wire before cleanup sends any explicit OFF commands. A failed pause leaves resume explicitly unverified in the JSON and Markdown reports.

An explicitly timed-out manual-light start is never resent. The group opens a new session and confirms the same board identity before cleanup and continuation; the missing acknowledgement remains a failed case. Framing/CRC errors or unsuccessful reconnection/cleanup still stop the endpoint. These are simulated functional checks, not calibrated physical light power or waveform measurements.

Feeder lifecycle assertions require both a low automatic-enable GPIO (PF2) and a stopped peripheral before manual OFF cleanup. Snapshots retain independent run permission and manual-direction switches, so a disabled permission cannot conceal an incorrectly active output. Manual feed/retract remain separate UART commands.

System configuration and data management

python -m tests.board_protocol --group system --simulate --expected-version 1.7.0.24

The write/restart pair contains 26 write-phase and four restart cases. It covers eleven application fields, all seven current UI torch-kind selections (including OTHER=6), application user-default save/restore, factory-reset repeatability and test configuration backup/mutation/restoration. Mechanical configuration uses only each axis's first four words with individual FC06 writes. Invalid checksum, shape or identity is rejected before any restoration write.

Automatic persistence is not required for manual-light duration A832. Restart retention excludes this field; normal read/write, isolation, and explicit user-default save/restore checks remain. See the protocol design requirements.

Rotation/chamfer checks start from a verified 0/0 baseline. The rotation case temporarily selects a C single-table physical model and restores it afterwards; it does not certify the UI soft-limit side effect or rotary motion. Torch-kind cleanup is verified by readback. Configuration-only finalization verifies idle and that no motion/laser/feeder action commands were sent, rather than invoking unrelated actuation tests. External GPIOs are held high; whether this is active depends on the tested polarity.

Before restart, all six axes receive independently constructed mechanical values and each general input/output polarity is changed from its baseline. Restart compares these custom values without rewriting them; unchanged baseline data alone is not treated as persistence evidence.

configuration-backup.json format 2 contains eleven application words, six four-word mechanical blocks and 39 IO settings in a shared address order (18 axis inputs, three controls, eight inputs and ten outputs). Old 36-word format-1 backups are rejected without migration. This is a test-fixture format; it is not the updater's native backup format and does not claim format interoperability. Factory-reset checks compare two independently executed resets around custom application, X pitch and I1 polarity changes, each checked before reset; they establish repeatability and removal of those custom values, not a complete externally specified table of factory defaults. Restart first reads active data, then separately restores the persisted user default.

Cycle registers declared only in the host enum currently have no application caller and are not promoted to invented mandatory read/write requests. Hardware calibration, RTCP and the updater's legacy backup-file compatibility remain outside this group.

Firmware update and startup

python -m tests.board_protocol --group update --simulate --expected-version 1.7.0.28 --upgrade-version 1.7.0.24

The runner requires a unique *Upgrade*.bin in the runtime firmware directory, or an explicit --firmware path. The reset vector must target the B region and the image must fit it. --upgrade-version is required and independently declares the expected post-update version; --expected-version checks only the initial source firmware. These versions may differ. Firmware files remain external artifacts and reports record their SHA-256, length and reset vector without local paths.

Each transport has four processes: full upgrade, restart/readback, interrupted first-packet transfer, and restart/recovery by full reflash. They share only their disposable Flash copies and a session-bound manifest containing the source image SHA-256. A failed installation blocks dependent A/B checks, and reports retain verified and unverified transfer stages. Coverage includes Factory selection and A/B round trips only after a verified installation, authorization query, rejection of an invalid inner packet CRC without programming Flash, 8192-byte numbered packets and the short final packet, completion, running version/identity, retained configuration and application-region SHA-256. The successful installation also performs factory reset and restoration of the supported configuration snapshot.

After erase acknowledgement, the test independently checks that the target region in the temporary persistent image is all FF. After the first packet, its hash must match the source and the unwritten tail must remain erased. These checks are required even when re-installing the same image; final version/hash agreement alone cannot prove programming occurred.

Frames follow the current updater's little-endian length/data fields and CRC coverage. Commands that the updater only sends are recorded without inventing an acknowledgement requirement. Residual input is captured before clearing, matching the updater's input-buffer handling; normal read responses and complete upgrade responses remain strictly validated. Exit from upgrade communication follows the installation's reboot flow. Interrupted transfer cleanup sends neither completion nor reboot before the owning process exits. Cleanup checks idle/output state without sending motion, laser, feeder or external-trigger commands.

Upgrade commands use a 30-second deadline for erase and packet acknowledgement. No unknown-outcome firmware packet is resent. An explicit CRC-error reply is a successful negative test; a successful acknowledgement of the deliberately invalid data CRC is a failed case. Raw system-information version/identity fields are not silently equated with ordinary monitoring fields.

Complete-transfer, process-restart and interruption-recovery phases also have an independent post-software-reset check for a finite positive supply reading; --supply-range adds an optional operating-range requirement. The check uses the normal 45-word host monitoring request and allows the ordinary finite response/settling deadline. A correct version and image hash cannot hide a powered-board voltage of zero. An earlier transfer/session failure can block this check; blocked steps are not passing evidence.

Communication recovery

python -m tests.board_protocol --group robustness --simulate --expected-version 1.7.0.28
python -m tests.board_protocol --group robustness --port COM8

The direct-endpoint command runs seven read-only cases: a fragmented monitoring query, invalid CRC/noise/incomplete-frame recovery, and three COM/TCP reopen cycles. Its allowlist contains only FC03 0400/45, A800/3 and A950/10; it never calls simulation setup/cleanup or writes configuration, projects, outputs or reboot commands. Endpoint versions need not match one another. The overall direct-endpoint result remains PARTIAL because disposable-project round trips and process restart coverage require separate fixtures; each executed case retains its own result.

The three invalid-input expectations use the measured COM8 firmware 1.7.0.24 as their explicit reference: bad CRC returns 08 83 94 90 9D, the fixed noise 00 FF 55 AA returns 00 FF 96 B0 5E, and the incomplete request 08 03 04 00 00 returns 08 83 95 51 5D. These are regression observations for this board, not a claim about every firmware or standard Modbus broadcast behavior. Both real and virtual tests record the response and verify a fresh identity query on the same session before comparing the response, so a reply mismatch cannot hide successful recovery. The real incomplete-frame test records an additional 1.1-second wait; this is a test window, not a claimed firmware expiry. Failed real cases reopen and strictly confirm identity before cleanup can succeed; the original failure remains failed.

Each isolated transport has seven independent process phases: nine cases for virtual COM and ten for TCP, including an extra old-partial-frame disconnect check for a truly rebuilt TCP session. Coverage includes fragmented extended project transfer/readback; invalid RTU CRC; noise; an incomplete request followed by recovery; three abandoned-read/reconnect cycles; and two further process restarts reading the retained project. A fault cannot contaminate the next phase's process. Original images and existing endpoints remain untouched.

Fragmentation splits actual writes across header and wrapped-byte-count boundaries, including an 80-point spline, without changing the valid protocol payload or assuming TCP write boundaries are message boundaries. Invalid probes observe the configured inter-frame gap and full request timeout. CRC rejection expects the application's CheckError=-108 exception (08 83 94 90 9D), with a valid response CRC. Noise and truncated responses must match the real-board reference. The host bridge preserves noise and incomplete requests, then delivers the original bytes through UART/DMA/IDLE after one second of host silence. The firmware generates the error reply; the bridge does not synthesize responses. This host coalescing policy preserves TCP fragmentation tolerance and does not reproduce physical RTU idle timing. Half-frame recovery additionally waits beyond this host-time window. A bridge that silently filters these requests therefore fails the response comparison even if its following normal query succeeds. Every fault is followed by a fresh normal request; no altered read shape or implicit action retry is used.

The reconnect scenario abandons a ten-word query, closes the application connection, then requires a correctly framed 45-word identity query on the new session. It does not infer recovery merely from opening a socket. Serial tests reopen the application-side COM handle; the virtual-COM helper keeps its bridge TCP session alive. Only the TCP variant establishes a new bridge TCP session. The two restart phases use graceful process shutdown and do not simulate abrupt power loss. A restart manifest is written only after a successful fragmented project roundtrip.

This category checks protocol function and recovery only; it excludes sustained-load, endurance and throughput tests.

LWS action sequences

python -m tests.board_protocol --group lws-sequences --simulate --simulation-transport both --com COM16 --fast-serial
python tests/external_devices.py --lws-only --transport both --com COM14
python -m unittest tests.test_sequence_support -v

Choose unused virtual COM names; the existing COM12 pair is not removed or reassigned. Every firmware run uses disposable images. lws-sequences is a focused entry for the process/peripheral category, not an additional protocol category. It checks actual A803/1 polling, the next open after automatic completion, an observed close/reopen window, startup cancellation and alarm-off hold windows. Laser and PF2 cumulative rising-edge counts detect a brief reactivation even if the final levels are low. Cleanup occurs after observation and cannot make a failed action pass. A missing acknowledgement remains failed; a normal reply with the required action or an explicit TaskBusy reply is distinguished from silence.

The external-device sequences reproduce digital graphic field verification using A250/5, five-field corrective writes, and the 200 ms shape-switch readback path. Alarm forwarding requires exact detail bits and nonzero A0D6; restoration checks all seven words. Torch power tests require its connection flag to clear and recover. The current ELF does not actively probe an idle feeder: its A802 value is retained as an observation rather than assumed to mean live device availability. Feeder tests issue a real parameter read while off and again after power-on; an exception for slave 10 in response to a slave-8 request remains a failure. Known response frames are fully consumed before identity validation so a rejected frame's CRC cannot pollute restoration.

For project/process/system restart phases, an absent, corrupt or stale manifest or an aborted write phase marks the dependent restart as NOT_RUN with a link to its prerequisite. Independent failed assertions do not prevent restart verification when the complete manifest was produced. Update recovery phases remain independent of this manifest rule.

This group does not certify arbitrary pipelined requests, unsupported addresses, queue saturation, real electrical interference or physical waveform accuracy. Such behaviors need their own explicit protocol contract. The digital-IO homing scenario also uses the existing pulse-driven limit sensor so retreat/recontact follows actual PWM/DIR rather than a permanently held input.

WebSocket and simulation clock locking

python tests/websocket_clock_lock.py
python tests/websocket_gpio_lock.py
python -m unittest tests.test_renode_session_config -v
python tests/renode_session_isolation.py

Six isolated simulations exercise device power, momentary input and disconnect release, plus ordinary input application, unsubscribe and disconnect release. A test-only probe holds the actual Renode clock lock, waits for the WebSocket request to reserve its operation, then acquires the board state lock and invokes the real GPIO pulse callback. Each case requires bounded completion and verifies the applied state; commands that require acknowledgement also require their matching request ID. These simulations have no persistent Flash backing and cannot write the user's images. This regression checks host lock ordering without changing firmware or using probabilistic motion timing.

The GPIO regression holds the actual signal lock while querying full and default feeder snapshots or resetting laser/waveform state. It requires each operation to return before releasing that lock, then executes the real GPIO callbacks under the board state lock. Power-off/high-enable/power-on checks verify that the feeder follows physical input changes while unpowered. Each simulation copies the user's Renode configuration into its temporary session and assigns a separate Monitor history file so parallel instances cannot crash while writing shared command history.

Session-config unit tests cover existing/missing general sections and history keys, LF/CRLF, BOM, paths containing spaces and missing user configuration. The integration check runs two simultaneous instances in session directories containing spaces, sends finite concurrent Monitor commands and verifies separate history contents, normal shutdown and unchanged user config and .env. Other user settings and comments are retained in the session copy; no original user file is rewritten.