OSI-oriented modularization to customize communication between Layer-7-services on interchangeable Layer-1/2-interfaces.
The architecture is usable for all MCUs of the Arduino-platform.
📖 Checkout detailed Documentation
Provide configurable software-modules to customize the communication between services, hosted on different MCUs.
BusBricks are templates to derive classes for mapping service-specific rules to different bus-systems.
The communication between the hosting devices is therefore separated from the inter-service-communication.
That means, the communication between the services can follow it's own (from the chosen communication-interface independent) rules.
Hosting a messenger on different host-devices. Some of those hosts support a serial commuication for modbus-RTU, some only support I2C.
- Message content and payload definition
- Implementation of the Messenger-Service
- Implementation of the Modbus-RTU communication-interface
- Modbus-RTU frame definition
- Implementation of the Modbus-RTU Service-interface
- Registration of Messenger-Service-instance and Error-Service-instance within an service-cluster in main
- Implementation of Modbus-RTU Service-Interface to process payload from/for the registered services and frames of from/for Modbus-RTU communication-interface
- Setup of the Modbus-RTU Service-interface for Master-mode (polling and message-forwarding)
- Response-Timeout for Message-Service
- Implementation of a communication-interface derived I2C-communication-interface
- Definition of a Frame derived I2C-Frame-class
- Setup of an I2C service-interface
- Setup service-template and service-cluster for association in multiple service-interfaces
- Setup of a tiny chat-interface in main
The separation of inter-service- and inter-mcu-communication-rules requires a separation of the software modules.
Services are defined in the service-layer. All implementations in the service-layer are Service-specific and therefore they should not depend on the chosen communication-interface. That means, the service-layer-modules should be reusable, independent which communication-interface the host-device supports.
Modules being part of the Communication-Layer are specific for the chosen bus (e.g. I2C, OneWire, Musbus-rtu...) the host device provides. They are responsible for applying the bus-protocol-specific rules (e.g. CRC-check and -calculation, communication-timeouts, framelength...) to the content (PDU) provided by the services.
The cascading of processing information and the rules applied to it lead to the concept of content- and representation.
In every iteration, an information is processed and the rules of the next level, closer to the physical layer are applied to it, the information closer to the format the service is able to process is called Content.
Conversely, the format, the information has after applying the rules of the next level closer to "Layer-0" is called representation.
The Errors of a device are managed by a Service-derived ErrorService
. Every component (no matter, if Service, ServiceInterface or CommInterface) can be enabled to raise errors by deriving the ErrorState
and calling raiseError()
.
class CommInterfaceBase: public ErrorState{
public:
CommInterfaceBase(): ErrorState(){};
}
The class-instance is now able to raise Errors and, if applicable, to handle them within the class itself. E.g.:
// wait for Frame-timeout to ensure frame is complete, raise Error, if the silence-time is violated
if((_clearRxBuffer()>0) & (receiveBuffer->getSize()!=0)){
(numBytes>=MAXFRAMESIZE) ? raiseError(frameLengthError):raiseError(arbitrationError);
}
// handle the Arbitration-Error with waiting for bus-silence
if(getErrorState()==arbitrationError) while(_clearRxBuffer()!=0);
To handle the Error outside the instance, the Error-state of the components has to be processed within the ServiceInterface
by checking the ErrorState of the called instances and, if applicable raising the Error with the same Error-code within the ServiceInterface:
errorCodes commInterfaceErrorState = comm_interface->getErrorState();
if (commInterfaceErrorState!=noError) raiseError(commInterfaceErrorState);
Only Errors raised in ServiceInterface
are forwarded to an eventually registered ErrorService
, that can execute error-code-specific actions (like printing the Error or broadcasting errors to the network).
After handling the Error of the called instance, the ErrorState has to be cleared:
comm_interface->clearErrorState();
The Error-codes are enumerated in the Content-derived Error-class. To add new Error-cases, define a new Error-code in enum ErrorCodes
and add an Error-message to the getErrorMessage(errorCodes code)
function within the Error-class. The Error-Service will display those messages after an Error is either raised by another component on the local device or an Error-Frame was received.
To derive a Layer-7-service, that can be used in combination with the predefined Layer-2-communication-interfaces, you have to
Each Service has to know, how the conversion between the processable data structure (content) and the payload, that is sended or received by the communication-layer (representation) is defined. This is done by
- deriving a content-class from the content-template (docs)
The mapping between content and representation (in this case String
) is defined by:
-
overwriting and implementing the template functions
content_to_rep()
andrep_to_content()
ofContent
-
adding specific functions to be used inside of the service (optional) (e.g. string representation for printing of the Message-Class)
A Service is meant to process incoming payload and eventually generate new payload to be send. Received Payload is added to the Service with impart_pdu
and the services response is able to be picked up by calling get_response
. Both functions of the service-template are using the representation (for services contents always String) of the Content.
Defining a custom service is done by
- deriving a service-class from the service-template
- defining a default Service-ID (unique for each service)
- define construction with Service-ID and Instance-ID (unique for each instantiation of a service), e.g.:
#define SERVICEID 'e'
ErrorService::ErrorService(uint8_t instance_id): Service<Error, STACKSIZE>(SERVICEID, instance_id){}
- overwriting and implementing the template's
stack_processing
The stack_processing
function has to process all payloads from the receive-stack and add the payloads to be send to the send-stack. Check ErrorService
or MessageService
for example implementations.
- adding functions to interact with the service besides imparting/picking-up payloads (e.g.
sendMessage
of theMessageService
) (optional)
The project is implemented to be debugged on the local engineering-device (PC). Therefore testing-functions are defined for the local execution with clang (possible with other compilers, but only tested with clang).
Those functions are more thought as a "playground" for setting up scenarios to debug, than for approving complete functionality of the Program.
The clang-build is customized in tasks.
Because of the usage of Arduino-specific functions not supported by c++ natively, those functions are replaced by using the namespaces in Arduino-Mocking and Serial-Mocking.
The pio native-environment is setup in platformio.ini. All automated tests are executed in this environment.
The environments for the target-architecture are configured in platformio.ini.
This project is licensed under the GNU Affero General Public License v3.0. See the LICENSE file for details.
Copyright (c) 2024 Felix Schuelke