← Zinan Yang · lab
ROXSIE

Unofficial notes · SIMATIC Connector for ROS · formerly ROSie

ROXSIE
Architecture Lab

ROXSIE puts a ROS 2 graph and an S7-1500 Software Controller inside the same box, talking through a shared-memory window instead of a network. This page takes it apart: what generates what, where the determinism actually comes from, and which ceiling you hit first. Everything numeric here is traceable to the Siemens docs listed at the bottom.

Personal study notes — not a Siemens publication. The manuals on Siemens Industry Online Support are authoritative.

1
Device, two runtimes
8 MB
VMM shared memory cap
1000 Hz
Max topic rate
<20
Topics (OB ceiling)

01 · The thing people get wrong

ROXSIE is a code generator. RIB is the runtime.

Nothing called “ROXSIE” runs on the machine. ROXSIE is a containerised CLI that reads a YAML file and emits two artefacts; at runtime the data moves through the Realtime Information Backbone (RIB), a separate Siemens product with its own manual, versions and limits. Treat them as one thing and every performance question becomes unanswerable.

Engineering time — your laptop Runtime — the target device config.yaml topics · types · rates ROXSIE generator docker · CLI · v0.3.2 TIA Portal V20 UDT layout via .psc round trip ROS 2 package node + C++17 client API SCL source UDTs · DBs · connect FC RIB v2.1.1 / v2.2.4 lifetime buffer shared memory is the data path deploy import
Two artefacts, one runtime. The generator never appears on the target. The vertical rule is the handover: everything left of it happens once, at engineering time; everything right of it is what you are actually measuring when you talk about ROXSIE performance.

What ROXSIE owns

Parsing .msg definitions, flattening them to fixed-size SCL structs and matching C++ structs, emitting the connect state machine, wiring one cyclic interrupt OB per topic, and packaging the node with a Dockerfile.

What RIB owns

The lifetime buffer, the VMM shared-memory driver, symbol matching, the ODKP_ISC system function on the PLC side, the RIB_App broker and every timing guarantee — and every disclaimer about those guarantees.

What neither owns

DDS or Zenoh latency to the rest of your ROS graph, your Linux scheduler configuration, the OB cycle budget of your PLC program, and safety. RIB is not a safety path.

02 · Runtime architecture

One board, two operating systems, one memory window

The Siemens Hypervisor partitions an industrial PC: SIMATIC Industrial OS on some cores, the S7-1500 Software Controller on a core of its own. The interesting detail is that the TCP link is not the data path — it only negotiates which symbol lives at which offset.

SIMATIC IPC 427E / BX-39A · ET 200SP Open Controller CPU 1515SP PC2 / PC3 Siemens Hypervisor (RT-VMM) owns core 0 · pre-empts Linux · never pre-empts the Software Controller SIMATIC Industrial OS · Linux RT-Preempt cores 1…n · SCHED_FIFO · mlockall ROS 2 node generated package · Humble / Jazzy ROXSIE client API C++17 · not thread safe RIB Support Library librib_support.so RIB_App broker · taskset -c 0 · not realtime VMM shared memory lifetime buffer max 8 MB data plane · memcpy write read write read S7-1500 Software Controller own core · CPU 1505SP / 1507S / 1508S User program one cyclic interrupt OB per topic ODKP_ISC (SFC65490) INIT · CONNECT · READ · WRITE · DISCONNECT RIB data blocks generated UDTs, one DB per topic RIB_CONNECT_STATE_MACHINE_FC generated · drives the handshake control plane TCP 27567 · vNIC connect · symbol matching · disconnect — no payload crosses here
The socket is a registry, not a pipe. Client applications send RIB_App a JSON description of the symbols they provide and request; it matches them by case-sensitive name and hands back offsets. After that the payload never touches TCP — both sides read and write the same physical pages. That is also why killing RIB_App does not stop data flowing: the buffers keep working, the client logs the loss and never reconnects.

Core pinning is not optional

Linux shares core 0 with the hypervisor and the hypervisor wins. Pin RIB_App there (taskset -c 0) because it has no realtime requirement, and pin your realtime application to a core the hypervisor never touches.

Priority and memory locking

Non-root users cannot take RT priorities until rtprio is granted in limits.conf; then SCHED_FIFO at the priority you set and mlockall(MCL_CURRENT|MCL_FUTURE) to keep pages out of swap. Lock less than you use and the app crashes.

