Inspirel banner

YAMI4 on Arduino Uno

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!

Installing the library

The YAMI4 library for use in the Arduino IDE environment exists as a single ZIP file and can be downloaded here:

YAMI4.zip

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.

Using YAMI4

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:

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).

YAMI4Blink

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:

Of course, the more you will play with it, the more obvious it will look.

YAMI4AnalogRead

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:

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:

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?

YAMI4Print

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.

YAMI4Calculator

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.

Memory tuning

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.