RoCo, the Xplore's rover communication API, allows the Rover's different subsystems to communicate with each other. Every peer in a bus network can broadcast a message on the bus and this message might or might not be handled by the other peers.
Using a high degree of abstraction, this API ensures that a unique software can be shared across the subsystems without the need of porting or installing additional software. Since RoCo will also be used on embedded systems, the software is designed to have a low memory footprint and CPU overhead. Furthermore, static allocation predominates the code's architecture to avoid potential overflows.
In addition, RoCo also includes the communication protocol, that defines which packets will be transmitted and received through specific busses.
When using the RoCo API, please do only use the RoCo.h header file.
RoCo's highest degree of abstraction is the MessageBus API, which defines how packets are defined, handled, sent or forwarded. This MessageBus class only requires the implementations to define standard read and write functions to be functional, as described later. Since memory allocation is static, it was necessary to define a maximum packet size (256 bytes), as well as a maximum number of unique senders (64). Moreover, there are at most 64 different packet identifiers.
Every MessageBus method requires an additional type info, which should define the structure of the considered packet. Packets should always be defined in accordance with the following scheme.
struct SomePacket {
// Whatever content you need
} __attribute__((packed));If you fail to define the structure as packed, extra padding bytes will be added to the good will of the compiler, which is the most undesirable effect possible since the code is supposed to be completely portable between different system architectures.
Another extra step to define a packet is to add it to the protocol register in Protocol/ProtocolRegister.h. Doing so will allocate memory for the template type of the structure previously defined.
#ifdef YOUR_PROTOCOL
REGISTER(SomePacket)
#endif /* YOUR_PROTOCOL */Once those steps are done, you may use the MessageBus API to send this packet to any peer connected to the bus.
template<typename T> bool define(uint8_t identifier)The define method links the given packet structure to a unique identifier. Every identifier consist of two routing bits, which are reserved for now, and six identifier bits that can be freely assigned. It is recommended to define the different packets supported by the bus in the MessageBus' implementation's constructor. Once a packet is defined, it can be freely sent, received or forwarded. Otherwise, calling the later methods will result in failure. The define method will fail if one of the following conditions is met:
- The packet identifier is already in use.
- The packet size is too large (> 256 bytes).
- The packet type is already defined with another identifier.
template<typename T> bool send(T *message);The send method allows the user to broadcast a defined packet on the bus. Note that thank to the template argument, it is not necessary to specify the nature of the packet being sent: The compiler will infer its nature automatically. The send method will fail if the provided packet has no assigned identifier.
template<typename T> bool handle(void (*handler)(uint8_t source, T*));The handle method allows the user to receive a defined packet from the bus. This method requires the user to pass a callback function, that will handle the reception of the given packet. Depending on implementation, the source identifier might or might not accurately represent the sender of the packet. Note that thank to the template argument, it is not necessary to specify the nature of the packet being received: The compiler will infer its nature automatically. The handle method will fail if the provided packet has no assigned identifier.
template<typename T> bool forward(MessageBus* bus);The forward method allows the user to redirect a defined packet from one bus to another. All packets having the same template type as the passed one will be retransmitted to the given bus. The forward method will fail if the provided packet has no assigned identifier.
The MessageBus class defines two virtual methods that need to be implemented by subclasses:
virtual uint8_t append(uint8_t* buffer, uint32_t length) = 0; // Must be atomic
virtual void transmit() = 0;As you may guess, the append method is used to send data to the bus controller. Note that in a multithreaded environment, it is required that append gets called in thread-exclusive context (usage of Lock/Mutex is recommended). In addition, transmit is used to flush eventual data that might be buffered in the bus controller.
Moreover, the implementation must ensure that receive is called whenever a buffer is received from the bus. The receive method has the following signature:
void receive(uint8_t senderID, uint8_t *pointer, uint32_t length);By implementing the two above methods and calling receive when appropriate, the MessageBus implementation is complete and can be used on every platform that meets the dependency requirements.
The IOBus implementation is designed to manage a classic pre-allocated buffer provided by the constructor. Transmission and reception is managed by a dedicated class called IODriver. The latter must be inherited and overwrite the following virtual methods:
virtual void receive(const std::function<void (uint8_t sender_id, uint8_t* buffer, uint32_t length)> &receiver) = 0;
virtual void transmit(uint8_t* buffer, uint32_t length) = 0;receive provides the IODriver implementation with a callback reception function. This callback function must be called whenever there is incoming data from the bus.
transmit signals the IODriver that the given data buffer must be sent to the bus. As for every I/O driver, it is required for the transmit method to have exclusive context.
The NetworkBus API inherits the IOBus implementation with a standard buffer size of 256 bytes. The only interest in analysing this implementation is the way the IODriver were developed. Note that NetworkClientIO and NetworkServerIO define slightly different implementations of the IODriver. For simplicity, we will assume the low-level implementations of these two IODriver are equivalent.
Using the <sys/socket.h> API, this implementation provides a simple way to communicate between two peers on a network. Note that this implementation is not fully compatible with the LwIP Stack, and thus cannot be used on embedded software.
To establish or interrupt a connection, two methods are available for each IODriver. Their usage is transparent.
NetworkClientIO
int8_t connectClient();
void disconnectClient();NetworkServerIO
int8_t connectServer();
void disconnectServer();For both implementations, the packet reception is done by reading from the socket through an independent thread. Transmission is simply done by writing to the TCP socket.
The latest protocol (version 20W18) implements the following packet structures. Identifiers are not listed since they are bus-dependant.
Allows the user to assess the bus latency.
- Actual timestamp in nanoseconds as std::chrono::time_point
Signals a new connection on the bus.
- Actual timestamp in nanoseconds as std::chrono::time_point
Signals a disconnection on the bus.
- Actual timestamp in nanoseconds as std::chrono::time_point
Signals the peers that a given resource is requested.
- Request UUID as uint32_t
- Action identifier as uint8_t
- Target identifier as uint8_t
- Request payload as uint32_t
Acknowledges a request. Undefined if no request was previously performed.
- Request UUID as uint32_t
- Primary state of request as uint8_t
Responds to a request. Undefined if no request was previously performed.
- Request UUID as uint32_t
- Action identifier as uint8_t
- Target identifier as uint8_t
- Response payload as uint32_t
Signals the requester that the resource is being processed and defines an actual state of the request. Undefined if no request was previously performed.
- Request UUID as uint32_t
- Progress information as uint8_t
Broadcasts a generic data on the bus.
- Generic data as uint32_t
Broadcasts a generic message on the bus.
- Message data as uint8_t[128]
Signals the peers that the sender has experienced failure and defines an error identifier representing the situation.
- Error identifier as uint8_t
Since RoCo is designed to be portable software, the same code base can be run on multiple platforms. However, some advanced features, such as Network I/O are not compatible with embedded systems. This is why there are several build configurations that need to be specified when targeting a specific platform.
Before including the Roco.h header file, it is necessary to declare which platform you are targetting:
- BUILD_FOR_CONTROL_STATION
- BUILD_FOR_NAVIGATION
- BUILD_FOR_AVIONICS
- BUILD_FOR_TESTING
It is also of crucial importance that all the peers on the same bus communicate with the same version of RoCo and, in particular, with the same protocol specification.
To enforce this, please do always specify and check the target protocol version in Build/Build.h. The latest implementation is version 20W18.
Alice and Bob want to assess the quality of the link that connects them. In fact, Alice and Bob are both connected to Carol, which routes messages from Alice to Bob and from Bob to Alice. Alice tells Bob that she is going to send him her actual time. When Bob receives the message, he will compare his actual time to Alice's actual time and compute by how much it differs. For simplicity, we assume Alice, Bob and Carol are one the same machine.
Since Carol is only supposed to send what she receives, using the forward method is appropriate. The implementation of the transmission and reception becomes trivial with the RoCo API:
void handle_ping(uint8_t sender_id, PingPacket* packet) {
std::cout << "Ping C2C: " << (PingPacket().time - packet->time).count() << "ns" << std::endl;
}int main() {
NetworkServerIO* carol_io = new NetworkServerIO(42666);
NetworkClientIO* alice_io = new NetworkClientIO("127.0.0.1", 42666);
NetworkClientIO* bob_io = new NetworkClientIO("127.0.0.1", 42666);
carol_io->connectServer();
alice_io->connectClient();
bob_io->connectClient();
NetworkBus* carol_bus = new NetworkBus(charlie_io); // Alice-Carol-Bob bus
NetworkBus* alice_bus = new NetworkBus(alice_io); // Alice-Carol bus
NetworkBus* bob_bus = new NetworkBus(bob_io); // Carol-Bob bus
carol_bus->forward<PingPacket>(carol_bus); // Rebroadcast on the Alice-Carol-Bob bus
bob_bus->handle(handle_ping); // Configure the reception
PingPacket packet;
alice_bus->send(&packet); // Send to Carol, who will send to Bob
}