Running and Controlling
The stimulus glue: counters, monitors, traces and per-bus rates
A generated twin does nothing by itself: every signal stays at zero and every component
sends its zeros each step. The glue in innexis-vsi\twin_glue derives a stimulus from the
twin's own command file so that any twin, however large, can be exercised and checked
without writing behaviour by hand.
Roles, decided from the command file
- A signal that is the source of a
connect signalsline PRODUCES: it carries an 8-bit counter, distinct per signal, advancing everyperiodsteps. - The destination of a
connect signalsline CONSUMES: it is monitored (last value, change count) and traced. - On a CAN or LIN frame id defined by several components, the FIRST component defining it produces its frame signals and every other node consumes the same-position signal.
- Everything else (Ethernet sockets carry no signals) is traced only, and the checker reports such components as "nothing to check", never as passes.
The produced value is one expression, (k times 16 plus step divided by period) and 0xFF,
in generic_stim.h for C++ and generic_stim.py for Python, and the checker's self-test
proves the two agree with its own copy.
Per-bus periods (EXERCISED 2026-09-08)
Each producing bus, the generator's # bus NAME [PROTOCOL] group that a connection belongs
to, gets its own period from the list 1, 2, 3, 4, 5, 6, 8, 10, 12 in file order, baked into
the generated call as twin::produce(g_step, k, period). On Wildfire that gives nine
distinct rates, from the rangefinder serial link changing every step to the CAN control bus
changing every twelfth. The table lives in glue_wiring.json under buses, and every
produce entry carries its period as a third element. These are a stimulus schedule chosen
to make rates distinguishable in a visualisation; Teamcenter records no signal timing, so
they are not the real system's rates and must not be presented as such.
The bus name in that comment is read as one token. A name with a space folds every flow into one unnamed bus at period 1, silently: the twin still builds, runs and passes the link check with the wrong rates. Generators in this workspace write names without spaces.
Seeing the rates
Those nine periods are nine visible densities of traffic on an animated page. One symbol leaves its sender for every producer value change, travels to its bus, and fans out to the components that subscribe to it:
- Wildfire, 63 components, 459 messages and 1,930 deliveries over 100 steps
- Wildfire with four Python components, the same twin, four blocks rewritten
- MSP-2 with a SysML v2 block, 8 components
- The wizard's three-component twin, the smallest one
Append #t=36.8 to any of them to open paused at that step, which is how to catch the CAN
broadcast mid fan-out.
The pages carry the same three caveats this one does, stated on the page rather than left to
a reader: a message is derived from a value change because VSI logs no packets, the drawn
transit time is a visual convention rather than the 0-or-1-step delivery lag, and the
periods are a stimulus schedule rather than the real system's timing. They are generated
by vsi_flow.py in the vsi-message-flow repository, which ships a checker with negative
controls that fails the build if those sentences are edited out.
CAN consumers are muted
vsiBuild emits the send call in every node that has output or in-out signals on a frame,
and it sends unconditionally every step. The generator declares every bus signal in-out,
so all 22 nodes on Wildfire's frame 0x100 transmit, and 21 of them put a zero-payload frame
with the producer's id on the bus. Measured: every consumer read 0 for all 100 steps while
the owner sent 96 plus step. The glue mutes each non-owner: in C++ a macro in the globals
region rewrites the one sendCanPacket() call site into a harmless call, in Python the
gateway alias is rebound to a wrapper that swallows that one method. Neither wraps the send
in a block across regions (that also silenced the same component's other sends, measured)
nor returns early from a region (that skips the end-of-step handshake and deadlocks the
main thread). The muted list is silent_can in glue_wiring.json. The threshold at which
drowning starts has not been measured; a valid small control needs consumers that carry a
send call, which a three-node twin with input-only consumers does not.
Where the glue goes
apply_glue_generic.py TWIN.vsi.cmd TWIN_DIR patches three user regions of every
component, recognising both the C++ // and the Python # region markers:
- "Global Variables and Definitions": the include or import, the trace object, the consumer monitors, and for a muted CAN consumer the mute.
- "Before sending the packet": one
produceassignment per produced signal. - "After sending the packet": one
observeper consumed signal, the trace row, and the step increment.
It copies both stimulus files into the twin's src, writes glue_wiring.json (roles, frame
owners, muted nodes, the bus table, the language per component) and, for every Python
component, compiles the file to catch a skeleton that is not valid Python. Re-run it after
every generate; it rewrites all three regions each time.
A Python client runs from the twin root rather than from vsi.sim\_logs, so the Python
trace writer redirects into vsi.sim\_logs when it can see that folder, and one traces
directory serves the checker for a mixed twin.
The scripted alternative: msp2_glue
For a small twin whose behaviour matters more than coverage, msp2_glue is the model: a
header with a scenario (card insertion at step 3, boot to healthy at 20, a fault at 60 that
clears at 65, a heartbeat stall from 70 that trips a watchdog at 75), a host-side test
program that proves the scenario with negative controls before any twin is built, and an
apply script that writes the calls into the seven skeletons by region name. The run is
judged by reading the traces against the scenario, and it matched exactly with the designed
one-step lag on every reaction.
Source: innexis-vsi/twin_glue/ (generic_stim.h, generic_stim.py, apply_glue_generic.py, README.md), msp2_glue/ (EXERCISED 2026-09-06 to 2026-09-08) · retrieved Tue Sep 08 2026 00:00:00 GMT+0000 (Coordinated Universal Time)