|
Arduino Uno is a very simple prototyping board with 8-bit microcontroller, only 2kB of RAM and (obviously) no operating system.
Even though the processing and memory parameters are so low, this board is very flexible and a number of extension shields exist that provide various interfacing options to the base controller - in particular, the Ethernet shield allows Uno to connect to the network.
YAMI4 is a messaging solution for distributed systems that was designed with control systems in mind, but porting it to Arduino Uno was particularly challenging due to the memory constraints and very limited run-time support. At the same time, the YAMI4 MISRA-C package was already developed for use in industrial embedded systems and this package was a natural candidate as a basis for the Arduino library. This effort was successful and allowed YAMI4 to enter the world of Arduino.
In short: let's have fun!
The YAMI4 library for use in the Arduino IDE environment exists as a single ZIP file and can be downloaded here:
This package is a free software, distributed with the GPL v. 3.0 license.
In order the install this library, follow the standard procedure, which means executing the Sketch/Import Library.../Add Library... command from the main menu of the Arduino IDE. Choose the YAMI4.zip
file and... that's it. The library will be installed in the directory managed by the IDE and included examples will be available from the File/Examples list as well.
YAMI4 is built on top of the standard Ethernet library and due to the way IDE parses sketches, the Ethernet include directives have to be used as well. The complete sequence looks like this:
#include <SPI.h> #include <Ethernet.h> #include <EthernetUdp.h> #include <YAMI4.h>
After that, the UDP transport has to be set up as usual, which (as copied from standard Ethernet library examples) can be:
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; IPAddress ip(192, 168, 1, 177); // local port to listen on: unsigned int localPort = 12345; EthernetUDP udp; void setup() { // start the Ethernet and UDP: Ethernet.begin(mac,ip); udp.begin(localPort); // see below ... }
The above code is a very minimal setup sequence for the EthernetUDP interface - and once this is done, the YAMI4 agent can be set up on top of it. In particular, you will want to declare the agent as a static object:
YAMI4Agent agent;
and complete the setup()
code by setting up the agent to use the already initialized UDP transport:
void setup() { // ... agent.init(udp, &call); }
Note that call
above is a user-defined function, which will receive incoming YAMI4 messages (that is, requests from remote clients) as well as responses from remote servers. The call
function is where messages have to be parsed, interpreted and acted on. It can have any other convenient name, but "call" is consistently used in all other YAMI4 examples. This function has the following signature:
void call(IPAddress remoteIp, uint16_t remotePort, enum yami_message_type message_type, const char object_name[], const char message_name[], const char exception_ms g[], int64_t message_id, const uint8_t body[], size_t body_size) { // ... }
The parameters of this function are:
IPAddress
and port number of the remote program that has sent the message, most of the time this parameter will be used to show where to send the response,These parameters are used in example programs that are part of the library package and are explained below.
Once all this infrastructure is in place, we only need to let the agent process its events from time to time (at this moment YAMI4 does not use interrupts and has to be driven by user). This can be done conveniently in the loop()
function, either as the only activity or in addition to anything else that the actual application has to do in its main loop:
void loop() { // if there is any incoming message, // the call function will be executed: agent.receive(); delay(100); }
That's it! The remote part can be either another Arduino board or a regular computer with software written in any of the supported programming languages. The YouTube examples show the code in Python for its natural brevity, but this is not a requirement and other languages like Ada, C++, Java and .NET (C#) can be used as well.
The YAMI4Agent
class has functions for sending messages and replies and there is also a set of functions that make it easy to parse and compose message payloads according to the YAMI4 protocol. These functions are taken from the MISRA-C package, which means that user-level code can be shared between these packages or can benefit from the same tools like code generators (this also explains a small naming convention inconsistency, as many of the names follow the MISRA-C package convention instead of the Arduino one).
The YAMI4Blink
is an example sketch that is included in the library package and it is a distributed version of the standard Blink
base example. The idea is to have a remote program sending commands to Arduino with instructions to control the LED.
The most important part of this example is of course the call
function:
// ignore the message body, only consider message_name if (strcmp(message_name, "ON") == 0) { digitalWrite(LED, HIGH); } else { digitalWrite(LED, LOW); }
Hopefully, this is self-descriptive. In this example most of the parameters to call
are ignored and the only one that is taken into account is the message_name
. If it's "ON"
the LED will light up and switched off otherwise. The idea is to control these events from remote location and the complete Python client code that lights up the LED can be:
import yami agent = yami.Agent() agent.send_one_way("udp://192.168.1.177:12345", "LED", "ON")
As already stated, this example can be reimplemented in any other supported programming language, but in all cases the following will be true:
"udp://192.168.1.177:12345"
means that we are sending the UDP packet to the address 192.168.1.177
(this is set up in the sketch, use any other address that is appropriate in your network) and port number 12345
(this is how the UDP transport in Arduino is initialized in the setup()
function),"LED"
will be ignored by Arduino, but it is a good idea to always use some meaningful names for target objects - here we control the LED, so it's like sending the message to the LED itself; use names that make sense in your application,"ON"
will be understood as an instruction to light up the LED (by setting the HIGH
state on the relevant pin, which in this example is pin number 2).Of course, the more you will play with it, the more obvious it will look.
The YAMI4AnalogRead
example is similar to the standard AnalogRead
example that reads the value from one of its analog inputs and displays it. Here, instead of displaying the value on the serial output, the program will send it to remote clients as response to their queries.
The call
function will be a bit more complicated than in the YAMI4Blink
example, because now the function will have to prepare the proper response. This is where the memory constraints of Arduino Uno are mostly visible - with no support for dynamic memory (and with so small amount of it!) it was impossible to retain the API known from general-purpose YAMI4 libraries, where the data model is based on dictionaries that are dynamically created and explored. Instead, the user is involved in preparing the serialized message payload and has to be aware of how the YAMI4 protocol is defined. This will be explained below.
The call
function performs these actions:
const int32_t sensor_value = analogRead(A0);
The value of analog input is obtained and after that the message payload is prepared by putting the items as follows:
1
will be written at the beginning,"value"
here - the remote side will be able to find the field by this name,It is not very difficult to create messages this way and even arrays and recursive data structures can be composed using this scheme. In this example the complete code that prepares the message payload for the reply is:
const int32_t num_of_entries = 1; const size_t reply_buffer_size = 100u; uint8_t reply_body[reply_buffer_size]; // serialized reply body size_t reply_body_size = 0u; // actual size of serialized reply // number of fields yami_put_integer(reply_body, reply_buffer_size, &reply_body_size, num_of_entries); // field name yami_put_cstring(reply_body, reply_buffer_size, &reply_body_size, "value"); // field type yami_put_type(reply_body, reply_buffer_size, &reply_body_size, yami_integer); // field value yami_put_integer(reply_body, reply_buffer_size, &reply_body_size, sensor_value);
Note that the reply_body_size
variable starts with value 0
and is consistently updated to move through the buffer - at the end it has the value that is equal to the size of the complete payload.
Once the response is complete, it can be sent to remote client:
agent.sendReply(remoteIp, remotePort, message_id, reply_body, reply_body_size);
Note how remoteIp
and remotePort
parameters are used in this example. Note also how the message_id
value is used to match response with the request (the matching is done by the client, but Arduino has to send a correct value when responding to the message). Above, reply_body
and reply_body_size
give the information on what should be sent back to the client.
The Python client that sends the message to Arduino and reads the reply can be written in this way:
import yami agent = yami.Agent() msg = agent.send("udp://192.168.1.177:12345", "ADC", "read") msg.wait_for_completion() reply = msg.get_reply() del msg print reply["value"]
The code above shows a typical client-side pattern:
"value"
, which is the same name that was used in the call
function when the response was prepared.The messaging handling interface in general-purpose libraries is much more elaborated (see the documentation for the relevant programming language), the code above is a minimal scheme that allows to execute the client-server interaction in the two-way mode.
In the YAMI4AnalogRead
example the object_name
and message_name
parameters of the call
function are ignored - but can you already see how they might be used to control the blinking LED and an analog readouts in the same device?
This example was not shown in the demo movie, but is provided in the package for completeness and consistency with all other YAMI4 libraries. For every supported programming language there is a "print" example that shows a very basic client-server interaction in the one-way mode, where the client sends lines of text from its input to the server, which just prints them on its output. Now, you can also try to pair the Arduino server with any of the existing client examples and see the lines of text being printed on the Arduino's serial line.
Even though the YAMI4Print
example does not involve any additional electronic components (and is therefore much less spectacular than the examples above), you should analyze its call
function as it shows how message parameters can be parsed by Arduino. The rules are exactly the same as explained with the YAMI4AnalogRead
example, but instead of writing the values to the buffer, they are extracted from the buffer that already exists. Moreover, the code shows how to use error return codes, which were ignored in previous examples for simplicity.
Similarly to the print example, this was also provided for completeness with other YAMI4 libraries. The calculator example is a simple client-server system with two-way messages, where the server replies to client requests. The client sends a message with two integers and the server replies with results of all four basic calculations: sum, difference, product and ratio. Any existing calculator client can be used with Arduino board playing the role of the external computing server.
Again, analyze the call
function to see how the parameters are extracted and note that depending on the client values, the response might or might not contain the ratio result. This shows that YAMI4 parameters have a dynamic nature, which can be useful in some applications.
Due to the memory constraints on Arduino Uno it is not possible to pretend that there is infinite memory in the system, which is a common approach in desktop or even server programming. In fact, the existing 2kB of RAM is small enough that the programmer has to think hard about the kind of messages that are going in both directions. It is possible to tune the YAMI4 library by modifying the following definitions in the YAMI4.h
header file:
/* max size of output message payload */ #define MAX_OUTGOING_MESSAGE_PAYLOAD 250u /* max size of input message payload */ #define MAX_INCOMING_MESSAGE_PAYLOAD 250u
Both buffers take up precious RAM space, and it is also normally necessary to manage an additional buffer for preparing message replies. All this has to be carefully tuned in order to leave enough space for other program needs (stack space and any other functionality). If you do not have any idea what are the safe values for buffer spaces, check the Easy Sniffer tip for some easy ways to analyze YAMI4 messages. Additional hint: incoming and outgoing buffers do not need to be of the same size and in particular, there is no need for the outgoing buffer if the program will never send anything, like in the blinking LED and print examples.
Note that it might be necessary to completely reinstall the YAMI4 library after modifying these values as the IDE tends to reuse the already compiled library files.
Check also the article about using YAMI4 on Arduino Due and stay tuned for descriptions on how to use YAMI4 on other single board devices.