SatCat5
ccsds_aos.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 <satcat5/ccsds_aos.h>
7 #include <satcat5/ccsds_spp.h>
8 #include <satcat5/log.h>
9 #include <satcat5/utils.h>
10 
14 using satcat5::ccsds_spp::APID_IDLE;
20 using satcat5::log::Log;
21 using satcat5::net::Type;
23 
24 // Set debugging verbosity (0/1/2)
25 #define DEBUG_VERBOSE 0
26 
27 // Define fields for Dispatch::m_state:
28 static constexpr u8 STATE_PRESYNC = 0x80; // Pre-packetized input?
29 static constexpr u8 STATE_DATA = 0x40; // Header OK, ready for data?
30 static constexpr u8 STATE_SYNC = 0x07; // Count matched sync-bytes
31 
32 // Define fields for the M_PDU and B_PDU headers.
33 static constexpr u16 MPDU_MASK = 0x07FF; // First header location
34 static constexpr u16 MPDU_NONE = MPDU_MASK;
35 static constexpr u16 BPDU_MASK = 0xCFFF; // Bitstream length
36 static constexpr u16 BPDU_FULL = BPDU_MASK;
37 static constexpr u16 BPDU_NULL = BPDU_MASK - 1;
38 
39 // Min and max size for inserting SPP idle packets.
40 static constexpr unsigned MIN_FILLER = 7;
41 static constexpr unsigned MAX_FILLER = 256;
42 
43 void Header::write_to(Writeable* wr) const {
44  u8 ext = u8(count >> 24) & FRCT_VAL_MASK;
45  u8 sig = signal & (REPLAY_MASK | FRCT_EXT_MASK);
46  u32 cbo = (count << 8) | u32(sig); // Combine count + signal
47  if (signal & FRCT_EXT_MASK) cbo |= ext; // Extended frame-count?
48  wr->write_u16(id);
49  wr->write_u32(cbo);
50 }
51 
53  if (rd->get_read_ready() < 6) return false;
54  id = rd->read_u16(); // ID field is straightforward
55  count = rd->read_u24(); // Basic frame-count (24 bit)
56  signal = rd->read_u8(); // Signaling field
57  if (signal & FRCT_EXT_MASK) // Extended frame-count?
58  count += 16777216 * (signal & FRCT_VAL_MASK);
59  return true;
60 }
61 
63  u32 rollover = (signal & FRCT_EXT_MASK) ? (1u << 28) : (1u << 24);
64  count = (count + 1) & (rollover - 1);
65  return *this;
66 }
67 
69  Dispatch* iface, Readable* src, Writeable* dst,
70  u8 scid, u8 vcid, bool pkt)
71  : Protocol(satcat5::net::TYPE_NONE)
72  , m_iface(iface)
73  , m_src(src)
74  , m_dst(dst)
75  , m_rx_spp(m_rx_tmp, sizeof(m_rx_tmp))
76  , m_rx_next(scid, vcid)
77  , m_tx_next(scid, vcid)
78  , m_rx_state(pkt ? State::RESYNC : State::RAW)
79  , m_rx_rem(0)
80  , m_tx_busy(0)
81  , m_tx_irem(0)
82  , m_tx_iseq(0)
83  , m_rx_tmp{}
84 {
85  m_filter = Type(m_rx_next.id);
86  if (m_iface) m_iface->add(this);
87  if (m_src) m_src->set_callback(this);
88 }
89 
90 #if SATCAT5_ALLOW_DELETION
91 Channel::~Channel() {
92  if (m_iface) m_iface->remove(this);
93  if (m_src) m_src->set_callback(0);
94 }
95 #endif
96 
98  if (DEBUG_VERBOSE > 0)
99  Log(DEBUG, "ccsds_aos::Channel Desync");
100 
101  if (m_rx_state != State::RAW) {
102  m_rx_state = State::RESYNC;
103  m_rx_spp.write_abort();
104  m_rx_rem = 0;
105  m_dst->write_abort();
106  m_iface->error_incr();
107  }
108 }
109 
111  // Accept incoming packets from any SCID and VCID pair.
112  // Outgoing packets continue to use the constructor SCID and VCID.
113  // Set the filter to match only against the CCSDS header as a bypass.
114  m_filter = Type(u32(VERSION_2), u32(VERSION_MASK));
115 }
116 
117 // Callback for each incoming AOS transfer frame.
119  // Sanity check: Abort & discard if there's output is disabled.
120  if (!m_dst) return;
121 
122  // Read the frame and PDU headers.
123  auto frm_hdr = m_iface->rcvd_hdr();
124  u16 pdu_hdr = src.read_u16();
125 
126  // Parse the transfer frame data field...
127  if (m_rx_state == State::RAW) {
128  // Byte-stream (B_PDU) = Section 4.1.4.3
129  // TODO: Handle inputs that aren't byte-aligned?
130  if ((pdu_hdr & BPDU_MASK) == BPDU_NULL) return;
131  unsigned pdu_bits = 1 + unsigned(pdu_hdr & BPDU_MASK);
132  unsigned pdu_bytes = min_unsigned(src.get_read_ready(), pdu_bits/8);
133  // Copy valid bytes to the output buffer.
134  // (Nothing we can do if we've lost a packet.)
135  LimitedRead(&src, pdu_bytes).copy_to(m_dst);
136  bool ok = m_dst->write_finalize();
137  if (DEBUG_VERBOSE > 0 && !ok)
138  Log(DEBUG, "ccsds_aos::Channel BPDU-Overflow");
139  } else {
140  // Packet mode (M_PDU) = Section 4.1.4.2
141  unsigned first_spp = unsigned(pdu_hdr & MPDU_MASK);
142  if (first_spp == MPDU_NONE) first_spp = src.get_read_ready();
143  // Desync if we've missed data or fail a sanity check.
144  unsigned expect_spp = min_unsigned(m_rx_rem, src.get_read_ready());
145  bool bad_align = (m_rx_state == State::DATA) && (first_spp != expect_spp);
146  bool bad_count = (m_rx_next.count != frm_hdr.count);
147  if (bad_align || bad_count) desync();
148  // If resync required, discard up to the next SPP header, if any.
149  if (m_rx_state == State::RESYNC) {
150  if (first_spp) src.read_consume(first_spp);
151  }
152  // Keep reading data until input is empty or output is full...
153  while (m_dst->get_write_space() && src.get_read_ready()) {
154  // Read next SPP packet header, if we haven't already.
155  if (!read_header(&src)) break;
156  // Read SPP data up to end of SPP or AOS, whichever comes first.
157  unsigned maxrd = min_unsigned(src.get_read_ready(), m_rx_rem);
158  if (m_rx_state == State::DATA) {
159  // Copy data to output until end-of-frame.
160  m_rx_rem -= LimitedRead(&src, maxrd).copy_to(m_dst);
161  bool ok = m_rx_rem || m_dst->write_finalize();
162  if (DEBUG_VERBOSE > 0 && !ok)
163  Log(DEBUG, "ccsds_aos::Channel MPDU-Overflow");
164  } else {
165  // Skip over idle frames.
166  src.read_consume(maxrd); m_rx_rem -= maxrd;
167  }
168  }
169  // If the output buffer overflowed, desync.
170  if (src.get_read_ready()) desync();
171  }
172 
173  // Update expected header for next time.
174  m_rx_next = frm_hdr;
175  ++m_rx_next;
176 }
177 
178 // Callback for queued outgoing data.
180  // Both transfer frame formats use a two-byte header.
181  const unsigned dmax = m_iface->dsize() - 2;
182  // Keep sending transfer frame(s) until we exhaust the input...
183  while (src->get_read_ready()) {
184  // Can the output fit another transfer frame?
185  Writeable* wr = m_iface->open_write(m_tx_next);
186  if (!wr) break; // Output is full, try again later.
187  ++m_tx_next; // Increment next sequence counter.
188  // What is the format for this channel?
189  if (m_rx_state == State::RAW) {
190  // Byte-stream (B_PDU) = Section 4.1.4.3
191  // Partial frames indicate the number of valid bits.
192  unsigned nbytes = min_unsigned(src->get_read_ready(), dmax);
193  wr->write_u16((nbytes < dmax) ? u16(8*nbytes-1) : BPDU_FULL);
194  // Copy stream data, then zero-pad as needed.
195  LimitedRead(src, nbytes).copy_to(wr);
196  for (unsigned a = nbytes ; a < dmax ; ++a) wr->write_u8(0);
197  } else {
198  // Packet mode (M_PDU) = Section 4.1.4.2
199  // Write the AOS header, indicating next SPP start position.
200  if (m_tx_busy) {
201  // Continue SPP packet from previous transfer frame.
202  // Next header starts immediately after, if there's room.
203  unsigned rem = min_unsigned(dmax, src->get_read_ready());
204  wr->write_u16((rem < dmax) ? u16(rem) : MPDU_NONE);
205  } else {
206  // Start first SPP immediately or after trailing idle.
207  wr->write_u16(m_tx_irem);
208  }
209  // Trailing bytes from a split minimum-length idle packet?
210  // (We try to avoid this, but it is sometimes inevitable.)
211  satcat5::io::LimitedWrite aos(wr, dmax);
212  if (m_tx_irem) m_tx_irem = idle_filler(&aos, MIN_FILLER);
213  // Copy SPPs until input is exhausted or PDU is filled.
214  unsigned trailing = 0;
215  while (aos.get_write_space() && src->get_read_ready()) {
216  src->copy_to(&aos);
217  trailing = src->get_read_ready();
218  if (!trailing) src->read_finalize();
219  }
220  m_tx_busy = (trailing ? 1 : 0);
221  // If there's any space left, add filler packet(s) as needed.
222  // (If possible, align the pad with the transfer frame boundary.)
223  while (aos.get_write_space())
224  m_tx_irem = idle_filler(&aos, aos.get_write_space());
225  }
226  // End of AOS transfer frame.
227  bool ok = wr->write_finalize();
228  if (DEBUG_VERBOSE > 0 && !ok)
229  Log(DEBUG, "ccsds_aos::Channel Rx Overflow");
230  }
231 }
232 
233 void Channel::data_unlink(Readable* src) {m_src = 0;} // GCOVR_EXCL_LINE
234 
235 u8 Channel::idle_filler(Writeable* dst, unsigned req) {
236  // Try to align to match the trasfer-frame boundary, but
237  // clamp as needed to the supported min/max SPP length.
238  if (req < MIN_FILLER) req = MIN_FILLER;
239  if (req > MAX_FILLER) req = MAX_FILLER;
240  // Trim if the *next* filler frame would need to split.
241  unsigned rem = dst->get_write_space();
242  if (req < rem && rem < req + MIN_FILLER) req -= MIN_FILLER;
243  // Generate SPP header for the next idle packet.
245  hdr.set(false, APID_IDLE, m_tx_iseq);
246  // Write the idle packet to a temporary buffer.
247  u8 tmp[MAX_FILLER] = {0};
248  satcat5::io::ArrayWrite wr(tmp, req);
249  wr.write_u32(hdr.value); // 6-byte header + Zero-pad
250  wr.write_u16(req - 7); // Pad length - 1
251  // Copy the temporary buffer to the output, with
252  // offset for bytes written on a previous attempt.
253  unsigned skip = m_tx_irem ? (req - m_tx_irem) : 0;
254  unsigned copy = min_unsigned(req - skip, dst->get_write_space());
255  dst->write_bytes(copy, tmp + skip);
256  return u8(req - skip - copy); // Split bytes in next frame?
257 }
258 
259 bool Channel::read_header(Readable* src) {
260  // Should we start reading a new SPP header?
261  constexpr unsigned SPP_HDR_LEN = 6;
262  if (m_rx_state != State::HEADER) {
263  if (m_rx_rem) return true; // Mid-packet (DATA or SKIP)
264  m_rx_state = State::HEADER; // Start of new SPP header
265  m_rx_rem = SPP_HDR_LEN;
266  }
267  // Sanity check: Pause until there's space in the output buffer.
268  if (m_dst->get_write_space() < SPP_HDR_LEN) return false;
269  // Copy bytes to the working buffer.
270  m_rx_rem -= src->copy_to(&m_rx_spp);
271  if (m_rx_rem) return false; // Incomplete header?
272  // Parse the complete SPP header.
273  ArrayRead rd(m_rx_tmp, sizeof(m_rx_tmp));
274  satcat5::ccsds_spp::Header spp {rd.read_u32()};
275  m_rx_rem = 1 + unsigned(rd.read_u16());
276  // Is this idle filler or real data?
277  if (spp.apid() == APID_IDLE) {
278  m_rx_state = State::SKIP; // Skip idle frames.
279  } else {
280  m_rx_state = State::DATA; // Copy header + data.
281  m_dst->write_bytes(sizeof(m_rx_tmp), m_rx_tmp);
282  }
283  m_rx_spp.write_finalize(); // Reset working buffer
284  return true; // Ready to read SPP contents
285 }
286 
287 Dispatch::Dispatch(Readable* src, Writeable* dst, u8* buff, unsigned dsize, bool insert)
288  : m_dsize(dsize)
289  , m_insert(insert)
290  , m_sync_state(0)
291  , m_src(src)
292  , m_dst(dst)
293  , m_work(buff, tsize())
294  , m_crc_rx(&m_work, 0xFFFF)
295  , m_crc_tx(dst, 0xFFFF)
296 {
297  if (m_src) m_src->set_callback(this);
298 }
299 
300 #if SATCAT5_ALLOW_DELETION
301 Dispatch::~Dispatch() {
302  if (m_src) m_src->set_callback(0);
303 }
304 #endif
305 
306 // Stub required for the Dispatch API (reply mode is not supported).
307 Writeable* Dispatch::open_reply(const Type& type, unsigned len) {return 0;} // GCOVR_EXCL_LINE
308 
310  // Sanity-check that a valid output exists.
311  if (!m_dst) return 0;
312 
313  // Sanity-check available buffer before we start.
314  unsigned required = tsize() + (m_insert ? 4 : 0);
315  if (m_crc_tx.get_write_space() < required) return 0;
316 
317  // If sync headers are enabled, they bypass the CRC system.
318  if (m_insert) m_dst->write_u32(TM_SYNC_WORD);
319 
320  // Write AOS frame header. User must write data and finalize.
321  m_crc_tx.write_obj(hdr);
322  return &m_crc_tx;
323 }
324 
326  while (src->get_read_ready()) {
327  // If applicable, find and read the sync word. Then copy data
328  // from source through the CRC check up to next frame boundary.
329  if (read_sync(src) && read_data(src)) {
330  // If CRC matches, read header and deliver to indicated Channel.
331  if (m_crc_rx.write_finalize()) {
332  ArrayRead rd(m_work.buffer(), m_work.written_len());
333  rd.read_obj(m_rcvd_hdr);
334  if (m_rcvd_hdr.vcid() != VCID_IDLE)
335  deliver(Type(m_rcvd_hdr.id), &rd, dsize());
336  } else if (DEBUG_VERBOSE > 0) {
337  Log(DEBUG, "ccsds_aos::Dispatch CRC mismatch");
338  }
339  // Regardless, reset state for the next transfer frame.
340  m_sync_state = 0;
341  bool more_data = (m_insert && src->get_read_ready());
342  if (!more_data) src->read_finalize();
343  }
344  }
345 }
346 
347 void Dispatch::data_unlink(Readable* src) {m_src = 0;} // GCOVR_EXCL_LINE
348 
349 bool Dispatch::read_sync(Readable* src) {
350  // Skip this process if sync-word insertion is disabled.
351  if (!m_insert) return true;
352  // Read one byte at a time until we find a sync header.
353  constexpr unsigned SYNC_LEN = sizeof(TM_SYNC_BYTES);
354  while (m_sync_state < SYNC_LEN && src->get_read_ready()) {
355  u8 next = src->read_u8();
356  if (next == TM_SYNC_BYTES[0]) {
357  m_sync_state = 1; // Match first sync byte?
358  } else if (next == TM_SYNC_BYTES[m_sync_state]) {
359  ++m_sync_state; // Match next sync byte?
360  } else {
361  m_sync_state = 0; // No match = start over.
362  }
363  }
364  return (m_sync_state >= SYNC_LEN);
365 }
366 
367 bool Dispatch::read_data(Readable* src) {
368  // Copy as much as we can through the CRC validator.
369  // Return true if we've written a complete transfer frame.
370  return src->copy_to(&m_crc_rx) && !m_work.get_write_space();
371 }
CCSDS "Advanced Orbiting Systems" (AOS) Space Data Link Protocol.
constexpr u32 TM_SYNC_WORD
Sync-word for CCSDS "TM Synchronization and Channel Coding".
Definition: ccsds_aos.h:52
CCSDS Space Packet Protocol.
CCSDS-AOS Virtual Channel.
Definition: ccsds_aos.h:111
void frame_rcvd(satcat5::io::LimitedRead &src) override
Process each received AOS frame.
Definition: ccsds_aos.cc:118
void data_rcvd(satcat5::io::Readable *src) override
The data_rcvd() callback is polled whenever data is available.
Definition: ccsds_aos.cc:179
Channel(satcat5::ccsds_aos::Dispatch *iface, satcat5::io::Readable *src, satcat5::io::Writeable *dst, u8 scid, u8 vcid, bool pkt)
Create a virtual channel, bound to an AOS interface.
Definition: ccsds_aos.cc:68
void data_unlink(satcat5::io::Readable *src) override
Unlink this EventListener from the designated source, because the designated Readable object is being...
Definition: ccsds_aos.cc:233
void set_wildcard()
Enable wildcard mode, ignoring incoming SCID and VCID.
Definition: ccsds_aos.cc:110
void desync()
Force resynchronization after an error.
Definition: ccsds_aos.cc:97
Implemention of "net::Dispatch" API for CCSDS-AOS protocol.
Definition: ccsds_aos.h:161
void data_rcvd(satcat5::io::Readable *src) override
The data_rcvd() callback is polled whenever data is available.
Definition: ccsds_aos.cc:325
satcat5::io::Writeable * open_write(const satcat5::ccsds_aos::Header &hdr)
Write CCSDS-AOS frame header and get Writeable object.
Definition: ccsds_aos.cc:309
void data_unlink(satcat5::io::Readable *src) override
Unlink this EventListener from the designated source, because the designated Readable object is being...
Definition: ccsds_aos.cc:347
satcat5::io::Writeable * open_reply(const satcat5::net::Type &type, unsigned len) override
Open a reply to the sender of the most recent message by writing frame header(s) and returning a stre...
Definition: ccsds_aos.cc:307
unsigned tsize() const
< Total size = Header + Data
Definition: ccsds_aos.h:189
unsigned dsize() const
< Data field size
Definition: ccsds_aos.h:185
const satcat5::ccsds_aos::Header & rcvd_hdr() const
Most recent received header.
Definition: ccsds_aos.h:191
bool write_finalize() override
Mark end of frame and release temporary working data.
Ephemeral Readable interface for a simple array.
Definition: io_readable.h:206
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
bool write_finalize() override
Mark end of frame and release temporary working data.
void write_abort() override
If possible, abort the current partially-written packet.
const u8 * buffer() const
Read-only access to the working buffer.
Definition: io_writeable.h:147
unsigned get_write_space() const override
How many bytes can be written without blocking?
unsigned get_write_space() const override
How many bytes can be written without blocking?
Definition: io_checksum.h:86
Limited read of next N bytes.
Definition: io_readable.h:255
bool read_consume(unsigned nbytes) override
Read and discard 0 or more bytes.
Definition: io_readable.cc:307
unsigned get_read_ready() const override
How many bytes can be read without blocking?
Definition: io_readable.cc:293
Limited write up to N bytes.
Definition: io_writeable.h:183
unsigned get_write_space() const override
How many bytes can be written without blocking?
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
u8 read_u8()
One of many functions for reading integer/floating point values, see details.
Definition: io_readable.cc:44
virtual void read_finalize()
Consume any remaining bytes in this frame, if applicable.
Definition: io_readable.cc:270
virtual void set_callback(satcat5::io::EventListener *callback)
Update registered callback for data_rcvd() events.
Definition: io_readable.cc:39
unsigned copy_to(satcat5::io::Writeable *dst)
Copy data to a Writeable object, without finalizing.
Definition: io_readable.cc:214
virtual unsigned get_read_ready() const =0
How many bytes can be read without blocking?
Abstract API for writing byte-streams and packets.
Definition: io_writeable.h:24
virtual unsigned get_write_space() const =0
How many bytes can be written without blocking?
virtual void write_bytes(unsigned nbytes, const void *src)
Write 0 or more bytes from a buffer.
virtual void write_abort()
If possible, abort the current partially-written packet.
void write_u8(u8 data)
One of many functions for writing integer/floating point values, see details.
Definition: io_writeable.cc:20
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
void add(satcat5::net::Protocol *proto)
Register a Protocol object.
Definition: net_dispatch.h:55
Dispatch()
Constructor and destructor access restricted to children.
Definition: net_dispatch.h:68
bool deliver(const satcat5::net::Type &type, satcat5::io::Readable *src, unsigned len)
Deliver current message by calling Protocol::frame_rcvd(...).
Definition: net_dispatch.cc:25
void remove(satcat5::net::Protocol *proto)
Unregister a Protocol object.
Definition: net_dispatch.h:59
satcat5::net::Type m_filter
Incoming packet filter.
Definition: net_protocol.h:52
Diagnostic logging to UART and/or Ethernet ports.
constexpr s8 DEBUG
Define basic priority codes for log messages.
Definition: log.h:109
CCSDS-AOS transfer frame header.
Definition: ccsds_aos.h:62
u16 id
Spaceraft ID + Virtual Channel ID.
Definition: ccsds_aos.h:63
u8 signal
Signaling field.
Definition: ccsds_aos.h:64
bool read_from(satcat5::io::Readable *rd)
Read from stream, populating header fields.
Definition: ccsds_aos.cc:52
Header & operator++()
Increment the Virtual Channel Frame Count.
Definition: ccsds_aos.cc:62
u32 count
Virtual Channel Frame Count.
Definition: ccsds_aos.h:65
u8 vcid() const
< Virtual Channel ID
Definition: ccsds_aos.h:89
Helper object for the CCSDS-SPP primary packet header.
Definition: ccsds_spp.h:47
void set(bool cmd, u16 apid, u16 seq)
Construct a basic single-part SPP header.
Definition: ccsds_spp.cc:29
Multipurpose filter for matching fields in network packets.
Definition: net_type.h:38
Miscellaneous mathematical utility functions.
constexpr unsigned min_unsigned(unsigned a, unsigned b)
Min and max functions.
Definition: utils.h:111