Skip to content

Add a diagnostics hook and exception isolation to EcSlaveBase - #244

Open
mcbed wants to merge 12 commits into
mainfrom
feat/slave-fault-isolation
Open

Add a diagnostics hook and exception isolation to EcSlaveBase#244
mcbed wants to merge 12 commits into
mainfrom
feat/slave-fault-isolation

Conversation

@mcbed

@mcbed mcbed commented Aug 19, 2026

Copy link
Copy Markdown
Member
  • collectDiagnostics(): lets any slave report itself (generic online/AL-state by default; subclasses extend it). setHealth() feeds in the master's per-cycle link/AL-state observation.
  • processDataSafe()/updateStateSafe(): any EcMasterBase implementation should call these instead of process_data()/updateState() directly — they catch, log, and record a fault so one misbehaving slave can't abort the read/write cycle for the rest of the bus.

To be merged after #241

mcbed added 9 commits August 18, 2026 13:21
…e identity check to the master abstraction

The master abstraction (EcMasterBase/EtherlabMaster) dropped several
capabilities EcMaster had before the split: a blocking CoE SDO upload
(read), public getters for master/domain/slave state, an is_valid()
guard, and add_slave()'s vendor/product identity verification against
what's actually on the bus. All are restored here at the EcMasterBase
interface (as backend-agnostic types with no ecrt.h/IgH dependency)
and implemented in EtherlabMaster:

- EcMasterBase gains is_valid(), upload_slave_sdo(),
  get_master_state()/get_domain_state()/get_slave_states(), backed by
  new portable EcMasterStateInfo/EcDomainStateInfo/EcSlaveStateInfo
  structs (no ecrt_master_state_t/ec_domain_state_t leakage, so a
  future non-IgH master plugin isn't tied to these types).
- EtherlabMaster::upload_slave_sdo() refuses (logs an error, returns
  -1) if called while activated_ — this is a blocking mailbox
  round-trip and must only run during the configure phase (after
  configure_slaves(), before start()), never while the cyclic
  process-data loop is running.
- EtherlabMaster::add_slave() again refuses to configure a slave whose
  vendor/product doesn't match what's actually on the bus at that ring
  position (alias-0 slaves only, addressed by absolute position),
  instead of silently leaving it unconfigured.
- EthercatBusManager's masterState()/domainState()/slaveStates()/
  readSlaveSdo() are wired to the new methods; their declared return
  types (ec_master_state_t/ec_domain_state_t, with no <ecrt.h> in
  reach from this header) would not have compiled otherwise. Also
  fixes a pre-existing bug (bus_config_.master_id doesn't exist; the
  field is master_iface) and drops a dead, already-commented-out SDO
  config block superseded by EtherlabMaster::configure_slaves().
…hex status logging

- Adopts the process_data(int index, ...) rename cleanly (removes the
  two commented-out old-signature leftovers from the rebase).
- Restores targetPositionPassthrough(): extracts the hardcoded
  "override 0x607a unless CSP" check into an overridable virtual, so a
  vendor subclass can extend which modes pass the target position
  through (e.g. an impedance mode that also uses 0x607a as an
  equilibrium setpoint) without duplicating process_data().
- Logs status_word in hex (matches how it's read/reported everywhere
  else) instead of decimal.
…driver

EthercatBusConfig::master_plugin defaults to "ethercat_master/EtherlabMaster",
pluginlib-loaded at runtime, but nothing declared that this package needs to
be installed/sourced for the default to resolve.
Port of 771e984 (never carried into EtherlabMaster when it superseded
EcMaster). Without ecrt_release_master() the kernel device stays
locked after the process exits, preventing re-initialization on
restart without unloading the kernel module — critical in
multi-master setups.
Generalizes the pattern behind a vendor's drive-commissioning check
(e.g. verifying a torque sensor is configured before enabling impedance)
into something any slave_config can use: a one-shot SDO upload
(read) at configure time, refusing to configure the slave if the value
isn't in an allowed set. Mirrors the existing sdo: write block
structurally, but never writes anything — for hardware preconditions
that must already be true on the physical drive (a commissioned sensor
type, a firmware revision, ...), as opposed to sdo: which pushes a
value onto the drive.

- SdoCheckEntry (ec_sdo_manager.hpp): parses index/sub_index/type plus
  either a single `value` or a `values` list, and an optional
  `description` surfaced in the failure message. decode()/matches()
  reuse the existing little-endian read_* macros so behaviour matches
  the wire format ecrt_master_sdo_upload returns.
- EcSlaveBase gains sdo_check_config_/get_sdo_check_config(), parsed
  from a slave_config's `sdo_check:` block in GenericEcSlave (and thus
  inherited by EcCiA402Drive) alongside the existing `sdo:` parsing.
- EtherlabMaster::configure_slaves() runs each slave's checks (after
  its sdo: writes, so a check can validate their result) via
  ecrt_master_sdo_upload, and returns false — refusing to configure —
  on a read failure, a short read, or a value outside the allowed set.
…ister)

A module that fails validation (vendor/product identity or sdo_check:)
used to always abort the whole bus, at any scale — one bad drive out
of 40 blocked everything. Splits bring-up into two phases:

- Phase 1 (validate, every module, no side effects): identity check +
  sdo_check: reads. Both are addressed by ring position at the master
  level and need no ecrt_master_slave_config() first, so this has
  nothing to undo on failure.
- Phase 2 (register, passing modules only): ecrt_master_slave_config()
  + PDO mapping + domain registration + sdo: writes — exactly what
  add_slave()/configure_slaves() already did, just restricted to the
  modules that passed phase 1.

