use alloc::string::String;
use alloc::vec::Vec;
use zerodds_opcua_gateway::data_value::Variant;
use zerodds_opcua_gateway::node_id::NodeId;
use zerodds_opcua_gateway::types::{BuiltinTypeKind, Guid, LocalizedText, QualifiedName};
use crate::uadp::dataset_message::FieldEncoding;
use crate::uadp::network_message::PublisherId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct DataSetFieldContentMask(u32);
impl DataSetFieldContentMask {
pub const STATUS_CODE: u32 = 0x01;
pub const SOURCE_TIMESTAMP: u32 = 0x02;
pub const SERVER_TIMESTAMP: u32 = 0x04;
pub const SOURCE_PICOSECONDS: u32 = 0x08;
pub const SERVER_PICOSECONDS: u32 = 0x10;
pub const RAW_DATA: u32 = 0x20;
#[must_use]
pub const fn from_bits(bits: u32) -> Self {
Self(bits)
}
#[must_use]
pub const fn bits(self) -> u32 {
self.0
}
#[must_use]
pub const fn contains(self, flag: u32) -> bool {
self.0 & flag != 0
}
#[must_use]
pub const fn variant() -> Self {
Self(0)
}
#[must_use]
pub const fn raw_data() -> Self {
Self(Self::RAW_DATA)
}
#[must_use]
pub const fn field_encoding(self) -> FieldEncoding {
if self.0 & Self::RAW_DATA != 0 {
FieldEncoding::RawData
} else if self.0 == 0 {
FieldEncoding::Variant
} else {
FieldEncoding::DataValue
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct DataSetMessageContentMask(u32);
impl DataSetMessageContentMask {
pub const TIMESTAMP: u32 = 0x01;
pub const PICOSECONDS: u32 = 0x02;
pub const STATUS: u32 = 0x04;
pub const MAJOR_VERSION: u32 = 0x08;
pub const MINOR_VERSION: u32 = 0x10;
pub const SEQUENCE_NUMBER: u32 = 0x20;
#[must_use]
pub const fn from_bits(bits: u32) -> Self {
Self(bits)
}
#[must_use]
pub const fn bits(self) -> u32 {
self.0
}
#[must_use]
pub const fn contains(self, flag: u32) -> bool {
self.0 & flag != 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct NetworkMessageContentMask(u32);
impl NetworkMessageContentMask {
pub const PUBLISHER_ID: u32 = 0x01;
pub const GROUP_HEADER: u32 = 0x02;
pub const WRITER_GROUP_ID: u32 = 0x04;
pub const GROUP_VERSION: u32 = 0x08;
pub const NETWORK_MESSAGE_NUMBER: u32 = 0x10;
pub const SEQUENCE_NUMBER: u32 = 0x20;
pub const PAYLOAD_HEADER: u32 = 0x40;
pub const TIMESTAMP: u32 = 0x80;
pub const PICOSECONDS: u32 = 0x100;
pub const DATASET_CLASS_ID: u32 = 0x200;
pub const PROMOTED_FIELDS: u32 = 0x400;
#[must_use]
pub const fn from_bits(bits: u32) -> Self {
Self(bits)
}
#[must_use]
pub const fn bits(self) -> u32 {
self.0
}
#[must_use]
pub const fn contains(self, flag: u32) -> bool {
self.0 & flag != 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ConfigurationVersion {
pub major_version: u32,
pub minor_version: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct KeyValuePair {
pub key: QualifiedName,
pub value: Variant,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FieldMetaData {
pub name: String,
pub description: LocalizedText,
pub field_flags: u16,
pub builtin_type: BuiltinTypeKind,
pub data_type: NodeId,
pub value_rank: i32,
pub array_dimensions: Vec<u32>,
pub max_string_length: u32,
pub data_set_field_id: Guid,
pub properties: Vec<KeyValuePair>,
}
impl FieldMetaData {
#[must_use]
pub fn scalar(name: impl Into<String>, builtin_type: BuiltinTypeKind) -> Self {
Self {
name: name.into(),
description: LocalizedText {
locale: None,
text: None,
},
field_flags: 0,
builtin_type,
data_type: NodeId::numeric(0, u32::from(builtin_type.value())),
value_rank: -1,
array_dimensions: Vec::new(),
max_string_length: 0,
data_set_field_id: Guid::from_bytes([0; 16]),
properties: Vec::new(),
}
}
#[must_use]
pub const fn is_promoted(&self) -> bool {
self.field_flags & 0x0001 != 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StructureType {
#[default]
Structure,
StructureWithOptionalFields,
Union,
StructureWithSubtypedValues,
UnionWithSubtypedValues,
}
impl StructureType {
#[must_use]
pub const fn to_i32(self) -> i32 {
match self {
Self::Structure => 0,
Self::StructureWithOptionalFields => 1,
Self::Union => 2,
Self::StructureWithSubtypedValues => 3,
Self::UnionWithSubtypedValues => 4,
}
}
#[must_use]
pub const fn from_i32(v: i32) -> Option<Self> {
Some(match v {
0 => Self::Structure,
1 => Self::StructureWithOptionalFields,
2 => Self::Union,
3 => Self::StructureWithSubtypedValues,
4 => Self::UnionWithSubtypedValues,
_ => return None,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct StructureField {
pub name: String,
pub description: LocalizedText,
pub data_type: NodeId,
pub value_rank: i32,
pub array_dimensions: Vec<u32>,
pub max_string_length: u32,
pub is_optional: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct StructureDefinition {
pub default_encoding_id: NodeId,
pub base_data_type: NodeId,
pub structure_type: StructureType,
pub fields: Vec<StructureField>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct StructureDescription {
pub data_type_id: NodeId,
pub name: QualifiedName,
pub structure_definition: StructureDefinition,
}
#[derive(Debug, Clone, PartialEq)]
pub struct EnumField {
pub value: i64,
pub display_name: LocalizedText,
pub description: LocalizedText,
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct EnumDefinition {
pub fields: Vec<EnumField>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct EnumDescription {
pub data_type_id: NodeId,
pub name: QualifiedName,
pub enum_definition: EnumDefinition,
pub builtin_type: u8,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SimpleTypeDescription {
pub data_type_id: NodeId,
pub name: QualifiedName,
pub base_data_type: NodeId,
pub builtin_type: u8,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DataSetMetaData {
pub name: String,
pub description: LocalizedText,
pub namespaces: Vec<String>,
pub structure_data_types: Vec<StructureDescription>,
pub enum_data_types: Vec<EnumDescription>,
pub simple_data_types: Vec<SimpleTypeDescription>,
pub fields: Vec<FieldMetaData>,
pub data_set_class_id: Guid,
pub configuration_version: ConfigurationVersion,
}
impl Default for DataSetMetaData {
fn default() -> Self {
Self {
name: String::new(),
description: LocalizedText {
locale: None,
text: None,
},
namespaces: Vec::new(),
structure_data_types: Vec::new(),
enum_data_types: Vec::new(),
simple_data_types: Vec::new(),
fields: Vec::new(),
data_set_class_id: Guid::from_bytes([0; 16]),
configuration_version: ConfigurationVersion::default(),
}
}
}
impl DataSetMetaData {
#[must_use]
pub fn new(name: impl Into<String>, fields: Vec<FieldMetaData>) -> Self {
Self {
name: name.into(),
fields,
..Self::default()
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct DataSetWriterConfig {
pub name: String,
pub data_set_writer_id: u16,
pub data_set_name: String,
pub field_content_mask: DataSetFieldContentMask,
pub key_frame_count: u32,
pub message_content_mask: DataSetMessageContentMask,
}
impl DataSetWriterConfig {
#[must_use]
pub fn new(
name: impl Into<String>,
data_set_writer_id: u16,
data_set_name: impl Into<String>,
) -> Self {
Self {
name: name.into(),
data_set_writer_id,
data_set_name: data_set_name.into(),
field_content_mask: DataSetFieldContentMask::variant(),
key_frame_count: 1,
message_content_mask: DataSetMessageContentMask::default(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct WriterGroupConfig {
pub name: String,
pub writer_group_id: u16,
pub publishing_interval_ms: f64,
pub group_version: u32,
pub network_message_content_mask: NetworkMessageContentMask,
}
impl WriterGroupConfig {
#[must_use]
pub fn new(name: impl Into<String>, writer_group_id: u16) -> Self {
Self {
name: name.into(),
writer_group_id,
publishing_interval_ms: 1000.0,
group_version: 0,
network_message_content_mask: NetworkMessageContentMask::from_bits(
NetworkMessageContentMask::PAYLOAD_HEADER
| NetworkMessageContentMask::GROUP_HEADER
| NetworkMessageContentMask::WRITER_GROUP_ID
| NetworkMessageContentMask::SEQUENCE_NUMBER,
),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct DataSetReaderConfig {
pub name: String,
pub publisher_id: Option<PublisherId>,
pub writer_group_id: u16,
pub data_set_writer_id: u16,
pub field_content_mask: DataSetFieldContentMask,
pub message_content_mask: DataSetMessageContentMask,
pub meta_data: DataSetMetaData,
}
impl DataSetReaderConfig {
#[must_use]
pub fn new(name: impl Into<String>, meta_data: DataSetMetaData) -> Self {
Self {
name: name.into(),
publisher_id: None,
writer_group_id: 0,
data_set_writer_id: 0,
field_content_mask: DataSetFieldContentMask::variant(),
message_content_mask: DataSetMessageContentMask::default(),
meta_data,
}
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ReaderGroupConfig {
pub name: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PubSubConnectionConfig {
pub name: String,
pub publisher_id: PublisherId,
pub transport_profile_uri: String,
pub address_url: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn content_mask_selects_field_encoding() {
assert_eq!(
DataSetFieldContentMask::variant().field_encoding(),
FieldEncoding::Variant
);
assert_eq!(
DataSetFieldContentMask::raw_data().field_encoding(),
FieldEncoding::RawData
);
assert_eq!(
DataSetFieldContentMask::from_bits(DataSetFieldContentMask::SOURCE_TIMESTAMP)
.field_encoding(),
FieldEncoding::DataValue
);
assert_eq!(
DataSetFieldContentMask::from_bits(
DataSetFieldContentMask::RAW_DATA | DataSetFieldContentMask::STATUS_CODE
)
.field_encoding(),
FieldEncoding::RawData
);
}
#[test]
fn field_metadata_scalar_defaults() {
let f = FieldMetaData::scalar("temperature", BuiltinTypeKind::Double);
assert_eq!(f.value_rank, -1);
assert_eq!(f.builtin_type, BuiltinTypeKind::Double);
assert!(!f.is_promoted());
}
#[test]
fn writer_group_default_mask_has_payload_and_group_header() {
let g = WriterGroupConfig::new("g1", 7);
let m = g.network_message_content_mask;
assert!(m.contains(NetworkMessageContentMask::PAYLOAD_HEADER));
assert!(m.contains(NetworkMessageContentMask::GROUP_HEADER));
assert!(m.contains(NetworkMessageContentMask::WRITER_GROUP_ID));
assert!(m.contains(NetworkMessageContentMask::SEQUENCE_NUMBER));
}
}