Design multi-sensors applications

In this tutorial we address the design of applications involving multiple sensors.

When dealing with multiple sensors, it is important to understand how to synchronize their data. As we have seen in the previous tutorials, channel elements provide several event-base logics to decide when and how to combine data from multiple input elements. Nevertheless, in some circumstances, it can be useful to combine and modify data with time-base logics. To do so, we can leverage on the timestamp source provided by sensors:

/*
 * sensor_label : the label of a previously declared sensor
 */
sensor_label.get_timestamp();

The timestamp source outputs the Unix timestamp in milliseconds at which the sensor data has been generated.

Collect data and its timestamp

The timestamp source is synchronized with the other sensor sources and it often used in combination with them. To see it in action, consider the following example in which we output the temperature and the timestamp at which the temperature data is generated:

#include <huxon/lang.hpp>
#include <huxon/sensors/STMicroelectronics.hpp>

HUX_DECLARE_SENSOR(temp_sensor, hux::sensors::STMicroelectronics::HTS221);

HUX_DECLARE_CHANNEL(temp_ch, zip_latest,
    temp_sensor.get_temperature(),
    temp_sensor.get_timestamp()
);

HUX_DECLARE_PROCESSING(app_output, temp_ch, {

    float temperature = hux::get<0>(hux_input);
    hux::uint64_t timestamp = hux::get<1>(hux_input);

    HUX_DECLARE_OUTPUT_VALUE(value_out, Float, "value", temperature);
    HUX_DECLARE_OUTPUT_VALUE(ts_out, Long, "timestamp", timestamp);
    HUX_DECLARE_OUTPUT_VALUE(temperature_out, Object, "temperature", value_out, ts_out);

    return temperature_out;
});

HUX_REGISTER_OUTPUT(app_output);

Merge data from two sensors

Consider a scenario in which we have two inertial sensors placed close to each other and physically attached to the same object. For redundancy purposes, we also assume that these two sensors are mounted on two independent sensor boards.

We would like the merge the X axis acceleration sources of the two sensors in a single coherent data stream. The first approach is to use event-base logic with a merge channel:

#include <huxon/lang.hpp>
#include <huxon/sensors/STMicroelectronics.hpp>

HUX_DECLARE_SENSOR(imu_1, hux::sensors::STMicroelectronics::LSM303AGR);
HUX_DECLARE_SENSOR(imu_2, hux::sensors::STMicroelectronics::LSM303AGR);

/* Merge the X acceleration sources from imu_1 and imu_2
 * as soon as one of the two acceleration values is received by the
 * channel, it is immediately emitted as output.
 * */
HUX_DECLARE_CHANNEL(acc_x_ch, merge,
    imu_1.get_accX(),
    imu_2.get_accX()
);

HUX_DECLARE_PROCESSING(app_output, acc_x_ch, {
    HUX_DECLARE_OUTPUT_VALUE(acc_x_out, Float, "acc_x", hux_input);
    return acc_x_out;
});

HUX_REGISTER_OUTPUT(app_output);

Since the two sensors imu_1 and imu_2 are placed on two independent boards, when Huxon partitions the algorithm to the target IoT infrastructure it will place the merge channel logic on a parent node of the imu_1 and imu_2 boards:

Huxon algorithm partitioning

The data from imu_1 and imu_2 is transferred via network to the parent node, and here, the merge channel logic is applied. Hence, the merge channel logic is performed with respect to the arrival time of the data at the parent node.

If the data is sent at high rate, it might be possible that a sample produced by imu_1 before imu_2 reaches the parent node after the sample from imu_2 (as an example due to network latency, packets routing and/or congestion). If we want to merge the data while ordering them with respect to the data generation time rather than the data arrival time at the parent node, we can leverage on the timestamp sources.

To achieve this goal we also need the combine_latest channel logic. A combine latest channel outputs a hux::tuple that combines the most recent input samples from each input when ANY of the input have produced at least one new sample.

Note

If an input of a combine latest channel has produced a new sample but the other has not, the output produced is an hux::tuple in which only one of the inputs is updated, while the other keeps the same value it had before.

The following is an example on how to merge sensors data based on the data generation timestamp:

#include <huxon/lang.hpp>
#include <huxon/sensors/STMicroelectronics.hpp>

HUX_DECLARE_SENSOR(imu_1, hux::sensors::STMicroelectronics::LSM303AGR);
HUX_DECLARE_SENSOR(imu_2, hux::sensors::STMicroelectronics::LSM303AGR);

/* Collect acceleration X and timestamp from imu_1 */
HUX_DECLARE_CHANNEL(imu_1_ch, zip_latest,
    imu_1.get_accX(),
    imu_1.get_timestamp()
);

/* Collect acceleration X and timestamp from imu_2 */
HUX_DECLARE_CHANNEL(imu_2_ch, zip_latest,
    imu_2.get_accX(),
    imu_2.get_timestamp()
);

/* Combine the data from imu_1 and imu_2 with combine latest logic.
 * A combine latest channel outputs a hux::tuple that combines the most recent
 * input samples from each input when ANY of the input have produced
 * at least one new sample.
 * NOTE: if an input has produced a new sample but the other has not, we get
 * a tuple in which only one of the inputs is updated, while the other keeps
 * the same value.
 * */
HUX_DECLARE_CHANNEL(imu_12_ch, combine_latest,
    imu_1_ch,
    imu_2_ch
);

/* Processing node that compares the timestamp of the imu_1 and imu_2 data
 * and returns the data from the sensor with higher timestamp.
 * */
HUX_DECLARE_PROCESSING(time_merge, imu_12_ch, {
    auto &imu_1_data = hux::get<0>(hux_input);
    auto &imu_2_data = hux::get<1>(hux_input);
    hux::uint64_t imu_1_ts = hux::get<0>(imu_1_data);
    hux::uint64_t imu_2_ts = hux::get<1>(imu_2_data);

    if(imu_1_ts > imu_2_ts) {
        return imu_1_data;
    } else {
        return imu_2_data;
    }
});

/* The time_merge node might return as output the same result multiple times.
 * This happens if, for example, imu_1 as higher timestamp, but we receive multiple samples
 * from imu_2 with lower timestamps. In this case the time_merge node will always return
 * the same data from imu_1.
 * To filter out repeated elements with use an on_change channel applied to the
 * output of time_merge.
 * The on_change acts on the tuple <acceleration_x, timestamp>, hence, since timestamps
 * are always increasing, the on_change logic effectively removes duplicated samples.
 * Indeed, the same acceleration value produced at consecutive timestamps leads to
 * distinct tuples.
 * */
HUX_DECLARE_CHANNEL(time_merge_ch, on_change, time_merge);

HUX_DECLARE_PROCESSING(app_output, time_merge_ch, {
    HUX_DECLARE_OUTPUT_VALUE(acc_x_out, Float, "acc_x", hux::get<0>(hux_input));
    return acc_x_out;
});

HUX_REGISTER_OUTPUT(app_output);