1
\$\begingroup\$

I have written a simple packet-framing class, for converting un-encoded 'packets' into encoded/delimited 'frames,' to be sent over a P2P serial connection. The functionality is based on the RFC 1662 "Point-to-Point" protocol, and the general design was inspired by the article "Framing in Serial Communications" by Eli Bendersky.

One notable departure from the design in the article: this implementation has opted to use a single 'frame terminator' byte at the end, rather than separate 'begin/end' bytes.

The code leverages template arguments and standard iterator; users can choose to use whichever standard container is appropriate for their environment. In particular, it is an ideal utility for use in a (C++11-compatible) embedded environment - a fixed-size array with standard 'inserter' iterator support can be used.

#pragma once
#include <cstdint>

// The simple_frame_encoding class provides packet/frame encoding and decoding, using a simple
// framing strategy with the following rules:
// 
// - The end of an input packet is suffixed with the "frame separator" reserved byte.
// - Any occurrences of incidental 'reserved bytes' within the input packet are escaped:
//     - The reserved byte is prefixed with the "escape" reserved byte (signaling that the next byte must be decoded).
//     - The reserved byte is XOR'd using the "XOR encode" byte.
//
// When decoding, the reserve strategy is employed:
// - Append bytes to the decoded packet
// - When the 'escape' is encountered, make sure the next byte is 'decoded' (XOR) before appending
// - The frame is complete once the 'frame separator' is encountered
template<std::uint8_t FrameSeparator, std::uint8_t Escape, std::uint8_t XorEncode>
class simple_frame_encoding final {
public:

    // Ensure none of the 'reserved' bytes (separator, escape) conflict with encoded versions of the same
    static_assert((FrameSeparator ^ XorEncode) != FrameSeparator, "Encoded byte conflict: the encoded 'FrameSeparator' byte is equal to the unencoded 'FrameSeparator' byte");
    static_assert((FrameSeparator ^ XorEncode) != Escape, "Encoded byte conflict: the encoded 'FrameSeparator' byte is equal to the unencoded 'Escape' byte");
    static_assert((Escape ^ XorEncode) != FrameSeparator, "Encoded byte conflict: the encoded 'Escape' byte is equal to the unencoded 'FrameSeparator' byte");
    static_assert((Escape ^ XorEncode) != Escape, "Encoded byte conflict: the encoded 'Escape' byte is equal to the unencoded 'Escape' byte");

    simple_frame_encoding() = default;
    ~simple_frame_encoding() = default;

    // Iterate over the specified packet range, encoding the contents
    // and inserting the resulting 'encoded' frame bytes in the 'output' inserter.
    template<typename TInputIter, typename TInserter> 
    void encode(TInputIter first, TInputIter last, TInserter output) {
        // Append each byte to the encoded packet;
        // ensure any 'reserved' bytes are XOR'd, and prepended with an escape character
        while (first != last) {
            if (is_reserved(*first)) {
                *output++ = Escape;
                *output++ = (*first ^= XorEncode);
            }
            else {
                *output++ = *first;
            }
            first++;
        }

        // Append the frame separator, after the contents
        *output++ = FrameSeparator;
    }

    // Frame decoding occurs over a stream of individual bytes, and tracks internal state;
    // the decoder class encapsulates the current frame-decoding state into a self-contained class.
    // The decoder is fed with individual 'encoded' bytes, automatically decoding
    // them and inserting them into a 'decoded' packet structure, as appropriate.
    class decoder {
    public:
        // Decode the next byte, appending the result to the output iterator.
        // Return true if the packet is 'complete.'
        template<typename TInserter>
        bool decode_next_byte(std::uint8_t byte, TInserter output) {
            // If we encounter the 'escape' byte, mark the next byte for decoding
            // and return *without* appending
            if (byte == Escape) {
                decode_next_byte_ = true;
                return false;
            }

            // If we encounter the 'separator' byte, then the packet is done!
            // Return true and reset the state.
            if (byte == FrameSeparator) {
                reset();
                return true;
            }

            // If the previous byte was an 'escape' char,
            // decode this byte before adding it to the packet
            if (decode_next_byte_) {
                byte ^= XorEncode;
                decode_next_byte_ = false;
            }

            // If we reach this point, simply append the byte to the packet in progress...
            *output++ = byte;
            return false;
        }

        void reset() {
            decode_next_byte_ = false;
        }
    private:
        bool decode_next_byte_ = false;
    };

    // Create and return an instance of a frame decoder, for the current encoding
    decoder create_decoder() {
        return decoder();
    }

protected:
    // Determine if a given byte is reserved by the FrameSeparator or Escape characters
    static bool is_reserved(std::uint8_t byte) {
        return byte == FrameSeparator || byte == Escape;
    }
};

// Default frame encoding, effectively matching RFC 1662, the point-to-point protocol.
using default_simple_frame_encoding = simple_frame_encoding<0x7E, 0x7D, 0x20>;

When decoding: the decode_next_packet function will return true when the frame delimiter was found. This indicates the packet is 'complete' and should be dispatched to the next layer of application code.

Users are left to clear their own 'in progress' packet buffer, prior to receiving the next byte.

Example usage when encoding a packet to be sent over the wire:

auto packet = vector<uint8_t>{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F };
vector<uint8_t> frame;

auto encoding = default_simple_frame_encoding();    
encoding.encode(begin(packet), end(packet), back_inserter(frame));

serialPort.send(frame);

Example usage, when receiving a stream of frame-encoded bytes:

auto encoding = default_simple_frame_encoding();
auto decoder = encoding.create_decoder();
uint8_t nextByte = 0;
static_buffer packet;
while (serialPort.waitNextByte(nextByte)) {
    if(decoder.decode_next_byte(nextByte, back_inserter(packet)) {
        dispatch_completed_packet(packet);
        packet.clear();
    }
}

I am looking for any and all criticisms on style, efficiency, reusability, and readability.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.