///|
/// Machine-readable description of an IEC 104 application type.
pub struct TypeDescriptor {
type_id : ApplicationType
name : String
direction : PointDirection
time_tag : TimeTagKind
value_width : Int
supported : Bool
command : Bool
} derive(Eq, Debug)
///|
fn descriptor(
type_id : ApplicationType,
name : String,
direction : PointDirection,
time_tag : TimeTagKind,
value_width : Int,
supported : Bool,
command : Bool,
) -> TypeDescriptor {
{ type_id, name, direction, time_tag, value_width, supported, command }
}
///|
pub fn TypeDescriptor::type_id(self : TypeDescriptor) -> ApplicationType {
self.type_id
}
///|
pub fn TypeDescriptor::name(self : TypeDescriptor) -> String {
self.name
}
///|
pub fn TypeDescriptor::direction(self : TypeDescriptor) -> PointDirection {
self.direction
}
///|
pub fn TypeDescriptor::time_tag(self : TypeDescriptor) -> TimeTagKind {
self.time_tag
}
///|
pub fn TypeDescriptor::value_width(self : TypeDescriptor) -> Int {
self.value_width
}
///|
pub fn TypeDescriptor::supported(self : TypeDescriptor) -> Bool {
self.supported
}
///|
pub fn TypeDescriptor::is_command(self : TypeDescriptor) -> Bool {
self.command
}
///|
/// Return the standard catalog used to negotiate object capabilities.
pub fn application_type_catalog() -> Array[TypeDescriptor] {
[
descriptor(MSpNa, "M_SP_NA_1", MonitorDirection, NoTimeTag, 1, true, false),
descriptor(MDpNa, "M_DP_NA_1", MonitorDirection, NoTimeTag, 1, true, false),
descriptor(MStNa, "M_ST_NA_1", MonitorDirection, NoTimeTag, 2, true, false),
descriptor(MBoNa, "M_BO_NA_1", MonitorDirection, NoTimeTag, 5, true, false),
descriptor(MMeNa, "M_ME_NA_1", MonitorDirection, NoTimeTag, 3, true, false),
descriptor(MMeNb, "M_ME_NB_1", MonitorDirection, NoTimeTag, 3, true, false),
descriptor(MMeNc, "M_ME_NC_1", MonitorDirection, NoTimeTag, 5, true, false),
descriptor(MItNa, "M_IT_NA_1", MonitorDirection, NoTimeTag, 5, true, false),
descriptor(
MSpTa,
"M_SP_TA_1",
MonitorDirection,
Cp24TimeTag,
1,
true,
false,
),
descriptor(
MDpTa,
"M_DP_TA_1",
MonitorDirection,
Cp24TimeTag,
1,
true,
false,
),
descriptor(
MStTa,
"M_ST_TA_1",
MonitorDirection,
Cp24TimeTag,
2,
true,
false,
),
descriptor(
MBoTa,
"M_BO_TA_1",
MonitorDirection,
Cp24TimeTag,
5,
true,
false,
),
descriptor(
MMeTa,
"M_ME_TA_1",
MonitorDirection,
Cp24TimeTag,
3,
true,
false,
),
descriptor(
MMeTb,
"M_ME_TB_1",
MonitorDirection,
Cp24TimeTag,
3,
true,
false,
),
descriptor(
MMeTc,
"M_ME_TC_1",
MonitorDirection,
Cp24TimeTag,
5,
true,
false,
),
descriptor(
MItTa,
"M_IT_TA_1",
MonitorDirection,
Cp24TimeTag,
5,
true,
false,
),
descriptor(
MEpTa,
"M_EP_TA_1",
MonitorDirection,
Cp24TimeTag,
4,
false,
false,
),
descriptor(
MEpTb,
"M_EP_TB_1",
MonitorDirection,
Cp56TimeTag,
4,
false,
false,
),
descriptor(
MEpTc,
"M_EP_TC_1",
MonitorDirection,
Cp56TimeTag,
4,
false,
false,
),
descriptor(MPsNa, "M_PS_NA_1", MonitorDirection, NoTimeTag, 5, true, false),
descriptor(MMeNd, "M_ME_ND_1", MonitorDirection, NoTimeTag, 2, true, false),
descriptor(
MSpTb,
"M_SP_TB_1",
MonitorDirection,
Cp56TimeTag,
1,
true,
false,
),
descriptor(
MDpTb,
"M_DP_TB_1",
MonitorDirection,
Cp56TimeTag,
1,
true,
false,
),
descriptor(
MStTb,
"M_ST_TB_1",
MonitorDirection,
Cp56TimeTag,
2,
true,
false,
),
descriptor(
MBoTb,
"M_BO_TB_1",
MonitorDirection,
Cp56TimeTag,
5,
true,
false,
),
descriptor(
MMeTd,
"M_ME_TD_1",
MonitorDirection,
Cp56TimeTag,
3,
true,
false,
),
descriptor(
MMeTe,
"M_ME_TE_1",
MonitorDirection,
Cp56TimeTag,
3,
true,
false,
),
descriptor(
MMeTf,
"M_ME_TF_1",
MonitorDirection,
Cp56TimeTag,
5,
true,
false,
),
descriptor(
MItTb,
"M_IT_TB_1",
MonitorDirection,
Cp56TimeTag,
5,
true,
false,
),
descriptor(
MEpTd,
"M_EP_TD_1",
MonitorDirection,
Cp56TimeTag,
4,
false,
false,
),
descriptor(
MEpTe,
"M_EP_TE_1",
MonitorDirection,
Cp56TimeTag,
4,
false,
false,
),
descriptor(
MEpTf,
"M_EP_TF_1",
MonitorDirection,
Cp56TimeTag,
4,
false,
false,
),
descriptor(MEiNa, "M_EI_NA_1", MonitorDirection, NoTimeTag, 1, true, false),
descriptor(CScNa, "C_SC_NA_1", ControlDirection, NoTimeTag, 1, true, true),
descriptor(CDcNa, "C_DC_NA_1", ControlDirection, NoTimeTag, 1, true, true),
descriptor(CRcNa, "C_RC_NA_1", ControlDirection, NoTimeTag, 1, true, true),
descriptor(CSeNa, "C_SE_NA_1", ControlDirection, NoTimeTag, 3, true, true),
descriptor(CSeNb, "C_SE_NB_1", ControlDirection, NoTimeTag, 3, true, true),
descriptor(CSeNc, "C_SE_NC_1", ControlDirection, NoTimeTag, 5, true, true),
descriptor(CBoNa, "C_BO_NA_1", ControlDirection, NoTimeTag, 4, true, true),
descriptor(CIcNa, "C_IC_NA_1", ControlDirection, NoTimeTag, 1, true, true),
descriptor(CCiNa, "C_CI_NA_1", ControlDirection, NoTimeTag, 1, true, true),
descriptor(CRdNa, "C_RD_NA_1", ControlDirection, NoTimeTag, 0, true, true),
descriptor(CCsNa, "C_CS_NA_1", ControlDirection, NoTimeTag, 7, true, true),
descriptor(CTsNa, "C_TS_NA_1", ControlDirection, NoTimeTag, 2, true, true),
descriptor(CRpNa, "C_RP_NA_1", ControlDirection, NoTimeTag, 1, true, true),
]
}
///|
pub fn type_descriptor(type_id : ApplicationType) -> TypeDescriptor {
for descriptor in application_type_catalog() {
if descriptor.type_id() == type_id {
return descriptor
}
}
descriptor(
type_id,
"UNKNOWN",
point_direction(type_id),
NoTimeTag,
0,
false,
false,
)
}
///|
pub fn supported_type_catalog() -> Array[TypeDescriptor] {
let result : Array[TypeDescriptor] = []
for descriptor in application_type_catalog() {
if descriptor.supported() {
result.push(descriptor)
}
}
result
}
///|
pub fn monitoring_type_catalog() -> Array[TypeDescriptor] {
let result : Array[TypeDescriptor] = []
for descriptor in supported_type_catalog() {
if descriptor.direction() == MonitorDirection {
result.push(descriptor)
}
}
result
}
///|
pub fn control_type_catalog() -> Array[TypeDescriptor] {
let result : Array[TypeDescriptor] = []
for descriptor in supported_type_catalog() {
if descriptor.direction() == ControlDirection {
result.push(descriptor)
}
}
result
}
///|
pub fn type_name(type_id : ApplicationType) -> String {
type_descriptor(type_id).name()
}
///|
pub fn type_is_supported(type_id : ApplicationType) -> Bool {
type_descriptor(type_id).supported()
}
///|
pub fn type_wire_width(type_id : ApplicationType) -> Int {
type_descriptor(type_id).value_width() +
3 +
time_tag_kind_for_type(type_id).width()
}
///|
/// An ASDU consistency report that can be shown in CLI diagnostics.
pub struct ConformanceReport {
mut valid : Bool
type_id : ApplicationType
mut object_count : Int
expected_width : Int
mut actual_width : Int
issues : Array[String]
} derive(Eq, Debug)
///|
pub fn ConformanceReport::new(type_id : ApplicationType) -> ConformanceReport {
{
valid: true,
type_id,
object_count: 0,
expected_width: type_wire_width(type_id),
actual_width: 0,
issues: [],
}
}
///|
pub fn ConformanceReport::valid(self : ConformanceReport) -> Bool {
self.valid
}
///|
pub fn ConformanceReport::type_id(self : ConformanceReport) -> ApplicationType {
self.type_id
}
///|
pub fn ConformanceReport::object_count(self : ConformanceReport) -> Int {
self.object_count
}
///|
pub fn ConformanceReport::expected_width(self : ConformanceReport) -> Int {
self.expected_width
}
///|
pub fn ConformanceReport::actual_width(self : ConformanceReport) -> Int {
self.actual_width
}
///|
pub fn ConformanceReport::issues(self : ConformanceReport) -> Array[String] {
self.issues.copy()
}
///|
pub fn ConformanceReport::add_issue(
self : ConformanceReport,
message : String,
) -> Unit {
self.valid = false
self.issues.push(message)
}
///|
pub fn validate_envelope(envelope : AsduEnvelope) -> ConformanceReport {
let report = ConformanceReport::new(envelope.type_id())
report.object_count = envelope.count()
if !type_is_supported(envelope.type_id()) {
report.add_issue("application type is outside the supported catalog")
}
for object in envelope.objects() {
if object.type_id() != envelope.type_id() {
report.add_issue("object type does not match ASDU type")
}
if (object.time_tag() is Some(_)) != envelope.type_id().has_time_tag() {
report.add_issue("object time-tag presence does not match ASDU type")
}
report.actual_width += object.address().number() * 0 +
application_value_width(object.value()) +
3 +
object.time_tag().map_or(0, tag => tag.width())
}
report
}
///|
/// Validate a VSQ byte and return its count/sequence pair.
pub fn parse_vsq(value : Int) -> Result[(Int, Bool), String] {
if value < 0 || value > 255 {
Err("VSQ must fit one byte")
} else if (value & 0x7f) == 0 {
Err("VSQ object count must not be zero")
} else {
Ok((value & 0x7f, (value & 0x80) != 0))
}
}
///|
pub fn validate_common_address(
value : Int,
) -> Result[CommonAddress, Diagnostic] {
match CommonAddress::new(value) {
Ok(address) => Ok(address)
Err(message) => Err(Diagnostic::new(InvalidAddress, message))
}
}
///|
pub fn validate_information_address(
value : Int,
) -> Result[InformationAddress, Diagnostic] {
match InformationAddress::new(value) {
Ok(address) => Ok(address)
Err(message) => Err(Diagnostic::new(InvalidAddress, message))
}
}
///|
pub fn conformance_examples() -> Array[TypeDescriptor] {
[
type_descriptor(MSpNa),
type_descriptor(CScNa),
type_descriptor(UnknownType(222)),
]
}