Skip to content

SPoE fieldbus driver #36

Closed

Description

@markosankovic

User story

As an engineer commissioning a SOMANET Integro over its Ethernet port,
I want Motion Master to reach the drive over SPoE with the same API, Console and procedures it offers over EtherCAT,
so that I can commission a drive on a PROFINET or EtherNet/IP machine, or beside a running EtherCAT master, without a second tool.

Context

SPoE is Synapticon's protocol for commissioning an Integro over Ethernet. It runs over TCP, port 8080, and mimics a non-real-time fieldbus: AL-like states, SDO read and write, a batch SDO read, the parameter list, file read and write, a firmware update trigger, process data, device locate and a watchdog. It has two modes. In Control mode the client sets the state and commands the drive, and a watchdog faults the drive when the client goes quiet. In Monitoring mode a fieldbus master controls the drive, and the client only parametrises and observes.

Sources:

  • The specification, "Ethernet (SPoE) interface definition", version 1.2_draft. It is confidential and is not in this repository. Ask the firmware team for it.
  • The firmware server, somanet_software at commit 422e1a144, arm_application/Components/cifXApplicationSockIf/ (AppSockIf_MessageHandler.{h,c}, AppSockIf_ApplicationFunctions.h). Where the specification and the firmware disagree, the firmware decides, and the issue records the difference.
  • The old client, motion_master/libs/comm/spoe.{h,cc} and SPoE.md, and its HTTP wrapper motion_master/apps/ec2h/.
  • A Console mock of the mode choice, "SPoE EtherCAT Monitor Control Mode Selection Page".

v6 has no SPoE driver. The design always meant to have one: FieldbusDriver is not EtherCAT-specific for this reason (NEXTGEN.md, the IEthercatDriver to IFieldbusDriver rename), the config already names fieldbus.driver: "spoe" with ipAddresses, and the Console shows the option as planned.

What the firmware does that the driver must respect:

  • One client per drive, and the first client keeps it. The listen socket stays in the poll set while a client is connected, so the firmware accepts a second connection (AppSockIf_ApplicationFunctions.c, SOCK_EVENT_NEW_CONNECTION_PENDING). Sock_SaveHandle then finds its one client slot full, logs "No space to save handle", and drops the new handle. Nothing reads, answers or closes that connection. So OBLAC or a second Motion Master gets a connection and then silence, and v6 keeps the drive. Whether the netX TCP stack completes that accept is not confirmed on hardware. Every ignored connection may also leave an open socket inside the netX stack. That is not confirmed either.
  • The drive closes an idle connection. AppSockIf_AcyclicHandler (AppSockIf_Handlers.c) aborts the connection when no request arrives for 5000 of its ticks. It also aborts on link loss and on an IP address change. How long a tick is was not found, so the idle time is not known.
  • The Control-mode watchdog faults the drive. It is 75 ms by default, and at least 50 ms (AppSockIf_MessageHandler.c:480-505). Any message resets it.
  • An unknown message type clears the drive's "SPoE active" flag (AppSockIf_MessageHandler.c default case). The driver sends no type that the firmware does not handle.
  • The drive buffers its TX process data. One exchange can return several input frames at once. The frames are joined with nothing between them, so the client splits them by the input frame size (pop_from_pdo_fifo_buffer, cyclic_communication.c).
  • One reply carries at most 500 bytes of inputs, and can cut a frame. 500 is not a multiple of most frame sizes, so the rest of a frame arrives at the start of the next reply.
  • A full input buffer is emptied, not trimmed. The buffer holds 512 bytes. When a frame does not fit, push_to_pdo_fifo_buffer discards every frame in it, not only the oldest one.
  • The input buffer fills in OP only. sqi_cyclic_rx_handler pushes a frame only when the device is in OP. In any other state a process-data reply carries no inputs. The firmware pushes on every fourth SQI cycle. The SQI cycle rate is not confirmed.
  • The index list and the parameter list hold at most 200 objects. sdoinfo_get_list is called with MAX_INDEX_LIST (200, ObjectDictionary/Includes/sdo.h), so a larger dictionary is cut short with no error. The v5.6.6 ESI gives an Integro 170 objects, so it fits.
  • One parameter-list entry is the firmware's struct _sdoinfo_entry_description copied as it lies in memory. The struct is not packed. With the natural alignment of its members it is 68 bytes, which matches the specification, but the layout the ARM compiler produces is not confirmed on hardware.
  • FIRMWARE_UPDATE applies both binaries. Over EtherCAT, leaving BOOT for INIT restarts the netX into its update mode, and only after com_firmware.bin was written (is_foe_for_update_complete, AppECS_Functions_Boot.c). The SPoE handler has no such rule: the explicit FIRMWARE_UPDATE message restarts the netX, the restart loads the COM firmware from flash, and the drive then starts the SoC firmware written in BOOT. The team confirmed this on 2026-10-03.
  • The firmware answers FIRMWARE_UPDATE before it resets. AppSockIf_StartUpdateReq asks for the reset 1000 ms later (ulTimeToReset). A client that reconnects inside that second reaches the old firmware. v6 waits for the drive to stop answering, then for it to answer again.
  • The first packet of com_firmware.bin in BOOT answers 0x00 for success. The firmware puts the return value of storage_prepare_for_writing in the status, and that is 0 on success. Every other file packet answers ACK (0x58), and 0x00 elsewhere is the FoE error "undefined". Before it answers, the firmware erases 512 KB of flash in 64 KB sectors, so v6 gives file packets 30 s.
  • An empty batch read is a firmware defect. AppSockIf_GetParameterBatch decrements its entry count before it checks it, so a batch of zero entries wraps the count and reads stale bytes. The driver never sends an empty batch.
  • A frame payload is at most 512 bytes (MAX_FRAME_SIZE). Files and the parameter list are segmented with first, middle and last packet states.
  • SII, ESC registers, distributed clocks and the ESC diagnostics do not exist over SPoE.

