Contents

Arduino C++ Controls Messaging

14 January 2023

MessageData Class

The MessageData class provides the message processing for incoming messages and stores the results. It is an attribute of each connection and is available as a parameter in the incoming message callback that you must setup for each connection (BLE, TCP or MQTT). The connection incoming message callback should take the form:

void processIncomingMessage(MessageData *messageData) {
    switch (messageData->control) {
    case status:
      // TODO send update message from each control to the Dash IoT dashboard
      break;
    case button:
      // TODO process the incoming button message
      break;
    case slider:
      // TODO process the incoming slider message
      break;

      // etc
      // etc
      // etc

    default:
      break;
    }
}

The important attributes of the MessageData are:

  • connectionType - Set at instantiation of the class and use to define the type of connection: enum {BLE_CONN, TCP_CONN, MQTT_CONN}. The MessageData must be instantiated with the connectionType as an attribute.
  • control - Contains the Control_Type of the received message: enum {deviceView, label, button, menu, buttonGroup, eventLog, slider, knob, dial, direction, textBox, selector, chart, timeGraph, mapper, colorPicker, audioVisual}.
  • idStr - Contains the control_ID of the received message.
  • payloadStr - Contains the primary payload field of the received message.
  • payloadStr2 - Contains the. secondary payload field of the received message.

Messageing Principles

Controls within an IoT device may send an receive messages at any time the IoT device is connected.

  • Receiving a message allows the IoT device to take action according to the contents of the message received from the Dash app.
  • Sending a message allows the IoT device to send information or provide feedback to the user of the Dash app.
  • It is good practice to reply to a received control message. This is to provide feedback to the user of the Dash app that an action has taken place, or not.

Incoming Messages

Incoming messages are received in the incoming message callback, which includes the MessageData as a parameter. The contents of the incoming message are contained within the MessageData class as described above.

Use the control (Control_Type) and idStr (ControlID) to address the control the MessageData is referring to. Then, use payloadStr and payloadStr2 to access the contents of the message.

For example, consider when a button message is received from the Dash app. The received message tells tha IoT device that the particular button, addressed by the control and idStr, has been tapped on the Dash app and needs to be toggled in the IoT device. The payloadStr or payloadStr2 message components are not used in this case:

void processIncomingMessage(MessageData *messageData) { // callback
    switch (messageData->control) {
    case button: // Process Button
        if (messageData->idStr == "BUTTON_ID") {
            buttonValue = !buttonValue;
        }
    default:
        break;
    }
}

where buttonValue is a boolean representing whether the button should be display as on (true) or off (false) on the Dash app.

A button control reply message may be created in the IoT device and returned to a connection (in this example both BLE or MQTT) as follows:

void processIncomingMessage(MessageData *messageData) { // callback
    switch (messageData->control) {
    case button: // Process Button
        if (messageData->idStr == "BUTTON_ID") {
            buttonValue = !buttonValue;
            String message = dashioDevice.getButtonMessage("BUTTON_ID", buttonValue);
            if (messageData->connectionType == BLE_CONN) {
                ble_con.sendMessage(message);
            } else if (messageData->connectionType == MQTT_CONN) {
                mqtt_con.sendMessage(message);
            }
        }
    default:
        break;
    }
}

An instance of DashioDevice is used to create the button message by using the function dashioDevice.getButtonMessage("BUTTON_ID", buttonValue)

Sending Messages

Once messages are created, they are sent through connections and are typically, but not always, sent as a reply through the connection where an incoming message arrived. Below is an example of a method to send a message to a specific connection:

void sendMessage(ConnectionType connectionType, const String& message) {
    if (connectionType == TCP_CONN) {
        tcp_con.sendMessage(message);
    } else if (connectionType == BLE_CONN) {
        ble_con.sendMessage(message);
    } else {
        mqtt_con.sendMessage(message);
    }
}

All Controls

In the following sub-sections, controlID is the control identifier and message is a String containing the contents of a message the may be sent to the Dash app through a connection (BLE, TCP or MQTT).

