Skip to content

SPoE fieldbus driver - #68

Merged
markosankovic merged 13 commits into
mainfrom
36-spoe-fieldbus-driver
Oct 10, 2026
Merged

markosankovic merged 13 commits into
mainfrom
36-spoe-fieldbus-driver

Conversation

@markosankovic

@markosankovic markosankovic commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Closes #36.

The SPoE fieldbus driver, built in slices. This PR collects every commit for #36.

Already on main before this branch existed:

  • 6f6860a test(comm): add a fake SPoE server
  • 9a098ee test(hil): add a script that asks a SPoE drive about a second client
  • ddc1b64 fix(comm): make the fake SPoE server build in CI

Planned slices:

  • The wire layer: frames and one connection per drive (60bcfe3)
  • The driver's control plane: init, scan, states, SDO, object dictionary, config, main.cc (6d0bf06)
  • Process data: exchange thread, queue, cut frames, working counter, Monitor-mode state following (75716ec)
  • Files, firmware installation, device locate (0e48a3f)
  • swagger.yml, the generated client, the Console, and the dropped input-frame count in bus health (fe64695)

Before merge: hardware checks on an Integro with Ethernet firmware

These need a drive, and #36 lists each one:

  • hil/spoe/second_client.py: a second client gets no answer, and the first keeps the drive
  • The idle time after which the drive closes a quiet connection
  • The object dictionary over SPoE matches the same drive over EtherCAT, which confirms the 68-byte entry layout
  • Monitor mode beside a PLC: monitoring starts when the PLC reaches OP, and follows a mapping change
  • Control mode: motion, and the watchdog fault when the client stops
  • A firmware installation over SPoE
  • The Console's Fieldbus page against a real drive

🤖 Generated with Claude Code

Running a script under hil/ writes __pycache__ next to it. Only
clients/python ignored it, so the root ignores it for every directory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@markosankovic markosankovic linked an issue Oct 3, 2026 that may be closed by this pull request
38 tasks
markosankovic and others added 12 commits October 3, 2026 11:53
The SPoE driver talks to each drive over one TCP connection. This adds the
frame codec and that connection, with no FieldbusDriver yet.

A reply is matched to its request by sequence id and type. A reply that does
not match is a late answer to a request that timed out, so it is discarded and
never returned for a later request. Received bytes stay in a buffer between
requests, so a late reply does not break the framing of the next one. A header
that claims more than 512 bytes closes the connection, because the firmware
never sends one.

A failed request reports a timeout, a closed connection or an invalid request.
The driver needs that difference: a closed connection means the device is
lost, and a timeout leaves the connection usable.

The drive answers one request at a time, so requests take turns behind one
mutex. The control plane and the process-data exchange will share it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SpoeFieldbusDriver reaches each drive over its own SPoE connection. It scans,
reads the identity, reads and changes the state, reads and writes SDOs, and
reads the object dictionary. Process data and file transfer come later.

A position is the place of an address in the configured list. A drive that
does not answer keeps its position with no state known, so the positions
after it do not shift.

The bulk parameter list is not trusted alone. The status of each middle packet
comes from an uninitialised variable in the firmware, and when it reads as
BUSY the packet's entries are lost. So the list is checked against the index
list, and every gap is filled one description at a time. Only an entry the
drive never answers counts as missing, so a lossy read is never cached.

In Monitor mode a PLC owns the drive's state. FieldbusDriver gains
stateChangeRefusal, so DeviceManager refuses the change with that reason and
POST /api/devices/state answers 409, instead of a vague "target not reached".

The config file and POST /api/init share one fieldbus block: ipAddresses and a
spoe block with mode, watchdogMs and port.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SPoE is TCP, one request and one reply at a time, so a round trip cannot
run on the RT thread. Each drive gets an exchange thread. The RT thread
hands it the newest outputs through a triple buffer and takes one input
frame per cycle from a single-producer single-consumer queue, so neither
side waits.

The exchange thread starts the next request as soon as the previous one
answers. That keeps the drive's input buffer small, and the queue paces
its batched frames to the RT cycle, so monitoring sees every frame in
order. A frame cut at the 500-byte reply limit is joined to the rest, and
the framing heals itself if the drive discarded that rest.

In Monitor mode a PLC owns the state and nothing calls transitionToState,
which is where the process image is published. followObservedStates
re-maps when a drive enters SAFE-OP or OP and tears down when none
exchanges. A state-follower thread in main.cc repeats it while the driver
refuses state changes, because DeviceManager owns no thread.

