|
Motion Master 6.0.0-alpha.86
Next-generation motion control software
|
Owns the fieldbus driver and node collection, and drives PDO exchange. More...
#include <device_manager.h>
Classes | |
| class | CycleGuard |
| Holds a cyclic task's whole body open against a device-set rebuild. RT-safe. More... | |
| struct | PdoSampleSpec |
| Everything needed to decode one PDO object from a raw process-image snapshot: which image it lives in, where, and how wide / what type. More... | |
Public Member Functions | |
| DeviceManager () | |
| ~DeviceManager () | |
Defined out-of-line so unique_ptr<ProcessData> can hold an incomplete type. | |
| std::expected< void, std::string > | init (std::unique_ptr< mm::comm::FieldbusDriver > driver, const DeviceManagerConfig &config={}) |
Takes ownership of driver and initialises it. | |
| void | configureParameterCache (const ParameterCacheConfig &config) |
| Sets the on-disk parameter-cache policy and location. | |
| const ParameterCache & | parameterCache () const |
| The on-disk parameter cache, for the management API (list / download / remove). | |
| std::expected< int, std::string > | scan () |
| Scans the bus for nodes and populates the device list. | |
| void | reset () |
| Stops the fieldbus driver and clears the device list. | |
| bool | initialised () const |
Whether a driver is currently held: init() succeeded, and no reset() followed it. Thread-safe. | |
| bool | hasDevice (uint16_t slavePosition) const |
Whether a device holds slavePosition. Thread-safe; no bus access. | |
| DeviceHandle | deviceAt (uint16_t slavePosition) const |
The device at slavePosition, as a handle that keeps it alive. Falsy if absent. | |
| std::shared_ptr< DeviceSet > | deviceSet () const |
The current device set, as a pointer that keeps it alive. Never nullptr. | |
| Device * | findDevice (uint16_t slavePosition) |
Device lookup by bus position. O(N) over a handful of devices; nullptr if absent. | |
| const Device * | findDevice (uint16_t slavePosition) const |
Const overload of findDevice. Same contract. | |
| uint64_t | topologyGeneration () const |
Monotonic counter bumped every time the device set is rebuilt (scan / reset). | |
| std::expected< void, std::string > | configureProcessData () |
| Maps process data and publishes the process image for exchange. | |
| void | exchangeProcessData () |
| Exchanges one cycle of process data: sends the current outputs, captures inputs. | |
| bool | processDataConfigured () const |
Whether configureProcessData published a process image for exchange. | |
| uint64_t | processImageGeneration () const |
| Monotonic counter bumped every time a new process image is published (each (re)map). | |
| int | lastWorkingCounter () const |
| The working counter from the most recent process-data exchange (0 before any). | |
| int | expectedWorkingCounter () const |
| The working counter expected from the devices currently exchanging (SAFE-OP/OP). | |
| bool | processDataHealthy () const |
| Whether every device that should be exchanging is contributing to the cycle. | |
| ProcessImageInfo | processImageInfo () const |
| Snapshots the published process image and its runtime health for the API. | |
| uint64_t | shortWkcCycles () const |
| Cycles that answered with a short working counter since the bus came up. | |
| std::expected< std::string, std::string > | dumpProcessData () |
Serialises the recorder's current span to a .mmpd dump file and returns its path. | |
| std::expected< std::string, std::string > | dumpProcessDataBuffer () |
Serialises the recorder dump into an in-memory buffer — the same .mmpd bytes dumpProcessData writes to disk — for streaming over HTTP. | |
| std::vector< SlaveConfigInfo > | busConfig () const |
| Snapshots each slave's static ESC configuration (SM, FMMU, mailbox, DC) for the API. | |
| std::expected< std::vector< DeviceStateInfo >, std::string > | transitionToState (const std::vector< uint16_t > &positions, mm::comm::EtherCatState targetState, std::chrono::steady_clock::duration timeout) |
Transitions a set of devices to targetState, blocking until all arrive or timeout elapses, and reports the final state of each. | |
| std::expected< std::vector< DeviceStateInfo >, std::string > | deviceStates (const std::vector< uint16_t > &positions) |
| Reads the current AL state for a set of devices. | |
| std::expected< std::vector< DeviceDiagnosticsInfo >, std::string > | deviceDiagnostics (const std::vector< uint16_t > &positions) |
| Reads live ESC health diagnostics (link quality, error counters, watchdogs) for a set of devices, resolving each slave's device name. | |
| std::expected< std::vector< DcSyncInfo >, std::string > | dcSync (const std::vector< uint16_t > &positions) |
| Reads live distributed-clock synchronisation status for a set of devices, resolving each slave's device name. | |
| std::expected< mm::comm::ProcessDataWatchdogConfig, std::string > | processDataWatchdog (uint16_t slavePosition) |
| Reads the process-data (sync-manager) watchdog configuration of one device. | |
| std::expected< mm::comm::ProcessDataWatchdogConfig, std::string > | setProcessDataWatchdog (uint16_t slavePosition, std::chrono::nanoseconds timeout) |
| Sets the process-data (sync-manager) watchdog timeout of one device. | |
| std::expected< void, std::string > | initializeDeviceParameters (uint16_t slavePosition, bool readValues) |
Enumerates the CoE object dictionary of one device and populates its parameter map. See Device::initializeParameters for details. | |
| std::expected< void, std::string > | readAllDeviceParameters (uint16_t slavePosition) |
| Refreshes the cached value of every readable parameter of one device, in place. | |
| std::expected< DeviceParameter, std::string > | deviceParameterView (uint16_t slavePosition, uint16_t index, uint8_t subindex, bool refreshFromBus) |
| Returns a full parameter struct (value + metadata), optionally refreshed from the bus. | |
| std::expected< DeviceParameter, std::string > | writeDeviceParameter (uint16_t slavePosition, uint16_t index, uint8_t subindex, const DeviceParameterValue &newValue) |
| Convenience: finds a device by position, writes one of its parameters, returns it. | |
| std::expected< void, std::string > | writeDevicePdoMapping (uint16_t slavePosition, const PdoMapping &mapping) |
| Convenience: finds a device by position and writes its PDO mapping. | |
| std::vector< OutputStageResult > | stageProcessDataOutputs (std::span< const OutputStageRequest > requests) |
| Stages a batch of output objects into the process image in one call. | |
| std::optional< PdoSampleSpec > | pdoSampleSpec (uint16_t slavePosition, uint16_t index, uint8_t subindex) const |
| Resolves a PDO-mapped object's decode spec from the published image. Thread-safe. | |
| uint64_t | recorderHead () const |
The recorder ring's next sequence number; recorderHead()-1 is the newest recorded cycle and recorderHead()==0 means nothing is recorded yet. | |
| uint64_t | recorderOldestSeq () const |
The oldest sequence number still present in the ring (max(0, head - capacity)). A cursor below this was lapped (overwritten) and must resync to it. Thread-safe on the same terms as recorderHead. | |
| bool | readRecord (uint64_t seq, ProcessDataRing::Record &out) const |
Copies the recorded cycle for seq into out. Thread-safe on the same terms as recorderHead (the shared lock is taken per record, not per span). | |
| bool | deviceExchangesProcessData (uint16_t slavePosition) const |
Whether the device at slavePosition is currently exchanging (SAFE-OP/OP). Thread-safe; reads the driver's cached AL state, no bus I/O. | |
Owns the fieldbus driver and node collection, and drives PDO exchange.
The surface, grouped — every member is documented at its declaration below. The documentation is long on purpose: it records why each lock exists and what breaks without it. This map is so a reader can find the part they need without reading all of it.
init, scan, reset, initialised, topologyGeneration, configureParameterCache, parameterCache deviceAt (a handle that owns its set), deviceSet (the whole set), hasDevice (a predicate, no device), findDevice (real-time only)configureProcessData, exchangeProcessData (real-time), processDataConfigured, processImageInfo, processImageGeneration, stageProcessDataOutputs, pdoSampleSpec, deviceExchangesProcessData lastWorkingCounter, expectedWorkingCounter, processDataHealthy recorderHead, recorderOldestSeq, readRecord, dumpProcessData, dumpProcessDataBuffer transitionToState, busConfig, deviceStates, deviceDiagnostics, dcSync, processDataWatchdog, setProcessDataWatchdog initializeDeviceParameters and readAllDeviceParameters (they inject the manager's useCompleteAccess setting), writeDeviceParameter and deviceParameterView (they compose two steps into one call so a re-enumeration cannot land between them), writeDevicePdoMapping (it takes busOperationMutex_, because rewriting a mapping is a bus operation)CycleGuard (taken by GameLoop, never by a task)Everything else about a device is on Device, reached through deviceAt. A method that would only resolve a position and forward does not belong here.
The driver is not required at construction — call init() to supply one. This allows the app to start without a driver and be initialised later via the HTTP API. Injected into GameLoop (for exchangeProcessData) and HttpServer (for SDO/state operations).
| mm::node::DeviceManager::DeviceManager | ( | ) |
|
default |
Defined out-of-line so unique_ptr<ProcessData> can hold an incomplete type.
| std::vector< SlaveConfigInfo > mm::node::DeviceManager::busConfig | ( | ) | const |
Snapshots each slave's static ESC configuration (SM, FMMU, mailbox, DC) for the API.
Passes through the driver's cached configuration (no bus I/O) and resolves each slave's device name. Returns an empty vector when no driver is set or the transport has no ESC (e.g. SPoE). Runs on the (non-RT) caller's thread.
| void mm::node::DeviceManager::configureParameterCache | ( | const ParameterCacheConfig & | config | ) |
Sets the on-disk parameter-cache policy and location.
The cache is a process-level concern (its directory comes from the config file, like the ports), independent of whether a driver is initialised — so this is called once at startup, not from init. The configured location is then correct for the management API and for every device's initializeParameters regardless of init order. Safe to call again to re-apply.
| std::expected< void, std::string > mm::node::DeviceManager::configureProcessData | ( | ) |
Maps process data and publishes the process image for exchange.
Calls FieldbusDriver::configureProcessData (which maps the IOmap and lays out the FMMUs), reads each device's PDO mapping via SDO, assembles a ProcessImage that resolves every mapped object to an absolute position and to its owning parameter's cell, and publishes the image so exchangeProcessData begins exchanging. All devices must be in PRE-OP (mailbox active) for the mapping reads to succeed.
Re-runnable: a later call (e.g. after a device returns from a firmware download) re-maps the whole bus and republishes. While re-mapping it first unpublishes the image so exchangeProcessData becomes a no-op; call it with the GameLoop stopped or drained, as for init / reset (see the exchangeProcessData warning).
| std::expected< std::vector< DcSyncInfo >, std::string > mm::node::DeviceManager::dcSync | ( | const std::vector< uint16_t > & | positions | ) |
Reads live distributed-clock synchronisation status for a set of devices, resolving each slave's device name.
If positions is empty, all discovered devices are queried. Forwards to FieldbusDriver::readDcSync (FPRD reads — not cached) and wraps each result with its device name. The system-time difference is meaningful only while the bus is exchanging in SAFE-OP/OP; a page polls this and watches for a slave whose deviation from the reference clock stays large or grows rather than converging toward zero.
Must be called after both init() and scan().
| positions | 1-based slave positions to query; empty = all devices. |
| DeviceHandle mm::node::DeviceManager::deviceAt | ( | uint16_t | slavePosition | ) | const |
The device at slavePosition, as a handle that keeps it alive. Falsy if absent.
The way to reach a device from outside DeviceManager. It resolves the position in the current DeviceSet and hands back a DeviceHandle holding both. Keep the handle for as long as you work with the device — a millisecond or ten minutes, it makes no difference, and no lock is held either way.
A scan that lands while you hold one publishes a new set. Your device keeps working, and its next bus transaction fails against hardware that moved, or that is no longer there. That is the intended outcome: a rescan never waits for a procedure, and a procedure never reads freed memory.
| std::expected< std::vector< DeviceDiagnosticsInfo >, std::string > mm::node::DeviceManager::deviceDiagnostics | ( | const std::vector< uint16_t > & | positions | ) |
Reads live ESC health diagnostics (link quality, error counters, watchdogs) for a set of devices, resolving each slave's device name.
If positions is empty, all discovered devices are queried. Forwards to FieldbusDriver::readDiagnostics (FPRD reads — not cached) and wraps each result with its device name. The returned counters are monotonic since the last clear, so a diagnostics page polls this and watches for a rising delta rather than an absolute value.
Must be called after both init() and scan().
| positions | 1-based slave positions to query; empty = all devices. |
| bool mm::node::DeviceManager::deviceExchangesProcessData | ( | uint16_t | slavePosition | ) | const |
Whether the device at slavePosition is currently exchanging (SAFE-OP/OP). Thread-safe; reads the driver's cached AL state, no bus I/O.
The monitoring sampler's per-device live gate: only an exchanging device has live process data, so a non-exchanging device's parameters sample null. false for an unknown position.
| std::expected< DeviceParameter, std::string > mm::node::DeviceManager::deviceParameterView | ( | uint16_t | slavePosition, |
| uint16_t | index, | ||
| uint8_t | subindex, | ||
| bool | refreshFromBus | ||
| ) |
Returns a full parameter struct (value + metadata), optionally refreshed from the bus.
Backs the GET /api/devices/{pos}/parameters/{index}/{subindex} route. When refreshFromBus is true (the ?source=auto default) the cache is first synced via the live PDO image or an SDO upload (routing lives in Device::readParameter); when false (the ?source=cache path) the cached struct is returned without any bus I/O. Either way the returned value is a copy taken under the parameter-cache lock, so it is safe off-thread.
| slavePosition | 1-based bus position of the target device. |
| index | CoE object index. |
| subindex | CoE object subindex. |
| refreshFromBus | When true, sync the cache from the device before copying. |
| std::shared_ptr< DeviceSet > mm::node::DeviceManager::deviceSet | ( | ) | const |
The current device set, as a pointer that keeps it alive. Never nullptr.
One mutex acquisition, long enough to copy a shared_ptr, then nothing. For work that spans several devices — a JSON listing, a bus-wide walk. deviceAt is the one-device form. Before the first init the set is empty, not absent.
| std::expected< std::vector< DeviceStateInfo >, std::string > mm::node::DeviceManager::deviceStates | ( | const std::vector< uint16_t > & | positions | ) |
Reads the current AL state for a set of devices.
If positions is empty, all discovered devices are queried. Must be called after both init() and scan().
| positions | 1-based slave positions to query; empty = all devices. |
| std::expected< std::string, std::string > mm::node::DeviceManager::dumpProcessData | ( | ) |
Serialises the recorder's current span to a .mmpd dump file and returns its path.
Freezes the recorder span at the instant of the call — [recorderOldestSeq(), recorderHead()) — and writes every cycle in it (full raw inputs + outputs, sequence, epoch-ns timestamp) plus the process image embedded as a header, so the file decodes fully offline. Works in any state: while exchanging (OP/SAFE-OP) it dumps tail→head at that moment and ignores cycles the producer records afterwards; after teardown it uses the most recent retained image generation. The header objects' names/data types come from each device's parameter map (empty/0 when the object dictionary is not enumerated).
The file is written to the configured recorderDumpDir (created if absent); an empty recorderDumpDir (the default) resolves to a "dumps" subdirectory of the per-user cache directory. The filename is dump-<UTC-timestamp>- <endSequence>.mmpd. Only the path is returned, but because the default location sits under the user-cache root, the written file is reachable through the /api/user-cache endpoints — listed, downloaded and deleted remotely rather than only from a shell on the host.
| std::expected< std::string, std::string > mm::node::DeviceManager::dumpProcessDataBuffer | ( | ) |
Serialises the recorder dump into an in-memory buffer — the same .mmpd bytes dumpProcessData writes to disk — for streaming over HTTP.
Buffers the whole span in memory (a deep recorder ring can be large; true row-by-row streaming is a future optimisation). Same image/span semantics as dumpProcessData; the RT producer is never blocked. Runs on the (non-RT) caller's thread.
| void mm::node::DeviceManager::exchangeProcessData | ( | ) |
Exchanges one cycle of process data: sends the current outputs, captures inputs.
Composes the output image from each output object's parameter cell, calls FieldbusDriver::exchangeProcessData, appends the cycle (both directions) to the recorder ring for non-RT readers, and decodes every mapped input back into its own cell — which is what makes Device::value<T>() a single atomic load rather than an image lookup. No-op until configureProcessData published an image, so the GameLoop can call it unconditionally every cycle. Runs on the RT thread and takes no lock.
exchangeProcessData() runs on the RT GameLoop thread while init(), scan(), reset(), and configureProcessData() may be called from the HTTP server thread. The published-image pointer gates exchange off during a re-map, and the device set it reads is the raw published pointer, which the control plane replaces only with the cycle drained. Every control-plane operation drains before it mutates, so no caller has to stop the loop first. | int mm::node::DeviceManager::expectedWorkingCounter | ( | ) | const |
The working counter expected from the devices currently exchanging (SAFE-OP/OP).
Computed from each device's PDO presence and current AL state, so it tracks a partially operational bus: an OP device with outputs and inputs contributes 3, a SAFE-OP device contributes 1 (inputs only), and PRE-OP/below contribute 0. Recomputed on configure and on each state transition — not the whole-bus all-OP figure.
| Device * mm::node::DeviceManager::findDevice | ( | uint16_t | slavePosition | ) |
Device lookup by bus position. O(N) over a handful of devices; nullptr if absent.
For the RT thread. It reads the raw published device set and takes no lock, which is what makes it callable from a cyclic task. Off the RT thread use deviceAt or deviceSet, which hold the set alive by refcount.
Lifetime. The returned pointer — and any DeviceParameter* obtained through it — is valid for the body of one cycle, which is the scope GameLoop already holds the cycle open for. A scan or reset publishes a new set and drops the RT reference to the old one, so a cyclic task must re-resolve each cycle and never cache a Device* across cycles.
Position is not identity. Inserting a device into the chain shifts every position after it, so a task pinned to position 4 can silently find different hardware there after a rescan. Capture topologyGeneration() when binding and abandon the work when it changes.
| const Device * mm::node::DeviceManager::findDevice | ( | uint16_t | slavePosition | ) | const |
Const overload of findDevice. Same contract.
| bool mm::node::DeviceManager::hasDevice | ( | uint16_t | slavePosition | ) | const |
Whether a device holds slavePosition. Thread-safe; no bus access.
For the caller that only needs to tell "no such device" from a failure of the operation itself — an HTTP route answering 404 before it starts. It is a point-in-time answer, so it does not entitle the caller to a Device&: use deviceAt for that.
| std::expected< void, std::string > mm::node::DeviceManager::init | ( | std::unique_ptr< mm::comm::FieldbusDriver > | driver, |
| const DeviceManagerConfig & | config = {} |
||
| ) |
Takes ownership of driver and initialises it.
Must be called before scan() and exchangeProcessData(). One-shot: fails if a driver is already held — call reset() first. Replacing a live driver would dangle the FieldbusDriver& that every Device holds.
| driver | Concrete fieldbus driver to own and operate. |
| config | Runtime tuning (recorder depth, cycle period). Defaults are used for any field left at its default; omit the argument entirely to use all defaults. |
| bool mm::node::DeviceManager::initialised | ( | ) | const |
| std::expected< void, std::string > mm::node::DeviceManager::initializeDeviceParameters | ( | uint16_t | slavePosition, |
| bool | readValues | ||
| ) |
Enumerates the CoE object dictionary of one device and populates its parameter map. See Device::initializeParameters for details.
| slavePosition | 1-based bus position of the target device. |
| readValues | When true, also issue an SDO upload for each entry and store the decoded value on the parameter. |
| int mm::node::DeviceManager::lastWorkingCounter | ( | ) | const |
The working counter from the most recent process-data exchange (0 before any).
| const ParameterCache & mm::node::DeviceManager::parameterCache | ( | ) | const |
The on-disk parameter cache, for the management API (list / download / remove).
A disk store independent of the bus; usable with or without a driver initialised.
| std::optional< DeviceManager::PdoSampleSpec > mm::node::DeviceManager::pdoSampleSpec | ( | uint16_t | slavePosition, |
| uint16_t | index, | ||
| uint8_t | subindex | ||
| ) | const |
Resolves a PDO-mapped object's decode spec from the published image. Thread-safe.
The sampler captures this once per PDO parameter (and re-captures when processImageGeneration changes), then for each recorded cycle reads the matching region of the ring record (inputs for an input object, outputs for an output) and runs extractBits + decodeSdoBytes itself — so the bus is never touched on the sampling path and every value in a row comes from the same cycle's record.
nullopt if no image is published, the object is not PDO-mapped, or its data type is unknown (object dictionary not enumerated). | bool mm::node::DeviceManager::processDataConfigured | ( | ) | const |
Whether configureProcessData published a process image for exchange.
| bool mm::node::DeviceManager::processDataHealthy | ( | ) | const |
Whether every device that should be exchanging is contributing to the cycle.
True when process data is configured and the last working counter is at least the expected value. A drop below expected means a device that should be in SAFE-OP/OP stopped contributing (cable, fault, or an unexpected state change).
| std::expected< mm::comm::ProcessDataWatchdogConfig, std::string > mm::node::DeviceManager::processDataWatchdog | ( | uint16_t | slavePosition | ) |
Reads the process-data (sync-manager) watchdog configuration of one device.
Forwards to FieldbusDriver::processDataWatchdog after resolving the device. Returns the configured timeout itself (the divider/time registers), not the expiration counter that deviceDiagnostics surfaces.
| slavePosition | 1-based bus position of the target device. |
| uint64_t mm::node::DeviceManager::processImageGeneration | ( | ) | const |
Monotonic counter bumped every time a new process image is published (each (re)map).
A consumer that captured per-object layout (bit offsets/lengths) from the image compares this on each use: a change means the bus was re-mapped and the offsets can differ, so the captured layout must be refreshed. Lock-free (atomic). Distinct from topologyGeneration (scan/reset), which signals the device set itself changed.
| ProcessImageInfo mm::node::DeviceManager::processImageInfo | ( | ) | const |
Snapshots the published process image and its runtime health for the API.
Resolves every mapped object to an absolute bit offset and a name (looked up from each device's parameter map, empty if the OD is not enumerated) and reports the byte sizes, expected/last working counter, health, and the number of retained image generations. Returns configured false with empty object lists when no image is published. Runs on the (non-RT) caller's thread; reads the published image lock-free.
| std::expected< void, std::string > mm::node::DeviceManager::readAllDeviceParameters | ( | uint16_t | slavePosition | ) |
Refreshes the cached value of every readable parameter of one device, in place.
Unlike initializeDeviceParameters this does not re-enumerate the object dictionary — it re-reads the values of the parameters already loaded (PDO-aware per entry). See Device::readAllParameters for the per-entry semantics. Best-effort: a single object that fails to read is logged and the call still succeeds.
| slavePosition | 1-based bus position of the target device. |
| bool mm::node::DeviceManager::readRecord | ( | uint64_t | seq, |
| ProcessDataRing::Record & | out | ||
| ) | const |
Copies the recorded cycle for seq into out. Thread-safe on the same terms as recorderHead (the shared lock is taken per record, not per span).
true if seq is present and copied without a concurrent overwrite; false if it is no longer in the ring or the copy raced the producer (the caller skips it). | uint64_t mm::node::DeviceManager::recorderHead | ( | ) | const |
The recorder ring's next sequence number; recorderHead()-1 is the newest recorded cycle and recorderHead()==0 means nothing is recorded yet.
A monitoring holds a read cursor and ships every record in [cursor, recorderHead()) per flush, advancing the cursor — a non-destructive reader that never gates the RT producer.
Thread-safe against both writers of the ring, which are not the same adversary and do not need the same protection. ProcessDataRing is lock-free against the RT producer's write(); this and the two accessors below additionally take processDataMutex_ shared, because the control plane also writes the ring — allocate / clear release its storage on a re-map, scan and reset — and those take it exclusively. Reading it without that lock is a use-after-free rather than a torn read.
| uint64_t mm::node::DeviceManager::recorderOldestSeq | ( | ) | const |
The oldest sequence number still present in the ring (max(0, head - capacity)). A cursor below this was lapped (overwritten) and must resync to it. Thread-safe on the same terms as recorderHead.
| void mm::node::DeviceManager::reset | ( | ) |
Stops the fieldbus driver and clears the device list.
Transitions all slaves to INIT state, closes the network interface, and removes all Device objects. After this returns, init() and scan() may be called again. Must not be called while exchangeProcessData() is running concurrently.
| std::expected< int, std::string > mm::node::DeviceManager::scan | ( | ) |
Scans the bus for nodes and populates the device list.
Must be called after init(). Forwards to FieldbusDriver::scan().
| std::expected< mm::comm::ProcessDataWatchdogConfig, std::string > mm::node::DeviceManager::setProcessDataWatchdog | ( | uint16_t | slavePosition, |
| std::chrono::nanoseconds | timeout | ||
| ) |
Sets the process-data (sync-manager) watchdog timeout of one device.
Forwards to FieldbusDriver::setProcessDataWatchdog after resolving the device. A larger timeout lets a device tolerate the brief PDO pause of a whole-bus re-map without faulting; a timeout of zero disables the watchdog. The achieved timeout is rounded to the device's watchdog tick base — the returned config reports what was programmed. The write persists until the ESC reloads EEPROM (power cycle).
| slavePosition | 1-based bus position of the target device. |
| timeout | Desired watchdog timeout; zero disables the watchdog. |
timeout is unrepresentable, or a register access fails. | uint64_t mm::node::DeviceManager::shortWkcCycles | ( | ) | const |
Cycles that answered with a short working counter since the bus came up.
One atomic load, so it is cheap enough to poll as a change signal — which processImageInfo is not, since that takes a lock and walks the mapped-object lists. The count only ever rises while the bus exchanges, and reset() is the one place it returns to zero.
The same value ProcessImageInfo::shortWkcCycles carries. Read it there when the timestamps are wanted too.
| std::vector< OutputStageResult > mm::node::DeviceManager::stageProcessDataOutputs | ( | std::span< const OutputStageRequest > | requests | ) |
Stages a batch of output objects into the process image in one call.
Backs POST /api/process-data/outputs, the "send all" action of the Process Data page: the user edits several outputs and ships them together. Each request is routed through Device::writeParameter (same coercion + PDO-staging path as writeDeviceParameter), and each gets an OutputStageResult reporting whether it actually landed in the cyclic output image — so the UI can flag any object that was not staged without failing the whole batch.
Best-effort atomicity: the slots are stored sequentially on this (non-RT) thread, so a batch can straddle two consecutive RT cycles (≤1 ms skew worst case). Composing every slot in a single cycle would require RT-side generation gating, which would break the lock-free output path — out of scope; the brief skew is acceptable for manual process-data writes.
| requests | The objects to stage, each with a value coerced to its declared type. |
| uint64_t mm::node::DeviceManager::topologyGeneration | ( | ) | const |
Monotonic counter bumped every time the device set is rebuilt (scan / reset).
An off-thread consumer that pinned work to a device position records this value and compares it on each use: a change means the topology was re-scanned (or cleared) and a previously-valid position may now name a different device or none at all, so the consumer must re-validate. Lock-free (atomic). Starts at 0; the first scan makes it 1.
| std::expected< std::vector< DeviceStateInfo >, std::string > mm::node::DeviceManager::transitionToState | ( | const std::vector< uint16_t > & | positions, |
| mm::comm::EtherCatState | targetState, | ||
| std::chrono::steady_clock::duration | timeout | ||
| ) |
Transitions a set of devices to targetState, blocking until all arrive or timeout elapses, and reports the final state of each.
If positions is empty, all discovered devices are targeted. After the transition settles, the final AL state of every targeted device is read back and returned — so callers see exactly where each device ended up rather than a bare success flag. A device reached the target when its returned snapshot has error clear and alState equal to targetState; otherwise alStatusCode explains why.
Must be called after both init() and scan().
| positions | 1-based slave positions to transition; empty = all devices. |
| targetState | Desired EtherCAT AL state. |
| timeout | Maximum time to wait for all devices. |
| std::expected< DeviceParameter, std::string > mm::node::DeviceManager::writeDeviceParameter | ( | uint16_t | slavePosition, |
| uint16_t | index, | ||
| uint8_t | subindex, | ||
| const DeviceParameterValue & | newValue | ||
| ) |
Convenience: finds a device by position, writes one of its parameters, returns it.
Equivalent to findDevice(slavePosition)->writeParameter(index, subindex, value) — see Device::writeParameter for the online/offline semantics (offline edits succeed and are held as SyncState::Pending).
The updated parameter is returned rather than left for the caller to read back, because the write coerces the value to the declared type and moves syncState: a caller that wants either has to ask, and asking in a second call would resolve the position again. A scan landing in that gap publishes a new set, so the read-back would fail for a write that succeeded — leaving the caller to answer with something other than the parameter it is expected to produce. One call over both halves removes the gap.
| slavePosition | 1-based bus position of the target device. |
| index | CoE object index. |
| subindex | CoE object subindex. |
| value | Value to set; coerced to the parameter's declared type. |
syncState — or an error string if the device or parameter is unknown, the value cannot be coerced, or an online download fails. | std::expected< void, std::string > mm::node::DeviceManager::writeDevicePdoMapping | ( | uint16_t | slavePosition, |
| const PdoMapping & | mapping | ||
| ) |
Convenience: finds a device by position and writes its PDO mapping.
Equivalent to findDevice(slavePosition)->writePdoMapping(mapping) — see Device::writePdoMapping for the procedure and the PRE-OP requirement. Does not itself re-map the process image: the caller drives the device back to SAFE-OP/OP afterwards, and transitionToState re-reads the mapping and rebuilds the whole-bus image at that point.
| slavePosition | 1-based bus position of the target device. |
| mapping | Desired output (RxPDO) and input (TxPDO) mapping, in assignment order. |