Model-driven simulation¶
The v2 source uses the published, exact modpoll-wot==0.2.0a1 dependency.
Install with poetry install. Model parsing and TM instantiation belong to that
shared package; Modsim owns state and protocol execution.
One device¶
poetry run modsim --config contrib/advantech/adam-5017uh/adam-5017uh-slot-0.tm.json \
--unit-id 1 --port 5020 \
--scenario contrib/advantech/adam-5017uh/temperature.sim.json
--config also accepts HTTPS model URLs. GitHub blob links are converted to
raw file URLs. Prefer a tag or commit SHA for repeatable runs; branch URLs change.
Downloads have a 15-second timeout and a 1 MiB limit, and use the shared bounded
JSON loader. Only models are downloaded; no script is fetched or executed from a
model. JSON-LD context URLs are vocabulary identifiers, not execution sources.
The configured runtime supports profile 0.2 integer types (16/32/64-bit, signed/unsigned), IEEE float16/32/64, fixed-capacity UTF-8 strings, register and physical scalar booleans, physical boolean arrays, legacy register bool/bool8/ bool16 arrays, register bit selectors and explicit legacy-MSB packed bit payloads. Byte and word ordering follow the shared contract; legacy packed bit numeric payloads ignore word ordering, and packed strings ignore both, matching Modpoll.
Initialization, script updates, generators and Modbus writes use the same codec.
Integer-wire multipliers are inverted with exact rational arithmetic: values that
require fractional raw integers or overflow are rejected instead of truncated.
Negative factors are supported. A zero factor can only encode engineering zero,
using raw zero. IEEE values use their representable wire precision; startup
reports and device.get return the decoded value. Model constraints apply after
quantization too: an unrepresentable exact const is rejected. Numeric initial
values use bounded candidates and arithmetic, not enumeration of 32/64-bit ranges.
Complex constraints may require an explicit initial value/default/enum.
Strings must fit their UTF-8 byte capacity, use zero padding and cannot end in NUL (consumers strip trailing NULs). Physical bit arrays have their declared length. Legacy register bool arrays retain the consumer's byte/LSB layout and preserve unused padding. No engineering-unit conversion is guessed for raw ADAM channels. Profile 0.2 is required; the unconfigured demonstration is unchanged.
Multiple devices on one listener¶
poetry run modsim --deployment examples/lab.sim.json
Deployment version "1" has one optional server object and a nonempty devices
list. Server fields are comm (currently tcp), bind (default 127.0.0.1) and
port (default 5020). --bind and --port override these listener settings.
Each device requires unique name and unitId (1–254), plus model (local path
or HTTPS URL). Optional fields: scenario, script, initial, parameters.
Parameters instantiate model placeholders. A concrete TD with a conflicting
Unit ID is rejected by the shared interpreter. A TD's remote endpoint never
selects the listener bind address.
Deployment files are local. Relative model/scenario/script paths are resolved against the deployment directory. Script paths inside a scenario are relative to that scenario. CLI paths are relative to the working directory. Device state, script module state and random generators are independent even when the same TM is instantiated repeatedly. Duplicate names/Unit IDs, unsupported fields and invalid initial values fail before the listener opens. Unconfigured Unit IDs return an error and never inherit another device's state.
Scenarios¶
Version "1" fields:
| Field | Meaning |
|---|---|
initial |
Property-name/value overrides |
tick |
Update interval in seconds, default 1, minimum 0.001 |
seed |
Per-instance random seed, default 0 |
generators |
Property-name to generator specification |
bindings |
Target property to source property for synthetic loopback |
writePolicy |
hold (default) or overwrite for generated properties |
script |
Local Python file |
parameters |
Script values exposed as device.parameters |
Generators: constant (value); sine (center, amplitude, positive period);
ramp (start, rate, minimum, maximum, clamped to bounds);
noise (minimum, maximum, uniform seeded noise). Integer-property noise
uses integer sampling to preserve the full 64-bit range.
Numeric generators round to the nearest integer for integer model properties.
Boolean channels can use constant values, bindings or scripts. All updates must
satisfy the model's schema and raw encoding bounds. Unknown properties are errors.
Binding chains and cycles are rejected; use a script for ordered dependencies.
Initialization order: model defaults/type fallback → scenario initial →
deployment/CLI initial → script initialize → first dynamic update at elapsed 0.
--initial and --initial-file remain available for single-device runs. Startup
logs include the final initialized values; raw values are synthetic unless the
contribution explicitly documents their source.
On each tick: generators → script tick → bindings. Modbus writes validate the
whole request, then run on_write and bindings before responding. Invalid write
requests or failed write hooks roll back register changes. Partial register
writes re-decode every overlapping property, including properties whose first
register was not touched. Batch updates and alias constraints are checked before
commit. Bit-field updates preserve neighboring bits. Protocol writes may not
change read-only/reserved bits; a writable bit sharing a register with read-only
bits can be changed while preserving those bits. Reads that would expose a
write-only field are rejected, while FC5/6 write echoes remain supported.
Model read-only measurements may be changed internally but cannot be written by clients.
hold stops a generator for a property after a valid client write until restart;
overwrite permits the next tick to replace it. Scripts define their own write
response and may deliberately overwrite values.
Local Python behaviors¶
poetry run modsim --config contrib/advantech/adam-5017uh/adam-5017uh-slot-0.tm.json \
--scenario contrib/advantech/adam-5017uh/scripted.sim.json --allow-scripts
Optional synchronous hooks:
def initialize(device):
device.set("AI0_RAW", 100)
def tick(device, elapsed, dt):
device.set("AI0_RAW", 100 + int(elapsed) % 100)
def on_write(device, changes):
if "AO0_RAW" in changes:
device.set("AI0_RAW", changes["AO0_RAW"])
Use get(name), set(name, value) and set_many({...}). Batch updates validate
all values before changing cells. Hooks run on the server event loop and must not
block; async hooks are rejected. Python is ordinary local code, enabled explicitly
by --allow-scripts. Each device loads its own module instance. Runtime errors
in periodic behavior stop the simulation and release its listener rather than
silently serving stale values. SIGINT/SIGTERM cancel updates and close the server.
The sample loopback is an explicit synthetic assumption, not a claim that real ADAM hardware automatically connects AO to AI or DO to DI. No fault injection, recording/replay, multi-listener runtime or remote behavior execution is included.