# VSI twin templates

Small, hand-written vsiBuild command files that each prove one thing about
Innexis VSI 2026.2 on this machine. Every one here has been run through
`vsiBuild -c -f <file>` with a real build (bundled mingw64 11.2) on 2026-09-05.
The grammar is the binary's own (`help`, `help -gateway <g>`, `help -command <c>`),
not the PDF's; see the table at the top of `../README.md` for the differences.

Run one:

```powershell
. D:\InnexisVSI\innexis_home\vsi_2026.2\env_vsi.ps1
mkdir D:\vsi_work\templates\<name>; cd D:\vsi_work\templates\<name>
copy C:\Users\chris\Documents\Siemens\innexis-vsi\templates\<name>\<name>.vsi.cmd .
vsiBuild -c -f .\<name>.vsi.cmd
```

Run it through `Start-Process` with a bounded `WaitForExit` if there is any chance
the licence is unavailable: on licence failure vsiBuild raises a modal dialog and
never exits. Output lands in `.\workspace\<twinName>\`: `src\<component>\` holds
the per-component skeleton with **user custom code regions** (the only places to
edit; they survive `generate -overwrite`), `vsi.build\_objs\` holds the built
`FabricServer.exe` and one `VsiClientN.exe` per component, `vsi.sim` the run side.

| template | components | proves | result 2026-09-05 (all generated and **built**) |
|---|---|---|---|
| `cpp-pair-genericpayload` | C++ producer, C++ consumer, 2 int signals over `c++2DtGenericPayload`, `connect signals` | smallest complete twin; the toolchain compiles; `producer.cxx` here carries the counter in its *Before sending the packet* region | 44 s: FabricServer.exe, VsiClient0.exe, VsiClient1.exe |
| `python-cpp-pair-genericpayload` | Python producer (`python2DtGenericPayload`), C++ consumer | a Python component: skeleton `pyProducer.py`, gateway extension `VsiPythonGateways.cp313-win_amd64.pyd` compiled against CPython 3.13 | 40 s |
| `python-pair-genericpayload` | two Python components, no C++ | Python at both ends: `pySource.py`, `pySink.py` | 44 s |
| `ethernet-pair-tcp` | two C++ on `c++2DtEthernet`, one tcpIp `portSocket` (hub server, node client), signals both ways | the Ethernet form the ICD generator emits | 46 s |
| `can-frame-pair` | two C++ on `c++2DtCan`, one standard frame 0x100 with two 16-bit signals | the CAN/LIN form the generator emits | 39 s |
| `systemc-cpp-pair` | SystemC sensor, C++ logger over GenericPayload | a local SystemC component builds on mingw64 (systemc-2.3.4 ships in `common\systemc`) | 41 s |
| `fmu-cpp-pilot` | `qx250.fmu` (Amesim QX-250 6DOF, FMI 2.0 co-sim) as airframe, C++ pilot sending x/y/z/yaw references and reading x/y/z back; `double` signals | an FMU component; copy `qx250.fmu` next to the file first | 39 s |
| `python-delay-chain` | Python source, two instances of a reusable `VariableDelay` module back to back, Python sink; glue applied by region name with `apply_glue.py`; arithmetic proven by a plain-Python harness | a reusable multi-instance Python component with per-instance and run-time configuration; see its own README | 55 s |

`display fmuSignals airframe` lists all 17 FMU variables with VSI directions
(x_ref, y_ref, z_ref, yaw_ref inputs; x, y, z, u, v, w, phi ... outputs). The
archive is complete for Windows: `binaries\win64\qx250.dll`, `sources\qx250_fmu.c`,
`modelDescription.xml`.

## Running a twin (2026-09-08: all seven RUN)

Siemens issued `VeloceStratoOS` for vendor `saltd` on 2026-09-08 (`Downloads\license (9).txt`,
installed as `ActiveLicenses\velocestratoos-saltd.lic`, term to 2031) and the licence blocker is
closed. All seven templates now build AND run: 10,000,000 ns simulated, 100 steps per component,
exit 0, zero fatal or bind errors.

```
vsiSim <twin>.dt --batch --run --enableStats
```

⛔ **Do NOT use `mingw32-make sim`.** Under a redirected console it never runs: vsiSim waits for a
`run` command in its Simulation Control window and every `check.*.log` stays at 0 bytes for as long
as you wait. `--batch --run` exits by itself after the simulated end. (Reported by the JPO VSI
session from the Wildfire runs; the earlier text here pointed at the make target and was wrong.)

⚠ **Judge a run by the fabric log, not the exit code.** `check.<twin>.log` carries
`Total SystemC time = <ns>` and each component log carries one `VSI time:` line per step. The
63-component Wildfire twin completes its simulation and THEN dies at teardown with
`Error -1073740940` (0xC0000374, heap corruption), so vsiSim returns exit 1 and prints a
zero-filled stats banner for a run that actually succeeded. None of these seven two-to-four
component templates show that, so it looks specific to scale.

⚠ **Ports.** The fabric binds fixed ports `50101 + componentIndex`. That block used to sit inside
the Windows ephemeral range, so any outbound connection could steal one and a twin would fail
mid-block with `bind(): Error 10013`. Fixed machine-wide 2026-09-08 (roadmap `rm-uo6rsn4x`):
dynamic range moved to 50200, administered exclusion on 50101-50200. There is no application-level
workaround: `XL_TLM_SOCKET_PORT_NUM` logs a port change and the fabric still binds 50101.

⚠ **MAX_PATH breaks Python twins.** The Python gateway nests
`pythonGateways0\build\temp.win-amd64-cpython-313\Release\` under the twin. Past ~260 characters
the link fails with `cannot find ...VsiPythonGateways.cp313-win_amd64.def: No such file or
directory`, which names a missing file rather than a path-length problem. Build Python twins from a
SHORT root (`D:\vw` works; a deep temp path does not). Non-Python templates are unaffected.

⚠ **After copying a source file into a generated twin, set its mtime.** `Copy-Item` preserves the
SOURCE's timestamp, so `make` sees the `.o` as newer, relinks, exits 0 and compiles nothing. The
twin then runs perfectly with the old behaviour. Verify by grepping the build log for the file
name, never by reading the source.

Kill `FabricServer`/`VsiClient*` after any bounded run; logs are in `vsi.sim\_logs\check.*.log`.

### A bus name with a space silently collapses every per-bus period

`twin_glue/apply_glue_generic.py` keys per-bus periods on the generator's
`# bus <name> [<protocol>]` comment and reads the name as **one token**. A bus hint containing a
space therefore folds every flow into a single unnamed bus at period 1, which is exactly the
uniform-rate picture the per-bus work existed to remove, and it fails silently: the twin builds,
runs and passes its link check, only the rates are wrong.