All time infomration is sent and received as an IOS 8601 format String representing the UTC time i.e. "yyyy-MM-dd’T’HH:mm:ssZ".

All color information is a String with the color name (as used in the Dash app, e.g. "blue", or a hex color, e.g. "#RRGGBB")

Button

Incoming Messages

An incoming Button control message indicates that the Button control on the Dash app has been tapped and the state on the Button control in the IoT device should be toggled.

MessageData Payload

  • Payload: unused
  • Payload2: unused

Outgoing Messages

Button outgoing messages can send the button state, which only changes the button's color on the Dash app, or it can also specify the icon and text to be displayed.

message = dashioDevice.getButtonMessage(controlID, on);
message = dashioDevice.getButtonMessage(controlID, on, iconName);
message = dashioDevice.getButtonMessage(controlID, on, iconName, text);

Parameters

  • on: bool indicating if the button is on (true) or off
  • iconName: String parameter containing the name of the icon to display on the button. If blank (""), the icon on the button is NOT changed.
  • text: String parameter containing the text to display on the button. If blank, (""), the text on the button is NOT changed. If text is NOT blank, setting the iconName to blank will remove the icon from the button.

Example Code

Process incoming status request and respond to a button message by toggling the button state and replying:

bool btnState = false;

void processIncomingMessage(MessageData *messageData) {
    switch (messageData->control) {
    case status:
        String message = dashioDevice.getButtonMessage("BT01", btnState);
        sendMessage(messageData->connectionType, message);
        break;
    case button:
        if (messageData->idStr == “BT01”) {
            btnState = !btnState;
            String message = dashioDevice.getButtonMessage("BT01", btnState);
            sendMessage(messageData->connectionType, message);
        }
        Break;
    }
}

Send the button state, change the button icon to "stop" and change the button text to "OFF":