New per-module "required" param (absent or "true" — today's only
behavior, unchanged — or "true": failure refuses to configure the
whole bus, exactly as before. "false": the module is excluded instead
— never added, absent from the cyclic domain, not readable or
writable — while the rest of the bus still comes up. No new state or
interface needed for this: EthercatClient::slaveHealth() (in the
downstream consumer) already default-initializes to offline and only
overwrites entries it finds a matching alias/position for in
get_slave_states(), so an excluded module already reads as
slave_online: false with zero changes on that side.

EcMasterBase gains check_slave() (new pure virtual — validate only,
no registration) alongside the existing add_slave(). EtherlabMaster
factors the identity check and the sdo_check: reads (moved out of
configure_slaves(), which now only does sdo: writes) into two shared
private helpers used by both check_slave() and add_slave(), so
add_slave()'s own behavior/contract for any direct caller is
unchanged.

Transparent for existing configs: with "required" absent everywhere
(the default), phase 1 fail-fast-aborts on the first failing module
exactly as add_slave() used to, byte for byte.
deactivateBus() only stopped our own cyclic loop; it never told the master
or slaves anything, so slaves stayed stuck at OP indefinitely with no
mailbox/SDO access. Per spec, loss of cyclic data should let each slave's
Sync Manager Watchdog autonomously drop OP -> Safe-OP instead.

Add EcMasterBase::deactivate() (ecrt_master_deactivate(), implemented in
EtherlabMaster), call it from deactivateBus(), and gate master (re)request
on master_ existing rather than on configured_ so a later activation
re-requests the network but not the master.

Verified on hardware: slave drops OP -> PREOP immediately on deactivate,
reactivation brings it back to OP, SIGINT shutdown still releases the
master cleanly.
deactivateBus() clearing configured_ meant a reactivation re-ran the full
network setup, including the one-shot config SDO downloads (the
slave_config YAML sdo: block) — those must be sent exactly once, not
repeated every time the bus comes back from inactive.

Split configNetwork() into registerSlaves() (identity/sdo_check: + PDO
domain registration — must be re-runnable, since deactivate() frees that
registration) and downloadSdoConfig() (the config SDO writes — must run
exactly once, ever). Track their validity separately: network_registered_
is what deactivateBus() clears and activateBusLocked() rebuilds via
registerSlaves() alone; configured_ now means "SDO config has been
downloaded" and is never cleared by deactivateBus().

Verified on hardware: "Downloading config SDO(s)" logs exactly once
across configure + two full deactivate/activate cycles, with the drive
still reaching OP cleanly each time.
activateBusLocked() called spin_slaves_until_operational(), a hand-timed
clock_nanosleep loop (no timeout) run outside the real RT cycle, purely to
wait for a byproduct of the ordinary cyclic exchange: AL-state progression
and DC sync already happen unconditionally on every
read_process_data()/write_process_data() call, starting with the very
first cycle once the bus is active. Each module's own state-machine walk
already gates on its real per-slave AL state (is_operational_), so nothing
needed this wait to be safe.

Drop the call; on_activate() now returns as soon as master_->start()
and transfer-net registration succeed. Slaves walk to OPERATIONAL over
the following ordinary RT cycles instead of a bolted-on timing loop.
spin_slaves_until_operational() and its sole helper update() had no other
caller, so both are removed rather than left dead.

Verified on hardware: activateBusLocked() now returns in ~1.6ms instead of
blocking; the bus still reaches OP (EthercatClient::start(), unaffected,
absorbs the wait via its own pre-existing settle loop), and a
deactivate/reactivate cycle still works correctly.
mcbed added 3 commits August 24, 2026 16:59
be93554 removed spin_slaves_until_operational() (an untimed
clock_nanosleep loop, with its own private read/write reimplementation)
but left nothing in its place: activateBus() reported success as soon as
master_->start() succeeded, with no guarantee any slave had actually
reached OPERATIONAL before a caller's first read().

The old wait isn't a fit to restore even with a timeout added: it polled
EcCiA402Drive::initialized() (CiA402 Operation Enabled), which can never
become true during activation on a stack that doesn't auto-enable - it
would just hang until timeout every time.

waitForSlavesOperational() instead polls EcSlaveStateInfo::operational
(bus AL-state, via slaveStates()) through the ordinary read()/write()
cycle, bounded by a new readiness_timeout_s bus parameter (default 25s),
rolling back via deactivateBus() on timeout or a read()/write() failure.

Confirmed on real hardware (2dof bench): both slaves reached OPERATIONAL
in ~9.7s, activateBus() blocked correctly until then, and controller
spawning proceeded normally afterward.
A slave has no visibility into its own link/AL state; add setHealth()
so whoever owns the master can push that in each cycle, and
collectDiagnostics() so a slave can report itself. The base
implementation covers generic EtherCAT-layer health (online, al_state);
subclasses with more specific diagnostics override it, call the base
first, then append.

Non-pure virtual with a working default, so existing slave plugins are
unaffected until they choose to extend it.
A throw from one slave's process_data()/updateState() would otherwise
propagate out of the caller's per-cycle loop over the whole bus, aborting
it for every other slave. Add processDataSafe()/updateStateSafe()
wrappers on EcSlaveBase that any EcMasterBase implementation should call
instead of the raw virtuals: they catch, log once, and record the fault
so collectDiagnostics() can report it on that slave only.
@mcbed
mcbed force-pushed the feat/slave-fault-isolation branch from 703da33 to 99b959b Compare August 24, 2026 15:35
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.

1 participant