///|
/// CLI scan command: discover EtherCAT slaves on the bus,
/// assign station addresses, read identity, and report results.
///
/// This module provides the library-level scan logic that
/// the CLI entry point invokes.
///|
/// ScanReport holds the results of a bus scan.
pub(all) struct ScanReport {
slave_count : Int
slaves : Array[SlaveReport]
} derive(Eq, Debug)
///|
/// SlaveReport holds info for one discovered slave.
pub(all) struct SlaveReport {
position : Int
station_address : UInt
alias_address : UInt?
vendor_id : UInt
product_code : UInt
revision : UInt
serial : UInt
identification : UInt
} derive(Eq, Debug)
///|
/// Default IdentificationAdo register used for explicit device identification.
let default_identification_ado : UInt = 0x0014U
///|
/// Base of the reserved scan-time station address range.
let scan_station_address_base : UInt = 0x1001U
///|
pub(all) enum ScanStationAddressStrategy {
PreserveExistingUniqueNonZero
TwinCATCompatible
} derive(Eq, Debug)
///|
let default_scan_station_address_strategy : ScanStationAddressStrategy = TwinCATCompatible
///|
/// Bytes needed to cover the fixed SII header through mailbox protocol word.
let sii_header_read_dwords : Int = 15
///|
pub(all) struct ObservedSlaveAddress {
position : UInt
current_station_address : UInt
} derive(Eq, Debug)
///|
/// Run the scan procedure: count slaves, assign addresses, read identity
/// from SII EEPROM registers.
/// Returns a ScanReport summarizing what was found.
pub fn[N : @hal.Nic] scan(
nic : N,
timeout : @hal.Duration,
) -> ScanReport raise @hal.EcError {
scan_with_identification_ado(nic, timeout, default_identification_ado)
}
///|
/// Run the scan procedure with a configurable IdentificationAdo register.
pub fn[N : @hal.Nic] scan_with_identification_ado(
nic : N,
timeout : @hal.Duration,
identification_ado : UInt,
) -> ScanReport raise @hal.EcError {
scan_with_identification_ado_and_station_address_strategy(
nic, timeout, identification_ado, default_scan_station_address_strategy,
)
}
///|
pub fn[N : @hal.Nic] scan_with_identification_ado_and_station_address_strategy(
nic : N,
timeout : @hal.Duration,
identification_ado : UInt,
strategy : ScanStationAddressStrategy,
) -> ScanReport raise @hal.EcError {
let count = @protocol.count_slaves(nic, timeout)
let observed_addresses = observe_slave_station_addresses(nic, count, timeout)
let planned_addresses = if strategy == default_scan_station_address_strategy {
plan_scan_station_addresses(observed_addresses)
} else {
plan_scan_station_addresses_with_strategy(observed_addresses, strategy)
}
apply_scan_station_addresses(
nic, observed_addresses, planned_addresses, timeout,
)
let slaves : Array[SlaveReport] = []
for i = 0; i < count; i = i + 1 {
let position = i.reinterpret_as_uint()
let station = planned_addresses[i]
let alias_address = try
@protocol.read_station_alias(nic, station, timeout)
catch {
_ => None
} noraise {
value => Some(value)
}
let identity = read_best_identity(nic, position, station, timeout)
let identification_pdu = @protocol.read_register_ap(
nic, position, identification_ado, 4, timeout,
)
let identification = read_u32_le(identification_pdu.data, 0)
slaves.push({
position: i,
station_address: station,
alias_address,
vendor_id: identity.vendor_id,
product_code: identity.product_code,
revision: identity.revision,
serial: identity.serial,
identification,
})
}
{ slave_count: count, slaves }
}
///|
fn[N : @hal.Nic] observe_slave_station_addresses(
nic : N,
count : Int,
timeout : @hal.Duration,
) -> Array[ObservedSlaveAddress] raise @hal.EcError {
let observed : Array[ObservedSlaveAddress] = []
for i = 0; i < count; i = i + 1 {
let position = i.reinterpret_as_uint()
observed.push({
position,
current_station_address: @protocol.read_station_address_ap(
nic, position, timeout,
),
})
}
observed
}
///|
fn plan_scan_station_addresses(
observed : Array[ObservedSlaveAddress],
) -> Array[UInt] raise @hal.EcError {
plan_scan_station_addresses_with_strategy(
observed, default_scan_station_address_strategy,
)
}
///|
pub fn plan_scan_station_addresses_with_strategy(
observed : Array[ObservedSlaveAddress],
strategy : ScanStationAddressStrategy,
) -> Array[UInt] raise @hal.EcError {
match strategy {
PreserveExistingUniqueNonZero =>
plan_scan_station_addresses_preserve_existing_unique_non_zero(observed)
TwinCATCompatible =>
plan_scan_station_addresses_preserve_existing_unique_non_zero(observed)
}
}
///|
fn plan_scan_station_addresses_preserve_existing_unique_non_zero(
observed : Array[ObservedSlaveAddress],
) -> Array[UInt] raise @hal.EcError {
let planned : Array[UInt] = []
let used : Array[UInt] = []
for item in observed {
let current_station_address = item.current_station_address
if should_preserve_scan_station_address(current_station_address, observed) &&
!array_contains_uint(used, current_station_address) {
planned.push(current_station_address)
used.push(current_station_address)
} else {
let reassigned = next_scan_station_address(used)
planned.push(reassigned)
used.push(reassigned)
}
}
planned
}
///|
fn should_preserve_scan_station_address(
station_address : UInt,
observed : Array[ObservedSlaveAddress],
) -> Bool {
station_address != 0U &&
scan_station_address_occurs_once(station_address, observed)
}
///|
fn scan_station_address_occurs_once(
station_address : UInt,
observed : Array[ObservedSlaveAddress],
) -> Bool {
let mut matches = 0
for item in observed {
if item.current_station_address == station_address {
matches = matches + 1
}
}
matches == 1
}
///|
fn next_scan_station_address(used : Array[UInt]) -> UInt raise @hal.EcError {
let mut candidate = scan_station_address_base
while array_contains_uint(used, candidate) {
if candidate == 0xFFFFU {
raise @hal.EcError::ConfigMismatch("scan station address pool exhausted")
}
candidate = candidate + 1U
}
candidate
}
///|
fn array_contains_uint(values : Array[UInt], target : UInt) -> Bool {
for value in values {
if value == target {
return true
}
}
false
}
///|
fn[N : @hal.Nic] apply_scan_station_addresses(
nic : N,
observed : Array[ObservedSlaveAddress],
planned : Array[UInt],
timeout : @hal.Duration,
) -> Unit raise @hal.EcError {
for i = 0; i < observed.length(); i = i + 1 {
let item = observed[i]
let station_address = planned[i]
if item.current_station_address != station_address {
@protocol.assign_station_address(
nic,
item.position,
station_address,
timeout,
)
}
}
}
///|
fn[N : @hal.Nic] read_best_identity(
nic : N,
position : UInt,
station : UInt,
timeout : @hal.Duration,
) -> @hal.SlaveIdentity {
let ap_sii = Result::Ok(read_identity_from_sii_ap(nic, position, timeout)) catch {
err => Result::Err(err)
}
match ap_sii {
Ok(identity) if !identity_is_zero(identity) => identity
_ => {
let fp_sii = Result::Ok(read_identity_from_sii(nic, station, timeout)) catch {
err => Result::Err(err)
}
match fp_sii {
Ok(identity) if !identity_is_zero(identity) => identity
_ => {
let ap_regs = Result::Ok(
read_identity_from_registers_ap(nic, position, timeout),
) catch {
err => Result::Err(err)
}
match ap_regs {
Ok(identity) if !identity_is_zero(identity) => identity
_ =>
read_identity_from_registers(nic, station, timeout) catch {
_ =>
{ vendor_id: 0U, product_code: 0U, revision: 0U, serial: 0U }
}
}
}
}
}
}
}
///|
fn identity_is_zero(identity : @hal.SlaveIdentity) -> Bool {
identity.vendor_id == 0U &&
identity.product_code == 0U &&
identity.revision == 0U &&
identity.serial == 0U
}
///|
fn[N : @hal.Nic] read_identity_from_sii_ap(
nic : N,
position : UInt,
timeout : @hal.Duration,
) -> @hal.SlaveIdentity raise @hal.EcError {
let eeprom = @protocol.eeprom_read_ap(
nic, position, 0U, sii_header_read_dwords, timeout,
)
let sii = @mailbox.parse_sii_header(eeprom)
{
vendor_id: sii.vendor_id,
product_code: sii.product_code,
revision: sii.revision,
serial: sii.serial,
}
}
///|
fn[N : @hal.Nic] read_identity_from_sii(
nic : N,
station : UInt,
timeout : @hal.Duration,
) -> @hal.SlaveIdentity raise @hal.EcError {
let eeprom = @protocol.eeprom_read(
nic, station, 0U, sii_header_read_dwords, timeout,
)
let sii = @mailbox.parse_sii_header(eeprom)
{
vendor_id: sii.vendor_id,
product_code: sii.product_code,
revision: sii.revision,
serial: sii.serial,
}
}
///|
fn[N : @hal.Nic] read_identity_from_registers(
nic : N,
station : UInt,
timeout : @hal.Duration,
) -> @hal.SlaveIdentity raise @hal.EcError {
let vendor_pdu = @protocol.read_register_fp(nic, station, 0x0008U, 4, timeout)
let product_pdu = @protocol.read_register_fp(
nic, station, 0x000CU, 4, timeout,
)
let revision_pdu = @protocol.read_register_fp(
nic, station, 0x0010U, 4, timeout,
)
let serial_pdu = @protocol.read_register_fp(nic, station, 0x0014U, 4, timeout)
{
vendor_id: read_u32_le(vendor_pdu.data, 0),
product_code: read_u32_le(product_pdu.data, 0),
revision: read_u32_le(revision_pdu.data, 0),
serial: read_u32_le(serial_pdu.data, 0),
}
}
///|
fn[N : @hal.Nic] read_identity_from_registers_ap(
nic : N,
position : UInt,
timeout : @hal.Duration,
) -> @hal.SlaveIdentity raise @hal.EcError {
let vendor_pdu = @protocol.read_register_ap(
nic, position, 0x0008U, 4, timeout,
)
let product_pdu = @protocol.read_register_ap(
nic, position, 0x000CU, 4, timeout,
)
let revision_pdu = @protocol.read_register_ap(
nic, position, 0x0010U, 4, timeout,
)
let serial_pdu = @protocol.read_register_ap(
nic, position, 0x0014U, 4, timeout,
)
{
vendor_id: read_u32_le(vendor_pdu.data, 0),
product_code: read_u32_le(product_pdu.data, 0),
revision: read_u32_le(revision_pdu.data, 0),
serial: read_u32_le(serial_pdu.data, 0),
}
}
///|
/// Read a little-endian u32 from bytes at the given offset.
fn read_u32_le(data : Bytes, offset : Int) -> UInt {
if data.length() < offset + 4 {
return 0U
}
data[offset].to_uint() |
(data[offset + 1].to_uint() << 8) |
(data[offset + 2].to_uint() << 16) |
(data[offset + 3].to_uint() << 24)
}
///|
/// Format a ScanReport as a human-readable string.
pub fn ScanReport::format(self : ScanReport) -> String {
let buf = StringBuilder(size_hint=256)
buf.write_string("EtherCAT Bus Scan Report\n")
buf.write_string("========================\n")
buf.write_string("Slaves found: " + self.slave_count.to_string() + "\n\n")
for i = 0; i < self.slaves.length(); i = i + 1 {
let s = self.slaves[i]
buf.write_string("Slave #" + s.position.to_string() + "\n")
buf.write_string(" Station: " + s.station_address.to_string() + "\n")
match s.alias_address {
Some(alias_value) =>
buf.write_string(" Alias: " + alias_value.to_string() + "\n")
None => ()
}
buf.write_string(" Vendor: " + s.vendor_id.to_string() + "\n")
buf.write_string(" Product: " + s.product_code.to_string() + "\n")
buf.write_string(" Revision: " + s.revision.to_string() + "\n")
buf.write_string(" Serial: " + s.serial.to_string() + "\n")
buf.write_string(" Identification: " + s.identification.to_string() + "\n")
if i < self.slaves.length() - 1 {
buf.write_string("\n")
}
}
buf.to_string()
}