String message = dashioDevice.getButtonMessage("BT01", btnState, "stop", "OFF);
sendMessage(messageData->connectionType, message);

Slider

Incoming Messages

An incoming Slider control message indicates that the Slider control on the Dash app has been moved.

MessageData Payload

  • Payload: Value of the new position of the slider.
  • Payload2: unused

Outgoing Messages

message = dashioDevice.getSliderMessage(controlID, value);
message = dashioDevice.getSingleBarMessage(controlID, value);
message = dashioDevice.getDoubleBarMessage(controlID, value1, value2);

The slider outgoing messages are of three different types:

  • getSliderMessage: set the position of the knob on the slider (float or int).
  • getSingleBarMessage: set the position of a singe bar under the slider (float or int).
  • getDoubleBarMessage: set the position of two bars under the slider (float or int).

Parameters

  • value, value1, value2: position of the knob or bar. The minimum and maximum values of the slider are specified in the slider's configuration data.

Example Code

Process incoming status request and respond to a slider message by updating the sliderValue:

float sliderValue = 60;

void processIncomingMessage(MessageData * messageData) {
    switch (messageData->control) {
    case status:
        String message = dashioDevice.getSliderMessage("SL01", sliderValue);
        sendMessage(messageData->connectionType, message);
        break;
    case slider:
        if (messageData->idStr == "SL01") {
            sliderValue = messageData->payloadStr.toFloat();
        }
        break;
    }
}

Send the slider bar values (only required if the slider bar is not set to follow the slider knob):

float barValue1 = 40;
float barValue2 = 50;

String message = dashioDevice.getDoubleBarMessage("SL01", barValue1, barValue2);
connection.sendMessage(message);

Knob

Incoming Messages

An incoming Knob control message indicates that the Knob control on the Dash app has been rotated.

MessageData Payload

  • Payload: New value of the Knob position.
  • Payload2: unused

Outgoing Messages

message = dashioDevice.getKnobMessage(controlID, value);
message = dashioDevice.getKnobDialMessage(controlID, value);

The knob messages are of two different types:

  • getKnobMessage: set the position of the knob (float or int).
  • getKnobDialMessage: set the position of a dial surrounding the knob (float or int).

Parameters

  • value: position of the knob or dial. The minimum and maximum values of the knob are specified in the knob's configuration data.

Example Code

Process incoming status request and respond to a knob message by updating the knobValue:

float knobValue = 30;

void processIncomingMessage(MessageData * messageData) {
    switch (messageData->control) {
    case status:
        String message = dashioDevice.getKnobMessage("KB01", knobValue); // int or float
        sendMessage(messageData->connectionType, message);
        break;
    case knob:
        if (messageData->idStr == "KB01") {
            knobValue = messageData->payloadStr.toFloat()
        }
        break;
    }
}

Send the knob dial value (only required if the dial is not set to follow the knob):

float dialValue = 20;

String message = dashioDevice.getKnobDialMessage("KB01", dialValue);
connection.sendMessage(message);

Dial

Incoming Messages

There are no incoming messages for the Dial control.

Outgoing Messages

Dial control outgoing messages contain the position of the dial.

message = dashioDevice.getDialMessage(controlID, value);

Parameters

  • value: position of the dial (float or int). The minimum and maximum values of the dial are specified in the dial's configuration data.

Example Code

Process incoming status request:

float dialValue = 5;

void processIncomingMessage(MessageData * messageData) {
    switch (messageData->control) {
    case status:
        String message = dashioDevice.getDialMessage("D01", dialValue); // int or float
        sendMessage(messageData->connectionType, message);
        break;
    }
}

Send the dial value:

String message = dashioDevice.getDialMessage("D01", dialValue);
connection.sendMessage(message);

Direction

Incoming Messages

There are no incoming messages for the Direction control.

Outgoing Messages

Direction control outgoing messages contain the position of the direction indicator.

message = dashioDevice.getDirectionMessage(controlID, direction);
message = dashioDevice.getDirectionMessage(controlID, direction, speed);

Parameters

  • direction: direction in decimal degrees (float or int).
  • speed: speed where the units are specified in the direction control's configuration data (float).

Example Code

Process incoming status request:

int windDirectionIndex = 5;
float windSpeedKnotts = 32;

void processIncomingMessage(MessageData * messageData) {
    switch (messageData->control) {
    case status:
        String message = dashioDevice.getDirectionMessage("WD01", windDirectionIndex, windSpeedKnotts);
        sendMessage(messageData->connectionType, message);
        break;
    }
}

Send the direction and speed values:

String message = dashioDevice.getDirectionMessage("WD01", windDirectionIndex, windSpeedKnotts);
connection.sendMessage(message);

Text Box

Incoming Messages

An incoming Text Box control message contains a text string that can be used by the IoT device as text or converted to a number.

MessageData Payload

  • Payload: Text string
  • Payload2: unused

Outgoing Messages

Text Box control outgoing messages contain the text to display on the Text Box on the Dash app.

message = dashioDevice.getTextBoxMessage(controlID, text);

Parameters

  • text: String to be displayed in the text box.

Example Code

Process incoming status request and respond to a text message by updating the textValue:

String textValue = "";

void processIncomingMessage(MessageData * messageData) {
    switch (messageData->control) {
    case status:
        String  message = dashioDevice.getTextBoxMessage("TB01", String(menuTextBoxValue));
        sendMessage(messageData->connectionType, message);
        break;
    case textBox:
        if (messageData->idStr == "TB01") {
            textValue = messageData->payloadStr.toFloat();
        }
        break;
    }
}

Send the text value:

String message = dashioDevice.getTextBoxMessage("TB01", textValue);
connection.sendMessage(message);

Selector

Incoming Messages

An incoming Selector control message indicates that the Selector control on the Dash app has had a selection made.

MessageData Payload

  • Payload: Index of the snew election.
  • Payload2: unused

Outgoing Messages

Text Selector control outgoing messages contain the current selection index, and optionally, the selection items.

message = dashioDevice.getSelectorMessage(controlID, index);
message = dashioDevice.getSelectorMessage(controlID, index, selectionItems, numItems);

Parameters

  • index: index to the currently selected item.
  • selectionItems: array of String where eack element in the arrray is a selection item.
  • numItems: number of selection items.

Example Code

Process incoming status request and respond to a selector message by updating the selectorIndex:

int selectorIndex = 1;

void processIncomingMessage(MessageData * messageData) {
    switch (messageData->control) {
    case status:
        String selection[] = {F("Dogs"), F("Bunnys"), F("Cats")};
        String message = dashioDevice.getSelectorMessage("SR01", selectorIndex, selection, 3);
        sendMessage(messageData->connectionType, message);
        break;
    case selector:
        if (messageData->idStr == "SR01") {
            selectorIndex = messageData->payloadStr.toInt();
        }
        break;
    }
}

Menu

Incoming Messages

An incoming Menu control message indicates that the Menu control on the Dash app has been tapped.

MessageData Payload

  • Payload: unused
  • Payload2: unused

Other incoming messages are handled by the Menu's sub-controls.

Outgoing Messages

There are no outgoing messages available for the Menu control. All outgoing messages are handled by the Menu's sub-controls.

Button Group

Incoming Messages

An incoming Button Group control message indicates that the Button Group control on the Dash app has been tapped.

MessageData Payload

  • Payload: unused
  • Payload2: unused

Other incoming messages are handled by the Button Group's sub-controls.

Outgoing Messages

There are no outgoing messages available for the Button Group control. All outgoing messages are handled by the Button Group's sub-controls.

Chart

Incoming Messages

There are no incoming messages for the Chart control.

Outgoing Messages

Chart control outgoing messages contain the information for a single line to display on the Chart on the Dash app.

message = dashioDevice.getChartLineInts(controlID, lineID, lineName, lineType, color, lineData, dataLength);
message = dashioDevice.getChartLineFloats(controlID, lineID, lineName, lineType, color, lineData, dataLength);

Parameters

  • lineID: String identifier of the chart line.
  • lineName: Name of the chart line
  • lineType: Enumerator for the style or type of line (see LineType below).
  • color: color of the line.
  • lineData: array or int or float.
  • dataLength: number of elements in the lineData array.
enum LineType {
    line,    // Line betwen each point
    bar,     // Bar chart
    segBar,  // Bar chart with segmented bars (not available in Time Graphs)
    peakBar, // Bar chart with a howizontal line at the top or peak.  (not available in Time Graphs) instead of a filles or segmented bar. (not available in Time Graphs)
    bln,     // Shades an area of the graph (only available in TimeGraphs)
};

Example Code

Send chart data for 3 lines;

int data1[] = {150, 270, 390, 410, 400};
String message = dashioDevice.getChartLineInts("G01", "L1", "Line One", line, "3", data1, sizeof(data1)/sizeof(int));
int data2[] = {160, 280, 400, 410, 420};
message += dashioDevice.getChartLineInts("G01", "L2", "Line Two", line, "4", data2, sizeof(data2)/sizeof(int));
int data3[] = {170, 290, 410, 400, 390};
message += dashioDevice.getChartLineInts("G01", "L3", "Line Three", line, "5", data3, sizeof(data3)/sizeof(int));
line, "8", data5, sizeof(data5)/sizeof(int));
connection.sendMessage(message);