The MFP exception

On a CPU 1518(F)-4 PN/DP MFP only one core is available to Linux, so core pinning is pointless there — and RIB_App needs root, unlike on the Software Controller.

Shared memory is not private. The RIB manual states plainly that the shared memories used for data exchange are not exclusive to the applications that opened them, so malicious software could alter a lifetime buffer unnoticed. The RIB_App port is also fixed at 27567 and exposed on every interface RIB can reach; if the port is taken, RIB_App aborts. Plan the firewall rule (ufw allow from <SWCPU_VNIC> to <LINUX_VNIC> port 27567 proto tcp) as part of commissioning, not afterwards.

03 · The mechanism · interactive

The lifetime buffer, running

There is no lock. Consistency comes from a ring of segments sized so that a writer cannot come back round to a segment before the reader has had time to finish with it. Drive the three knobs below and watch where it breaks.

Writer: take the next segment, confirm its lifetime has expired, write the payload, then publish by updating LastValidBufferElementIndex in the 16-byte header.

Reader: note the time, read the index, read that segment, note the time again. If less than one lifetime elapsed, the data is consistent. If not, throw it away and repeat.

The segment count follows directly: N = 3 + Lifetime / Cycle‑time. The +3 is the margin — it buys the reader two extra writer cycles beyond the lifetime it was promised.

Lifetime buffer simulator
100 Hz

Sets the writer cycle 10.00 ms. YAML rate, capped at 1000 Hz.

10 ms

RIB_App -l. Default 10 ms, minimum 1 ms, no upper bound.

0.40 ms

Time from reading the index to finishing the payload. Grows with symbol count and system load.

1:100

Slow-motion factor. Real hardware runs this far too fast to see.

Segments N
13
Untouched for
12.00 ms
Margin over lifetime
2.00 ms
Torn reads
0
Reader window is shorter than the lifetime — every sample is consistent.
Why the writer never blocks. Nothing in this scheme makes the writer wait for a reader. If the reader is too slow it simply gets torn data and retries — it degrades into a retry loop, not a stall. That is the right trade for a control system, but it means a struggling consumer shows up as missed deadlines on the consumer, never as back-pressure you can see from the PLC. The RIB manual also warns against running the lifetime near its limit, because clock accuracy and memory management eat the margin.

04 · Sizing · interactive

Which ceiling do you actually hit?

Build an interface below. The table sizes each topic's buffer from the documented formula and checks it against every published limit at once. Select a row to drive the simulator above.

Interface budget
DirMessage type Rate Segment N Buffer Flag
Totals  
Cyclic interrupt OBs
0 / 20
Per direction
0 / 0
Shared memory
0 KB / 8 MB
Highest rate
Add a topic to size the interface.
How the segment size is worked out. ROXSIE flattens every ROS 2 message to a fixed-size struct: a string becomes a 254-byte array, a variable-length sequence becomes exactly 64 elements, and the RIB formula then rounds the sum up to the next multiple of 8. The byte figures in this table are computed from those documented rules, field by field — they are a close estimate, not a number Siemens publishes, and they ignore any per-field padding the generator may insert. Check against the generated SCL before you design a machine around them.

binding The OB ceiling

Every topic gets its own cyclic interrupt OB and a Software Controller supports 20 of them in total — for everything, not just ROXSIE. Fewer than 20 topics, and up to 16 per direction. This is almost always what stops you first.

watch Complex types above 100 Hz

The documented cap is 1000 Hz, but strings, sequences and nested structures “should be avoided at higher frequencies (>100 Hz)” because of data loss and PLC cycle time violations. Treat 1000 Hz as reachable only for flat scalar messages.

rarely binding The 8 MB window

The 64-element sequence cap is what keeps you under 8 MB — you structurally cannot put a point cloud through here. The one way to blow it is arrays of strings: 64 × 254 bytes is 16 KB per field, and JointState alone is about 18 KB per segment.

05 · Performance

The transport is free. The sampling is not.

The shared-memory hop is a memcpy of a few hundred bytes. What dominates end-to-end age is that both sides sample on independent clocks — and the two directions are not symmetric about it.

