Datalogger: A Semi-Realistic Project (Threaded Version)#

Add Second Sensor (Message Queue)#

  • Add second sensor

  • ⟶ From interrupt, communicate sensor name

Attention

std::string_view does not own ⟶ Lifetime!

../../../../../../_images/datalogger-two-sensors.svg
#include <why.h>

#include <why-sensor.h>
#include <why-irq.h>
#include <why-messagequeue.h>
#include <why-time.h>

#include <string>


struct sensor_data
{
    Why::TimeSpec timestamp;
    std::string_view sensorname;
    uint64_t value;
};

int main()
{
    Why::init();

    Why::RandomSensor s1(0, 100);
    Why::RandomSensor s2(100, 200);

    auto sensor_data_queue = Why::MessageQueue<sensor_data>::create();
    assert(sensor_data_queue);

    // activate sensor interrupts
    {
        auto isr = [&sensor_data_queue, &s1, &s2](int irqnum){
            sensor_data data;
            if (irqnum == 7)
                data = {
                    .timestamp=Why::now_monotonic(), 
                    .sensorname="s1",                  // <-- std::string_view lifetime!
                    .value=s1.get_value(),
                };
            else if (irqnum == 42)
                data = {
                    .timestamp=Why::now_monotonic(), 
                    .sensorname="s2",                  // <-- std::string_view lifetime!
                    .value=s2.get_value(),
                };
            else
                assert(!"unexpected irq");
        
            auto ok = sensor_data_queue->put_noblock(data);
            assert(ok);
        };
        Why::IRQ::connect(7, isr);
        Why::IRQ::connect(42, isr);
    }

    // consume data
    while (true) {
        auto sample = sensor_data_queue->get();
        assert(sample);
        auto [timestamp, sensorname, value] = *sample;
        std::println("timestamp={}, sensorname={}, value={}", timestamp.to_seconds(), sensorname, value);
    }
    return 0;
}

Add Watchdog#

  • Shift sensor data consumption into a dedicated thread

  • Feed Watchdog in main thread (which has highest priority)

../../../../../../_images/datalogger-watchdog.svg
#include <why.h>
#include <why-sensor.h>
#include <why-irq.h>
#include <why-messagequeue.h>
#include <why-time.h>
#include <why-watchdog.h>
#include <why-thread.h>

#include <string>


struct sensor_data
{
    Why::TimeSpec timestamp;
    std::string_view sensorname;
    uint64_t value;
};

int main()
{
    Why::init();

    Why::RandomSensor s1(0, 100);
    Why::RandomSensor s2(100, 200);

    auto sensor_data_queue = Why::MessageQueue<sensor_data>::create();
    assert(sensor_data_queue);

    // consume data in dedicated thread
    auto sensor_data_consumer = Why::Thread::create(
        [&sensor_data_queue](){
            while (true) {
                auto sample = sensor_data_queue->get();
                assert(sample);
                auto [timestamp, sensorname, value] = *sample;
                std::println("timestamp={}, sensorname={}, value={}", timestamp.to_seconds(), sensorname, value);
            }
        });
    assert(sensor_data_consumer);

    // activate sensor interrupts
    {
        auto isr = [&sensor_data_queue, &s1, &s2](int irqnum){
            sensor_data data;
            if (irqnum == 7)
                data = {.timestamp=Why::now_monotonic(), .sensorname="s1", .value=s1.get_value()};
            else if (irqnum == 42)
                data = {.timestamp=Why::now_monotonic(), .sensorname="s2", .value=s2.get_value()};
            else
                assert(!"unexpected irq");
        
            auto ok = sensor_data_queue->put_noblock(data);
            assert(ok);
        };
        Why::IRQ::connect(7, isr);
        Why::IRQ::connect(42, isr);
    }

    // activate and feed watchdog
    const Why::TimeSpec watchdog_timeout(2, 0);
    Why::Watchdog::activate(watchdog_timeout);
    while (true) {
        Why::Watchdog::feed();
        Why::sleep(watchdog_timeout - Why::TimeSpec(0, 500'000'000));
    }

    return 0;
}

Add CSV And MQTT#

  • Add datalogger functionality to consumer thread

  • Write CSV: Why::File

  • Publish on an MQTT topic: Why::MQTTPublisher

Note

Both File IO and network IO are notoriously blocking!

../../../../../../_images/datalogger-csv-mqtt.svg
#include <why.h>
#include <why-sensor.h>
#include <why-irq.h>
#include <why-messagequeue.h>
#include <why-time.h>
#include <why-watchdog.h>
#include <why-thread.h>
#include <why-file.h>
#include <why-mqtt.h>

#include <string>
#include <format>


struct sensor_data
{
    Why::TimeSpec timestamp;
    std::string_view sensorname;
    uint64_t value;
};