Time Graph

Incoming Messages

An incoming Time Graph control message indicates that Dash app require an update of the line data for the Time Graph.

MessageData Payload

  • Payload: UTC time String from which the Time Graph on the Dash app requires line data.
  • Payload2: unused

Outgoing Messages

Time Graph control outgoing line messages contain the information for a single line to display on the Time Graph on the Dash app.

TimeGraph line messages may contain all the metadata (name, color etc.) and line data, or just the metadata. TimeGraph point messages are a short format messages for just sending a single point on the graph line.

message = dashioDevice.getTimeGraphLineFloats(controlID, graphLineID, lineName, lineType, color, String times, lineData, dataLength);
message = dashioDevice.getTimeGraphLineFloats(controlID, graphLineID, lineName, lineType, color, String times, lineData, dataLength, breakLine);
message = dashioDevice.getTimeGraphLineBools(controlID, graphLineID, lineName, lineType, color, String times, lineData, dataLength);
message = dashioDevice.getTimeGraphLine(controlID, graphLineID, lineName, lineType, color);
message = dashioDevice.getTimeGraphPoint(controlID, graphLineID, value);
message = dashioDevice.getTimeGraphPoint(controlID, graphLineID, time, value);

Parameters

  • graphLineID: String identifier of the graph line.
  • lineName: Name of the graph line
  • lineType: Enumerator for the style or type of line (see LineType in Chart control above).
  • color: color of the line.
  • times array of time Strings.
  • lineData: array of float or bool where each element matches the correcponding time in the times parameter.
  • breakline: Optional. When true, creates a break in the line data toseperate widely dispersed points.
  • dataLength: number of elements in the lineData array.
  • time: Time of the graph line point. If the time is not used, the current time will be inserted by either the dash server or Dash app when the message is received. optional
  • value: float value of a single point.

