Unofficial notes · SIMATIC Connector for ROS · formerly ROSie
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.
01 · The thing people get wrong
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.
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.
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.
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
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.
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.
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.
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.
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.
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
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.
Sets the writer cycle 10.00 ms. YAML rate, capped at 1000 Hz.
RIB_App -l. Default 10 ms, minimum 1 ms, no upper bound.
Time from reading the index to finishing the payload. Grows with symbol count and system load.
Slow-motion factor. Real hardware runs this far too fast to see.
04 · Sizing · interactive
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.
| Dir | Message type | Rate | Segment | N | Buffer | Flag | |
|---|---|---|---|---|---|---|---|
| Totals | — | — | |||||
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.
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.
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.
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 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.
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.
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
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.
.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.
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.
# 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.
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.
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
.pscWindowsImport <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.
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.
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.
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.
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
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.
| Limit | Value | Where it bites | Source |
|---|---|---|---|
| Cyclic interrupt OBs | 20 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 Hz | Strings, 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 Hz | Rejected by YAML validation above this. Realistic only for flat scalar messages. | YAML config reference |
| Sequence length | 64 elements | Fixed at generation. Anything longer is silently truncated — data loss with no error. | ROXSIE limitations |
| String length | 254 bytes | Fixed byte array. wstring was unsupported until v0.3.2. | ROXSIE limitations · changelog |
| Client pairing | 1 ROS 2 + 1 SW-PLC | ROXSIE 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 safety | none | The generated C++ API is not thread safe. Your node's callback design has to serialise access itself. | ROXSIE limitations |
| RIB symbol capacity | 1024 (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 size | 1 MiB | The JSON connection response is capped; long symbol names and descriptions count. Failure reads TooLongMessage. | RIB §6.7 |
| VMM shared memory | 8 MB | Also bounded by Software Controller code memory, roughly 2–12.5 MB depending on CPU variant. | RIB §4.2.3 · ROXSIE limitations |
| Lifetime | min 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 clients | 128 guaranteed | RIB's own figure; not the ROXSIE figure. IPv4 only — IPv6 is untested. | RIB §6.1 |
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.
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.
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.
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.
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 driftA .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
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.
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
One hardware option, all of the software options. Versions are not interchangeable.
| Item | Choice | Versions | Example 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 medium | 6ES7647-8CE21-3AW1 | |
| IPC427E, IPC477E (Pro) BX-39A, PX-39A (Pro) |
NVRAM needed for remanent data without a UPS | 6AG4141-…, 6AG4142-… | |
| Industrial OS | SIMATIC Industrial OS (Linux) | V3.4.2 · V3.5.3 · V4.2 | 6ES7648-6LA81-0YA8 |
| Software Controller | CPU 1505SP (F) | V30.1 · V31.1 · V40.0 | included with the Open Controller |
| CPU 1507S (F) / 1508S (F) | V30.1 · V31.1 | 6ES7672-7AD02-0YG0 6ES7672-8AD02-0YG0 | |
| Engineering | TIA Portal | V19 (SW-PLC V30.1 only) · V20 ≥ Update 1 | 6ES7810-5CC16-0YA5 6ES7810-5CC17-0YA5 |
| Runtime middleware | Realtime Information Backbone | v2.1.1 · v2.2.4 | install on IndOS; RIB_App --version |
| ROS 2 | Humble or Jazzy | Jazzy for the current toolchain; C++17 | — |