///|
/// A logical address area used by a device profile.
pub(all) enum ProfileArea {
Coils
DiscreteInputs
HoldingRegisters
InputRegisters
} derive(Debug, Eq)
///|
/// Encoding used when a profile point is mapped to registers.
pub(all) enum ProfileEncoding {
Bit
Unsigned16
Signed16
Unsigned32
Signed32
Unsigned64
RawWords
} derive(Debug, Eq)
///|
/// A single named data point exposed by an industrial device.
pub(all) struct ProfilePoint {
id : String
label : String
area : ProfileArea
address : UInt16
quantity : Int
encoding : ProfileEncoding
writable : Bool
scale_numerator : Int
scale_denominator : Int
offset : Int
}
///|
pub fn ProfilePoint::new(
id : String,
label : String,
area : ProfileArea,
address : UInt16,
quantity : Int,
encoding : ProfileEncoding,
writable? : Bool = false,
scale_numerator? : Int = 1,
scale_denominator? : Int = 1,
offset? : Int = 0,
) -> Result[ProfilePoint, ModbusError] {
if id == "" || label == "" {
return Err(InvalidData)
}
if quantity < 1 {
return Err(InvalidQuantity)
}
if scale_denominator == 0 {
return Err(InvalidData)
}
if !profile_area_accepts(area, encoding, quantity) {
return Err(InvalidData)
}
if !valid_address_range(address, quantity * profile_words_per_unit(encoding)) {
return Err(InvalidAddress)
}
if writable && !profile_area_writable(area) {
return Err(Unsupported)
}
Ok({
id,
label,
area,
address,
quantity,
encoding,
writable,
scale_numerator,
scale_denominator,
offset,
})
}
///|
fn profile_area_writable(area : ProfileArea) -> Bool {
match area {
Coils | HoldingRegisters => true
DiscreteInputs | InputRegisters => false
}
}
///|
fn profile_words_per_unit(encoding : ProfileEncoding) -> Int {
match encoding {
Bit | Unsigned16 | Signed16 => 1
Unsigned32 | Signed32 => 2
Unsigned64 => 4
RawWords => 1
}
}
///|
fn profile_area_accepts(
area : ProfileArea,
encoding : ProfileEncoding,
quantity : Int,
) -> Bool {
match area {
Coils | DiscreteInputs => encoding == Bit && quantity <= 2000
HoldingRegisters | InputRegisters => encoding != Bit && quantity <= 125
}
}
///|
pub fn ProfilePoint::id(self : ProfilePoint) -> String {
self.id
}
///|
pub fn ProfilePoint::label(self : ProfilePoint) -> String {
self.label
}
///|
pub fn ProfilePoint::area(self : ProfilePoint) -> ProfileArea {
self.area
}
///|
pub fn ProfilePoint::address(self : ProfilePoint) -> UInt16 {
self.address
}
///|
pub fn ProfilePoint::quantity(self : ProfilePoint) -> Int {
self.quantity
}
///|
pub fn ProfilePoint::encoding(self : ProfilePoint) -> ProfileEncoding {
self.encoding
}
///|
pub fn ProfilePoint::writable(self : ProfilePoint) -> Bool {
self.writable
}
///|
pub fn ProfilePoint::end_exclusive(self : ProfilePoint) -> Int {
self.address.to_int() + self.quantity * profile_words_per_unit(self.encoding)
}
///|
pub fn ProfilePoint::register_count(self : ProfilePoint) -> Int {
self.quantity * profile_words_per_unit(self.encoding)
}
///|
pub fn ProfilePoint::contains(self : ProfilePoint, address : UInt16) -> Bool {
address.to_int() >= self.address.to_int() &&
address.to_int() < self.end_exclusive()
}
///|
pub fn ProfilePoint::contains_range(
self : ProfilePoint,
address : UInt16,
length : Int,
) -> Bool {
length >= 1 &&
address.to_int() >= self.address.to_int() &&
address.to_int() + length <= self.end_exclusive()
}
///|
pub fn ProfilePoint::scale_numerator(self : ProfilePoint) -> Int {
self.scale_numerator
}
///|
pub fn ProfilePoint::scale_denominator(self : ProfilePoint) -> Int {
self.scale_denominator
}
///|
pub fn ProfilePoint::offset(self : ProfilePoint) -> Int {
self.offset
}
///|
pub fn ProfilePoint::raw_to_engineering(self : ProfilePoint, raw : Int) -> Int {
raw * self.scale_numerator / self.scale_denominator + self.offset
}
///|
pub fn ProfilePoint::engineering_to_raw(
self : ProfilePoint,
value : Int,
) -> Result[Int, ModbusError] {
if self.scale_numerator == 0 {
return Err(InvalidData)
}
Ok((value - self.offset) * self.scale_denominator / self.scale_numerator)
}
///|
pub fn ProfilePoint::function_code(self : ProfilePoint) -> Byte {
match self.area {
Coils => 1
DiscreteInputs => 2
HoldingRegisters => 3
InputRegisters => 4
}
}
///|
pub fn ProfilePoint::read_request(
self : ProfilePoint,
unit_id : Byte,
) -> Result[Frame, ModbusError] {
if !is_valid_unit_id(unit_id) {
return Err(InvalidUnitId)
}
let address = self.address
let quantity = self.register_count()
match self.area {
Coils => read_coils(unit_id, address, self.quantity.to_uint16())
DiscreteInputs =>
read_discrete_inputs(unit_id, address, self.quantity.to_uint16())
HoldingRegisters => Ok(read_holding(unit_id, address, quantity.to_uint16()))
InputRegisters =>
read_input_registers(unit_id, address, quantity.to_uint16())
}
}
///|
pub fn ProfilePoint::write_request(
self : ProfilePoint,
unit_id : Byte,
value : Int,
) -> Result[Frame, ModbusError] {
if !self.writable {
return Err(Unsupported)
}
if !is_valid_unit_id(unit_id, broadcast=false) {
return Err(InvalidUnitId)
}
match self.area {
Coils => write_single_coil(unit_id, self.address, value != 0)
HoldingRegisters => {
let raw = match self.engineering_to_raw(value) {
Ok(value) => value
Err(error) => return Err(error)
}
if raw < 0 || raw > 65535 {
Err(InvalidData)
} else {
write_single_register_checked(unit_id, self.address, raw.to_uint16())
}
}
DiscreteInputs | InputRegisters => Err(Unsupported)
}
}
///|
pub fn ProfilePoint::summary(self : ProfilePoint) -> String {
self.id +
"=" +
profile_area_name(self.area) +
"@" +
self.address.to_string() +
"/" +
self.quantity.to_string() +
" writable=" +
(if self.writable { "true" } else { "false" })
}
///|
pub fn profile_area_name(area : ProfileArea) -> String {
match area {
Coils => "coils"
DiscreteInputs => "discrete-inputs"
HoldingRegisters => "holding-registers"
InputRegisters => "input-registers"
}
}
///|
pub fn profile_encoding_name(encoding : ProfileEncoding) -> String {
match encoding {
Bit => "bit"
Unsigned16 => "u16"
Signed16 => "i16"
Unsigned32 => "u32"
Signed32 => "i32"
Unsigned64 => "u64"
RawWords => "words"
}
}
///|
/// A named set of points belonging to one device model.
pub(all) struct DeviceProfile {
name : String
model : String
unit_id : Byte
revision : String
points : Array[ProfilePoint]
max_points : Int
}
///|
pub fn DeviceProfile::new(
name : String,
model : String,
unit_id : Byte,
revision? : String = "1",
max_points? : Int = 512,
) -> Result[DeviceProfile, ModbusError] {
if name == "" || model == "" || revision == "" {
return Err(InvalidData)
}
if !is_valid_unit_id(unit_id, broadcast=false) || max_points < 1 {
return Err(InvalidUnitId)
}
Ok({ name, model, unit_id, revision, points: [], max_points })
}
///|
pub fn DeviceProfile::name(self : DeviceProfile) -> String {
self.name
}
///|
pub fn DeviceProfile::model(self : DeviceProfile) -> String {
self.model
}
///|
pub fn DeviceProfile::unit_id(self : DeviceProfile) -> Byte {
self.unit_id
}
///|
pub fn DeviceProfile::revision(self : DeviceProfile) -> String {
self.revision
}
///|
pub fn DeviceProfile::point_count(self : DeviceProfile) -> Int {
self.points.length()
}
///|
pub fn DeviceProfile::add_point(
self : DeviceProfile,
point : ProfilePoint,
) -> Result[Unit, ModbusError] {
if self.points.length() >= self.max_points {
return Err(CapacityExceeded)
}
if point.writable() && point.area() == DiscreteInputs {
return Err(Unsupported)
}
for current in self.points {
if current.id() == point.id() {
return Err(InvalidData)
}
if current.area() == point.area() &&
current.contains_range(point.address(), point.register_count()) {
return Err(InvalidAddress)
}
}
self.points.push(point)
Ok(())
}
///|
pub fn DeviceProfile::remove_point(
self : DeviceProfile,
id : String,
) -> Result[ProfilePoint, ModbusError] {
for index, point in self.points {
if point.id() == id {
ignore(self.points.remove(index))
return Ok(point)
}
}
Err(InvalidData)
}
///|
pub fn DeviceProfile::find(self : DeviceProfile, id : String) -> ProfilePoint? {
for point in self.points {
if point.id() == id {
return Some(point)
}
}
None
}
///|
pub fn DeviceProfile::point(
self : DeviceProfile,
index : Int,
) -> Result[ProfilePoint, ModbusError] {
if index < 0 || index >= self.points.length() {
Err(InvalidAddress)
} else {
Ok(self.points[index])
}
}
///|
pub fn DeviceProfile::points(self : DeviceProfile) -> Array[ProfilePoint] {
let result : Array[ProfilePoint] = []
for point in self.points {
result.push(point)
}
result
}
///|
pub fn DeviceProfile::points_in_area(
self : DeviceProfile,
area : ProfileArea,
) -> Array[ProfilePoint] {
let result : Array[ProfilePoint] = []
for point in self.points {
if point.area() == area {
result.push(point)
}
}
result
}
///|
pub fn DeviceProfile::writable_points(
self : DeviceProfile,
) -> Array[ProfilePoint] {
let result : Array[ProfilePoint] = []
for point in self.points {
if point.writable() {
result.push(point)
}
}
result
}
///|
pub fn DeviceProfile::read_requests(
self : DeviceProfile,
) -> Result[Array[Frame], ModbusError] {
let result : Array[Frame] = []
for point in self.points {
match point.read_request(self.unit_id) {
Ok(frame) => result.push(frame)
Err(error) => return Err(error)
}
}
Ok(result)
}
///|
pub fn DeviceProfile::validate(
self : DeviceProfile,
) -> Result[Unit, ModbusError] {
if self.points.length() == 0 {
return Err(InvalidData)
}
for point in self.points {
if point.id() == "" ||
point.quantity() < 1 ||
!valid_address_range(point.address(), point.register_count()) {
return Err(InvalidData)
}
}
Ok(())
}
///|
pub fn DeviceProfile::summary(self : DeviceProfile) -> String {
self.name +
" model=" +
self.model +
" unit=" +
self.unit_id.to_string() +
" revision=" +
self.revision +
" points=" +
self.points.length().to_string()
}
///|
/// Registry for profiles loaded from an application configuration.
pub(all) struct ProfileRegistry {
profiles : Array[DeviceProfile]
max_profiles : Int
}
///|
pub fn ProfileRegistry::new(
max_profiles? : Int = 64,
) -> Result[ProfileRegistry, ModbusError] {
if max_profiles < 1 {
Err(CapacityExceeded)
} else {
Ok({ profiles: [], max_profiles })
}
}
///|
pub fn ProfileRegistry::count(self : ProfileRegistry) -> Int {
self.profiles.length()
}
///|
pub fn ProfileRegistry::add(
self : ProfileRegistry,
profile : DeviceProfile,
) -> Result[Unit, ModbusError] {
match profile.validate() {
Err(error) => return Err(error)
Ok(_) => ()
}
for current in self.profiles {
if current.name() == profile.name() ||
current.unit_id() == profile.unit_id() {
return Err(InvalidData)
}
}
if self.profiles.length() >= self.max_profiles {
return Err(CapacityExceeded)
}
self.profiles.push(profile)
Ok(())
}
///|
pub fn ProfileRegistry::replace(
self : ProfileRegistry,
profile : DeviceProfile,
) -> Result[Unit, ModbusError] {
match profile.validate() {
Err(error) => return Err(error)
Ok(_) => ()
}
for index, current in self.profiles {
if current.name() == profile.name() {
self.profiles[index] = profile
return Ok(())
}
}
self.add(profile)
}
///|
pub fn ProfileRegistry::by_name(
self : ProfileRegistry,
name : String,
) -> DeviceProfile? {
for profile in self.profiles {
if profile.name() == name {
return Some(profile)
}
}
None
}
///|
pub fn ProfileRegistry::by_unit(
self : ProfileRegistry,
unit_id : Byte,
) -> DeviceProfile? {
for profile in self.profiles {
if profile.unit_id() == unit_id {
return Some(profile)
}
}
None
}
///|
pub fn ProfileRegistry::snapshot(
self : ProfileRegistry,
) -> Array[DeviceProfile] {
let result : Array[DeviceProfile] = []
for profile in self.profiles {
result.push(profile)
}
result
}
///|
pub fn ProfileRegistry::remove_by_name(
self : ProfileRegistry,
name : String,
) -> Result[DeviceProfile, ModbusError] {
for index, profile in self.profiles {
if profile.name() == name {
ignore(self.profiles.remove(index))
return Ok(profile)
}
}
Err(InvalidData)
}
///|
/// Return all points that are safe to poll in one contiguous register range.
pub fn profile_contiguous_points(
points : Array[ProfilePoint],
area : ProfileArea,
) -> Array[ProfilePoint] {
let result : Array[ProfilePoint] = []
let mut next_address = 0
let mut started = false
for point in points {
if point.area() == area {
if !started || point.address().to_int() == next_address {
result.push(point)
next_address = point.end_exclusive()
started = true
}
}
}
result
}
///|
pub fn profile_point_value_from_registers(
point : ProfilePoint,
registers : Array[UInt16],
) -> Result[ProfileValue, ModbusError] {
if point.area() == Coils || point.area() == DiscreteInputs {
return Err(InvalidData)
}
if registers.length() != point.register_count() {
return Err(InvalidLength)
}
match point.encoding() {
Unsigned16 => Ok(U16(registers[0]))
Signed16 => Ok(I16(signed_register(registers[0])))
Unsigned32 =>
Ok(U32((registers[0].to_uint() << 16) | registers[1].to_uint()))
Signed32 =>
Ok(
I32(
((registers[0].to_uint() << 16) | registers[1].to_uint()).reinterpret_as_int(),
),
)
Unsigned64 => {
let mut value : UInt64 = 0
for register in registers {
value = (value << 16) | register.to_uint64()
}
Ok(U64(value))
}
RawWords => Ok(Words(registers))
Bit => Err(InvalidData)
}
}
///|
/// A typed value returned by a profile point decoder.
pub(all) enum ProfileValue {
BitValue(Bool)
U16(UInt16)
I16(Int)
U32(UInt)
I32(Int)
U64(UInt64)
Words(Array[UInt16])
} derive(Debug)
///|
pub fn ProfileValue::as_int(self : ProfileValue) -> Result[Int, ModbusError] {
match self {
BitValue(value) => Ok(if value { 1 } else { 0 })
U16(value) => Ok(value.to_int())
I16(value) => Ok(value)
U32(value) =>
if value > 2147483647 {
Err(InvalidData)
} else {
Ok(value.reinterpret_as_int())
}
I32(value) => Ok(value)
U64(_) => Err(InvalidData)
Words(_) => Err(InvalidData)
}
}
///|
pub fn ProfileValue::word_count(self : ProfileValue) -> Int {
match self {
BitValue(_) | U16(_) | I16(_) => 1
U32(_) | I32(_) => 2
U64(_) => 4
Words(values) => values.length()
}
}
///|
pub fn ProfileValue::summary(self : ProfileValue) -> String {
match self {
BitValue(value) => "bit=" + (if value { "true" } else { "false" })
U16(value) => "u16=" + value.to_string()
I16(value) => "i16=" + value.to_string()
U32(value) => "u32=" + value.to_string()
I32(value) => "i32=" + value.to_string()
U64(value) => "u64=" + value.to_string()
Words(values) => "words=" + values.length().to_string()
}
}