Where the specification and the firmware disagree. v6 follows the firmware in every case. The firmware is somanet_software develop, read on 2026-10-03:

  • A second client. The specification says the drive disconnects the first client and serves the second. The firmware keeps the first client and never answers the second (see "One client per drive" above). hil/spoe/second_client.py asks a drive which one is true. On 2026-10-10 it ran against an Integro with firmware v5.6.0-beta.0+26, which speaks protocol 1.0. The first client kept the drive and got an answer before and after the second attempt. The second client could not connect at all: its TCP connection attempt timed out after 2 s. So on that firmware the netX stack does not complete the accept, and the second client sees no connection rather than silence on a connection. After both closed, a third client connected and got an answer. Firmware with protocol 1.2 is not tested yet.
  • A state change in Monitor mode. The specification says a state change works only outside monitoring mode. AppUtil_ChangeState does not check the PDO mode, so the firmware changes the state in Monitor mode too. v6 refuses it in Monitor mode on its own, because a PLC owns the state there.
  • The response to a state change. The specification describes no data. The firmware returns the current state as one byte of data. Both use status 1 for success and 0 for failure, which is the reverse of every other message.
  • The server information. The specification calls the third data byte reserved. The firmware puts the current PDO mode there. The version is 0x0102, written low byte first.
  • A busy file read or write. The specification says the drive answers with status 0x28, FLASH BUSY, and the client keeps requesting. The firmware never sends 0x28. While the SoC is busy, it holds the reply and answers later (frame_processed_flag is false). A write also holds the reply to its last packet until the SoC confirms the file. A FoE error, SQI_BRG_FOE_ERR_BUSY (0x0C) included, ends the transfer with the last-packet state. So the request timeout must cover the busy time.
  • A batch read that fails. The specification says the values are returned up to the error. The firmware returns none of them, only the error status.
  • The size of a batch read reply. The specification says the values continue until the maximum data is reached. AppSockIf_GetParameterBatch does not check the reply against the 512-byte buffer. The driver must size each batch so that its values fit.
  • Stopping the device locate. The specification reads status 0 as a failure. The firmware answers 0 whenever the request stops the LEDs.
  • The watchdog. The specification says the drive goes to a fault state. The firmware marks the RX process data as invalid toward the SoC (check_spoe_heartbeat). What the SoC then does is not confirmed. The watchdog runs in Control mode only. Any known message resets it. A value below 50 ms is raised to 50 ms, and the status is always 0.
  • SDO access in INIT and BOOT. The firmware answers status 0xFFFF, which the specification does not list.
  • The parameter list, first packet. The specification says the data holds the number of objects. The firmware writes the count as one byte into a two-byte field, so a dictionary of more than 255 objects reports a wrong count, and the second byte is stale.
  • The parameter list, middle packets. The status of a middle packet comes from an uninitialised variable in AppSockIf_ReadObjectInfo, unless it is the last packet. The driver must not trust that status.
  • Message types. The firmware has 0x11 and 0x12, a description of one object or one entry, which the specification does not list. It names the full parameter list 0x13 PARAM_FULL_DESC and the index list 0x10 PARAM_LIST. 0x05 is in the firmware enum with no handler. 0x90 and 0x91, PROFINET record access, exist only in DEBUG_PNS builds.
  • The data length. The specification allows 0 to 511 bytes. The firmware buffer holds 512.

