This is a library for sending and receiving SLIP using any object that
implements Stream
. It implements
RFC 1055.
Framing is important because it prevents streamed data from becoming unsynchronized. This library provides a well-known way to frame data over any stream.
Notable features:
- Can seamlessly be used as a
Stream
orPrint
object. - Read and write availablility functions work.
- Properly sets the write error state, inclusive of tracking the underlying stream's write error state.
- Provides a way to detect protocol violations.
- There are nuances to the SLIP protocol and this library aims to get them right.
To use this library, simply treat it as you would any Stream
object. When
constructing an instance of SLIPStream
, you need to pass in a reference to
an existing Stream
object, for example Serial1
.
Additional functions:
writeEnd()
: This writes an END marker for the current frame.isEnd()
: Determines if the lastread()
call detected a frame END marker.isBadData()
: Determines if the lastread()
call returned data that was involved in a protocol violation. That is, an escaped character that shouldn't have been escaped.read(uint8_t *, size_t)
: A multi-byte read implementation, intended to replacereadBytes
if the caller desires to know if corrupt data has been read.
Simply send data as normal by using one of the write
calls. To send an END
marker for the current frame, call writeEnd()
. Note that the stream is not
flushed, so to ensure all the bytes are sent, including the END marker, call
flush()
.
To determine the number of characters that can be written without blocking, call
the availableForWrite()
function. This makes the conservative assumption that
each character will be written as two bytes, even though the majority of
characters are sent as one byte. Therefore, treat this number as a minimum. It
just returns the underlying stream's availability divided by two.
This class tracks the write error state of the underlying stream, in addition to tracking write counts. The write error state will be set if:
- Not all bytes are written successfully. This may happen when single-byte writes return zero or when multi-byte writes return a number less than the specified write count.
- The underlying stream has its write error set after any write or flush call.
Note that clearing a write error by setting it to zero is possible, but be aware that only the first byte of a two-byte character may have been sent (the ESC byte). This error condition may be cleared by sending two frame END markers, depending on how the SLIP receiver behaves. The first may or may not be interpreted as a regular byte, depending on how the receiver processes protocol violations (if there was indeed a dangling ESC byte sent), but the second should be interpreted as a proper frame END marker because it's not preceded by an ESC byte.
Data can be received normally using the read()
call. This will, as usual,
return -1 when no data is available, but will return SLIPStream::END_FRAME
when a frame END marker has been received. The value of this constant is -2.
In other words, stopping conditions are indicated by negative values, and so should work with code that only checks for negative values instead of just -1.
When an END marker is received, the "END" condition is set. If corrupt SLIP data was received, that is, unnecessarily escaped bytes are in the data, then they are returned as normal and the "Bad Data" condition is set. More details on these two conditions are below.
There are two additional conditions after a read()
call:
- A frame END marker was encountered.
- Corrupt data was received.
The END marker can be detected with the isEnd()
call and corrupt data can be
detected with the isBadData()
call. Both functions only apply to the latest
call to read()
. In other words, if read()
is called again, then both
condition-testing functions may return new results.
If isEnd()
returns true
then the last read()
call returned -2. If
isBadData()
returns true
then the last read()
call returned a valid byte
value, but it was inappropriately escaped.
A note on data corruption: There are some cases where the data is considered
corrupt. This happens when unnecessarily escaped bytes are received. That is,
bytes that are preceded by a SLIP escape byte that don't need to be. The only
two valid escapable bytes in the SLIP protocol are ESC and END. If an improperly
escaped byte is received then it is returned as normal, however the
isBadData()
function will return true
.
It is suggested that readBytes
in the Stream
class not be used to read
multiple bytes for when there's any likelihood of protocol violations. This
is because if corrupt data was received somewhere in the middle of the bytes
and not the last byte, then it's not possible to detect that data corruption
has occurred, or which bytes are considered corrupt.
The isBadData()
call only applies to the latest read()
call, and readBytes
is not overridable, so it's not possible to intercept its behaviour.
To fix this, there is a new multi-byte read
function that will stop under the
usual conditions, no more bytes and an END marker, but will also stop when
corrupt data is received. This way, the isEnd()
and isBadData()
calls will
still behave as expected. Note that this function is defined in the SLIPStream
class and not in the Stream
class.
Having said all that, readBytes
will still work, but it won't be possible to
determine which data is corrupt, but this may not be an issue.
The isBadData()
call's return value only applies to the most recent call to
read()
.
It was decided to return a non-negative value for corrupt data because the
caller can still decide what to do with corrupt data but not have to use the
stopping logic for what is essentially a different concern. In addition, the
readBytes()
call will still properly stop only on frame END markers and EOF
conditions.
This library does not provide:
- The ability to determine frame length.
- Checksumming.
- Buffering.
If any bytes cannot be written or if the underlying Stream
object has its
write error set, then the SLIPStream
object's write error will be set.
It was decided to have corrupt data not cause read()
to return a negative
value for these reasons:
- In some cases it might be useful to examine what data was being received. Dropping it on the floor loses this information.
- Implementation experience with code that uses this class showed that it was
slightly simpler to process the logic this way. For example, when skipping
bytes, a common paradigm is:
while (stream.read() >= 0) { // Skip while available }
- For properly-written SLIP encoders, seeing this error in a connected receiver is unlikely, and adding an extra error condition for this case just doesn't seem necessary.
Because the SLIP protocol encodes some characters as two bytes, caution must be
used when deciding availability with peek()
and determining correctness with
the multi-byte read
.
Normally, when peek()
returns -1, it means no bytes are available in the
stream. However, an incomplete 2-byte character---that is, only the first byte
is available---means that the whole character is effectively not available.
Therefore, code that determines data availability by relying on just peek()
returning -1 without calling read
will receive -1 from peek()
forever, and
the stream will never advance.
The second caution is for using the return value from the multi-byte read
to
determine whether corrupt data was received. The read stops when a corrupt byte
has been received, including the last requested byte. Therefore, even though it
may appear that all requested bytes have been received, the last byte may still
be invalid. This condition can be checked by calling isBadData()
.
Code style for this project mostly follows the Google C++ Style Guide.
Copyright (c) 2018-2023 Shawn Silverman