ros2_to_plc — one sampling boundary your publisher anywhere in the graph ROXSIE subscriber event-driven lifetime buffer PLC cyclic interrupt OB samples at rate user program DDS writes on arrival + up to 1 cycle of age plc_to_ros2 — two independent sampling boundaries user program PLC cyclic interrupt OB writes at rate lifetime buffer ROS 2 timer callback reads at the same rate publisher → DDS two clocks, never phase-locked worst-case added age ≈ 2 × cycle time, and it beats slowly through the data as the phases drift
The asymmetry is documented, not incidental. In the YAML reference, rate under ros2_to_plc “configures PLC interrupt cycle”; under plc_to_ros2 it “configures both PLC interrupt and ROS 2 timer callback”. So PLC → ROS 2 carries twice the sampling penalty of the other direction, and the beat between the two clocks means the latency is not constant — it walks.
Staleness budget for the selected topic

Read the bar honestly. The copy segment is drawn at a floor width so it stays visible; in reality moving a few hundred bytes between two cores' view of the same pages is nanoseconds to low microseconds. That is an order-of-magnitude statement, not a Siemens figure — but it is the right order, and it is why arguing about shared memory versus PROFINET on transport speed misses the point. What you buy with ROXSIE is the elimination of the network stack, the NIC and the second device; what you still pay is two sampling boundaries and whatever DDS does afterwards.

And determinism is conditional. The RIB manual is explicit: it does not guarantee deterministic exchange when system utilisation is high or when large data blocks are exchanged, and timing depends on utilisation, process priorities and hypervisor interrupts. The Software Controller side is genuinely deterministic — it has its own core and the hypervisor does not pre-empt it. The Linux side is only as deterministic as you configure it to be.

06 · How you actually use it

Generation is a loop, not a line

You cannot generate the ROS 2 package in one pass, because the C++ structs have to match the memory layout TIA Portal really produced. So ROXSIE stops halfway and asks for a .psc export of your project.

config.yaml you write this roxsie -c pass 1 → SCL · pass 2 → ROS 2 TIA Portal V20 import SCL · generate blocks deploy docker compose up *_roxsie_generated_rib_scl_structs.scl tia_project.psc — the real UDT layout, hashed for matching Linux / WSL Windows target device
The .psc round trip is the whole reason this is a two-pass tool. RIB matches generated structs by hash, so the C++ side must be derived from the UDTs TIA Portal actually built — not from the IDL you started with. Change a message type and the entire generation must be re-run; editing generated SCL or headers by hand is explicitly forbidden.
01
Prepare the engineering systemWindows + WSL

TIA Portal V20 ≥ Update 1 on Windows 10/11, ROS 2 Jazzy and Docker ≥ v20 with the compose plugin in Ubuntu under WSL, VS Code with the Remote Development pack.

02
Install the generatorLinux
# the installer creates roxsie_workspace/ wherever you run it
sudo chmod +x <ROXSIE_installer>
sudo bash <ROXSIE_installer>
# detects /opt/ros/jazzy; asks whether you use custom .msg packages

Writes .env with USER_ROS2_INSTALLATION_PATH, USER_ROXSIE_WORKSPACE_PATH and optionally USER_ROS2_INTERFACE_INSTALLATION_PATH. Move the workspace later and you must re-run the installer in the new location.

03
Write the interfaceinput/
system:
  ros2:
    package_name: roxsie_ros2_package
    node_name:    roxsie_ros2_node
  rib:
    RIB_App_IPv4: 127.0.0.1
    RIB_App_port: 27567
    RIB_Version:  "v2.2.4"

ros2_to_plc:
  topics:
    - type: "geometry_msgs/msg/Twist.msg"
      ros2_topic: "/cmd_vel"
      rate: 100.0

plc_to_ros2:
  topics:
    - type: "nav_msgs/msg/Odometry.msg"
      ros2_topic: "/odom"
      rate: 50.0

Optional plc: block renames RIB_CONFIG_DB and RIB_CONNECT_STATE_MACHINE_FC. Topic names must be unique; namespaces are allowed; rate must be > 0 and ≤ 1000.

04
Validate, then generate the SCLLinux
start-roxsie                       # or: docker compose run --name ROXSIE --rm ROXSIE
roxsie --verify -c config.yaml     # syntax check only
roxsie -c config.yaml              # pass 1 — stops and asks for the .psc
05
Import into TIA Portal and export the .pscWindows