What the old client teaches. Keep its process-data model, which is why monitoring works there:

  • Every RT cycle starts an exchange if none is in progress (Device::exchangeProcessDataAndUpdateParameters, motion_master/libs/comm/spoe.cc). So the exchanges run back to back, as fast as the network answers. That keeps the drive's input buffer small, and the connection never goes idle while it exchanges.
  • The exchange runs off the RT path. The input frames of each reply go into a queue.
  • Each RT cycle takes one frame from the queue. A missed cycle drops one frame. The queue keeps at most 30 frames and drops the oldest. So the monitoring sees one drive frame per RT cycle, in order.
  • It exchanges only in SAFE-OP and OP.

Do not repeat its defects:

  • It reads the drive's state and PDO mapping once, at startup (motion_master/apps/motion_master/ethernet_slave.cc), and nothing reads the state again. In Monitor mode the PLC owns the state. So a drive that reaches OP after the start, or whose mapping the PLC changes, is probably not monitored until a restart. This comes from reading the code and is not confirmed on hardware.

  • One async_read_some per response, with no framing, so a split or merged TCP segment mis-parses.

  • parseMessage copies the size bytes the header claims without checking how many arrived, which reads past the buffer.

  • The response's sequence id and type are never compared with the request, so a late answer is taken for the next request.

  • The protocol version is read big-endian, and the firmware writes it little-endian.

  • An empty file write never sends the last packet.

  • FLASH BUSY on a file read is treated as a failure. The firmware sends no busy status on a file read, so the real defect is a timeout too short for a busy SoC.

  • Each reply is split by the frame size on its own. A frame cut at the 500-byte limit becomes a short frame, and every frame of the next reply is shifted.

  • A detached thread per exchange captures this.

  • The PDO map is hard-coded, and its refresh ignores 0x1C12 and 0x1C13.

Acceptance criteria

Transport:

  • SpoeFieldbusDriver in libs/comm/spoe_fieldbus_driver.{h,cc} implements FieldbusDriver, with no exceptions, on Linux, macOS and Windows.
  • Every response is read as the 7-byte header and then exactly the data length it states. A data length above the frame limit closes the connection with an error.
  • A response whose sequence id or type does not match the request is discarded and logged. A late response is never returned for a later request.
  • Each request has a timeout, 1 s by default, as the specification suggests.
  • The driver reads the server information first, decodes the protocol version little-endian, and logs it. It refuses a version it does not know, with a message that names the version.
  • A dropped connection marks the device as lost. A rescan reconnects. The log says when the drive closed the connection. The firmware does that on an idle timeout, on link loss and on an IP address change.

Configuration:

  • fieldbus.driver: "spoe" with fieldbus.ipAddresses, a list. Each address is one device, and the list order is the position order. An optional port defaults to 8080.
  • fieldbus.spoe.mode, control or monitor. Monitor is the default, because it cannot fault a drive that a master controls.
  • fieldbus.spoe.watchdogMs for Control mode, at least 50, default 75. The value is validated when the config is parsed.

