#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
use core::cell::UnsafeCell;
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
use core::mem::MaybeUninit;
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
use core::sync::atomic::AtomicBool;
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
use core::sync::atomic::Ordering;
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
use mbus_client::services::ClientServices;
#[cfg(feature = "internal-lock-stubs")]
use crate::c::lock_stubs::*;
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
use super::app::CApp;
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
use crate::c::error::MbusStatusCode;
#[cfg(feature = "serial-ascii")]
use crate::c::transport::CAsciiTransport;
#[cfg(feature = "serial-rtu")]
use crate::c::transport::CRtuTransport;
#[cfg(feature = "network-tcp")]
use crate::c::transport::CTcpTransport;
#[cfg(any(feature = "serial-ascii", feature = "serial-rtu"))]
use crate::MAX_SERIAL_CLIENTS;
#[cfg(feature = "network-tcp")]
use crate::MAX_TCP_CLIENTS;
#[cfg(feature = "network-tcp")]
pub(super) const TCP_PIPELINE: usize = 10;
#[cfg(any(feature = "serial-rtu", feature = "serial-ascii"))]
pub(super) const SERIAL_PIPELINE: usize = 1;
pub type MbusClientId = u16;
pub const MBUS_INVALID_CLIENT_ID: MbusClientId = 0xFFFF;
#[cfg(feature = "network-tcp")]
const TAG_TCP: u8 = 0x00;
#[cfg(feature = "serial-rtu")]
const TAG_SERIAL_RTU: u8 = 0x01;
#[cfg(feature = "serial-ascii")]
const TAG_SERIAL_ASCII: u8 = 0x02;
#[cfg(all(
not(feature = "internal-lock-stubs"),
any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
)
))]
unsafe extern "C" {
fn mbus_pool_lock();
fn mbus_pool_unlock();
fn mbus_client_lock(id: MbusClientId);
fn mbus_client_unlock(id: MbusClientId);
}
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
pub(super) struct PoolLockGuard;
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
impl PoolLockGuard {
pub(super) fn new() -> Self {
unsafe { mbus_pool_lock() };
Self
}
}
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
impl Drop for PoolLockGuard {
fn drop(&mut self) {
unsafe { mbus_pool_unlock() };
}
}
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
pub(super) struct ClientLockGuard(MbusClientId);
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
impl ClientLockGuard {
pub(super) fn new(id: MbusClientId) -> Self {
unsafe { mbus_client_lock(id) };
Self(id)
}
}
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
impl Drop for ClientLockGuard {
fn drop(&mut self) {
unsafe { mbus_client_unlock(self.0) };
}
}
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
pub(super) struct BorrowGuard<'a>(&'a AtomicBool);
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
impl<'a> BorrowGuard<'a> {
pub(super) fn new(flag: &'a AtomicBool) -> Self {
Self(flag)
}
}
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
impl Drop for BorrowGuard<'_> {
fn drop(&mut self) {
self.0.store(false, Ordering::SeqCst);
}
}
#[cfg(feature = "network-tcp")]
pub(super) type TcpInner = ClientServices<CTcpTransport, CApp, TCP_PIPELINE>;
#[cfg(feature = "serial-rtu")]
pub(super) type SerialRtuInner = ClientServices<CRtuTransport, CApp, SERIAL_PIPELINE>;
#[cfg(feature = "serial-ascii")]
pub(super) type SerialAsciiInner = ClientServices<CAsciiTransport, CApp, SERIAL_PIPELINE>;
#[inline(always)]
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
fn id_tag(id: MbusClientId) -> u8 {
(id >> 8) as u8
}
#[inline(always)]
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
fn id_index(id: MbusClientId) -> usize {
(id & 0xFF) as usize
}
#[inline(always)]
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
fn encode_id(tag: u8, index: usize) -> MbusClientId {
((tag as u16) << 8) | (index as u16)
}
#[inline(always)]
#[cfg(feature = "network-tcp")]
fn is_tcp_id(id: MbusClientId) -> bool {
id != MBUS_INVALID_CLIENT_ID && id_tag(id) == TAG_TCP
}
#[inline(always)]
#[cfg(any(feature = "serial-rtu", feature = "serial-ascii"))]
fn is_serial_id(id: MbusClientId) -> bool {
#[cfg(all(feature = "serial-rtu", feature = "serial-ascii"))]
return id != MBUS_INVALID_CLIENT_ID
&& (id_tag(id) == TAG_SERIAL_RTU || id_tag(id) == TAG_SERIAL_ASCII);
#[cfg(all(feature = "serial-rtu", not(feature = "serial-ascii")))]
return id != MBUS_INVALID_CLIENT_ID && id_tag(id) == TAG_SERIAL_RTU;
#[cfg(all(feature = "serial-ascii", not(feature = "serial-rtu")))]
return id != MBUS_INVALID_CLIENT_ID && id_tag(id) == TAG_SERIAL_ASCII;
}
#[inline(always)]
#[cfg(feature = "serial-rtu")]
#[allow(dead_code)]
fn is_serial_rtu_id(id: MbusClientId) -> bool {
id != MBUS_INVALID_CLIENT_ID && id_tag(id) == TAG_SERIAL_RTU
}
#[inline(always)]
#[cfg(all(feature = "serial-ascii", test))]
fn is_serial_ascii_id(id: MbusClientId) -> bool {
id != MBUS_INVALID_CLIENT_ID && id_tag(id) == TAG_SERIAL_ASCII
}
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
struct Slot<T> {
occupied: bool,
value: MaybeUninit<T>,
borrow_flag: AtomicBool,
}
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
impl<T> Slot<T> {
const fn empty() -> Self {
Self {
occupied: false,
value: MaybeUninit::uninit(),
borrow_flag: AtomicBool::new(false),
}
}
}
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
struct Pool {
#[cfg(feature = "network-tcp")]
tcp_slots: [Slot<TcpInner>; MAX_TCP_CLIENTS],
#[cfg(feature = "serial-rtu")]
serial_rtu_slots: [Slot<SerialRtuInner>; MAX_SERIAL_CLIENTS],
#[cfg(feature = "serial-ascii")]
serial_ascii_slots: [Slot<SerialAsciiInner>; MAX_SERIAL_CLIENTS],
}
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
impl Pool {
const fn new() -> Self {
Self {
#[cfg(feature = "network-tcp")]
tcp_slots: [const { Slot::empty() }; MAX_TCP_CLIENTS],
#[cfg(feature = "serial-rtu")]
serial_rtu_slots: [const { Slot::empty() }; MAX_SERIAL_CLIENTS],
#[cfg(feature = "serial-ascii")]
serial_ascii_slots: [const { Slot::empty() }; MAX_SERIAL_CLIENTS],
}
}
#[cfg(feature = "network-tcp")]
fn allocate_tcp(&mut self, value: TcpInner) -> Option<MbusClientId> {
for (i, slot) in self.tcp_slots.iter_mut().enumerate() {
if !slot.occupied {
slot.value = MaybeUninit::new(value);
slot.borrow_flag.store(false, Ordering::SeqCst);
slot.occupied = true;
return Some(encode_id(TAG_TCP, i));
}
}
None
}
#[cfg(feature = "serial-rtu")]
fn allocate_serial_rtu(&mut self, value: SerialRtuInner) -> Option<MbusClientId> {
for (i, slot) in self.serial_rtu_slots.iter_mut().enumerate() {
if !slot.occupied {
slot.value = MaybeUninit::new(value);
slot.borrow_flag.store(false, Ordering::SeqCst);
slot.occupied = true;
return Some(encode_id(TAG_SERIAL_RTU, i));
}
}
None
}
#[cfg(feature = "serial-ascii")]
fn allocate_serial_ascii(&mut self, value: SerialAsciiInner) -> Option<MbusClientId> {
for (i, slot) in self.serial_ascii_slots.iter_mut().enumerate() {
if !slot.occupied {
slot.value = MaybeUninit::new(value);
slot.borrow_flag.store(false, Ordering::SeqCst);
slot.occupied = true;
return Some(encode_id(TAG_SERIAL_ASCII, i));
}
}
None
}
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
fn free(&mut self, id: MbusClientId) -> bool {
let idx = id_index(id);
match id_tag(id) {
#[cfg(feature = "network-tcp")]
TAG_TCP => {
if idx >= MAX_TCP_CLIENTS {
return false;
}
let slot = &mut self.tcp_slots[idx];
if !slot.occupied {
return false;
}
unsafe { slot.value.assume_init_drop() };
slot.borrow_flag.store(false, Ordering::SeqCst);
slot.occupied = false;
true
}
#[cfg(feature = "serial-rtu")]
TAG_SERIAL_RTU => {
if idx >= MAX_SERIAL_CLIENTS {
return false;
}
let slot = &mut self.serial_rtu_slots[idx];
if !slot.occupied {
return false;
}
unsafe { slot.value.assume_init_drop() };
slot.borrow_flag.store(false, Ordering::SeqCst);
slot.occupied = false;
true
}
#[cfg(feature = "serial-ascii")]
TAG_SERIAL_ASCII => {
if idx >= MAX_SERIAL_CLIENTS {
return false;
}
let slot = &mut self.serial_ascii_slots[idx];
if !slot.occupied {
return false;
}
unsafe { slot.value.assume_init_drop() };
slot.borrow_flag.store(false, Ordering::SeqCst);
slot.occupied = false;
true
}
_ => false,
}
}
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
fn is_occupied(&self, id: MbusClientId) -> bool {
let idx = id_index(id);
match id_tag(id) {
#[cfg(feature = "network-tcp")]
TAG_TCP => idx < MAX_TCP_CLIENTS && self.tcp_slots[idx].occupied,
#[cfg(feature = "serial-rtu")]
TAG_SERIAL_RTU => idx < MAX_SERIAL_CLIENTS && self.serial_rtu_slots[idx].occupied,
#[cfg(feature = "serial-ascii")]
TAG_SERIAL_ASCII => idx < MAX_SERIAL_CLIENTS && self.serial_ascii_slots[idx].occupied,
_ => false,
}
}
}
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
struct SyncPool(UnsafeCell<Pool>);
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
unsafe impl Sync for SyncPool {}
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
static POOL: SyncPool = SyncPool(UnsafeCell::new(Pool::new()));
#[cfg(feature = "network-tcp")]
pub(super) fn pool_allocate_tcp(inner: TcpInner) -> Result<MbusClientId, MbusStatusCode> {
let _guard = PoolLockGuard::new();
let pool = unsafe { &mut *POOL.0.get() };
pool.allocate_tcp(inner)
.ok_or(MbusStatusCode::MbusErrPoolFull)
}
#[cfg(feature = "serial-rtu")]
pub(super) fn pool_allocate_serial_rtu(
inner: SerialRtuInner,
) -> Result<MbusClientId, MbusStatusCode> {
let _guard = PoolLockGuard::new();
let pool = unsafe { &mut *POOL.0.get() };
pool.allocate_serial_rtu(inner)
.ok_or(MbusStatusCode::MbusErrPoolFull)
}
#[cfg(feature = "serial-ascii")]
pub(super) fn pool_allocate_serial_ascii(
inner: SerialAsciiInner,
) -> Result<MbusClientId, MbusStatusCode> {
let _guard = PoolLockGuard::new();
let pool = unsafe { &mut *POOL.0.get() };
pool.allocate_serial_ascii(inner)
.ok_or(MbusStatusCode::MbusErrPoolFull)
}
#[cfg(any(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
pub(super) fn pool_free(id: MbusClientId) -> bool {
let _client_guard = ClientLockGuard::new(id);
let _guard = PoolLockGuard::new();
let pool = unsafe { &mut *POOL.0.get() };
pool.free(id)
}
#[cfg(feature = "network-tcp")]
pub(super) fn with_tcp_client<F, R>(id: MbusClientId, f: F) -> Result<R, MbusStatusCode>
where
F: FnOnce(&mut TcpInner) -> R,
{
if !is_tcp_id(id) {
return Err(MbusStatusCode::MbusErrClientTypeMismatch);
}
let _guard = ClientLockGuard::new(id);
let pool = unsafe { &mut *POOL.0.get() };
if !pool.is_occupied(id) {
return Err(MbusStatusCode::MbusErrInvalidClientId);
}
let idx = id_index(id);
let slot = &mut pool.tcp_slots[idx];
if slot.borrow_flag.swap(true, Ordering::SeqCst) {
return Err(MbusStatusCode::MbusErrBusy);
}
let _borrow = BorrowGuard::new(&slot.borrow_flag);
let inner = unsafe { slot.value.assume_init_mut() };
Ok(f(inner))
}
#[cfg(any(feature = "serial-rtu", feature = "serial-ascii"))]
macro_rules! dispatch_serial {
($id:expr, $pool:expr, $slots:ident, $f:expr) => {{
let idx = id_index($id);
let slot = &mut $pool.$slots[idx];
if slot.borrow_flag.swap(true, Ordering::SeqCst) {
return Err(MbusStatusCode::MbusErrBusy);
}
let _borrow = BorrowGuard::new(&slot.borrow_flag);
let inner = unsafe { slot.value.assume_init_mut() };
Ok($f(inner))
}};
}
#[cfg(all(feature = "serial-rtu", feature = "serial-ascii"))]
pub(super) fn with_serial_client<F1, F2, R>(
id: MbusClientId,
f_rtu: F1,
f_ascii: F2,
) -> Result<R, MbusStatusCode>
where
F1: FnOnce(&mut SerialRtuInner) -> R,
F2: FnOnce(&mut SerialAsciiInner) -> R,
{
if !is_serial_id(id) {
return Err(MbusStatusCode::MbusErrClientTypeMismatch);
}
let _guard = ClientLockGuard::new(id);
let pool = unsafe { &mut *POOL.0.get() };
if !pool.is_occupied(id) {
return Err(MbusStatusCode::MbusErrInvalidClientId);
}
if is_serial_rtu_id(id) {
dispatch_serial!(id, pool, serial_rtu_slots, f_rtu)
} else {
dispatch_serial!(id, pool, serial_ascii_slots, f_ascii)
}
}
#[cfg(all(feature = "serial-rtu", not(feature = "serial-ascii")))]
pub(super) fn with_serial_client<F1, F2, R>(
id: MbusClientId,
f_rtu: F1,
_f_ascii: F2,
) -> Result<R, MbusStatusCode>
where
F1: FnOnce(&mut SerialRtuInner) -> R,
F2: FnOnce(&mut SerialRtuInner) -> R,
{
if !is_serial_id(id) {
return Err(MbusStatusCode::MbusErrClientTypeMismatch);
}
let _guard = ClientLockGuard::new(id);
let pool = unsafe { &mut *POOL.0.get() };
if !pool.is_occupied(id) {
return Err(MbusStatusCode::MbusErrInvalidClientId);
}
dispatch_serial!(id, pool, serial_rtu_slots, f_rtu)
}
#[cfg(all(feature = "serial-ascii", not(feature = "serial-rtu")))]
pub(super) fn with_serial_client<F1, F2, R>(
id: MbusClientId,
_f_rtu: F1,
f_ascii: F2,
) -> Result<R, MbusStatusCode>
where
F1: FnOnce(&mut SerialAsciiInner) -> R,
F2: FnOnce(&mut SerialAsciiInner) -> R,
{
if !is_serial_id(id) {
return Err(MbusStatusCode::MbusErrClientTypeMismatch);
}
let _guard = ClientLockGuard::new(id);
let pool = unsafe { &mut *POOL.0.get() };
if !pool.is_occupied(id) {
return Err(MbusStatusCode::MbusErrInvalidClientId);
}
dispatch_serial!(id, pool, serial_ascii_slots, f_ascii)
}
#[cfg(any(feature = "serial-rtu", feature = "serial-ascii"))]
macro_rules! with_serial_client_uniform {
($id:expr, |$inner:ident| $body:expr) => {
$crate::c::client::pool::with_serial_client($id, |$inner| $body, |$inner| $body)
};
}
#[cfg(any(feature = "serial-rtu", feature = "serial-ascii"))]
pub(super) use with_serial_client_uniform;
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(feature = "network-tcp")]
fn tcp_id_encoding() {
let id = encode_id(TAG_TCP, 0);
assert_eq!(id, 0x0000);
assert!(is_tcp_id(id));
assert!(!is_serial_id(id));
let id = encode_id(TAG_TCP, 5);
assert_eq!(id, 0x0005);
assert!(is_tcp_id(id));
assert!(!is_serial_id(id));
}
#[test]
#[cfg(feature = "serial-rtu")]
fn serial_rtu_id_encoding() {
let id = encode_id(TAG_SERIAL_RTU, 0);
assert_eq!(id, 0x0100);
#[cfg(any(feature = "serial-rtu", feature = "serial-ascii"))]
assert!(is_serial_id(id));
assert!(is_serial_rtu_id(id));
#[cfg(feature = "serial-ascii")]
assert!(!is_serial_ascii_id(id));
#[cfg(feature = "network-tcp")]
assert!(!is_tcp_id(id));
let id = encode_id(TAG_SERIAL_RTU, 5);
assert_eq!(id, 0x0105);
#[cfg(any(feature = "serial-rtu", feature = "serial-ascii"))]
assert!(is_serial_id(id));
assert!(is_serial_rtu_id(id));
}
#[test]
#[cfg(feature = "serial-ascii")]
fn serial_ascii_id_encoding() {
let id = encode_id(TAG_SERIAL_ASCII, 0);
assert_eq!(id, 0x0200);
#[cfg(any(feature = "serial-rtu", feature = "serial-ascii"))]
assert!(is_serial_id(id));
assert!(is_serial_ascii_id(id));
#[cfg(feature = "serial-rtu")]
assert!(!is_serial_rtu_id(id));
#[cfg(feature = "network-tcp")]
assert!(!is_tcp_id(id));
let id = encode_id(TAG_SERIAL_ASCII, 3);
assert_eq!(id, 0x0203);
#[cfg(any(feature = "serial-rtu", feature = "serial-ascii"))]
assert!(is_serial_id(id));
assert!(is_serial_ascii_id(id));
}
#[test]
#[cfg(feature = "network-tcp")]
fn tcp_index_roundtrip() {
for i in 0..MAX_TCP_CLIENTS {
let id = encode_id(TAG_TCP, i);
assert_eq!(id_index(id), i, "roundtrip failed for tcp index {i}");
}
}
#[test]
#[cfg(feature = "serial-rtu")]
fn serial_rtu_index_roundtrip() {
for i in 0..MAX_SERIAL_CLIENTS {
let id = encode_id(TAG_SERIAL_RTU, i);
assert_eq!(id_index(id), i, "roundtrip failed for rtu index {i}");
}
}
#[test]
#[cfg(feature = "serial-ascii")]
fn serial_ascii_index_roundtrip() {
for i in 0..MAX_SERIAL_CLIENTS {
let id = encode_id(TAG_SERIAL_ASCII, i);
assert_eq!(id_index(id), i, "roundtrip failed for ascii index {i}");
}
}
#[test]
fn invalid_id_is_neither_tcp_nor_serial() {
#[cfg(feature = "network-tcp")]
assert!(!is_tcp_id(MBUS_INVALID_CLIENT_ID));
#[cfg(any(feature = "serial-rtu", feature = "serial-ascii"))]
assert!(!is_serial_id(MBUS_INVALID_CLIENT_ID));
#[cfg(feature = "serial-rtu")]
assert!(!is_serial_rtu_id(MBUS_INVALID_CLIENT_ID));
#[cfg(feature = "serial-ascii")]
assert!(!is_serial_ascii_id(MBUS_INVALID_CLIENT_ID));
}
fn make_pool() -> Pool {
Pool::new()
}
#[test]
fn pool_starts_fully_empty() {
let pool = make_pool();
#[cfg(feature = "network-tcp")]
for i in 0..MAX_TCP_CLIENTS {
assert!(!pool.tcp_slots[i].occupied);
}
#[cfg(feature = "serial-rtu")]
for i in 0..MAX_SERIAL_CLIENTS {
assert!(!pool.serial_rtu_slots[i].occupied);
}
#[cfg(feature = "serial-ascii")]
for i in 0..MAX_SERIAL_CLIENTS {
assert!(!pool.serial_ascii_slots[i].occupied);
}
}
#[test]
fn is_occupied_rejects_invalid_id() {
let pool = make_pool();
assert!(!pool.is_occupied(MBUS_INVALID_CLIENT_ID));
#[cfg(feature = "network-tcp")]
assert!(!pool.is_occupied(encode_id(TAG_TCP, 0)));
#[cfg(feature = "serial-rtu")]
assert!(!pool.is_occupied(encode_id(TAG_SERIAL_RTU, 0)));
#[cfg(feature = "serial-ascii")]
assert!(!pool.is_occupied(encode_id(TAG_SERIAL_ASCII, 0)));
}
#[test]
fn free_of_unoccupied_slot_returns_false() {
let mut pool = make_pool();
#[cfg(feature = "network-tcp")]
assert!(!pool.free(encode_id(TAG_TCP, 0)));
#[cfg(feature = "serial-rtu")]
assert!(!pool.free(encode_id(TAG_SERIAL_RTU, 0)));
#[cfg(feature = "serial-ascii")]
assert!(!pool.free(encode_id(TAG_SERIAL_ASCII, 0)));
assert!(!pool.free(MBUS_INVALID_CLIENT_ID));
}
#[test]
#[cfg(feature = "network-tcp")]
fn free_out_of_bounds_id_returns_false() {
let mut pool = make_pool();
let oob = encode_id(TAG_TCP, MAX_TCP_CLIENTS);
let result = pool.free(oob);
assert!(!result || !pool.is_occupied(oob));
}
#[test]
#[cfg(feature = "network-tcp")]
fn with_tcp_client_rejects_serial_id() {
assert!(!is_tcp_id(encode_id(TAG_SERIAL_RTU, 0)));
assert!(!is_tcp_id(encode_id(TAG_SERIAL_ASCII, 0)));
}
#[test]
#[cfg(any(feature = "serial-rtu", feature = "serial-ascii"))]
fn with_serial_client_rejects_tcp_id() {
assert!(!is_serial_id(encode_id(TAG_TCP, 0)));
}
#[test]
#[cfg(feature = "network-tcp")]
fn tcp_pool_capacity_boundary() {
if MAX_TCP_CLIENTS > 0 {
let last = encode_id(TAG_TCP, MAX_TCP_CLIENTS - 1);
assert!(is_tcp_id(last));
#[cfg(any(feature = "serial-rtu", feature = "serial-ascii"))]
assert!(!is_serial_id(last));
assert_ne!(last, MBUS_INVALID_CLIENT_ID);
}
}
#[test]
#[cfg(feature = "serial-rtu")]
fn serial_rtu_pool_capacity_boundary() {
if MAX_SERIAL_CLIENTS > 0 {
let last = encode_id(TAG_SERIAL_RTU, MAX_SERIAL_CLIENTS - 1);
#[cfg(any(feature = "serial-rtu", feature = "serial-ascii"))]
assert!(is_serial_id(last));
assert!(is_serial_rtu_id(last));
#[cfg(feature = "network-tcp")]
assert!(!is_tcp_id(last));
assert_ne!(last, MBUS_INVALID_CLIENT_ID);
}
}
#[test]
#[cfg(feature = "serial-ascii")]
fn serial_ascii_pool_capacity_boundary() {
if MAX_SERIAL_CLIENTS > 0 {
let last = encode_id(TAG_SERIAL_ASCII, MAX_SERIAL_CLIENTS - 1);
#[cfg(any(feature = "serial-rtu", feature = "serial-ascii"))]
assert!(is_serial_id(last));
assert!(is_serial_ascii_id(last));
#[cfg(feature = "network-tcp")]
assert!(!is_tcp_id(last));
assert_ne!(last, MBUS_INVALID_CLIENT_ID);
}
}
#[test]
#[cfg(all(
feature = "network-tcp",
feature = "serial-rtu",
feature = "serial-ascii"
))]
fn tcp_rtu_ascii_id_spaces_do_not_overlap() {
for ti in 0..MAX_TCP_CLIENTS {
for si in 0..MAX_SERIAL_CLIENTS {
let tcp = encode_id(TAG_TCP, ti);
let rtu = encode_id(TAG_SERIAL_RTU, si);
let ascii = encode_id(TAG_SERIAL_ASCII, si);
assert_ne!(tcp, rtu, "TCP/RTU collision at tcp={ti} rtu={si}");
assert_ne!(tcp, ascii, "TCP/ASCII collision at tcp={ti} ascii={si}");
assert_ne!(rtu, ascii, "RTU/ASCII collision at rtu={si} ascii={si}");
}
}
}
#[test]
#[cfg(feature = "network-tcp")]
fn tcp_pool_all_slots_start_unoccupied() {
let pool = Pool::new();
assert!(pool.tcp_slots.iter().all(|s| !s.occupied));
}
#[test]
#[cfg(feature = "network-tcp")]
fn tcp_pool_fill_then_all_occupied() {
let mut pool = Pool::new();
for slot in pool.tcp_slots.iter_mut() {
slot.occupied = true;
}
for i in 0..MAX_TCP_CLIENTS {
assert!(
pool.is_occupied(encode_id(TAG_TCP, i)),
"slot {i} should be occupied"
);
}
}
#[test]
#[cfg(feature = "network-tcp")]
fn tcp_free_marks_slot_unoccupied() {
let mut pool = Pool::new();
pool.tcp_slots[0].occupied = true;
assert!(pool.is_occupied(encode_id(TAG_TCP, 0)));
assert!(pool.free(encode_id(TAG_TCP, 0)));
assert!(!pool.is_occupied(encode_id(TAG_TCP, 0)));
}
#[test]
#[cfg(feature = "serial-rtu")]
fn serial_rtu_free_marks_slot_unoccupied() {
let mut pool = Pool::new();
pool.serial_rtu_slots[0].occupied = true;
assert!(pool.is_occupied(encode_id(TAG_SERIAL_RTU, 0)));
assert!(pool.free(encode_id(TAG_SERIAL_RTU, 0)));
assert!(!pool.is_occupied(encode_id(TAG_SERIAL_RTU, 0)));
}
#[test]
#[cfg(feature = "serial-ascii")]
fn serial_ascii_free_marks_slot_unoccupied() {
let mut pool = Pool::new();
pool.serial_ascii_slots[0].occupied = true;
assert!(pool.is_occupied(encode_id(TAG_SERIAL_ASCII, 0)));
assert!(pool.free(encode_id(TAG_SERIAL_ASCII, 0)));
assert!(!pool.is_occupied(encode_id(TAG_SERIAL_ASCII, 0)));
}
#[test]
#[cfg(feature = "serial-rtu")]
fn free_clears_borrow_flag() {
let mut pool = Pool::new();
pool.serial_rtu_slots[0].occupied = true;
pool.serial_rtu_slots[0]
.borrow_flag
.store(true, Ordering::SeqCst);
pool.free(encode_id(TAG_SERIAL_RTU, 0));
assert!(!pool.serial_rtu_slots[0].borrow_flag.load(Ordering::SeqCst));
}
#[test]
#[cfg(feature = "serial-rtu")]
fn double_free_returns_false() {
let mut pool = Pool::new();
pool.serial_rtu_slots[0].occupied = true;
assert!(pool.free(encode_id(TAG_SERIAL_RTU, 0)));
assert!(!pool.free(encode_id(TAG_SERIAL_RTU, 0)));
}
#[test]
fn all_slots_free_after_full_fill_and_clear() {
let mut pool = Pool::new();
#[cfg(feature = "network-tcp")]
for slot in pool.tcp_slots.iter_mut() {
slot.occupied = true;
}
#[cfg(feature = "serial-rtu")]
for slot in pool.serial_rtu_slots.iter_mut() {
slot.occupied = true;
}
#[cfg(feature = "serial-ascii")]
for slot in pool.serial_ascii_slots.iter_mut() {
slot.occupied = true;
}
#[cfg(feature = "network-tcp")]
for i in 0..MAX_TCP_CLIENTS {
assert!(pool.free(encode_id(TAG_TCP, i)));
}
#[cfg(feature = "serial-rtu")]
for i in 0..MAX_SERIAL_CLIENTS {
assert!(pool.free(encode_id(TAG_SERIAL_RTU, i)));
}
#[cfg(feature = "serial-ascii")]
for i in 0..MAX_SERIAL_CLIENTS {
assert!(pool.free(encode_id(TAG_SERIAL_ASCII, i)));
}
#[cfg(feature = "network-tcp")]
for i in 0..MAX_TCP_CLIENTS {
assert!(!pool.is_occupied(encode_id(TAG_TCP, i)));
}
#[cfg(feature = "serial-rtu")]
for i in 0..MAX_SERIAL_CLIENTS {
assert!(!pool.is_occupied(encode_id(TAG_SERIAL_RTU, i)));
}
#[cfg(feature = "serial-ascii")]
for i in 0..MAX_SERIAL_CLIENTS {
assert!(!pool.is_occupied(encode_id(TAG_SERIAL_ASCII, i)));
}
}
}