Import <timestamp>_roxsie_generated_rib_scl_structs.scl as an external source file, generate blocks from it, then export the device as a PC system configuration file. No whitespace in the filename. Drop it into the ROXSIE input directory and answer the prompt.

06
Or do the whole thing headlessCI
roxsie --non_interactive -c config.yaml --psc_file tia_project.psc

-y requires -t. Useful once the interface is stable; useless for the first run, because you do not have a .psc yet.

07
Build the deployment containerLinux
bash build_deploy_container.sh \
  --ros2-version jazzy \
  --container roxsie_bridge_example \
  --rmw zenoh

Output lands under output/<timestamp>/output/ with plc_code/, ros2_deployment/, the config that was used, and logs/ — check *_extended_roxsie_logs.log when generation fails.

08
Bring it up in the right ordertarget
RIB_App --version            # confirm it matches RIB_Version in the YAML
RIB_App -l 10                # lifetime in ms; default 10, min 1
# then: download the PLC program, then: docker compose up

RIB_App first, PLC second, ROS 2 last. The connect state machine on the PLC drives the handshake; ODKP_ISC in OP_INIT belongs in the startup OB, never in a time-critical one.

Version coupling is tight and mostly undocumented in one place. RIB_Version in the YAML must match the RIB_App on the target (RIB_App --version); RIB v2.1.1 allows 1024 symbols and v2.2.4 allows 8192; TIA Portal V19 only pairs with Software Controller V30.1, and V20 must be at Update 1 or later. Industrial OS ships fixed to the hardware generation — v3.x on the IPC 427E and CPU 1515SP PC2, v4.x on the BX-39A and PC3. Getting one of these wrong usually surfaces as a connect failure, not a version error.

07 · Analysis

Every published ceiling, in one place

Sorted by how likely it is to stop you. The ones at the top you will meet on your first real machine; the ones at the bottom are theoretical for most applications.

LimitValueWhere it bitesSource
Cyclic interrupt OBs20 total
~16 per direction
Hard cap on topic count — and it is shared with the rest of your program, not reserved for ROXSIE.ROXSIE limitations
Complex types above 100 Hz> 100 HzStrings, sequences and nested structures risk data loss and PLC cycle-time violations. The practical rate ceiling, well below the documented one.ROXSIE limitations
Topic rate≤ 1000 HzRejected by YAML validation above this. Realistic only for flat scalar messages.YAML config reference
Sequence length64 elementsFixed at generation. Anything longer is silently truncated — data loss with no error.ROXSIE limitations
String length254 bytesFixed byte array. wstring was unsupported until v0.3.2.ROXSIE limitations · changelog
Client pairing1 ROS 2 + 1 SW-PLCROXSIE allows exactly one of each, although RIB itself guarantees 128 simultaneous socket clients. No multi-node fan-out through one bridge.ROXSIE limitations · RIB §6.1
Thread safetynoneThe generated C++ API is not thread safe. Your node's callback design has to serialise access itself.ROXSIE limitations
RIB symbol capacity1024 (v2.1.1)
8192 (v2.2.4)
Total symbols after flattening — a handful of wide messages gets you there faster than you expect.ROXSIE limitations
Connect response size1 MiBThe JSON connection response is capped; long symbol names and descriptions count. Failure reads TooLongMessage.RIB §6.7
VMM shared memory8 MBAlso bounded by Software Controller code memory, roughly 2–12.5 MB depending on CPU variant.RIB §4.2.3 · ROXSIE limitations
Lifetimemin 1 ms
default 10 ms
Set once per RIB_App instance, not per topic. No upper bound, but running near the limit is advised against.RIB §6.1
Socket clients128 guaranteedRIB's own figure; not the ROXSIE figure. IPv4 only — IPv6 is untested.RIB §6.1

Failure modes worth designing against

Silent stale data

If RIB_App dies, the client notices and logs it — then never reconnects, while shared-memory reads and writes keep succeeding. Without your own heartbeat in the payload, both sides happily process data that stopped updating. Add a counter and check it.

Truncated sequences

A vector longer than 64 elements loses the remainder. No exception, no return code, nothing in the PLC. Validate length at the publisher, not at the bridge.

Case-sensitive name matching

