Exercise: OneWire Sensor Factory

Problem

  • On Linux, Onewire device addresses are mapped to aptly named directories - e.g. /sys/bus/w1/devices/28-02131d959eaa

  • W1Sensor reads from a sysfs file, e.g. /sys/bus/w1/devices/28-02131d959eaa/temperature, just like

    $ cat /sys/bus/w1/devices/28-02131d959eaa/temperature
    23062
    

    or, in C++,

    W1Sensor sensor("/sys/bus/w1/devices/28-02131d959eaa/temperature");
    std::cout << sensor.get_temperature() << std::endl;
    
  • A hypothetical system (a heating control, for example) configuration does not want to mention file system paths like that

    • Highly OS dependent

    • Humans who configure systems might not be Linux experts

  • Device addresses are well understood

  • ⟶ need something that …

    • Searches the Onewire tree (/sys/bus/w1) for a device directory that looks something like devices/*-<address in hex>. (The * matches 28 in our example - the vendor ID is not relevant because the address part is globally unique in the Onewire universe).

    • Having found that directory, we know that it must contain a file named temperature

    • Creates a W1Sensor, passing the temperature file /sys/bus/w1/devices/28-02131d959eaa/temperature to its constructor.

  • That something (call it W1SensorFactory) is then used to create sensors from configuration data, like so …

    uint64_t address = ...;                  // <--- address, taken from a config of any kind
    W1SensorFactory factory("/sys/bus/w1");  // <--- onewire tree, rooted at /sys/bus/w1
    W1Sensor* sensor = factory.find_by_address(address);
    std::cout << sensor->get_temperature() << std::endl;
    

Implementation

Lets create a class W1SensorFactory (a factory is something that creates something) that fulfills the requirements listed further below.

Fixture

As a test-implementation detail, the fixture class sensor_w1_factory_suite

  • Creates a temporary directory dirname for the duration of the test run. That directory is taken as a simulated /sys/bus/w1 Onewire sysfs directory.

  • That directory dirname is arranged to contain a device, <dirname>/devices/28-02131d959eaa.

  • <dirname>/devices/28-02131d959eaa/temperature is the device’s temperature file.

  • The method void change_temperature(double temperature) is used to modify the temperature from within test code.

#pragma once

#include "fixture-tmpdir.h"
#include <filesystem>
#include <fstream>


struct sensor_w1_factory_suite : public tmpdir_fixture
{
    sensor_w1_factory_suite()
    {
        std::filesystem::path device_dir = dirname / "devices" / "28-02131d959eaa";
        std::filesystem::create_directories(device_dir);
        std::ofstream(device_dir / "temperature") << "42459" << std::flush;
    }

    void change_temperature(double temperature)
    {
        unsigned temp_milli = temperature * 1000;
        std::ofstream(dirname / "devices" / "28-02131d959eaa" / "temperature") << temp_milli << std::flush;
    }
};

Note

Download that file, place it into your project’s tests/ directory, and update that directory’s CMakeLists.txt file accordingly.

Unit Tests

One by one, download the following files into tests/ (and to the obvious CMakeLists.txt dance). when one test passes, procees to the next.

basic

The sunny case: given an existing Onewire address, the factory returns a W1Sensor object.

#include "sensor-w1-factory-fixture.h"
#include <sensor-w1.h>
#include <sensor-w1-factory.h>
#include <gtest/gtest.h>

TEST_F(sensor_w1_factory_suite, basic)
{
    std::filesystem::path w1_fs_root = dirname;         // <--- using dirname from fixture, simulating /sys/bus/w1
    W1SensorFactory sensor_factory(w1_fs_root);

    W1Sensor* sensor = sensor_factory.find_by_address(0x02131d959eaa);
    ASSERT_NE(sensor, nullptr);

    change_temperature(36.5);                           // <--- write "36500" into device's temperature file
    ASSERT_FLOAT_EQ(sensor->get_temperature(), 36.5);   // <--- W1Sensor picks up new value

    change_temperature(42.324);                         // <--- temperature changes again
    ASSERT_FLOAT_EQ(sensor->get_temperature(), 42.324); // <--- sensor return new value

    delete sensor;                                      // <--- smart pointer not yet on the horizon, sigh
}

notfound

One possible error: address not found. Lacking any knowledge of C++ smart pointers, a raw pointer with the value nullptr is returned.

#include "sensor-w1-factory-fixture.h"
#include <sensor-w1.h>
#include <sensor-w1-factory.h>
#include <gtest/gtest.h>

TEST_F(sensor_w1_factory_suite, notfound)
{
    std::filesystem::path w1_fs_root = dirname;         // <--- using dirname from fixture, simulating /sys/bus/w1
    W1SensorFactory sensor_factory(w1_fs_root);

    W1Sensor* sensor = 
        sensor_factory.find_by_address(0x012345678901); // <--- that device does not exist under /sys/bus/w1 (err, dirname)
    ASSERT_EQ(sensor, nullptr);                         // <--- returns NULL in case address is not found

    delete sensor;                                      // <--- smart pointer not yet on the horizon, sigh
}

find_is_const

Exercising our C++ expertise, we know that something that creates something rarely modifies itself - hence the W1SensorFactory::find_by_address() could just as well be const.

#include "sensor-w1-factory-fixture.h"
#include <sensor-w1.h>
#include <sensor-w1-factory.h>
#include <gtest/gtest.h>

TEST_F(sensor_w1_factory_suite, find_is_const)
{
    std::filesystem::path w1_fs_root = dirname;
    const W1SensorFactory sensor_factory(w1_fs_root);   // <--- *const*
    sensor_factory.find_by_address(0x02131d959eaa);     // <--- if that compiles, then the test passes
}

Testing In Isolation

$ pwd
/home/jfasch/tmp/FH-ECE20-final-x86_64    # <--- my PC build directory (yours might be different)

ALl green …

$ ./tests/FH-ECE20-final--suite
Running main() from /home/jfasch/work/FH-ECE20-final/googletest/googletest/src/gtest_main.cc
[==========] Running 5 tests from 3 test suites.
[----------] Global test environment set-up.
[----------] 2 tests from sensor_const_suite
[ RUN      ] sensor_const_suite.basic
[       OK ] sensor_const_suite.basic (0 ms)
[ RUN      ] sensor_const_suite.is_a_sensor
[       OK ] sensor_const_suite.is_a_sensor (0 ms)
[----------] 2 tests from sensor_const_suite (0 ms total)

[----------] 2 tests from sensor_random_suite
[ RUN      ] sensor_random_suite.basic
[       OK ] sensor_random_suite.basic (0 ms)
[ RUN      ] sensor_random_suite.is_a_sensor
[       OK ] sensor_random_suite.is_a_sensor (0 ms)
[----------] 2 tests from sensor_random_suite (0 ms total)

[----------] 1 test from w1_sensor_suite
[ RUN      ] w1_sensor_suite.test_read_sensor
[       OK ] w1_sensor_suite.test_read_sensor (0 ms)
[----------] 1 test from w1_sensor_suite (0 ms total)

[----------] Global test environment tear-down
[==========] 5 tests from 3 test suites ran. (0 ms total)
[  PASSED  ] 5 tests.

Testing In Reality

On The PC

Once the project compiles on and for the development machine, you are able to test it; no need for target hardware.

  • Create a simulated sysfs tree at /tmp/w1-root (for example), together with a fully functional sensor device

    $ mkdir -p /tmp/w1-root/devices/32-deadbeef
    $ echo 36700 >> /tmp/w1-root/devices/32-deadbeef
    
  • Run our sophisticated application on it (taking the role of a the system configurator mentioned above)

    $ pwd
    /home/jfasch/tmp/FH-ECE20-final-x86_64    # <--- my PC build directory (yours might be different)
    
    $ ./bin/onewire-temperature-factory
    ./bin/onewire-temperature-factory <basedir> <address-in-hex> [interval]
        basedir                 ... e.g. /sys/bus/w1
        address-in-hex          ... 0xdeadbeef
        interval                ... in seconds
        n-iterations (optional) ... number of measurements
                                    before termination
    
    $ ./bin/onewire-temperature-factory /tmp/w1-root 0xdeadbeef 2 4
    36.7
    36.7
    36.7
    36.7
    

On The Target

Having cross compiled the project (see here), we test it against a real sensor device.

$ pwd
/home/jfasch/tmp/FH-ECE20-final-pi    # <--- my Pi build directory (yours might be different)
$ scp -P 2020 ./bin/onewire-temperature-factory joerg.faschingbauer@jfasch.bounceme.net:
$ ssh -p 2020 joerg.faschingbauer@jfasch.bounceme.net ./onewire-temperature-factory /sys/bus/w1 0x2131d959eaa 2 4
... real temperatures ...