///|
/// PROFIdrive controller application – drive states (IEC 61800-7-200 §6.3).
pub(all) enum DriveState {
S1_SwitchingOnInhibited
S2_ReadyToSwitchOn
S3_SwitchedOn
S4_Operation
AUS2_CoastStop
AUS3_QuickStop
} derive(Eq, Debug)
///|
pub fn drive_state_label(state : DriveState) -> String {
match state {
S1_SwitchingOnInhibited => "S1 Switching On Inhibited"
S2_ReadyToSwitchOn => "S2 Ready To Switch On"
S3_SwitchedOn => "S3 Switched On"
S4_Operation => "S4 Operation"
AUS2_CoastStop => "AUS2 Coast Stop"
AUS3_QuickStop => "AUS3 Quick Stop"
}
}
///|
/// STW1 bit masks (control word 1).
pub let stw1_on_off1 : Int = 0x0001
///|
pub let stw1_no_coast_stop : Int = 0x0002
///|
pub let stw1_no_quick_stop : Int = 0x0004
///|
pub let stw1_enable_operation : Int = 0x0008
///|
pub let stw1_enable_ramp : Int = 0x0010
///|
pub let stw1_unfreeze_ramp : Int = 0x0020
///|
pub let stw1_enable_setpoint : Int = 0x0040
///|
pub let stw1_fault_ack : Int = 0x0080
///|
/// ZSW1 bit masks (status word 1).
pub let zsw1_ready_to_switch_on : Int = 0x0001
///|
pub let zsw1_ready_to_operate : Int = 0x0002
///|
pub let zsw1_operation_enabled : Int = 0x0004
///|
pub let zsw1_fault_active : Int = 0x0008
///|
pub let zsw1_no_coast_stop : Int = 0x0010
///|
pub let zsw1_no_quick_stop : Int = 0x0020
///|
pub let zsw1_switching_on_inhibited : Int = 0x0040
///|
pub let zsw1_warning_active : Int = 0x0080
///|
pub let zsw1_speed_deviation : Int = 0x0100
///|
pub let zsw1_control_requested : Int = 0x0200
///|
pub let zsw1_target_reached : Int = 0x0400
///|
/// Derive drive state from ZSW1 status word.
pub fn drive_state_from_zsw1(zsw1 : Int) -> DriveState {
if (zsw1 & zsw1_switching_on_inhibited) != 0 {
S1_SwitchingOnInhibited
} else if (zsw1 & zsw1_operation_enabled) != 0 {
S4_Operation
} else if (zsw1 & zsw1_ready_to_operate) != 0 {
S3_SwitchedOn
} else if (zsw1 & zsw1_ready_to_switch_on) != 0 {
S2_ReadyToSwitchOn
} else {
S1_SwitchingOnInhibited
}
}
///|
/// Build STW1 for a desired target state transition.
pub fn stw1_for_transition(target : DriveState) -> Int {
match target {
S1_SwitchingOnInhibited => 0
S2_ReadyToSwitchOn => stw1_no_coast_stop | stw1_no_quick_stop
S3_SwitchedOn => stw1_on_off1 | stw1_no_coast_stop | stw1_no_quick_stop
S4_Operation =>
stw1_on_off1 |
stw1_no_coast_stop |
stw1_no_quick_stop |
stw1_enable_operation |
stw1_enable_ramp |
stw1_unfreeze_ramp |
stw1_enable_setpoint
AUS2_CoastStop => 0
AUS3_QuickStop => stw1_no_coast_stop
}
}
///|
/// Telegram data structure (PDC_DO_TELEGRAM_TYPE, §5.2).
pub(all) struct TelegramData {
/// Control word 1
stw1 : Int
/// Control word 2
stw2 : Int
/// Speed setpoint 16-bit (N_SOLL_A, 0x4000 = 100%)
n_soll_a : Int
/// Speed setpoint 32-bit (N_SOLL_B)
n_soll_b : Int
/// Sensor 1 control word
g1_stw : Int
/// Sensor 2 control word
g2_stw : Int
/// System deviation
xerr : Int
/// Position controller gain
kpc : Int
/// Status word 1
zsw1 : Int
/// Status word 2
zsw2 : Int
/// Actual speed 16-bit (NIST_A)
nist_a : Int
/// Actual speed 32-bit (NIST_B)
nist_b : Int
/// Sensor 1 status word
g1_zsw : Int
/// Sensor 1 position 1
g1_xist1 : Int
/// Sensor 1 position 2
g1_xist2 : Int
/// Sensor 2 status word
g2_zsw : Int
/// Sensor 2 position 1
g2_xist1 : Int
/// Sensor 2 position 2
g2_xist2 : Int
} derive(Eq, Debug)
///|
/// Speed setpoint normalisation: 0x4000 (16384) = 100%.
pub let n_soll_a_100_percent : Int = 0x4000
///|
/// Speed setpoint normalisation for 32-bit: 0x40000000 = 100%.
pub let n_soll_b_100_percent : Int = 0x40000000
///|
pub fn speed_percent_a(n_soll : Int) -> Double {
n_soll.to_double() / n_soll_a_100_percent.to_double() * 100.0
}
///|
/// Encode setpoint portion of telegram (STW1+STW2+N_SOLL_A = 6 bytes).
pub fn encode_telegram_setpoint(tg : TelegramData) -> Bytes {
let out : Array[Byte] = []
push_u16_be(out, tg.stw1)
push_u16_be(out, tg.stw2)
push_u16_be(out, tg.n_soll_a)
Bytes::from_array(out)
}
///|
/// Parse actual-value portion of telegram (ZSW1+ZSW2+NIST_A = 6 bytes).
pub fn parse_telegram_actual(
data : Bytes,
offset : Int,
) -> TelegramData raise @frame.FrameError {
guard data.length() >= offset + 6 else {
raise @frame.FrameError::InvalidMacLength(data.length())
}
TelegramData::{
stw1: 0,
stw2: 0,
n_soll_a: 0,
n_soll_b: 0,
g1_stw: 0,
g2_stw: 0,
xerr: 0,
kpc: 0,
zsw1: read_u16_be(data, offset),
zsw2: read_u16_be(data, offset + 2),
nist_a: read_u16_be(data, offset + 4),
nist_b: 0,
g1_zsw: 0,
g1_xist1: 0,
g1_xist2: 0,
g2_zsw: 0,
g2_xist1: 0,
g2_xist2: 0,
}
}
///|
/// Format a telegram summary.
pub fn format_telegram(tg : TelegramData) -> String {
let lines : Array[String] = []
let state = drive_state_from_zsw1(tg.zsw1)
lines.push("state=" + drive_state_label(state))
lines.push("STW1=0x" + hex16(tg.stw1) + " ZSW1=0x" + hex16(tg.zsw1))
lines.push(
"N_SOLL_A=" + tg.n_soll_a.to_string() + " NIST_A=" + tg.nist_a.to_string(),
)
if (tg.zsw1 & zsw1_fault_active) != 0 {
lines.push("FAULT ACTIVE")
}
if (tg.zsw1 & zsw1_warning_active) != 0 {
lines.push("WARNING ACTIVE")
}
if (tg.zsw1 & zsw1_target_reached) != 0 {
lines.push("TARGET REACHED")
}
lines.join("\n")
}
///|
fn hex16(v : Int) -> String {
let hex = "0123456789ABCDEF"
let c : Array[Char] = []
c.push(hex[(v >> 12) & 0xF].to_int().unsafe_to_char())
c.push(hex[(v >> 8) & 0xF].to_int().unsafe_to_char())
c.push(hex[(v >> 4) & 0xF].to_int().unsafe_to_char())
c.push(hex[v & 0xF].to_int().unsafe_to_char())
String::from_array(c)
}
///|
/// Additional standard PROFIdrive PNUs.
pub let pnu_telegram_selection : Int = 922
///|
pub let pnu_station_name_2 : Int = 924
///|
pub let pnu_sign_of_life_tolerance : Int = 925
///|
pub let pnu_operating_mode : Int = 930
///|
pub let pnu_fault_message_counter : Int = 944
///|
pub let pnu_fault_code : Int = 945
///|
pub let pnu_fault_code_list : Int = 946
///|
pub let pnu_fault_number : Int = 947
///|
pub let pnu_fault_time : Int = 948
///|
pub let pnu_fault_value : Int = 949
///|
pub let pnu_fault_buffer_scaling : Int = 950
///|
pub let pnu_fault_number_text : Int = 951
///|
pub let pnu_fault_situation_counter : Int = 952
///|
pub let pnu_do_identification : Int = 975
///|
pub let pnu_do_id_list : Int = 978
///|
pub let pnu_sensor_format : Int = 979
///|
/// Extended PNU label lookup.
pub fn pnu_label_ext(pnu : Int) -> String {
match pnu {
922 => "TelegramSelection"
925 => "SignOfLifeTolerance"
930 => "OperatingMode"
944 => "FaultMsgCounter"
945 => "FaultCode"
946 => "FaultCodeList"
947 => "FaultNumber"
948 => "FaultTime"
949 => "FaultValue"
950 => "FaultBufferScaling"
951 => "FaultNumberText"
952 => "FaultSituationCounter"
975 => "DOIdentification"
978 => "DOIdList"
979 => "SensorFormat"
_ => pnu_label(pnu)
}
}
///|
/// Drive Object properties (DO Identification, PNU 975).
pub(all) struct DriveObjectProperties {
telegram_no : Int
hw_version : String
sw_version : String
speed_norm_value : Int
max_speed_rpm : Int
rated_current_ma : Int
rated_torque_nm : Int
} derive(Eq, Debug)
///|
pub fn format_drive_object_properties(props : DriveObjectProperties) -> String {
let lines : Array[String] = []
lines.push("=== Drive Object Properties ===")
lines.push("TelegramNo=" + props.telegram_no.to_string())
lines.push("HW=" + props.hw_version + " SW=" + props.sw_version)
lines.push("SpeedNormValue=" + props.speed_norm_value.to_string())
lines.push("MaxSpeed=" + props.max_speed_rpm.to_string() + " rpm")
lines.push("RatedCurrent=" + props.rated_current_ma.to_string() + " mA")
lines.push("RatedTorque=" + props.rated_torque_nm.to_string() + " Nm")
lines.join("\n")
}
///|
/// PNC address types (PN-Controller geographic addressing).
pub(all) enum PncAddressType {
/// PNC_GEO_ADDR_SUBMOD — Geographic address of sub-module.
GeoAddrSubmod
/// PNC_SUBMOD_LIST — Sub-module list for a device.
SubmodList
/// PNC_CTRL_INFO — Controller-specific information.
CtrlInfo
} derive(Eq, Debug)
///|
/// PNC geographic submodule address.
pub(all) struct PncGeoAddr {
station_number : Int
slot_number : Int
subslot_number : Int
} derive(Eq, Debug)
///|
pub fn format_pnc_geo_addr(addr : PncGeoAddr) -> String {
"station=" +
addr.station_number.to_string() +
" slot=" +
addr.slot_number.to_string() +
" subslot=" +
addr.subslot_number.to_string()
}
///|
/// Speed reference calculation: convert RPM to normalized setpoint.
pub fn rpm_to_n_soll_a(rpm : Double, max_rpm : Double) -> Int {
let ratio = rpm / max_rpm
let raw = (ratio * n_soll_a_100_percent.to_double()).to_int()
if raw > 0x7FFF {
0x7FFF
} else if raw < -0x7FFF {
-0x7FFF
} else {
raw
}
}
///|
/// Reverse: normalized setpoint → RPM.
pub fn n_soll_a_to_rpm(n_soll : Int, max_rpm : Double) -> Double {
n_soll.to_double() / n_soll_a_100_percent.to_double() * max_rpm
}
///|
/// Position setpoint: convert encoder counts to normalized value.
pub fn counts_to_position_norm(counts : Int, resolution : Int) -> Double {
if resolution == 0 {
0.0
} else {
counts.to_double() / resolution.to_double()
}
}
// ─── PNC Sub-module List Types ───
///|
/// Data properties for a PNC submodule.
pub(all) struct PncSubmodDataProp {
len_in : Int
len_out : Int
} derive(Eq, Debug)
///|
/// Communication properties for a PNC submodule.
pub(all) struct PncSubmodComProp {
cycle_time : Int
cacf : Int
red_factor : Int
phase : Int
ti : Int
to : Int
} derive(Eq, Debug)
///|
/// PROFINET properties for a PNC submodule.
pub(all) struct PncSubmodPnProp {
mod_id : Int
submod_id : Int
} derive(Eq, Debug)
///|
/// PNC_SUBMOD_LIST — Full sub-module entry in the PNC device list.
pub(all) struct PncSubmodList {
sm_ref : Int
data_prop : PncSubmodDataProp
com_prop : PncSubmodComProp
pn_prop : PncSubmodPnProp
} derive(Eq, Debug)
///|
pub fn format_pnc_submod_list(entry : PncSubmodList) -> String {
let lines : Array[String] = []
lines.push("=== PNC SubmodList ===")
lines.push("SmRef=" + entry.sm_ref.to_string())
lines.push(
"Data: in=" +
entry.data_prop.len_in.to_string() +
" out=" +
entry.data_prop.len_out.to_string(),
)
lines.push(
"Com: cycle=" +
entry.com_prop.cycle_time.to_string() +
" cacf=" +
entry.com_prop.cacf.to_string() +
" red=" +
entry.com_prop.red_factor.to_string(),
)
lines.push(
"PN: modId=0x" +
to_hex_pad(entry.pn_prop.mod_id, 8) +
" submodId=0x" +
to_hex_pad(entry.pn_prop.submod_id, 8),
)
lines.join("\n")
}
///|
fn to_hex_pad(value : Int, width : Int) -> String {
let hex = to_hex(value)
let pad = width - hex.length()
if pad > 0 {
"0".repeat(pad) + hex
} else {
hex
}
}
///|
fn to_hex(value : Int) -> String {
if value == 0 {
return "0"
}
let digits = "0123456789ABCDEF"
let buf : Array[String] = []
let mut v = if value < 0 { -value } else { value }
while v > 0 {
buf.push(digits[v % 16].to_string())
v = v / 16
}
buf.rev().join("")
}
// ─── PNC Controller Info ───
///|
/// PNC_CTRL_INFO — Controller-specific information.
pub(all) struct PncCtrlInfo {
ctrl_name : String
ctrl_ip : String
subnet_mask : String
gateway_ip : String
} derive(Eq, Debug)
///|
pub fn format_pnc_ctrl_info(info : PncCtrlInfo) -> String {
let lines : Array[String] = []
lines.push("=== PNC Controller Info ===")
lines.push("Name: " + info.ctrl_name)
lines.push("IP: " + info.ctrl_ip)
lines.push("Mask: " + info.subnet_mask)
lines.push("GW: " + info.gateway_ip)
lines.join("\n")
}
// ─── PNC Alarm Types ───
///|
/// PNC_PN_ALARM_TYPE — Alarm type enumeration (corresponds to alarm module).
pub(all) enum PncAlarmType {
Diagnostic
Process
Pull
Plug
Status
Update
Redundancy
Controlled
Released
PlugWrong
Return
Diagnosis
MaintenanceRequired
MaintenanceDemanded
PortDataChanged
SyncDataChanged
IsochDataChanged
NetworkComponentProblem
MultiplexerProblem
UploadRetrievalNotification
DevFailure
DevReturn
} derive(Eq, Debug)
///|
pub fn pnc_alarm_type_value(alarm : PncAlarmType) -> Int {
match alarm {
Diagnostic => 0x0001
Process => 0x0002
Pull => 0x0003
Plug => 0x0004
Status => 0x0005
Update => 0x0006
Redundancy => 0x0007
Controlled => 0x0008
Released => 0x0009
PlugWrong => 0x000A
Return => 0x000B
Diagnosis => 0x000C
MaintenanceRequired => 0x000D
MaintenanceDemanded => 0x000E
PortDataChanged => 0x000F
SyncDataChanged => 0x0010
IsochDataChanged => 0x0011
NetworkComponentProblem => 0x0012
MultiplexerProblem => 0x0013
UploadRetrievalNotification => 0x0014
DevFailure => 0x10000
DevReturn => 0x10001
}
}
// ─── PDC API — State Machine Control Functions ───
///|
/// Build STW1 for PDC_Precharge: S2 → S3 transition.
pub fn pdc_precharge(stw1 : Int) -> Int {
stw1 | stw1_on_off1 | stw1_no_coast_stop | stw1_no_quick_stop
}
///|
/// Build STW1 for PDC_PulseEnable: S3 → S4 transition.
pub fn pdc_pulse_enable(stw1 : Int) -> Int {
stw1 | stw1_on_off1 | stw1_enable_operation | stw1_enable_ramp
}
///|
/// Build STW1 for PDC_PulseInhibit: S4 → S3 transition.
pub fn pdc_pulse_inhibit(stw1 : Int) -> Int {
stw1 & lnot(stw1_enable_operation)
}
///|
/// Build STW1 for coast stop (AUS2): any → AUS2.
pub fn pdc_coast_stop(stw1 : Int) -> Int {
stw1 & lnot(stw1_no_coast_stop)
}
///|
/// Build STW1 for quick stop (AUS3): any → AUS3.
pub fn pdc_quick_stop(stw1 : Int) -> Int {
stw1 & lnot(stw1_no_quick_stop)
}
///|
/// Build STW1 for fault acknowledge.
pub fn pdc_fault_acknowledge(stw1 : Int) -> Int {
stw1 | stw1_fault_ack
}
///|
/// Set speed setpoint in a TelegramData.
pub fn pdc_set_speed(
td : TelegramData,
rpm : Double,
max_rpm : Double,
) -> TelegramData {
let n_soll = rpm_to_n_soll_a(rpm, max_rpm)
{ ..td, n_soll_a: n_soll }
}
///|
/// Get actual speed from TelegramData.
pub fn pdc_get_speed(td : TelegramData, max_rpm : Double) -> Double {
n_soll_a_to_rpm(td.nist_a, max_rpm)
}
///|
fn lnot(x : Int) -> Int {
x.lxor(0xFFFF)
}