Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ the HTTP/WebSocket API may break between any two alphas.

## [Unreleased]

### Added

- **Motion Master can operate a safe drive over Safety over EtherCAT.** A new device page, **Safety**, opens an FSoE connection to a drive, releases or applies Safe Torque Off, and shows the safe process values the drive publishes — safe position, velocity and torque, each with the drive's own validity flag, beside the raw SafeData octets they were decoded from. The page appears for any device in OP, because an FSoE frame travels in the cyclic process data and nothing can be exchanged outside OP. **This is a protocol master, not a safety master:** it implements ETG.5100, not the integrity of the machine running it, and that needs certified hardware. What makes it useful anyway is that the drive stays safe on its own — it authenticates every frame and drops its outputs to the safe state when the frames stop, whatever this master does. Use it to commission, to diagnose, and to move a safe axis on a bench. Do not use it as the safety function of a machine; the page and the API both say so in their own answers.
- **Safety over EtherCAT is on the API.** `GET /api/fsoe` lists the open connections with their protocol state, fault reasons — including the code the *drive* reported, which is the first thing to read when a handshake will not complete — and the decoded safe values. `POST /api/fsoe` opens a connection, and opening one again is how a caller re-binds after the bus was re-mapped. `PUT /api/fsoe/{slavePosition}/sto` releases or applies Safe Torque Off, `PUT .../data-command` chooses ProcessData or FailSafeData, `PUT .../safe-outputs` writes the raw SafeData octets, `DELETE /api/fsoe/{slavePosition}` stops driving the connection, and `POST .../reset` restarts the handshake. Two things to know: a connection starts in FailSafeData and returns to it after every fault, so the data command has to be set before SafeOutputs mean anything; and torque needs both halves to agree — STO released and ProcessData being sent. `swagger.yml` carries the whole surface, so `@synapticon/motion-master-client` is typed for it.

## [6.0.0-alpha.82] - 2026-08-21

### Added
Expand Down
86 changes: 84 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,9 @@ motion-master/
playground/ ← scratch binary
libs/
core/ ← version, CyclicTimer, CyclicTask/CycleContext, RT setup, utils
etg/ ← mm::etg: ESI XML parser + object-dictionary flattener. Offline
etg/ ← mm::etg: ESI parser, object-dictionary flattener, FSoE master. Offline
comm/ ← fieldbus interfaces; soem.cc, spoe.cc, igh.cc
node/ ← Device, DeviceManager, CiA402, profiles, RT tasks. No HTTP
node/ ← Device, DeviceManager, CiA402, profiles, FSoE connections, RT tasks. No HTTP
api/ ← mm::api: HTTP glue. The only lib that knows uWebSockets
example/ ← copy-me starter: a route plug-in and a cyclic task
hil/
Expand Down Expand Up @@ -736,6 +736,88 @@ Rules that are easy to get wrong and are pinned by tests:
`MM_ETG_TEST_DATA_DIR` compile definition. It pins the parser against a document nobody shaped
for it.

### FSoE Master (`libs/etg`)

`fsoe_master.{h,cc}` is the ETG.5100 ch. 8.4 master state machine, over `fsoe_frame.{h,cc}` (Safety
PDU layout) and `fsoe_crc.{h,cc}` (the Annex A hash). It opens and holds an FSoE connection to a
safe drive, so a tool can release STO, read safe process values, and explain a connection fault.

> [!WARNING]
> **This is a protocol master, not a safety master.** It implements the protocol, not the integrity
> of the device that runs it. A certified master needs certified hardware and a certified stack. The
> reason a tool-side master is still useful is that **the slave is the one that stays safe** — it
> authenticates every frame and drops its outputs when the frames stop, whatever the master does.

Pure, like the rest of `etg`: no socket, no thread, no clock, no allocation after construction. One
`cycle()` call per bus cycle takes the octets that arrived and returns the octets to send.

Rules that are easy to get wrong and are pinned by tests:

- **A Safety PDU is `1 + 2n + 2` octets, not `1 + n + 2`.** Each block of at most two SafeData
octets carries its own CRC. Getting this wrong produces a frame the peer drops silently, which
reads as a dead bus. Ask `FsoeFrameLayout`, never a formula at the call site.
- **The two directions have independent lengths.** A drive with the safe-sensor option takes 8
octets and returns 12 — a 19-octet frame against a 27-octet one. Sharing one length between the
directions is the bug above, in the form it actually shipped once.
- **The transport copies `txPdu()` into the output image every cycle**, including the cycles where
`FsoeCycleResult::txUpdated` is false. One state-table row sends nothing at all, and a fieldbus
keeps presenting the last image.
- **A repeated input frame is not an event.** ETG.5100 defines the frame-received event as a PDU in
which a bit changed, and `cycle()` enforces it. Answering the same frame twice sends a sequence
number the slave is not expecting.
- **A wrong-sized `rx` span is a transport defect and returns `std::unexpected`.** A CRC failure,
a watchdog expiry or a bad connection ID is not an error — it is a `fault` in the result.
- **A connection starts in `FailSafeData` and returns to it after every fault.** Call
`setDataCommand(ProcessData)` to leave the safe state, and call it again after a fault. SafeOutputs
that seem to be ignored are almost always this.
- **`safeInputs()` reads all zero whenever `inputsValid()` is false**, by clearing the buffer on
every transition that invalidates it. A caller that ignores the flag still gets fail-safe data.
- **The session ID is a counter, not a random value.** That is what makes the interop trace
reproducible, and it is a gap a certified master must close.

`fsoe_master_interop_test.cc` replays a recorded exchange with the real drive firmware's slave,
octet for octet. It was produced by linking this master against the firmware's own FSoE slave (the
`libs/etg5100_fsoe/tools/master_interop.sh` harness in the firmware repository) and running a full connection with
no bus. **There is no conformance test for the master role in the lab** — the CTT's FSoE suite tests
a slave — so that trace is the interoperability evidence. Regenerate it whenever either state
machine changes and read the diff.

### FSoE on the bus (`libs/node`, `/api/fsoe`)

The master above is pure; this is what puts its octets on the wire. `fsoe_connection.{h,cc}` binds
one connection to one drive, `fsoe_manager.{h,cc}` owns them and carries `FsoeCyclicTask`, and
`/api/fsoe` is the HTTP surface. The Console's device **Safety** page drives it.

- **The frame goes out through parameter cells, and comes back raw.** `exchangeProcessData` zeroes
the output image and recomposes it from each object's cell every cycle, so a raw write into that
image is overwritten. Every octet of the master frame *is* a mapped object, so `step` writes the
built frame back through those cells with `setValue`. The input direction cannot work that way: a
SafeData value wider than two octets is split by the interleaved CRCs, and the ESI maps the second
half as an alignment gap with no object behind it. It is read from `DeviceManager::cycleInputs()`,
the image the cycle just captured.
- **Never read a "safe position" object out of the process image.** You would get half of a 32-bit
value, and you would get it without any CRC, sequence or watchdog check. Read the frame, let the
master validate it, then decode — that is what `FsoeConnectionState::processValues()` does.
- **Register `FsoeCyclicTask` after `ProcessDataCyclicTask`.** It reads the input image that task
captured, and the frame it stages goes out on the next exchange. That one cycle of delay is what
a master sees on any fieldbus.
- **The transport must not present the same frame twice.** The master ignores an unchanged input,
and the drive's own glue does the same, because a peer that answers one frame twice breaks the CRC
chain. `fsoe_manager_test.cc` models that, and the test would pass a broken master without it.
- **Offsets belong to one process image.** A connection captures `processImageGeneration()` and
`topologyGeneration()`; if either changes it reports `bound: false` and stops driving rather than
write a Safety PDU into whatever now occupies those octets. Re-open to re-bind.
- **`open` is the only expensive call.** It reads the drive's PDO mapping over SDO, derives the
SafeData lengths from the mapping (a frame is `1 + 2n + 2`, so `n = (octets - 3) / 2`), checks
every field is byte-aligned and typed, and allocates. `step` allocates nothing.
- **Connections are appended, never removed.** The cycle thread walks a fixed pointer array whose
length only grows, published with a release store, so it needs no lock. Closing marks a connection
inactive; re-opening appends a new one and retires the old.
- **The state crosses to HTTP through a sequence lock**, so a display cannot show a safe position
from one cycle beside a validity flag from another. Same shape as the recorder ring's.

Rationale: `NEXTGEN.md`, Session 2026-08-22.

### Generated Object Addresses

Three headers carry one `ObjectAddress<T>` constant per SOMANET dictionary entry:
Expand Down
Loading
Loading