///|
/// The three-byte information object address used by IEC 104.
pub struct InformationAddress {
value : Int
} derive(Eq, Debug)
///|
/// Create an information object address in the inclusive 0..0xffffff range.
pub fn InformationAddress::new(
value : Int,
) -> Result[InformationAddress, String] {
if value < 0 || value > 0xffffff {
Err("information object address must fit 24 bits")
} else {
Ok({ value, })
}
}
///|
/// Construct an information object address from its three octets.
pub fn InformationAddress::from_octets(
low : Int,
middle : Int,
high : Int,
) -> Result[InformationAddress, String] {
if low < 0 ||
low > 255 ||
middle < 0 ||
middle > 255 ||
high < 0 ||
high > 255 {
Err("information object address octets must fit one byte")
} else {
Ok({ value: low | (middle << 8) | (high << 16) })
}
}
///|
/// Return the numeric address.
pub fn InformationAddress::number(self : InformationAddress) -> Int {
self.value
}
///|
/// Return the least significant address octet.
pub fn InformationAddress::low(self : InformationAddress) -> Int {
self.value & 0xff
}
///|
/// Return the middle address octet.
pub fn InformationAddress::middle(self : InformationAddress) -> Int {
(self.value >> 8) & 0xff
}
///|
/// Return the most significant address octet.
pub fn InformationAddress::high(self : InformationAddress) -> Int {
(self.value >> 16) & 0xff
}
///|
/// Return whether the address is the protocol's broadcast address.
pub fn InformationAddress::is_broadcast(self : InformationAddress) -> Bool {
self.value == 0xffffff
}
///|
/// Common address of ASDU values. Zero is reserved for an unassigned station.
pub struct CommonAddress {
value : Int
} derive(Eq, Debug)
///|
/// Create a two-byte common address.
pub fn CommonAddress::new(value : Int) -> Result[CommonAddress, String] {
if value < 0 || value > 0xffff {
Err("common address must fit 16 bits")
} else {
Ok({ value, })
}
}
///|
/// Return the numeric common address.
pub fn CommonAddress::number(self : CommonAddress) -> Int {
self.value
}
///|
/// Return whether this common address is the global station address.
pub fn CommonAddress::is_global(self : CommonAddress) -> Bool {
self.value == 0xffff
}
///|
/// Cause of transmission category. The numeric value is kept separate from
/// the qualifier so applications can preserve vendor extensions.
pub enum CauseCategory {
Periodic
Background
Spontaneous
Initialised
Request
Activation
ActivationConfirmation
ActivationTermination
ReturnInformationRemote
ReturnInformationLocal
UnknownCause
} derive(Eq, Debug)
///|
/// Return the standard six-bit cause value.
pub fn CauseCategory::number(self : CauseCategory) -> Int {
match self {
Periodic => 1
Background => 2
Spontaneous => 3
Initialised => 4
Request => 5
Activation => 6
ActivationConfirmation => 7
ActivationTermination => 10
ReturnInformationRemote => 11
ReturnInformationLocal => 12
UnknownCause => 0
}
}
///|
/// Decode a standard cause category without rejecting extension values.
pub fn cause_category(value : Int) -> CauseCategory {
match value {
1 => Periodic
2 => Background
3 => Spontaneous
4 => Initialised
5 => Request
6 => Activation
7 => ActivationConfirmation
10 => ActivationTermination
11 => ReturnInformationRemote
12 => ReturnInformationLocal
_ => UnknownCause
}
}
///|
/// Full cause of transmission, including the originator and qualifier bits.
pub struct CauseOfTransmission {
category : CauseCategory
number : Int
positive : Bool
test_flag : Bool
originator : Int
qualifier : Int
} derive(Eq, Debug)
///|
/// Construct a validated cause of transmission.
pub fn CauseOfTransmission::new(
category : CauseCategory,
positive? : Bool = true,
test_flag? : Bool = false,
originator? : Int = 0,
qualifier? : Int = 0,
) -> Result[CauseOfTransmission, String] {
if originator < 0 || originator > 255 {
Err("originator address must fit 8 bits")
} else if qualifier < 0 || qualifier > 0x3f {
Err("cause qualifier must fit 6 bits")
} else {
Ok({
category,
number: category.number(),
positive,
test_flag,
originator,
qualifier,
})
}
}
///|
/// Construct a cause from a raw standard number while preserving extension data.
pub fn CauseOfTransmission::from_number(
number : Int,
positive? : Bool = true,
test_flag? : Bool = false,
originator? : Int = 0,
qualifier? : Int = 0,
) -> Result[CauseOfTransmission, String] {
match
CauseOfTransmission::new(
cause_category(number),
positive~,
test_flag~,
originator~,
qualifier~,
) {
Ok(value) =>
Ok({
category: value.category,
number,
positive: value.positive,
test_flag: value.test_flag,
originator: value.originator,
qualifier: value.qualifier,
})
Err(error) => Err(error)
}
}
///|
/// Return the raw cause number.
pub fn CauseOfTransmission::raw(self : CauseOfTransmission) -> Int {
self.number
}
///|
/// Return the four low flag bits in the IEC COT representation.
pub fn CauseOfTransmission::flags(self : CauseOfTransmission) -> Int {
(if self.positive { 0 } else { 0x40 }) |
(if self.test_flag { 0x80 } else { 0 }) |
(self.number & 0x3f)
}
///|
/// Return the qualifier value.
pub fn CauseOfTransmission::qualifier(self : CauseOfTransmission) -> Int {
self.qualifier
}
///|
/// Return the originator address.
pub fn CauseOfTransmission::originator(self : CauseOfTransmission) -> Int {
self.originator
}
///|
/// Standard IEC 104 application type identifiers.
pub enum ApplicationType {
MSpNa
MSpTa
MDpNa
MDpTa
MStNa
MStTa
MBoNa
MBoTa
MMeNa
MMeTa
MMeNb
MMeTb
MMeNc
MMeTc
MItNa
MItTa
MEpTa
MEpTb
MEpTc
MPsNa
MMeNd
MSpTb
MDpTb
MStTb
MBoTb
MMeTd
MMeTe
MMeTf
MItTb
MEpTd
MEpTe
MEpTf
MEiNa
CScNa
CDcNa
CRcNa
CSeNa
CSeNb
CSeNc
CBoNa
CIcNa
CCiNa
CRdNa
CCsNa
CTsNa
CRpNa
UnknownType(Int)
} derive(Eq, Debug)
///|
/// Return an IEC 104 type identifier number.
pub fn ApplicationType::number(self : ApplicationType) -> Int {
match self {
MSpNa => 1
MSpTa => 2
MDpNa => 3
MDpTa => 4
MStNa => 5
MStTa => 6
MBoNa => 7
MBoTa => 8
MMeNa => 9
MMeTa => 10
MMeNb => 11
MMeTb => 12
MMeNc => 13
MMeTc => 14
MItNa => 15
MItTa => 16
MEpTa => 17
MEpTb => 18
MEpTc => 19
MPsNa => 20
MMeNd => 21
MSpTb => 30
MDpTb => 31
MStTb => 32
MBoTb => 33
MMeTd => 34
MMeTe => 35
MMeTf => 36
MItTb => 37
MEpTd => 38
MEpTe => 39
MEpTf => 40
MEiNa => 70
CScNa => 45
CDcNa => 46
CRcNa => 47
CSeNa => 48
CSeNb => 49
CSeNc => 50
CBoNa => 51
CIcNa => 100
CCiNa => 101
CRdNa => 102
CCsNa => 103
CTsNa => 104
CRpNa => 105
UnknownType(value) => value
}
}
///|
/// Decode all currently assigned standard IEC 104 type identifiers.
pub fn application_type(value : Int) -> ApplicationType {
match value {
1 => MSpNa
2 => MSpTa
3 => MDpNa
4 => MDpTa
5 => MStNa
6 => MStTa
7 => MBoNa
8 => MBoTa
9 => MMeNa
10 => MMeTa
11 => MMeNb
12 => MMeTb
13 => MMeNc
14 => MMeTc
15 => MItNa
16 => MItTa
17 => MEpTa
18 => MEpTb
19 => MEpTc
20 => MPsNa
21 => MMeNd
30 => MSpTb
31 => MDpTb
32 => MStTb
33 => MBoTb
34 => MMeTd
35 => MMeTe
36 => MMeTf
37 => MItTb
38 => MEpTd
39 => MEpTe
40 => MEpTf
45 => CScNa
46 => CDcNa
47 => CRcNa
48 => CSeNa
49 => CSeNb
50 => CSeNc
51 => CBoNa
70 => MEiNa
100 => CIcNa
101 => CCiNa
102 => CRdNa
103 => CCsNa
104 => CTsNa
105 => CRpNa
_ => UnknownType(value)
}
}
///|
/// Whether an application type belongs to monitor direction data.
pub fn ApplicationType::is_monitoring(self : ApplicationType) -> Bool {
self.number() < 45 || self is MEiNa
}
///|
/// Whether an application type belongs to control direction data.
pub fn ApplicationType::is_control(self : ApplicationType) -> Bool {
(self.number() >= 45 && self.number() < 70) || self.number() >= 100
}
///|
/// Whether a type carries a CP24 or CP56 time tag.
pub fn ApplicationType::has_time_tag(self : ApplicationType) -> Bool {
match self {
MSpTa
| MDpTa
| MStTa
| MBoTa
| MMeTa
| MMeTb
| MMeTc
| MItTa
| MSpTb
| MDpTb
| MStTb
| MBoTb
| MMeTd
| MMeTe
| MMeTf
| MItTb
| MEpTa
| MEpTb
| MEpTc
| MEpTe
| MEpTf => true
_ => false
}
}
///|
/// Preferred information-object payload width, excluding IOA and time tag.
pub fn ApplicationType::value_width(self : ApplicationType) -> Int? {
match self {
MSpNa | MSpTa | MSpTb => Some(1)
MDpNa | MDpTa | MDpTb => Some(1)
MStNa | MStTa | MStTb => Some(2)
MBoNa | MBoTa | MBoTb => Some(5)
MMeNa | MMeTa | MMeNb | MMeTb | MMeNd | MMeTd | MMeTe => Some(3)
MMeNc | MMeTc | MMeTf => Some(5)
MItNa | MItTa | MItTb => Some(5)
_ => None
}
}
///|
/// IEC 104 link-layer timing and window parameters.
pub struct ConnectionParameters {
k : Int
w : Int
t0_seconds : Int
t1_seconds : Int
t2_seconds : Int
t3_seconds : Int
} derive(Eq, Debug)
///|
/// Default parameters from the commonly deployed IEC 104 profile.
pub fn ConnectionParameters::default() -> ConnectionParameters {
{
k: 12,
w: 8,
t0_seconds: 30,
t1_seconds: 15,
t2_seconds: 10,
t3_seconds: 20,
}
}
///|
/// Validate and construct link-layer parameters.
pub fn ConnectionParameters::new(
k : Int,
w : Int,
t0_seconds : Int,
t1_seconds : Int,
t2_seconds : Int,
t3_seconds : Int,
) -> Result[ConnectionParameters, String] {
if k < 1 || k > 32767 {
Err("k must be between 1 and 32767")
} else if w < 1 || w > 32767 || w > k {
Err("w must be between 1 and k")
} else if t0_seconds < 1 || t1_seconds < 1 || t2_seconds < 1 || t3_seconds < 1 {
Err("IEC timers must be positive")
} else if t2_seconds > t1_seconds || t1_seconds > t0_seconds {
Err("IEC timer ordering requires t2 <= t1 <= t0")
} else {
Ok({ k, w, t0_seconds, t1_seconds, t2_seconds, t3_seconds })
}
}
///|
pub fn ConnectionParameters::k(self : ConnectionParameters) -> Int {
self.k
}
///|
pub fn ConnectionParameters::w(self : ConnectionParameters) -> Int {
self.w
}
///|
pub fn ConnectionParameters::t0(self : ConnectionParameters) -> Int {
self.t0_seconds
}
///|
pub fn ConnectionParameters::t1(self : ConnectionParameters) -> Int {
self.t1_seconds
}
///|
pub fn ConnectionParameters::t2(self : ConnectionParameters) -> Int {
self.t2_seconds
}
///|
pub fn ConnectionParameters::t3(self : ConnectionParameters) -> Int {
self.t3_seconds
}
///|
/// A stable classification for protocol diagnostics.
pub enum DiagnosticKind {
MalformedFrame
InvalidSequence
InvalidAddress
InvalidType
InvalidQualifier
UnsupportedFeature
WindowExhausted
TimerExpired
StateViolation
TransportFailure
} derive(Eq, Debug)
///|
/// Structured diagnostic returned by validation and service layers.
pub struct Diagnostic {
kind : DiagnosticKind
message : String
offset : Int?
} derive(Eq, Debug)
///|
pub fn Diagnostic::new(
kind : DiagnosticKind,
message : String,
offset? : Int,
) -> Diagnostic {
{ kind, message, offset }
}
///|
pub fn Diagnostic::kind(self : Diagnostic) -> DiagnosticKind {
self.kind
}
///|
pub fn Diagnostic::message(self : Diagnostic) -> String {
self.message
}
///|
pub fn Diagnostic::offset(self : Diagnostic) -> Int? {
self.offset
}
///|
/// Produce a compact human-readable diagnostic line for CLI and logs.
pub fn Diagnostic::to_line(self : Diagnostic) -> String {
match self.offset {
Some(offset) => "\{self.message} at byte \{offset}"
None => self.message
}
}
///|
/// Keep all diagnostic categories available to host-side telemetry adapters.
pub fn diagnostic_kind_examples() -> Array[DiagnosticKind] {
[
MalformedFrame,
InvalidSequence,
InvalidAddress,
InvalidType,
InvalidQualifier,
UnsupportedFeature,
WindowExhausted,
TimerExpired,
StateViolation,
TransportFailure,
]
}