Implementing an Actuator in the BlueRange Firmware

Let’s imagine you want to build a light fixture that is controllable by a smartphone and also controllable through a 3rd party backend e.g. for configuring different light scenarios. With BlueRange you can do that (of course). After you have now learned from the previous chapter how to automatically report sensor information, such as e.g. the brightness of a lamp, its power consumption, etc,…​ you will now learn how to implement a mechanism for actually controlling it in a vendor independent way. After completing the final step of this guide, you will know how the BlueRange platform can manage devices from a wide range of vendors at the same time.

This guide assumes that you have an nRF52-DK or an nRF52840 DK or some other hardware that is equipped with an LED or sth. else that will actually result in some visible output.

Implementing a Simple Actuator

For an actuator, you will want to add some code to the MeshMessageReceivedHandler that checks all incoming mesh messages and toggles a debug LED on your device after the correct message was received. Two lines were added to disable the LED control for the IoModule as this would always interfere with the LED state that you are setting.

Actor Action Types

There are a number of different action types for actor values. The different types are listed in our Sensors and Actuators documentation. Their corresponding mapping in our MQTT RWIO API is listed in our RWIO API documentation.

ActionType MQTT Topic Description

RESERVED

-

Reserved for future use

WRITE

write

Used to write a value to a register, characteristic, variable, exceute a command …​.

READ

read

Used to request a read from a register, characteristic, variable, …​.

WRITE_ACK

writeAck

Similar to write but will generate an acknowledge message with the value that was requested to be written or a RESULT_RSP

For most vendor specific protocols you should not mix between READ_RSP/WRITE_RSP and RESULT_RSP. You will either have a protocol that is based on writing and reading from registers or you have a protocol that is intended to execute commands and respond with result codes.

Whenever possible, your protocol should use read an write actions as the BlueRange mesh and platform do not provide a guaranteed message delivery. It is therefore important that messages can be re-sent if necessary in case a timeout or an error occurs. Error handling will be very complicated if your vendor protocol does not support idempotent messages. This means, instead of using messages such as "Configure new luminaire and return its handle", you should use "Configure new luminaire with handle 0". This will allow you to re-send the message multiple times without changing the state of your device more than once.
#include <IoModule.h>

[...]

void VendorTemplateModule::MeshMessageReceivedHandler(BaseConnection* connection, BaseConnectionSendData* sendData, ConnPacketHeader const * packetHeader)
{
    //Must call superclass for handling
    Module::MeshMessageReceivedHandler(connection, sendData, packetHeader);

    if(packetHeader->messageType == MessageType::COMPONENT_ACT && sendData->dataLength >= SIZEOF_CONN_PACKET_COMPONENT_MESSAGE_VENDOR){
        ConnPacketComponentMessageVendor const * packet = (ConnPacketComponentMessageVendor const *)packetHeader;

        //Check if the component_act message was adressed to our module
        if(packet->componentHeader.moduleId == vendorModuleId){
            if(
                sendData->dataLength >= SIZEOF_CONN_PACKET_COMPONENT_MESSAGE_VENDOR + 1
                && (
                    packet->componentHeader.actionType == (u8)ActorMessageActionType::WRITE
                    || packet->componentHeader.actionType == (u8)ActorMessageActionType::WRITE_ACK
                )
                && packet->componentHeader.component == (u8)VendorTemplateComponents::EXAMPLE_COMPONENT_1
                && packet->componentHeader.registerAddress == (u8)VendorTemplateComponent1Registers::EXAMPLE_LED
            ) {
                //Stop the IoModule from changing the LED state
                IoModule* ioMod = (IoModule*)GS->node.GetModuleById(ModuleId::IO_MODULE);
                if(ioMod != nullptr) ioMod->currentLedMode = LedMode::CUSTOM;

                //Change the LED state depending on the payload sent
                if(packet->payload[0]){
                    GS->ledRed.On();
                } else {
                    GS->ledRed.Off();
                }

                logt("TMOD", "LED is now %s", packet->payload[0] ? "on" : "off");

                //Send a response
                if(packet->componentHeader.actionType == (u8)ActorMessageActionType::WRITE_ACK)
                {
                    DYNAMIC_ARRAY(buffer, sizeof(ConnPacketComponentMessageVendor) + 1);
                    ConnPacketComponentMessageVendor* message = (ConnPacketComponentMessageVendor*)buffer;
                    message->componentHeader.header.messageType = MessageType::COMPONENT_SENSE;
                    message->componentHeader.header.sender = GS->node.configuration.nodeId;
                    //Answer the sender of this message
                    message->componentHeader.header.receiver = packetHeader->sender;
                    message->componentHeader.moduleId = VENDOR_TEMPLATE_MODULE_ID;
                    //Use a WRITE_RSP to answer a WRITE_ACK
                    message->componentHeader.actionType = (u8)SensorMessageActionType::WRITE_RSP;
                    message->componentHeader.component = (u16)VendorTemplateComponents::EXAMPLE_COMPONENT_1;
                    message->componentHeader.registerAddress = (u16)VendorTemplateComponent1Registers::EXAMPLE_LED;
                    message->componentHeader.requestHandle = 0;

                    //Assign the current state to the payload
                    message->payload[0] = packet->payload[0];

                    //Send the message through the mesh network
                    GS->cm.SendMeshMessage(
                        buffer,
                        SIZEOF_CONN_PACKET_COMPONENT_MESSAGE_VENDOR + 1);
                }
            }
        }
    }
}

Testing the Actuator on the Terminal

Now, flash this firmware on one or two development boards, open a terminal and type the following:

component_act [nodeId] [moduleId] [actionType] [component] [registerAddress] [hexString]

//Switches the LED on using WRITE
component_act 0 0xABCD01F0 1 1 2 01

//Switches the LED off
component_act 0 0xABCD01F0 1 1 2 00

//Switches the LED on using WRITE_ACK (will generate a WRITE_RSP response)
component_act 0 0xABCD01F0 3 1 2 01

Check out the documentation for the component_act command for more information.

Sending these commands to the broadcast address (0) will turn the LED off for all nodes in the current mesh network. It can also be seen in this example, how we immediately respond with a message that confirms the current status of the LED using an actionType WRITE_RSP. As there is quite a bit of boilerplate code involved, you could wrap the sending functionality into a reusable method.

If you want, you can also try out our Java code sample for triggering an actuator that is available here on GitHub.

You are now ready to continue with the Capabilities chapter.