///|
/// Lifecycle state for a multi-request transaction.
pub(all) enum TransactionState {
TransactionOpen
TransactionPrepared
TransactionCommitted
TransactionRolledBack
TransactionFailed
} derive(Debug, Eq)
///|
/// A request in a transaction with an optional compensation frame.
pub(all) struct TransactionItem {
id : String
request : Frame
compensation : Frame?
mut state : TransactionState
order : Int
}
///|
pub fn TransactionItem::new(
id : String,
request : Frame,
order : Int,
compensation? : Frame = Frame::new(0, 0, []),
) -> Result[TransactionItem, ModbusError] {
if id == "" || order < 0 || request.pdu.function == 0 {
return Err(InvalidData)
}
let selected = if compensation.unit_id == 0 {
None
} else {
Some(compensation)
}
Ok({ id, request, compensation: selected, state: TransactionOpen, order })
}
///|
pub fn TransactionItem::id(self : TransactionItem) -> String {
self.id
}
///|
pub fn TransactionItem::request(self : TransactionItem) -> Frame {
self.request
}
///|
pub fn TransactionItem::compensation(self : TransactionItem) -> Frame? {
self.compensation
}
///|
pub fn TransactionItem::state(self : TransactionItem) -> TransactionState {
self.state
}
///|
pub fn TransactionItem::order(self : TransactionItem) -> Int {
self.order
}
///|
pub fn TransactionItem::mark(
self : TransactionItem,
state : TransactionState,
) -> Unit {
self.state = state
}
///|
/// A transaction result with response and error context.
pub(all) struct TransactionOutcome {
item_id : String
state : TransactionState
response : Frame?
error : ModbusError?
timestamp : Int
}
///|
pub fn TransactionOutcome::new(
item_id : String,
state : TransactionState,
timestamp : Int,
response? : Frame = Frame::new(0, 0, []),
error? : ModbusError = Incomplete,
) -> Result[TransactionOutcome, ModbusError] {
if item_id == "" || timestamp < 0 {
return Err(InvalidData)
}
let selected_response = if response.unit_id == 0 {
None
} else {
Some(response)
}
let selected_error = if state == TransactionCommitted {
None
} else {
Some(error)
}
Ok({
item_id,
state,
response: selected_response,
error: selected_error,
timestamp,
})
}
///|
pub fn TransactionOutcome::item_id(self : TransactionOutcome) -> String {
self.item_id
}
///|
pub fn TransactionOutcome::state(self : TransactionOutcome) -> TransactionState {
self.state
}
///|
pub fn TransactionOutcome::response(self : TransactionOutcome) -> Frame? {
self.response
}
///|
pub fn TransactionOutcome::error(self : TransactionOutcome) -> ModbusError? {
self.error
}
///|
pub fn TransactionOutcome::timestamp(self : TransactionOutcome) -> Int {
self.timestamp
}
///|
/// Ordered transaction coordinator for write-and-verify workflows.
pub(all) struct ModbusTransaction {
id : String
unit_id : Byte
items : Array[TransactionItem]
outcomes : Array[TransactionOutcome]
max_items : Int
mut state : TransactionState
mut next_order : Int
}
///|
pub fn ModbusTransaction::new(
id : String,
unit_id : Byte,
max_items? : Int = 64,
) -> Result[ModbusTransaction, ModbusError] {
if id == "" || !is_valid_unit_id(unit_id) || max_items < 1 {
return Err(InvalidData)
}
Ok({
id,
unit_id,
items: [],
outcomes: [],
max_items,
state: TransactionOpen,
next_order: 0,
})
}
///|
pub fn ModbusTransaction::id(self : ModbusTransaction) -> String {
self.id
}
///|
pub fn ModbusTransaction::unit_id(self : ModbusTransaction) -> Byte {
self.unit_id
}
///|
pub fn ModbusTransaction::state(self : ModbusTransaction) -> TransactionState {
self.state
}
///|
pub fn ModbusTransaction::item_count(self : ModbusTransaction) -> Int {
self.items.length()
}
///|
pub fn ModbusTransaction::outcome_count(self : ModbusTransaction) -> Int {
self.outcomes.length()
}
///|
pub fn ModbusTransaction::add(
self : ModbusTransaction,
request : Frame,
compensation? : Frame = Frame::new(0, 0, []),
) -> Result[String, ModbusError] {
if self.state != TransactionOpen || self.items.length() >= self.max_items {
return Err(Busy)
}
if request.unit_id != self.unit_id {
return Err(UnitMismatch)
}
let id = self.id + "-item-" + self.next_order.to_string()
let item = match
TransactionItem::new(id, request, self.next_order, compensation~) {
Ok(value) => value
Err(error) => return Err(error)
}
self.items.push(item)
self.next_order += 1
Ok(item.id())
}
///|
/// Prepare all requests, rejecting malformed frames before transport I/O.
pub fn ModbusTransaction::prepare(
self : ModbusTransaction,
) -> Result[Unit, ModbusError] {
if self.state != TransactionOpen || self.items.length() == 0 {
return Err(Busy)
}
for item in self.items {
match validate_frame(item.request(), false) {
Err(error) => {
self.state = TransactionFailed
return Err(error)
}
Ok(_) => ()
}
item.mark(TransactionPrepared)
}
self.state = TransactionPrepared
Ok(())
}
///|
/// Record a successful response for the next prepared item.
pub fn ModbusTransaction::acknowledge(
self : ModbusTransaction,
item_id : String,
response : Frame,
timestamp : Int,
) -> Result[Unit, ModbusError] {
if self.state != TransactionPrepared || timestamp < 0 {
return Err(Busy)
}
for item in self.items {
if item.id() == item_id {
if item.state() != TransactionPrepared {
return Err(Busy)
}
if response.unit_id != self.unit_id {
return Err(UnitMismatch)
}
item.mark(TransactionCommitted)
let outcome = match
TransactionOutcome::new(
item_id,
TransactionCommitted,
timestamp,
response~,
) {
Ok(value) => value
Err(error) => return Err(error)
}
self.outcomes.push(outcome)
return Ok(())
}
}
Err(InvalidData)
}
///|
/// Mark an item failed and retain the protocol error.
pub fn ModbusTransaction::fail(
self : ModbusTransaction,
item_id : String,
error : ModbusError,
timestamp : Int,
) -> Result[Unit, ModbusError] {
if self.state != TransactionPrepared || timestamp < 0 {
return Err(Busy)
}
for item in self.items {
if item.id() == item_id {
item.mark(TransactionFailed)
self.state = TransactionFailed
let outcome = match
TransactionOutcome::new(item_id, TransactionFailed, timestamp, error~) {
Ok(value) => value
Err(failure) => return Err(failure)
}
self.outcomes.push(outcome)
return Ok(())
}
}
Err(InvalidData)
}
///|
/// Commit only when every prepared item has acknowledged successfully.
pub fn ModbusTransaction::commit(
self : ModbusTransaction,
) -> Result[Unit, ModbusError] {
if self.state != TransactionPrepared {
return Err(Busy)
}
for item in self.items {
if item.state() != TransactionCommitted {
return Err(Incomplete)
}
}
self.state = TransactionCommitted
Ok(())
}
///|
/// Roll back a prepared or partially committed transaction.
pub fn ModbusTransaction::rollback(
self : ModbusTransaction,
timestamp : Int,
) -> Result[Array[Frame], ModbusError] {
if timestamp < 0 ||
self.state == TransactionCommitted ||
self.state == TransactionRolledBack {
return Err(Busy)
}
let compensations : Array[Frame] = []
for item in self.items {
if item.state() == TransactionCommitted {
match item.compensation() {
Some(frame) => compensations.push(frame)
None => ()
}
}
item.mark(TransactionRolledBack)
}
self.state = TransactionRolledBack
Ok(compensations)
}
///|
pub fn ModbusTransaction::next_prepared(
self : ModbusTransaction,
) -> TransactionItem? {
for item in self.items {
if item.state() == TransactionPrepared {
return Some(item)
}
}
None
}
///|
pub fn ModbusTransaction::outcomes(
self : ModbusTransaction,
) -> Array[TransactionOutcome] {
self.outcomes
}
///|
pub fn ModbusTransaction::is_successful(self : ModbusTransaction) -> Bool {
self.state == TransactionCommitted
}
///|
pub fn transaction_state_name(state : TransactionState) -> String {
match state {
TransactionOpen => "open"
TransactionPrepared => "prepared"
TransactionCommitted => "committed"
TransactionRolledBack => "rolled-back"
TransactionFailed => "failed"
}
}
///|
/// A transaction batch containing several independent units.
pub(all) struct TransactionBatch {
transactions : Array[ModbusTransaction]
max_transactions : Int
}
///|
pub fn TransactionBatch::new(
max_transactions? : Int = 128,
) -> Result[TransactionBatch, ModbusError] {
if max_transactions < 1 {
Err(InvalidData)
} else {
Ok({ transactions: [], max_transactions })
}
}
///|
pub fn TransactionBatch::count(self : TransactionBatch) -> Int {
self.transactions.length()
}
///|
pub fn TransactionBatch::add(
self : TransactionBatch,
transaction : ModbusTransaction,
) -> Result[Unit, ModbusError] {
if self.transactions.length() >= self.max_transactions {
return Err(CapacityExceeded)
}
for existing in self.transactions {
if existing.id() == transaction.id() {
return Err(InvalidData)
}
}
self.transactions.push(transaction)
Ok(())
}
///|
pub fn TransactionBatch::successful(self : TransactionBatch) -> Int {
let mut count = 0
for transaction in self.transactions {
if transaction.is_successful() {
count += 1
}
}
count
}
///|
pub fn TransactionBatch::failed(self : TransactionBatch) -> Int {
let mut count = 0
for transaction in self.transactions {
if transaction.state() == TransactionFailed {
count += 1
}
}
count
}