The driver clears the PDO mode on stop and on destruction, so a clean
shutdown does not trip a Control-mode drive's watchdog.

A state change is judged by the state in the reply, because firmware
versions disagree on the status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SPoE driver reads and writes files, so the existing firmware
installation runs over SPoE. A file write sends the name, then 500-byte
chunks, and an empty file still sends its last packet. File packets get
30 s, because the firmware holds its reply while the SoC is busy and erases
512 KB of flash before it answers the first COM-firmware packet.

Over EtherCAT, leaving BOOT applies the firmware. Over SPoE an explicit
FIRMWARE_UPDATE message does, and it restarts the drive. The installation
gains an activate-firmware step for it, backed by
FieldbusDriver::activateFirmware, which does nothing by default. The
firmware answers before it resets, so the driver waits for the drive to
stop answering and then to answer again, rather than sleeping.

The first packet of com_firmware.bin in BOOT answers 0x00 for success: the
firmware returns what storage_prepare_for_writing returned. The driver
accepts that for this one packet.

SPoE has no SII, so the installation records that and continues. A
device-locate procedure blinks the LEDs where the transport can, and stops
them at the end or on cancel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SPoE reaches each drive without an EtherCAT Slave Controller, so the
routes that need SII, ESC registers, DC or the ESC diagnostics answer 409
with the reason, instead of a driver error as a 500. Each device reports
supportsEsc and supportsLocate, so a client knows which pages apply
without trying them.

GET /api/process-image reports droppedInputFrames: the input frames a
SPoE drive returned that the RT loop could not take in time.

The Console's Fieldbus page enables SPoE. It takes one IP address per
drive and asks for Monitoring or Control mode, with what each one means.
The pages that need an ESC say that SPoE does not provide them, and send
no requests. The Process Image page warns when input frames were dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The control-mode SPoE concurrency test re-mapped back to back. A re-map
holds the image unpublished for most of its duration, and on the macOS
runner one re-map took 100 to 150 ms. The cycle guard was falsy for the
whole run, so the test counted zero cycles and failed.

A 10 ms pause after each re-map gives the cycle a window to run in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drives with firmware older than the SPoE watchdog setting report protocol
version 1.0. Its server information carries no PDO mode, and it has no
WATCHDOG_TIMEOUT message. The driver refused these drives, because it
needed version 1.2 and three bytes of server information.

The driver now accepts 1.0 and 1.2 and keeps the version of each drive.
In Control mode it sends WATCHDOG_TIMEOUT to a 1.2 drive only. A 1.0
drive keeps the watchdog that its firmware fixes, and the driver logs
that spoe.watchdogMs has no effect. After a firmware update the driver
reads the version again.

The fake server answers as 1.0 firmware on request, and tests cover
identification and Control-mode setup against it.

Tested on an Integro with firmware v5.6.0-beta.0+26: identification,
object dictionary, Monitor process data, file reads, and Control mode up
to Operation Enabled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the firmware refuses the first packet of a file write, it replies
in the last-packet state and puts the FoE error in the code. That error
can be 0, which is SQI_BRG_FOE_ERR_UNDEFINED. The driver accepted a 0
code for com_firmware.bin, so it read such a refusal as success.

The driver now treats a first reply in the last-packet state as a
refusal, for every file. A reply in the first-packet state succeeds
when its code is ACK or 0. In BOOT the COM firmware answers with the
return value of storage_prepare_for_writing, and 0 is success.

The fake server can refuse a file write, and a test covers the refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An SPoE drive starts in OP. The process image is published only when
transitionToState brings a device to SAFE-OP or OP, so in Control mode
no image was built and Motion Master did not exchange process data with
a drive that was already in OP.

The scan now reads the device states. When a device is in SAFE-OP or
OP, the scan reads its object dictionary and publishes the image. A
failure of that step is logged, and the scan still succeeds. A SOEM
scan leaves every slave in INIT, so nothing changes for EtherCAT.

The image binds each entry to its parameter cell when it is built. So
the dictionary is read before the image, both at the scan and in the
Monitor-mode state follower. Without it, the values arrive and nothing
decodes them until the next re-map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The release ships SPoE for testing on hardware that reaches SAFE-OP,
which this bench does not have. The changelog tells the testers what
works and what to avoid. NEXTGEN.md keeps the hardware findings and the
list of untested items, known problems and later work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@markosankovic
markosankovic marked this pull request as ready for review October 10, 2026 11:24
@markosankovic
markosankovic merged commit 1146db3 into main Oct 10, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SPoE fieldbus driver

1 participant