Example Code

Process incoming status request by updating the line names and colors for lines "l1" and "l2":

void processIncomingMessage(MessageData * messageData) {
    switch (messageData->control) {
    case status:
        String message = dashioDevice.getTimeGraphLine("TG01", "l1", "Roger", line, "purple");
        message += dashioDevice.getTimeGraphLine("TG01", "l2", "Betty", line, "white");
        sendMessage(messageData->connectionType, message);
        break;
    }
}

Send a Time Graph data point to the Dash app. The dash server will also store this data point. The receiver of the message will assign a timestamp to the data point:

float num = 44;
String message = dashioDevice.getTimeGraphPoint("TG01", "l1", num);
connection.sendMessage(message);

Send a Time Graph data point with timestamp to the Dash app. The dash server will also store this data point. This example uses the TimeLib library: https://github.com/PaulStoffregen/Time. The time could be obtained from external sources such as a Real Time Clock, internet NTP time service or GPS time data:

#include <TimeLib.h>

static String timeToString(long time) {
  char buffer[22];
  sprintf(buffer, "%04d-%02d-%02dT%02d:%02d:%02dZ", year(time), month(time), day(time), hour(time), minute(time), second(time)); // Z = UTC time
  String dateTime(buffer);
  return dateTime;
}

float num = 33;
long time = 1666957371; // time as seconds since Jan 1 1970
String localTimeStr = timeToString(time);

String message = dashioDevice.getTimeGraphPoint("TG01", "l1", localTimeStr, num);
connection.sendMessage(message);

Event Log

Incoming Messages

There are no incoming messages for the Event Log

Outgoing Messages

EventLog outgoing messages contain event data.

message = dashioDevice.getEventLogMessage(controlID, color, textArr, numTextRows);
message = dashioDevice.getEventLogMessage(controlID, timeStr, color, textArr, numTextRows);
message = dashioDevice.getEventLogMessage(controlID, eventsArr, numEvents);

Parameters

  • timeStr: Event time. If the timeStr is left blank (""), the current time will be inserted by either the dash server or Dash app when the message is received.
  • color: color of the event.
  • textArr: array of row of text to display on th event.
  • numTextRows: number of elements in the textArr.
  • eventsArr: array of Event struct (see below).
  • numEvents: umber of elements in the eventsArr
struct Event {
    String time;    // Event time
    String color;   // Color of the event.
    String *lines;  // Lines of text
    int numLines;   // Number of lines of text
};

Example Code

Send an event to the Dash app. The Dash server will also store this event. The receiver of the message will assign a timestamp to the event:

String message = dashioDevice.getEventLogMessage("LG01", "blue", lines, 2);

Send an event with timestamp to the Dash app. The dash server will also store this event. This example uses the TimeLib library: https://github.com/PaulStoffregen/Time. The time could be obtained from external sources such as a Real Time Clock, internet NTP time service or GPS time data:

