///|
/// Typed interpretation for a register-backed device schema.
pub(all) enum RegisterFieldKind {
Unsigned16
Signed16
Unsigned32
Signed32
Float32
Unsigned64
BitField(Int)
} derive(Debug, Eq)
///|
pub fn RegisterFieldKind::width(self : RegisterFieldKind) -> Int {
match self {
Unsigned16 | Signed16 => 1
Unsigned32 | Signed32 | Float32 => 2
Unsigned64 => 4
BitField(bits) => (bits + 15) / 16
}
}
///|
pub fn RegisterFieldKind::name(self : RegisterFieldKind) -> String {
match self {
Unsigned16 => "u16"
Signed16 => "i16"
Unsigned32 => "u32"
Signed32 => "i32"
Float32 => "f32"
Unsigned64 => "u64"
BitField(_) => "bit-field"
}
}
///|
/// A value returned by schema-aware reads.
pub(all) enum RegisterValue {
U16(UInt16)
I16(Int)
U32(UInt)
I32(Int)
F32(Float)
U64(UInt64)
Bits(Array[Bool])
}
///|
/// A named register field with an explicit wire representation.
pub(all) struct RegisterField {
name : String
address : UInt16
kind : RegisterFieldKind
writable : Bool
scale : Float
}
///|
pub fn RegisterField::new(
name : String,
address : UInt16,
kind : RegisterFieldKind,
writable? : Bool = false,
scale? : Float = 1.0,
) -> Result[RegisterField, ModbusError] {
if name.length() == 0 ||
!valid_address_range(address, kind.width()) ||
scale == 0.0 {
Err(InvalidData)
} else {
Ok({ name, address, kind, writable, scale })
}
}
///|
pub fn RegisterField::name(self : RegisterField) -> String {
self.name
}
///|
pub fn RegisterField::address(self : RegisterField) -> UInt16 {
self.address
}
///|
pub fn RegisterField::kind(self : RegisterField) -> RegisterFieldKind {
self.kind
}
///|
pub fn RegisterField::width(self : RegisterField) -> Int {
self.kind.width()
}
///|
pub fn RegisterField::is_writable(self : RegisterField) -> Bool {
self.writable
}
///|
pub fn RegisterField::scale(self : RegisterField) -> Float {
self.scale
}
///|
/// A schema with unique named fields and no overlapping wire ranges.
pub struct RegisterSchema {
fields : Array[RegisterField]
max_fields : Int
}
///|
pub fn RegisterSchema::new(
max_fields? : Int = 256,
) -> Result[RegisterSchema, ModbusError] {
if max_fields < 1 {
Err(CapacityExceeded)
} else {
Ok({ fields: [], max_fields })
}
}
///|
pub fn RegisterSchema::add(
self : RegisterSchema,
field : RegisterField,
) -> Result[Unit, ModbusError] {
if self.fields.length() >= self.max_fields {
return Err(CapacityExceeded)
}
for existing in self.fields {
if existing.name == field.name {
return Err(InvalidData)
}
if ranges_overlap(
existing.address.to_int(),
existing.width(),
field.address.to_int(),
field.width(),
) {
return Err(InvalidAddress)
}
}
self.fields.push(field)
Ok(())
}
///|
fn ranges_overlap(a : Int, a_width : Int, b : Int, b_width : Int) -> Bool {
a < b + b_width && b < a + a_width
}
///|
pub fn RegisterSchema::length(self : RegisterSchema) -> Int {
self.fields.length()
}
///|
pub fn RegisterSchema::field(
self : RegisterSchema,
name : String,
) -> RegisterField? {
for field in self.fields {
if field.name == name {
return Some(field)
}
}
None
}
///|
pub fn RegisterSchema::fields(self : RegisterSchema) -> Array[RegisterField] {
let out : Array[RegisterField] = []
for field in self.fields {
out.push(field)
}
out
}
///|
/// Decode one schema field from a register bank.
pub fn RegisterSchema::read(
self : RegisterSchema,
name : String,
bank : RegisterBank,
) -> Result[RegisterValue, ModbusError] {
let field = match self.field(name) {
Some(value) => value
None => return Err(InvalidAddress)
}
let values = match bank.read(field.address, field.width()) {
Ok(value) => value
Err(error) => return Err(error)
}
decode_field(field, values)
}
///|
fn decode_field(
field : RegisterField,
values : Array[UInt16],
) -> Result[RegisterValue, ModbusError] {
match field.kind {
Unsigned16 => Ok(U16(values[0]))
Signed16 => Ok(I16(signed_register(values[0])))
Unsigned32 =>
match registers_to_u32(values, HighWordFirst) {
Ok(value) => Ok(U32(value))
Err(error) => Err(error)
}
Signed32 =>
match registers_to_i32(values, HighWordFirst) {
Ok(value) => Ok(I32(value))
Err(error) => Err(error)
}
Float32 =>
match registers_to_f32(values, HighWordFirst) {
Ok(value) => Ok(F32(value))
Err(error) => Err(error)
}
Unsigned64 =>
match registers_to_u64(values, HighWordFirst) {
Ok(value) => Ok(U64(value))
Err(error) => Err(error)
}
BitField(bits) => {
let out : Array[Bool] = []
for index in 0.. Result[Unit, ModbusError] {
let field = match self.field(name) {
Some(value) => value
None => return Err(InvalidAddress)
}
if !field.writable {
return Err(Unsupported)
}
let values = match encode_field(field, value) {
Ok(encoded) => encoded
Err(error) => return Err(error)
}
bank.write(field.address, values)
}
///|
fn encode_field(
field : RegisterField,
value : RegisterValue,
) -> Result[Array[UInt16], ModbusError] {
match (field.kind, value) {
(Unsigned16, U16(value)) => Ok([value])
(Signed16, I16(value)) => Ok([value.to_uint16()])
(Unsigned32, U32(value)) => Ok(u32_to_registers(value, HighWordFirst))
(Signed32, I32(value)) => Ok(i32_to_registers(value, HighWordFirst))
(Float32, F32(value)) => Ok(f32_to_registers(value, HighWordFirst))
(Unsigned64, U64(value)) => Ok(u64_to_registers(value, HighWordFirst))
(BitField(bits), Bits(values)) => {
if values.length() != bits {
return Err(InvalidLength)
}
let words : Array[UInt16] = []
for _ in 0..<((bits + 15) / 16) {
words.push(0)
}
for index, bit in values {
if bit {
words[index / 16] = words[index / 16] |
(1 << (index % 16)).to_uint16()
}
}
Ok(words)
}
_ => Err(InvalidData)
}
}
///|
/// Return the values of every field that fits in a bank snapshot.
pub fn RegisterSchema::read_all(
self : RegisterSchema,
bank : RegisterBank,
) -> Result[Array[(String, RegisterValue)], ModbusError] {
let out : Array[(String, RegisterValue)] = []
for field in self.fields {
match bank.read(field.address, field.width()) {
Ok(values) =>
match decode_field(field, values) {
Ok(value) => out.push((field.name, value))
Err(error) => return Err(error)
}
Err(error) => return Err(error)
}
}
Ok(out)
}
///|
/// Validate a schema against a register bank before starting a service.
pub fn RegisterSchema::validate_against(
self : RegisterSchema,
bank : RegisterBank,
) -> Result[Unit, ModbusError] {
for field in self.fields {
if !bank.contains(field.address, field.width()) {
return Err(InvalidAddress)
}
if field.writable && !bank.is_writable() {
return Err(Unsupported)
}
}
Ok(())
}
///|
/// Render a typed value as a compact diagnostic string.
pub fn register_value_name(value : RegisterValue) -> String {
match value {
U16(value) => "u16:" + value.to_string()
I16(value) => "i16:" + value.to_string()
U32(value) => "u32:" + value.to_string()
I32(value) => "i32:" + value.to_string()
F32(value) => "f32:" + value.to_string()
U64(value) => "u64:" + value.to_string()
Bits(values) => "bits:" + values.length().to_string()
}
}