FieldbusDriver mapping:

  • init and scan connect to every address and read the server information. A device that does not answer is reported and does not stop the others.
  • slaveInfo takes vendor, product, revision and serial from 0x1018 and the name from 0x1008, read over SDO.
  • slaveState, readStates and transitionToState use the state read and the state change. In Monitor mode a state change is refused with a message that says why.
  • readSdo and writeSdo use SDO read and write. Each SPoE SDO status has a name and a message.
  • readSdoComplete is not supported. The existing per-subindex fallback reads the entries.
  • readObjectDictionary uses the segmented parameter list and returns an OdRead. An entry that is cut short counts in missingEntries, so a lossy read is not cached.
  • readFile and writeFile use the segmented file messages, wait for a reply the firmware holds while the SoC is busy, send the last packet for an empty file, and map each file error to a FoeErrorKind. fs-getlist, fs-remove= and fs-buffer work as they do over FoE, or the issue records which do not.
  • configureProcessData and processDataLayout read 0x1C12, 0x1C13 and the assigned 0x16xx/0x1Axx entries over SDO, then switch the PDO mode to Monitor or to Monitor and Control.
  • readSii, writeSii, readRegister, writeRegister, readDiagnostics, readDcSync and the ESC watchdog return "not supported by SPoE". Every route that uses them answers 409 with that message.

Process data:

  • exchangeProcessData never blocks on the network. A thread owned by the driver exchanges frames with each drive back to back: it starts the next exchange as soon as the previous one answers, as the old client does. That keeps the drive's input buffer small, so the TxPDO stream reaches monitoring complete and in order. The gap between two exchanges stays below the watchdog and the idle timeout. exchangeProcessData hands it the latest outputs and returns the next queued input frame.
  • A frame that a reply cuts at the 500-byte limit is joined to the rest at the start of the next reply. No short frame enters the queue.
  • Every buffered TX frame enters the queue in order. When the queue passes its limit, the oldest frames are dropped and counted, and the count is visible in bus health.
  • A working counter equivalent reports whether the last exchange with each drive succeeded, so the existing short-WKC notification works for SPoE.
  • In Monitor mode the process-data exchange runs as well. It is how v6 sees the TxPDO of a drive that a PLC controls, so monitoring depends on it. The specification also expects a process-data request from a Monitor-mode client, empty or not, to show that the client is connected.
  • In Monitor mode v6 reads each drive's state on a fixed period, because the PLC changes it and v6 sends no state change. When a drive enters SAFE-OP or OP, v6 reads its PDO mapping, publishes the process image and starts the exchange. When a drive leaves SAFE-OP or OP, v6 tears its part of the image down. On the next entry v6 reads the mapping again, because the PLC can change it in PRE-OP.
  • On stop the driver sets the PDO mode to none before it disconnects, so a Control-mode drive is not faulted by its watchdog on a clean shutdown.

Procedures and routes:

  • firmware-installation has a SPoE path: INIT, BOOT, app_firmware.bin, then com_firmware.bin, then the firmware update message. It then polls the server information until the drive answers again, with a timeout, instead of fixed sleeps.
  • A device-locate procedure turns the locate LEDs on and off on a SPoE device. Over EtherCAT it is not listed.
  • A procedure that needs a state change is refused in Monitor mode, with a message that says why.

API and Console:

  • swagger.yml documents the SPoE config, the 409 answers and device-locate, and the generated client is regenerated.
  • The Console's Fieldbus page enables SPoE, takes the list of IP addresses, and asks for Control or Monitoring mode with the text of the mock: what each mode allows and what it requires.
  • Pages that need SII, registers or DC say that SPoE does not provide them, instead of failing.