#include <TimeLib.h>

static String timeToString(long time) {
  char buffer[22];
  sprintf(buffer, "%04d-%02d-%02dT%02d:%02d:%02dZ", year(time), month(time), day(time), hour(time), minute(time), second(time)); // Z = UTC time
  String dateTime(buffer);
  return dateTime;
}

float num = 33;
long time = 1666957371; // time as seconds since Jan 1 1970
String eventTimeStr = timeToString(time);

String message = dashioDevice.getEventLogMessage("LG01", eventTimeStr, "blue", lines, 2);
connection.sendMessage(message);

Send an array of time-stamped events. In this case, the array contains only one event: Event event; event.time = eventTimeStr; event.color = "green"; event.lines = lines; event.numLines = 2; Event events[1]; events[0] = event; String message = dashioDevice.getEventLogMessage("LG01", events, 1); connection.sendMessage(message);

Map

Incoming Messages

An incoming Map control message indicates that Dash app require an update of the asset or track data for the Map control.

MessageData Payload

  • Payload: UTC time String from which the Map on the Dash app requires track data.
  • Payload2: unused

Outgoing Messages

Map control outgoing messages contain the information for a single asset or track to display on the Map on the Dash app.

Map outgoing messages provide GPS data to the map control. The message can be either a single waypoint or many waypoints that form part of a track.

message = dashioDevice.getMapWaypointMessage(controlID, trackID, latitude, longitude);
message = dashioDevice.getMapTrackMessage(controlID, trackID, text, colour, waypoints, numWaypoints);
message = dashioDevice.getMapTrackMessage(controlID, trackID, text, colour);

Parameters

  • trackID: String identifier of the map track.
  • latitude: Latitude decimal degrees as a String.
  • longitude: Longitude decimal as a String.
  • text: Test that is displayed on the asset pin.
  • color: Colour of the track and asset pin.
  • waypoints: Array or GPS Waypoint data (see below). Fields that are not required may be left blank ("").
  • numWaypoints: Number of waypoints in the waypoint array.
struct Waypoint {
    String time;        // Time of the waypoint
    String latitude;    // Latitude in decimal degrees
    String longitude;   // Longitude in decimal degrees
    String avgeSpeed;   // Average speed since the last message in meters/second
    String peakSpeed;   // Maximum speed since the last message in meters/second
    String course;      // Course direction in decimal degrees. A negative value indicates an unknown heading
    String altitude;    // Altitude in meters
    String distance;    // Accumulated distance since the last message in meters
};

Example Code

Process incoming status request:

void processIncomingMessage(MessageData * messageData) {
    switch (messageData->control) {
    case status:
        String message = dashioDevice.getMapTrackMessage("MP01", "T3", "StatusX", "blue");
        sendMessage(messageData->connectionType, message);
        break;
    }
}

Send a Map waypoint lat/lon message: ``` float lat = -43.559880 + (float)r / 10000; char latBuffer[16]; sprintf(latBuffer, "%f", lat);

float lon = 172.655620 + (float)random(-49, 50) / 10000; char lonBuffer[16]; sprintf(lonBuffer, "%f", lon);

String message = dashioDevice.getMapWaypointMessage(MAP_ID, "T3", latBuffer, lonBuffer); connection.sendMessage(message); ```

Color Picker

Incoming Messages

An incoming Color Picker control message indicates that a new color has been chosen from the Color Picker control on the Dash app.

MessageData Payload

  • Payload: New color String.
  • Payload2: unused

Outgoing Messages

message = dashioDevice.getColorMessage(controlID, color);

Parameters

  • color: Current colour of the colorPicker control.

Example Code

Process incoming status request and respond to a color message by updating the colorStr:

String colorStr = "";

void processIncomingMessage(MessageData * messageData) {
    switch (messageData->control) {
    case status:
        String message = dashioDevice.getColorMessage("CP01", "#0080FF");
        sendMessage(messageData->connectionType, message);
        break;
    case color:
        colorStr = messageData->payloadStr;
        break;
    }
}

