1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
// SPDX-License-Identifier: MIT OR Apache-2.0
//!
//! Passive bus observer — attached to the physical transport via
//! `SniffIo`.
//!
//! Unlike higher-level abstractions, `WireTap` knows nothing about Modbus,
//! slave addresses, or protocol framing. It sees only bytes moving across
//! the wire and the microseconds at which they moved — exactly like a
//! hardware logic analyzer clipped onto an RS-485 bus.
//!
//! ## Design
//!
//! Hooks fire at the I/O boundary (immediately after `poll_read` / `poll_write`
//! return) so timestamps reflect real wire activity. The tap is driven
//! exclusively by `SniffIo` — neither the client nor the server
//! participates in capture.
//!
//! ## Consumers
//!
//! Implement this trait to build recording backends:
//! - [`BusCapture`](crate::BusCapture) — in-memory buffer + statistics
//! - [`RingBufferCapture`](crate::monitor::RingBufferCapture) — bounded / unbounded ring
//! - [`ChannelRecorder`](crate::monitor::ChannelRecorder) + [`RecordSink`](crate::monitor::RecordSink) — async dispatch
//! - [`FileRecorder`](crate::monitor::FileRecorder) — disk logging
//! - [`TrafficStats`](crate::TrafficStats) — lightweight counters
/// Passive observer attached to the physical transport.
///
/// Every method has a default no-op implementation — implement only the
/// hooks you need. Attach via
/// [`ClientOptions::with_tap`](crate::ClientOptions::with_tap) or
/// [`ClientOptions::with_tap`](crate::ClientOptions::with_tap).
///
/// # Custom implementation
///
/// ```no_run
/// use oms_modbus::*;
/// use std::sync::atomic::{AtomicU64, Ordering};
///
/// struct ByteCounter {
/// tx: AtomicU64,
/// rx: AtomicU64,
/// }
///
/// impl WireTap for ByteCounter {
/// fn on_write(&self, bytes: &[u8], _ts: u64) {
/// self.tx.fetch_add(bytes.len() as u64, Ordering::Relaxed);
/// }
/// fn on_read(&self, bytes: &[u8], _ts: u64) {
/// self.rx.fetch_add(bytes.len() as u64, Ordering::Relaxed);
/// }
/// // on_error has a default no-op — not implemented here
/// }
///
/// let tap = std::sync::Arc::new(ByteCounter { tx: 0.into(), rx: 0.into() });
/// let opts = ClientOptions::default().with_tap(tap);
/// ```
///
/// ## Timestamps
///
/// `timestamp_us` is microseconds since Unix epoch — hybrid `Instant` +
/// `SystemTime` clock. Real wall-clock time, formatted as ISO 8601 by
/// [`format_timestamp`](crate::format_timestamp).
/// Microsecond timestamp since Unix epoch — real wall-clock time.
///
/// # Performance
///
/// On first call, captures both `Instant::now()` and `SystemTime::now()`
/// to compute a fixed epoch-offset. All subsequent calls use only
/// `Instant::elapsed()` + offset — one monotonic clock read, no syscall.
/// At Modbus rates (≤1000 exchanges/sec), the overhead is invisible.
///
/// # Precision
///
/// The absolute value is meaningful: `PacketRecord::Display` and
/// `FileRecorder` format it as human-readable date/time. Monotonic
/// within a process lifetime — immune to system clock adjustments.