Implementing a Sensor in the BlueRange Firmware

No matter, if you use our BlueRange firmware to implement your custom Mesh Node, be it a light fixture, a valve, a ceiling sensor, or if you want to implement a battery powered Asset Tag to measure CO2, temperature or movement, you will most likely have some sensor data that you want to report to our platform. It will give you the possibility to access a live stream of your data and get access to historical data at the same time.

What you need to implement is our component_sense message.

This guide assumes that you have already followed our tutorial on how to implement a custom module. This will give you the basic template that you need to implement your own logic. You also need to have some sensor attached and implemented that actually allows you to measure and then publish some actual data. However, you can easily use some mock data for the sake of this tutorial.

Two different concepts

There are two different concepts in the BlueRange firmware that I should explain to you. One of them is the Sensor and Actuator concept, the other one is called Module Messages. Module messages are used throughout our firmware to communicate between different devices running BlueRange firmware. Our firmware is split into several functional parts such as enrollment, mesh access, status reporting, etc, …​ and the module messages allow us to group our firmware messages as well. This is mostly used for core functionality but you may use module messages yourself if you intend to communicate between several BlueRange devices.

Most often, you will however want to use the component_sense and component_act messages that are intended to be a tunnel for vendor specific protocols that need to be tunneled through the BlueRange mesh. These messages provide a way to do a vendor specific mapping of functionality while still being able to interpret and map these messages in our platform. Most often, our firmware is used with existing products that are either implemented in one or multiple seperate controllers. In such cases, the component messages are a perfect fit to map your existing protocol and get your existing product working with our platform.

Publishing the Sensor Data

To simplify the example, we will use the TimerEventHandler to generate a new counter value each 5 seconds. This value should be reported through the mesh and should be sent to our platform. Take a look at the following code sample to see how this can be done. The code was intentionally kept as simple as possible. You can choose a larger payload size if you want and you are free to use a struct to put multiple sensor values in the message as well. You will be able to dissect this message on the Gateway side and use a Sensor definition to give more context to this raw data. But more on that in the the next chapter of this guide.

