use super::config::{FaultTypeConfig, PartialFrameMode};
use super::stats::{FaultStats, FaultStatsSnapshot};
use super::targeting::FaultTarget;
use super::{FaultAction, ModbusFault, ModbusFaultContext, TransportKind};
pub struct PartialFrameFault {
mode: PartialFrameMode,
byte_count: usize,
percentage: f64,
target: FaultTarget,
stats: FaultStats,
}
impl PartialFrameFault {
pub fn new(mode: PartialFrameMode, target: FaultTarget) -> Self {
Self {
mode,
byte_count: 2,
percentage: 0.5,
target,
stats: FaultStats::new(),
}
}
pub fn with_byte_count(mut self, n: usize) -> Self {
self.byte_count = n;
self
}
pub fn with_percentage(mut self, p: f64) -> Self {
self.percentage = p.clamp(0.0, 1.0);
self
}
pub fn from_config(config: &FaultTypeConfig, target: FaultTarget) -> Self {
Self {
mode: config.partial_mode.unwrap_or(PartialFrameMode::UpToFc),
byte_count: config.partial_bytes.unwrap_or(2),
percentage: config.partial_percentage.unwrap_or(0.5),
target,
stats: FaultStats::new(),
}
}
fn build_partial(&self, unit_id: u8, pdu: &[u8]) -> Vec<u8> {
let mut full_frame = Vec::with_capacity(1 + pdu.len() + 2);
full_frame.push(unit_id);
full_frame.extend_from_slice(pdu);
let crc = compute_crc(&full_frame);
full_frame.push((crc & 0xFF) as u8);
full_frame.push((crc >> 8) as u8);
let total_len = full_frame.len();
let keep = match self.mode {
PartialFrameMode::FixedCount => self.byte_count.min(total_len).max(1),
PartialFrameMode::Percentage => ((total_len as f64 * self.percentage).ceil() as usize)
.max(1)
.min(total_len),
PartialFrameMode::UpToFc => {
2.min(total_len)
}
PartialFrameMode::UpToData => {
if total_len > 2 {
total_len - 2
} else {
total_len
}
}
};
full_frame.truncate(keep);
full_frame
}
}
impl ModbusFault for PartialFrameFault {
fn fault_type(&self) -> &'static str {
"partial_frame"
}
fn is_enabled(&self) -> bool {
self.stats.is_enabled()
}
fn set_enabled(&self, enabled: bool) {
self.stats.set_enabled(enabled);
}
fn should_activate(&self, ctx: &ModbusFaultContext) -> bool {
self.stats.record_check();
self.target.should_activate(ctx.unit_id, ctx.function_code)
}
fn apply(&self, ctx: &ModbusFaultContext) -> FaultAction {
self.stats.record_activation();
self.stats.record_affected();
let partial = self.build_partial(ctx.unit_id, &ctx.response_pdu);
FaultAction::SendPartial { bytes: partial }
}
fn stats(&self) -> FaultStatsSnapshot {
self.stats.snapshot()
}
fn reset_stats(&self) {
self.stats.reset();
}
fn is_short_circuit(&self) -> bool {
true
}
fn compatible_transport(&self) -> Option<TransportKind> {
Some(TransportKind::Rtu)
}
}
fn compute_crc(data: &[u8]) -> u16 {
let mut crc: u16 = 0xFFFF;
for &byte in data {
crc ^= byte as u16;
for _ in 0..8 {
if crc & 0x0001 != 0 {
crc = (crc >> 1) ^ 0xA001;
} else {
crc >>= 1;
}
}
}
crc
}
#[cfg(test)]
mod tests {
use super::*;
fn rtu_ctx() -> ModbusFaultContext {
ModbusFaultContext::rtu(
1,
0x03,
&[0x03, 0x00, 0x00, 0x00, 0x01],
&[0x03, 0x02, 0x00, 0x64],
1,
)
}
#[test]
fn test_up_to_fc() {
let fault = PartialFrameFault::new(PartialFrameMode::UpToFc, FaultTarget::new());
let action = fault.apply(&rtu_ctx());
match action {
FaultAction::SendPartial { bytes } => {
assert_eq!(bytes.len(), 2);
assert_eq!(bytes[0], 1); assert_eq!(bytes[1], 0x03); }
_ => panic!("Expected SendPartial"),
}
}
#[test]
fn test_up_to_data() {
let fault = PartialFrameFault::new(PartialFrameMode::UpToData, FaultTarget::new());
let action = fault.apply(&rtu_ctx());
match action {
FaultAction::SendPartial { bytes } => {
assert_eq!(bytes.len(), 5);
assert_eq!(bytes[0], 1); assert_eq!(bytes[1], 0x03); assert_eq!(bytes[2], 0x02); assert_eq!(bytes[3], 0x00); assert_eq!(bytes[4], 0x64); }
_ => panic!("Expected SendPartial"),
}
}
#[test]
fn test_fixed_count() {
let fault = PartialFrameFault::new(PartialFrameMode::FixedCount, FaultTarget::new())
.with_byte_count(3);
let action = fault.apply(&rtu_ctx());
match action {
FaultAction::SendPartial { bytes } => {
assert_eq!(bytes.len(), 3);
assert_eq!(bytes[0], 1); assert_eq!(bytes[1], 0x03); assert_eq!(bytes[2], 0x02); }
_ => panic!("Expected SendPartial"),
}
}
#[test]
fn test_fixed_count_minimum() {
let fault = PartialFrameFault::new(PartialFrameMode::FixedCount, FaultTarget::new())
.with_byte_count(0);
let action = fault.apply(&rtu_ctx());
match action {
FaultAction::SendPartial { bytes } => {
assert_eq!(bytes.len(), 1); }
_ => panic!("Expected SendPartial"),
}
}
#[test]
fn test_percentage() {
let fault = PartialFrameFault::new(PartialFrameMode::Percentage, FaultTarget::new())
.with_percentage(0.5);
let action = fault.apply(&rtu_ctx());
match action {
FaultAction::SendPartial { bytes } => {
assert_eq!(bytes.len(), 4);
}
_ => panic!("Expected SendPartial"),
}
}
#[test]
fn test_is_short_circuit() {
let fault = PartialFrameFault::new(PartialFrameMode::UpToFc, FaultTarget::new());
assert!(fault.is_short_circuit());
}
#[test]
fn test_rtu_only() {
let fault = PartialFrameFault::new(PartialFrameMode::UpToFc, FaultTarget::new());
assert_eq!(fault.compatible_transport(), Some(TransportKind::Rtu));
}
#[test]
fn test_stats() {
let fault = PartialFrameFault::new(PartialFrameMode::UpToFc, FaultTarget::new());
let ctx = rtu_ctx();
assert!(fault.should_activate(&ctx));
fault.apply(&ctx);
let stats = fault.stats();
assert_eq!(stats.checks, 1);
assert_eq!(stats.activations, 1);
assert_eq!(stats.affected_requests, 1);
}
#[test]
fn test_from_config() {
let config = FaultTypeConfig {
partial_mode: Some(PartialFrameMode::FixedCount),
partial_bytes: Some(4),
..Default::default()
};
let fault = PartialFrameFault::from_config(&config, FaultTarget::new());
let action = fault.apply(&rtu_ctx());
match action {
FaultAction::SendPartial { bytes } => {
assert_eq!(bytes.len(), 4);
}
_ => panic!("Expected SendPartial"),
}
}
}