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 asysfs
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 likedevices/*-<address in hex>
. (The*
matches28
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 ...