use crate::constants::MAX_READ_REGISTERS;
pub const DEFAULT_GAP_THRESHOLD: u16 = 10;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReadRequest {
pub slave_id: u8,
pub function: u8,
pub address: u16,
pub quantity: u16,
}
impl ReadRequest {
pub fn new(slave_id: u8, function: u8, address: u16, quantity: u16) -> Self {
Self {
slave_id,
function,
address,
quantity,
}
}
#[inline]
fn end_address(&self) -> u16 {
self.address.saturating_add(self.quantity)
}
}
#[derive(Debug)]
pub struct CoalescedRead {
pub slave_id: u8,
pub function: u8,
pub address: u16,
pub quantity: u16,
pub mappings: Vec<(usize, u16, u16)>,
}
pub struct ReadCoalescer {
gap_threshold: u16,
max_registers: u16,
}
impl Default for ReadCoalescer {
fn default() -> Self {
Self::new()
}
}
impl ReadCoalescer {
pub fn new() -> Self {
Self {
gap_threshold: DEFAULT_GAP_THRESHOLD,
max_registers: MAX_READ_REGISTERS as u16,
}
}
pub fn with_gap_threshold(gap_threshold: u16) -> Self {
Self {
gap_threshold,
max_registers: MAX_READ_REGISTERS as u16,
}
}
pub fn with_config(gap_threshold: u16, max_registers: u16) -> Self {
Self {
gap_threshold,
max_registers,
}
}
pub fn coalesce(&self, requests: &[ReadRequest]) -> Vec<CoalescedRead> {
if requests.is_empty() {
return Vec::new();
}
let mut indexed: Vec<(usize, &ReadRequest)> = requests.iter().enumerate().collect();
indexed.sort_by_key(|(_, r)| (r.slave_id, r.function, r.address));
let mut result: Vec<CoalescedRead> = Vec::new();
let mut group_slave = indexed[0].1.slave_id;
let mut group_fn = indexed[0].1.function;
let mut group_start = indexed[0].1.address;
let mut group_end = indexed[0].1.end_address(); let mut group_mappings: Vec<(usize, u16, u16)> = Vec::new();
{
let (orig_idx, req) = indexed[0];
let offset = req.address - group_start;
group_mappings.push((orig_idx, offset, req.quantity));
}
for &(orig_idx, req) in &indexed[1..] {
let same_group = req.slave_id == group_slave && req.function == group_fn;
if same_group {
let new_end = req.end_address().max(group_end);
let merged_qty = new_end - group_start;
if merged_qty <= self.max_registers {
let gap = req.address.saturating_sub(group_end);
if gap <= self.gap_threshold || req.address <= group_end {
group_end = new_end;
let offset = req.address - group_start;
group_mappings.push((orig_idx, offset, req.quantity));
continue;
}
}
}
result.push(CoalescedRead {
slave_id: group_slave,
function: group_fn,
address: group_start,
quantity: group_end - group_start,
mappings: std::mem::take(&mut group_mappings),
});
group_slave = req.slave_id;
group_fn = req.function;
group_start = req.address;
group_end = req.end_address();
group_mappings.push((orig_idx, 0, req.quantity));
}
result.push(CoalescedRead {
slave_id: group_slave,
function: group_fn,
address: group_start,
quantity: group_end - group_start,
mappings: group_mappings,
});
result
}
pub fn extract_results(&self, coalesced: &CoalescedRead, data: &[u16]) -> Vec<Vec<u16>> {
coalesced
.mappings
.iter()
.map(|&(_, offset, qty)| {
let start = offset as usize;
let end = (offset + qty) as usize;
if end <= data.len() {
data[start..end].to_vec()
} else {
Vec::new()
}
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn req(slave_id: u8, function: u8, address: u16, quantity: u16) -> ReadRequest {
ReadRequest::new(slave_id, function, address, quantity)
}
#[test]
fn test_empty_requests() {
let coalescer = ReadCoalescer::new();
let result = coalescer.coalesce(&[]);
assert!(result.is_empty());
}
#[test]
fn test_single_request() {
let coalescer = ReadCoalescer::new();
let requests = vec![req(1, 0x03, 10, 5)];
let result = coalescer.coalesce(&requests);
assert_eq!(result.len(), 1);
assert_eq!(result[0].address, 10);
assert_eq!(result[0].quantity, 5);
assert_eq!(result[0].mappings.len(), 1);
assert_eq!(result[0].mappings[0], (0, 0, 5));
}
#[test]
fn test_adjacent_merge() {
let coalescer = ReadCoalescer::new();
let requests = vec![
req(1, 0x03, 0, 2), req(1, 0x03, 2, 2), ];
let result = coalescer.coalesce(&requests);
assert_eq!(result.len(), 1, "相邻请求应合并为一个");
assert_eq!(result[0].address, 0);
assert_eq!(result[0].quantity, 4);
assert_eq!(result[0].mappings.len(), 2);
let m0 = result[0].mappings.iter().find(|m| m.0 == 0).unwrap();
assert_eq!(m0.1, 0);
assert_eq!(m0.2, 2);
let m1 = result[0].mappings.iter().find(|m| m.0 == 1).unwrap();
assert_eq!(m1.1, 2);
assert_eq!(m1.2, 2);
}
#[test]
fn test_overlapping_merge() {
let coalescer = ReadCoalescer::new();
let requests = vec![
req(1, 0x03, 0, 5), req(1, 0x03, 3, 5), ];
let result = coalescer.coalesce(&requests);
assert_eq!(result.len(), 1, "重叠请求应合并为一个");
assert_eq!(result[0].address, 0);
assert_eq!(result[0].quantity, 8); }
#[test]
fn test_gap_merge() {
let coalescer = ReadCoalescer::new();
let requests = vec![
req(1, 0x03, 0, 2), req(1, 0x03, 2, 2), req(1, 0x03, 10, 2), ];
let result = coalescer.coalesce(&requests);
assert_eq!(result.len(), 1, "间隙 ≤ threshold,应合并为一个");
assert_eq!(result[0].address, 0);
assert_eq!(result[0].quantity, 12);
let m2 = result[0].mappings.iter().find(|m| m.0 == 2).unwrap();
assert_eq!(m2.1, 10);
assert_eq!(m2.2, 2);
}
#[test]
fn test_no_merge() {
let coalescer = ReadCoalescer::with_gap_threshold(5);
let requests = vec![
req(1, 0x03, 0, 2), req(1, 0x03, 10, 2), ];
let result = coalescer.coalesce(&requests);
assert_eq!(result.len(), 2, "间隙 > threshold,不应合并");
assert_eq!(result[0].address, 0);
assert_eq!(result[0].quantity, 2);
assert_eq!(result[1].address, 10);
assert_eq!(result[1].quantity, 2);
}
#[test]
fn test_max_registers_split() {
let coalescer = ReadCoalescer::with_config(100, 10);
let requests = vec![
req(1, 0x03, 0, 6), req(1, 0x03, 6, 6), ];
let result = coalescer.coalesce(&requests);
assert_eq!(result.len(), 2, "合并后超过 max_registers,应分拆");
assert_eq!(result[0].address, 0);
assert_eq!(result[0].quantity, 6);
assert_eq!(result[1].address, 6);
assert_eq!(result[1].quantity, 6);
}
#[test]
fn test_different_slaves_no_merge() {
let coalescer = ReadCoalescer::new();
let requests = vec![
req(1, 0x03, 0, 2), req(2, 0x03, 2, 2), ];
let result = coalescer.coalesce(&requests);
assert_eq!(result.len(), 2, "不同 slave_id 不应合并");
let s1 = result.iter().find(|r| r.slave_id == 1).unwrap();
let s2 = result.iter().find(|r| r.slave_id == 2).unwrap();
assert_eq!(s1.quantity, 2);
assert_eq!(s2.quantity, 2);
}
#[test]
fn test_different_functions_no_merge() {
let coalescer = ReadCoalescer::new();
let requests = vec![
req(1, 0x03, 0, 2), req(1, 0x04, 2, 2), ];
let result = coalescer.coalesce(&requests);
assert_eq!(result.len(), 2, "不同 function code 不应合并");
let fc03 = result.iter().find(|r| r.function == 0x03).unwrap();
let fc04 = result.iter().find(|r| r.function == 0x04).unwrap();
assert_eq!(fc03.quantity, 2);
assert_eq!(fc04.quantity, 2);
}
#[test]
fn test_extract_results() {
let coalescer = ReadCoalescer::new();
let merged_data: Vec<u16> = (0..12).collect();
let coalesced = CoalescedRead {
slave_id: 1,
function: 0x03,
address: 0,
quantity: 12,
mappings: vec![
(0, 0, 2), (1, 2, 2), (2, 10, 2), ],
};
let extracted = coalescer.extract_results(&coalesced, &merged_data);
assert_eq!(extracted.len(), 3);
assert_eq!(extracted[0], vec![0, 1]); assert_eq!(extracted[1], vec![2, 3]); assert_eq!(extracted[2], vec![10, 11]); }
#[test]
fn test_coalesce_full_example() {
let coalescer = ReadCoalescer::new();
let requests = vec![req(1, 0x03, 0, 2), req(1, 0x03, 2, 2), req(1, 0x03, 10, 2)];
let coalesced = coalescer.coalesce(&requests);
assert_eq!(coalesced.len(), 1);
assert_eq!(coalesced[0].address, 0);
assert_eq!(coalesced[0].quantity, 12);
assert_eq!(coalesced[0].mappings.len(), 3);
let data: Vec<u16> = (100..112).collect();
let results = coalescer.extract_results(&coalesced[0], &data);
assert_eq!(results.len(), 3);
assert_eq!(results[0], vec![100, 101]); assert_eq!(results[1], vec![102, 103]); assert_eq!(results[2], vec![110, 111]); }
#[test]
fn test_extract_results_out_of_bounds() {
let coalescer = ReadCoalescer::new();
let short_data: Vec<u16> = vec![1, 2, 3];
let coalesced = CoalescedRead {
slave_id: 1,
function: 0x03,
address: 0,
quantity: 10,
mappings: vec![
(0, 0, 2), (1, 8, 2), ],
};
let extracted = coalescer.extract_results(&coalesced, &short_data);
assert_eq!(extracted[0], vec![1, 2]); assert!(extracted[1].is_empty()); }
}