///|
/// ESM (EtherCAT State Machine) transition engine.
/// Drives slave state transitions with confirmation and error rollback.
///|
let request_state_max_polls : Int = 32
///|
let request_state_extended_max_polls : Int = 4096
///|
pub(all) struct EsmStatusSnapshot {
state : EsmState
error_indication : Bool
status_code : UInt?
} derive(Eq, Debug)
///|
/// Final observation from a single AL Control state request.
pub(all) struct EsmStateRequestObservation {
state : EsmState
error_indication : Bool
status_code : UInt?
raw_status_code : UInt?
control_wkc : UInt
} derive(Eq, Debug)
///|
fn decode_al_status_snapshot(
status : Byte,
status_code : UInt?,
) -> EsmStatusSnapshot raise @hal.EcError {
let error_indication = (status & b'\x10') != b'\x00'
{
state: EsmState::from_byte(status),
error_indication,
status_code: if error_indication {
status_code
} else {
None
},
}
}
///|
/// Request an ESM state transition on a specific slave (by configured address).
/// Writes AL Control, then reads AL Status to confirm.
/// Returns the confirmed state or raises an error.
pub fn[N : @hal.Nic] request_state_observe(
nic : N,
station : UInt,
target : EsmState,
timeout : @hal.Duration,
) -> EsmStateRequestObservation raise @hal.EcError {
let current_status = read_al_status_raw_fp(nic, station, timeout)
if (current_status & b'\x10') != b'\x00' {
ignore(
request_state_control_fp(
nic,
station,
(current_status & b'\x0F') | b'\x10',
timeout,
),
)
}
let target_byte = target.to_byte()
let (_, control_wkc) = request_state_control_fp_with_wkc(
nic, station, target_byte, timeout,
)
let status = wait_for_state_fp(nic, station, target, timeout)
let actual_state = EsmState::from_byte(status)
if actual_state != target && (status & b'\x10') != b'\x00' {
let _ = request_state_control_fp(
nic,
station,
target_byte | b'\x10',
timeout,
)
let acknowledged = wait_for_state_fp(nic, station, target, timeout)
return request_observation_from_status_fp(
nic, station, acknowledged, control_wkc, timeout,
)
}
request_observation_from_status_fp(nic, station, status, control_wkc, timeout)
}
///|
pub fn[N : @hal.Nic] request_state(
nic : N,
station : UInt,
target : EsmState,
timeout : @hal.Duration,
) -> EsmState raise @hal.EcError {
request_state_observe(nic, station, target, timeout).state
}
///|
/// Request an ESM state transition while continuing cyclic PDO exchange,
/// and return the last confirmed AL snapshot observed during polling.
pub fn[N : @hal.Nic] request_state_with_process_data_observe(
nic : N,
station : UInt,
target : EsmState,
pdo_ctx : PdoContext,
timeout : @hal.Duration,
) -> EsmStateRequestObservation raise @hal.EcError {
if pdo_ctx.logical_size <= 0 {
return request_state_observe(nic, station, target, timeout)
}
let current_status = read_al_status_raw_fp(nic, station, timeout)
if (current_status & b'\x10') != b'\x00' {
ignore(
request_state_control_fp(
nic,
station,
(current_status & b'\x0F') | b'\x10',
timeout,
),
)
}
let target_byte = target.to_byte()
let (_, control_wkc) = request_state_control_fp_with_wkc(
nic, station, target_byte, timeout,
)
let snapshot = wait_for_state_fp_with_process_data_snapshot(
nic, station, target, pdo_ctx, timeout,
)
request_observation_from_snapshot_fp(
nic, station, snapshot, control_wkc, timeout,
)
}
///|
pub fn[N : @hal.Nic] request_state_with_process_data_snapshot(
nic : N,
station : UInt,
target : EsmState,
pdo_ctx : PdoContext,
timeout : @hal.Duration,
) -> EsmStatusSnapshot raise @hal.EcError {
let observed = request_state_with_process_data_observe(
nic, station, target, pdo_ctx, timeout,
)
{
state: observed.state,
error_indication: observed.error_indication,
status_code: observed.status_code,
}
}
///|
/// Request an ESM state transition while continuing cyclic PDO exchange.
///
/// This is primarily needed for SAFEOP -> OP on actuator-style devices where
/// the SubDevice only confirms OP once output process data has been received
/// during the confirmation window.
pub fn[N : @hal.Nic] request_state_with_process_data(
nic : N,
station : UInt,
target : EsmState,
pdo_ctx : PdoContext,
timeout : @hal.Duration,
) -> EsmState raise @hal.EcError {
let snapshot = request_state_with_process_data_observe(
nic, station, target, pdo_ctx, timeout,
)
if snapshot.state != target && snapshot.error_indication {
let _ = request_state_control_fp(
nic,
station,
target.to_byte() | b'\x10',
timeout,
)
return wait_for_state_fp_with_process_data_snapshot(
nic, station, target, pdo_ctx, timeout,
).state
}
snapshot.state
}
///|
/// Request an ESM state transition using auto-increment addressing.
pub fn[N : @hal.Nic] request_state_ap(
nic : N,
position : UInt,
target : EsmState,
timeout : @hal.Duration,
) -> EsmState raise @hal.EcError {
let current_status = read_al_status_raw_ap(nic, position, timeout)
if (current_status & b'\x10') != b'\x00' {
ignore(
request_state_control_ap(
nic,
position,
(current_status & b'\x0F') | b'\x10',
timeout,
),
)
}
let target_byte = target.to_byte()
let _ = request_state_control_ap(nic, position, target_byte, timeout)
let status = wait_for_state_ap(nic, position, target, timeout)
let actual_state = EsmState::from_byte(status)
if actual_state != target && (status & b'\x10') != b'\x00' {
let _ = request_state_control_ap(
nic,
position,
target_byte | b'\x10',
timeout,
)
let acknowledged = wait_for_state_ap(nic, position, target, timeout)
return EsmState::from_byte(acknowledged)
}
actual_state
}
///|
pub fn[N : @hal.Nic] read_al_status_snapshot(
nic : N,
station : UInt,
timeout : @hal.Duration,
) -> EsmStatusSnapshot raise @hal.EcError {
let status = read_al_status_raw_fp(nic, station, timeout)
let status_code = if (status & b'\x10') != b'\x00' {
Some(read_al_status_code(nic, station, timeout))
} else {
None
}
decode_al_status_snapshot(status, status_code)
}
///|
pub fn[N : @hal.Nic] read_al_status_snapshot_ap(
nic : N,
position : UInt,
timeout : @hal.Duration,
) -> EsmStatusSnapshot raise @hal.EcError {
let status = read_al_status_raw_ap(nic, position, timeout)
let status_code = if (status & b'\x10') != b'\x00' {
Some(read_al_status_code_ap(nic, position, timeout))
} else {
None
}
decode_al_status_snapshot(status, status_code)
}
///|
pub fn[N : @hal.Nic] clear_al_error(
nic : N,
station : UInt,
timeout : @hal.Duration,
) -> EsmStatusSnapshot raise @hal.EcError {
let snapshot = read_al_status_snapshot(nic, station, timeout)
if !snapshot.error_indication {
return snapshot
}
let cleared_status = request_state_control_fp(
nic,
station,
snapshot.state.to_byte() | b'\x10',
timeout,
)
let cleared = decode_al_status_snapshot(cleared_status, None)
if cleared.error_indication {
let status_code = Some(read_al_status_code(nic, station, timeout)) catch {
_ => None
}
let status_code_text = match status_code {
Some(code) =>
code.to_string() + " (" + al_status_code_description(code) + ")"
None => "unknown"
}
raise @hal.EcError::EsmStateError(
"failed to clear AL error indication: station=" +
station.to_string() +
" state=" +
@debug.to_string(cleared.state) +
" status_code=" +
status_code_text,
)
}
cleared
}
///|
fn should_rollback_al_error_to_init(status_code : UInt?) -> Bool {
match status_code {
Some(0x0016U) | Some(0x001DU) | Some(0x001EU) | Some(0x0070U) => true
_ => false
}
}
///|
/// Recover from a pending AL error before the caller selects a new target
/// state. Configuration-stage failures are stabilized by rolling back to Init;
/// other errors are acknowledged in place.
pub fn[N : @hal.Nic] recover_al_error_for_transition(
nic : N,
station : UInt,
timeout : @hal.Duration,
) -> EsmStatusSnapshot raise @hal.EcError {
let snapshot = read_al_status_snapshot(nic, station, timeout)
if !snapshot.error_indication {
return snapshot
}
if should_rollback_al_error_to_init(snapshot.status_code) &&
snapshot.state != Init {
let final_state = request_state(nic, station, Init, timeout)
return read_al_status_snapshot(nic, station, timeout) catch {
_ => { state: final_state, error_indication: false, status_code: None }
}
}
clear_al_error(nic, station, timeout)
}
///|
pub fn[N : @hal.Nic] clear_al_error_ap(
nic : N,
position : UInt,
timeout : @hal.Duration,
) -> EsmStatusSnapshot raise @hal.EcError {
let snapshot = read_al_status_snapshot_ap(nic, position, timeout)
if !snapshot.error_indication {
return snapshot
}
let cleared_status = request_state_control_ap(
nic,
position,
snapshot.state.to_byte() | b'\x10',
timeout,
)
let cleared = decode_al_status_snapshot(cleared_status, None)
if cleared.error_indication {
let status_code = Some(read_al_status_code_ap(nic, position, timeout)) catch {
_ => None
}
let status_code_text = match status_code {
Some(code) =>
code.to_string() + " (" + al_status_code_description(code) + ")"
None => "unknown"
}
raise @hal.EcError::EsmStateError(
"failed to clear AP AL error indication: position=" +
position.to_string() +
" state=" +
@debug.to_string(cleared.state) +
" status_code=" +
status_code_text,
)
}
cleared
}
///|
fn[N : @hal.Nic] wait_for_state_fp(
nic : N,
station : UInt,
target : EsmState,
timeout : @hal.Duration,
) -> Byte raise @hal.EcError {
let mut last_status = b'\x00'
let poll_budget = request_state_poll_budget(target, timeout)
for _attempt = 0; _attempt < poll_budget; _attempt = _attempt + 1 {
last_status = read_al_status_raw_fp(nic, station, timeout)
if (last_status & b'\x0F') == target.to_byte() {
return last_status
}
}
last_status
}
///|
fn[N : @hal.Nic] snapshot_from_status_fp(
nic : N,
station : UInt,
status : Byte,
timeout : @hal.Duration,
) -> EsmStatusSnapshot raise @hal.EcError {
let status_code = if (status & b'\x10') != b'\x00' {
Some(read_al_status_code(nic, station, timeout))
} else {
None
}
decode_al_status_snapshot(status, status_code)
}
///|
fn[N : @hal.Nic] wait_for_state_fp_with_process_data_snapshot(
nic : N,
station : UInt,
target : EsmState,
pdo_ctx : PdoContext,
timeout : @hal.Duration,
) -> EsmStatusSnapshot raise @hal.EcError {
let mut last_status = b'\x00'
let poll_budget = request_state_poll_budget(target, timeout)
for _attempt = 0; _attempt < poll_budget; _attempt = _attempt + 1 {
ignore(pdo_exchange_result(nic, pdo_ctx, timeout))
last_status = read_al_status_raw_fp(nic, station, timeout)
if (last_status & b'\x0F') == target.to_byte() ||
(last_status & b'\x10') != b'\x00' {
return snapshot_from_status_fp(nic, station, last_status, timeout)
}
}
snapshot_from_status_fp(nic, station, last_status, timeout)
}
///|
fn[N : @hal.Nic] wait_for_state_ap(
nic : N,
position : UInt,
target : EsmState,
timeout : @hal.Duration,
) -> Byte raise @hal.EcError {
let mut last_status = b'\x00'
let poll_budget = request_state_poll_budget(target, timeout)
for _attempt = 0; _attempt < poll_budget; _attempt = _attempt + 1 {
last_status = read_al_status_raw_ap(nic, position, timeout)
if (last_status & b'\x0F') == target.to_byte() {
return last_status
}
}
last_status
}
///|
fn request_state_poll_budget(target : EsmState, timeout : @hal.Duration) -> Int {
let @hal.Duration(transport_timeout_ns) = timeout
let @hal.Duration(transition_timeout_ns) = esm_default_timeout(target)
let effective_transport_timeout_ns = if transport_timeout_ns > 0L {
transport_timeout_ns
} else {
1L
}
let suggested_polls = ((
transition_timeout_ns + effective_transport_timeout_ns - 1L
) /
effective_transport_timeout_ns).to_int()
if suggested_polls <= request_state_max_polls {
request_state_max_polls
} else if suggested_polls >= request_state_extended_max_polls {
request_state_extended_max_polls
} else {
suggested_polls
}
}
///|
fn[N : @hal.Nic] request_state_control_fp(
nic : N,
station : UInt,
control : Byte,
timeout : @hal.Duration,
) -> Byte raise @hal.EcError {
let (status, _) = request_state_control_fp_with_wkc(
nic, station, control, timeout,
)
status
}
///|
fn[N : @hal.Nic] request_state_control_fp_with_wkc(
nic : N,
station : UInt,
control : Byte,
timeout : @hal.Duration,
) -> (Byte, UInt) raise @hal.EcError {
let buf = Bytes::from_array([control, b'\x00'])
let write_pdu = write_register_fp(nic, station, reg_al_control, buf, timeout)
let status_pdu = read_register_fp(nic, station, reg_al_status, 2, timeout)
if status_pdu.data.length() < 1 {
raise @hal.EcError::EsmStateError("empty AL Status response")
}
(status_pdu.data[0], write_pdu.wkc)
}
///|
fn[N : @hal.Nic] read_al_status_raw_fp(
nic : N,
station : UInt,
timeout : @hal.Duration,
) -> Byte raise @hal.EcError {
let status_pdu = read_register_fp(nic, station, reg_al_status, 2, timeout)
if status_pdu.data.length() < 1 {
raise @hal.EcError::EsmStateError("empty AL Status response")
}
status_pdu.data[0]
}
///|
fn[N : @hal.Nic] request_state_control_ap(
nic : N,
position : UInt,
control : Byte,
timeout : @hal.Duration,
) -> Byte raise @hal.EcError {
let buf = Bytes::from_array([control, b'\x00'])
let _ = write_register_ap(nic, position, reg_al_control, buf, timeout)
let status_pdu = read_register_ap(nic, position, reg_al_status, 2, timeout)
if status_pdu.data.length() < 1 {
raise @hal.EcError::EsmStateError("empty AL Status response")
}
status_pdu.data[0]
}
///|
fn[N : @hal.Nic] read_al_status_raw_ap(
nic : N,
position : UInt,
timeout : @hal.Duration,
) -> Byte raise @hal.EcError {
let status_pdu = read_register_ap(nic, position, reg_al_status, 2, timeout)
if status_pdu.data.length() < 1 {
raise @hal.EcError::EsmStateError("empty AL Status response")
}
status_pdu.data[0]
}
///|
fn[N : @hal.Nic] maybe_read_al_status_code_fp(
nic : N,
station : UInt,
timeout : @hal.Duration,
) -> UInt? {
Some(read_al_status_code(nic, station, timeout)) catch {
_ => None
}
}
///|
fn[N : @hal.Nic] request_observation_from_status_fp(
nic : N,
station : UInt,
status : Byte,
control_wkc : UInt,
timeout : @hal.Duration,
) -> EsmStateRequestObservation raise @hal.EcError {
let raw_status_code = maybe_read_al_status_code_fp(nic, station, timeout)
let snapshot = decode_al_status_snapshot(status, raw_status_code)
{
state: snapshot.state,
error_indication: snapshot.error_indication,
status_code: snapshot.status_code,
raw_status_code,
control_wkc,
}
}
///|
fn[N : @hal.Nic] request_observation_from_snapshot_fp(
nic : N,
station : UInt,
snapshot : EsmStatusSnapshot,
control_wkc : UInt,
timeout : @hal.Duration,
) -> EsmStateRequestObservation {
{
state: snapshot.state,
error_indication: snapshot.error_indication,
status_code: snapshot.status_code,
raw_status_code: maybe_read_al_status_code_fp(nic, station, timeout),
control_wkc,
}
}
///|
/// Optional per-target ESM transition timeout overrides.
///
/// Fields map to the target state of each transition step.
/// If a field is None, ETG-based default timeout is used.
pub(all) struct EsmTimeoutPolicy {
init : @hal.Duration?
preop : @hal.Duration?
safeop : @hal.Duration?
op : @hal.Duration?
boot : @hal.Duration?
} derive(Eq, Debug)
///|
/// Return the baseline ESM transition timeout for a given target state.
///
/// Defaults are aligned with common EtherCAT master practice and can be
/// overridden by SII/ESI-derived values through EsmTimeoutPolicy.
pub fn esm_default_timeout(target : EsmState) -> @hal.Duration {
match target {
Init => @hal.Duration(5_000_000_000L)
PreOp => @hal.Duration(3_000_000_000L)
SafeOp => @hal.Duration(10_000_000_000L)
Op => @hal.Duration(10_000_000_000L)
Boot => @hal.Duration(5_000_000_000L)
}
}
///|
/// Build a timeout policy that uses ETG-based defaults for all transitions.
pub fn default_esm_timeout_policy() -> EsmTimeoutPolicy {
{ init: None, preop: None, safeop: None, op: None, boot: None }
}
///|
/// Resolve transition timeout for a target state using policy override first,
/// then falling back to ETG-based defaults.
pub fn esm_timeout_for(
target : EsmState,
policy : EsmTimeoutPolicy,
) -> @hal.Duration {
let timeout_override = match target {
Init => policy.init
PreOp => policy.preop
SafeOp => policy.safeop
Op => policy.op
Boot => policy.boot
}
match timeout_override {
Some(timeout) => timeout
None => esm_default_timeout(target)
}
}
///|
/// Transition a slave through a sequence of ESM states.
/// Each step validates `can_transition` before issuing the request.
/// On failure it attempts rollback to the previous state.
pub fn[N : @hal.Nic] transition_through(
nic : N,
station : UInt,
path : Array[EsmState],
timeout : @hal.Duration,
) -> EsmState raise @hal.EcError {
if path.is_empty() {
raise @hal.EcError::Internal("empty transition path")
}
let mut current = path[0]
for i = 1; i < path.length(); i = i + 1 {
let target = path[i]
if !current.can_transition(target) {
raise @hal.EcError::EsmTransitionFailed(
from=@debug.to_string(current),
to=@debug.to_string(target),
)
}
let confirmed = request_state(nic, station, target, timeout) catch {
err => {
// Attempt rollback to previous state
let _ = Result::Ok(request_state(nic, station, current, timeout)) catch {
err => Result::Err(err)
}
raise err
}
}
current = confirmed
}
current
}
///|
/// Transition through an ESM path using per-target timeout policy.
///
/// Callers may feed SII/ESI-derived timeouts through `policy` while retaining
/// sane defaults for missing entries.
pub fn[N : @hal.Nic] transition_through_with_timeout_policy(
nic : N,
station : UInt,
path : Array[EsmState],
policy : EsmTimeoutPolicy,
) -> EsmState raise @hal.EcError {
if path.is_empty() {
raise @hal.EcError::Internal("empty transition path")
}
let mut current = path[0]
for i = 1; i < path.length(); i = i + 1 {
let target = path[i]
if !current.can_transition(target) {
raise @hal.EcError::EsmTransitionFailed(
from=@debug.to_string(current),
to=@debug.to_string(target),
)
}
let step_timeout = esm_timeout_for(target, policy)
let rollback_timeout = esm_timeout_for(current, policy)
let confirmed = request_state(nic, station, target, step_timeout) catch {
err => {
let _ = Result::Ok(
request_state(nic, station, current, rollback_timeout),
) catch {
err => Result::Err(err)
}
raise err
}
}
current = confirmed
}
current
}
///|
/// Transition through an ESM path and return the final step observation.
pub fn[N : @hal.Nic] transition_through_observe(
nic : N,
station : UInt,
path : Array[EsmState],
timeout : @hal.Duration,
) -> EsmStateRequestObservation? raise @hal.EcError {
if path.is_empty() {
raise @hal.EcError::Internal("empty transition path")
}
let mut current = path[0]
let mut last_observation : EsmStateRequestObservation? = None
for i = 1; i < path.length(); i = i + 1 {
let target = path[i]
if !current.can_transition(target) {
raise @hal.EcError::EsmTransitionFailed(
from=@debug.to_string(current),
to=@debug.to_string(target),
)
}
let observed = request_state_observe(nic, station, target, timeout) catch {
err => {
let _ = Result::Ok(request_state(nic, station, current, timeout)) catch {
err => Result::Err(err)
}
raise err
}
}
current = observed.state
last_observation = Some(observed)
}
last_observation
}
///|
/// Transition through an ESM path using per-target timeout policy and return
/// the final step observation.
pub fn[N : @hal.Nic] transition_through_with_timeout_policy_observe(
nic : N,
station : UInt,
path : Array[EsmState],
policy : EsmTimeoutPolicy,
) -> EsmStateRequestObservation? raise @hal.EcError {
if path.is_empty() {
raise @hal.EcError::Internal("empty transition path")
}
let mut current = path[0]
let mut last_observation : EsmStateRequestObservation? = None
for i = 1; i < path.length(); i = i + 1 {
let target = path[i]
if !current.can_transition(target) {
raise @hal.EcError::EsmTransitionFailed(
from=@debug.to_string(current),
to=@debug.to_string(target),
)
}
let step_timeout = esm_timeout_for(target, policy)
let rollback_timeout = esm_timeout_for(current, policy)
let observed = request_state_observe(nic, station, target, step_timeout) catch {
err => {
let _ = Result::Ok(
request_state(nic, station, current, rollback_timeout),
) catch {
err => Result::Err(err)
}
raise err
}
}
current = observed.state
last_observation = Some(observed)
}
last_observation
}
///|
pub fn[N : @hal.Nic] transition_through_ap(
nic : N,
position : UInt,
path : Array[EsmState],
timeout : @hal.Duration,
) -> EsmState raise @hal.EcError {
if path.is_empty() {
raise @hal.EcError::Internal("empty transition path")
}
let mut current = path[0]
for i = 1; i < path.length(); i = i + 1 {
let target = path[i]
if !current.can_transition(target) {
raise @hal.EcError::EsmTransitionFailed(
from=@debug.to_string(current),
to=@debug.to_string(target),
)
}
let confirmed = request_state_ap(nic, position, target, timeout) catch {
err => {
let _ = Result::Ok(request_state_ap(nic, position, current, timeout)) catch {
err => Result::Err(err)
}
raise err
}
}
current = confirmed
}
current
}
///|
/// Build the standard forward transition path from Init to a target state.
pub fn build_transition_path(target : EsmState) -> Array[EsmState] {
match target {
Init => [Init]
PreOp => [Init, PreOp]
SafeOp => [Init, PreOp, SafeOp]
Op => [Init, PreOp, SafeOp, Op]
Boot => [Init, Boot]
}
}
///|
/// Build the safe shutdown path from a current state to Init.
pub fn build_shutdown_path(current : EsmState) -> Array[EsmState] {
match current {
Init => [Init]
PreOp => [PreOp, Init]
SafeOp => [SafeOp, PreOp, Init]
Op => [Op, SafeOp, PreOp, Init]
Boot => [Boot, Init]
}
}
///|
/// Build a complete transition route from the current state to a target state.
///
/// This reuses direct transitions when possible and otherwise falls back through
/// the standard shutdown and forward startup paths.
pub fn build_transition_route(
current : EsmState,
target : EsmState,
) -> Array[EsmState] {
if current == target {
return [current]
}
if target == Init {
return build_shutdown_path(current)
}
if current == Init {
return build_transition_path(target)
}
if current.can_transition(target) {
return [current, target]
}
let shutdown_path = build_shutdown_path(current)
let startup_path = build_transition_path(target)
if shutdown_path.is_empty() {
return startup_path
}
if startup_path.is_empty() {
return shutdown_path
}
let result = shutdown_path
for i = 1; i < startup_path.length(); i = i + 1 {
result.push(startup_path[i])
}
result
}
///|
/// Build a strict route that respects ETG-defined legal ESM step directions.
///
/// Callers should use the returned path together with `transition_through`, or
/// use `transition_to_target_strict` for a guarded one-shot operation.
pub fn build_strict_transition_route(
current : EsmState,
target : EsmState,
) -> Array[EsmState] {
build_transition_route(current, target)
}
///|
/// Strict state transition entry point.
///
/// If AL Error Indication is currently set, this function refuses to guess the
/// recovery policy and requires the caller to recover the error first via
/// `recover_al_error_for_transition(...)` (or `clear_al_error(...)` when the
/// caller knows an in-place acknowledge is correct), then explicitly choose
/// the next target state.
pub fn[N : @hal.Nic] transition_to_target_strict(
nic : N,
station : UInt,
target : EsmState,
timeout : @hal.Duration,
) -> EsmState raise @hal.EcError {
let snapshot = read_al_status_snapshot(nic, station, timeout)
if snapshot.error_indication {
raise @hal.EcError::EsmStateError(
"AL error indication is set; clear the error before selecting a new target state",
)
}
let path = build_strict_transition_route(snapshot.state, target)
transition_through(nic, station, path, timeout)
}
///|
/// Strict state transition entry point that returns the final request observation.
pub fn[N : @hal.Nic] transition_to_target_strict_observe(
nic : N,
station : UInt,
target : EsmState,
timeout : @hal.Duration,
) -> EsmStateRequestObservation? raise @hal.EcError {
let snapshot = read_al_status_snapshot(nic, station, timeout)
if snapshot.error_indication {
raise @hal.EcError::EsmStateError(
"AL error indication is set; clear the error before selecting a new target state",
)
}
let path = build_strict_transition_route(snapshot.state, target)
transition_through_observe(nic, station, path, timeout)
}
///|
pub fn[N : @hal.Nic] transition_to_target_strict_ap(
nic : N,
position : UInt,
target : EsmState,
timeout : @hal.Duration,
) -> EsmState raise @hal.EcError {
let snapshot = read_al_status_snapshot_ap(nic, position, timeout)
if snapshot.error_indication {
raise @hal.EcError::EsmStateError(
"AL error indication is set; clear the error before selecting a new target state",
)
}
let path = build_strict_transition_route(snapshot.state, target)
transition_through_ap(nic, position, path, timeout)
}
///|
/// Build an EsmTimeoutPolicy from SII-parsed timeouts (ETG.2010 category 41).
///
/// Mapping:
/// - preop_timeout_ms → preop (transition TO PreOp)
/// - safeop_op_timeout_ms → op (SafeOp→Op transition)
/// - back_to_init_timeout_ms → init (transition TO Init)
/// - back_to_safeop_timeout_ms → safeop (transition TO SafeOp)
/// - boot → None (not specified by SII)
///
/// Returns a default (all-None) policy when timeouts are absent.
pub fn esm_timeout_policy_from_sii_timeouts(
timeouts : @mailbox.SiiTimeouts?,
) -> EsmTimeoutPolicy {
match timeouts {
None => default_esm_timeout_policy()
Some(t) => {
let ms_to_duration = fn(ms : UInt) -> @hal.Duration? {
if ms == 0U {
None
} else {
Some(@hal.Duration(ms.reinterpret_as_int().to_int64() * 1_000_000L))
}
}
{
init: ms_to_duration(t.back_to_init_timeout_ms),
preop: ms_to_duration(t.preop_timeout_ms),
safeop: ms_to_duration(t.back_to_safeop_timeout_ms),
op: ms_to_duration(t.safeop_op_timeout_ms),
boot: None,
}
}
}
}
///|
/// Strict transitions via multi-step route with per-target timeout policy.
///
/// Same as `transition_to_target_strict` but resolves per-step timeouts from
/// the supplied policy, allowing SII/ESI-derived timeout overrides.
pub fn[N : @hal.Nic] transition_to_target_strict_with_timeout_policy(
nic : N,
station : UInt,
target : EsmState,
policy : EsmTimeoutPolicy,
fallback_timeout : @hal.Duration,
) -> EsmState raise @hal.EcError {
let snapshot = read_al_status_snapshot(nic, station, fallback_timeout)
if snapshot.error_indication {
raise @hal.EcError::EsmStateError(
"AL error indication is set; clear the error before selecting a new target state",
)
}
let path = build_strict_transition_route(snapshot.state, target)
transition_through_with_timeout_policy(nic, station, path, policy)
}
///|
/// Strict transitions via multi-step route with per-target timeout policy,
/// returning the final step observation when a control write was issued.
pub fn[N : @hal.Nic] transition_to_target_strict_with_timeout_policy_observe(
nic : N,
station : UInt,
target : EsmState,
policy : EsmTimeoutPolicy,
fallback_timeout : @hal.Duration,
) -> EsmStateRequestObservation? raise @hal.EcError {
let snapshot = read_al_status_snapshot(nic, station, fallback_timeout)
if snapshot.error_indication {
raise @hal.EcError::EsmStateError(
"AL error indication is set; clear the error before selecting a new target state",
)
}
let path = build_strict_transition_route(snapshot.state, target)
transition_through_with_timeout_policy_observe(nic, station, path, policy)
}
///|
/// Read the current ESM state from a slave.
pub fn[N : @hal.Nic] read_al_status(
nic : N,
station : UInt,
timeout : @hal.Duration,
) -> EsmState raise @hal.EcError {
let pdu = read_register_fp(nic, station, reg_al_status, 2, timeout)
if pdu.data.length() < 1 {
raise @hal.EcError::EsmStateError("empty AL Status response")
}
EsmState::from_byte(pdu.data[0])
}
///|
/// Read the current ESM state using auto-increment addressing.
pub fn[N : @hal.Nic] read_al_status_ap(
nic : N,
position : UInt,
timeout : @hal.Duration,
) -> EsmState raise @hal.EcError {
let pdu = read_register_ap(nic, position, reg_al_status, 2, timeout)
if pdu.data.length() < 1 {
raise @hal.EcError::EsmStateError("empty AL Status response")
}
EsmState::from_byte(pdu.data[0])
}
///|
/// Read the AL Status Code from a slave (error code for failed transitions).
pub fn[N : @hal.Nic] read_al_status_code(
nic : N,
station : UInt,
timeout : @hal.Duration,
) -> UInt raise @hal.EcError {
let pdu = read_register_fp(nic, station, reg_al_status_code, 2, timeout)
if pdu.data.length() < 2 {
return 0U
}
pdu.data[0].to_int().reinterpret_as_uint() |
(pdu.data[1].to_int().reinterpret_as_uint() << 8)
}
///|
pub fn[N : @hal.Nic] read_al_status_code_ap(
nic : N,
position : UInt,
timeout : @hal.Duration,
) -> UInt raise @hal.EcError {
let pdu = read_register_ap(nic, position, reg_al_status_code, 2, timeout)
if pdu.data.length() < 2 {
return 0U
}
pdu.data[0].to_int().reinterpret_as_uint() |
(pdu.data[1].to_int().reinterpret_as_uint() << 8)
}
///|
/// Request all slaves to transition to a target state via broadcast.
/// Uses BWR to AL Control register. Returns the working counter.
pub fn[N : @hal.Nic] broadcast_state(
nic : N,
target : EsmState,
timeout : @hal.Duration,
ack_error? : Bool = false,
) -> UInt raise @hal.EcError {
let target_byte = target.to_byte()
let control = if ack_error { target_byte | b'\x10' } else { target_byte }
let buf = Bytes::from_array([control, b'\x00'])
let addr = broadcast_address(reg_al_control)
let pdu = transact_pdu(nic, EcCommand::BWR, addr, buf, timeout)
pdu.wkc
}
///|
/// Reset all slaves to a known default state before scanning.
///
/// Broadcasts zero-writes to FMMU, SM, DC, CRC, IRQ, and EEPROM registers,
/// matching the sequence used by SOEM `ecx_set_slaves_to_default`.
/// This ensures a clean slate regardless of any previous master session state.
pub fn[N : @hal.Nic] set_slaves_to_default(
nic : N,
timeout : @hal.Duration,
) -> Unit raise @hal.EcError {
let zbuf48 = Bytes::new(48)
let zbuf32 = Bytes::new(32)
let zbuf8 = Bytes::new(8)
let zbuf4 = Bytes::new(4)
// Deactivate loop-manual forwarding
let _ = transact_pdu(
nic,
EcCommand::BWR,
broadcast_address(reg_dl_port),
Bytes::from_array([b'\x00']),
timeout,
)
// Set IRQ mask (0x0004 = AL Status Change Event)
let _ = transact_pdu(
nic,
EcCommand::BWR,
broadcast_address(reg_irq_mask),
Bytes::from_array([b'\x04', b'\x00']),
timeout,
)
// Reset CRC / RX error counters (8 bytes at 0x0300)
let _ = transact_pdu(
nic,
EcCommand::BWR,
broadcast_address(reg_rx_error_counter),
zbuf8,
timeout,
)
// Reset FMMU0..2 (3 * 16 = 48 bytes at 0x0600)
let _ = transact_pdu(
nic,
EcCommand::BWR,
broadcast_address(reg_fmmu_base),
zbuf48,
timeout,
)
// Reset SM0..3 (4 * 8 = 32 bytes at 0x0800)
let _ = transact_pdu(
nic,
EcCommand::BWR,
broadcast_address(reg_sm_base),
zbuf32,
timeout,
)
// Deactivate DC sync
let _ = transact_pdu(
nic,
EcCommand::BWR,
broadcast_address(reg_dc_sync_act),
Bytes::from_array([b'\x00']),
timeout,
)
// Clear DC system time (4 bytes)
let _ = transact_pdu(
nic,
EcCommand::BWR,
broadcast_address(reg_dc_system_time),
zbuf4,
timeout,
)
// DC speed counter start = 0x1000
let _ = transact_pdu(
nic,
EcCommand::BWR,
broadcast_address(reg_dc_speed_count),
Bytes::from_array([b'\x00', b'\x10']),
timeout,
)
// DC time loop filter = 0x0C00
let _ = transact_pdu(
nic,
EcCommand::BWR,
broadcast_address(reg_dc_time_filter),
Bytes::from_array([b'\x00', b'\x0C']),
timeout,
)
// Clear station alias
let _ = transact_pdu(
nic,
EcCommand::BWR,
broadcast_address(reg_station_alias),
Bytes::from_array([b'\x00']),
timeout,
)
// Reset all slaves to Init + Ack
let _ = broadcast_state(nic, Init, timeout, ack_error=true)
// Force EEPROM from PDI, then set to master
let _ = transact_pdu(
nic,
EcCommand::BWR,
broadcast_address(reg_sii_config),
Bytes::from_array([b'\x02']),
timeout,
)
let _ = transact_pdu(
nic,
EcCommand::BWR,
broadcast_address(reg_sii_config),
Bytes::from_array([b'\x00']),
timeout,
)
}