void VendorTemplateModule::TimerEventHandler(u16 passedTimeDs)
{
    //Use e.g. an enum in a central place to hold the definitions for all of your components
    //for your module. You can specify up to 65535 components (different sub-devices)
    //Normally, you would place this definition in your header file
    enum class VendorTemplateComponents : u16
    {
        EXAMPLE_COMPONENT_1 = 0x01,
    };

    //For each component (and each actionType) you can have a list of up to 65535 different "registers"
    //that you can map to any functionality you like
    //If you want, to can use seperate mappings for READ, WRITE, .... but it might make sense to keep them
    //in the same address range, e.g. similar to the Modbus protocol
    //Normally, you would place this definition in your header file
    enum class VendorTemplateComponent1Registers : u16
    {
        EXAMPLE_COUNTER = 0x01,
    };

    static u8 exampleCounter = 0;

    if(SHOULD_IV_TRIGGER(GS->appTimerDs, passedTimeDs, SEC_TO_DS(5)))
    {
        //Fill all the necessary header information
        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;
        //Use NODE_ID_SHORTEST_SINK if other Mesh Nodes do not need to reveice the message
        //Sending the event to NODE_ID_BROADCAST is less common in production setups
        message->componentHeader.header.receiver = NODE_ID_BROADCAST;
        message->componentHeader.moduleId = VENDOR_TEMPLATE_MODULE_ID;
        //UNSPECIFIED is used to report events whereas e.g. READ_RSP is used to report the result of a read request
        message->componentHeader.actionType = (u8)SensorMessageActionType::UNSPECIFIED;
        message->componentHeader.component = (u16)VendorTemplateComponents::EXAMPLE_COMPONENT_1;
        message->componentHeader.registerAddress = (u16)VendorTemplateComponent1Registers::EXAMPLE_COUNTER;
        //Can optionally be used
        message->componentHeader.requestHandle = 0;

        //Assign the counter value to the payload
        message->payload[0] = exampleCounter++;

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

Best Practice for Mapping Component and Registers

When defining a sensor, the developer has to choose a component and a register. These values should be chosen so that the component represents a device and the register represents some kind of value. It is very similar to MODBUS and, if properly defined, a setting will be able to match a whole family of devices and not just a single device. Care has to be taken to avoid migration issues between different firmware versions as well. This is a small collection of tips to minimize mistakes and configuration issues in the future. If you follow these steps, you will be able to create a single device catalog entry that matches a whole family of devices and that can be used with each and every firmware version:

  • Assign different components (attached via UART, SPI, …​.) each to a different componentId. A component could be a lighthead with a number of sensors, a sensor module that produces a number of different measurements, your main control unit, etc …​.

  • Make sure to reserve ranges of components and registers for future use, e.g. if you have a large product portfolio that might be migrated at a later point in time. Grouping the products and registers into address ranges will make your definitions better maintainable and easier to remember.

  • If you are going to produce different kinds of devices (composed of different sub-devices aka. components), make sure to use the same componentId if you are re-using technology. E.g. if you produce a number of luminaires and you have a main controller with a number of sensors that you will be reusing, assign the same component id. You will have to make sure that there is no clash between the register ids. If you are developing a new main unit, it might be a better idea to use a new component id to get a clean start.

  • You must make sure that you will never reuse a register id with the same component id in your entire device family for a different purpose.

  • A single setting or device catalog entry should be able to match each of your firmware versions:

    • Never re-use a component-register pair for a different purpose

    • You are allowed to extend the value for a component-register pair by e.g. sending 10 bytes instead of 8. This will however produce errors for all your old devices once the sensor definitions are updated. This is not critical but might be unwanted.

    • You are not allowed to decrease the number of bytes being sent. This will forever produce errors as you cannot modify the sensor definition file as you still need to support your old devices.

    • You must never change the contents of a component-register pair value. This will generate corrupt data.

  • In all the above cases, you can simply use a new register id and use the new desired value. Then, in your new firmware, mark the old register id as deprecated. You can then duplicate the entries in your sensor definition to match the new register id. You must leave the old entries in the definition to make sure that both your old and your new devices will be supported.

  • Use standard sensor names and units defined in the BlueRange ecosystem wherever possible. This will guarantee the best compatibility with other devices. Make sure to ask us to extend our definitions for your use-case before defining vendor specific entries.

Sensor Action Types

There are a number of different action types for sensor 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

UNSPECIFIED

unspecified

Used by a device to specify that this is an unsolicited message such as an event without any prior request. This is what will be used for most sensor values.

ERROR_RSP

errorRsp

Used to answer requests that could not be processed because of an error

READ_RSP

readRsp

A direct response to a read which contains the data that was read

WRITE_RSP

writeRsp

A direct response to a write which contains the data that was requested to be written

RESULT_RSP

-

A direct response to a WRITE_ACK that contains a result code of the command (not yet implemented in the platform)

You will learn about the corresponding ActionTypes for the actor message in the next part of this tutorial.

Testing the Firmware

After compiling and flashing this firmware (See Firmware Quick Start Guide), your firmware should report the counter value periodically. The easiest way to check if everything is working is to flash the same firmware on two development boards. If you do not flash any UICR information (e.g. just run the generated prod_template_nrf52_flash Utility from within VsCode), the two nodes will automatically be connected to each other and will use a random serial number and nodeId.

If you do not have two devices at hand, you can also flash the firmware on a single device, and you will see the output on the terminal. This is possible because we are sending the sensor data to the broadcast address which means that the device will receive the sent data itself as well and will print the json output on the terminal

If you have a BlueRange Gateway available, you can also enroll your Mesh Node and then activate DEBUG logging through the Gateway Console which will give you the chance to see all mesh messages that are received by the gateway in json format. For enrolling the mesh nodes, consult our BlueRange user manual.

component sense terminal

In the above screenshot you can see that both nodes receive the counter values from each other. As the prod_template_nrf52 featureset was also configured to periodically send status messages, you can also see these in the terminal output.

Now, let’s look at one of these messages:

{
    "nodeId":430, //Exemplary nodeId, randomly generated
    "type":"component_sense",
    "moduleId": 2882339312, //The example module id of the VendorTemplateModule (converted 0xABCD01F0 to decimal)
    "requestHandle":0,
    "actionType":0, //This corresponds to SensorActionType::UNSPECIFIED, meaning it was generated as an event
    "component":"0x0001", //The component you specified
    "register":"0x0001", //The register for this component
    "payload":"Uw==" //Base64 encoded => 0x53
}

The payload Uw== is Base64 encoded. Use a converter such as e.g. Cryptii to decode the value, and you will see that the reported value was 0x53.

Instead of "moduleId": <decimal number> it is also possible to use "module": "<hex number>". It is important to not start the number with 0x.. or with leading zeros. However, this approach is not recommended. Please use "moduleId" with decimal numbers instead.

You have now successfully implemented all the necessary reporting of a sensor value on the firmware side. If you want to implement an Actuator as well, continue with implementing an Actuator. If not, jump directly to implementing Capabilities.