///|
/// Address space selected by a polling point.
pub(all) enum PollArea {
PollCoils
PollDiscreteInputs
PollHoldingRegisters
PollInputRegisters
} derive(Debug, Eq)
///|
/// A point requested by a periodic acquisition job.
pub(all) struct PollPoint {
id : String
device_id : String
unit_id : Byte
area : PollArea
address : UInt16
quantity : Int
period : Int
priority : Int
mut enabled : Bool
}
///|
pub fn PollPoint::new(
id : String,
device_id : String,
unit_id : Byte,
area : PollArea,
address : UInt16,
quantity : Int,
period : Int,
priority? : Int = 0,
) -> Result[PollPoint, ModbusError] {
if id == "" ||
device_id == "" ||
!is_valid_unit_id(unit_id) ||
quantity < 1 ||
period < 1 ||
priority < 0 {
return Err(InvalidData)
}
let limit = match area {
PollCoils | PollDiscreteInputs => 2000
PollHoldingRegisters | PollInputRegisters => 125
}
if quantity > limit || !valid_address_range(address, quantity) {
return Err(InvalidAddress)
}
Ok({
id,
device_id,
unit_id,
area,
address,
quantity,
period,
priority,
enabled: true,
})
}
///|
pub fn PollPoint::id(self : PollPoint) -> String {
self.id
}
///|
pub fn PollPoint::device_id(self : PollPoint) -> String {
self.device_id
}
///|
pub fn PollPoint::unit_id(self : PollPoint) -> Byte {
self.unit_id
}
///|
pub fn PollPoint::area(self : PollPoint) -> PollArea {
self.area
}
///|
pub fn PollPoint::address(self : PollPoint) -> UInt16 {
self.address
}
///|
pub fn PollPoint::quantity(self : PollPoint) -> Int {
self.quantity
}
///|
pub fn PollPoint::period(self : PollPoint) -> Int {
self.period
}
///|
pub fn PollPoint::priority(self : PollPoint) -> Int {
self.priority
}
///|
pub fn PollPoint::enabled(self : PollPoint) -> Bool {
self.enabled
}
///|
pub fn PollPoint::set_enabled(self : PollPoint, enabled : Bool) -> Unit {
self.enabled = enabled
}
///|
pub fn PollPoint::end_exclusive(self : PollPoint) -> Int {
self.address.to_int() + self.quantity
}
///|
pub fn PollPoint::overlaps(self : PollPoint, other : PollPoint) -> Bool {
self.device_id == other.device_id &&
self.unit_id == other.unit_id &&
self.area == other.area &&
self.address.to_int() < other.end_exclusive() &&
other.address.to_int() < self.end_exclusive()
}
///|
pub fn poll_area_function(area : PollArea) -> Byte {
match area {
PollCoils => 1
PollDiscreteInputs => 2
PollHoldingRegisters => 3
PollInputRegisters => 4
}
}
///|
pub fn poll_area_name(area : PollArea) -> String {
match area {
PollCoils => "coils"
PollDiscreteInputs => "discrete-inputs"
PollHoldingRegisters => "holding-registers"
PollInputRegisters => "input-registers"
}
}
///|
/// A contiguous request made from one or more polling points.
pub(all) struct PollRequestGroup {
id : String
device_id : String
unit_id : Byte
area : PollArea
address : UInt16
mut quantity : Int
points : Array[String]
mut period : Int
mut priority : Int
}
///|
pub fn PollRequestGroup::new(
id : String,
first : PollPoint,
) -> PollRequestGroup {
{
id,
device_id: first.device_id(),
unit_id: first.unit_id(),
area: first.area(),
address: first.address(),
quantity: first.quantity(),
points: [first.id()],
period: first.period(),
priority: first.priority(),
}
}
///|
pub fn PollRequestGroup::id(self : PollRequestGroup) -> String {
self.id
}
///|
pub fn PollRequestGroup::device_id(self : PollRequestGroup) -> String {
self.device_id
}
///|
pub fn PollRequestGroup::unit_id(self : PollRequestGroup) -> Byte {
self.unit_id
}
///|
pub fn PollRequestGroup::area(self : PollRequestGroup) -> PollArea {
self.area
}
///|
pub fn PollRequestGroup::address(self : PollRequestGroup) -> UInt16 {
self.address
}
///|
pub fn PollRequestGroup::quantity(self : PollRequestGroup) -> Int {
self.quantity
}
///|
pub fn PollRequestGroup::period(self : PollRequestGroup) -> Int {
self.period
}
///|
pub fn PollRequestGroup::priority(self : PollRequestGroup) -> Int {
self.priority
}
///|
pub fn PollRequestGroup::point_count(self : PollRequestGroup) -> Int {
self.points.length()
}
///|
pub fn PollRequestGroup::points(self : PollRequestGroup) -> Array[String] {
self.points
}
///|
pub fn PollRequestGroup::end_exclusive(self : PollRequestGroup) -> Int {
self.address.to_int() + self.quantity
}
///|
pub fn PollRequestGroup::can_merge(
self : PollRequestGroup,
point : PollPoint,
gap : Int,
) -> Bool {
if gap < 0 ||
point.device_id() != self.device_id ||
point.unit_id() != self.unit_id ||
point.area() != self.area {
return false
}
if point.address().to_int() < self.address.to_int() {
return false
}
point.address().to_int() <= self.end_exclusive() + gap &&
point.end_exclusive() <= 65536
}
///|
pub fn PollRequestGroup::merge(
self : PollRequestGroup,
point : PollPoint,
) -> Result[Unit, ModbusError] {
if !self.can_merge(point, 0) {
return Err(UnitMismatch)
}
let end = if point.end_exclusive() > self.end_exclusive() {
point.end_exclusive()
} else {
self.end_exclusive()
}
self.quantity = end - self.address.to_int()
self.points.push(point.id())
if point.period() < self.period {
self.period = point.period()
}
if point.priority() > self.priority {
self.priority = point.priority()
}
Ok(())
}
///|
/// Build the request frame for a contiguous poll group.
pub fn PollRequestGroup::request(
self : PollRequestGroup,
) -> Result[Frame, ModbusError] {
match self.area {
PollCoils =>
read_coils(self.unit_id, self.address, self.quantity.to_uint16())
PollDiscreteInputs =>
read_discrete_inputs(
self.unit_id,
self.address,
self.quantity.to_uint16(),
)
PollHoldingRegisters =>
read_holding_registers(
self.unit_id,
self.address,
self.quantity.to_uint16(),
)
PollInputRegisters =>
read_input_registers(
self.unit_id,
self.address,
self.quantity.to_uint16(),
)
}
}
///|
pub fn PollRequestGroup::summary(self : PollRequestGroup) -> String {
self.id +
" " +
poll_area_name(self.area) +
" " +
self.address.to_int().to_string() +
"+" +
self.quantity.to_string() +
" points=" +
self.points.length().to_string()
}
///|
/// A complete plan generated from point definitions.
pub(all) struct PollGroupPlan {
points : Array[PollPoint]
groups : Array[PollRequestGroup]
max_groups : Int
merge_gap : Int
mut revision : Int
}
///|
pub fn PollGroupPlan::new(
max_groups? : Int = 2048,
merge_gap? : Int = 0,
) -> Result[PollGroupPlan, ModbusError] {
if max_groups < 1 || merge_gap < 0 {
Err(InvalidData)
} else {
Ok({ points: [], groups: [], max_groups, merge_gap, revision: 0 })
}
}
///|
pub fn PollGroupPlan::point_count(self : PollGroupPlan) -> Int {
self.points.length()
}
///|
pub fn PollGroupPlan::group_count(self : PollGroupPlan) -> Int {
self.groups.length()
}
///|
pub fn PollGroupPlan::revision(self : PollGroupPlan) -> Int {
self.revision
}
///|
pub fn PollGroupPlan::groups(self : PollGroupPlan) -> Array[PollRequestGroup] {
self.groups
}
///|
pub fn PollGroupPlan::points(self : PollGroupPlan) -> Array[PollPoint] {
self.points
}
///|
pub fn PollGroupPlan::add_point(
self : PollGroupPlan,
point : PollPoint,
) -> Result[Unit, ModbusError] {
for existing in self.points {
if existing.id() == point.id() {
return Err(InvalidData)
}
}
self.points.push(point)
self.rebuild()
Ok(())
}
///|
pub fn PollGroupPlan::remove_point(
self : PollGroupPlan,
id : String,
) -> Result[PollPoint, ModbusError] {
let mut index = -1
for i in 0.. Unit {
self.groups.clear()
let pending = poll_sorted_points(self.points)
for point in pending {
if !point.enabled() {
continue
}
let mut merged = false
for group in self.groups {
if group.can_merge(point, self.merge_gap) {
ignore(group.merge(point))
merged = true
break
}
}
if !merged {
if self.groups.length() < self.max_groups {
let group_id = "poll-" + self.groups.length().to_string()
self.groups.push(PollRequestGroup::new(group_id, point))
}
}
}
self.revision += 1
}
///|
fn poll_sorted_points(points : Array[PollPoint]) -> Array[PollPoint] {
let remaining = points.copy()
let result : Array[PollPoint] = []
while remaining.length() > 0 {
let mut best = 0
for i in 1.. Int {
if left.device_id() < right.device_id() {
return -1
}
if left.device_id() > right.device_id() {
return 1
}
if left.unit_id() < right.unit_id() {
return -1
}
if left.unit_id() > right.unit_id() {
return 1
}
if poll_area_function(left.area()) < poll_area_function(right.area()) {
return -1
}
if poll_area_function(left.area()) > poll_area_function(right.area()) {
return 1
}
if left.address() < right.address() {
-1
} else if left.address() > right.address() {
1
} else {
0
}
}
///|
pub fn PollGroupPlan::request_for(
self : PollGroupPlan,
group_id : String,
) -> Result[Frame, ModbusError] {
for group in self.groups {
if group.id() == group_id {
return group.request()
}
}
Err(InvalidData)
}
///|
pub fn PollGroupPlan::groups_for(
self : PollGroupPlan,
device_id : String,
) -> Array[PollRequestGroup] {
let result : Array[PollRequestGroup] = []
for group in self.groups {
if group.device_id() == device_id {
result.push(group)
}
}
result
}
///|
pub fn PollGroupPlan::due_groups(
self : PollGroupPlan,
now : Int,
last_run : Array[(String, Int)],
) -> Result[Array[String], ModbusError] {
if now < 0 {
return Err(InvalidData)
}
let result : Array[String] = []
for group in self.groups {
let mut previous = 0
for item in last_run {
if item.0 == group.id() {
previous = item.1
}
}
if previous == 0 || now - previous >= group.period() {
result.push(group.id())
}
}
Ok(result)
}
///|
pub fn PollGroupPlan::validate(self : PollGroupPlan) -> Array[ModbusError] {
let result : Array[ModbusError] = []
for point in self.points {
if !point.enabled() {
continue
}
if point.quantity() < 1 {
result.push(InvalidQuantity)
}
if !valid_address_range(point.address(), point.quantity()) {
result.push(InvalidAddress)
}
}
for i in 0.. 125 &&
self.groups[i].area() != PollCoils &&
self.groups[i].area() != PollDiscreteInputs {
result.push(InvalidQuantity)
}
}
result
}
///|
/// A budget for limiting requests emitted in one scheduler cycle.
pub(all) struct PollBudget {
max_requests : Int
max_registers : Int
mut requests : Int
mut registers : Int
mut rejected : Int
}
///|
pub fn PollBudget::new(
max_requests : Int,
max_registers : Int,
) -> Result[PollBudget, ModbusError] {
if max_requests < 1 || max_registers < 1 {
Err(InvalidData)
} else {
Ok({ max_requests, max_registers, requests: 0, registers: 0, rejected: 0 })
}
}
///|
pub fn PollBudget::max_requests(self : PollBudget) -> Int {
self.max_requests
}
///|
pub fn PollBudget::max_registers(self : PollBudget) -> Int {
self.max_registers
}
///|
pub fn PollBudget::requests(self : PollBudget) -> Int {
self.requests
}
///|
pub fn PollBudget::registers(self : PollBudget) -> Int {
self.registers
}
///|
pub fn PollBudget::rejected(self : PollBudget) -> Int {
self.rejected
}
///|
pub fn PollBudget::accept(self : PollBudget, group : PollRequestGroup) -> Bool {
let amount = if group.area() == PollCoils ||
group.area() == PollDiscreteInputs {
(group.quantity() + 15) / 16
} else {
group.quantity()
}
if self.requests >= self.max_requests ||
self.registers + amount > self.max_registers {
self.rejected += 1
false
} else {
self.requests += 1
self.registers += amount
true
}
}
///|
pub fn PollBudget::reset(self : PollBudget) -> Unit {
self.requests = 0
self.registers = 0
self.rejected = 0
}
///|
pub fn PollBudget::remaining_requests(self : PollBudget) -> Int {
self.max_requests - self.requests
}
///|
pub fn PollBudget::remaining_registers(self : PollBudget) -> Int {
self.max_registers - self.registers
}
///|
/// Produce a budgeted batch while preserving group priority order.
pub fn PollGroupPlan::budgeted(
self : PollGroupPlan,
budget : PollBudget,
) -> Array[PollRequestGroup] {
let ordered = poll_sorted_groups(self.groups)
let result : Array[PollRequestGroup] = []
for group in ordered {
if budget.accept(group) {
result.push(group)
}
}
result
}
///|
fn poll_sorted_groups(
groups : Array[PollRequestGroup],
) -> Array[PollRequestGroup] {
let remaining = groups.copy()
let result : Array[PollRequestGroup] = []
while remaining.length() > 0 {
let mut best = 0
for i in 1.. remaining[best].priority() {
best = i
}
}
result.push(remaining.remove(best))
}
result
}
///|
/// A plan statistic used to tune address maps and transport budgets.
pub(all) struct PollPlanStats {
points : Int
enabled_points : Int
groups : Int
merged_points : Int
estimated_registers : Int
highest_priority : Int
}
///|
pub fn PollGroupPlan::stats(self : PollGroupPlan) -> PollPlanStats {
let mut enabled = 0
let mut highest = 0
for point in self.points {
if point.enabled() {
enabled += 1
}
if point.priority() > highest {
highest = point.priority()
}
}
let mut registers = 0
for group in self.groups {
registers += group.quantity()
}
{
points: self.points.length(),
enabled_points: enabled,
groups: self.groups.length(),
merged_points: enabled - self.groups.length(),
estimated_registers: registers,
highest_priority: highest,
}
}
///|
pub fn PollPlanStats::points(self : PollPlanStats) -> Int {
self.points
}
///|
pub fn PollPlanStats::enabled_points(self : PollPlanStats) -> Int {
self.enabled_points
}
///|
pub fn PollPlanStats::groups(self : PollPlanStats) -> Int {
self.groups
}
///|
pub fn PollPlanStats::merged_points(self : PollPlanStats) -> Int {
self.merged_points
}
///|
pub fn PollPlanStats::estimated_registers(self : PollPlanStats) -> Int {
self.estimated_registers
}
///|
pub fn PollPlanStats::highest_priority(self : PollPlanStats) -> Int {
self.highest_priority
}