Tests:

  • A fake SPoE server in libs/comm/tests speaks the protocol on loopback. Tests cover a response split over several TCP segments, two responses in one segment, a stale response with the wrong sequence id, a header that claims more data than the frame limit, segmented files including an empty file and a reply held while the SoC is busy, a second client that is connected and never answered, and a dropped connection.
  • libs/node/tests/concurrency_test.cc runs the RT stand-in against the SPoE driver with the fake server, and passes under tools/tsan.sh.
  • Hardware-verified on an Integro with Ethernet firmware: scan, read the dictionary, read and write parameters, a file round trip, monitoring in Monitor mode beside a PLC, motion in Control mode, the watchdog fault when the client stops, and a firmware update.
  • Hardware-verified: when a second client connects while v6 holds the drive, the second client gets no answer and v6 keeps working. Repeat the second connection many times, and check that v6 can still reconnect after a restart, because each ignored connection may leave a socket open in the netX stack.
  • Hardware-verified in Monitor mode: start v6 while a PLC holds the drive in PRE-OP. The PLC then brings the drive to OP. v6 starts monitoring the TxPDO without a restart. The PLC changes the PDO mapping in PRE-OP and brings the drive back to OP. v6 monitors the new mapping.
  • Hardware-verified: the object dictionary of an Integro reads complete over SPoE, and its entries match the same drive read over EtherCAT. This confirms the 68-byte entry layout.
  • Hardware-verified: the idle time after which the drive closes a quiet connection is measured and recorded here.

Implementation

  1. Add standalone asio to vcpkg.json for the TCP client, header-only and cross-platform. Update the dependency table in CLAUDE.md. Done in 6f6860a, together with the fake SPoE server libs/comm/tests/fake_spoe_server.{h,cc}.
  2. libs/comm/spoe_fieldbus_driver.{h,cc}: the framing, the message builders and parsers as pure functions, the per-device connection, and the exchange thread.
  3. main.cc builds the driver from the config.
  4. DeviceManager publishes the process image only inside transitionToState, when a device enters SAFE-OP or OP (libs/node/device_manager.cc). In Monitor mode v6 sends no state change, so that path never runs. Add a path that follows the observed state: when a drive is seen entering SAFE-OP or OP, re-map and publish, and when it is seen leaving, tear down. It reuses the existing re-map and teardown, and it runs off the RT thread. Device stays unchanged.
  5. The SPoE branch in firmware-installation, the device-locate row in libs/node/procedure_catalogue.cc, the 409 answers in the routes.
  6. swagger.yml, the generated client, and the Console.

Out of scope

  • The PROFINET record messages 0x90 and 0x91. They exist only in debug builds.
  • A separate PDO map for monitoring. The specification lists it as future work.
  • Authentication. The specification has none.
  • Assigning IP addresses. SPoE.md describes DHCP and PRONETA for that.

Notes

  • The specification is confidential. This issue names the document and its behaviour, and does not copy its message tables. The code in libs/comm will carry the wire format, which was decided to be acceptable.
  • SPoE over EoE works with no extra code. A drive on an EtherCAT bus is reached through the EtherCAT master's EoE gateway, for example a CODESYS PLC, and to v6 it is one more IP address. That is the Monitor-mode setup beside a PLC. The old client was used this way (motion_master 0821970).
  • One client per drive means that the Console must reach a SPoE drive only through Motion Master. OBLAC started beside v6 gets no answer from the drive. OBLAC started first holds the drive, and v6 gets no answer.
  • The specification asks for parameter access in about 20 ms per object and process data about every 100 ms. A full dictionary read is therefore slower than over EtherCAT, and the batch SDO read is what keeps a refresh usable.

References

  • "Ethernet (SPoE) interface definition", version 1.2_draft (confidential)
  • "SPoE EtherCAT Monitor Control Mode Selection Page"
  • somanet_software: arm_application/Components/cifXApplicationSockIf/, arm_application/Components/sqi_bridge/Sources/cyclic_communication.c
  • motion_master/libs/comm/spoe.{h,cc}, motion_master/libs/comm/SPoE.md, motion_master/apps/ec2h/
  • libs/comm/fieldbus_driver.h, libs/comm/soem_fieldbus_driver.{h,cc}, libs/comm/foe_error.h
  • NEXTGEN.md (the FieldbusDriver rename and the config posture)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions