SatCat5
eth_interface.cc
1 // Copyright 2024-2025 The Aerospace Corporation.
3 // This file is a part of SatCat5, licensed under CERN-OHL-W v2 or later.
5 
6 #include <ctime>
7 #include <hal_test/eth_interface.h>
8 #include <hal_test/sim_utils.h>
9 #include <satcat5/log.h>
10 
15 using satcat5::log::Log;
17 using satcat5::ptp::Time;
20 
21 // Set debugging verbosity level (0/1/2)
22 static constexpr unsigned DEBUG_VERBOSE = 0;
23 
24 EthernetInterface::EthernetInterface(Writeable* pcap)
25  : satcat5::ptp::Interface()
26  , ArrayWrite(m_txbuff, sizeof(m_txbuff))
27  , ReadableRedirect(&m_rxbuff_data)
28  , m_txpcap(pcap)
29  , m_txbuff_data(0)
30  , m_txbuff_time(0)
31  , m_rxbuff_data()
32  , m_rxbuff_time()
33  , m_time_rx(TIME_ZERO)
34  , m_time_tx0(TIME_ZERO)
35  , m_time_tx1(TIME_ZERO)
36  , m_tx_count(0)
37  , m_rx_count(0)
38  , m_zero_pad(60)
39  , m_support_one_step(true)
40  , m_loss_threshold(0)
41 {
42  // Configure callback for incoming packets.
43  m_rxbuff_data.set_callback(this);
44 }
45 
47 {
48  // Forward data to the destination's primary receive buffer.
49  // Keep a pointer to the side-channel buffer for timestamps.
50  m_txbuff_data = &dst->m_rxbuff_data;
51  m_txbuff_time = &dst->m_rxbuff_time;
52 }
53 
55 {
56  // In one step mode, this sets the effective timestamp.
57  if (DEBUG_VERBOSE > 1) Log(DEBUG, "EthInterface::ptp_tx_start");
58  m_time_tx0 = m_support_one_step ? ptp_time_now() : TIME_ZERO;
59  return m_time_tx0;
60 }
61 
63 {
64  // Normal redirect forwards set_callback(...) directly to the source:
65  // * Source request_poll() -> Destination data_rcvd()
66  // This class must override to intercept data_rcvd() callbacks:
67  // * Source request_poll() -> Local data_rcvd()
68  // * Local data_rcvd() -> Destination data_rcvd()
69  Readable::set_callback(callback);
70 }
71 
73 {
74  if (DEBUG_VERBOSE > 1) Log(DEBUG, "EthInterface::read_finalize");
75 
76  // Forward the event to both sources simultaneously.
77  m_rxbuff_data.read_finalize();
78  m_rxbuff_time.read_finalize();
79 
80  // Clear receive timestamp, and read the next one if possible.
81  m_time_rx = TIME_ZERO;
82  read_begin_packet();
83 }
84 
86 {
87  if (DEBUG_VERBOSE > 1) Log(DEBUG, "EthInterface::write_finalize");
88 
89  // Enable randomized packet loss?
90  bool drop = false;
91  if (m_loss_threshold == UINT32_MAX) {
92  // Special case for 100% loss rate.
93  drop = true;
94  } else if (m_loss_threshold > 0) {
95  // Randomly drop packet if die-roll is under threshold.
96  drop = (satcat5::test::rand_u32() < m_loss_threshold);
97  }
98 
99  if (drop) {
100  // Drop this packet. Since we're simulating an event where it's
101  // sent, but dropped in transit, the result is still "success".
102  if (DEBUG_VERBOSE > 0) Log(DEBUG, "EthInterface: Dropped packet.");
103  ArrayWrite::write_abort();
104  return true;
105  }
106 
107  // Intercepted end-of-packet event.
108  // Attempt to finalize the data queue first...
109  if (ArrayWrite::write_finalize()) {
110  // Update packet statistics.
111  ++m_tx_count;
112  // Use one-step pre-timestamp if it exists, otherwise current time.
113  // In either case, clear the pre-timestamp for next time around.
114  m_time_tx1 = (m_time_tx0 == TIME_ZERO) ? ptp_time_now() : m_time_tx0;
115  m_time_tx0 = TIME_ZERO;
116  // Pad to minimum Ethernet frame length = 60 bytes by default.
117  unsigned wrlen = written_len();
118  while (wrlen < m_zero_pad) m_txbuff[wrlen++] = 0;
119  // Copy data and/or timestamps to each enabled destination.
120  // TODO: Is it possible to gracefully handle desync errors?
121  bool desync = false;
122  if (m_txpcap) {
123  m_txpcap->write_bytes(wrlen, m_txbuff);
124  if (!m_txpcap->write_finalize()) desync = true;
125  }
126  if (m_txbuff_data) {
127  m_txbuff_data->write_bytes(wrlen, m_txbuff);
128  if (!m_txbuff_data->write_finalize()) desync = true;
129  }
130  if (m_txbuff_time) {
131  m_txbuff_time->write_obj(m_time_tx1);
132  m_txbuff_time->write_finalize();
133  }
134  if (desync) Log(satcat5::log::CRITICAL, "EthInterface: Desync");
135  return true;
136  } else {
137  if (DEBUG_VERBOSE > 0) Log(DEBUG, "EthInterface: Write overflow.");
138  return false;
139  }
140 }
141 
143 {
144  // PRNG generates integers in the range [0..2**32)
145  // Set threshold to achieve the desired probability.
146  if (rate <= 0.0f) {
147  m_loss_threshold = 0;
148  } else if (rate < 1.0f) {
149  m_loss_threshold = (u32)(rate * (float)UINT32_MAX);
150  } else {
151  m_loss_threshold = UINT32_MAX;
152  }
153 }
154 
156 {
157  if (DEBUG_VERBOSE > 1) Log(DEBUG, "EthInterface::data_rcvd");
158 
159  // Update packet statistics if applicable.
160  // (In rare cases, "data_rcvd" may be called twice for the same packet.)
161  read_begin_packet();
162 
163  // Forward the new-data notification to the appropriate callback.
164  unsigned peek_len = m_rxbuff_data.get_peek_ready();
165  if (ptp_dispatch(m_rxbuff_data.peek(peek_len), peek_len)) {
166  // Forward PTP notification in immediate mode.
167  if (DEBUG_VERBOSE > 0) Log(DEBUG, "EthInterface: Received PTP.");
168  ptp_notify_now();
169  } else {
170  // Forward notification to the ReadableRedirect callback.
171  // See discussion under EthernetInterface::set_callback().
172  if (DEBUG_VERBOSE > 0) Log(DEBUG, "EthInterface: Received Non-PTP.");
173  ReadableRedirect::read_notify();
174  }
175 }
176 
177 void EthernetInterface::read_begin_packet()
178 {
179  // Have we already read the timestamp for the current packet?
180  // (Don't double-count packets if data_rcvd is called twice.)
181  if (m_time_rx != TIME_ZERO) return;
182 
183  // Is there a new packet waiting in the primary receive buffer?
184  if (m_rxbuff_data.get_read_ready() == 0) return;
185  ++m_rx_count;
186 
187  // Read Rx timestamp if available, otherwise fallback to "now".
188  if (m_rxbuff_time.get_read_ready() > 0) {
189  m_rxbuff_time.read_obj(m_time_rx);
190  } else {
191  m_time_rx = ptp_time_now();
192  }
193 }
194 
195 // Read the current system time.
197 {
198  struct timespec tv;
199  int errcode = clock_gettime(CLOCK_MONOTONIC, &tv);
200  if (errcode) {
201  // Fallback to clock() function, usually millisecond resolution.
202  const u64 SCALE = 1000000000ull / CLOCKS_PER_SEC;
203  return Time(SCALE * clock());
204  } else {
205  // Higher resolution using clock_gettime(), if available.
206  return Time(tv.tv_sec, tv.tv_nsec);
207  }
208 }
209 
210 // One-liners for most recent Tx/Rx timestamps.
212  { return m_time_tx1; }
214  { return m_time_rx; }
216  { return this; }
218  { return this; }
Ephemeral Writeable interface for a simple array.
Definition: io_writeable.h:127
unsigned written_len() const
Report total length after write_finalize() is called.
Definition: io_writeable.h:151
Event-handler interface for newly received data.
Definition: io_readable.h:43
const u8 * peek(unsigned nbytes) const
Peek nbytes into the circular buffer.
Definition: pkt_buffer.cc:248
unsigned get_peek_ready() const
Find the longest available contiguous segment that can be requested by peek().
Definition: pkt_buffer.cc:242
unsigned get_read_ready() const override
How many bytes can be read without blocking?
Definition: pkt_buffer.cc:177
void read_finalize() override
Consume any remaining bytes in this frame, if applicable.
Definition: pkt_buffer.cc:266
Abstract API for reading byte-streams and packets.
Definition: io_readable.h:68
bool read_obj(T &t)
Templated wrapper for any object with the following method: bool read_from(satcat5::io::Readable* rd)...
Definition: io_readable.h:140
virtual void set_callback(satcat5::io::EventListener *callback)
Update registered callback for data_rcvd() events.
Definition: io_readable.cc:39
Wrapper class for forwarding reads to another object.
Definition: io_readable.h:299
Abstract API for writing byte-streams and packets.
Definition: io_writeable.h:24
virtual void write_bytes(unsigned nbytes, const void *src)
Write 0 or more bytes from a buffer.
virtual bool write_finalize()
Mark end of frame and release temporary working data.
void write_obj(const T &obj)
Templated wrapper for any object with the following method: void write_to(satcat5::io::Writeable* wr)...
Definition: io_writeable.h:104
The Log class creates and formats one log message.
Definition: log.h:195
bool ptp_dispatch(const u8 *peek, unsigned length)
Determine if an incoming packet is a PTP message.
void ptp_notify_now()
Notify the PTP callback object in immediate mode.
Definition: ptp_interface.h:77
High-precision timestamp for use with PTP / IEEE1588.
Definition: ptp_time.h:41
Simulation of a PTP-compatible Ethernet interface controller.
Definition: eth_interface.h:25
satcat5::io::Writeable * ptp_tx_write() override
Return an object suitable for writing the next PTP frame.
satcat5::ptp::Time ptp_time_now() override
Return the best available estimate of the current time.
void set_loss_rate(float rate)
Set rate for randomized drops of outgoing packets.
void connect(satcat5::test::EthernetInterface *dst)
Crosslink to specified destination object.
void read_finalize() override
Consume any remaining bytes in this frame, if applicable.
satcat5::ptp::Time ptp_rx_timestamp() override
Return timestamp of the current incoming message.
satcat5::ptp::Time ptp_tx_timestamp() override
Return timestamp of the most recent outgoing message.
void set_callback(satcat5::io::EventListener *callback) override
Update registered callback for data_rcvd() events.
bool write_finalize() override
Mark end of frame and release temporary working data.
satcat5::ptp::Time ptp_tx_start() override
Begin sending a timestamped message.
satcat5::io::Readable * ptp_rx_read() override
Return an object suitable for reading the next PTP frame.
void data_rcvd(satcat5::io::Readable *src) override
The data_rcvd() callback is polled whenever data is available.
Diagnostic logging to UART and/or Ethernet ports.
constexpr s8 DEBUG
Define basic priority codes for log messages.
Definition: log.h:109
constexpr satcat5::ptp::Time TIME_ZERO(0LL)
Common time-related constants.
Miscellaneous simulation and test helper functions.
u32 rand_u32()
Reproducible PRNG used for unit tests.
Definition: sim_utils.cc:42