int main()
{
    Why::init();

    Why::RandomSensor s1(0, 100);
    Why::RandomSensor s2(100, 200);

    auto sensor_data_queue = Why::MessageQueue<sensor_data>::create();
    assert(sensor_data_queue);

    auto csv_file = Why::File::open("data.csv", Why::File::WriteOnly|Why::File::Append|Why::File::Create);
    assert(csv_file);
    auto mqtt = Why::MQTTPublisher::create("why-topic", "127.0.0.1");
    assert(mqtt);

    // consume data in dedicated thread
    auto sensor_data_consumer = Why::Thread::create(
        [&sensor_data_queue, &csv_file, &mqtt](){
            while (true) {
                auto sample = sensor_data_queue->get();
                assert(sample);
                auto [timestamp, sensorname, value] = *sample;

                // write to CSV
                std::string line = std::format("{};{};{}\n", timestamp.to_seconds(), sensorname, value);
                auto written = csv_file->write((const uint8_t*)line.c_str(), line.size());
                assert(written);

                // publish MQTT
                std::string json = std::format("{{\"timestamp\": {}, \"sensorname\": {}, \"value\": {} }}", 
                                               timestamp.to_seconds(), sensorname, value);
                auto ok = mqtt->publish(json);
                assert(ok);
            }
        });
    assert(sensor_data_consumer);

    // activate sensor interrupts
    {
        auto isr = [&sensor_data_queue, &s1, &s2](int irqnum){
            sensor_data data;
            if (irqnum == 7)
                data = {.timestamp=Why::now_monotonic(), .sensorname="s1", .value=s1.get_value()};
            else if (irqnum == 42)
                data = {.timestamp=Why::now_monotonic(), .sensorname="s2", .value=s2.get_value()};
            else
                assert(!"unexpected irq");
        
            auto ok = sensor_data_queue->put_noblock(data);
            assert(ok);
        };
        Why::IRQ::connect(7, isr);
        Why::IRQ::connect(42, isr);
    }

    // activate and feed watchdog
    const Why::TimeSpec watchdog_timeout(2, 0);
    Why::Watchdog::activate(watchdog_timeout);
    while (true) {
        Why::Watchdog::feed();
        Why::sleep(watchdog_timeout - Why::TimeSpec(0, 500'000'000));
    }

    return 0;
}

Add Snapshot Button (Message Queue)#

../../../../../../_images/datalogger-snapshot-button.svg
  • Interrupt from edge triggered GPIO (with hardware debouncing, say 😇): “create CSV snapshot”

  • Dedicated handler thread

    • Use a mutex to guard CSV file rotation against concurrent CSV writes

    • Closes and renames file

    • Creates a new file to use from now on

Note

As button presses are enqueued

#include <why.h>
#include <why-sensor.h>
#include <why-irq.h>
#include <why-messagequeue.h>
#include <why-time.h>
#include <why-watchdog.h>
#include <why-thread.h>
#include <why-file.h>
#include <why-mqtt.h>
#include <why-mutex.h>

#include <string>
#include <format>


struct sensor_data
{
    Why::TimeSpec timestamp;
    std::string_view sensorname;
    uint64_t value;
};

int main()
{
    Why::init();

    Why::RandomSensor s1(0, 100);
    Why::RandomSensor s2(100, 200);

    auto sensor_data_queue = Why::MessageQueue<sensor_data>::create();
    assert(sensor_data_queue);
    auto snapshot_queue = Why::MessageQueue<Why::TimeSpec/*timestamp*/>::create();
    assert(snapshot_queue);

    auto csv_file = Why::File::open("data.csv", Why::File::WriteOnly|Why::File::Append|Why::File::Create);
    assert(csv_file);
    Why::Mutex csv_snapshot_lock;
    auto mqtt = Why::MQTTPublisher::create("why-topic", "127.0.0.1");
    assert(mqtt);

    // consume data in dedicated thread
    auto sensor_data_consumer = Why::Thread::create(
        [&sensor_data_queue, &csv_file, &csv_snapshot_lock, &mqtt](){
            while (true) {
                auto sample = sensor_data_queue->get();
                assert(sample);
                auto [timestamp, sensorname, value] = *sample;

                // write to CSV (protecting against concurrent
                // snapshot creation)
                csv_snapshot_lock.lock();
                std::string line = std::format("{};{};{}\n", timestamp.to_seconds(), sensorname, value);
                auto written = csv_file->write((const uint8_t*)line.c_str(), line.size());
                assert(written);
                csv_snapshot_lock.unlock();

                // publish MQTT
                std::string json = std::format("{{\"timestamp\": {}, \"sensorname\": {}, \"value\": {} }}", 
                                               timestamp.to_seconds(), sensorname, value);
                auto ok = mqtt->publish(json);
                assert(ok);
            }
        });
    assert(sensor_data_consumer);

    // snapshot button functionality
    auto snapshot_handler = Why::Thread::create(
        [&snapshot_queue, &csv_file, &csv_snapshot_lock](){
            while (true) {
                auto timestamp = snapshot_queue->get();
                assert(timestamp);
                std::string snapshot_filename = std::format("snapshot-{}.csv", timestamp->to_seconds());

                csv_snapshot_lock.lock();
                Why::File::rename("data.csv", snapshot_filename);
                csv_file = Why::File::open("data.csv", Why::File::WriteOnly|Why::File::Append|Why::File::Create);
                csv_snapshot_lock.unlock();
            }
        });
    assert(snapshot_handler);
    Why::IRQ::connect(666, [&snapshot_queue](int /*irqnum*/){
        auto ok = snapshot_queue->put_noblock(Why::now_monotonic());
        assert(ok);
    });

    // activate sensor interrupts
    {
        auto isr = [&sensor_data_queue, &s1, &s2](int irqnum){
            sensor_data data;
            if (irqnum == 7)
                data = {.timestamp=Why::now_monotonic(), .sensorname="s1", .value=s1.get_value()};
            else if (irqnum == 42)
                data = {.timestamp=Why::now_monotonic(), .sensorname="s2", .value=s2.get_value()};
            else
                assert(!"unexpected irq");
        
            auto ok = sensor_data_queue->put_noblock(data);
            assert(ok);
        };
        Why::IRQ::connect(7, isr);
        Why::IRQ::connect(42, isr);
    }

    // activate and feed watchdog
    const Why::TimeSpec watchdog_timeout(2, 0);
    Why::Watchdog::activate(watchdog_timeout);
    while (true) {
        Why::Watchdog::feed();
        Why::sleep(watchdog_timeout - Why::TimeSpec(0, 500'000'000));
    }

    return 0;
}