Artifact c03da856c4a84530be5e9dabfd5d0a303dd7c9f894d01485dff382ceba4b28b4:
- File description.markdown — part of check-in [5cbb0e4c9b] at 2022-01-18 16:10:47 on branch plexer_base — Apparently fossil's markdown parser doesn't like inline code snippets (user: luna size: 13656) [more...]
ProtoPlexer protocol
Description
ProtoPlexer protocol is lightweight hardware protocol designed to be used in small satellite onboard network. It was designed to with CAN satellite network in mind, yet it should be suitable for other onboard hardware networks and for other applications.
Features
- Network address handling
- Message priorities
- Secure merging of the short chunks to the messages up to 64kbytes long;
- Message labeling with 16bit ID describing message type and content. Message types are unique within mission.
- FIFO for incoming messages
- Event generation for received full messages
- Customizable RAM footprint
Underlaying protocol requirement.
To be implemented underlaying protocol should provide several features such as:
- Broadcasting messages
- Ability to separate chunk messages from the stream (in CAN networks it’s one CAN frame) at least 6bytes long. Small chunks should have explicit way to determine first byte
- Ability for every node to broadcast
Deployment overview
To deploy and use ProtoPlexer user should:
- Create hardware interface derived from either plexer_queued or
plexer_blocking depending on whether message processing should be
synchronous or deferred. Both classes are defined in
plexer.h
- plexer_blocking will immediately call rx_event() when add_chunk() has received a full message. Outoing messages are immediately transferred to send_chunk(). You should choose this if your application is running on an RTOS where one task is dedicated to processing incomming messages and the driver implementation has an internal tx queue to prevent blocking.
- plexer_queued will queue incomming messages and call the message handler (rx_event()) when the application calls poll_new(). In a similar fashion, outgoing chunks are buffered and only transferred to the send_chunk() driver method when poll_tx() is called. You should choose this if your application is a super loop, you want precise control over when new messages are processed or if your driver does not have an integrated tx queue.
- Implement send_chunk() to send chunks originating from the application
- Either call add_chunk() every time a chunk is received fom the network or if you are using plexer_queued, implement receive_chunk() and call poll_rx() regularly.
- Implement rx_event() to handle new received messages
- If you are using plexer_queued:
- Find some place in your software to regularly call poll_tx() and poll_new()
- Optionally implement rx_notification() which will be called in add_chunk() whenever a message is received. This can be used by the application to determine when to call poll_new()
Protocol key parts and description
First lets describe some key definitions of the protocol parts.
Nodes
Every logical instance within network have 12bit address and called node. Nodes could be hardware devices, software applications, or logical parts of software applications. Every time you need to separate network flow to or from one part of your system - you could create a node for it and assign address.
Messages
Protocol transfer data between nodes using messages, messages have following structure:
- Hardware header
- Address from
- Address to
- Inverted priority
- Message header
- Message ID
- Length
- CRC
- Data
Hardware header contains data used to transmit data over underlaying protocol.
Addresses from an to - are addresses of transmitting and receiving nodes accordingly.
Inverted priority - is priority used to transmit message over underlaying protocol. Because ProtoPlexer designed with CAN network in mind and main application is satellite protocol mostly for CAN network, which uses inverted priority for messages, we also use inverted one. So the lowest parameter value means biggest priority. It's advised to use lowest priority (biggest value) unless you have to do otherwise.
Message header contains data used to transmit data within ProtoPlexer protocol
Message ID is unique ID for specific message type. Protocol does not yet specify Message ID's or their usage, but range 0xFFF0-0xFFFF should be considered reserved for protocol purposes. Later it may contain messages like "ping" "version" and other, which should be implemented on every node. It's possible to use the same Message ID for different purposes within differed devices, yet it's strongly not recommended, unless you have to.
Length and CRC used within protocol and shouldn't be used by user. They, as you could expect are the whole length of the message and it's CRC16 value. 16bit length means messages longer then 64kbytes are not possible.
Data is any user data associated with Message ID it's advised to use strong correlation between the Message ID and data interpritation. Also you should consider using messages without data. Sometimes Message ID is enough to transmit command for the node.
Chunks
Chunks - are minimal frames used to transmit data over underlaying protocol. ProtoPlexer separate every message to the chunks according to hardware settings, and transmit data over underlaying protocol in chunks. In CAN network they are equal to CAN frames. During receive ProtoPlexer receive separate chunks from network, and keep them till the whole message is ready.
Every chunk have the following fields
- Hardware header
- Address from
- Address to
- Inverted priority
- Starting bit
- Data
Different approaches could be used to transmit chunks on different hardware networks, example for some of them would be provided in further articles. Underlaying protocol and it's hardware driver should receive data in chunks, transmit them and provide received chunks to the protocol.
Plexer
Plexer is actual ProtoPlexer implementation class. It has following interface
plexer_result poll_rx();
plexer_result poll_tx();
plexer_result poll_new();
plexer_result send_msg (plexer_msg * msg);
plexer_result add_chunk(const plexer_chunk * chunk);
virtual plexer_event_result rx_event (plexer_msg *, plexer_eventtype);
virtual shared_ptr<plexer_chunk> receive_chunk();
virtual plexer_hw_result send_chunk(plexer_chunk * chunk);
uint16_t own_address() const;
uint_fast16_t max_send_attempts() const;
void setMax_send_attempts(const uint_fast16_t &max_send_attempts);
uint_fast16_t max_chunk_length() const;
static std::string result2string(plexer_result value);
poll_rx function should be called from user software. This function check (once) plexer_hw for new messages, and in case last chunk of valid message received it also calls receive event with polling type of event. This function will not keep asking plexer_hw till it's empty. In case you want such behavior you should poll plexer itself till it respond that hardware is empty.
poll_tx function is used to transmit data to network. When message sent protocol will not block thread till it actually transmitted over network. ProtoPlexer will cut the message to chunks according to settings and put them into outgoing buffer. To send message you should call poll_tx till this buffer is empty. ProtoPlexer will try to send every message up to 5 times as reaction to poll_tx and will return "ok" if hardware is busy. Only afterwards it'll respond with error code.
Note: this is only required when using plexer_queued. plexer_blocking will send outgoing chunks immediately.
poll_new function used to check received messages buffer. We need one because messages not always cleared after event. In case some messages exist in ProtoPlexer internal buffer it'll also call receive event with polling event type.
Note: this is only required when using plexer_queud. plexer_blocking will react to new messages immediately.
add_chunk function used internally by poll_rx but you could also use it in your hardware driver. Your driver may call this function right after receiving chunk. This way plexer will not need to wait till application poll it for the new messages, and will generate event (if needed) right away
It's possible to use both ways to add chunks to the protocol. But you should guarantee sequence order. Easiest way to do so is limiting receive to one of two ways only calling add_chunk from hardware driver, or only responding to poll_rx
rx_event is virtual method used to provide reaction to received messages. Your event should react to the message according to your needs.
receive_chunk is a virtual methods providing access to underlaying protocol driver. It's used by plexer_queued to poll the hardware driver for new chunks in poll_rx
send_chunk is one of virtual methods providing access to underlaying protocol driver. It's used by plexer internally to send chunks over underlaying protocol during message transmission.
own_address is address of this node in the ProtoPlexer network
max_send_attempts / setMax_send_attempts is getter and setter for send attempts limit in plexer_queued. Default value is 5. Every time user poll poll_tx he will receive result of such polling. In case plexer will be unable to send chunk within max_send_attempts count user will still receive ok error code. If plexer will fail to transmit one chunks more than max_send_attempts count user will receive hw_send_too_many_attempts result. Plexer will reset counter to zero, but will not remove such chunk, and will continue with it later.
Note: plexer_queued assumes that the driver will take care of handling retries.
max_chunk_length will be used by protocol to cut long messages to chunks during send implementation. Yet your driver could return longer chunks from receive_chunk() it's set once by plexer constructor
result2string will just convert error codes to string for debug/logging purposes
Deployment
First you need to create class derived from plexer_queued for example
class some_example_class : public plexer_queued
{
public:
// where some_hardware_const is the maximum number transmission unit of the network
// (i.e. the number of bytes that can be transmitted in one network packet)
// E.g. 8 for CAN, 64 for CAN-FD, 1500 for Ethernet and so on
some_example_class(uint16_t own_address) :plexer(own_address, some_hardware_const) { }
shared_ptr<plexer_chunk> receive_chunk() override;
plexer_hw_result send_chunk(plexer_chunk *chunk) override;
plexer_event_result rx_event(plexer_msg * msg) override;
};
Also you need some underlaying protocol driver connected to plexers methods. It could be inside or outside new class, example not provided.
You also need some event to process received messages. Example of such event could look like this:
void some_example_class::rx_event(plexer_msg * msg)
{
switch (static_cast<some_mission_specific_enum>(msg->message_ID()))
{
case some_mission_specific_enum::ID_reboot:
{
reboot();
} break;
case some_mission_specific_enum::ID_hello:
{
cout << "Hello";
} break;
default:
{
cout << "Unknown message";
} break;
}
}
All you need now is an instance of your class to communicate with ProtoPlexer network.
auto plexer_inst = make_shared<plexer>(some_own_address_constant);
Hardware specific implementation
Since ProtoPlexer use some features of underlaying protocol for addressing and header transmission those rules should be fixed for every givend underlaying protocol. If some is not covered in this document you could design your own approach, and use it within your system. You could also send it to be integrated in this document.
CAN implementation
In CAN implementation maximum chunk length is fixed to be 8bytes exactly and chunks are actually CAN frames. CAN bus should use 29bit CAN_ID ProtoPlexer hardware header is transmitted inside CAN_ID as following:
CAN_ID bits | 25-28 | 24 | 12-23 | 0-11 |
---|---|---|---|---|
fields | priority inverted | starting bit | address from | address to |
UART implementation
In UART implementation maximum chunk length is mission specific, yet no more than 250bytes and chunks contain 4 bytes header with all chunk specific information. Chunks are separated using COBS protocol
UART bits | 25-32 | 24 | 12-23 | 0-11 |
---|---|---|---|---|
fields | priority inverted | starting bit | address from | address to |