RIB pairs provided and requested symbols by exact, case-sensitive name. A mismatch is simply an unmatched symbol — the connection succeeds and the data never arrives.

The init cost

ODKP_ISC in OP_INIT resolves and decomposes every DB symbol; the manual warns that a high symbol count can push the OB into milliseconds. Startup OB only, and raise the CPU's maximum cycle time if the interface is large.

Pointer-update stalls

When the symbol set changes, refreshing a large number of symbol pointers inside the application loop takes real time. On a realtime thread that is a missed deadline.

.psc drift

A .psc from a different YAML fails generation with Wrong PSC File For Provided YAML Configuration. The hash check is doing its job — but it means your TIA project and your config are one versioned unit.

08 · The decision

What you are actually choosing between

The alternative to ROXSIE is not “no ROS”. It is a separate industrial PC talking to the PLC over PROFINET or OPC UA. Here is the difference, drawn.

A · ROXSIE — one device ROS 2 node shared memory SW-PLC one memcpy · no NIC · no cable · no protocol stack B · Conventional — two devices ROS 2 node PLC program OPC UA / PN stack + NIC OPC UA / PN stack + NIC cable added: two stacks, a link, a second device to power, mount, patch and spare Option B buys back: any topic count, any message size, unlimited nodes, free choice of PLC hardware, and a ROS box you can specify for GPU compute. Option A buys: one enclosure, and no network in the loop.
Draw the edges that change. ROXSIE removes two protocol stacks, a NIC pair and a cable. It adds a hard coupling between your ROS interface and your TIA project, and it constrains you to Siemens Industrial OS hardware with a Software Controller.

Reach for ROXSIE when

  • The robot is the machine — one enclosure, one control cabinet, no room for a second PC.
  • The interface is narrow and stable: a handful of scalar or small fixed-size topics.
  • You need perception or planning in ROS but the motion and interlocks stay in the PLC, certified.
  • Removing the network from the loop genuinely matters — EMC, cable count, cost per unit.

Do not reach for it when

  • You need point clouds, images, or any sequence longer than 64 elements.
  • More than about a dozen topics, or an interface that will keep changing.
  • Several ROS nodes need to touch the PLC directly — you get exactly one client.
  • You want GPU compute on the same box, or a PLC that is not a Software Controller.
  • Anything safety-related crosses the boundary. It does not.

The honest summary

ROXSIE is a good answer to a specific question: how do I get a modest, well-defined data interface between ROS 2 and a Software Controller without a network? It is a poor answer to how do I connect ROS to my PLC in general. The narrower and more stable your interface, the better it looks.

09 · What you need to order

Hardware, software, access

One hardware option, all of the software options. Versions are not interchangeable.

ItemChoiceVersionsExample MLFB
Hardware
one required
ET 200SP Open Controller
CPU 1515SP PC2 / PC2 F
IndOS v3.x · SW-PLC v30.1 / v31.1 6ES7677-2DB43-0GB1
6ES7677-2SB43-0GB1 (F)
ET 200SP Open Controller
CPU 1515SP PC3 / PC3 F
IndOS v4.x · SW-PLC v40.0 6ES7677-2DD43-0GB0
6ES7677-2SD43-0GB0 (F)
SIMATIC IPC (box / panel)
IPC227G, IPC277G — CPU 1507S only
≥ 4 GB free on target medium6ES7647-8CE21-3AW1
IPC427E, IPC477E (Pro)
BX-39A, PX-39A (Pro)
NVRAM needed for remanent data without a UPS6AG4141-…, 6AG4142-…
Industrial OSSIMATIC Industrial OS (Linux)V3.4.2 · V3.5.3 · V4.26ES7648-6LA81-0YA8
Software ControllerCPU 1505SP (F)V30.1 · V31.1 · V40.0included with the Open Controller
CPU 1507S (F) / 1508S (F)V30.1 · V31.16ES7672-7AD02-0YG0
6ES7672-8AD02-0YG0
EngineeringTIA PortalV19 (SW-PLC V30.1 only) · V20 ≥ Update 16ES7810-5CC16-0YA5
6ES7810-5CC17-0YA5
Runtime middlewareRealtime Information Backbonev2.1.1 · v2.2.4install on IndOS; RIB_App --version
ROS 2Humble or JazzyJazzy for the current toolchain; C++17