Reported by the JPO VSI session, which now emits single-token names from `sysml2vsi.py` for this
reason. If you hand-write a command file with `# bus` comments, keep the name one token.

### Python and C++ clients run from DIFFERENT working directories

A generated Makefile launches the two languages from different places, so any per-client output
file written on a relative path lands somewhere different depending on the component's language.
The two `sim` targets, quoted from generated makefiles here:

```
C++     cd vsi.sim/_logs && set XL_TLM_AF_UNIX_PATH=.. && $(abspath ./$(TB).exe) --domain=...
Python  set XL_TLM_AF_UNIX_PATH=vsi.sim &&  python $(TB) --domain=...
```

The C++ client `cd`s into `vsi.sim/_logs` first. The Python client does not: it runs from the twin
root. Note `XL_TLM_AF_UNIX_PATH` differs to match (`..` against `vsi.sim`).

Observed, not just read off the makefiles. `python-delay-chain` writes `sink_trace.csv` on a
relative path and it landed at the twin ROOT, while the C++ Wildfire twin's traces landed in
`vsi.sim/_logs/`. Same code shape, two destinations.

⇒ If a template or its glue writes a file, either use an absolute path or resolve it against a
known anchor rather than the cwd. Reported by the JPO VSI session, whose `Trace` redirects into
`vsi.sim/_logs` when it can see that directory; confirmed here from both languages' artefacts.

### Python + Ethernet: an unconnected socket generates an uncompilable file

vsiBuild 2026.2 emits an EMPTY `def establishTcpUdpConnection(self):` for a Python component whose
Ethernet socket carries no connected signals. The file is an `IndentationError` before any user
code is added, so the twin cannot build. Reported by the JPO VSI session from
`Gimbal_Video_Aggregator`; **precondition isolated here 2026-09-08 with both controls**, two
components each, generate only:

| variant | Ethernet socket | outcome |
|---|---|---|
| signals connected across the socket | carries signals | method has a real `tcpListen` body, both files compile |
| signals defined, none connected across the socket | carries none | **empty method, IndentationError in both files** |
| no signals defined at all | n/a | vsiBuild refuses at generate: `No signals are defined for component` |

