Repository navigation
SPoE fieldbus driver #36
Description
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_softwareat commit422e1a144,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}andSPoE.md, and its HTTP wrappermotion_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_SaveHandlethen 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.cdefault 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_bufferdiscards every frame in it, not only the oldest one. - The input buffer fills in OP only.
sqi_cyclic_rx_handlerpushes 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_listis called withMAX_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_descriptioncopied 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_UPDATEapplies both binaries. Over EtherCAT, leaving BOOT for INIT restarts the netX into its update mode, and only aftercom_firmware.binwas written (is_foe_for_update_complete,AppECS_Functions_Boot.c). The SPoE handler has no such rule: the explicitFIRMWARE_UPDATEmessage 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_UPDATEbefore it resets.AppSockIf_StartUpdateReqasks 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.binin BOOT answers 0x00 for success. The firmware puts the return value ofstorage_prepare_for_writingin 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_GetParameterBatchdecrements 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.pyasks 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_ChangeStatedoes 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_flagis 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_GetParameterBatchdoes 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
0x11and0x12, a description of one object or one entry, which the specification does not list. It names the full parameter list0x13PARAM_FULL_DESCand the index list0x10PARAM_LIST.0x05is in the firmware enum with no handler.0x90and0x91, PROFINET record access, exist only inDEBUG_PNSbuilds. - 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_someper response, with no framing, so a split or merged TCP segment mis-parses. -
parseMessagecopies thesizebytes 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
0x1C12and0x1C13.
Acceptance criteria
Transport:
-
SpoeFieldbusDriverinlibs/comm/spoe_fieldbus_driver.{h,cc}implementsFieldbusDriver, 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"withfieldbus.ipAddresses, a list. Each address is one device, and the list order is the position order. An optionalportdefaults to 8080. -
fieldbus.spoe.mode,controlormonitor. Monitor is the default, because it cannot fault a drive that a master controls. -
fieldbus.spoe.watchdogMsfor Control mode, at least 50, default 75. The value is validated when the config is parsed.
FieldbusDriver mapping:
-
initandscanconnect to every address and read the server information. A device that does not answer is reported and does not stop the others. -
slaveInfotakes vendor, product, revision and serial from0x1018and the name from0x1008, read over SDO. -
slaveState,readStatesandtransitionToStateuse the state read and the state change. In Monitor mode a state change is refused with a message that says why. -
readSdoandwriteSdouse SDO read and write. Each SPoE SDO status has a name and a message. -
readSdoCompleteis not supported. The existing per-subindex fallback reads the entries. -
readObjectDictionaryuses the segmented parameter list and returns anOdRead. An entry that is cut short counts inmissingEntries, so a lossy read is not cached. -
readFileandwriteFileuse 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 aFoeErrorKind.fs-getlist,fs-remove=andfs-bufferwork as they do over FoE, or the issue records which do not. -
configureProcessDataandprocessDataLayoutread0x1C12,0x1C13and the assigned0x16xx/0x1Axxentries over SDO, then switch the PDO mode to Monitor or to Monitor and Control. -
readSii,writeSii,readRegister,writeRegister,readDiagnostics,readDcSyncand the ESC watchdog return "not supported by SPoE". Every route that uses them answers409with that message.
Process data:
-
exchangeProcessDatanever 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.exchangeProcessDatahands 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
stopthe 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-installationhas a SPoE path: INIT, BOOT,app_firmware.bin, thencom_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-locateprocedure 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.ymldocuments the SPoE config, the409answers anddevice-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/testsspeaks 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.ccruns the RT stand-in against the SPoE driver with the fake server, and passes undertools/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
- Add standalone
asiotovcpkg.jsonfor the TCP client, header-only and cross-platform. Update the dependency table inCLAUDE.md. Done in 6f6860a, together with the fake SPoE serverlibs/comm/tests/fake_spoe_server.{h,cc}. 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.main.ccbuilds the driver from the config.DeviceManagerpublishes the process image only insidetransitionToState, 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.Devicestays unchanged.- The SPoE branch in
firmware-installation, thedevice-locaterow inlibs/node/procedure_catalogue.cc, the409answers in the routes. swagger.yml, the generated client, and the Console.
Out of scope
- The PROFINET record messages
0x90and0x91. 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.mddescribes 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/commwill 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_master0821970). - 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.cmotion_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.hNEXTGEN.md(theFieldbusDriverrename and the config posture)
Activity