Skip to content

Testing and interop

Two kinds of OT Fabric repositories share the testing of PROFINET. profinet-interop owns the reference implementations: adapters around independent PROFINET stacks behind one container contract, pinned by commit or version and hash. It knows nothing about any library under test. go-profinet is a consumer: its interop tests drive the images on Docker bridge networks, own the scenarios and assertions, and write the results that profinet-interop’s matrix is generated from.

profinet-interop v0.6.0 (container contract 1.2) packages:

Adapter Implementation Version Licence Role go-profinet tested as
pnet rt-labs p-net (C) ea5e554 (2023-11-10) GPL-3.0-only or commercial IO-Device IO-Controller, DCP requester, LLDP agent
pnio-rs pnio-rs (Rust) v0.1.0 MIT OR Apache-2.0 IO-Device IO-Controller, DCP requester
profinet-py profinet-py (Python) 0.6.6 GPL-3.0-only or commercial IO-Controller, DCP tool IO-Device, DCP responder
pnio-dcp pnio-dcp (Python) 1.2.0 MIT DCP tool DCP responder
umrp umrp (C, user space) v4.0 GPL-2.0 MRP ring node MRP manager, client and automatic manager
mrp microchip-ung/mrp (C, on the kernel’s bridge MRP) 177717e GPL-2.0 MRP ring node MRP manager, client and automatic manager
netsnmp Net-SNMP manager 5.9.3 BSD-style SNMP manager SNMP agent of the IO-Device
scapy, tshark scapy contrib pnio, Wireshark tshark 2.6.1, 4.0.17 GPL-2.0 Decoders Every capture

The p-net tree is the last complete public GPLv3 snapshot: since 2024 the public branch of p-net is an evaluation version without build files, ports or sample application. That tree predates p-net’s v1.0.2 security fixes, so it is a lab oracle for isolated test networks only. Two candidates were evaluated and not used: profinet-python (no licence, and a Connect that compliant devices reject) and the PI Community Stack (members only, under an agreement that forbids combining it with copyleft code).

Every upstream is pinned in versions.yaml. No image is published: most of the implementations are GPL, so every user builds the images from upstream. A weekly workflow compares each pin with its latest upstream release, builds and smoke-tests a candidate, and keeps an issue open while any pin is behind; pins are never moved automatically.

  1. Build the images, tagged profinet-interop-<adapter>:dev:

    Terminal window
    git clone https://github.com/otfabric/profinet-interop && cd profinet-interop
    make images
  2. Every container needs NET_RAW and NET_ADMIN, and except for the MRP ring nodes, a Docker bridge network with a static address:

    Terminal window
    docker network create --subnet 10.77.0.0/24 pn
    docker run -d --name pnet --network pn --ip 10.77.0.10 \
    --cap-add NET_RAW --cap-add NET_ADMIN profinet-interop-pnet:dev device --station pnet-dut
  3. Talk to it, with another adapter or your own code:

    Terminal window
    docker run --rm --network pn --ip 10.77.0.4 --cap-add NET_RAW --cap-add NET_ADMIN \
    profinet-interop-pnio-dcp:dev dcp identify-all

Every image has the same entrypoint and commands for its roles: print-capabilities and version, device (runs until stopped and writes JSON Lines events), controller (one scenario, one report), dcp <operation>, decode and check for the decoders, node and status for the MRP ring nodes, and snmp for Net-SNMP. LLDP does not cross a Linux bridge unless the bridge forwards its group address, so the harness sets group_fwd_mask, and reports LLDP as not applicable where it cannot. The container contract has every flag and document.

go-profinet’s Interop workflow runs on pull requests, pushes to main and weekly; a Kernel MRP workflow runs the microchip-ung/mrp tests in micro-VMs with a Linux kernel built with bridge MRP and without switchdev, which hosted runners do not have. Every exchange is captured and decoded by scapy and tshark, and a frame that cannot decode, or that tshark warns about, fails the test unless it is an attributed deviation of the peer.

Peer go-profinet as Pass Fail n/a Peer deviation
rt-labs p-net IO-Controller, DCP requester, LLDP agent 56 0 1 0
pnio-rs IO-Controller, DCP requester 13 0 3 0
profinet-py IO-Device, DCP responder 32 0 1 3
pnio-dcp DCP responder 8 0 0 0
umrp MRP manager, client and automatic manager 32 0 0 2
microchip-ung/mrp MRP manager, client and automatic manager 31 0 1 0
Net-SNMP SNMP agent of the IO-Device 29 0 0 0
scapy and tshark Decoder of every capture 47 0 0 9
Total 248 0 6 14

The n/a are limits of the peers: pnio-rs has no diagnosis records and no DCP Set, p-net and pnio-rs refuse re-parameterization in data exchange, profinet-py ignores a GSDML’s UsedInSlots, and the kernel flushes its forwarding table where the mrp adapter cannot observe it. The deviations are profinet-py’s I&M1 write with a block two octets too long, umrp’s automatic manager reclaiming the manager role as a client, and decoder limits. The full matrix, feature by feature with the evidence of each check, is MATRIX.md, summarized in go-profinet’s FEATURE-STATUS.md.

Terminal window
make interop-images # clones otfabric/profinet-interop and builds its images
make interop # runs the suite; results in interop-results/
make interop-matrix # regenerates profinet-interop's matrix from them

go-profinet’s own tests run a controller and devices on the in-memory virtual Ethernet: end-to-end tests of every service, chaos tests with seeded loss, duplication, reordering, corruption and jitter and pulled cables, a fuzz target for every decoder, and real AF_PACKET tests on a veth pair in a privileged container. Coverage is held at 90 % or more, and a release needs 25 green soak runs. profinetctl’s tests run its command tree against go-profinet devices on the virtual network, and make pnet-smoke runs the DCP, record, I&M, diagnosis, topology and AR commands against p-net on a real Linux bridge.

Validated: DCP in both roles against p-net, pnio-rs, profinet-py and pnio-dcp; the controller’s legacy startup against p-net and advanced startup against p-net and pnio-rs, with cyclic data at 1 ms against p-net, records, I&M, diagnosis, alarms, ModuleDiff, shared device with two go-profinet controllers on p-net and port checks that p-net evaluates; the device against profinet-py’s controller with cyclic data, alarms, records over a device access AR and re-parameterization; MRP rings with umrp and microchip-ung/mrp as manager, client and automatic manager, including cut cables; the SNMP agent against Net-SNMP.

Not validated: there is no second open IO-Controller to test the device against, other than profinet-py. No commercial PLC, device or engineering tool has been tested, and no GSDML file generated by go-profinet has been imported by TIA Portal or CODESYS yet. Npcap on Windows is not tested. System redundancy S2 has no open peer and is an experiment.

Running the stacks against go-profinet changed go-profinet where it disagreed with every reference, because the standard is not public and the stacks agree with each other:

  • p-net ties an AR to the RPC activity of its Connect, so a PrmEnd on another activity is acknowledged but never processed.
  • p-net starts its input provider only after PrmEnd, so a consumer watchdog armed at the Connect expires.
  • p-net, pnio-rs and profinet-py all number the first alarm 0xFFFF, not 0.
  • p-net does not answer DCP Get with the AllSelector, and ignores another requester for 3 seconds after a Get or Set.
  • A Linux bridge with LLDP forwarding returns LLDP frames to their sender, so p-net lists itself as its own neighbour.
  • pnio-rs accepts only RT_CLASS_2 frame IDs, so a controller must use the advanced startup with it.

Recorded and not worked around: p-net plugs any module a controller expects instead of reporting it in the ModuleDiffBlock, so a wrong module cannot be detected against p-net; pnio-rs answers diagnosis indices and I&M4 with invalid index and rejects a Connect without an exact ModuleDiff match.