Audio Visual

Incoming Messages

There are no incoming messages for the Audio Visual control.

Outgoing Messages

message = dashioDevice.getAudioVisualMessage(controlID, url);

Parameters

  • url: Set the URL of the audioVisual control.

Example Code

Process incoming status request:

void processIncomingMessage(MessageData * messageData) {
    switch (messageData->control) {
    case status:
        String message = dashioDevice.getAudioVisualMessage(AV_ID, F("http://192.168.68.170/mjpeg/1"));
        sendMessage(messageData->connectionType, message);
        break;
    }
}

Label

The Label control does not receive or send messages.

Alarms

Alarm messages may be sent to the dash server, which in turn sends push notifications to your mobile devices.

Incoming Messages

There are no incoming messages for the Audio Visual control.

Outgoing Messages

String getAlarmMessage(const String& alarmID, const String& title, const String& description);
String getAlarmMessage(Notification alarm);

Parameters

  • title: title of the alarm message
  • description: description of the alarm
  • alarm: structured details of an alarm (see below)
struct Notification {
    String identifier;    // Identifier of the notification.
    String title;         // Displayed in the notification title.
    String description;   // Displayed in the notification body.
};

Example Code

Send an alarm message to the dash server. This can only be done on an MQTT connection:

String message = dashioDevice.getAlarmMessage("AL03", "An Alarm", "This is a test alarm");
mqtt_con.sendAlarmMessage(message);

Concatenating Status Request Messages

Messages can be concatenated and sens as a single message. This is particularly usef for sending all the Status request messages. The followinf example shows how to create a function for responding to the Srtatus request message for all controls. A message variable is created and sufficient memory is allocated for the concatentated messages and the message for each control is added. Don't concatenate too many messages that you exceed you reserved memory for the message as you can always sent them in smaller groups:

void processStatus(ConnectionType connectionType) {
    String message((char *)0);
    message.reserve(1024);

    // First group
    message = dashioDevice.getButtonMessage("BT01", btnState);
    String selection[] = {F("Dogs"), F("Bunnys"), F("Cats")};
    message += dashioDevice.getSelectorMessage("SR01", selectorIndex, selection, 3);
    message += dashioDevice.getKnobMessage("KB01", knobValue);
    message += dashioDevice.getTextBoxMessage("TB01", String(menuTextBoxValue));
    sendMessage(connectionType, message);
    
    // Second group
    message = dashioDevice.getColorMessage("CP01", "#0080FF");
    message += dashioDevice.getAudioVisualMessage(AV_ID, F("http://192.168.68.170/mjpeg/1"));
    message += dashioDevice.getMapTrackMessage("MP01", "T3", "StatusX", "blue");
    sendMessage(connectionType, message);    
}

void processIncomingMessage(MessageData *messageData) {
    switch (messageData->control) {
    case status:
        processStatus(messageData->connectionType);
        break;
    case button:
      // TODO process the incoming button message
      break;
    case slider:
      // TODO process the incoming slider message
      break;
         
      // etc
      // etc
      // etc

    default:
        break;
    }
}

Sending Messages in the Loop

It is good practice to only send messages from the loop() when the processor has time to do so. An messages should never be sent within an interrupt. A good way to manage sending messages is to create a global variable, e.g. String messageToSend that can be set with a message anywhere in the code and is sent in the loop(). The example below shows how to setup and reserve memory for the messageToSend:

String messageToSend = ((char *)0);

void setup() {
    messageToSend.reserve(200);
}

loop() {
    ble_con.run();
    wifi.run();

    if (messageToSend.length() > 0) {
        ble_con.sendMessage(messageToSend);
        tcp_con.sendMessage(messageToSend);
        mqtt_con.sendMessage(messageToSend);
        messageToSend = "";
    }
}

You simply assign messageToSend with one or more messages and let the loop() send it when it's ready:

messageToSend = dashioDevice.getTimeGraphPoint(TGRAPH_ID, "l1", eventTimeStr, num);