This package mostly contains a generic implementation of a packet reassembly logic. It takes byte-oriented I/O and allows you to turn this stream of bytes into a stream of well-defined, validated packages to be interpreted by a higher-level logic (e.g. data demarshalling).
The core problem this aims at solving is the (lot of) misunderstandings that stems from the complexity of low-level I/O. Additionally, it provides an I/O independence layer that is used to build a test harness for C++ libraries. Finally, within the Rock oroGen integration, it allows to monitor and log byte-level I/O.
Within Rock, it is highly encouraged to use this package to build custom device drivers, even for protcols that are naturally packet-based such as e.g. UDP, as the driver packet-extraction logic should do as many sanity checks on the bytestream as possible.
The main functionalities that iodrivers_base
provide are:
- infrastructure to convert byte-oriented streams into streams of validated packages
- library-level test harness
- generic oroGen integration (for Rock users)
- built-in support for multiple type of I/O:
- serial
- udp server: listens on a UDP port. Sends to the IP of the last received UDP message.
- udp client: connects to a given IP and port.
- tcp client: connects to a given IP and port.
- file-based (e.g. for named pipes or Unix sockets)
Note that iodrivers_base
can (and maybe should) be used even for
datagram-oriented mediums such as UDP. These mediums do not need
the packet-reassembly, but do need packet validation.
One starts by subclassing iodrivers_base::Driver
and providing the size of
the driver's static internal buffer. This buffer should be a multiple of the maximum
packet size (twice as big is usually enough):
class Driver : iodrivers_base::Driver
{
static const int MAX_PACKET_SIZE = 32;
static const int INTERNAL_BUFFER_SIZE = MAX_PACKET_SIZE * 4;
public:
Driver();
};
Driver::Driver()
: iodrivers_base::Driver::Driver(INTERNAL_BUFFER_SIZE);
{
}
The second required step is to implement iodrivers_base::Driver::extractPacket
.
This is the method that tells iodrivers_base
's internal logic what is meaningful
data and what isn't.
See below the pseudo-code implementation of most extractPacket
methods. Note
that the amount of validation you do in extractPacket
may vary, but usually
"more is better", as it is validation that won't have to be done later.
The return value is fully documented within the method documentation. Check it
out for a complete specification.
int Driver::extractPacket(uint8_t const* buffer, size_t buffer_size) const {
if (not enough bytes in buffer to contain a start code) {
return 0; // wait for new bytes
}
if (buffer does not start with packet start code) {
for (int i = 1; i < buffer_size; ++i) {
if (packet start code found at i) {
return -i; // skip i bytes from buffer and call again
}
}
return -buffer_size; // discard the whole buffer
}
if (not enough bytes in buffer to validate packet starting at zero) {
return 0; // wait for new bytes
}
else if (packet starting at zero is valid) {
return packet_size;
}
else {
return -1; // discard first byte and start searching again
}
}
All URIs follow the general format scheme://NAME:NUMBER?option1=value1&option2=value2
Which parts of the URI is accepted by which scheme is detailed below
Open a serial device and configure it. A basic serial URI is
serial://${DEVICE_PATH}:${BAUDRATE}
. Note that absolute paths lead to
having three consecutive slashes (two for the ://
and one for the path)
The serial URIs accept the following options:
byte_size
byte size in bits, from 5 to 8. The default is 8parity
parity, eithernone
,even
orodd
. The default isnone
stop_bits
stop bits (either 1 or 2). The default is 1
Examples:
serial:///dev/ttyUSB0:115200
serial:///dev/ttyUSB0:115200?parity=even
Open an UDP socket on a random port which sends to the given host and port.
If the local_port
option is given, the socket is bound to the given local port
Examples:
udp://localhost:4000
udp://localhost:4000?local_port=4001
The Connection Refused error If configured to do so, UDP streams will report
a connection refused error if there are no processes listening on the configured
remote peer. This is controlled by the ignore_connrefused
parameter which has
to be set to 0 or 1. For backward compatibility reasons, the default behavior of
UDP streams with respect to this option is complex, see below for details.
Note that the connection refused error depends on the reception of a ICMP message,
sent by the remote host. The error might not appear at all if this message is not
sent by the remote peer, or if the ICMP message is blocked. Moreover, the
error reporting in this case is asynchronous and will be reported on follow-up
writePacket
or readPacket
after the ICMP message is received, that is after
the actuall writePacket
call that generated the error in the first place.
Connected UDP sockets If configured to do so, UDP streams are connected,
that is will only accept packets from the configured remote host. If
unconnected, they will receive from any host (but still send to the
configured host). In addition, sending a lot of packets to the same host will
have a better performance with connected sockets. This is controlled by the
connected
parameter which has to be set to 0 or 1. For backward
compatibility reasons, the default behavior of UDP streams with respect to
this option is complex, see below for details.
Default connected
and ignore_connrefused
parameters
For backward compatibility reasons, the default values for connected
and
ignore_connrefused
depends on whether the UDP stream was created with or without
a local port. The behavior described below is deprecated. In the future, the default
will be set to connected=1
and ignore_connrefused=1
. Warnings are currently issued
when the current defaults are used. Explicitely set these parameters to shut the
warnings and ensure your code will continue working as-is when the defaults change.
-
ignore_connrefused
may be set to zero (to report connrefused) only on connected sockets. Attempting to setignore_connrefused=0&connected=0
will throw inopenURI
-
setting
connected=0
automatically setsignore_connrefused=1
even if the default (as described below) would be 0 -
UDP sockets for which the local port is unspecified (without the
local_port
option) are configured withconnected=1
andignore_connrefused=0
by default. -
UDP sockets for which the local port is given (with the
local_port
option) are configured withconnected=0
andignore_connrefused=1
by default.
Host and Network Unreachable errors If configured to do so, UDP streams
will either report or ignore host and/or network unreachable errors, as
reported by the underlying socket implementation This is controlled
respectively by the ignore_hostunreach
and ignore_netunreach
parameters
which have to be set to 0 or 1. The default is to not ignore the error.
Note that the reliability of this error reporting is complex. They may be reported
locally if the localhost routing tables do not provide a link to the target,
but may also depend on the reception of a ICMP message, sent by routers on the path
to the remote host. In this latter case, the error might not appear at all if
this message is not sent by the remote peer, or if the ICMP message is
blocked. Moreover, the error reporting in this case is asynchronous and will
be reported on follow-up writePacket
or readPacket
after the ICMP message
is received, that is after the actuall writePacket
call that generated the
error in the first place.
The recommendation is to keep the default in situations where the path to the remote host should always be available - i.e. wired scenarios. Disable both of them when using wireless links, as some wireless routers report these errors when the link is down.
Passively listens to UDP packets on a given port. Writing to the driver will send data back to the last UDP client whose packet was received (and does nothing if nothing has been received yet).
Examples:
udpserver://5000
Connect to a Unix socket server. The path is right after the :// scheme marker, so you should end up with three slashes for absolute paths (e.g. unixstreamserver:///tmp/sock)
Create a Unix stream server on the given path. The path is right after the :// scheme marker, so you should end up with three slashes for absolute paths (e.g. unixstreamserver:///tmp/sock). The driver will delete a socket at this path if it exists, and create a new one.
The server can handle only a single client. It will stop working if the first client disconnects.
Connect to a Unix datagram server. The path is right after the :// scheme marker, so you should end up with three slashes for absolute paths (e.g. unixstreamserver:///tmp/sock)
The client can work bidirectional.
Create a Unix datagram server on the given path. The path is right after the :// scheme marker, so you should end up with three slashes for absolute paths (e.g. unixstreamserver:///tmp/sock). The driver will delete a socket at this path if it exists, and create a new one.
The server can only receive data if the client is not bound to a path -
something the unixdgram
URL does not allow to do yet.
Open a TCP connection to the given remote host and port
Examples:
tcp://localhost:5000
Open a file. Note that absolute paths lead to having three consecutive slashes
(two for the ://
and one for the file)
Examples:
- `file:///path/to/file
Use an existing file descriptor
The fd URIs accept the following options:
auto_close
chooses whether the driver will close the FD on destruction ("1") or not ("0"). The default is "1"has_eof
tells the Driver logic whether the underlying file descriptor will notify EOF when the remote side gets closed, or not. The default is "0" (not).
Examples:
fd://10?auto_close=0
This package provides a testing harness that allows you to write integration
tests drivers based on iodrivers_base::Driver
.
From a design perspective, one should start by separating the protocol implementation
(including the extractPacket
logic by iodrivers_base::Driver
) into a separate
set of stateless functions. This ensures they will be fully and easily testable.
The test harness, then, allows you to check the Driver
class logic from the
perspective of the device, that is by checking what is being sent by the
driver, and sending data to it. The goal is to verify the Driver
's logic, not
the protocol parsing since this one has been implemented in separation (and
separately tested).
Check the documentation of iodrivers_base/Fixture.hpp
for more information
on how to use the harness.
This package provides two command-line utilities:
iodrivers_base_forward
forwards one data stream to another. Both streams are defined by iodrivers_base's URIsiodrivers_base_cat
outputs the data from a stream to stdout, in hex and ascii formats
For anything more complicated, we recommend usage of socat
- do not overload
openURI
. This will make testing harder (you have to "mock" whathever is being done inopenURI
for each test), and will definitely reduce the usefulness of your driver. Finally, it is incompatible with Rock's oroGen integration foriodrivers_base
.
See this document
This software is licensed under the GNU LGPL version 2 or later
Copyright 2008-2017 DFKI Robotics Innovation Center 2014-2017 SENAI-CIMATEC 2017-2019 13 Robotics 2019 TideWise