So the trigger is **an Ethernet socket with no connected signals**, not the Python gateway itself,
and not a component that merely lacks signals on other ports. A socket that carries traffic is
fine. Command files and the as-generated broken file are in `runs/evidence/`
(`python-ethernet-*-20260908.*`). Workaround until Siemens fixes it: give the socket at least one
`connect signals`, or drop that block to C++, which does not have the defect.

⚠ Relevant to the ICD pipeline, where Ethernet ports routinely get sockets and no signals because
Teamcenter records no signal-level mapping for them. That is the same gap behind the 246 flat
trace columns, so **any Ethernet-heavy block converted to Python will hit this.**

### CAN: generated stubs transmit unconditionally (scope matters)

The JPO VSI session found that every node carrying a `define frame` calls `sendCanPacket()` each
step with whatever is in `mySignals`, so a CONSUMER puts a zero-payload frame with the PRODUCER's
id on the bus. On the 63-component Wildfire twin, with 21 consumers of frame `0x100`, that drowned
the real producer: 147 of 177 links read 0 for all 100 steps. Their mute is
`#define sendCanPacket() setCanId(256)` in the consumer's *Global Variables & Definitions* region.
Do NOT wrap the send in `if(false)` across regions (it also mutes that component's generic-payload
sends) and do NOT early-return from *Before sending the packet* (it skips the end-of-step handshake
and deadlocks mainThread). Both measured by them.

RETRACTED 2026-09-08, same day it was written. This section briefly claimed the defect does
NOT reproduce at three nodes, on the strength of a `Can3` twin here. **That twin was not a valid
control and the claim is withdrawn.** Its consumers were declared `speed:int:input,rpm:int:input`,
so vsiBuild emitted no send call in them at all, and the twin never contained the mechanism the
finding is about. Checked in the generated sources rather than inferred:

```
Wildfire twin   22 components contain sendCanPacket()   (21 consumers + the owner)
Can3             1 component  contains sendCanPacket()   (the producer only)
```

**The precondition is signal DIRECTION, and it is the one line to get right when writing a CAN
template.** vsiBuild emits the send call in a node whose frame signals are `output` or `in_out`.
The ICD generator declares essentially every bus signal `in_out`, so every node on the bus
transmits, including ones that only consume. Counted in the two command files:

```
wildfire_grouped.vsi.cmd   329 in_out    56 input    56 output
can-frame-pair.vsi.cmd       0 in_out     2 input     2 output
```

Confirmed independently by the JPO VSI session, which also corrected a figure of mine: 44 files
contain `handleReceivedCanPacket`, but that is 22 `.cxx` plus 22 `.h`, so it is 22 components, not
44. Count components by `--include='*.cxx'`.

So the Wildfire finding stands as written and nothing here contradicts it. The lesson is the
instrument one: a small twin is only a control for this defect if its consumers actually get a
send call, which happens when the generator gives them output signals. A faithful minimal repro
would put three nodes on frame `0x100` with EACH declaring its own output field and inputs for the
others (say `speed:0:16` owned by `ecu`, `ack1:16:8` by `dash1`, `ack2:24:8` by `dash2`), so all
three transmit. **That experiment has not been run.** The threshold between 3 and 22 consumers is
unknown and unbisected. `D:\vw4\can3.vsi.cmd` is kept only as the example of the invalid
control, not as evidence about the defect.

The Python gateway extension is compiled against whichever interpreter the
`py` launcher defaults to; here that was `C:\Python313`, hence `cp313`. Run the
Python component with that same interpreter.

What the skeletons look like, so a template can carry real behaviour:

- C++ (`src/<name>/<name>.cxx`): regions named *Global Variables & Definitions*,
  *Constructor*, *Constructor End*, *Destructor*, *Start of main thread step*,
  *Before sending the packet*, *After sending the packet*, *Start of main thread
  iteration*, *Protocol's callback function*. Outputs live in `mySignals.<signal>`;
  increment them in *Before sending the packet*; read inputs in the callback.
- Python (`src/<name>/<name>.py`): the same region names in a `class <Name>`
  with `self.mySignals.<signal>`, a `run()` that connects to the fabric, and a
  main thread driven by `simulationStep`.

Not built yet, in the order they are worth doing: an Ethernet socket pair
(`add portSocket` tcpIp, hub and one client), a CAN frame pair (`define frame`),
SystemC pair, FMU plus C++ observer using `qx250.fmu`, and a Python-only pair.
A twin has not yet been *run* here: running needs the fabric server and clients
launched from `vsi.sim`, and may need the runtime licence features
(`s2sSystemBus`, `velpave360app`) that are still issued for the wrong vendor
daemon; see `../LICENCE-REQUEST.md`.
