Comms CCF
This is a simple communication layer based on COBS, CBOR and FNV-1A
Loading...
Searching...
No Matches
ccf.hpp
Go to the documentation of this file.
1
23
24
25#pragma once
26
27#include "circular_buffer.hpp"
28#include "cbor.hpp"
29#include "cobs.hpp"
30#include "fnv1a.hpp"
31
32#if defined(DEBUG_CCF)
33#include DEBUG_CCF
34#else
35#include "ndebug.hpp"
36#endif
37
38#include <stdarg.h>
39#include <stddef.h>
40#include <stdint.h>
41#include <stdio.h>
42#include <string.h>
43
44#include <algorithm>
45#include <iterator>
46#include <optional>
47#include <span>
48
50{
51 size_t rxBufSize;
52 size_t txBufSize;
53 size_t maxPktSize;
54};
55
56enum class Channels : uint8_t
57{
58 Rpc = 0,
59 Log = 1,
61 Trace = 2,
64};
65
66enum class LogLevel : uint8_t
67{
68 Debug,
69 Info,
70 Warn,
71 Error,
72
73};
74
75template<CcfConfig Config>
76class Ccf
77{
78 using RxFrame = CircularBuffer<uint8_t, Config.rxBufSize, Config.maxPktSize>::Frame;
79public:
80 using TxFrame = CircularBuffer<uint8_t, Config.txBufSize, Config.maxPktSize>::Frame;
81
92 bool receiveCharacter(uint8_t byte)
93 {
94 // Do get before feed, and always do feed to e.g. reset on \0
95 const uint8_t value = decoder.get(byte);
96 const bool do_output = decoder.feed(byte);
97 // Not storing null byte because packet length is indicated in
98 // a different way
99 if (byte == 0)
100 {
101 if (!rxBuf.dropping())
102 {
103 rxBuf.notify();
104 }
105 else
106 {
107 rxBuf.reset_dropped();
108 }
109 return true;
110 }
111 else if (do_output)
112 {
113 rxBuf.push_back(value);
114 }
115 else
116 {
117 // This is expected for the header byte and continuation bytes
118 // (frames >255 bytes without any null terminators)
119 debugf(DEBUG "dropping %02X as !=0 and !do_output" END LOGLEVEL_ARGS, byte);
120 }
121 return false;
122 }
123
125 bool charactersToSend(std::optional<TxFrame> & frame)
126 {
127 return txBuf.get_frame(frame);
128 }
129
140 template<typename Rpc>
141 bool poll(const Rpc & rpc)
142 {
143 bool output = false;
144 std::optional<RxFrame> frame;
145 while (rxBuf.get_frame(frame))
146 {
147 size_t len = 0;
148 // Copy to a local buffer to make sure it is contiguous
149 for (auto c : *frame)
150 {
151 pktBuf[len++] = c;
152 }
153 std::span span{pktBuf, len};
154
155 if (len < 6)
156 {
159 debugf(WARN "Bad RPC! (len=%zu)" END LOGLEVEL_ARGS, len);
160 std::ranges::copy("Bad RPC!\n", std::back_inserter(txBuf));
161 txBuf.push_back(static_cast<uint8_t>(0));
162 txBuf.notify();
163 output = true;
164 continue;
165 }
166
167 uint8_t channel = span[0];
169 (void)channel;
170
171 if (!Fnv1a::checkAtEnd(span))
172 {
175 debugf(WARN "Corrupted request (chan=%u)" END LOGLEVEL_ARGS, channel);
176 std::ranges::copy("Corrupted request\n", std::back_inserter(txBuf));
177 txBuf.push_back(static_cast<uint8_t>(0));
178 txBuf.notify();
179 output = true;
180 continue;
181 }
182 // Remove channel + checksum
183 span = span.subspan(
184 sizeof(channel),
185 span.size() - Fnv1a::size - sizeof(channel));
186
187 uint8_t seqNo = span[0];
188 uint8_t function = span[1];
189 span = span.subspan(sizeof(seqNo) + sizeof(function));
190 // Reuse the pktBuf buffer for return (leaving space for
191 // channel, function, and checksum)
192 auto header = sizeof(channel) + sizeof(seqNo) + sizeof(function);
193 auto ret = std::span<uint8_t>(
194 pktBuf + header, sizeof(pktBuf) - header - Fnv1a::size);
195 if (!rpc.call(function, span, ret))
196 {
199 debugf(WARN "RPC failed (function=%u)" END LOGLEVEL_ARGS, function);
200 std::ranges::copy("RPC failed\n", std::back_inserter(txBuf));
201 txBuf.push_back(static_cast<uint8_t>(0));
202 txBuf.notify();
203 output = true;
204 continue;
205 }
206 pktBuf[0] = channel;
207 pktBuf[1] = seqNo + 1;
208 pktBuf[2] = function;
209 auto respLen = static_cast<size_t>(ret.data() - pktBuf);
210 std::span resp{pktBuf + sizeof(channel), respLen - sizeof(channel)};
211 output = output || send(Channels::Rpc, resp);
212 }
213 return output;
214 }
215
228 bool send(Channels channel, std::span<uint8_t> & data)
229 {
230 const size_t toSend = data.size() + sizeof(channel) + Fnv1a::size;
231 if (toSend > sizeof(pktBuf))
232 {
233 debugf("Data for send too large\n");
234 return false;
235 }
236 std::span resp{pktBuf, toSend};
237 // Note: memmove caters for overlapping pktBuf/data (though no
238 // need to move if it is already in place)
239 if (&data[0] != &resp[1])
240 {
241 memmove(&resp[1], data.data(), data.size_bytes());
242 }
243 uint8_t chan = static_cast<uint8_t>(channel);
244 resp[0] = chan;
245 Fnv1a::putAtEnd(resp);
246 for (auto c : Cobs::Encoder(resp))
247 {
248 txBuf.push_back(c);
249 }
250 txBuf.push_back(static_cast<uint8_t>(0));
251 if (txBuf.dropping())
252 {
253 txBuf.reset_dropped();
254 return false;
255 }
256 else
257 {
258 txBuf.notify();
259 return true;
260 }
261 }
262
285#if defined(DEFERRED_FORMATTING)
286 template <typename... Args>
287 std::optional<size_t> logToBuffer(
288 std::span<uint8_t> & span,
289 LogLevel level,
290 uint8_t module,
291 std::string_view fmt,
292 Args && ... args)
293#else
294 std::optional<size_t> logToBuffer(
295 std::span<uint8_t> & span,
296 LogLevel level,
297 uint8_t module,
298 const char * fmt,
299 ...)
300 {
301 va_list args;
302 va_start(args, fmt);
303 const auto ret = vLogToBuffer(span, level, module, fmt, args);
304 va_end(args);
305 return ret;
306 }
307
308 std::optional<size_t> vLogToBuffer(
309 std::span<uint8_t> & span,
310 LogLevel level,
311 uint8_t module,
312 const char * fmt,
313 va_list args)
314#endif
315 {
316 if (module > (1 << 5) - 1)
317 {
318 return {};
319 }
320 uint8_t initialByte = (static_cast<uint8_t>(level) << 5) | module;
321 span[0] = initialByte;
322 // Space for length
323 const auto start = span.begin() + 2;
324#if defined(DEFERRED_FORMATTING)
325 const auto written = std::distance(start, std::ranges::copy(fmt, start).out);
326 auto rest = span.subspan(2 + written);
327 bool ok = (Cbor::Cbor<Args>::encode(std::forward<Args>(args), rest) && ...);
328 const auto end = rest.begin();
329#else
330 int written = vsnprintf(
331 reinterpret_cast<char *>(&*start),
332 std::distance(start, span.end()),
333 fmt,
334 args
335 );
336 bool ok = 0 < written && static_cast<size_t>(written) < span.size();
337 const auto end = start + written;
338#endif
339 span[1] = written;
340 if (!ok)
341 {
342 return {};
343 }
344 size_t total = std::distance(span.begin(), end);
345 span = span.subspan(total);
346 return {total};
347 }
348
363#if defined(DEFERRED_FORMATTING)
364 template <typename... Args>
365 std::optional<size_t> log(
366 LogLevel level,
367 uint8_t module,
368 std::string_view fmt,
369 Args && ... args)
370#else
371 std::optional<size_t> log(
372 LogLevel level,
373 uint8_t module,
374 const char * fmt,
375 ...)
376#endif
377 {
378 std::span<uint8_t> span{pktBuf};
379#if defined(DEFERRED_FORMATTING)
380 const auto formatted = logToBuffer(span, level, module, fmt, std::forward<Args>(args)...);
381#else
382 va_list args;
383 va_start(args, fmt);
384 const auto formatted = vLogToBuffer(span, level, module, fmt, args);
385 va_end(args);
386#endif
387 if (formatted)
388 {
389 std::span<uint8_t> toSend{pktBuf, *formatted};
390 if (send(Channels::Log, toSend))
391 {
392 return formatted;
393 }
394 }
395 return {};
396 }
397
398
399private:
400 CircularBuffer<uint8_t, Config.txBufSize, Config.maxPktSize> txBuf;
401 CircularBuffer<uint8_t, Config.rxBufSize, Config.maxPktSize> rxBuf;
402 Cobs::Decoder decoder{};
403 uint8_t pktBuf[Config.maxPktSize];
404};
405
406// This is a header, undefine the debugf macro
407#include "debug_end.hpp"
Encoding for structured data (numbers, strings, bytes, arrays and maps). Also supports custom tags.
Channels
Definition ccf.hpp:57
@ Trace
Definition ccf.hpp:61
Buffer for queuing received bytes.
Definition ccf.hpp:77
bool receiveCharacter(uint8_t byte)
Push RX'ed character to RX queue. Safe to call from interrupt context.
Definition ccf.hpp:92
bool poll(const Rpc &rpc)
Process incoming packets to dispatch e.g. RPC.
Definition ccf.hpp:141
std::optional< size_t > vLogToBuffer(std::span< uint8_t > &span, LogLevel level, uint8_t module, const char *fmt, va_list args)
Same as logToBuffer but taking va_list instead of ... varargs.
Definition ccf.hpp:308
bool charactersToSend(std::optional< TxFrame > &frame)
Get TX queue size. Safe to call from interrupt context.
Definition ccf.hpp:125
bool send(Channels channel, std::span< uint8_t > &data)
Send data over a channel.
Definition ccf.hpp:228
std::optional< size_t > logToBuffer(std::span< uint8_t > &span, LogLevel level, uint8_t module, const char *fmt,...)
Logs to a buffer, returning logged size. Threadsafe because it doesn't send the message,...
Definition ccf.hpp:294
std::optional< size_t > log(LogLevel level, uint8_t module, const char *fmt,...)
Sends a logs message.
Definition ccf.hpp:371
Definition circular_buffer.hpp:82
Decodes data as a state-machine.
Definition cobs.hpp:108
Definition cobs.hpp:61
Definition rpc.hpp:175
Simple framing (marking packet start/stop) with self-synchronisation.
Simple non-cryptographic hash with decent performance.
constexpr void putAtEnd(std::span< uint8_t, Size > span, uint32_t hash=initialHash)
Definition fnv1a.hpp:65
constexpr bool checkAtEnd(std::span< uint8_t, Size > span, uint32_t hash=initialHash)
Definition fnv1a.hpp:82
#define debugf(...)
Definition ndebug.hpp:23
#define END
Terminator of log message at the end.
Definition ndebug.hpp:50
#define LOGLEVEL_ARGS
Definition ndebug.hpp:28
#define DEBUG
Debug levels at the start of debugf.
Definition ndebug.hpp:33
Parametrized CBOR interface.
Definition cbor.hpp:400
Definition ccf.hpp:50